What are Monads?

In essence a monad is anything that just has two functions (though some languages break them up into more, but this is the basics) and follows a few laws:

  1. First you need to have the type, it can be anything that can contain anything else, 0 or more times, so an optional (0 or 1 times), a list (0 or more times), a tuple (N times), a map (0 or more times), or anything you can think of or create that can hold ‘something’ (like a future in java/c++/rust/javascript/etc… is a monad type for example).
  2. First function is the unit function, it just takes a value and wraps it in the type you specify, it is often named unit/return/wrap/etc… The Elixir Enum.wrap/2 is such a unit function if you pass it only a single value (it technically does more than just what unit should do, a real unit in elixir is something like the list constructor like [42] or {42} or %{a: 42} or so forth).
  3. Second function is the bind function, often called flat_map (as it is in Elixir), it is just a function that takes an instance of a type (like an elixir list) and a function, and it calls that function on each element inside the monad and returns that same monadic type back (this is where Elixir’s flat_map breaks down, making it not monadic either, it just accepts returning a list), that list of monadic type instances returned are then flattened back down one level, so something like (in Elixir parlance) flat_map([1, 2, 3, 4], fn x when even?(x) -> [x, x]; _ -> [] end) returns [2, 2, 4, 4] and flat_map(%{a: 1, b: 2}, fn {key, value} -> %{key => value; value => key} end) would return %{a: 1, b: 2, 1 => :a, 2 => :b} and so forth.
  4. The first law being that (I’ll use elixir’s terminology for these of wrap/flat_map) wrap when passed to flat_map is the same as the original instance, I.E. something === flat_map(something, &wrap(&1, theType) and flat_map(wrap(v), f) === f.(v) (it is this why Elixir’s flat_map is not monadic, it breaks this law).
  5. The second law being that the succession order of flat_mapping does not matter, I.E. something |> flat_map(f) |> flat_map(g) === flat_map(something, fn x -> flat_map(f.(x), g) end)

So yes, that looks long but only because there are so many examples, more succinctly:

defmodule ListMonad do
  def wrap(v), do: [v]
  def flat_map([], _f), do: []
  def flat_map([head, rest], f), do: f.(head) ++ flat_map(rest, f)
end

defmodule OkMonad do
  def wrap(v), do: {:ok, v}
  def flat_map(:error, _f), do: :error
  def flat_map({:ok, v}, f), do: f.(v) 
end

Which you could use like:

[1, 2, 3]
|> ListMonad.flat_map(&[&1*2]) # Result: [2, 4, 6]
|> ListMonad.flat_map(&if(&1>=4, do: [], else: [&1])) # Returns: [2, 4]

21
|> OkMonad.wrap() # Returns {:ok, 21}
|> OkMonad.flat_map(&{:ok, &1&2}) # Returns: {:ok, 42}
|> OkMonad.flat_map(&if(&1>30, do: :error, else: {:ok, &1})) # Returns: :error
|> OkMonad.flat_map(&{:ok, &1&2}) # Still returns just: :error

Any kind of container can become a monad if you can define anything that follows the above requirements. In a properly typed system you can make generic functions that work over it all, but in Elixir you need some way to define types, the easiest way is to just name them somehow, like via a module name ListMonad/OkMonad or via atoms passed in plain instances there-of (which is what Enum.wrap/2 does) or whatever.

But overall yeah, Monads are dead-stupid-simple, they just have a few rules that make them complex, but they are so very simple.

27 Likes