Monads

A monad is a functor type that defines a flatMap (or bind, in other languages) function, that receives a lambda that returns the same type. Let me explain it with an example. Luckily for us, List<T> defines a flatMap function:

fun main(args: Array<String>) {
val result = listOf(1, 2, 3)
.flatMap { i ->
listOf(i * 2, i + 3)
}
.joinToString()

println(result) //2, 4, 4, 5, 6, 6
}

In a map function, we just transform the List value's content, but in flatMap, we can return a new List type with less or more items, making it a lot more potent than map.

So, a generic monad will look like this (just remember that we don't have higher-kinded types):

interface Monad<C<_>>: Functor<C> { //Invalid Kotlin code
fun <A, B> flatMap(ca:C<A>, fm:(A) -> C<B>): C<B>
}

Now, we can write a flatMap function for our Option type:

fun <T, R> Option<T>.flatMap(fm: (T) -> Option<R>): Option<R> = when (this) {
Option.None -> Option.None
is Option.Some -> fm(value)
}

If you pay close attention, you can see that flatMap and map look very similar; so similar that we can rewrite map using flatMap:

fun <T, R> Option<T>.map(transform: (T) -> R): Option<R> = flatMap { t -> Option.Some(transform(t)) }

And now we can use a flatMap function's power in cool ways that will be impossible with a plain map:

fun calculateDiscount(price: Option<Double>): Option<Double> {
return price.flatMap { p ->
if (p > 50.0) {
Option.Some(5.0)
} else {
Option.None
}
}
}

fun main(args: Array<String>) {
println(calculateDiscount(Option.Some(80.0))) //Some(value=5.0)
println(calculateDiscount(Option.Some(30.0))) //None
println(calculateDiscount(Option.None)) //None
}

Our function, calculateDiscount, receives and returns Option<Double>. If the price is higher than 50.0, we return a discount of 5.0 wrapped on Some, and None if it doesn't.

One cool trick with flatMap is that it can be nested:

fun main(args: Array<String>) {
val maybeFive = Option.Some(5)
val maybeTwo = Option.Some(2)

println(maybeFive.flatMap { f ->
maybeTwo.flatMap { t ->
Option.Some(f + t)
}
}) // Some(value=7)
}

In the inner flatMap function, we have access to both values and operate over them.

We can write this example in a slightly shorter way by combining flatMap and map:

fun main(args: Array<String>) {
val maybeFive = Option.Some(5)
val maybeTwo = Option.Some(2)

println(maybeFive.flatMap { f ->
maybeTwo.map { t ->
f + t
}
}) // Some(value=7)
}

As such, we can rewrite our first flatMap example as a composition of two lists—one of numbers and another one of functions:

fun main(args: Array<String>) {
val numbers = listOf(1, 2, 3)
val functions = listOf<(Int) -> Int>({ i -> i * 2 }, { i -> i + 3 })
val result = numbers.flatMap { number ->
functions.map { f -> f(number) }
}.joinToString()

println(result) //2, 4, 4, 5, 6, 6
}

This technique of nesting several flatMap or combinations of flatMap with map is very powerful and is the primary idea behind another concept named monadic comprehensions, which allow us to combine monadic operations (more about comprehensions in Chapter 13, Arrow Types).

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

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