A Simple, Real World Example of Nothing in Kotlin.
Nothing makes sense!
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.
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 implementfun 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 aSuccess
result; otherwise an error message is returned via themessage
field of aFailure
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 typeString?
and could benull
and the compiler will enforce that in code following the declarationpresent
is of typeString
, cannot benull
and that is enforced by the compiler following the declarationcontent
is of typeString
, cannot benull
(enforced by the compiler); additionallygetParameter
can guarantee thatcontent
is not blank following the declarationsomeHash
is of typeString
, cannot benull
(enforced by the compiler); additionallygetParameter
can guarantee thatsomeHash
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!
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 typeString?
and could benull
and the compiler will enforce that in code following the declarationpresent
is of typeString
, cannot benull
and is enforced by the compiler following the declarationcontent
is of typeString
, cannot benull
(enforced by the compiler); additionallygetParameter
can guarantee thatcontent
is not blank following the declarationsomeHash
is of typeString
, cannot benull
(enforced by the compiler); additionallygetParameter
can guarantee thatsomeHash
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!