I see this desire to get rid of shadowing all the time, but in practice it's such a disruptive restriction.
I see this desire to get rid of shadowing all the time, but in practice it's such a disruptive restriction.
> You'll have to be more creative when coming up with variables that don't shadow existing ones (which, for me, generally means using more obscure names).
The last adjective I want used in relation to my variable names is “obscure.”
It’s just so damn useful and I’m not sure what the downside is, it sucks when you have to keep coming up with different names so you can keep around an identifier that you don’t need anymore.
My day job is predominantly in Typescript and a lot of code winds up reading significantly worse than it needs to. A common pattern for me is unique-ifying some sort of array—“const dataUnique = new Set(data);” is horrible, and if there’s no reason to keep the original “data” variable in scope then it’s doubly bad; I want to keep as little context in my head as possible.
That said, I still think sparing use of this is justified, especially with an editor which can show types on mouseover.
let data = get();
let uniqueData = Array.from(new Set(data));
// ... (snipped many lines)
process(data); // should have been uniqueDataAlso useful because you can’t have abstracted local types, so let’s say you’re building an iterator in a language with interfaces you could do something like
let it: Iterator = some().thing();
// intermediate stuff
it = it.some().transform();
// more intermediate stuff
it = it.final().transform();
But in Rust that won’t work, every adapter yields a different concrete type, you’d have to box every layer to make them compatible. Intra-scope shadowing solves that issue.The biggest downside is that it’s possible to reuse names for completely unrelated purposes, which can make code much harder to understand. Clippy has a shadow_unrelated lint but it’s allowed by default because it’s a bit limited.
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).
I semi-regularly introduce local variables named exactly like one of the function's parameter and then spend several minutes trying to understand why the line expression on the right-hand side of assignment throws badmatch: of course, it doesn't, it's the assignment itself that throws it.
To me either has pros and cons.
Rust's Into::into() is consuming the object in the old (now shadowed) rect variable. So conveniently the old rect variable which we can't access also no longer has a value†. In many languages a method can't consume the object like that, so the old object still exists but we can't access it because it is shadowed.
For example in C++ they have move semantics, but their move isn't destructive, so the object is typically hollowed out, but still exists until the end of the scope at least.
Rust's type strictness matters here too. It means if you later modify some code using rect meaning whatever it was before that statement morphing it into a Rect<f32> chances are it doesn't type check and is rejected. For example in many languages if (rect) { ... } would be legal code and might change meaning as a result of the transformation, but in Rust only booleans are true or false.
† Unless this previous variable's type implemented the Copy trait and therefore it has Copy semantics and consuming it doesn't do anything.
I assume accessing it is undefined behavior?
I would assume you could change this without affecting backwards compatibility.
Since you don't know what valid state it has, calls with pre-requisites are nonsense (e.g. if you have a Bird and the method land requires that the Bird should be flying, you can't call land() on a moved from Bird, because you don't know if it's flying) but all calls without pre-requisites are fine e.g. asking how long a string you moved is would work - it's probably zero length now, but maybe not.
> Presumably it wouldn’t be hard for the compiler to yell at you.
In the general case this is Undecidable, so, the opposite of not hard.
> I would assume you could change this without affecting backwards compatibility.
C++ which relies on this exists today, the most likely path to actually landing destructive move in C++ would be to add a whole new set of construction and assignment operators for destructive move, forcing people to opt in and adding to the many sets C++ already has, and likely angering C++ developers a great deal in the process.
Howard Hinnant, whose design today's non-destructive move is, did argue that in principle it's possible to add destructive move to the language later if desired, but his description rather undersells the benefits of this design, presumably because he couldn't deliver it. Maybe he'd watched enough Mad Men (yup, Mad Men's early seasons pre-date C++ having move semantics) to know you shouldn't tell the customer what they can't have or they'll want it.
Common things to actually do with a C++ variable after moving from it are:
* Nothing, but in the knowledge it won't be cleaned up until the scope ends * Re-assign it, destroying the hollowed out object immediately * Re-use the hollowed out object, e.g. call a clear() method on it and then use as normal
let name = "Arthur".to_owned();
//... Do something with &Arthur
let name = "Bethan".to_owned();
//... Do something with &Bethan
In this situation, ownership of the first string was never passed on, and the value will be dropped (deallocated, destructed, etc) at the end of the scope, meaning that also in Rust, the first string value is shadowed and becomes inaccessible, much like in your description of C++. In addition, because in this case both variables have the same type, you can use second variable thinking that you're using the first one, and the compiler will not help you, you'll just end up using the wrong name somewhere.Fwiw, I find this feature very useful, and it's helpful more often than it is a nuisance. But there are no guarantees that you're consuming or transforming the object you're shadowing, and the compiler won't necessarily help you out if you simply accidentally use the same name twice.
Shadowing is not a big deal with IDEs; you can always see the type of the variable , jump to definition easily etc etc.
The rule to not shadow variables makes more sense when you want to understand the code just by looking at it.
I’ve tripped over unexpected shadowing often enough that I wish more languages would forbid it. I rarely have trouble choosing appropriate variable names to avoid shadowing.
I focused on IDEs in my comment because I find shadowing to be a problem even with immutable variables, because it's hard for you to tell what is the type of a variable if it keeps change throughout the function body.
let x: u32 = 5;
let x: u32 = 10; // You can write this, but why?
let x: u32 = 20; // I really feel like you should re-consider
If you end up shadowing this way in a long function it more likely means the function got too long. On the other hand, I certainly have had cause to shadow variables in inner scopes e.g. let x = some_complicate_stuff();
for dx in [-1, 0, 1] {
let x = x + dx;
// Do stuff with x very naturally here, rather than keep saying "x + dx" everywhere
}
// But outside the loop x is just x, it's not x + dxMutable state is 'evil' and makes your program harder to reason about on a semantic level. Shadowing is merely a syntactic choice with pros and cons.
I like shadowing in Rust, it works well there. In eg Python or Haskell, it works less well, but for different reasons. (In Haskell it's because of laziness and definitions being co-recursive by default. In Python it's because the language doesn't give you any tools to tell apart assignment to an existing variable from creation of a new variable.)
Fair point, though in that case I'd be more comfortable separating those variables into scopes.
> Mutable state is 'evil' and makes your program harder to reason about on a semantic level. Shadowing is merely a syntactic choice with pros and cons.
Both result in multiple states of the same identifier, so I don't quite see the big difference here. In Rust I already have the clearly visible "mut" keyword telling me that it'll be overwritten.
Shadowing is something you can figure out purely on the syntactic level.
Figuring out mutable state requires solving the halting problem.
> Fair point, though in that case I'd be more comfortable separating those variables into scopes.
Well, they effectively have different scopes. The scope is just not delimited with curly braces.
It's similar to how eg Haskell's variable binding in do-notation extent to the rest of the do-block. Each line introduces a new scope.
However, I can re-interpret your comment as saying that you want a more explicit syntactic marker for a new scope. And that's a fair enough request.
The downside is that you may get a weird bug and only after a while see that you accidentally overwrote a function parameter and the Rust compiler didn't even warn you about it.
For this reason I always add the following line to my projects to enable warnings:
#![warn(clippy::shadow_reuse, clippy::shadow_same, clippy::shadow_unrelated)]
You can also use "deny" instead of "warn" to make it an error.
I also like "#![deny(unreachable_patterns)]", which detects bugs in enum pattern matching if you accidentally match "Foo" instead of "Type::Foo" - I honestly don't know why this isn't set by default.If you “overwrite” a function parameter without using it, the compiler will warn you of an unused variable.
If you “overwrite” a function parameter because you’re converting it, it’s a major use case of the feature.
> I honestly don't know why this isn't set by default.
Because the author of the match can’t necessarily have that info e.g. if you match on `Result<A, B>` but `B` is an uninhabited type (e.g. Infallible), should the code fail to compile? That would make 95% of the Result API not work in those cases. Any enum manipulating generic types could face that issue.
IIRC it was originally a hard error, and was downgraded because there were several edge cases where compilation failed either on valid code, or on code which was not fixable (for reasons like the above).
Or it's unintended and thus a bug. I personally almost never intentionally shadow variables so I turned it into warnings.
> e.g. if you match on `Result<A, B>` but `B` is an uninhabited type (e.g. Infallible), should the code fail to compile?
This specific example you chose is probably the least relevant here, as the Result type doesn't require you to write "Result::Err(_)" instead of just "Err(_)", both will correctly match. Which can of course also be done for custom enums by "importing" their variants ("use EnumName::*;"). But in my experience it's easy to accidentally omit the type in the match pattern and then suddenly it matches everything. I personally can't imagine a situation where this is intentional and have spent way too much time debugging this specific issue, hence I choose to turn it into an error.
It will absolutely warn about this:
fn foo(i: u32) -> u32 {
let i = 42;
i
}
fn main() {
dbg!(foo(42));
}
results in warning: unused variable: `i`
--> src/main.rs:1:8
|
1 | fn foo(i: u32) -> u32 {
| ^ help: if this is intentional, prefix it with an underscore: `_i`
|
= note: `#[warn(unused_variables)]` on by defaultTo "accidentally" overwrite it, you have to either:
a) explicitly mark the parameter binding as mutable: fn foo(mut bar: T)
b) explicitly re-bind the variable with let (let bar: T = …) let x = foo() in
let x = bar x in
let x = baz x in
print x
In a functional language where mutation is less convenient than in C++, this is really handy, and I wanted Rust to support the same idiom.Well, second-to-last right after "misleading".
Specifically this kind of code (and I do not feel calling them e.g. iter1 and iter2 or foo_iter and bar_iter make the code nice):
var iter1 = foo_dir.iterate();
while (try iter1.next()) |entry| {
// Do something
}
var iter2 = bar_dir.iterate();
while (try iter2.next()) |entry| {
// Do something else
} {
var iter = foo_dir.iterate();
while (try iter.next()) |entry| {
// Do something
}
}
{
var iter = bar_dir.iterate();
while (try iter.next()) |entry| {
// Do something
}
}But also:
var iter = foo_dir.iterate();
while (try iter.next()) |entry| {
// Do something
}
iter = bar_dir.iterate();
while (try iter.next()) |entry| {
// Do something else
}
You can totally just reassign `iter` here since you're done with the first iterator (assuming they are both the same type, which appears to be the case).1) have bad coding habits. (playing with zig broke me of some of these)
or
2) you need to autogenerate code.
2 is a much harder nut to crack.
what to name things
cache coherency
off by one bugs