Smart cast not possible for lazy


#1

Recently, I’ve had a huge annoyance: I wanted to use a lazy value and smart cast it. A lazy value computes its result once and then always returns the same value, so I thought it could surely be smart-casted. Turns out, it can’t. Why?
Is there simply no possibility for the compiler to express such a thing? I’d rather not use let blocks or local variables everytime I access that lazy, since I might as well use a var if I have to check it all the time anyways.

The problem why I can’t do it differently is that I am writing an android application and I have a variable that has to be initialized in onCreate. As such it can’t be a simple val, since it is not initialized in the constructor, yet it does not change its value ever after creation. I opted for lazy since that also implies it to be a val, but Kotlin refuses to treat it as “real” val.


#2

I don’t really understand your problem. Can you give us a short example?


#3

This is a pretty random example, but it illustrates the point:

sealed class Value {
	data class IntValue(val int: Int): Value()
	data class StringValue(val string: String): Value()
}

val value: Value by lazy {
	if(System.currentTimeMillis() % 2 == 1L)
		Value.IntValue(1)
	else
		Value.StringValue("test")
}

fun main() {
	if(value is Value.IntValue) {
		println(value.int) // Compilation error: Smartcast is impossible because property has open or custom getter
	}
}

#4

In order to make smart cast here, you need to create specialized intrinsic for lazy delegate. In general, delegated value could change therefore no smart casts. For your particular example you can just write:

val _value = value
if(_value is Value.IntValue) {
	println(_value.int)
}

#5

Would this be something contracts could deal with?

I don’t have experience with contracts so I may be off here. Is there a contract for purity or, promising to return the same value, that could be made to solve all cases where a Smartcast is impossible because property has open or custom getter?


#6

Currently this can’t be solved with contracts. It might be possible once contracts can be used for class members, but even then we would need some kind of purity contract as you say.


#7

Yep, that works for that particular case. I’ll expand my example to come closer to my real-world problem:

sealed class Value {
	data class IntValue(val int: Int): Value()
	data class DoubleValue(val double: Double): Value()
	data class StringValue(val string: String): Value()
}

val value: Value by lazy {
	if(System.currentTimeMillis() % 2 == 1L)
		Value.IntValue(1)
	else
		Value.StringValue("test")
}

fun main() {
	when(value) {
		is Value.IntValue -> println(value.int)
		is Value.DoubleValue -> println(value.double)
		is Value.StringValue -> println(value.string)
	}
}

I know, I can use a local variable here as well. But what if every time I use this variable I have such a when statement (which I do in my Android project that uses the MVI model)? I do not want to declare a local variable every single time, neither use a let block or something that increases nesting even more.

I think arocnies is right, this should probably be solved with contracts.


#8

Probably it could. But for now, it is not possible since lazy{} is just regular delegate-generating function and not a language construct. You can redefine it


#9

“You can redefine it”

What do you mean with that? What use could there be in redefining it?


#10

I mean that it is library function, you can in theory create a function with the same name in different package and use it. So the only way to make smart cast is to add intrinsic for this specific function. It does not make sense. Of course contracts are made just to solve that.


#11

Since Kotlin 1.3 you can write this, which avoids leaking the local var to the outside scope:

when(val _value = value) {
    is Value.IntValue -> println(_value.int)
    is Value.DoubleValue -> println(_value.double)
    is Value.StringValue -> println(_value.string)
}