Kotlin Contract DSL
Kotlin's Contract DSL (Domain Specific Language) is an advanced feature that allows developers to provide hints to the compiler about the behavior of functions. These "contracts" help the compiler make smarter decisions during static analysis, leading to better type inference, null safety checks, and overall code optimization.
Introduction to Contracts
When you write code in Kotlin, the compiler analyzes your code to ensure type safety and identify potential issues. However, there are scenarios where the compiler cannot infer certain behaviors or guarantees without additional information. This is where contracts come in.
Contracts allow you to explicitly tell the compiler about:
- Function invocation effects
- Return value conditions
- Relationships between function parameters and results
Contracts are still an experimental feature in Kotlin, so you'll need to opt-in to use them.
Setting Up for Contracts
Before using contracts, you need to enable experimental features:
@OptIn(ExperimentalContracts::class)
fun myFunctionWithContracts() {
// Function using contracts
}
Or at file level:
@file:OptIn(ExperimentalContracts::class)
package com.example.myapp
Basic Contract Syntax
The basic structure of a contract looks like this:
import kotlin.contracts.*
@OptIn(ExperimentalContracts::class)
fun myFunction(param: Any?) {
contract {
// Contract statements go here
}
// The rest of the function implementation
}
Common Contract Types
1. Returns Contracts
Returns contracts specify conditions about when a function returns normally:
@OptIn(ExperimentalContracts::class)
fun String?.isNotNull(): Boolean {
contract {
returns(true) implies (this@isNotNull != null)
}
return this != null
}
// Usage
fun processString(str: String?) {
if (str.isNotNull()) {
// The compiler now knows str is not null
println(str.length) // No need for str!! or str?.length
}
}
In this example, the contract tells the compiler that if isNotNull()
returns true
, then this
is not null, allowing for smart casts.
2. CallsInPlace Contracts
This type of contract informs the compiler how many times a lambda parameter will be invoked:
@OptIn(ExperimentalContracts::class)
inline fun executeOnce(block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
}
fun example() {
var x: String
executeOnce {
x = "Hello" // Compiler knows this is assigned exactly once
}
println(x) // No "variable might not have been initialized" error
}
The InvocationKind
enum has four values:
EXACTLY_ONCE
: The lambda is called exactly onceAT_MOST_ONCE
: The lambda is called zero or one timesAT_LEAST_ONCE
: The lambda is called one or more timesUNKNOWN
: No guarantee about invocation (default)
Real-World Applications
Example 1: Improved Smart Casting
Contracts are particularly useful for custom utility functions where you want to maintain smart casting:
@OptIn(ExperimentalContracts::class)
fun ensureNotNull(value: Any?): Boolean {
contract {
returns(true) implies (value != null)
}
return value != null
}
fun processData(data: String?) {
if (ensureNotNull(data)) {
// Without contracts, the compiler wouldn't know data is non-null here
println("Data length: ${data.length}")
} else {
println("No data provided")
}
}
Example 2: Resource Management
Contracts can help with ensuring proper resource management:
@OptIn(ExperimentalContracts::class)
inline fun <T : AutoCloseable, R> T.use(block: (T) -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
when {
exception == null -> close()
else -> try {
close()
} catch (closeException: Throwable) {
// Suppressed exception handling
}
}
}
}
// Usage
fun readFirstLine(filePath: String): String {
val reader = java.io.BufferedReader(java.io.FileReader(filePath))
reader.use { r ->
return r.readLine() ?: ""
}
// Compiler knows the lambda is called exactly once and the resource is properly closed
}
Example 3: Custom Control Flow Functions
You can create your own control flow functions with contracts:
@OptIn(ExperimentalContracts::class)
inline fun runIf(condition: Boolean, block: () -> Unit) {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
if (condition) {
block()
}
}
fun demo() {
var message: String? = null
runIf(true) {
message = "Hello, Contracts!"
}
// Without contracts, the compiler wouldn't know message might be initialized
println(message?.length ?: 0)
}
Advanced Contract Usage
Combining Multiple Contract Clauses
You can combine multiple contract statements for more complex behaviors:
@OptIn(ExperimentalContracts::class)
fun validate(value: String?, process: (String) -> Unit): Boolean {
contract {
returns(true) implies (value != null)
callsInPlace(process, InvocationKind.AT_MOST_ONCE)
}
if (value != null) {
process(value)
return true
}
return false
}
Limitations of Contracts
Contracts have some important limitations to keep in mind:
- Contracts must be at the very beginning of a function
- The condition in a contract must be directly related to function parameters or the receiver
- Contracts are still experimental and might change in future Kotlin versions
- Function bodies must match the contract statements (the compiler doesn't verify this)
When to Use Contracts
Use contracts when:
- You're creating utility functions that perform checks (like null checks)
- You need to preserve smart casting in custom control flow functions
- You're building a DSL or library where type inference is important
- You're working with lambda callbacks where execution guarantees are needed
Avoid overusing contracts for simple functions where they don't provide significant benefits.
Summary
Kotlin's Contract DSL provides a powerful way to give the compiler additional information about your code's behavior. By using contracts, you can:
- Improve smart casting for custom functions
- Ensure proper lambda invocation
- Create more expressive and type-safe APIs
- Help the compiler optimize your code
While contracts are still experimental, they offer significant advantages for library developers and those building complex applications where type safety and static analysis are critical.
Additional Resources
- Kotlin Official Documentation on Contracts
- Kotlin Contract KDoc
- KotlinConf: Deep Dive into Contracts
Exercises
- Create a custom
requireNotNull
function with contracts that improves on the standard library version. - Implement a
runAtMostOnce
function that guarantees a lambda is executed no more than once. - Write a contract for a function that checks if a collection is not empty and allows smart casting for the collection.
- Create a simple resource management utility with contracts that ensures proper resource cleanup.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)