I was having a look at the Arrow library found here. Why would ever want to use an Option
type instead of Kotlin's built in nullables?
It is special symbol used to separate code with different purposes. It can be used to: Separate the parameters and body of a lambda expression val sum = { x: Int, y: Int -> x + y } Separate the parameters and return type declaration in a function type (R, T) -> R.
Depending on whom you ask, using Optional as the type of a field or return type is bad practice in Java. Fortunately, in Kotlin there is no arguing about it. Using nullable types in properties or as the return type of functions is considered perfectly valid and idiomatic in Kotlin.
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.
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!
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):
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.
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.
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
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".
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With