Sometimes compiler needs help and more information. Kotlin contracts are a way to transmit more information to the compiler.
Kotlin contracts with the custom contract are experimental, but the stdlib
already uses it.
This is a new language mechanism, it allows developers to pass more detailed information to the compiler and let the compiler utilize and analyze more data.
The new mechanism gives new possibilities:
- improving smartcasts analysis
- improve variable initialization analysis
Improve smartcasts analysis
Smartcast dissappears whenever we extract any checks to a new function, to keep the same behavior we can apply kotlin contracts.
Let's imagine the situation when we want to make object validation before processing. Validation is not so simple so we decide to extract it to the new function.
private fun validateUserWithoutContract(user: User?) {
if (user == null) {
throw Exception("We have big problem!")
}
...
}
When we try to reach properties of a given user
object without a safe call, we will meet a compilation error.
fun processWithoutContract(user: User?) {
validateUserWithoutContract(user)
println(user.name) // Compilation error
}
We know that this object cannot be null because we already make a null-check, but the compiler doesn't know that. Kotlin contracts allow us to pass this information to the compiler.
@ExperimentalContracts
private fun validateUserWithContract(user: User?) {
contract {
returns() implies (user != null)
}
if (user == null) {
error("We have big problem!")
}
}
With this contract assumption, we can use user.name
to reach name property, without a safe-call outside of the function, so processWithoutContract
method will compile without any errors. The example above is the good practice of using this feature, another good way of utilization is casting.
Improve variable initialization analysis for higher-order functions
Let's check the second part of the contracts, we can tell the compiler how many times a function will be called by callsInPlace
and InvocationKind
.
Kotlin contracts have four invocation kinds:
- UNKNOWN - can't initialize var or val, doesn't have to return.
- AT_MOST_ONCE - same as UNKNOWN.
- AT_LEAST_ONCE - can initialize var, but can't be used to initialize val, has to return after returned in the block.
- EXACTLY_ONCE- can initialize val and var, but also has to return after returned in the block.
Let's have a look at run
function:
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return block()
}
This function uses EXACTLY_ONCE
so it means that you can use it to initialization of var and val variables. That's why the below code will compile, without this contract
statement compiler will complain about Captured values initialization is forbidden due to possible reassignment
and suggest changing to var
instead of val
.
This example illustrates one of the most common ways to use kotlin contracts, the initialization of objects in lambdas.
fun main() {
val website: Website
run {
website = Website("www.codingflower.com")
}
print(website)
}
Limitations
- Contract has to be on the very first lines of the code.
- It can be used only on top functions or member functions.
- There is no verification of the contracts.
Summary
This mechanism works in compile-time, the developer may provide additional guarantees or restrictions, which could be utilized by the compiler to perform analysis. This is the solution for the problem when we know that the object is not null but we had to use a safe call because of removed smartcasting. Some of the functions in stdlib are annotated with contracts. This contract concept is nothing new because this mechanism is already implemented in C++.