Map, Bind, and Apply from examples
Sunday, June 5, 2016
Many blog posts have covered this topic by explaining the function signatures of each of these functions. While I think that is valuable, that's not the way I think about them when I'm writing code.
In this post we'll look at one of my typical workflows and see which function we want in each case.
Our first example will start very simple. A function that adds 2 to the input.
//int -> int let add2 x = x + 2 add2 5 // 7
Now that we have our function we'll need some data. Map, Bind, and Apply are all about contexts, so for our first example we'll choose the
seq context. If we have an
int seq and we want to return an
int seq where every item in the sequence has been incremented by 2 we'll want to use
[1;2;3;4] |> Seq.map add2 // [3;4;5;6]
Of course this same approach works if we are in the
Option context as well.
(Some 2) |> Option.map add2 // Some 4 None |> Option.map add2 // None
What happens if we change our function to something slightly more interesting though? Our
add2 function didn't know anything about contexts, so let's see how our approach might change if we were to use a function that involved contexts.
int -> int seq let factors x = [2..x-1] |> Seq.filter (fun i -> x % i = 0) factors 10 // seq [2; 5]
In this case our function takes an
int and returns an
int seq. What happens if we want to call this function for each element in a sequence?
[5;6;7] |> Seq.map factors // seq [seq ; seq [2; 3]; seq ]
We get a
seq<int seq>, which is a context inside of a context...
Now depending upon what you were needing, this could be valid. But what if you wanted to return a non nested sequence of elements? As it turns out, this is exactly what
bind does. For some reason the default library for F# calls this
collect within the
Seq context, but it has the same signature as
[5;6;7] |> Seq.collect factors // seq [2; 3]
Great, so if you have a value in a context and you have a function that returns a value in the context you can use bind rather than map to keep yourself from having a value in a doubly nested context. Here they are side by side to show the difference.
[5;6;7] |> Seq.map factors // seq [seq ; seq [2; 3]; seq ] [5;6;7] |> Seq.collect factors // seq [2; 3]
All of our functions so far have only accepted one argument (ok, so F# is a curried language and technically all functions accept one argument, but that's not the scope of this post). What sorts of situations do we find ourselves in if we have functions with 2 arguments?
let add x y = x + y
What happens if we have a value in a context and we want to call our
add function? We saw earlier that if we are in a context and our function doesn't know about contexts we should use
(Some 3) |> Option.map add // (int -> int) option
We end up with a function in a context. In this case we may have a function that adds 3 or we may have
None. So how do we go about sending another argument to this value?
As our title suggests, this is where Apply comes in. Unfortunately F# doesn't have apply defined in the
Option module. Fortunately I can "borrow proudly" from Scott Wlaschin :). In this case I've swapped the argument order so we can use the pipe operator.
module Option = let apply xOpt fOpt = match fOpt,xOpt with | Some f, Some x -> Some (f x) | _ -> None
This may look complex at first but is actually just a simple match. This function takes a function in the
Option context and a value in the
Option context. If both the function and the value are
Some it applies the value to the function else it returns
Option.apply we are ready to finish applying arguments to our function.
(Some 3) |> Option.map add |> Option.apply (Some 2) // Some 5 None |> Option.map add |> Option.apply (Some 2) // None (Some 3) |> Option.map add |> Option.apply None // None
When I'm trying to determine which function to use I find it helpful to think about which (if any) context my input is in and what I want to return. If my input is in a context and my function knows nothing about contexts then
map works. If my input is in a context and my function also returns a context I typically want
bind to keep from being in a doubly nested context. And if I am trying to pass an argument to a function that's in a context,
apply will do that job for me, even if I have to define it myself.