Skip to main content

Why nullable types & Option?

If you have worked with Java at all in the past, you have likely come across a NullPointerException at some time (other languages will throw similarly named errors in such a case). Usually, this happens because some method returns null when you weren't expecting it and, thus, isn't dealing with that possibility in your client code. A value of null is often abused to represent an absent optional value. Kotlin already solves the problem by getting rid of null values altogether and providing its own unique syntax Null-safety machinery based on ?.

Since Kotlin already has nullable types, why do we need Arrow's Option type? There are only a few cases where you should use Option instead of nullable types, and one is the nested nullability problem. Let's see an example:

We write a small firstOrElse function, which should return the list's first element or the default value if the list is empty.

fun <A> List<A>.firstOrElse(default: () -> A): A = firstOrNull() ?: default()

fun example() {
emptyList<Int?>().firstOrElse { -1 } shouldBe -1
listOf(1, null, 3).firstOrElse { -1 } shouldBe 1
}

Running this code with an emptyList or a non-empty list seems to work as expected.

Unexpected result

We get an unexpected result if we run this function with a list that contains null as the first value.

fun example() {
listOf(null, 2, 3).firstOrElse { -1 } shouldBe null
}

Now we're executing the function on a list that isNotEmpty, so we expect it to return the first element of value null. Instead, it returns -1, the default value we specified in case the list isEmpty!

Exception in thread "main" java.lang.AssertionError: Expected null but actual was -1

This is known as the nested nullability problem, which can be solved using Option instead of nullable types. So let's analyze what is going wrong here and how we can fix it. When we look at the implementation of our firstOrElse function, we see that we're using firstOrNull to get the first element of the list, and if that is null, we return the default value.

However, our generic parameter of Any has an upperbound of Any?, so we can pass in a list of nullable values. This means that firstOrNull can return null if the first element of the list is null, and we're not handling that case.

fun <A> List<A>.firstOrElse(default: () -> A): A = firstOrNull() ?: default()

We can solve this in two ways. One is by restricting A to have an upperbound of Any instead, but then we limit this function to only work with non-nullable types.

fun <A : Any> List<A>.firstOrElse(default: () -> A): A = firstOrNull() ?: default()

Our previous examples of List<Int?> would not even compile in that case, so this is not a good solution. Instead, we could use firstOrNone, and then we can handle the case where the first element is null:

fun <A> List<A>.firstOrElse(default: () -> A): A =
when(val option = firstOrNone()) {
is Some -> option.value
None -> default()
}

If we rerun our previous examples, they all behave as expected since we can rely on None to detect the case where the list is empty.

fun example() {
emptyList<Int?>().firstOrElse { -1 } shouldBe -1
listOf(1, null, 3).firstOrElse { -1 } shouldBe 1
listOf(null, 2, 3).firstOrElse { -1 } shouldBe null
}

Sometimes you might still want to use Option instead of nullable types, even when you're not the author of these generic functions. Some libraries such as RxJava and Project Reactor don't support nullable types in all their APIs. If you still need to work with null in combination with generic APIs that don't allow nullable types, you can use Option to work around this problem.

Arrow DSL

Arrow also provides special DSL syntax for nullable & Option types

Working with Option

Arrow offers a special DSL syntax for all of its types and provides it for nullable types. So let's review both below. Before we get started, we need to know how to construct an Option from a (nullable) value and vice versa.

Option<A> is a container for an optional value of type A. If the value of type A is present, the Option<A> is an instance of Some<A>, containing the current value of type A. If the value is absent, the Option<A> is the object None. And we have four constructors available to create an Option<A>, their regular class constructors, and two extension functions that return Option<A>.

val some: Some<String> = Some("I am wrapped in something")
val none: None = None

val optionA: Option<String> = "I am wrapped in something".some()
val optionB: Option<String> = none<String>()

fun example() {
some shouldBe optionA
none shouldBe optionB
}

Creating a Option<A> from a nullable type A? can be helpful when we need to lift nullable values into Option. This can be done with the Option.fromNullable function or the A?.toOption() extension function.

fun example() {
val some: Option<String> = Option.fromNullable("Nullable string")
val none: Option<String> = Option.fromNullable(null)

"Nullable string".toOption() shouldBe some
null.toOption<String>() shouldBe none
}
Take care

If A? is null, you should explicitly use the Some or .some() constructor. Otherwise, you will get a None instead of a Some due to the nested nullable problem.

fun example() {
val some: Option<String?> = Some(null)
val none: Option<String?> = Option.fromNullable(null)

some shouldBe null.some()
none shouldBe None
}

Extracting values from Option

So now that we know how to construct Option values, how can we extract the value from it? The easiest way to extract the String value from the Option would be to turn it into a nullable type using getOrNull and work with it as we would typically do with nullable types.

fun example() {
Some("Found value").getOrNull() shouldBe "Found value"
None.getOrNull() shouldBe null
}

Another way would be to provide a default value using getOrElse. This is similar to the ?: operator in Kotlin, but instead of giving a default value for null, we provide a default value for None. In the example below, we provide a default value of "No value"when the Option is None.

fun example() {
Some( "Found value").getOrElse { "No value" } shouldBe "Found value"
None.getOrElse { "No value" } shouldBe "No value"
}

Since Option is modeled as a sealed class, we can use exhaustive when statements to pattern match on the possible cases.

fun example() {
when(val value = 20.some()) {
is Some -> value.value shouldBe 20
None -> fail("$value should not be None")
}

when(val value = none<Int>()) {
is Some -> fail("$value should not be Some")
None -> value shouldBe None
}
}

Option & nullable DSL

Now that we know how to construct Option values and turn Option back into regular (nullable) values, let's see how we can use the Option and nullable DSL to work with Option & nullable values in an imperative way.

When working with nullable types, we often need to check if the value is null or not and then do something with it. We typically do that by using ?.let { }, but this quickly results in a lot of nested ?.let { } blocks. Arrow offers bind() and ensureNotNull to get rid of this issue, so let's look at an example and some other interesting functions that Arrow provides in its DSL.

Imagine we have a User domain class that has nullable email address, and we want to find a user by their id and then email them.

@JvmInline value class UserId(val value: Int)
data class User(val id: UserId, val email: Email?)

fun QueryParameters.userId(): UserId? = get("userId")?.toIntOrNull()?.let { UserId(it) }
fun findUserById(id: UserId): User? = TODO()
fun sendEmail(email: Email): SendResult? = TODO()
fun sendEmail(params: QueryParameters): SendResult? =
params.userId()?.let { userId ->
findUserById(userId)?.email?.let { email ->
sendEmail(email)
}
}

There is already quite some nesting going on and quite a lot of ?, but we can use bind() and ensureNotNull to get rid of the nesting.

Seamlessly mix

The nullable DSL can seamlessly be mixed with Option by calling bind on Option values.

@JvmInline value class UserId(val value: Int)
data class User(val id: UserId, val email: Email?)

fun QueryParameters.userId(): UserId? = get("userId")?.toIntOrNull()?.let { UserId(it) }
fun findUserById(id: UserId): Option<User> = TODO()
fun sendEmail(email: Email): SendResult? = TODO()
fun sendEmail(params: QueryParameters): SendResult? = nullable {
val userId = ensureNotNull(params.userId())
val user = findUserById(userId).bind()
val email = user.email.bind()
sendEmail(email)
}

Similarly, this same pattern applies to Option and other data types such as Either, which is covered in other sections.

Seamlessly mix

The Option DSL can seamlessly be mixed with nullable types using ensureNotNull.

Ignoring errors

Sometimes you need to "forget" the error type if consuming more informative types like Either. Wrap the .bind() in ignoreErrors to make this explicit.

@JvmInline value class UserId(val value: Int)
data class User(val id: UserId, val email: Email?)

fun QueryParameters.userId(): Option<UserId> =
get("userId")?.toIntOrNull()?.let(::UserId).toOption()

fun findUserById(id: UserId): Option<User> = TODO()
fun sendEmail(email: Email): Option<SendResult> = TODO()
fun sendEmail(params: QueryParameters): Option<SendResult> = option {
val userId = params.userId().bind()
val user = findUserById(userId).bind()
val email = ensureNotNull(user.email)
sendEmail(email).bind()
}

Inspecting Option values

Besides extracting the value from an Option or sequencing nullable or Option based logic, we often just need to inspect the values inside it. With nullable types, we can simply use != null to inspect the value, but with Option, we can check whether option has value or not using isSome and isNone.

fun example() {
Some(1).isSome() shouldBe true
none<Int>().isNone() shouldBe true
}

The same function exists to check if Some contains a value that passes a certain predicate. For nullable types, we would use ?.let { } ?: false.

fun example() {
Some(2).isSome { it % 2 == 0 } shouldBe true
Some(1).isSome { it % 2 == 0 } shouldBe false
none<Int>().isSome { it % 2 == 0 } shouldBe false
}

And, finally, sometimes we just need to execute a side effect if the value is present. For nullable types, we would use ?.also { } or ?.also { if(it != null) { } }.

fun example() {
Some(1).onSome { println("I am here: $it") }
none<Int>().onNone { println("I am here") }

none<Int>().onSome { println("I am not here: $it") }
Some(1).onNone { println("I am not here") }
}
I am here: 1
I am here

Conclusion

Typically, when working in Kotlin, you should prefer working with nullable types over Option as it is more idiomatic. However, when writing generic code, we sometimes need Option to avoid the nested nullability issues, or when working with libraries that don't support null values such as Project Reactor or RxJava.

Arrow offers a neat DSL to work with Option and nullable types in an imperative way, which makes it easy to work with them both in a functional way. They seamlessly integrate, so you can use whatever you need and prefer when you need it.