val (thing1, thing2) = mutableValueWithComponentGetters
/* do stuff that might mutate mutableValueWithComponentGetters */
val (thing1prime, thing2prime) = mutableValueWithComponentGetters
/* here I want to be careful not to use thing1 or thing2 since they might have changed above. */
In Java, I might wrap the first two lines in a block and then reuse the names (thing1, thing2) later on.
Kotlin obviously doesn’t allow raw blocks since that’d be ambiguous with lambda syntax.
What if the do keyword were reused to allow blocks for scoping purposes so that I could write
do {
val (thing1, thing2) = mutableValueWithComponentGetters
/* do stuff that might mutate mutableValueWithComponentGetters */
}
do {
val (thing1, thing2) = mutableValueWithComponentGetters
/* I don't have to be so careful here */
}
Often breaking things out into helper functions helps code readability, but helpers do require the reader to jump around so not always.
Current workarounds are
when { else -> {
} }
do {
} while (false);
The first is awkward looking, and the latter requires readers to scan forward for the while (false) to realize that the body is not actually a loop.
You could use the standard library scoping functions.
Here’s an example using run:
val mutableValueWithComponentGetters: MutableList<String> = mutableListOf("thing1", "thing2")
fun main() {
//sampleStart
run {
val (thing1, thing2) = mutableValueWithComponentGetters
println("do stuff that might mutate mutableValueWithComponentGetters")
}
run {
val (thing1, thing2) = mutableValueWithComponentGetters
println("I don't have to be so careful here")
}
//sampleEnd
}
Here’s an example using let:
val mutableValueWithComponentGetters: MutableList<String> = mutableListOf("thing1", "thing2")
fun main() {
//sampleStart
mutableValueWithComponentGetters.let { (thing1, thing2) ->
// do stuff with thing1 and thing2 in scope.
}
//sampleEnd
}
Unless I’m missing something, there’s a case they don’t handle though.
var thingWithComponentGetters = ...;
if (thingWithComponentGetters != null) {
// Here, thingWithComponentGetters is known to be non-null.
run {
// Here thingWithComponentGetters is not known to be non-null because the
// compiler makes conservative assumptions about when nested functions might be called.
val (thing1, thing2) = thingWithComponentGetters; // ERROR: can't unpack a nullable
if (complexExpression(thing1, thing2)) {
thingWithComponentGetters = thingWithComponentGetters.derive(...);
}
}
run {
// Nor here.
val (thing1, thing2) = thingWithComponentGetters;
...
}
}
It’s possible since (presumably) run is an inline function, that a sufficiently-smart-compiler™ could realize post-inlining that run’s input is affine (called at most once and before the function to which it’s passed exits) but AFAICT, that’s not done yet.
One workaround would be to use a return value to move the assignment out of the lambda.
// Globally
fun <IN, OUT> runUsing(x: IN, f: (x: IN) -> OUT): OUT = f(x)
// Locally
var thingWithComponentGetters = ...;
if (thingWithComponentGetters != null) {
// Here, thingWithComponentGetters is known to be non-null.
thingWithComponentGetters = runUsing(thingWithComponentGetters) { thingWithComponentGetters ->
// Here thingWithComponentGetters is known to be non-null by type inference. 🎉
val (thing1, thing2) = thingWithComponentGetters;
if (complexExpression(thing1, thing2)) {
thingWithComponentGetters = thingWithComponentGetters.derive(...);
}
}
thingWithComponentGetters = runUsing(thingWithComponentGetters) { thingWithComponentGetters ->
// Since OUT is inferred separately from IN, both type guards from the `if` and from the body
// of the first call to runUsing should be apparent here.
val (thing1, thing2) = thingWithComponentGetters;
...
}
}
But, thingWithComponentGetters = runUsing(thingWithComponentGetters) { thingWithComponentGetters -> is super ugly and triggers (correctly) IDEA’s variable masking warnings.
I don’t want to introduce different names for thingWithComponentGetters in two different scopes because that reintroduces the thing1 vs thing1prime problem that I was initially trying to avoid; having two different names for a single conceptual entity makes for confusing code.
Yeah it is. This is a part of kotlin since 1.3, I belive and it works fine for me
data class Foo(val a: Int = 1, val b: Int = 2)
fun main() {
//sampleStart
var thingWithComponentGetters: Foo? = Foo()
if (thingWithComponentGetters != null) {
run {
val (thing1, thing2) = thingWithComponentGetters;
println(thing1)
println(thing2)
}
}
//sampleEnd
}
This is done with Contracts, which are fully suported on a bytecode level but the api is still experimental. Just google them, there are plenty of blog posts about them.
You’re right. Your code fails for me as well. I can’t really explain why though. It looks like somethings up with the componentN functions messing this up.
It feels like a bug to me, but maybe there is something going on here that I don’t understand.
I think it happens because compiler does not see run inlining for some reason. Without inline it is rather obvious. You capture the state of external scope when you invoke run and foo is a variable, so it is possible that somewone could assign it to null in process. First case works only because compiler is smart and it sees that foo is local and could not be reassigned between initial assignment and usage.
I expect it’s because JetBrains doesn’t want your smart casts to break if the implementation (not the signature) of run is changed, so in trying to prove that foo is non-null, it doesn’t consider the implementation.
A different implementation of run could execute the block twice or do something else first that causes foo==null.
Nope, but it’s not the same use case, since in that previous comment, the variable in question is not changed inside the closure. It wouldn’t matter if the lambda was called twice or not at all, and the variable could not be changed by run no matter what the implementation was.
Fair point. I had interpreted that as “since Kotlin 1.3, flow-sensitive checks of bodies of inline lambda arguments to inline functions is delayed until after inlining happens” in which case the two should be equivalent, but I suppose suppose some checks could be delayed and others not.