Kotlin standard library scope functions
The Kotlin language standard library comes with numerous very useful functions. Probably the most often used ones are the scope functions let
, with
, run
, also
, apply
. In this post we will focus only on a common use case for the function let
. For further information about the others please refer to the official library documentation available here: Kotlin standard library
Let’s use let
for nullability checks
This is by far one of the most common uses I have observed in code. I assume that it is partially due to the fact that many mobile developers have tried the Swift language where optional types are in the type system just as nullable types are for Kotlin. Moreover, the keyword let
also exists in Swift, and is commonly used to safely unwrap optional types and use them if they are not null.
However, the let
in Kotlin is not a keyword but a higher order function, which brings some benefits but also hidden traps if you use it in a naive way. Let’s dive into an example.
Simple example of nullability handling
Let’s assume we have a weather report about current temperature. However, the temperature could also be null if for example our sensors have not delivered any data yet.
var temperature: Int? = 20
temperature?.let {
storeInDb(it)
}
This is a perfectly valid use case for a simple let function usage. We use it all the time and it is totally fine. However, the problems start if you actually need to also perform and action in case when the variable has value null
. Normally in Kotlin, when handling nullability we are used to using the Elvis operator ?:
. Let’s just naively apply this practice to this use case as well. We’ll end up with the following code:
var temperature: Int? = 20
temperature?.let {
storeInDb(it)
} ?: {
clearStoredValueFromDb()
}
Now, at first glance this code looks just fine and logical. It should work, right? Unfortunately not always.
The issue with using let
and ?:
for control flow
Our control flow logic is not anymore binary. It all depends down on what the storeInDb
is returning… Let me explain. To understand what goes wrong, we need to take a closer look at what the let
function is actually doing.
inline fun <T, R> T.let(block: (T) -> R): R
So, the let
function returns the value, which the passed lambda function return. We know that lambda functions in Kotlin normally have the return value of their last expression. So in our case, this will be the return value of the storeInDb
function. The problem is, in the naive and quick fix we have done, it is not obvious what this value is. Therefore we need to look at the source code of the function storeInDb
to figure out what is going on.
fun storeInDb(temperature: Int): Error? {
try {
db.store("temp", temprarature)
return null
} catch (e: Exception) {
return Error("Could not store in the DB.")
}
}
So I think by now it would be clear that the addition we did actually created a bug. You see, the storeInDb
has a return type, which is an optional Error. When it fails to store the value, it returns an error with a message. However, when it successfully stores the value, it returns null. So how will this affect our initial code? Well let’s put this all together and see the execution step by step.
fun storeInDb(temperature: Int): Error? {
try {
db.store("temp", temprarature) // 4. The store is successful
return null // 5. Returning null, since no error happened
} catch (e: Exception) {
return Error("Could not store in the DB.")
}
}
var temperature: Int? = 20 // 1. The value we have is not null
temperature?.let { // 2. Since the value is not null, we invoke the lambda in the let function
storeInDb(it) // 3. We call the store function. 6. The store function returns `null`
} ?: {
// 7. Since the whole previous block returned a null value, we execute the operation inside the elvis block
clearStoredValueFromDb() // 8. We delete the value we have just stored.
}
The solution
It’s simple. Just don’t use let
function and ?:
operators for control flow decisions. Just use plain old if else with a temp local variable to allow for smart casing to work in Kotlin, like this:
var temperature: Int? = 20
val temp = temperature
if (temp != null) {
storeInDb(temp)
} else {
clearStoredValueFromDb()
}
It doesn’t look any more cluttered than before, but you don’t risk having weird bugs.
The moral of the story
Be mindful when using Kotlin’s amazing language features and rich standard library. Do not fall for the trap for fancy looking or minimalistic code, if it makes the code non-readable or prone to errors.