Monad behaviour
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 or read ”Real World Haskell” or ”Learn You a Haskell for Great Good!”.
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:
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”.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.
- 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 uselet
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 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.
Summary
Macros
Helper for defining monads
Callbacks
Bind a value in the monad to the passed function which returns a new monad
Inject a value into a monad
Types
monad :: any
Macros
Callbacks
Bind a value in the monad to the passed function which returns a new monad.