More on Null Safety

In my previous post about null safety, I wrote a bit about what it is and why it is such a good thing that Kotlin has a clever way to deal with that by verifying all your assignments to make sure you don’t accidentally assign null values to variables that are not marked as nullable.

Since Kotlin makes this distinction between nullable and non-nullable types, the compiler is aware of the possible tricky (not to say dangerous) situations that might come up when you ask a variable defined as nullable to do something when it might actually not even exist.

So, to protect the code against this, it’s not possible to call a function on a value defined as nullable until you accept the fact that you are responsible for this less-than-ideal situation. With great power…

Back to our Lord of the Rings game, let’s say you are playing as Frodo and he’s on his journey to destroy the ring. In this very (VERY) short version of the saga Frodo has access to a Quest Log where his missions will be displayed based on his level.

Consider the elements in the code below:

The game has two variables to store the player name and level, there’s a quest log where the player can go to check for the next quest. This is done by using the function checkQuestLog() which displays which quest the player should solve and this function calls another function obtainQuest()that determines which quest will be displayed in the quest log based on the player level.

There are also two other factors that will impact the quest that will be displayed: if Frodo has the ring and if he has met Aragorn (he’s too dumb to go on alone).

const val CHAR_NAME = "Frodo"
var charLevel = 1

fun main() {
    println("$CHAR_NAME departs on his journey.. ")
    println("What level is $CHAR_NAME?")
    val charLevelInput = readLine()!!
    
    println("$CHAR_NAME's level is $charLevel.")

    checkQuestLog()

    println("Time passes...")
    println("$CHAR_NAME returns from his quest.")

    charLevel += 1
    println(charLevel)
    checkQuestLog()
}

private fun checkQuestLog() {
    val quest: String? = obtainQuest(charLevel)
    println(
        """
$CHAR_NAME checks the quest log. It reads:
"$quest"
            """
    )
}
private fun obtainQuest(
    playerLevel: Int,
    hasTheRing: Boolean = true,
    hasFoundAragorn: Boolean = true
): String? = when (playerLevel) {
    1 -> "Meet Samwise Gamgee and leave the Shire"
    in 2..4 -> {
        val canGoToRivendell = hasFoundAragorn && hasTheRing

        if (canGoToRivendell) {
            "Follow Aragorn to Rivendell"
        } else {
            "Go back to the The Prancing Pony"
        }
    }
    5 -> "Help Aragorn defeat the Nazgûl"
    6 -> "Defeat Laracna"
    7 -> "Head to Mount Doom"
    8 -> "Destroy the ring in the fire of Mount Doom and defeat Sauron"
    else -> null
}

A few things to consider:

  • The Char level variable is initialized to 0 (zero)
  • The obtainQuest function has quests if the player is between level 1 and 8.
  • When prompted with the question about the Char level, if the player input is different than 1 – 8 (whole numbers) the function returns null

Because obtainQuest returns null you need to tell the compiler the string quest can be nullable. If you don’t, the IDE will complain:

So we need to set both the return type of the obtainQuest function to String? and the type of the variable quest to String? as well.

Now, when Frodo gets to level 8 he receives a quest to destroy the Ring and defeat Sauron.

What some people might not know (or remember) is that Sauron’s name should not be said (YES, this is actually from LOTR and J.K.Rolling just copied it).

In order to prevent this, we can create a new variable called restrictedQuest where the name Sauron will be replaced by “him who we do not name.”

If we create the variable
val restrictedQuest = quest.replace("Sauron", "him who we do not name.")
the IDE will return an error:

Only safe (?.) or non-null asserted (!!.) calls
are allowed on a nullable receiver of type String?

So, how do you call a function on a nullable variable, you ask? There are a few options.

The first option would be to ask yourself if you really need that value to be null. In this case, we don’t. We can simply say something like else “There are no quests for you at the moment”

BUT, for the sake and the purpose of this post, let’s pretend we do.

The second option would be to check the nullability with an if statement. So you could have something like this:

private fun checkQuestLog() {
    val quest: String? = obtainQuest(charLevel)
    if (quest != null) {
        val restrictedQuest = quest.replace("Sauron", "him who we do not name.")
        println(
            """
$CHAR_NAME checks the quest log. It reads:
"$restrictedQuest"
            """
        )
    }
}

Pretty simple and pretty intuitive. If you run the code and enter level 8 when prompted to input Frodo’s level it will run, replace Sauron’s name, increase Frodo’s level to 9 and print nothing else.

Output:

Frodo departs on his journey.. 
What level is Frodo?
8
Frodo's level is 8.

Frodo checks the quest log. It reads:
"Destroy the ring in the fire of Mount Doom and defeat him who we do not name."
            
Time passes...
Frodo returns from his quest.
9

Process finished with exit code 0

Hovering the mouse over the quest variable you can see another Kotlin feature is used here. It’s called Smart Casting. When you check a type of a variable using an if statement Kotlin automatically casts it to the type inside the if branch. If you verify that a property is not null, Kotlin will automatically cast it to the corresponding non-null type.

But, (there’s always a but) smart casting won’t work in every scenario. If you are using file-mutable level variables, for example, it’s not safe for the compiler to perform a smart cast because the value of the variable can change between the check and the casting.

So, what’s the third option?

A safe call operator 😁. Remember this ⬇️ ?

On top of the Smart Casting issue mentioned above, if statements work well in the case of this game, but it’s not the best option in general. Imagine if you have chained functions and each one calls a nullable value and you need to check all of them using an if statement. It can get out of hand and convoluted pretty fast. So, you can use the safe call operator, or ?. to safely make function calls on nullable objects.

When the compiler finds the safe call operator it knows it should check for a null value and at runtime, if a safe call operator is called on a null value, the program will skip it and won’t evaluate it, returning null.

So, in this case, if quest is non-null, a restricted version is returned. If quest is null, the function replace is not called.

What if you need to do more than simply calling one function on a nullable type? What if you need to add a new variable? Safe calls only allow you to call one single function on a nullable type, but that’s where option number four comes into play.

You can use let combined with the safe call operator. Let is a scope function (more about that on a different post) that can be added to any value and creates a new scope with access to that value it’s being called on, and then you can execute code.

The change made to the code below is, again, not needed in this case, but serves a purpose:

private fun checkQuestLog() {
    val quest: String? = obtainQuest(charLevel)
    val message: String? = quest?.replace("Sauron", "him who we do not name.")
        ?.let { restrictedQuest ->
            """
            $CHAR_NAME checks the quest log. It reads:
            "$restrictedQuest"
            """
    }
    println(message)
}

We are defining message as a nullable variable and assign its value to the (safe) call on let on the quest variable after using replace. So, in the code above, when quest is not null and let is invoked, the code inside the curly braces after let is executed. There we defined a new variable called restrictedQuest that will have the value of quest?.replace("Sauron", "him who we do not name." And if quest is null, all will be skipped.

Option number five is the null coalescing operator also known as The Elvis operator. If we run the code and input Frodo’s level as 12 we will get null as the response from the quest log, since there are no more quests after level 8 and we determined anything other than levels 1-8 will return null. Since we have addressed it with the let function, it will not crash, but return null. Although it’s great that the app won’t charsh, reading null in the console is not very nice.

Frodo departs on his journey.. 
What level is Frodo?
8
Frodo's level is 8.

Frodo checks the quest log. It reads:
"Destroy the ring in the fire of Mount Doom and defeat him who we do not name."
            
Time passes...
Frodo returns from his quest.
9
null

Process finished with exit code 0

We can do better. We can make sure that instead of null, when there are no quests available, Frodo gets an actual message saying something like “There are no quests for you at the moment”. We could again do that with an if/else statement, but just like before, not the best solution for every case. Kotlin has yet another feature if we want to use a fallback value when null is returned. It’s called the null coalescing operator or the Elvis operator ?: It’s called like this because it resembles Elvis’ pompadour hairstyle (or quiff 😋). It basically says: if whatever is on the lefthand side of me is null, do whatever is on the righthand side.

And when you run the code:

Frodo departs on his journey.. 
What level is Frodo?
8
Frodo's level is 8.

Frodo checks the quest log. It reads:
"Destroy the ring in the fire of Mount Doom and            defeat him who we do not name."
            
Time passes...
Frodo returns from his quest.
9
There are no quests for you at the moment

Process finished with exit code 0

📌 The Elvis operator does need to be used associated with a let function.

Option number… (I lost count but I think it’s six) six is the non-null assertion operator (!!). It can be used to force the compiler to allow you to call a function on a nullable type. This, however, should be used with extreme caution or not used at all. It’s very dramatic and sounds almost like an ultimatum. It basically says to the compiler “either run this operation or don’t bother running anything else in my code”.

We use it when we call the readLine() function:

val charLevelInput = readLine()!!.toInt()

In this case, we are saying it doesn’t matter if readLine( ) returns null. Turn it into an Int anyways. And if it returns null we get a NullPointerException error

To avoid this we can wrap the charLevel variable in an if statement to guarantee two things:

First that a null value is not entered, so we take care of it.
Second, that an actual Int is entered

const val CHAR_NAME = "Frodo"
var charLevel = 1

fun main() {
    println("$CHAR_NAME departs on his journey.. ")
    println("What level is $CHAR_NAME?")
    val charLevelInput = readLine()!!
    charLevel = if (charLevelInput.matches("""\d+""".toRegex())) {
        charLevelInput.toInt()
    } else {
        1
    }

So here we basically that if the input provided from the player does not match a number using 0-9 (using this weird expression here matches("""\d+""".toRegex())) ), the level will be set to 1.

And with that, we avoid the NullPointerException error. But much like Sauron’s name, this should be avoided.

Published by Rosie M

Android bug creator and full-time nerd. Joined Automattic in 2017. Passionate about music, books, games, photography and Doctor Who. Open-source enthusiast, remote-work advocate and Globetrotter.

One thought on “More on Null Safety

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: