And there are cases where shadowing is not acceptable e.g. when updating bindings within a loop, you usually want to see the updates from outside the loop.
Nitpicking: assignment is an expression that returns () in Rust, meaning `foo(a = 1, a)` is valid if foo has a signature like `fn foo(x: (), y: i32)`. However, arguments are (mostly) defined to be evaluated from left to right.
There is literally a Rust example in your wiki link saying that this is shadowing. You can even redefine the variable to have a different type, how can this be reassignment.
> Note that this is distinct from "auto-deref" (or deref coercion), another Rust feature that will automatically dereference pointers as necessary in certain cases, like calling a method on a struct (so there is no arrow operator "->" in Rust like in C++).
Is this actually distinct? The docs for the reference type explicitly says that &T implements Deref for T, so it seems like it's the exact same thing to me.
On the other hand you can assign within expressions in Rust, and it's generally not been an issue that I've seen anyone complain about. However, that might be because the type checker usually catches you if you're making a mistake; assignment/variable binding return the type `()`, which doesn't work inside ifs or whiles
- you have some AST object and want to lower it in the compiler into an intermediate representation
- you want to add some interesting parts of the AST to your state while lowering and refer to them later in the lowering process
- This seems fine: the AST is first created, then you lower to the intermediate representation, then you can destroy the AST, so while you’re doing the lowering, the AST objects should all be alive and therefore ok to store references to in the lowerer-state
- However there is some mostly unnoticed code that is roughly desugaring some syntax by constructing temporary AST nodes
- So your addition may have your state including references to these temporary nodes
- And those temporary nodes are freed shortly after being created rather than after the lowering is all done
> T: ‘a is T outlives ‘a; but types are not generated at runtime.
Yes, that’s true, but you can make it a bit more specific by saying “all the references in T outlive ‘a”. Then it should make more sense, as references are generated at runtime.
This gets to the heart of what makes Rust an interesting language, as references are generated at runtime, but how long those references are valid for is checked at compile time using lifetime bounds.
I've not done any rust dev before but the whole variable shadowing thing really scares me, in that it makes it not obvious where the initial declaration comes from. This seems difficult to reason about.
Thank you for the correction. My confusion comes from being able to command-click on standard library types and methods, and being taken to their implementation, not just a declaration. I realize these aren't really concepts in Rust the same way they are in e.g. C/C++, but I figured if the source is present, the compiler is smart enough to not be over-compiling unused symbols.
It's always interesting to see the experiences of new people to Rust. There's a few curious misconceptions in here that I hadn't seen before:
> Iterators are a great and reusable way to encapsulate your blah-blah-blah-blah-blah, but of course they’re impossible to optimize.
I'm very curious to know where you got this idea from. Iterators actually optimize very well, in most cases being indistinguishable from a manual imperative for-loop. If you forget to turn on compiler optimizations then you'll see a significant performance difference, but that's true for a lot of different things you might want to do. Compiler optimizations are important whenever you're measuring performance.
> pragma
It's not a pragma. It uses the same # sigil that C compilers use to introduce pragmas, but in Rust, it actually denotes an attribute which modifies the next item (or in the enclosing item with the #! syntax). This is used for a number of things. As you've seen, it can be used to automatically derive implementations of traits, and it can be used to mark a function as being a candidate for inlining, among other things.
> Rust rather inelegantly overloads the assignment operator to mean either binding or copying.
This is indeed a very curious misconception. Assignment actually isn't overloaded like that at all. In your code example, when you say
let y = x;
You're not binding y to the value of x, you're actually moving the value of x into y. There is no implicit bind-by-reference in Rust. If you want a reference, you need to use the & sigil.
The confusion here stems from the fact that some values can be copied and some values cannot. When you move a value in Rust, if the value can be copied (if it conforms to the Copy trait), then the original value is still usable after the move. This means you can say
let x = 1;
let y = x;
let z = x;
The line `let y = x` moves the value of x into y, but since the value is copyable, it's really just moving a copy of the value. In this context, "copyable" basically means memcpy() can be used to produce a valid copy. There's a different trait called Clone which has an explicit .clone() method that is used for values that require additional work beyond memcpy() to copy.
In your code example, your struct is not Copy, so moving its value makes the original value inaccessible. Basically, it's considered garbage memory and cannot be read from again. This is why your code
let x = MyValue::Digit(10);
let y = x;
let z = x;
throws an error. It isn't because `y` and `z` would be referring to the same value, it's because after the line `let y = x;` the original value `x` is garbage.
So really, any time you do assignment like that (or any time you pass a value as an argument to a function) without taking an explicit reference (using &), you're doing a move. Values that confirm to Copy will move a copy of the value, and all other values will leave the original value inaccessible. This should actually be familiar to people coming from C++, where values that aren't Copy are basically like std::unique_ptr, except that instead of leaving the original value with a known-"zero" state, the compiler prevents you from accessing the original value at all.
let is the standard keyword for introducing a new, shadowing variable binding. It is a very old tradition and is immediately recognizable to functional programmers.
> If you want easy adoptation from one of the two largest languages in the world, change what matters and stick with the rest.
In the case of "let", it would be a false consistency with C/Java. Assignment in C/Java does not have the same semantics as a let binding in Rust, although a let binding in Rust has very similar semantics to a let binding in Scheme or Haskell.
> Compiler flag? Probably, I am not an expert on compiler flags that change language semantics; we don’t do that in Rust, but I know C and C++ compilers do.
> Just, for example, creating a closure that references a variable outside of its scope is not simple.
It's a lot simpler and more performant than in other languages. Rust will tell you if the closure might outlive the variable you're referencing, and it's easy to pick the best option for addressing the issue (move semantics, Rc<>/Arc<>, lifetime specifiers etc.) while keeping best-in-class performance. This is a big part of what Rust is best at, and it's hard to get these advantages any other way.
That’s the point, you can because rust supports intra-scope shadowing.
If it didn’t you’d have to type-erase, or create a new independently-named binding for every step, as you do in e.g. Erlang (can’t say this is / was my favourite feature of the langage).
That's a declaration not an assignment, TFA is (somewhat oddly) using the language precisely and fittingly: https://doc.rust-lang.org/reference/expressions.html#assignm...
And there are cases where shadowing is not acceptable e.g. when updating bindings within a loop, you usually want to see the updates from outside the loop.
reply