article banner (priority)

Effective Kotlin Item 5: Specify your expectations for arguments and state

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

When you have expectations, declare them as soon as possible. In Kotlin, we mainly do this using:

  • require block - a universal way to specify expectations for arguments.
  • check block - a universal way to specify expectations for state.
  • error function - a universal way to signal that application reached an unexpected state.
  • The Elvis operator with return or throw.

Here is an example that uses these mechanisms:

// Part of Stack<T> fun pop(num: Int = 1): List<T> { require(num <= size) { "Cannot remove more elements than current size" } check(isOpen) { "Cannot pop from closed stack" } val ret = collection.take(num) collection = collection.drop(num) return ret }

Specifying expectations this way does not free us from the necessity of specifying them in documentation, but it is really helpful anyway. Such declarative checks have many advantages:

  • Expectations are visible even to programmers who have not read the documentation.
  • If they are not satisfied, a function throws an exception instead of leading to unexpected behavior. It is important that these exceptions are thrown before the state is modified because this means we don’t have a situation where only some modifications are applied and others are not. Such situations are dangerous and hard to managepokemon. Thanks to assertive checks, errors are harder to miss and our state is more stable.
  • Code is self-checking to some degree. There is less need for unit-testing when these conditions are checked in the code.
  • All checks listed above work with smart casting, therefore less casting is required.

Let’s talk about different kinds of checks and why we need them. Starting from the most popular one: the arguments check.

Arguments

When you define a function with arguments, it is not uncommon for these arguments to have some expectations on them that cannot be expressed using the type system. Just take a look at a few examples:

  • When you calculate the factorial of a number, you might require this number to be a positive integer.
  • When you look for clusters, you might require a list of points to not be empty.
  • When you send an email, you might require that the email address is valid.

The most universal and direct way to state these requirements in Kotlin is by using the require function, which checks this requirement and throws IllegalArgumentException if it is not satisfied:

fun factorial(n: Int): Long { require(n >= 0) return if (n <= 1) 1 else factorial(n - 1) * n } fun findClusters(points: List<Point>): List<Cluster> { require(points.isNotEmpty()) //... } fun sendEmail(user: User, message: String) { requireNotNull(user.email) require(isValidEmail(user.email)) //... }

Notice that these requirements are highly visible because they are declared at the very beginning of functions, therefore they are clear for a user who is reading these functions (but requirements should also be stated in documentation because not everyone reads function bodies).

These expectations cannot be ignored because the require function throws an exception when the predicate is not satisfied. When such a block is placed at the beginning of a function, we know that if an argument is incorrect, the function will stop immediately and the user won’t miss the fact that they are using this function incorrectly. An exception will be clear, unlike a potentially strange result that might propagate a long way until it fails. In other words, when we properly specify our expectations for arguments at the beginning of a function, we can then assume that these expectations will be satisfied.

We can also specify a lazy message for this exception in a lambda expression after the call:

fun factorial(n: Int): Long { require(n >= 0) { "Cannot calculate factorial of $n " + "because it is smaller than 0" } return if (n <= 1) 1 else factorial(n - 1) * n }

I often see require inside init block of data classes. It is used to make sure that constructor arguments are correct, by making it impossible to create invalid instances according to the requirements.

data class User( val name: String, val email: String ) { init { require(name.isNotEmpty()) require(isValidEmail(email)) } }

The require function is used when we specify requirements for arguments. Another common case is when we have expectations of the current state; in such a case, we can use the check function instead, which throws IllegalStateException.

State

It is not uncommon that we only allow some functions to be used in concrete conditions. A few common examples:

  • Some functions might need an object to be initialized first.
  • Some actions might be allowed only if the user is logged in.
  • Some functions might require an object to be open.

The standard way to check that these expectations of a state are satisfied is to use the check function:

fun speak(text: String) { check(isInitialized) //... } fun getUserInfo(): UserInfo { checkNotNull(token) //... } fun next(): T { check(isOpen) //... }

The check function works similarly to require, but it throws an IllegalStateException when the stated expectation is not met. It checks if a state is correct. An exception message can be customized using a lazy message, just like with require. When the expectation is on the whole function, we place it at the beginning, generally after the require blocks. However, some state expectations are local, and check can be used later.

We use such checks especially when we suspect that a user might break our contract and call a function when it should not be called. Instead of trusting that they won’t do that, it is better to check and throw an appropriate exception. We might also use explicit checks when we do not trust that our own implementation handles the state correctly. However, for such cases, when we are checking mainly for the sake of testing our own implementation, we have another function called assert.

Nullability and smart casting

Both require and check have Kotlin contracts that state that when a function returns, its predicate is true after this check.

public inline fun require(value: Boolean): Unit { contract { returns() implies value } require(value) { "Failed requirement." } }

Everything that is checked in these blocks will later be treated as true in the same function. This works well with smart casting because once we have checked that something is true, the compiler will treat it as something that is certain. In the example below, we require a person’s outfit to be a Dress. After that, assuming that the outfit property is final, it will be smart casted to Dress.

fun changeDress(person: Person) { require(person.outfit is Dress) val dress: Dress = person.outfit //... }

This characteristic is especially useful when we check if something is null:

class Person(val email: String?) fun sendEmail(person: Person, message: String) { require(person.email != null) val email: String = person.email //... }

For such cases, we even have special functions: requireNotNull and checkNotNull. They both have the capability to smart cast variables, and they can also be used as expressions to “unpack” variables:

class Person(val email: String?) fun validateEmail(email: String) { /*...*/ } fun sendEmail(person: Person, text: String) { val email = requireNotNull(person.email) validateEmail(email) //... } fun sendEmail(person: Person, text: String) { requireNotNull(person.email) validateEmail(person.email) //... }

The problems with the non-null assertion !!

Instead of using requireNotNull or checkNotNull we can use the non-null assertion !! operator. This is conceptually similar to what happens in Java: we think something is not null, and an NPE is thrown if we are wrong. The non-null assertion !! is a lazy option. It throws a generic NullPointerException exception that explains nothing. It is also short and simple, which makes it easy to abuse or misuse. The non-null assertion !! is often used in situations where a type is nullable but null is not expected. The problem is that even if this is not currently expected, it almost always can be in the future, and this operator only quietly hides the nullability.

A very simple example is a function that looks for the largest of 4 argumentsmaxof. Let’s say that we decided to implement it by packing all these arguments into a list and then using maxOrNull to find the biggest one. The problem is that this returns nullable because it returns null when the collection is empty. Only a developer who knows that this list cannot be empty will likely use a non-null assertion !!:

fun largestOf(a: Int, b: Int, c: Int, d: Int): Int = listOf(a, b, c, d).maxOrNull()!!

As was shown to me by Márton Braun, a reviewer of this book, a non-null assertion !! can even lead to an NPE in such a simple function. Someone might need to refactor this function to accept any number of arguments, but this person might forget that a collection cannot be empty if we want to use maxOrNull on it:

fun largestOf(vararg nums: Int): Int = nums.maxOrNull()!! largestOf() // NPE

In the example above, the information about nullability was silenced and can easily be missed when it might be important. It’s a similar situation with variables. Let’s say that you have a variable that needs to be set later but will surely be set before its first use. Setting it to null and using a non-null assertion !! is not a good option. It is annoying that we need to unpack these properties every time, and we also block the possibility of these properties actually having a meaningful null in the future:

class UserControllerTest { private var dao: UserDao? = null private var controller: UserController? = null @BeforeEach fun init() { dao = mockk() controller = UserController(dao!!) } @Test fun test() { controller!!.doSomething() } }

Nobody can predict how code will evolve in the future; so, if you use a non-null assertion !! or an explicit error throw, you should assume that it will throw an error one day. Exceptions are thrown to indicate something unexpected and incorrect (Item 7: Prefer a null or a sealed result class result when the lack of a result is possible). However, explicit errors say much more than generic NPEs and they should nearly always be preferred.

The rare cases in which the non-null assertion !! does make sense are mainly a result of interoperability between our code and libraries in which nullability is not expressed correctly. When you interact with an API that is properly designed for Kotlin, this shouldn’t be the norm.

In general, we should avoid using the non-null assertion !!. This suggestion is rather widely approved by our community; in fact, many teams have a policy to enforce it. Some set the Detekt static analyzer to throw an error whenever it is used. I think such an approach is too extreme, but I do agree that it is often a code smell. It seems that this operator’s appearance is no coincidence. !! seems to be screaming “Be careful” or “There is something wrong here”.

To avoid using !!, we should avoid meaningless nullability. In a case like the one presented above, we should use lateinit or Delegates.notNull. lateinit is good practice when we are sure that a property will be initialized before the first use. We deal with such a situation mainly when classes have a lifecycle, and we set properties in one of the first-invoked methods. For instance, when we set objects in onCreate in an Android Activity, viewDidAppear in an iOS UIViewController, or componentDidMount in a React React.Component.

class UserControllerTest { private lateinit var dao: UserDao private lateinit var controller: UserController @BeforeEach fun init() { dao = mockk() controller = UserController(dao!!) } @Test fun test() { controller.doSomething() } }

Know that you can always check if a lateinit property is initialized by referencing it, and using isInitialized property, so in the above example, I could check if dao is initialized by using ::dao.isInitialized.

Using Elvis operator

For nullability, it is also popular to use the Elvis operator with throw or return on the right side. Such a structure is highly readable and it gives us more flexibility in deciding what behavior we want to achieve. First of all, we can easily stop a function using return instead of throwing an error:

fun sendEmail(person: Person, text: String) { val email: String = person.email ?: return //... }

If we need to take more than one action if a property is incorrectly null, we can always add these actions by wrapping return or throw into the run function. Such a capability might be useful if we need to log why a function was stopped:

fun sendEmail(person: Person, text: String) { val email: String = person.email ?: run { log("Email not sent, no email address") return } //... }

The Elvis operator with return or throw is a popular and idiomatic way to specify what should happen in the case of variable nullability, and we should not hesitate to use it. Again, if possible, keep such checks at the beginning of the function to make them visible and clear.

error function

In Kotlin stdlib you can find error function, that is used to throw an IllegalStateException. It is often used to handle a situation that we do not expect to ever take place, like an unexpected value type.

// error implementation from Kotlin stdlib public inline fun error(message: Any): Nothing = throw IllegalStateException(message.toString()) // example usage fun handleMessage(message: Message) = when(message) { is TextMessage -> showTest(message.text) is ImageMessage -> showImage(message.image) else -> error("Unknown message type") }

Summary

Specify your expectations to:

  • Make them more visible.
  • Protect your application stability.
  • Protect your code correctness.
  • Smart cast variables.

The main mechanisms we use for this are:

  • require block - a universal way to specify expectations for arguments.
  • check block - a universal way to specify expectations for states.
    • error function - a universal way to signal that application reached an unexpected state.
  • The Elvis operator with return or throw.

We prefer avoiding not-null assertion !!, however, it is sometimes useful when we are sure that a variable is not null, but the compiler cannot infer it. One feature that helps us avoid !! is lateinit property initialization.

pokemon:

I remember how, in a Gameboy Pokemon game, one could copy a pokemon by making a transfer and disconnecting the cable at the right moment. After that, this pokemon was present on both Gameboys. Similar hacks worked in many games and they generally involved turning off the Gameboy at the correct moment. The general solution to all such problems is to make connected transactions atomic: either all occur or none occur. For instance, when we add money to one account and subtract it from another. Atomic transactions are supported by most databases.

maxof:

In the Kotlin Standard Library, this function is called maxOf, but it accepts any number of arguments.