Option

The Option<T> datatype is the representation of a presence or absence of a value T. In Arrow, Option<T> is a sealed class with two sub-types, Some<T>, a data class that represents the presence of value T and None, and an object that represents the absence of value. Defined as a sealed class, Option<T> can't have any other sub-types; therefore the compiler can check clauses exhaustively, if both cases, Some<T> and None are covered.

I know (or I pretend to know) what you're thinking at this very moment—why do I need Option<T> to represent the presence or absence of T, if in Kotlin we already have T for presence and T? for absence?

And you are right. But Option provides a lot more value than nullable types, let's jump directly to an example:

fun divide(num: Int, den: Int): Int? {
return if (num % den != 0) {
null
} else {
num / den
}
}

fun division(a: Int, b: Int, den: Int): Pair<Int, Int>? {
val aDiv = divide(a, den)
return when (aDiv) {
is Int -> {
val bDiv = divide(b, den)
when (bDiv) {
is Int -> aDiv to bDiv
else -> null
}
}
else -> null
}
}

The division function takes three parameters—two integers (a, b) and a denominator (den) and returns a Pair<Int, Int>, if both numbers are divisible by den or null otherwise.

We can express the same algorithm with Option:

import arrow.core.*
import arrow.syntax.option.toOption

fun
optionDivide(num: Int, den: Int): Option<Int> = divide(num, den).toOption()

fun optionDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
val aDiv = optionDivide(a, den)
return when (aDiv) {
is Some -> {
val bDiv = optionDivide(b, den)
when (bDiv) {
is Some -> Some(aDiv.t to bDiv.t)
else -> None
}
}
else -> None
}
}

The function, optionDivide takes the nullable result from divide and returns it as an Option, using the toOption() extension function.

There are no major changes on optionDivision compared to division, it is the same algorithm expressed with different types. If we stop here, then Option<T> doesn't provide extra value on top of nullables. Luckily, that isn't the case; there are more ways to use Option:

fun flatMapDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
return optionDivide(a, den).flatMap { aDiv: Int ->
optionDivide(b, den).flatMap { bDiv: Int ->
Some(aDiv to bDiv)
}
}
}

Option provides several functions to process its internal value, in this case, flatMap (as a monad) and now our code looks a lot shorter.

Take a look at the following short list with some of the Option<T> functions:

Function

Description

exists(p :Predicate<T>): Boolean

Returns predicate p result if value T exists, otherwise null.

filter(p: Predicate<T>): Option<T>

Returns Some<T> if the value T exists and fulfills the predicate p, otherwise None.

 flatMap(f: (T) -> Option<T>): Option<T>

A flatMap transform function (like monad).

<R> fold(ifEmpty: () -> R, some: (T) -> R): R<R>

Returns value transformed as R, invoking ifEmpty for None and some for Some<T>.

getOrElse(default:() -> T): T

Returns value T if exists, otherwise returns default result.

<R> map(f: (T) -> R):Option<T>

A transform function (like functor).

orNull(): T?

Returns the value T as a nullable T?.

The last implementation of division will use comprehensions:

import arrow.typeclasses.binding

fun
comprehensionDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
return Option.monad().binding {
val aDiv: Int = optionDivide(a, den).bind()
val bDiv: Int = optionDivide(b, den).bind()
aDiv to bDiv
}.ev()
}

Comprehension is a technique to compute sequentially over any type (such as Option, List, and others) that contains a flatMap function and can provide an instance of monad (more on this later).

In Arrow, comprehensions use coroutines. Yes, coroutines are useful outside the asynchronous execution domain.

If we outline the continuations from our previous example it will look like this (which is a helpful mental model to understand coroutines)

fun comprehensionDivision(a: Int, b: Int, den: Int): Option<Pair<Int, Int>> {
return Option.monad().binding {
val aDiv: Int = optionDivide(a, den).bind()
// start continuation 1
val bDiv: Int = optionDivide(b, den).bind()
//start continuation 2
aDiv to bDiv
//end continuation 2
// end continuation 1
}.ev()
}

Option.monad().binding is a coroutine builder and the bind() function is a suspended function. If you recall correctly from our coroutines chapter, a continuation is a representation of any code after a suspension point (that is, when a suspended function is invoked). In our example, we have two suspension points and two continuations, when we return (in the last block line) we are in the second continuation, and we have access to both values, aDiv and bDiv.

Reading this algorithm as continuations is very similar to our flatMapDivision function. Behind the scenes, Option.monad().binding uses Option.flatMap with continuations to create the comprehension; once compiled, both comprehensionDivision and flatMapDivision are equivalent, roughly speaking.

The ev() method will be explained in the next section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.21.158