Kotlin Contracts

27 September 2018

The Kotlin type system is fairly flexible but there are a few corner cases where the compiler enforces rules which can be a little restrictive. In this article we explore how the 'contracts' feature in Kotlin 1.3 can be used to inform the compiler about a functions behaviour.

The Kotlin type system is fairly flexible but there are a few corner cases where the compiler enforces rules which can be a little restrictive. For example, we can't declare a val then initialise it within a lambda as the compiler does not know how the lambda will be executed and therefore cannot guarantee that the val will not be reassigned.

val name: String
synchronized(this) {
    name = "Instil" // 1.
}
print(name) // 2.
  1. Compilation error as the compiler cannot determine that name will only be assigned once.
  2. Compilation error as name has not be assigned.

Another source of minor frustration is that while we can use smart casts to avoid explicit casting or when dealing with nullable types, we can't use standard library functions such as isNullOrEmpty() with smart casts. Consider the following two examples of the same function, the latter is not able to use a smart cast.

fun capitalize(text: String?): String {
    if (text != null) {
        return text.capitalize() // 1.
    }
    return ""
}

fun capitalize(text: String?): String {
    if (!text.isNullOrEmpty()) {
        return text.capitalize() // 2.
    }
    return ""
}
  1. Compiles successfully.
  2. Compilation error as while isNullOrEmpty() performs a null check, the compiler does not have enough information to guarantee that text is not null.

Contracts

With the Contracts feature in Kotlin 1.3, a new DSL has been introduced which allows us to provide this extra information to the compiler. As of Kotlin 1.3-M2, we can provide information about how a function may return or how lambda arguments are executed which solves the two problems outlined above. At a high level, the Contracts DSL consists of a builder function contract to which we pass a lambda argument defining the function behaviours or effects, some of which we'll cover below. JetBrains have also included contracts in a lot of existing standard library functions such as synchronized or isNullOrEmpty which we referenced earlier meaning our previous examples will now compile in Kotlin 1.3.

Calls In Place

If we look at the source for the updated synchronized function, we can now see that the function contains a contract. The following contract guarantees that the synchronized function will only execute the block argument once within the scope of this function and it will not be passed to any other functions.

public actual inline fun <R> synchronized(lock: Any, block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }

    ...
}

Returns

Now if we look at the source for the updated isNullOrEmpty() function, we can see it also contains a contract. This time the contract specifies that the function will return false if the character sequence is not null and this allows us to use isNullOrEmpty() with smart casts! The implies function defines the condition of return and accepts a boolean value but is limited to null checks, type checks and logic operators.

public inline fun CharSequence?.isNullOrEmpty(): Boolean {
    contract {
        returns(false) implies (this@isNullOrEmpty != null)
    }

    return this == null || this.length == 0
}

Conclusion

While still an experimental feature, contracts look extremely promising and we're looking forward to the release of Kotlin 1.3 to start making use of contracts in our own types. If you'd like to read up more on the background of the contracts feature, see this KEEP for more information.

Article By
blog author

Chris van Es

Head of Engineering