Kotlin Contracts
This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.
One of Kotlin’s most mysterious features is Kotlin Contracts. Most developers use them without even knowing how they work, or even how to define them. Kotlin Contracts make the Kotlin compiler smarter and allow us to do things that would otherwise be impossible. But before we see them in action, let’s start with a contract definition which can be found in some Kotlin stdlib functions.
A Kotlin Contract is defined using the contract
function, which for now can only be used as the start of the body of a top-level function. The contract
function starts the DSL builder. This is a peculiar function because it is an inline function with an empty body.
Inline function calls are replaced with the body of these functions0. If this body is empty, it means that such a function call is literally replaced with nothing, so it is gone. So, why might we want to call a function if its call is gone during code compilation? Kotlin Contracts are a way of communicating with the compiler; therefore, it’s good that they are replaced with nothing, otherwise they would only disturb and slow down our code after compilation. Inside Kotlin Contracts, we specify extra information that the compiler can utilize to improve the Kotlin programming experience. In the above example, the isNullOrBlank
contract specifies when the function returns false
, thus the Kotlin compiler can assume that the receiver is not null
. This information is used for smart-casting. The contract of measureTimeMillis
specifies that the block
function will be called in place exactly once. Let’s see what this means and how exactly we can specify a contract.
The meaning of a contract
In the world of programming, a contract is a set of expectations on an element, library, or service. By “contract”, we mean what is "promised" by the creators of this solution in documentation, comments, or by explicit code structures2. It’s no wonder that this name is also used to describe language structures that are used to specify expectations on some code elements. For instance, in C++ there is a structure known as a contract that is used to demand certain conditions for the execution of a function:
// C++
int mul(int x, int y)
[[expects: x > 0]]
[[expects: y > 0]]
[[ensures audit res: res > 0]]{
return x * y;
}
In Kotlin, we use the require
and check
functions to specify expectations on arguments and states.
The problem is with expressing messages that are directed to the compiler. For this, we could use annotations (which was actually considered when Kotlin Contracts were being designed), but their expressiveness is much more limited than DSL. So, the creators of Kotlin defined a DSL that starts function definitions. Let's see what we can express using Kotlin Contracts.
How many times do we invoke a function from an argument?
As part of a contract, we can use callsInPlace
to guarantee that a parameter with a functional type is called in place during function execution. Using a value from the InvocationKind
enum, this structure can also be used to specify how many times this function is executed.
There are four possible kinds of invocation:
EXACTLY_ONCE
- specifies that this function will be called exactly once.AT_MOST_ONCE
- specifies that this function will be called at most once.AT_LEAST_ONCE
- specifies that this function will be called at least once.UNKNOWN
- does not specify the number of function invocations.
Execution of callsInPlace
is information to the compiler, which can use this information in a range of situations. For instance, a read-only variable cannot be reassigned, but it can be initialized separately from the definition.
When a functional parameter is specified to be called exactly once, a read-only property can be defined outside a lambda expression and initialized inside it.
Consider the result
variable, which is defined in the forceExecutionTimeMillis
function and initialized in the lambda expression in the measureTimeMillis
function. This is possible only because the measureTimeMillis
parameter block
is defined to be called in place exactly once.
Contracts are still an experimental feature. To define contracts in your own functions, you need to use
OptIn
annotation withExperimentalContracts
. It is extremely unlikely Kotlin will drop this functionality, but its API might change.
The EXACTLY_ONCE
invocation kind is the most popular because it offers the most advantages. With this invocation kind, read-only properties can only be initialized in blocks. Read-write properties can also be initialized and re-initialized in blocks whose invocation kind is AT_LEAST_ONCE
.
In the above code, only the first call of
callback
is called in place, so this contract is not completely correct.
Another example of how the Kotlin Compiler uses the information specified by the callsInPlace
function is when a statement inside a lambda is terminal for function execution (declares the Nothing
result type1), therefore everything after it is unreachable. Thanks to the contract, this might include statements outside this lambda.
I've seen this used in many projects to return from a lambda expression.
Implications of the fact that a function has returned a value
Another kind of contract statement includes the returns
function and the infix implies
function to imply some value-type implications based on the result of the function. The compiler uses this feature for smart-casting. Consider the isNullOrEmpty
function, which returns true
if the receiver collection is null
or empty. Its contract states that if this function returns false
, the compiler can infer that the receiver is not null
.
Here is a custom example in which the compiler can infer that the receiver is of type Loading
because startedLoading
returns true
.
Currently, the returns
function can only use true
, false
, and null
as arguments. The implication must be either a parameter (or receiver) that is of some type or is not null
.
Using contracts in practice
Some important functions from Kotlin stdlib define their contract; we benefit from this as it makes the compiler smarter. Some developers don’t even notice how they use contracts and might be surprised to see how Kotlin allows something that would not be allowed in other languages, like smart-casting a variable in an unusual case. Having said that, you don’t even need to know how a contract works in order to benefit from it.
Defining contracts in our own top-level functions is extraordinarily rare, even for library creators. I’ve seen it done in some projects, but most projects don’t even have a single custom function with a specified contract. However, it’s good to understand contracts because they can be really helpful in some situations. A good example is presented in the article Slowing down your code with Coroutines by Jan Vladimir Mostert, which presents a technique to make some requests take a specific time. Consider the code below. The function measureCoroutineTimedValue
needs to return the measured time as well as the value which is calculated during its execution. To measure time, it uses measureCoroutineDuration
, which returns Duration
. To store the result of the body
, it needs to define a variable. The body of measureCoroutineTimedValue
only works because measureCoroutineDuration
is defined in its callsInPlace
contract
with InvocationKind.EXACTLY_ONCE
.
Summary
Kotlin Contracts let us specify information that is useful for the compiler. They help us define functions that are more convenient to use. Kotlin Contracts are defined in some Kotlin stdlib functions, like run
, let
, also
, use
, measureTime
, isNullOrBlank
, and many more; this makes their usage more elastic and supports better smart-casting. We rarely define contracts ourselves, but it’s good to know about them and what they offer.
I described inline functions in the Inline functions chapter in Functional Kotlin.
For details, see The beauty of the Kotlin type system chapter in Kotlin Essentials.
This is known as Design by contract, the advantage of which is that it frees a function from having to handle cases outside of the precondition. Bertrand Meyer coined this term in connection with his design of the Eiffel programming language.