Skip to content

Instantly share code, notes, and snippets.

@programaker
Last active August 13, 2021 04:19
Show Gist options
  • Save programaker/07d117b239566879c088eb4e13037469 to your computer and use it in GitHub Desktop.
Save programaker/07d117b239566879c088eb4e13037469 to your computer and use it in GitHub Desktop.
Why do we need Monads?
//Functional programming is programming combining functions.
//
//We have a bunch of simple functions, then combine them into
//more and more complex functions until have a full program.
//
//The simplest case is when the functions are COMPOSABLE, for example, given the functions:
f: A => B
g: B => C
h: C => D
//Note how the types align: A => B,B => C,C => D. One's output is other's input.
//Therefore, we can combine them like this:
h(g(f(a))) //this will produce a value of type D
//or, with F#/Elixir pipes, to enhance the sense of sequencing:
a |> f |> g |> h
//However, not all functions combine through direct composition.
//Functions that produce results in a context (effect) don't!
//Some examples of contexts/effects
fl: A => List[B] //List[B]: the context where we can have 0, 1 or many Bs
fo: A => Option[B] //Option[B]: the context where we can have 1 B or none
ff: A => Future[B] //Future[B]: the context where we will possibly have a B in the future (asynchronous operation)
fe: A => Either[X,B] //Either[X,B]: the context where we can have a B or an X, but never both at the same time
ft: A => Try[B] //Try[B]: the context where we can have a B or an Exception
//Let's stick with the List context and change our first functions accordingly:
f1: A => List[B]
g1: B => List[C]
h1: C => List[D]
//With these functions we can't do:
h1(g1(f1(a)))
a |> f1 |> g1 |> h1
//because the types don't fit!
//So, is there a way to combine such functions?
/****** MONADS ******/
//A Monad - M - can be defined as:
//--- A Type Constructor, M[_], representing the context ---//
//In OO languages it can be a class with a type parameter
M[_]
//--- A way to put a normal value A in the context ---//
//Usually, it's a function of type: "A => M[A]" called "unit" or "pure" or "return" (haskell)
//In OO languages, it can be the constructor
new M(a)
//or in case of Scala the special apply method
M(a)
//or even unit itself as a factory method
M.unit(a)
//--- A function of type: "M[A] => (A => M[B]) => M[B]" ---//
//Here is where the function combination happens
//In OO languages it can be a method of M[A] with the type "(A => M[B]) => M[B]"
//Depending on the language, this function can be called: bind, flatMap, >>=, etc
def flatMap[B](f: A => M[B]): M[B]
//--- Adherence to the Monad laws ---//
//Let "ma" be a Monad of type M[A] and "f" and "g" be functions of type "f: A => M[B]" and "g: B => M[C]"
//1. Left Identity: M.unit(a).flatMap(f) === f(a)
//2. Right Identity: ma.flatMap(M.unit) === ma
//3. Associativity: ma.flatMap(f).flatMap(g) === ma.flatMap(a => f(a).flatMap(g))
//Now, given the functions:
f2: A => M[B]
g2: B => M[C]
h2: C => M[D]
//Then...
M(a).flatMap(f2).flatMap(g2).flatMap(h2) //this will produce a value of type M[D]
//looks familiar, right?
//let's rename flatMap to ">>=" and compare to the basic function composition:
M(a) >>= f2 >>= g2 >>= h2
a |> f |> g |> h
//Conclusion: we need Monads to combine functions that produce values in a context/effect, which don't compose directly!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment