Kotlin Smart Casting By Immutability
When we write Kotlin, in most cases we don’t need to be explicit about the types that we are using, because Kotlin type system is smart enough to figure it out by itself. In this post we will try to understand how that’s possible.
Let’s start by defining the following classes
open class Person()
class Adult : Person() {
fun payBills()
}
The Java way of checking if a given object (in this case, a person) is an adult, we would do:
void evaluate(Person person) {
if(person instanceof Adult) {
((Adult) person).payBills();
}
}
In Kotlin, no typecasting is needed!
open class Person
class Adult: Person() { fun payBills() { println(“Pay your bills!”) } }
fun main() { evaluate(Adult()) } //sampleStart fun evaluate(person: Person) { if(person is Adult) { person.payBills() } } //sampleEnd
The compiler is smart to know that by the time person.payBills()
is evaluated, the person will without any doubt be of type Adult
. This is possible because:
- We checked the type using the
is
keyword. - The operation is performed within the type-checking block (in this case, the
if
block). - The variable is immutable.
If any of those conditions fail, then smart casting will fail. For instance, the following blocks will throw a compiler error.
Missing type check
open class Person
class Adult: Person() { fun payBills() { println(“Pay your bills!”) } }
fun main() { evaluate(Adult()) } //sampleStart fun evaluate(person: Person) { person.payBills() // <— missing type check! } //sampleEnd
Outside of the type-checked block
open class Person
class Adult: Person() { fun payBills() { println(“Pay your bills!”) } }
fun main() { evaluate(Adult()) } //sampleStart fun evaluate(person: Person) { if(person is Adult) { // … } person.payBills() // <— out of smart cast scope! } //sampleEnd
Mutable variable
open class Person
class Adult: Person() { fun payBills() { println(“Pay your bills!”) } }
fun main() { evaluate(Response(Adult())) }
//sampleStart data class Response(var person: Person)
fun evaluate(response: Response) { if(response.person is Adult) { response.person.payBills() } } //sampleEnd
Run the code above, the exception is pretty self explanatory:
Smart cast to ‘Adult’ is impossible, because ‘response.person’ is a mutable property that could have been changed by this time
In this case, given the mutability of the variable (declared as var
instead of val
), smart cast cannot be applied because there is no warranty after the type check was performed, the value will remain the same.
Given that the response
is an object, its properties may have been modified somewhere else on the code (other threads referencing the same object, race conditions, etc.). In this particular case, we know that the value won’t change from one line to the next, but the compiler cannot make that assumption for us.
How to deal with mutable data?
This is one of the most common problems that I’ve seen on Kotlin codebases. The following example may look familiar to you (run the code!)
fun log(message: String) { // <— does not accept nulls! print(“The message is $message”) }
var message: String? = “Hello world”
fun main() { if(message != null) { log(message) } }
Let’s see some common ways to solve this
By using not-null assertion operator
fun log(message: String) { print(“The message is $message”) }
var message: String? = “Hello world”
fun main() { //sampleStart if(message != null) { log(message!!) } //sampleEnd }
This approach generally not recommended, because it means “There is a chance this could throw a Null-Pointer Exception. Use it with caution”. If you are 100% sure that there is no way the value will be modified somewhere else on the code, then you can use it. However, if that’s the case, you should reconsider why that variable is even mutable.
By storing it on a local, immutable variable
fun log(message: String) { print(“The message is $message”) }
var message: String? = “Hello world”
fun main() { //sampleStart val inmutableMessage = message if(inmutableMessage != null) { log(inmutableMessage) } //sampleEnd }
You can also escape the function, and the smart casting persist. This is possible because all the code after return
is within the smart-cast scope.
fun log(message: String) { print(“The message is $message”) }
var message: String? = “Hello world”
fun main() { //sampleStart val inmutableMessage = message if(inmutableMessage == null) return log(inmutableMessage) // <— at this point, it will never be null //sampleEnd }
In this case, the compiler knows that we are type-checking a non-mutable variable, so it’s safe to smart cast its value. Good approach, but it feels too verbose for Kotlin standards.
By using standard let function
fun log(message: String) { // <— does not accept nulls! print(“The message is $message”) }
var message: String? = “Hello world”
//sampleStart
fun main() {
message?.let { log(it) }
}
//sampleEnd
Much nicer! But what kind of magic allows us to do that? We can take a look at let implementation:
public inline fun <T, R> T.let(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block(this)
}
- Contract: Specifies that the
block
will only called once. This is not relevant (for now). block(this)
: Executes the block lambda and passes it self as parameter.
Let’s break it down step by step.
- We are using the safe operator
?.
before callinglet
. let
will only ever be executed ifmessage
is not null, and the compiler knows that.- Within the
let
function,this
references the message itself, because let is an extension function. - By calling
block(this)
, we are passing the non-nullable variable as a parameter. Parameters in Kotlin are immutable.
As you can see, all the conditions required for the compiler to be able to smart cast are fullfilled! No black magic, just type inference and a smart compiler.
By using contracts
Starting Kotlin 1.3, we have (experimental) contracts. By using contracts, we tell the compiler about what our function is doing. That way, the compiler no longer needs to make assumptions, but expects us to follow the contracts. Read the KEEP proposal to know the motivations behind contracts.
Let’s go back to the person / adult example. We were using is
to type check, but for the sake of the example, let’s imagine we need to create our own, custom adult validator.
open class Person
class Adult: Person() { fun payBills() { println(“Pay your bills!”) } }
fun main() { evaluate(Adult()) } //sampleStart fun evaluate(person: Person) { if(person.isAdult()) { person.payBills() } } //sampleEnd
Take a minute and think: how would you implement isAdult()
, so that person
can be smart casted within the if
block?
Normally that wouldn’t be possible. However, we can do it using contracts.
import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract
open class Person
class Adult: Person() { fun payBills() { println(“Pay your bills!”) } }
@ExperimentalContracts fun main() { evaluate(Adult()) }
//sampleStart @ExperimentalContracts fun Person.isAdult(): Boolean { contract { returns(true) implies (this@isAdult is Adult) } return this is Adult }
@ExperimentalContracts fun evaluate(person: Person) { if(person.isAdult()) { person.payBills() } }
//sampleEnd
In this case, that implementation is unnecessarily complicated, but may be handy on some cases. You can do lots of stuff with contracts, it is already been used on Kotlin standard functions. Use it when you see fit!
As we have seen, Kotlin’s compiler is pretty smart, and handles casting in a much safer way. And if it’s not smart enough for you, you can even define contracts and help it meet your needs!