A Simple, Real World Example of Nothing in Kotlin.

Nothing makes sense!

Jason McCormack
5 min readAug 31, 2020
Let’s make something from nothing.

Hi there! I want to share a pretty practical use of Kotlin’s Nothing type designation. If you are having trouble figuring out where Nothing is useful I hope the following example is a new way to see it in action in a tangible way.

Introducing Our Example

Our simple example will showcase the common use-case of parsing HTTP request parameters. Some parameters will be optional (nullable or missing), some parameters must be present (non-null), some must be present with content to parse (not blank), some must be present and alpha-numeric (like hex values), and so on. We want to enforce these restrictions as early as possible.

Here is a basic version:

// types added for clarity

// getParameter is defined as:
// fun getParameter(name:String):String?

fun handle(): Unit
{
val optional:String? = getParameter(QP_OPTIONAL)
val present:String = getParameter(QP_PRESENT)
?: return Logger.log("$QP_PRESENT was not provided.")

val content:String = getParameter(QP_CONTENT)
?: return Logger.log("$QP_CONTENT was not provided.")

if (content.isBlank())
{
return Logger.log("$QP_CONTENT was provided but empty.")
}

val someHash:String = getParameter(QP_HASH)
?: return Logger.log("$QP_HASH was not provided.")

if (someHash.isBlank() || !isAlphaNumeric(someHash))
{
return
Logger.log("$QP_HASH was provided but is not alpha-numeric.")
}

...
}

We’ve all been here, this can get quite lengthy and verbose. Let’s modify our getParameter function so that it can validate our parameters for us, so we don’t need to do it inline every time.

Nothing is coming, I promise.

Add Some Accountability

To make this happen, we will introduce a Result state to let us know if the parameter value meets our requirements or not.

Here is a simple Result object that’s going to come in handy:

sealed class Result<out T>
{
data class Success<out T>(val value:T): Result<T>()
data class Failure<out T>(val message:String): Result<T>()
}

Also, we’ll create a Restriction class to define what rules the given parameter should conform to:

sealed class Restriction
{
object NotNull: Restriction()
object NotBlank: Restriction()
object AlphaNumeric: Restriction()
}
// note: nullable is not included here, since
// getParameter(name:String):String? is nullable to begin with...

Now we can define and implement
fun getParameter(name:String, restriction:Restriction): Result<String>

For this method hopefully it’s obvious that when the query parameter value passes the given restriction then it will be provided via the value field of a Success result; otherwise an error message is returned via the message field of a Failure result.

Here is the basic version, updated to use our new Result and Restriction tools:

fun handle()
{
val optional = getParameter(QP_OPTIONAL)
val present =
when(val result = getParameter(QP_PRESENT, Restriction.NotNull)
{
is Result.Success -> result.value
is Result.Failure -> return Logger.log(result.message)
}

val content =
when(result = getParameter(QP_CONTENT, Restriction.NotBlank)
{
is Result.Success -> result.value
is Result.Failure -> return Logger.log(result.message)
}

val someHash =
when(result = getParameter(QP_HASH, Restriction.AlphaNumeric)
{
is Result.Success -> result.value
is Result.Failure -> return Logger.log(result.message)
}
// {checkpoint}
}

At {checkpoint} :

  • optional is of type String? and could be null and the compiler will enforce that in code following the declaration
  • present is of type String , cannot be null and that is enforced by the compiler following the declaration
  • content is of type String , cannot be null (enforced by the compiler); additionally getParameter can guarantee that content is not blank following the declaration
  • someHash is of type String , cannot be null (enforced by the compiler); additionally getParameter can guarantee that someHash is only alpha-numeric following the declaration

This is cleaner than our original example (since we have moved parameter validation to it’s own place), but it’s still cumbersome with some new boiler-plate code we have introduced. We can do better!

Here is where nothing matters.

Let’s jump right into it. We can add functionality to Result to enforce our heuristics here and remove a lot of duplicated code. For simplicity, I’ll add a function to Result like this:

inline infix fun <T> check(block:(Result.Failure<T>) -> Nothing):T {
return when (this) {
is Result.Success -> this.value
is Result.Failure -> block(this)
}
}

Did you notice the block function parameter that returns Nothing? What’s happening here?

First of all, when the result is a success, it’s just returning that non-null value and not using block at all.

But when the result is a failure, the block gets executed and returns… Nothing? What does that mean? Let’s look back at our example from above, using our new check(...) method.

fun handle()
{
val optional = getParameter(QP_OPTIONAL)
val present = getParameter(QP_PRESENT, Restriction.NotNull) check
{ result -> return Logger.log(result.message) }

val content = getParameter(QP_CONTENT, Restriction.NotBlank) check
{ return Logger.log(it.message) }

val someHash =
getParameter(QP_HASH, Restriction.AlphaNumeric) check
{ return Logger.log(it.message) }
// {checkpoint}
}

Notice that each block contains a return statement (which returns out of the function handle , not block). This means that if block is invoked it executes without returning anything to its caller (for us the caller is inside of our check function). This is the Nothing scenario.

This would also be true if we chose to throw inside the block, instead of return —throwing interrupts the execution of the block, and exits its scope immediately. These are two common ways that the compiler will enforce blocks that are defined to return Nothing .

In our example, this lets us very cleanly populate our variables with restrictions we choose to impose, and to return cleanly when those conditions are not met. To hammer home that point, the {checkpoint} conditions still hold that at {checkpoint}:

  • optional is of type String? and could be null and the compiler will enforce that in code following the declaration
  • present is of type String , cannot be null and is enforced by the compiler following the declaration
  • content is of type String , cannot be null (enforced by the compiler); additionally getParameter can guarantee that content is not blank following the declaration
  • someHash is of type String , cannot be null (enforced by the compiler); additionally getParameter can guarantee that someHash is only alpha-numeric following the declaration

Not only that, the compiler will force us to either return or throw in the failure block we provide to the check function. Once we reach {checkpoint} in our code, we can use our variables with all of our restrictions enforced.

That’s it! Thanks for reading — hopefully you learned something new about Nothing. Please get in touch if you have more to add to the conversation, or if I’ve made any errors and I will do my best to correct or address any and all of them.

Cheers!

--

--

Jason McCormack

Software Engineer, Partner and Technology Lead at KingsBridge BCP