Compiler Hinting with Kotlin Contracts

Kotlin’s built-in null-safety makes it easy for us to write code that’s less error-prone and less susceptible to the unwanted and unexpected NullpointerException at runtime. We can do a lot to make sure it doesn’t happen, but sometimes there’s no way around dealing with possible null-values. Kotlin is introducing Contracts in the upcoming 1.3 release (currently m2) in which we can use contracts to tell the compiler how a function behaves and what results are implied, so we can benefit from improved smart-casts.

Let’s assume we have some code describing a User. The implementation is a data class with two properties and a declared method that will return whether or not this instance is completely valid (in this case the name and email should be non-null and adhere to some simple rules). Let’s assume we do not know where and how this instance gets constructed and need to allow null values in construction.

Let’s write some code that uses the class above. We’ll assume that on the use site the user can again be null, because hey, it happens.

For this code to compile, we’ll need to apply some changes, we need to add an explicit null-check and a call to the isValid() function before we can successfully print the contents. We could also move this logic to an extension function like so:

This useful extension function now allows us to deal with the user being null, and also checks the validity for the user instance if it is non-null. We can now safely call it on any supplied parameter. We can now change our printUserDetails to this:

We’ve removed the null-check, but now there’s another issue, the compiler can no longer determine whether or not the user instance is null or not, and won’t smart cast to User but User?. The compiler will also complain about the unsafe usages of user in the println statement because of this. To solve this, we can add the null-safe assert (!!) or null-safe operator (?.) but this is unnecessary as we know from the implementation of isComplete() that within the if statement the user can never be null. What if we could remove this noise?

Returns and Implies

Starting with Kotlin 1.3, we can start defining contracts for our functions. In the example above, we know from the implementation of isComplete() that when the result of the call is ‘true‘, the user instance it was called on is not null. We’ll define a contract for this function so the compiler can determine the contract of the function and smart cast the user to a non-null equivalent in the if-body. We can use returns/implies for this.

Two things stand out, because of the experimental nature of the contracts in kotlin 1.3-m2 we need to annotate the method with @ExperimentalContracts. The first statement in the function now needs to be the contract definition, which in this case consists of two parts, a returns specification which specifies a return value for the contract and the implied result for the function. These are part of the newly introduced contract DSL introduced in this Kotlin KEEP.

When the function defining the contract now returns ‘true‘, this implies that the receiver on which the function was called (User) is not null. The contract is in turn interpreted by the compiler, which will allow a smart-cast to User instead of User? at the use site. We can now remove the null-safe/null-assert operators when accessing properties and functions on the User instance.

Higher Order Functions

There’s also support for contracts in higher order functions. Consider the following function, (that is part of the standard library for Kotlin).

Imagine a situation where you’d want to measure the time spent executing a function, but also want to capture something that’s being executed in the function itself. Here we just assign a fixed value to a String, but it’s not unimaginable to be calling a REST endpoint that returns a value while at the same time measuring the time it takes to execute. After execution of the measureTimeMillis, we want to print the resulting value of the measurement, along with the new value for the assign variable.

The Kotlin compiler will complain with regard to the println, as it cannot guarantee that assign was assigned a value inside the lambda — even though we know this is the case — so we’ll apply a null-safe assert to appease the compiler. We can remedy the need for this by adding a contract for the measureTimeMillis() function. Here’s what it looks like with a contract:

Adding this contract will hint to the compiler that we guarantee that the code block inside the lambda is executed exactly once, by adding the callsInPlace declaration. Now the compiler is aware that the value will be initialized — we guarantee the body() is executed exactly once — so we can omit the null-safe assert at the call site.

Wrapping up

When looking at the projects I’ve done in Kotlin, I can see benefit in specifying contracts for the functions I define. Supplying the compiler with contracts for applicable functions can help clean up code on the call site and remove redundant null-checks as well as further clarify your intentions for your functions. I’m very keen to see how this functionality is going to evolve in future Kotlin releases.

If you want to give this a spin, details on using Kotlin 1.3-M2 are here.

Did you enjoy this content? We have got more on the way! Sign up here for regular updates!

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *