Applicatives

Our previous example, invoking a lambda inside a wrapper with a parameter inside the same kind of wrapper, is the perfect way to introduce applicatives.

An applicative is a type that defines two functions, a pure(t: T) function that returns the T value wrapped in the applicative type, and an ap function (apply, in other languages) that receives a lambda wrapped in the applicative type.

In the previous section, when we explained monads, we made them extend directly from a functor but in reality, a monad extends from an applicative and an applicative extends from a functor. Therefore, our pseudo code for a generic applicative, and the entire hierarchy, will look like this:

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

interface Applicative<C<_>>: Functor<C> { //Invalid Kotlin code
fun <A> pure(a:A): C<A>

fun <A, B> ap(ca:C<A>, fab: C<(A) -> B>): C<B>
}

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

In short, an applicative is a more powerful functor, and a monad is a more powerful applicative.

Now, let's write an ap extension function for List<T>:

fun <T, R> List<T>.ap(fab: List<(T) -> R>): List<R> = fab.flatMap { f -> this.map(f) }

And we can revisit our last example from the Monads section:

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
}

Let's rewrite it with the ap function:

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
.ap(functions)
.joinToString()
println(result) //2, 4, 6, 4, 5, 6
}

Easier to read, but with a caveat—the result is in a different order. We need to be aware and choose which option is appropriate for our particular case.

We can add pure and ap to our Option class:

fun <T> Option.Companion.pure(t: T): Option<T> = Option.Some(t)

Option.pure is just a simple alias for the Option.Some constructor.

Our Option.ap function is fascinating:

//Option
fun <
T, R> Option<T>.ap(fab: Option<(T) -> R>): Option<R> = fab.flatMap { f -> map(f) }

//List
fun <T, R> List<T>.ap(fab: List<(T) -> R>): List<R> = fab.flatMap { f -> this.map(f) }

Both Option.ap and List.ap have the same body, using a combination of flatMap and map, which is precisely how we combine monadic operations.

With monads, we summed two Option<Int> using 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)
}

Now, using applicatives:

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

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

That is not very easy to read. First, we map maybeFive with a lambda (Int) -> (Int) -> Int (technically, a curried function, and there is more information about curried functions in Chapter 12, Getting Started with Arrow), that returns an Option<(Int) -> Int> that can be passed as a parameter for maybeTwo.ap.

We can make things easier to read with a little trick (that I'm borrowing from Haskell):

infix fun <T, R> Option<(T) -> R>.`(*)`(o: Option<T>): Option<R> = flatMap { f: (T) -> R -> o.map(f) }

The infix extension function Option<(T) -> R>.`(*)` will let us read the sum operation from left to right; how cool is that? Now, let's look at the following code, summing two Option<Int> using applicatives

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

println(Option.pure { f: Int -> { t: Int -> f + t } } `(*)` maybeFive `(*)` maybeTwo) // Some(value=7)
}

We wrap the (Int) -> (Int) -> Int lambda with the pure function and then we apply Option<Int>, one by one. We use the name `(*)` as a homage to Haskell's <*>.

So far, you can see that applicatives let you do some cool tricks, but monads are more powerful and flexible. When do use one or the other? It obviously depends on your particular problem, but our general advice is to use the abstraction with the least amount of power possible. You can start with a functor's map, then an applicative's ap, and lastly a monad's flatMap. Everything can be done with flatMap (as you can see  Option, map, and ap were implemented using flatMap), but most of the time map and ap can be more accessible to reason about it.

Coming back to functions, we can make a function behave as an applicative. First, we should add a pure function:

object Function1 {
fun <A, B> pure(b: B) = { _: A -> b }
}

First, we create an object Function1, as the function type (A) -> B doesn't have a companion object to add new extension functions as we did with Option:

fun main(args: Array<String>) {
val f: (String) -> Int = Function1.pure(0)
println(f("Hello,")) //0
println(f("World")) //0
println(f("!")) //0
}

Function1.pure(t: T) will wrap a T value in a function and will return it, regardless of the parameter that we use. If you have experience with other functional languages, you'll recognize function's pure as an identity function (more about identity functions in Chapter 12, Getting Started with Arrow).

Let's add flatMap, an ap, to a function (A) -> B:

fun <A, B, C> ((A) -> B).map(transform: (B) -> C): (A) -> C = { t -> transform(this(t)) }

fun <
A, B, C> ((A) -> B).flatMap(fm: (B) -> (A) -> C): (A) -> C = { t -> fm(this(t))(t) }

fun <A, B, C> ((A) -> B).ap(fab: (A) -> (B) -> C): (A) -> C = fab.flatMap { f -> map(f) }

We already cover map(transform: (B) -> C): (A) -> C and we know that it behaves as a forward function composition. If you pay close attention to flatMap and ap, you'll see that the parameter is kind of backwards (and that ap is implemented as all the other ap functions for other types).

But, what can we do with the function's ap? Let's look at the following code:

fun main(args: Array<String>) {
val add3AndMultiplyBy2: (Int) -> Int = { i: Int -> i + 3 }.ap { { j: Int -> j * 2 } }
println(add3AndMultiplyBy2(0)) //6
println(add3AndMultiplyBy2(1)) //8
println(add3AndMultiplyBy2(2)) //10
}

Well, we can compose functions, which is not exciting at all because we already did that with map. But there is a little trick with function's ap. We can access the original parameter:

fun main(args: Array<String>) {
val add3AndMultiplyBy2: (Int) -> Pair<Int, Int> = { i:Int -> i + 3 }.ap { original -> { j:Int -> original to (j * 2) } }
println(add3AndMultiplyBy2(0)) //(0, 6)
println(add3AndMultiplyBy2(1)) //(1, 8)
println(add3AndMultiplyBy2(2)) //(2, 10)
}

Accessing the original parameter in a function composition is useful in several scenarios, such as debugging and auditing.

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

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