The Kotlin type system is fairly flexible but there are a few corner cases where the compiler enforces rules which can be a little restrictive. For example, we can't declare a val
then initialise it within a lambda as the compiler does not know how the lambda will be executed and therefore cannot guarantee that the val will not be reassigned.
val name: String
synchronized(this) {
name = "Instil" // 1.
}
print(name) // 2.
- Compilation error as the compiler cannot determine that
name
will only be assigned once. - Compilation error as
name
has not be assigned.
Another source of minor frustration is that while we can use smart casts to avoid explicit casting or when dealing with nullable types, we can't use standard library functions such as isNullOrEmpty()
with smart casts. Consider the following two examples of the same function, the latter is not able to use a smart cast.
fun capitalize(text: String?): String {
if (text != null) {
return text.capitalize() // 1.
}
return ""
}
fun capitalize(text: String?): String {
if (!text.isNullOrEmpty()) {
return text.capitalize() // 2.
}
return ""
}
- Compiles successfully.
- Compilation error as while
isNullOrEmpty()
performs a null check, the compiler does not have enough information to guarantee thattext
is not null.
Contracts
With the Contracts feature in Kotlin 1.3, a new DSL has been introduced which allows us to provide this extra information to the compiler. As of Kotlin 1.3-M2, we can provide information about how a function may return or how lambda arguments are executed which solves the two problems outlined above. At a high level, the Contracts DSL consists of a builder function contract
to which we pass a lambda argument defining the function behaviours or effects, some of which we'll cover below. JetBrains have also included contracts in a lot of existing standard library functions such as synchronized
or isNullOrEmpty
which we referenced earlier meaning our previous examples will now compile in Kotlin 1.3.
Calls In Place
If we look at the source for the updated synchronized
function, we can now see that the function contains a contract. The following contract guarantees that the synchronized
function will only execute the block
argument once within the scope of this function and it will not be passed to any other functions.
public actual inline fun <R> synchronized(lock: Any, block: () -> R): R {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
...
}
Returns
Now if we look at the source for the updated isNullOrEmpty()
function, we can see it also contains a contract. This time the contract specifies that the function will return false if the character sequence is not null and this allows us to use isNullOrEmpty()
with smart casts! The implies
function defines the condition of return and accepts a boolean value but is limited to null checks, type checks and logic operators.
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}
return this == null || this.length == 0
}
Conclusion
While still an experimental feature, contracts look extremely promising and we're looking forward to the release of Kotlin 1.3 to start making use of contracts in our own types. If you'd like to read up more on the background of the contracts feature, see this KEEP for more information.