Exploring the Power of Kotlin Contracts for Better Code Quality
Contracts in programming are agreements between different parts of the code. They allow a function to explicitly describe its behavior in a way that is understood by the compiler. Kotlin contracts are an extension of this concept that was introduced in version 1.3.60 of Kotlin. They allow a function to explicitly describe its behavior in a way that is understood by the compiler.
One, very basic example of a Kotlin contract is theisNotNull
function.
fun Any?.isNotNull(): Boolean {
return this != null
}
When you use the code as such,
val foo: Int? = 42
if (foo.isNotNull()) {
println(foo)
}
The compiler will not allow you to compile the code. Since it cannot ensure the foo
is not null inside of the if-statement. However, with contracts, the compiler will trust you and allow the code to be compiled. Take a look below.
fun Any?.isNotNull(): Boolean {
contract {
returns(true) implies (this@isNotNull != null)
}
return this != null
}
Now, we tell the compiler that when this function returns true
, the value is not null. Thus, the compiler will allow you to compile your code.
Types of contracts in Kotlin
There are basically two types of contract in kotlin.
CallsInPlace
The CallsInPlace
contract is used to specify that a function parameter lambda is invoked in place.
@OptIn(ExperimentalContracts::class)
fun runOnlyOnce(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.AT_LEAST_ONCE)
}
block()
}
The different InvocationKind
options are AT_MOST_ONCE
, AT_LEAST_ONCE
, EXACTLY_ONCE
, and UNKNOWN
. I believe the names are very self-explanatory. These options’ promises are as follows:
InvocationKind | Times
------------------------------
AT_MOST_ONCE | (0, 1]
AT_LEAST_ONCE | [1, ∞)
EXACTLY_ONCE | 1
UNKNOWN | ?
Also, a "val" can be used in the lambda and initialized, If the InvocationKind
is EXACTLY_ONCE.
@OptIn(ExperimentalContracts::class)
fun runOnlyOnce(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}
fun main() {
var foo: Int
runOnlyOnce {
foo = 42
}
println(foo)
}
In any other InvocationKind
, the compiler will not allow the code to be compiled. Clearly, this is due to the fact that other types do not guarantee invocation of the block
. A “var” on the other hand, can be used with InvocationKind.AT_LEAST_ONCE
as well . Of course, the reason is again the promise you give.
One use-case of using the CallsInPlace
contract is 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.
Returns — implies
The returns - implies
contract is used to specify that a function will return a certain type by implying an 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.
One example of using the returns - implies
contract is when we want to check if a value is of a certain type. Let’s check the scenario below.
abstract class Baz {
abstract fun doSomething()
}
class Bar : Baz() {
override fun doSomething() {
println("Bar")
}
fun doSomethingElse() {
println("Bar")
}
}
class Foo : Baz() {
override fun doSomething() {
println("Foo")
}
}
And now assume we have check like this :
fun Baz.isBar(): Boolean {
return this is Bar
}
You will get compile time error, if you apply the isBar
function and use the value as Bar. Something like this :
if (fooclass.isBar()) {
fooclass.doSomethingElse()
}
This is where the fun begins actually. By telling the compiler that the function will return true when the type is actually Bar
, we can use the value as bar
in the if statement.
fun Baz.isBar(): Boolean {
contract {
returns(true) implies (this@isBar is Bar)
}
return this is Bar
}
Additionally, a function that uses a contract does not necessarily need to return a value. If a contract implies that the type is Bar
, the compiler will assume that after the function is called, the type will be Bar
.
fun onlyContract(value : Any?) {
contract {
returns() implies (value != null)
}
}
By using this function above, you can get rid of the nullability issue which prevents the code to be compiler by the compiler.
val nullValue : String? = null
onlyContract(nullValue)
println(nullValue.length) // no problem for compiler
Note
It’s important to note that even though you specify something with a contract, you can still return anything. The compiler trusts you with the contracts. This means that if you specify that a function will return a non-null value, you can still return null, and the compiler will not complain. So make sure that you hold your promise, especially for contracts placed in a public function.
Conclusion
Kotlin contracts are an experimental feature of Kotlin. They may change in the future or even be removed. However, they are already used in the Kotlin standard library and have been available since version 1.3.60 of Kotlin.So, it seems that Kotlin contracts are here to stay.
Love you all.
Stay tune for upcoming blogs.
Take care.