8 ms·
I really like in Rust how you can reinit a variable with a different type e.g. “let rect: Rect<f32> = rect.into();” It’s just so damn useful and I’m not sure w
by jackosdev 3y ago
I really like in Rust how you can reinit a variable with a different type e.g. “let rect: Rect<f32> = rect.into();”
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.
- pcwalton 3y agoI fought to keep this feature around in Rust. I was inspired by OCaml (which the old Rust compiler was written in), where you could write: 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.
- sophiabits 3y agoExactly! I was thinking of shadowing in Rust when I wrote my original comment. 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.
- cillian64 3y agoThe downside is when reading code you’re keeping in your head information about the type of each variable. If you skim through the code and miss one of these redefinitions then you may be mistaken about the variable’s type. That said, I still think sparing use of this is justified, especially with an editor which can show types on mouseover.
- iudqnolq 3y agoYou've got to keep info either way. I'm more worried about forgetting let data = get(); let uniqueData = Array.from(new Set(data)); // ... (snipped many lines) process(data); // should have been uniqueData
- jackosdev 3y agoThat’s true, but this has never been a problem for me looking through large codebases and doing code reviews, in other languages I was constantly annoyed by not being being able to shadow
- masklinn 3y agoDefinitely super useful, especially in a language where such conversions are rather common. Also 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.
- eptcyka 3y agoYou could just create new bindings for each new `it`, `let it = ...; let it = it.too();`
- masklinn 3y agoThat’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).
- Joker_vD 3y agoYes, the fact that "V = expression" means "if variable V doesn't exist, assign expression's value to it; otherwise compare the expression's value with the value of V and raise exception if they're not equal" is one of my least favourite parts of Erlang. 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.
- masklinn 3y ago
- signaru 3y agoAt the opposite end of the spectrum, there are languages with case insensitivity and even style insensitivity. I personally avoid them, but it's interesting how the users of these languages have a very different philosophy.
- FpUser 3y agoI use C++ and Delphi / Lazarus. I guess I have a "very different philosophy" then I ;) To me either has pros and cons.
- tialaramex 3y agoThis is idiomatic Rust, it works very nicely there, however most languages aren't Rust 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.
- mr_00ff00 3y agoThis is interesting to me that C++ allows you to access a value after move is called. Presumably it wouldn’t be hard for the compiler to yell at you. I assume accessing it is undefined behavior? I would assume you could change this without affecting backwards compatibility.
- mindv0rtex 3y agoThe only requirement placed on the “moved out” variable is that you should be able to call its destructor. Which means that it has to be in a valid but unspecified state. So it's fine to access such a variable, so long as you don't read its exact state. You can still assign to it, for instance.
- tialaramex 3y agoAs another commenter says the moved-from object should have "Valid but unspecified state" (types provided by the standard library will do that, custom types merely should do that) 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
- ithkuil 3y agothere are things that make perfect sense when a language forces you to use an IDE anyway if you want to do anything longer than a toy. 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.
- layer8 3y agoWith shadowing, you can use or mutate a variable, thinking you are using/mutating the outer instance because you’re unaware of the inner (shadowing) instance, which is the one you are really using/mutating. An IDE doesn’t help catching such an inadvertent error (unless it warns about shadowing variables, but then you’d want to rename it anyway, to get rid of the warning). 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.
- ithkuil 3y agoit's a footgun indeed and no IDE per se doesn't solve all the problems. But since rust was mentioned, there are other rust features that make that less of a problem: most of the rust code uses immutable variables and only rarely you do use mut variables and mut references and these can be under bigger scrutiny by reviews and linters. 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.
- andrepd 3y agoWell why not with the same type? Sounds like an arbitrary restriction: you can use this idiom, but only sometimes.
- alpaca128 3y agoWhy would you do that with the same type instead of just making the variable mutable? And you can do it, I just don't think it's a good idea as you now effectively have a mutable variable without it being marked as such.
- eru 3y agoNo, it's still better than a mutable variable. Because it's not a mutable variable, just a series of variables that happen to have the same name. 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. 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.)
- alpaca128 3y ago> it's not a mutable variable, just a series of variables that happen to have the same name. 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.
- eru 3y ago> Both result in multiple states of the same identifier, so I don't quite see the big difference here. 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.
- alpaca128 3y ago> I’m not sure what the downside is 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.
- stouset 3y ago> you accidentally overwrote a function parameter To "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 = …)
- masklinn 3y ago> 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. 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).
- alpaca128 3y ago> If you “overwrite” a function parameter because you’re converting it, it’s a major use case of the feature. 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.