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:
- 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).
- 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 ElixirEnum.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 realunit
in elixir is something like the list constructor like[42]
or{42}
or%{a: 42}
or so forth). - 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]
andflat_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. - The first law being that (I’ll use elixir’s terminology for these of
wrap
/flat_map
)wrap
when passed toflat_map
is the same as the original instance, I.E.something === flat_map(something, &wrap(&1, theType)
andflat_map(wrap(v), f) === f.(v)
(it is this why Elixir’s flat_map is not monadic, it breaks this law). - 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.