Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: March 19, 2024
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.
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.
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.
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.
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 that targets any non-null returned value:
contract {
returnsNotNull() implies (event is MyEvent)
}
The callsInPlace contract expresses the following guarantees:
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.
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:
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")
}
}
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.