This heavily depends on what you’re doing, and is definitely something pretty subjective. As you probably read here, there are many people that say the exact opposite. these parsed and validated variables still contain the exact same meaning as the data from before, the semantic meaning did not change. Shadowing the previous variables has obvious benefits, because it allows you to clearly focus on how your data is flowing through your function the same way as you could if you just did a method-chain without intermediate variables (which, in my examples, is mostly a viable option, but there are many cases where you don’t want that or where you’d have to do things in between, making it not an option). It also forces you to actually use the variable you want, making it impossible to accidentally refer to an old, unvalidated / normalized / parsed variable. It does, imo, not help to have variable names like val userInputString
, val normalizedUserInput
, val parsedUserInput
, because they just make reading and typing the code harder for no real benefit. especially when these calls are directly next to each other, the data flow is more obvious when the names stay the same.
I do still get your point that there are also cases where it’s a bad idea, and I see that you personally dislike this, which is in some way understandable. But most comments and thoughts from rust-people suggest that having this exact way of giving the same name to a variable pre-parsing and post-parsing is a good and helpful thing.
I see. But why wouldn’t it be good practice? Do you have any arguments against it? If it’s necessary to have a mutable version of a function parameter, I’d say it’s a great idea to just shadow the original parameters name, as you don’t have any use for that parameter in its read-only form.
Also, the main point regarding mutability is not to change val
s to var
s, but the other way around. (I think shadowing function parameters is actually possible at the moment, altough warned against, which might be why you think it’s bad practice).
The idea is to constrain mutability as much as possible by shadowing a variable that needed to be mutable for some bit of code with it’s read-only form to disallow further unwanted mutation and to make it obvious to the reader that the variable is not supposed to change from that point on.
Always remember, “everyone can write code a computer can understand. The hard thing is to write code that Humans can understand”. Every hint as to what a variable is meant to mean is a good thing, and every unnecessary bit of boilerplate and noise (even in variable names, especially when it’s just their type appended to them) that you can avoid does help make the data-flow a bit clearer.
I don’t see how this relates to this feature. Kotlin does, in fact, equally want you to write safe code. Rust does, in fact, not force you to write low-level code. While you do face a few more low-level things in rust, most use-cases for variable shadowing in Rust that I’ve seen and heard had nothing to do with unsafe
code, raw pointers or bit-streams. Nearly all are about the same things we are talking ab out here in kotlin: About parsing and validating data and unwrapping nullable (Option<T>
) types.
I Don’t know how much experience you have with rust, but you definitely don’t work with “raw machine pointers or other low-level conversions” a lot. If you do, thats in unsafe
blocks, which are pretty rarely needed, and which don’t have any special relationship with variable shadowing.
Please elaborate, as I actually don’t understand what your trying to say here.
How would you “bypass the process of conversion and validation”?
My proposal is not implying the ability to bypass anything. The idea is to have more readable and writable variable names and more safety by having guarantees that you’re not accidentally using old, unnormalized or unvalidated variables. This does not bypass anything.
I cannot really respond to this, as I don’t get the point behind the previous part. But I’ll repeat: this has nothing to do with low-level code. I’ll go into why, imo, modern high-level languages like Rust and Kotlin need this a lot more than languages like C later.
Seeing that multiple experienced Rust-developers have given examples of the benefits of variable-shadowing that are nearly identical to my example in the preface, I think it’s safe to say you’re just wrong here.
The programmer does already define the “difference” in source-code in a way, as he specifies what is done to the data. The semantic meaning stays the same. The only thing that changes is how far the data has gone through the necessary data-processing pipeline. would you say that when using call-chains (with map
, filter
, let
, etc), you’d also want to give names - names that contain the type of the variable in some way - in the lambdas? Does your code look like this?
val myValidatedNormalizedParsedList = someStringList
.map { str -> myParser.parse(str) }
.map { parsedData -> parsedData.normalize() }
.filter { normalizedData -> normalizedData.isValid() } }
// or would you, as i would, just write
val myList = someStringList
.map { myParser.parse(it).normalize() }
.filter { it.isValid() }
and yes, I know I’ve condensed the parsing and normalization into a single map-call in the second example, whilst seperating them in the first. Because that’s what you imply: That you should always clearly show the current stage of data in the processing-pipeline in the name of the variable. Something that NEEDS seperate lines and variables for each step.
I think this is a good example, as it shows how we already have reached the same “solution” in method-chains: implicit it
parameter, condensing multiple conversions into a single line and chain, without specifying the type in between. All I’m trying to do is bring the same ease of use to places where call chains are undesirable. It would otherwise be identical in use-case and “danger”, except for the problems of accidentally overwriting the variable later on in the code - which I obviously recognize and understand, and for which we’d need to find at least some sort of solution, or evidence that it does not matter as much as most people think.
Why Rust needs this, and how it matches why Kotlin could want it.
Rust, Kotlin, etc are all modern languages that have a complex and expressive type-system.
While languages like C would just return null
and have you deal with it, Kotlin and Rust wrap these cases as dedicated types (in Rust this is Option<T>
, in Kotlin it’s T?
).
These wrapper types (Functors / Monads) are a bit more common in Rust than in Kotlin, because Rust also uses them as a replacement for recoverable Exceptions. In Rust you have Result<T,E>
, which is used to represent failure state. Where Kotlin sadly still relies on Exceptions, Rust uses the type-system yet again, so there are more cases of “unwrapping” data from a Monad-like than in Kotlin, making this feature a bit more necessary and helpful in Rust, as in Kotlin you don’t need it as much.
BUT: Kotlin does have Result<T>
, which will see more use in the near future. This, too, is a wrapper-type for values that replaces exceptions, making unwrapping values from containers such as Result<T>
even more relevant.
Another reason Rust strongly benefit’s from these is how well it integrates with other language constructs like if let
, which allows you to do a kind of pattern-match-if-combination like
if let Some(foo) = foo {
...
}
allowing you to extract the value of foo: Option<T>
and use it without having to name it fooActual
or having to name the option fooOption
. Rust does not have implicit it
or anything simmilar, so you have to name every variable you use, regardless of the context. In kotlin you’d do .let
and use the implicit it
.
These are all reasons Rust already has this, and Kotlin has not yet had a strong desire to add it. It is not strictly necessary, but just helpful.