Several questions about Rust quiz

Hello,

I recently came across this Rust quiz and I have several questions. Sorry for the question-bombing in advance :smiley:

Question #2

I don't understand how h performs a bitwise-AND with an empty block. Shouldn't h result in a tuple with a reference to S(3)? (i.e. let h = (&S(3));).

Question #6

I do understand why this is possible, but I found it surprising that Rust doesn't warn about it. Maybe someone could give me an example of where such code might be useful? I can't think of any and it only seems like a footgun to me. This code compiles without warnings and I think any C/C++ developer would expect a and b to be both 3.

Question #13

I just didn't understand this one, I need an ELI5 about this. What is the memory address of a zero-sized type? I checked this on a playground and it does indeed look like there is an address for a zero-sized type.

Question #16

I think this is the first time that I've seen some Rust code and genuinely just thought to myself: "Wtf?". I have known that Rust does not support pre-/post-decrement operators since the first day I read the Rust book, and even knowing about it, it didn't even cross my mind that they weren't supported when I looked at the code in the quiz. I was also very surprised to see that this code compiles without warnings.

Question #19

I thought I understood the quiz code, but I got confused when I read the answer. If I run this code with let x = s instead (playground), it behaves the same. However, there is another example in question #28 that behaves as described in this question (#19).

Question #23

How come the S.f() syntax is valid? I would expect this to require S::f(). Is it because it's a zero-sized type?

Question #25

I got confused here because I learned in this quiz that in Rust there are two namespaces, a value namespace and a type namespace.

I would expect that when you write let S = f(); the compiler binding to a variable S rather than it doing pattern matching on a type S. On the other hand I would expect let _ : S = f(); to do the pattern matching.

What I also found interesting is that the same code but with let S: S = f(); also prints 212, but I'd expect that to print 12 instead, because the value returned by f() should now be indeed bound to the variable S, then 1 would be printed and then the variable S would be dropped. This, however, is not the case (playground).

Question #29

I'm confused about the 1-tuple concept and the trailing comma. Why is (0) not a valid tuple? Where can I read more about this?

Question #31

I don't understand why wwt.f() and wwwwwt.f() work.

Talking about wwt.f():

wwt.f(): The search order is &&T -> &&&T -> &mut &&T -> &T, and we're done. Upon invocation, the function prints 1.

Where does the last -> &T come from? Shouldn't the search order just be &&T -> &&&T -> &mut &&T?

Since Rust is an expression-oriented language, blocks have to evaluate to a value - blocks without a final expression (empty blocks or semicolon-terminated blocks) evaluate to ().

This is just a consequence of supporting unary negation; --x is -(-x).

1 Like

2: (x) is not a tuple, just value in parenthesis. You must be able to write (2 + 2) * 2 without multiplying a tuple. 1-element tuple is (x,)

6: It's merely a consequence of everything being an "expression", and statements returning (). There should probably be a warning for this.

13: In C every object has to have a real address. In Rust they don't. If a type takes 0 bytes, the compiler is free to use a "fake" address for it.

16: Clippy says: "warning: --x could be misinterpreted as pre-decrement by C programmers, is usually a no-op"

19: It's a weird one indeed, because let _ doesn't assign to _. It's a pattern that doesn't do anything. Since the value is not captured by the pattern, it's not moved, so s continues to live.

let s = Some(String::new());
match s { // looks like a move
    Some(ref s) => {}, // but none of the patterns moved it
    _ => {}, 
};
println!("{} still valid!", s);
1 Like

The key part in the explanation is

But if this line does move s , without binding it

let x = s moves s... but also binds it.

Question #28 is different because the expression on the right is a unit constructor, and not a bound variable. It is confusing. I believe at least some of these special _ rules are maintained for backwards compatibility reasons (but don't have a citation for that off hand).

How come i doesn’t evaluate the braces as an expression that returns ()?

It's probably the same syntax magic that lets you write:

if true {
}
x();

instead of:

if true {
}; // semicolon
x();
1 Like

There probably are Clippy lints for a lot of things in the questions I posted, to be honest a lot of those are examples of code that likely no one would write.

This one, however, feels to me like it should be a rustc warning, but then again, I’m no expert, just a regular dude.

I understand the x = s one. I don’t understand why in that quiz code let _ = s; doesn’t drop immediately as in #28 and prints 12 rather than 21

Questions 23 and 25:

In Rust there are four namespaces. The other two are lifetimes and macros. This is not well documented.

So, as you may or may not realize, tuple structs occupy a slot in both the type and value namespaces. If you have struct Foo(isize), Foo is both a type (clearly), but a tuple constructor is also in the value namespace, as a function (or pseudo function), so that you can do let foo = Foo(234);.

Similarly, unit structs occupy a slot in both the type and value namespaces, so that you can do

struct Unit;
let unit = Unit;

It's more like a value or pseudo-value than a function in this case, because unit structs only have one possible value. This is why you can do S.method(). If the tuple struct had an f method, you could do Foo(13).f() as well.

1 Like

This one makes complete sense. I think I got a bit of tunnel vision during the quiz

So if I have a Unit struct in scope, I can’t have a variable with the name of the Unit struct because the name would already be taken in the value namespace?

May I ask why you think there should be a warning for this (I agree), but don’t think there should be a warning for --x? There is probably also a Clippy lint for let a = b = 0; (haven’t checked)

In #28 the result of the unit constructor is a temporary, as if it were a function call. I found the issue, and in particular see this comment for summary of behaviour that was considered. Rust went with option 2, don't bind (or move) it, so the lifetime is governed by the rules for temporaries.

The RHS in #28 is a temporary, but the s in let _ = s is not.

1 Like

Correct.

How come in that example Rust tries to bind and fails, but the let S = f(); in #25 is considered a Unit struct pattern instead?

The order is

  T   &T   &mut T
 *T  &*T  &mut *T
**T &**T &mut **T

Etc. The reference explanation is phrased a bit odd, I agree. Each line above is the "for each candidate T , add &T and &mut T to the list immediately after T" part.

1 Like

I see, I understand this one now :slight_smile:

Does Rust continue to look for an implementation dereferencing as many times as there are reference levels?

For example, if I had a &&&&&&&&&&T, would Rust continue to look for an implementation with &**********T, etc, before giving up and deciding no impl was found?

It's a unit struct pattern in both. It fails in the example because there's an integer on the right. It succeeds in #25 because there's a matching unit struct on the right, so the pattern matches. (let patterns must be irrefuable -- always match.)

Here's some more examples which may make it more clear.

Yes. (Demonstrating a success because it doesn't tell you it checked everything on fail, but clearly it looked and found the method.)

1 Like