« Using Kotlin’s Contract APIs for Smarter Helper Functions
The Problem With Our Existing Functions
The following snippet represents a few examples of the kinds of helper functions we had in an old file.
1val Any?.isNull: Boolean get() = this == null2val Any?.isNotNull: Boolean get() = !this.isNull3val Any?.isNullOrEmpty: Boolean4 get() =5 when (this) {6 null -> true7 is Collection<*> -> this.isEmpty()8 is CharSequence -> this.isEmpty()9 else -> false10 }1112val Any?.isNotNullOrEmpty: Boolean get() = !this.isNullOrEmpty
While it’s pretty clear what these helpers are tying to do, there are some limitations with these implementations that maybe aren’t readily apparent until we actually use them.
1fun firstExample() {2 val name: String? = null3 if (name.isNull) return4 Log.i("anisham", "name is ${name?.length} characters long")5 //compiler error6 //Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?s7}
In this snippet, we return if name is null, but after this check, the compiler still can’t guarantee that name is non-null. No smartcasting kicks in to help us.
1fun secondExample() {2 val names: List<String>? = null3 if (names.isNullOrEmpty) return45 Log.i("anisham", "names is ${names?.size} characters long")6 // compiler error7 //Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type List<String>?8}
Even though we’ve already checked that the names variable is not null, the smartcast fails.
1fun thirdExample() {2 val names: List<String>? = null3 if (names.isNotNullOrEmpty) {4 Log.i("anisham", "names is ${names?.size} characters long")5 //compiler error6 //Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type List<String>?7 }8}
And again, smartcasting doesn’t work after the usage of our isNotNullOrEmpty helper extension.
In all of these cases, our helpers fail to leverage the full power of the Kotlin compiler. Developers are then forced to perform additional null checks — even when we’ve already used our extensions.
How Can Kotlin’s Contract APIs Help?
If we review the ContractBuilder interface, the public APIs direct us to what kinds of contract effects we can define
callsInPlace()
— Specifies that the function parameter lambda is invoked in place.returns()
— Specifies that a function will either return successfully or return with a specific valuereturnsNotNull()
— Specifies that a function will return normally with any value that is not null
Updating Our Helper Functions to Leverage Kotlin Contracts
1@OptIn(ExperimentalContracts::class)2fun Any?.isNull(): Boolean {3 contract {4 returns(false) implies (this@isNull != null)5 }6 return this == null7}89@OptIn(ExperimentalContracts::class)10fun Any?.isNullOrEmpty(): Boolean {11 contract {12 returns(false) implies (this@isNullOrEmpty != null)13 }14 return when (this) {15 null -> true16 is Collection<*> -> this.isEmpty()17 is CharSequence -> this.isEmpty()18 else -> false19 }20}2122@OptIn(ExperimentalContracts::class)23fun Any?.isNotNullOrEmpty(): Boolean {24 contract {25 returns(true) implies (this@isNotNullOrEmpty != null)26 }27 return !this.isNullOrEmpty()28}
The first thing we must do is convert it from an extension property to an extension method. This is because contracts cannot be applied to a property — only to functions and methods.
We make a call to the contract()
function as the first line in our method body. Within the lambda passed to contract() we are specifying two things:
- a return value constraint
- what that return value implies
In this example, if isNull()
returns false, it implies that the receiver object is not null. With that contract in place, if isNull()
returns false, subsequent usage of the receiver will not require null-safe accessors.
Contracts can only be understood by the compiler when applied in a called method.
1fun firstExample() {2 val name: String? = null3 if (name.isNull()) return4 Log.i("anisham", "name is ${name.length} characters long")5}67fun secondExample() {8 val names: List<String>? = null9 if (names.isNullOrEmpty()) return1011 println("")12 Log.i("anisham", "names is ${names.size} characters long")13}1415fun thirdExample() {16 val names: List<String>? = null17 if (names.isNotNullOrEmpty()) {18 Log.i("anisham", "names is ${names.size} characters long")19 }2021}
After adding the contract to the method, our smartcast succeeds — eliminating the need for us to perform additional null checks after the call
Examples From The Standard Library
You may already be using contracts without knowing it. They are being added to functions from the Kotlin Standard Library to help improve the type inference with each release.
requireNotNull()
1fun main() {2 val name: String? = null3 requireNotNull(name)45 println("name is ${name.length} characters long")6}
The requireNotNull()
function will throw an exception if the passed name
variable is null
. After the function returns, we can reference name as if it was a non-null type.
1public inline fun <T : Any> requireNotNull(value: T?): T {2 contract {3 returns() implies (value != null)4 }5 return requireNotNull(value) { "Required value was null." }6}
isNullOrEmpty()
1/**2 * Returns `true` if this nullable char sequence is either `null` or empty.3 *4 * @sample samples.text.Strings.stringIsNullOrEmpty5 */6@kotlin.internal.InlineOnly7public inline fun CharSequence?.isNullOrEmpty(): Boolean {8 contract {9 returns(false) implies (this@isNullOrEmpty != null)10 }1112 return this == null || this.length == 013}14/**15 * Returns `true` if this nullable collection is either null or empty.16 * @sample samples.collections.Collections.Collections.collectionIsNullOrEmpty17 */18@SinceKotlin("1.3")19@kotlin.internal.InlineOnly20public inline fun <T> Collection<T>?.isNullOrEmpty(): Boolean {21 contract {22 returns(false) implies (this@isNullOrEmpty != null)23 }2425 return this == null || this.isEmpty()26}
Opting-In to the Contracts Feature
- You can add the
@ExperimentalContracts
annotation to your functions that use a contract. If that annotation is used, then any callers of the annotated function will need to themselves opt-in to the Contracts feature.
1@ExperimentalContracts2fun <T> Collection<T>?.isNotNullOrEmpty(): Boolean { ... }34@ExperimentalContracts5fun main() {6 listOf("").isNotNullOrEmpty()7}
- Additionally, you can use the
@OptIn(ExperimentalContracts::class)
annotation to opt-in. If this method is used, then callers of the annotated function will not need to be annotated. However, using the@OptIn
annotation also requires the following compiler argument be used-Xopt-in=kotlin.RequiresOptIn
.
1@OptIn(ExperimentalContracts::class)2fun <T> Collection<T>?.isNotNullOrEmpty(): Boolean { ... }34fun main() {5 listOf("").isNotNullOrEmpty()6}
For full code please visit: