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:

  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 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

__using__(opts)

Helper for defining monads.

Just use Monad in your monad module and define return/1 and bind/2 to get the m macro.

Callbacks

bind(monad, list)

Specs

bind(monad, (any -> monad)) :: monad

Bind a value in the monad to the passed function which returns a new monad.

return(any)

Specs

return(any) :: monad

Inject a value into a monad.