Mastering Kotlin Contracts: Unlocking Smarter Code Analysis
Kotlin never ceases to amaze with its features that combine elegance and power. One advanced yet often underutilized tool in Kotlin’s arsenal is Contracts. Contracts let you guide the Kotlin compiler to make smarter decisions about your code—resulting in better null safety, optimized performance, and fewer runtime errors.
What Are Kotlin Contracts?
Kotlin Contracts allow you to define rules about how your functions behave, helping the compiler perform advanced static analysis. They enable features like smart-casts and context-aware checks beyond Kotlin’s default capabilities.
Why Use Contracts?
- Improve Null Safety: Eliminate redundant null checks by telling the compiler when something is guaranteed to be non-null.
- Optimize Smart-Casts: Make the compiler aware of variable types in custom scenarios.
- Reduce Boilerplate: Write cleaner, more intuitive code by offloading repetitive checks to the compiler.
Examples of Kotlin Contracts in Action
1. Simplify Null Checks
Let’s create a custom utility to validate non-null values:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| @OptIn(ExperimentalContracts::class)
inline fun <T> requireNotNull(value: T?, message: String): T {
contract {
returns() implies (value != null)
}
if (value == null) {
throw IllegalArgumentException(message)
}
return value
}
fun processName(name: String?) {
val nonNullName = requireNotNull(name, "Name cannot be null")
// No need for additional null checks; compiler knows 'nonNullName' is not null!
println("Processing name: $nonNullName")
}
fun main() {
processName("John") // Works fine
// processName(null) // Throws an IllegalArgumentException
}
|
Something similar is implemented in the functions require
and requireNotNull
from the Kotlin standard lib:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| /**
* Throws an [IllegalArgumentException] with the result of calling [lazyMessage] if the [value] is false.
*
* @sample samples.misc.Preconditions.failRequireWithLazyMessage
*/
@kotlin.internal.InlineOnly
public inline fun require(value: Boolean, lazyMessage: () -> Any): Unit {
contract {
returns() implies value
}
if (!value) {
val message = lazyMessage()
throw IllegalArgumentException(message.toString())
}
}
/**
* Throws an [IllegalArgumentException] with the result of calling [lazyMessage] if the [value] is null. Otherwise
* returns the not null value.
*
* @sample samples.misc.Preconditions.failRequireNotNullWithLazyMessage
*/
@kotlin.internal.InlineOnly
public inline fun <T : Any> requireNotNull(value: T?, lazyMessage: () -> Any): T {
contract {
returns() implies (value != null)
}
if (value == null) {
val message = lazyMessage()
throw IllegalArgumentException(message.toString())
} else {
return value
}
}
|
How Contracts Help Here
- The
returns() implies (value != null)
contract tells the compiler:If the function returns successfully, then value
is guaranteed to be non-null.
- This enables smart-casts, so you don’t need manual null checks after the function call.
2. Custom Assertions
Here’s how contracts can be used to define custom assertion functions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| @OptIn(ExperimentalContracts::class)
fun assertValidState(condition: Boolean, message: String) {
contract {
returns() implies condition
}
if (!condition) {
throw IllegalStateException(message)
}
}
fun performOperation(state: Boolean) {
val state: Any? = "Hello"
assertValidState(state is String, "Is String")
// Here the compiler knows that the state val is of type String so no need to other cast checks
println("String length: ${assertion.length}")
}
fun main() {
performOperation(true) // Prints success
// performOperation(false) // Throws IllegalStateException
}
|
3. Smart-Casts with Custom Conditions
Let’s create a utility function that checks if a value matches a specific type. This will demonstrate how contracts enable smarter casting:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| @OptIn(ExperimentalContracts::class)
inline fun <reified T> isOfType(value: Any?): Boolean {
contract {
returns(true) implies (value is T)
}
return value is T
}
fun main() {
val input: Any? = "Hello, Kotlin!"
if (isOfType<String>(input)) {
println("String length: ${input.length}")
}
val inputInt: Any? = 10
if (isOfType<Int>(inputInt)) {
println("The value is an integer ${input.toUInt()}")
}
}
|
With this implementation, the compiler knows that within the if
block, input
is a String
, thanks to the contract defined in isOfType
. Also the compilers knows that inputInt
is an Int
so you don’t need to cast it.
4. Optimizing Flow Control
Contracts can simplify flow control by enabling the compiler to understand loop invariants or conditions. Here’s an example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| inline fun isNotEmpty(list: List<*>?): Boolean {
contract {
returns(true) implies (list != null && list.isNotEmpty())
}
return list != null && list.isNotEmpty()
}
fun processItems(items: List<String>?) {
if (isNotEmpty(items)) {
// Compiler knows items is non-null and not empty
println("Processing ${items.size} items")
} else {
println("No items to process")
}
}
fun main() {
processItems(listOf("A", "B", "C"))
processItems(null)
processItems(emptyList())
}
|
Output
Processing 3 items
No items to process
No items to process
When to Use Contracts
Contracts are ideal for:
- Library Development: Safeguard public APIs by enforcing preconditions.
- DSLs and Frameworks: Simplify type-checking and state validations in Kotlin DSLs.
- Performance Optimization: Reduce runtime checks by letting the compiler infer conditions at compile time.
Conclusion
Kotlin Contracts are a hidden gem that can elevate your code by improving safety, reducing boilerplate, and enabling smarter compiler analysis. Whether you’re building libraries, writing complex DSLs, or just optimizing everyday code, contracts provide a powerful tool to guide the Kotlin compiler and ensure code correctness.
Also keep in mind that contracts are annotated as experimental feature but they are in Kotlin since 1.3 version and are being used in the standard library so they are stable enough to use them.