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 map.

[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 bind.

[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 map.

(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 None.

Armed with 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.