Skip to content

Instantly share code, notes, and snippets.

@gregghz
Last active February 22, 2018 18:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gregghz/239a3575e654b5dacac7de7e365ee355 to your computer and use it in GitHub Desktop.
Save gregghz/239a3575e654b5dacac7de7e365ee355 to your computer and use it in GitHub Desktop.

Function composition typically looks like this:

let x = 1;
f(g(h(x)));

This works as long as the result of h(1) is an acceptable parameter to g and if the results of g(h(1)) is an acceptable parameter to f.

To make this more concrete let's define these functions:

function f(num) { return num * 2; }
function g(num) { return num + 1; }
function h(num) { return num * 10; }

So what if instead of n number, x was a Promise<number>?

let x = new Promise(function (resolve, reject) {
    resolve(1);
});

We can't pass x directly to h anymore. So instead we use then to compose these functions:

x.then(h).then(g).then(f);

You'll notice that each call to then produces a new Promise with the function applied to the number "inside" the Promise. So after calling x.then(h) the value inside the Promise is 10.

This is exactly how map on any monad works in Scala (replacing Promise with Future):

val x = Future(1)
x.map(h).map(g).map(f)

Back in javascript, what if one of the functions we're calling also produces a Promise?

function h(num) {
    return new Promise(function (resolve, reject) {
        resolve(num * 10);
    });
}

Well it turns out you still just call then in javascript:

x.then(h).then(g).then(f)

Since Scala isn't quite as dynamic as javascript you have to introduce flatMap to accomplish the same thing:

def h(num) = Future(num * 10)
x.flatMap(h).map(g).map(f)

If you used map instead of flatMap to call h you end up with a nested type (since h returns a Future[Int]):

val y: Future[Future[Int]] = x.map(h)

This helps provide a sort of intuition of what flatMap does: It applies the operation and then flattens the result from a Future[Future[Int]] into a Future[Int].

For Promise/Future this makes sense. In JS the alternative is callback hell of the olden days, right?

What about something else like Option? In Scala it is common to use Option[A] where you would use A | null in typescript. So instead of code like this:

val x: String = getSomething() // maybe null
val y = if (x != null) { f(x) } else { null }

You can write:

val x: Option[String] = getSomething()
val y = x.map(f)

Let's say we want to call f, g, and h as above except any of them might also return null/None. First without Option:

val x: Int = getSomething() // maybe null
val y = if (x != null) {
  val l = h(x)
  if (l != null) {
    val m = g(l)
    if (m != null) {
      f(m)
    } else {
      null
    }
  } else {
    null
  }
} else {
  null
}

More likely you would move the null handling to inside each of the functions you're calling but you still end up with all the same boiler plate, it's just shuffled around and tucked away. Not very DRY. Now with Option (assuming h, g, and f are the same as above except they return Option[Int] instead of Int):

val x: Option[Int] = getSomething()
x.flatMap(h).flatMap(g).flatMap(f)

Much better.

In a much more general sense, a monad represents a sort of container (note that Promise, Future, and Option seen above all "contain" a value) with particular operations (then/map/flatMap) designed to allow you to apply functions to the values "inside" the container.

The particulars of how map or flatMap work will vary between different monads. Option and Future have roughly analogous behvaior (although, implementation details differ dramatically -- Option.map/flatMap is a synchronous operation while the same methods on Future happen asynchronously). List is similar except instead of 0 or 1 item as with Option, List operates on 0 or more items. map and flatMap iterate the list and apply operations on each element directly. flatMap on List is interesting because it allows expanding or filtering the list:

val l = List(1,2,3)

// repeat each element
val expanded = l.flatMap { item => List(item, item) }
// List(1,1,2,2,3,3)

// removes 2
val filtered = l.flatMap { item => if (item == 2) Nil else List(item) }
// List(1,3)

Many elementary operations can be defined in terms of flatMap (including map):

val l: List[A]

def filter(predicate: A => Boolean) = l.flatMap { item => if (predicate(item)) List(item) else Nil }
def map[B](f: A => B): List[B] = l.flatMap { item => List(f(item)) }
def product[B](other: List[B]): List[(A, B)] = l.flatMap { ls => other.flatMap { os => List((ls, os)) } }

Generalizing further you can replace List[A] with M[A]:

val l: M[A]

def filter(predicate: A => Boolean) = l.flatMap { item => if (predicate(item)) M.unit(item) else M.empty }
def map[B](f: A => B): M[B] = l.flatMap { item => M.unit(f(item)) }
def product[B](other: M[B]): M[(A, B)] = l.flatMap { ls => other.flatMap { os => M.unit((ls, os)) } }

M replaces List and M.unit() replaces List().

By providing flatMap you get a lot of other operations "for free". This is why Monads are a popular pattern. It generalizes operations on "collection-like" data and allows you to re-use implementations between otherwise unrelated data types.

You'll notice Nil and M.empty in filter. This technically moves us into a discussion about Monoids but I won't get into that. It's enough to know that something is a Monoid if it defines empty and combine (not seen here). For example the Monoid for numbers can use 0 for empty and addition (+) for combine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment