Computer scienceProgramming languagesKotlinAdvanced featuresDSL

Contracts

13 minutes read

Sometimes, when writing code, everything may seem correct, but the compiler still gives a warning. For example, it may not be able to determine (with all its mighty power of analysis) that a variable cannot be null and suggests using the ? operator. Should you listen and obey even if you know the compiler is wrong? Or is there a way to get the compiler on the same page as you, so everyone is satisfied in the end?

In this topic, we will discuss how to communicate with the compiler using a feature called contracts, which was introduced in Kotlin 1.31.3. Let's discover what kind of spells we can cast on the compiler.

Understanding contracts

Contracts are agreements between different parts of the code that allow a function to explicitly describe its behavior in a way that is understood by the compiler. This information can help the compiler make smart decisions, such as when to allow smartcasts or determine variable initialization. The compiler does extensive static analysis to provide warnings and reduce boilerplate, and contracts take this a step further.

Consider the following example:

fun foo(s: String?) {
    if (s != null) s.length // Compiler automatically casts 's' to 'String'
}

Here, the compiler smartcasts s to a String inside the if statement. However, if we extract the null check into a separate function, the compiler loses this context:

fun String?.isNotNull(): Boolean = this != null

fun foo(s: String?) {
    if (s.isNotNull()) s.length // No smartcast :(
}

This is where contracts come into play. By adding a contract to our isNotNull function, we can inform the compiler that when isNotNull returns true, the receiver of the function (like s) is not null:

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

@OptIn(ExperimentalContracts::class)
fun String?.isNotNull(): Boolean {
    contract {
        returns(true) implies (this@isNotNull != null)
    }
    return this != null
}

fun foo(s: String?) {
    if (s.isNotNull()) s.length // Smartcast :)
}

In this example, this@isNotNull represents the receiver (which has to be of type String) of the isNotNull extension function.

Now, the compiler will know from the contract that s is definitely not a String? type (based on our word), so it will smartcast it to a String inside the if statement, even though the null check is inside a separate function.

The @OptIn(ExperimentalContracts::class) annotation is used to indicate that the contracts feature is still experimental. Although the syntax of contracts is not yet stable, the binary implementation is stable and is already being utilized by Kotlin's stdlib (standard library). We will see more about this later.

Syntax of contracts

The standard format for a contract is as follows:

function {
    contract {
        Effect
    }
}

This can be interpreted as "calling the function results in the specified Effect".

The contract function provides a Domain-Specific Language (DSL) scope. This means that within the contract block, you can use a specific set of functions and syntax to define the behavior of the contract. The DSL scope provided by the contract function allows you to write contracts in a more concise and readable way, using a syntax that is specifically designed for this purpose.

For example, within the contract block, you can use functions such as returns, callsInPlace, and implies to specify the behavior of the contract. These functions are part of the DSL scope provided by the contract function and are not available outside of the contract block.

As of now, there are two main types of contracts: returnsimplies and callsInPlace. The former one is used in cases like Improving the compiler's smartcasts analysis by declaring the relation between a function's call outcome and the passed arguments' values, while the latter one is used in cases like Improving the compiler's variable initialization analysis in the presence of high-order functions. Let's explore each one of them in more detail.

"returns"–"implies" contract

The returnsimplies contract is used to specify that a function will return a certain type by implying a condition. This allows the compiler to assume that after the function is called, the implies part of the contract will be satisfied, which is, in fact, a boolean expression.

Currently, the returnsimplies contracts only permit the use of the values true, false, or null on the right-hand side of the implies function.

Let's consider the following example. First, we need to import the necessary packages to work with contracts:

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract

After that, we declare the Animal abstract class, which is implemented by two concrete classes Cat and Dog:

abstract class Animal {
    abstract fun doAnimalStuff()
}

class Cat : Animal() {
    override fun doAnimalStuff() { /* ... */
    }

    fun makeSound() {
        println("Meow!")
    }
}

class Dog : Animal() {
    override fun doAnimalStuff() { /* ... */
    }
}

Now, let's create an extension function to use the above-mentioned first type of contracts:

@OptIn(ExperimentalContracts::class)
fun Animal.isCat(): Boolean {
    contract {
        returns(true) implies (this@isCat is Cat)
    }
    return this is Cat
}

Here, the contract tells the compiler that if isCat returns true, then this is an instance of Cat. As a result, the compiler can smartcast an instance of Animal to Cat:

fun main() {
    val animalList = listOf(Cat(), Dog())

    for (animal in animalList) {
        if (animal.isCat()) {
            animal.makeSound() // Compiler smartcasts animal to Cat
        }
    }
}

Here we created a list of Animal objects and iterated through it. If an object is a Cat, the compiler smartcasts it to a Cat object and calls the makeSound function.

"callsInPlace" contract

The callsInPlace contract is used to specify that a function parameter lambda is invoked in place. There are four possible invocation kinds: AT_MOST_ONCE, AT_LEAST_ONCE, EXACTLY_ONCE, and UNKNOWN. These options allow different behaviors, and their promises are as follows:

  • AT_MOST_ONCE: the lambda is invoked zero or one time.
  • AT_LEAST_ONCE: the lambda is invoked at least one time.
  • EXACTLY_ONCE: the lambda is invoked exactly one time.
  • UNKNOWN: the number of invocations is unknown.

The callsInPlace contract is especially useful when you know that the lambda will assign a value to a variable but the compiler does not. By using the contract, you can tell the compiler that the lambda will assign a value to the variable, and the compiler will allow the code to be compiled.

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@OptIn(ExperimentalContracts::class)
fun myRun(block: () -> Unit) {
      contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
          }
      return block()
}

fun main() {
    val x: Int
    myRun {
        x = 10 // The compiler is satisfied :)
    }
    println(x) // 10
}

In this example, we defined a function myRun that takes a lambda function block as a parameter and executes it. The callsInPlace contract is used to specify that the lambda function block is invoked exactly once, in place, within the myRun function. This means that the lambda is executed at the location where it is passed as an argument to myRun, rather than being deferred or executed at a later time.

In the main function, a variable x of type Int is declared as immutable (val) but not initialized. The myRun function is then called with a lambda that assigns a value to x. Due to the contract specified within the myRun function, the compiler is aware that the lambda is executed exactly once and therefore does not produce an error for possible reassignment to an immutable (val) variable.

Contracts in standard library

Contracts are widely used in the standard library. Here are some examples:

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

    return this == null || this.length == 0
}
  • run and other scoped functions:
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
  • require and check functions:
public inline fun require(value: Boolean): Unit {
    contract {
        returns() implies value
    }
    require(value) { "Failed requirement." }
}

Limitations and future of contracts

While contracts are a promising feature, they currently have a few limitations:

  • They can only be applied to top-level functions with a body.
  • The contract call must be the first statement in the function body.
  • The compiler trusts contracts unconditionally, so it is up to the programmer to ensure that the contracts they write are correct and sound.

It is important to note that the syntax for contracts is currently unstable and may change in the future. However, the binary representation of contracts is stable and already part of stdlib. This means that binary artifacts with contracts, such as stdlib, will not change without a graceful migration cycle and will have all the usual compatibility guarantees.

Conclusion

Contracts in Kotlin serve as a great tool to enhance the compiler's understanding of a function's behavior. They allow for explicit declaration of function behavior, enabling the compiler to make intelligent decisions such as smartcasting and variable initialization. The two main types of contracts, returnsimplies and callsInPlace, offer different ways to specify function behavior and are widely used in Kotlin's standard library. Despite being an experimental feature with a few limitations, contracts have a stable binary implementation and hold a great potential for future development. As developers, it's crucial to understand and correctly use contracts to optimize code readability and efficiency. Let's have some practice now.

16 learners liked this piece of theory. 2 didn't like it. What about you?
Report a typo