Packages

Monads and do-syntax for Elixir

Current section

Files

Jump to
monad lib monad.ex
Raw

lib/monad.ex

defmodule Monad do
use Behaviour
@moduledoc """
Behaviour that provides monadic do-notation and pipe-notation.
## Terminology
The term "monad" is used here fairly loosely to refer to the whole concept of
monads. One way of looking at monads is as a kind of "programmable
semicolon". Monads define what happens between the evaluation of the
expressions. They control how and whether the results from one expression are
passed to the next. For a better explanation of what monads are, look
elsewhere, the internet is full of good (and not so good) monad tutorials;
e.g. have a look at the
[HaskellWiki](http://www.haskell.org/haskellwiki/Monads) or read ["Real World
Haskell"](http://book.realworldhaskell.org/) or ["Learn You a Haskell for
Great Good!"](http://learnyouahaskell.com/).
## Usage
To use do-notation you need a module that implements Monad's callbacks,
i.e. the module needs to have `return/1` and `bind/2`. This allows you to
write stuff like:
def call_if_safe_div(f, x, y) do
require Monad.Maybe, as: Maybe
import Maybe
Maybe.m do
result <- case y == 0 do
true -> fail "division by zero"
false -> return x / y
end
return f.(result)
end
end
The example above uses the `Maybe` monad to define `call_if_safe_div/3`. This
function takes three arguments: a function `f` and two numbers `x` and `y`. If
`x` is divisible by `y`, then `f` is called with `x / y` and the return value
is `{:just, f.(x / y)}`, else the computation fails and the return value is
`:nothing`.
## Do-Notation
The do-notation supported is pretty simple. Basically there are three rules to
remember:
1. Every "statement" (i.e. thing on it's own line or separated by `;`) has to
return a monadic value unless it's a "let statement".
2. To use the value "inside" a monad write "pattern <- action" where "pattern"
is a normal Elixir pattern and "action" is some expression which returns a
monadic value.
3. To use ordinary Elixir code inside a do-notation block prefix it with
`let`. For multiple expressions or those for which precedence rules cause
annoyances you can use `let` with a do block.
## Defining Monads
To define your own monad create a module and use `use Monad`. This marks the
module as a monad behaviour. You'll need to define `return/1` and `bind/2`.
Here's an example which defines the `List` monad:
defmodule Monad.List do
use Monad
def return(x), do: [x]
def bind(x, f), do: Enum.flat_map(x, f)
end
### Monad Laws
`return/1` and `bind/2` need to obey a few rules (the so-called "monad laws")
to avoid surprising the user. In the following equivalences `M` stands for
your monad module, `a` for an arbitrary value, `m` for a monadic value and `f`
and `g` for functions that given a value return a new monadic value.
Equivalence means you can always substitute the left side for the right side
and vice versa in an expression without changing the result or side-effects
* "left identity": `M.bind(M.return(m), f) <=> f.(m)`
* "right identity": `M.bind(m, &M.return/1) <=> m`
* "associativity": `M.bind(m, f) |> M.bind(g) <=> m |> M.bind(fn y -> M.bind(f.(y), g))`
See the [HaskellWiki](http://www.haskell.org/haskellwiki/Monad_laws) for more
explanation.
## Pipe Support
For monads that implement the `Monad.Pipeline` behaviour the `p` macro
supports monadic pipelines. For example:
Error.p, (File.read("/tmp/foo")
|> Code.string_to_quoted(file: "/tmp/foo")
|> Macro.safe_term)
If any of the terms returns `{:error, x}` then that's the return value of the
pipeline, when a term returns `{:ok, x}` the value `x` is passed to the next.
You can also use a `do`-block for less clutter:
Error.p do
File.read("/tmp/foo")
|> Code.string_to_quoted(file: "/tmp/foo")
|> Macro.safe_term
end
Under the hood pipe binding works by calling the `pipebind` function in a
monad module. If you use `use Monad.Pipeline` one is automatically created
(you can still override it though).
The `pipebind` function receives the AST form of a value argument and a
function. It has to return some AST that essentially does what bind does but
with a function that's missing the first argument. See the example below.
"""
use Behaviour
@doc """
Helper for defining monads.
Just `use Monad` in your monad module and define `return/1` and
`bind/2` to get the `m` macro.
"""
defmacro __using__(_opts) do
quote location: :keep do
@behaviour Monad
@doc """
Monad do-notation.
See the `Monad` module documentation and the
""" <> "`#{inspect __MODULE__}`" <> """
module documentation
"""
defmacro m(do: block) do
res = case block do
nil ->
raise ArgumentError, message: "missing or empty do block"
{:__block__, meta, exprs} ->
{:__block__, meta, Monad.Internal.expand(__MODULE__, exprs)}
expr ->
{:__block__, [], Monad.Internal.expand(__MODULE__, [expr])}
end
Monad.Internal.transform_return(__MODULE__, res)
end
end
end
@type monad :: any
@doc """
Inject a value into a monad.
"""
defcallback return(any) :: monad
@doc """
Bind a value in the monad to the passed function which returns a new monad.
"""
defcallback bind(monad, (any -> monad)) :: monad
end
defmodule Monad.Internal do
# Internal helpers for the monad stuff.
@moduledoc false
@doc false
def expand(_, [expr]) do
[expr]
end
def expand(mod, [{:let, _, let_exprs} | exprs]) do
if length(let_exprs) == 1 and is_list(hd(let_exprs)) do
case Keyword.fetch(hd(let_exprs), :do) do
:error ->
let_exprs ++ expand(mod, exprs)
{:ok, e} ->
[e | expand(mod, exprs)]
end
else
let_exprs ++ expand(mod, exprs)
end
end
def expand(mod, [{:<-, _, [lhs, rhs]} | exprs]) do
# x <- m ==> bind(b, fn x -> ... end)
expand_bind(mod, lhs, rhs, exprs)
end
def expand(mod, [expr | exprs]) do
# m ==> bind(b, fn _ -> ... end)
expand_bind(mod, quote(do: _), expr, exprs)
end
defp expand_bind(mod, lhs, rhs, exprs) do
[quote do
unquote(mod).bind(unquote(rhs),
fn unquote(lhs) ->
unquote_splicing(expand(mod, exprs))
end)
end]
end
@doc false
# Find unqualified mentions of `return` in the AST and translate them to
# `mod.return`.
def transform_return(mod, {:return, _, [arg]} ) do
quote do unquote(mod).return(unquote(arg)) end
end
def transform_return(mod, {call, meta, args}) do
{call, meta, transform_return(mod, args)}
end
def transform_return(mod, l) when is_list(l) do
Enum.map(l, &transform_return(mod, &1))
end
def transform_return(mod, {l, r}) do
{ transform_return(mod, l), transform_return(mod, r) }
end
def transform_return(_mod, x) do
x
end
end
defmodule Monad.Pipeline do
@moduledoc """
Helper for defining monads that support pipelines.
Just use `use Monad.Pipeline` in your monad module and define `return/1` and
`bind/2` and get `pipebind/2` for free.
"""
use Behaviour
defmacro __using__(_opts) do
quote location: :keep do
@behaviour Monad.Pipeline
@doc """
Pipeline form of the monad.
See `Monad` module documentation.
"""
defmacro p(pipeline) when is_list(pipeline) do
case pipeline[:do] do
nil ->
raise ArgumentError, message:
"Monad.p called with a list but it's not a keyword list with " <>
"a 'do' key (i.e. not a passed do block)"
{ __block__, _, [expr] } -> p_expand(expr)
expr -> p_expand(expr)
end
end
defmacro p(pipeline), do: p_expand(pipeline)
defp p_expand(pipeline) do
# Enum.reduce is a left fold
Macro.unpipe(pipeline)
|> Enum.reduce(&(__MODULE__.pipebind(&2, &1)))
|> elem(0)
end
def pipebind(x, fc) do
# Unpipe returns a tuple of the value and an insert position (currently
# always 0). We need to do some unwrapping and rewrapping of that tuple
# in order to make things work.
quote location: :keep do
# I think there should be no conflict with the variable used in `fn`,
# but just to be sure let's use an odd variable name.
{bind(unquote(elem(x, 0)), fn _monad_pipebind_arg ->
unquote(Macro.pipe(quote do _monad_pipebind_arg end, elem(fc, 0), 0))
end), 0}
end
end
defoverridable [pipebind: 2]
end
end
@doc """
Like bind/2 but works on ASTs and the second argument should be a function
call where the first argument is missing.
"""
defcallback pipebind(Macro.t, Macro.t) :: Macro.t
end