Kotlin contracts

6 minute read

Kotlin logo

Motivation

The Kotlin smartcast feature allows to write cleaner code and reduce boilerplate. However, it does not handle all possible situations properly. For example, suppose that we define a function that returns a distinct value if its parameter is not null. When we execute that function under the condition that the distinct value has been returned, then the Kotlin compiler does not automatically cast the parameter to a non-nullable type.

The following snippet illustrates this situation. It is a program that does not compile because the parameter s has not been casted to a non-null String.

fun checkNotNull(s: String?) = s != null
fun main(){
    //Not handeled by smartcast
    if (checkNotNull(s)) print(s.length)
}

It is clear for a human that if checkNotNull returns true, this implies that s is not null. We can thus use the return value of checkNotNull as a hint for casting s to a String instead of a String?. However, the Kotlin compiler, by itself, doesn’t make this guess.

In order to solve this problem, contracts were introduced in Kotlin 1.3. Their goal is improve smartcast analysis as well as variable initialization analysis in higher order function. We will give example of both cases in remainder of this article. Another benefit is that contracts are only used at compile time.

The next section shows how to use contracts in order to improve smartcast analysis.

Improve smartcast analysis

Defining a contract in checkNotNull allows the compiler to understand that when the functions returns true, then the parameter s will inevitably be not null. Thanks to that, in any code that is executed if checkNotNull returns true, then, its parameter s can be safely casted to a non-null string.

The following code snippet shows how to define such a contract:

contract {
    returns(true) implies(s != null)
}

This contract means that when the function returns true, then s is not null. In other words, if checkNotNull(s) is true, thus, s is also not null.

Here is the final code. Please note that we need to use the @ExperimentalContracts annotation to avoid the experimental feature warnings.

@ExperimentalContracts
fun checkNotNull(s: String?):Boolean {
    contract {
        returns(true) implies(s != null)
    }
    return s != null
}
@ExperimentalContracts
fun main(){
    val s: String? = null
    if(checkNotNull(s)) print(s.length)
}

This new code compiles just fine because s is automatically casted to a String thanks to the information provided by the contract.

Another neat example is the definition of a require function. Let’s define myRequire function that takes a condition as parameter. This function throws an exception when the condition is false. We can add a contract to myRequire that allows the caller to assume that the condition is true and apply it to smartcasting.

@ExperimentalContracts
fun myRequire(condition: Boolean) {
    contract {
        returns() implies condition
    }
    if(condition == false) throw IllegalArgumentException()
}
@ExperimentalContracts
fun main(){
    myRequire(s is String)
    //s smart casted because the app returns only if condition is true
    print(s.length)
}

In this example, the contract calls returns() without parameters. This means that the implies part is valid when the function just returns without causing an exception or crash.

The condition implied in the contract is passed as a parameter. Here, the condition is about the type of s. This means that when the function returns or exits, the compiler smartcasts s taking into account that s is String is true. This means that s will be casted to a non-nullable String. That’s a pretty powerful feature when we think of it :heart_eyes:.

Contracts are not just used to improve smartcasting. The next section shows how they can improve variable initialization analysis.

Improve variable initialization analysis in higher-order functions

Quick reminder: a higher order function is a function that takes a function as parameter. Some famous higher order functions are: map, filter and reduce.

We have seen in the previous section that require enables smartcasting based on the return value of a function. In addition to that, the require function has allows to help the compiler know many times a function is called inside a higher order one. This is possible thanks to the callsInPlace contract.

Let’s understand that with an example. Suppose that we have a function that calls another one passed as a parameter as follows:

fun executeOnceWithoutContracts(functionToRun: () -> Unit) {
    functionToRun()
}

The functionToRun parameter is called only once. This detail is important for the next part of the code. In fact, the next part calls the previous function as follows:

fun startGameV1(): Int {
    val lives: Int
    executeOnceWithoutContracts {
        lives = 5
    }
    //Compile error here !
    return lives
}

Inside the startGameV1 function, we call the executeOnceWithoutContracts.

From a human perspective, executeOnceWithoutContracts should initialize lives exactly once because we know that its parameter is called only once. Thus, since the initialization lives = 5 is executed only once, then lives remains a valid constant. However, the compiler doesn’t know how many times the function passed as a parameter to executeOnceWithoutContracts gets called. This means that the compiler assumes any number of calls and thus forbids declaring lives as val because it may be reassigned, once, many times, or not at all.

Thanks to contracts, we can give a helping hand to the compiler by telling it that the function passed to executeOnceWithoutContracts is executed only once. This is possible thanks to the callsInPlace function that takes the function to run and the number of times the latter is called as parameters. The second parameter is expressed as an enumeration with 4 possible values:

  • UNKNOWN: equivalent to not using a contract at all
  • AT_LEAST_ONCE
  • AT_MOST_ONCE
  • EXACTLY_ONCE: This correspond to our current case

The new definition of the executeOnce function is as follows:

@ExperimentalContracts
fun executeOnce(functionToRun: () -> Unit) {
    contract {
        // The compiler understands that functionToRun() will be called precisely one time
        callsInPlace(functionToRun, kotlin.contracts.InvocationKind.EXACTLY_ONCE)
    }
    functionToRun()
}

Please note that for kotlin < 1.3.20, an inline qualifier must be added to executeOnce. Kotlin 1.3.20 will not require the inline qualifier

The startGame function now compiles fine:

@ExperimentalContracts
fun startGameV2(): Int {
    val lives: Int
    executeOnce {
        lives = 5
    }
    // no compile error!
    return lives
}

Thank to the callsInPlace contract, the lambda passed to executeOnce is recognized by the compiler as a block that runs once and only once. Thus, the lives constant is correctly initialized and the code compiles and runs correctly.

The next section illustrates how we can already benefit from contract in stdlib.

Contracts are already used in Kotlin stdlib

Even though contracts are still experimental, the Kotlin stdlib already makes use of them and benefits from its improvements. The cherry on the cake is that this part is stable and does require additional annotations or flags to activate it.

The following code snippet shows some Kotlin stdlib functions that use contracts.

fun main(){

    var s: String? = null

    s = readLine()
    if(s == "null") s = null

    //isNullOrEmpty uses returns contract for smarcasting
    if( ! s.isNullOrEmpty() ){
        println("s is not null. Length ${s.length}")
    }else{
        println("s is null")
    }
    //require uses returns contact on the condition
    require(s is String)
    println("s is a String. Length: ${s.length}")

    val lock = "lock"
    val message: String
    //callsInPlaceContract once for the block
    synchronized(lock) {
        message = "initialized"
    }
    println("message $message")
}

In the above code, the standard functions isNullOrEmpty and require both use the returns contract to improve smartcasting. While the synchronized function uses callsInPlace to inform the compiler that the passed function is executed exactly once.

The following section concludes this post.

Conclusion

This article presented the contracts feature introduced in Kotlin 1.3. It allows to provide hints to the compiler to make it much smarter in some situations.

We have seen two use cases where contracts prove to be a useful approach. Firstly, they improve smartcast analysis (first example). Secondly, they allow to improve variable initialization analysis in higher-order functions (second example).

While contracts are still experimental, they are already used in the stdlib and are stable in that case.

Categories:

Updated:


Written by

Yassine Benabbas

Mobile developer at Worldline since 2011, I also happen to have PhD. I am interested in mobile and web subjects but I like sniffing my nose in many different subjects. I program with passion since more than 10 years.