Why use Arrow's Options instead of Kotlin nullable

I have been using Option data type provided by Arrow for over a year and there at the begining we did the exact same question to ourselves. The answer follows.

Option vs Nullable

If you compare just the option data type with nullables in Kotlin they are almost even. Same semanthics (there is some value or not), almost same syntax (with Option you use map, with nullables you use safe call operator).

But when using Options you enable the possibility to take benefits from the arrow ecosystem!

Arrow ecosystem (functional ecosystem)

When using Options you are using the Monad Pattern. When using the monad pattern with liberaries like arrow, scala cats, scalaz, you can take benefits from several functional concepts. Just 3 examples of benefits (there is a lot more than that):

1. Access to other Monads

Option is not the only one! For instance, Either is a lot useful to express and avoid to throw Exceptions. Try, Validated and IO are examples of other common monads that help us to do (in a better way) things we do on typical projects.

2. Conversion between monads + abstractions

You can easily convert one monad to another. You have a Try but want to return (and express) an Either? Just convert to it. You have an Either but doesn't care about the error? Just convert to Option.

val foo = Try { 2 / 0 }
val bar = foo.toEither()
val baz = bar.toOption()

This abstraction also helps you to create functions that doens't care about the container (monad) itself, just about the content. For example, you can create a extension method Sum(anyContainerWithBigDecimalInside, anotherContainerWithBigDecimal) that works with ANY MONAD (to be more precise: "to any instance of applicative") this way:

fun <F> Applicative<F>.sum(vararg kinds: Kind<F, BigDecimal>): Kind<F, BigDecimal> {
    return kinds.reduce { kindA, kindB ->
        map(kindA, kindB) { (a, b) -> a.add(b) }
    }
}

A little complex to understand, but very helpful and easy to use.

3. Monad comprehensions

Going from nullables to monads is not just about changing safe call operators to map calls. Take a look at the "binding" feature that arrow provides as the implementation of the pattern "Monad Comprehensions":

fun calculateRocketBoost(rocketStatus: RocketStatus): Option<Double> {
    return binding {
        val (gravity) = rocketStatus.gravity
        val (currentSpeed) = rocketStatus.currentSpeed
        val (fuel) = rocketStatus.fuel
        val (science) = calculateRocketScienceStuff(rocketStatus)
        val fuelConsumptionRate = Math.pow(gravity, fuel)
        val universeStuff = Math.log(fuelConsumptionRate * science)

        universeStuff * currentSpeed
    }
}

All the functions used and also the properties from rocketStatus parameter in the above example are Options. Inside the binding block the flatMap call is abstracted for us. The code is a lot easier to read (and write) and you don't need to check if the values are present, if some of them is not, the computation will stop and the result will be an Option with None!

Now try to imagine this code with null verifications instead. Not just safe call operators but also probably if null then return code paths. A lot harder isn't it?

Also, the above example uses Option but the true power about monad comprehensions as an abastraction is when you use it with monads like IO in which you can abstract asynchronous code execution in the exact same "clean, sequential and imperative" way as above :O

Conclusion

I strongly recommend you to start using monads like Option, Either, etc as soon as you see the concept fits the semanthics you need, even if you are not sure if you will take the other big benefits from the functional ecosystem or if you doesn't know them very well yet. Soon you'll be using it without noticing the learning-curve. That In my company we use it in almost all Kotlin projects, even in the object-oriented ones (which are the majority).


Disclaimer: If you really want to have a detailed talk about why Arrow is useful, then please head over to https://soundcloud.com/user-38099918/arrow-functional-library and listen to one of the people who work on it. (5:35min)

The people who create and use that library simple want to use Kotlin differently than the people who created it and use "the Option datatype similar to how Scala, Haskell and other FP languages handle optional values".

This is just another way of defining return types of values that you do not know the output of.

Let me show you three versions:

nullability in Kotlin

val someString: String? = if (condition) "String" else null

object with another value

val someString: String = if (condition) "String" else ""

the Arrow version

val someString: Option<String> = if (condition) Some("String") else None

A major part of Kotlin logic can be to never use nullable types like String?, but you will need to use it when interopting with Java. When doing that you need to use safe calls like string?.split("a") or the not-null assertion string!!.split("a").

I think it is perfectly valid to use safe calls when using Java libraries, but the Arrow guys seem to think different and want to use their logic all the time.

The benefit of using the Arrow logic is "empowering users to define pure FP apps and libraries built atop higher order abstractions. Use the below list to learn more about Λrrow's main features".


One thing other answers haven't mentioned: you can have Option<Option<SomeType>> where you can't have SomeType??. Or Option<SomeType?>, for that matter. This is quite useful for compositionality. E.g. consider Kotlin's Map.get:

abstract operator fun get(key: K): V?

Returns the value corresponding to the given key, or null if such a key is not present in the map.

But what if V is a nullable type? Then when get returns null it can be because the map stored a null value for the given key or because there was no value; you can't tell! If it returned Option<V>, there wouldn't be a problem.