1. Overview
In this tutorial, we will talk about Kotlin Contracts. Their syntax is not stable yet, but the binary implementation is, and Kotlin stdlib itself is already putting them to use.
Basically, Kotlin contracts are a way to inform the compiler about the behavior of a function.
2. Maven Setup
This feature is introduced in Kotlin 1.3, so we need to use this version or a newer one. For this tutorial, we’ll use the latest version available – 1.3.10.
Please refer to our introduction to Kotlin for more details about setting that up.
3. Motivation for Contracts
As smart as the compiler is, it doesn’t always come to the best conclusion.
Consider the example below:
data class Request(val arg: String) class Service { fun process(request: Request?) { validate(request) println(request.arg) // Doesn't compile because request might be null } } private fun validate(request: Request?) { if (request == null) { throw IllegalArgumentException("Undefined request") } if (request.arg.isBlank()) { throw IllegalArgumentException("No argument is provided") } }
Any programmer can read this code and know that request is not null if a call to validate doesn’t throw an exception. In other words, it’s impossible for our println instruction to throw a NullPointerException.
Unfortunately, the compiler is unaware of that and doesn’t allow us to reference request.arg.
However, we can enhance validate by a contract which defines that if the function successfully returns – that is, it doesn’t throw an exception – then the given argument is not null:
@ExperimentalContracts class Service { fun process(request: Request?) { validate(request) println(request.arg) // Compiles fine now } } @ExperimentalContracts private fun validate(request: Request?) { contract { returns() implies (request != null) } if (request == null) { throw IllegalArgumentException("Undefined request") } if (request.arg.isBlank()) { throw IllegalArgumentException("No argument is provided") } }
Next, let’s have a look at this feature in more detail.
4. The Contracts API
The general contract form is:
function { contract { Effect } }
We can read this as “invoking the function produces the Effect”.
In the following sections, let’s have a look at the types of effects that the language supports now.
4.1. Making Guarantees Based on the Return Value
Here we specify that if the target function returns, the target condition is satisfied. We used this in the Motivation section.
We can also specify a value in the returns – that would instruct Kotlin compiler that the condition is fulfilled only if the target value is returned:
data class MyEvent(val message: String) @ExperimentalContracts fun processEvent(event: Any?) { if (isInterested(event)) { println(event.message) } } @ExperimentalContracts fun isInterested(event: Any?): Boolean { contract { returns(true) implies (event is MyEvent) } return event is MyEvent }
This helps the compiler make a smart cast in the processEvent function.
Note that, for now, returns contracts allow only true, false, and null on the right-hand side of implies.
And even though implies takes a Boolean argument, only a subset of valid Kotlin expressions is accepted: namely, null-checks (== null, != null), instance-checks (is, !is), logic operators (&&, ||, !).
There is also a variation which targets any non-null returned value:
contract { returnsNotNull() implies (event is MyEvent) }
4.2. Making Guarantees About a Function’s Usage
The callsInPlace contract expresses the following guarantees:
- the callable won’t be invoked after the owner-function is finished
- it also won’t be passed to another function without the contract
This helps us in situations like below:
inline fun <R> myRun(block: () -> R): R { return block() } fun callsInPlace() { val i: Int myRun { i = 1 // Is forbidden due to possible re-assignment } println(i) // Is forbidden because the variable might be uninitialized }
We can fix the errors by helping the compiler to ensure that the given block is guaranteed to be called and called only once:
@ExperimentalContracts inline fun <R> myRun(block: () -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block() }
Standard Kotlin utility functions run, with, apply, etc already define such contracts.
Here we used InvocationKind.EXACTLY_ONCE. Other options are AT_LEAST_ONCE, AT_MOST_ONCE, and UNKNOWN.
5. Limitations of Contracts
While Kotlin contracts look promising, the current syntax is unstable at the moment, and it’s possible that it will be completely changed in the future.
Also, they have a few limitations:
- We can only apply contracts on top-level functions with a body, i.e. we can’t use them on fields and class functions.
- The contract call must be the first statement in the function body.
- The compiler trusts contracts unconditionally; this means the programmer is responsible for writing correct and sound contracts. A future version may implement verification.
And finally, contract descriptions only allow references to parameters. For example, the code below doesn’t compile:
data class Request(val arg: String?) @ExperimentalContracts private fun validate(request: Request?) { contract { // We can't reference request.arg here returns() implies (request != null && request.arg != null) } if (request == null) { throw IllegalArgumentException("Undefined request") } if (request.arg.isBlank()) { throw IllegalArgumentException("No argument is provided") } }
6. Conclusion
The feature looks rather interesting and even though its syntax is in the prototype stage, the binary representation is stable enough and is a part of stdlib already. It won’t change without a graceful migration cycle, and that means that we can depend on binary artifacts with contracts (e.g. stdlib) to have all the usual compatibility guarantees.
That’s why our recommendation is that it’s worth using contracts even now – it wouldn’t be too hard changing contract declarations if and when their DSL changes.
As usual, the source code used in this article is available over on GitHub.