Background
Previously, we learned about applicative functors, which are souped-up functors that support multi-argument mapping functions. In this post, I'd like to dive deeper into the practical usage of applicatives in F#.
We described an applicative as a type that:
- Is a functor, which means that it has a unary (i.e. single-argument)
map
function (and, thus, an infix version of the same function, called<!>
). - Provides a way to apply arguments one at a time, via a
<*>
function. - Provides a way to "
lift
" values into the applicative.
Example
F#'s Option<'t>
type is an applicative:
let (<!>) = Option.map
let (<*>) fOpt xOpt =
match fOpt, xOpt with
| Some f, Some x -> Some (f x)
| _ -> None
let lift x = Some x
This allows us, for example, to use multiplication to "map" over two options at once:
let mult = (*)
// functor: times3 takes one argument
let times3 = mult 3
Option.map times3 (Some 4) // Some 12
Option.map times3 None // None
// applicative: mult takes two arguments
mult <!> Some 3 <*> Some 4 // Some 12
mult <!> None <*> Some 4 // None
mult <!> Some 3 <*> None // None
Pipes
Applying arguments one at a time with <*>
is certainly a legitimate way to think about applicatives, but (IMHO) it's not the most intuitive, nor is it used often in F#. I think it's easier to conceptualize applicatives with the lifted function (e.g. mult
) at the end of a computation, instead of the beginning. Following the FParsec library, we'll call this "pipes" style:
let pipe2 a b f = f <!> a <*> b
In the case of options, pipe2
has the following signature:1
Option<'a> -> Option<'b> -> ('a -> 'b -> 'c) -> Option<'c>
So pipe2
is a function that sends the result of computations a
and b
to function f
, producing a new computation. We can see it in action with options here:
pipe2 (Some 3) (Some 4) mult // Some 12
pipe2 None (Some 4) mult // None
We can define pipe3
, pipe4
, etc. similarly.
Using pipes style, we can think of applicatives as performing computations in parallel at the same time, with no dependencies between them. For example, consider:
let runMult getOptA getOptB =
pipe2
(getOptA ())
(getOptB ())
mult
Note that getOptB
is executed even if getOptA
returns None
. There's no way to short-circuit the computation before getOptB
is invoked.
Monad-lite
Contrast this with monads, which are a way of performing computations in series, so that the result of one computation flows into the next. In fact, instead of thinking of applicatives as souped-up functors, it's often easier to think of them as a "lite" version of monads.2 Let's see where this notion takes us, starting with a basic computation builder for options:
type OptionBuilder() =
member __.Return(x) = Some x
member __.Bind(opt, binder) = Option.bind binder opt
let option = OptionBuilder()
Using this builder, we can create monadic computation expressions, such as:
option {
let! a = optA
let! b = optB
return a * b
}
Note that the computation of b
does not depend in any way on the value of a
, so a monad is overkill for this computation - it is merely applicative. We can enforce this limitation by removing the Bind
method from the computation builder, but then the expression doesn't compile at all.
F# 5 enhancements
Fortunately, F# now supports applicative computation expressions directly. There are two new core builder methods:
-
BindReturn
: Corresponds to<!>
, which is justmap
. -
MergeSources
: Similar to<*>
andpipe2
, except that it always produces a 2-tuple (instead of calling a function).
For our option
builder, their implementation looks like this:
member __.BindReturn(opt, f) =
Option.map f opt
member __.MergeSources(optA, optB) =
match optA, optB with
| Some a, Some b -> Some (a, b)
| _ -> None
Now that these two methods are present in the builder, and!
can be used instead of let!
to create an applicative expression:
option {
let! a = optA
and! b = optB // OK because b does not depend on a
return a * b
}
Because the applicative computations are independent, they can potentially be parallelized by the compiler, resulting in more efficient code. So be on the lookout for opportunities to use and!
instead of let!
in your F# code!
References
-
Haskell calls this function
liftA2
instead ofpipe2
, where theA
stands for "applicative". ↩ -
So, just as every applicative is also a functor, every monad is also an applicative. However, not every applicative is a monad, which is what makes the applicative pattern useful on its own, situated between its two better-known cousins. ↩
Top comments (0)