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
orthrow
.
Here is an example that uses these mechanisms:
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:
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:
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.
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:
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.
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
.
This characteristic is especially useful when we check if something is null:
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:
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 !!
:
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:
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:
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
.
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:
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:
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.
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
orthrow
.
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.
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.
In the Kotlin Standard Library, this function is called maxOf
, but it accepts any number of arguments.