Preface
Having worked with Rust recently, I’ve come to like one very specific feature a lot: Variable shadowing.
In rust it is possible to shadow variable names without warnings or errors, even in the same scope, and I propose adding this feature to Kotlin.
Here is an example of how it works in rust:
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
How it works
Variable shadowing means that it’s possible to create a new variable with the same name as another variable that has already been declared within the same scope.
This means that it is possible to instantiate a variable with type String, and then later have another variable with the same name in the same scope, but of type Int, or even just with a different value stored inside.
Difference to mutation
Mutation means actually changing the value the variable references, meaning that if you pass your variable to another place, it also changes there. This is the idea of var
.
Variable scoping however is different in that it does not change the original variable, but creates a new, independent one.
Why this is great
There are several reasons as to why variable-scoping is a great feature.
1. Increase in readability
It’s not uncommon to have long method call chains that transform a given piece of data, like the given rust-example. In Kotlin, one would currently implement the same code as follows:
val foo = "4".toIntOrNull() ?: 1234
This is not a problem. but now imagine a much longer call-chain, where it becomes necessary to put things into seperate variables:
val myUserData = "some; stuff: 12".trim()
val parsedUserData = myParser.parse(myUserData)
val sanitizedUserData = sanitizeUserData(
option1 = "foo", option2 = "bar", option3 = "baz",
parsedUserData
)
val updatedSanitizedUserData = sanitizedUserData.copy(name = someNewName)
val stringifiedUpdatedSanitizedUserData = gson.toJson(updatedSanitizedUserData)
println(stringifiedUpdatedSanitizedUserData)
To be fair, this is a contrived example and not really good code (altough i’ve actually seen variable names simmilar to stringifiedUpdatedSanitizedUserData
…), but it shows the improvement this feature could do:
val userData = "some; stuff: 12"
val userData = myParser.parse(userData)
val userData = sanitizeUserData(option1 = "foo", option2 = "bar", option3 = "baz", userData)
val userData = userData.copy(name = someNewName)
val userData = gson.toJson(userData)
println(userData)
The reason this is an improvement is that userData
is a good name (please imagine it actually being good, haha), and that after parsing and sanitization, the data is still very well described by the term “userData
”.
The most important difference imo is when using .copy
on immutable data classes.
If you store data in immutable classes and then update them, you will very rarely want to work with the old data afterwards. I’ve made the mistake of accidentally giving someData
to functions after creating someDataUpdated = someData.copy(...)
multiple times.
TL;DR: It saves you from having to either put everything into one big, unwieldly call-chain or alternatively having to think of variable names for the intermediate variables, and can also save you from accidentally working with outdated data when updating immutable classes and storing them in updatedData
-style-named variables
2. Making mutability even less necessary
Many times you see (mostly) beginners make variables mutable because they might want to work in them over the course of many function calls. an example:
var input = readLine() ?: return null
logger.info("user entered: $input")
input = input.trim().capitalize()
doSomething(input)
this, again, is obviously a contrived example, but i’m sure you can remember a point in time where you or someone you worked with made a variable mutable just to be able to change it in this way (non-dynamically).
This could be prevented if you could do
val input = readLine() ?: return null
logger.info("user entered $input")
val input = input.trim().capitalize()
doSomething(input)
while this does not make a big difference here, keeping things immutable is a great goal which in more complex scenarios CAN be a big factor.
Changing var
to val
There are still some valid use-cases for mutable variables and for
-loops that mutate them.
In these cases, most of the time the variable will only be mutated within the loop, and will be treated as immutable afterwards. Enforcing this would be great.
This could be done like this:
var someAggregatedNumber = 0
for(.....) { someAggregatedNumber = someFunction(someAggregatedValue) }
val someAggregatedNumber = someAggregatedValue
Having mutability be contained as tightly as possible is always a good idea!
Reasons this might not be a good idea
There are obviously some arguments against this proposal, some of which I hope to address here.
Let me start by saying that there are languages that have this feature, and I’ve not yet heard anyone complain about it. This are mostly languages that encourage immutable data, even in local scopes, simmilar to kotlin.
Variable-shadowing could make you use the wrong value accidentally
This is of course a problem. Indeed, you would be more likely to use a shadowed variable accidentally and get unexpected results than without this feature. This is the biggest argument against it.
BUT:
- This problem already exists when using nested scopes, altough most IDEs give warnings about name-shadowing, which would of course not be the case if this was implemented
- Making shadowing a language-feature would most likely cause you to keep this in mind, especially when actively working with shadowed declarations. As long as you’re aware of the feature, you might be even less likely to make this mistake than you would have been with it not being on your mind at all.
- Most of the time I’d see this feature be useful when also changing times, like I did in the parsing example. In this case there would be compiler-errors if you tried to use your parsed data somewhere where the string-version was expected, making the error obvious
What are the performance implications?
well,… I don’t know. Maybe encouraging the use of intermediate variables over method-chains could have a negative performance impact, but most likely this would be optimized away.
On the other hand, this would encourage the use of normal function-calls over method-chains containing .let
calls for static functions, which could potentially help performance. (altough I guess .let
call are most likely optimized to normal function calls under the hood anyways)
Just use Mutability and Call-chains
I already adressed mutable variables as a non-alternative, as they do not give the same guarantees and don’t allow for type-changes.
Call chains are an alternative most of the time, but can get unreadable when many static calls are need or call chains get to deep into lambda territory. Having intermediate variables is a great alternative, that currently faces the naming problem.