would it be possible (or even a good idea) to allo...
# language-proposals
l
would it be possible (or even a good idea) to allow and maybe even encourage local name-shadowing like rust does? i.e. in rust, you can do
Copy code
let foo: String = "4".to_string();
let foo: Option<i32> = foo.parse().ok(); // parse foo into the equivalent of Int?
let foo: i32 = foo.unwrap_or(1234); // equivalent of foo ?: 1234
i'd really love this for many reasons: - it keeps you from having to come up with either unreadable or needlessly complex names (i.e. here:
fooString
,
fooMaybeInt
,
foo
) or - keeps you from alternatively having to combine everything into a giv method chain (which many times is preferable, in my example it would absolutely be preferable. but there are cases like more complicated in-between-steps where forcing everything into a single method-chain hurts readability badly (-> deep nesting, functions that take the value as an argument instead of as a receiver) - it would even be great to make this good-practice for nullable-let-stuff (i.e.:
foo?.let { foo -> doSomething(foo) }
is currently a warning, and using
it
is not a good option when this turns into more complex nested chains or
doSomething
is a longer call some arguments against it i've considered: # "just use var and mutate" - No. I do not want to use var where i dont need to dynamically mutate a variable. mutation should only be used when it actually represents mutation, not just to make code more readable in cases like this. also this does not allow for type-changes like in my example # "This hurts safety by allowing you to accidentally try to use a variable that has been shadowed " - Maybe, but it would not be a big problem. Most of the time, you'd use this to actually change the types like in my example. this would mean that you will get compiler errors immediately nearly every time. This is still by far the biggest argument against it, as i could see it making things less obvious in some rare edge-cases. but it does not hurt your actual safety as this is NOT mutation, but name-shadowing. # "This hurts readability, i dont like it" - this is subjective, but imo, No. First of all, rust get's away with it very well, and it does help rust a lot. (but rust does also work with wrapped types like Option<T> and Result even more than kotlin, so it IS more necessary there) - While shadowing variables can make for ambiguity on first sight, consider the ways this could help: - reduce the amount of nested methods and indents by encouraging putting things in seperate lines (one of the biggest reasons people like using method chains is that they don't need to think of names for their intermediate variables) - reduce the need for mutable variables. this is a big one. Using mutable vars where they are not strictly necessary does hurt your compile-time guarantees a lot more. - another thing about mutables: sometimes you NEED mutable vars if you work with stuff in loops (most of the time it's better to replace these with map, filter, etc, but there are cases where a simple loop is better). now, after your loop, your variable is still mutable. with this feature, you could then do
var foo: Int = 0; for(...){/*change foo*/}; val foo = foo;
and have it be immutable after the loop, again helping your compile-time guarantees. # "What would happen with nested scopes? wouldn't this be mutation?" - No. shadowing stays shadowing. if you shadow your variable inside a nested scope, this does not change the original variable. are there any points i've missed or any big arguments against it?
s
i like the idea. Particularly if there could be some IDE support to highlight that a a variable is shadowing another (not a warning just another colour like we can have with smart casts)
l
Public proposal on Kotlin language-design forum: https://discuss.kotlinlang.org/t/rust-style-variable-shadowing/16338
h
Your proposal is very well researched, the interviews are really interesting, as are some aspects in favour of shadowing. I remember how i had to explain parameter shadowing and even in situations comparable to your examples, people reeeeally didn't like it, because they get confused having a single thing twice in a scope... Most situations i encountered where shadowing is inappropriate, the code itself could have been better, for example simple method extracted and overloaded or sth. So i am hesitating as well, in general i have no problem with shadowing where appropriate but it's veeery ugly to discuss that with people not already sold on Kotlin because people see it as a feature more probably doing harm than sth good.
l
thanks for your feedback! I agree that most code that would have problems with shadowing is code that should really not be in one function, as shadowing only becomes a problem when you try to use two unrelated things and give it the same name (in which case you most likely want to extract into a different function). I can imagine beginners getting confused by it, although I'd argue that most won't really notice it at the start, and those who do will at least also see the benefits, when layed out as such in the docs and see why this feature was added. I could also see it being introduced behind a warning at first, so people are still aware of it happening. You would still be able to use it normally by supressing the warning!
c
I think this should be called differently than "shadowing", which already has a meaning in Kotlin:
Copy code
val a = 3
    run {
        val a = "s"
        println(a)   // "s"
    }
    println(a)  // 3
I was thinking maybe "erase" because each step really makes the previous value disappear, but "erase" obviously has some baggage too.
l
But it is exactly the Same Thing as shadowing, just that you dont need to introduce a New scope. Variable shadowing as a concept does already exist and is already implemented in languages like rust, and with the Same Name. It would be a Bad idea to change that name
c
It's not exactly the same: when you shadow a variable, you recover the shadowed variable when you exit the scope
l
but it just is. Shadowing means changing what variable a name refers to until the end of the current scope. in nested scopes this is the end of the nested scope, because of which the variable is available again after the scope. if you shadow a variable in the same scope as it's created, the end of the scope is still the end of the shadowing, just that the variable is unavailable after the end of the scope as well because it was created within the scope. The name is also pretty irrelevant, i chose it because it - clearly shows the intention - is already used in languages where this is an encouraged feature (-> Rust) You could of course name it differently was this feature actually implemented, but i feel like this discussion is not really necesssary atm