Last active
August 13, 2021 04:19
-
-
Save programaker/07d117b239566879c088eb4e13037469 to your computer and use it in GitHub Desktop.
Why do we need Monads?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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