Software Engineering Blog

for Android and Kotlin developers

All Articles

Kotlin Smart Casting By Immutability

Published 2 Jun 2019

Article cover

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:

  1. We checked the type using the is keyword.
  2. The operation is performed within the type-checking block (in this case, the if block).
  3. 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.

  1. We are using the safe operator ?. before calling let.
  2. let will only ever be executed if message is not null, and the compiler knows that.
  3. Within the let function, this references the message itself, because let is an extension function.
  4. 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!