├── .gitignore ├── README.md ├── lib ├── error_m.ex ├── list_m.ex └── monad.ex ├── mix.exs └── test ├── monad_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /ebin 2 | /deps 3 | erl_crash.dump 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir Monads 2 | 3 | This provides a monadic system for [Elixir][elixir], a Ruby-flavored 4 | language for the [Erlang VM][erlang]. 5 | 6 | [elixir]: http://elixir-lang.org/ 7 | [erlang]: http://www.erlang.org/ 8 | 9 | When dealing with Erlang libraries, several common patterns emerge: 10 | 11 | case Library.might_fail() do 12 | {:ok, value} -> 13 | case Library.also_might_fail(value) do 14 | {:ok, something} -> 15 | some_pid <- {:ok, something} 16 | {:error, reason} -> 17 | some_pid <- {:error, reason} 18 | end 19 | {:error, reason} -> 20 | some_pid <- {:error, reason} 21 | end 22 | 23 | By stealing the marvelous idea of Monads from the more mainstream 24 | functional languages, you can abstract out that tree like this: 25 | 26 | import Monad 27 | import ErrorM 28 | 29 | some_pid <- (monad ErrorM do 30 | value <- Library.might_fail() 31 | Library.also_might_fail(value) 32 | end) 33 | 34 | Wasn't that easy? 35 | -------------------------------------------------------------------------------- /lib/error_m.ex: -------------------------------------------------------------------------------- 1 | defmodule ErrorM do 2 | @moduledoc """ 3 | The error monad. 4 | 5 | All actions are expected to return either `{:ok, value}` or `{:error, reason}`. 6 | If an error value (`{:error, reason}`) is passed to bind it will be returned 7 | immediately, the function passed will not be executed (in other words as soon 8 | as an error is detected further computation is aborted). 9 | 10 | Return puts the value inside an `{:ok, value}` tuple. 11 | 12 | ## Examples 13 | 14 | iex> require Monad 15 | iex> monad ErrorM do 16 | ...> a <- { :ok, 2 } 17 | ...> b <- { :ok, 3 } 18 | ...> return a * b 19 | ...> end 20 | { :ok, 6 } 21 | 22 | iex> monad ErrorM do 23 | ...> a <- { :error, "boom" } 24 | ...> b <- { :ok, 3 } 25 | ...> return a * b 26 | ...> end 27 | { :error, "boom" } 28 | 29 | iex> monad ErrorM do 30 | ...> a <- { :ok, 2 } 31 | ...> b <- { :error, "boom" } 32 | ...> return a * b 33 | ...> end 34 | { :error, "boom" } 35 | 36 | iex> monad ErrorM do 37 | ...> a <- { :ok, 2 } 38 | ...> b <- { :error, "boom" } 39 | ...> return a * b 40 | ...> end 41 | { :error, "boom" } 42 | 43 | """ 44 | 45 | def bind(x, f) 46 | def bind({:ok, a}, f) do 47 | f.(a) 48 | end 49 | def bind({:error, reason}, _f) do 50 | {:error, reason} 51 | end 52 | 53 | def return(a) do 54 | {:ok, a} 55 | end 56 | 57 | def fail(reason) do 58 | {:error, reason} 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/list_m.ex: -------------------------------------------------------------------------------- 1 | defmodule ListM do 2 | @moduledoc """ 3 | The list monad. 4 | 5 | Well, technically more of the Enum monad as it will accept any Enumerable, it 6 | will return a list though. 7 | 8 | In this monad bind is simply Enum.flat_map and return puts its argument in a 9 | list (so it creates a list with one value). 10 | 11 | ## Examples 12 | 13 | iex> require Monad 14 | iex> monad ListM do 15 | ...> a <- [1, 2, 3] 16 | ...> b <- [1, 2, 3] 17 | ...> return { a, b } 18 | ...> end 19 | [{1,1},{1,2},{1,3},{2,1},{2,2},{2,3},{3,1},{3,2},{3,3}] 20 | 21 | iex> monad ListM do 22 | ...> a <- [1, 2, 3] 23 | ...> b <- [1, 2, 3] 24 | ...> return a * b 25 | ...> end 26 | [1, 2, 3, 2, 4, 6, 3, 6, 9] 27 | 28 | iex> monad ListM do 29 | ...> return 1 30 | ...> end 31 | [1] 32 | 33 | """ 34 | def bind(x, f) do 35 | Enum.flat_map(x, f) 36 | end 37 | 38 | def return(a) do 39 | [a] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/monad.ex: -------------------------------------------------------------------------------- 1 | defmodule Monad do 2 | def line(mod_name, [ {:<-, _, [left, right]} | [] ]) do 3 | quote do 4 | f = fn (unquote(left)) -> 5 | unquote(left) 6 | end 7 | evaluated = unquote(right) 8 | unquote(mod_name).bind(evaluated, f) 9 | evaluated 10 | end 11 | end 12 | 13 | def line(mod_name, [ {:<-, _, [left, right]} | rest ]) do 14 | quote do 15 | f = fn unquote(left) -> 16 | unquote(line(mod_name, rest)) 17 | end 18 | unquote(mod_name).bind(unquote(right), f) 19 | end 20 | end 21 | 22 | def line(mod_name, [ other | [] ]) do 23 | case other do 24 | {:return, lineno, args} -> 25 | {{:., lineno, [mod_name, :return]}, lineno, args} 26 | _ -> 27 | other 28 | end 29 | end 30 | 31 | def line(mod_name, [ other | rest ]) do 32 | quote do 33 | f = fn (_) -> 34 | unquote(line(mod_name, rest)) 35 | end 36 | unquote(mod_name).bind(unquote(other), f) 37 | end 38 | end 39 | 40 | defmacro monad(mod_name, do: block) do 41 | n = Macro.expand mod_name, __CALLER__ 42 | case block do 43 | {:__block__, _, unwrapped} -> line(n, unwrapped) 44 | _ -> line(n, [block]) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Monad.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ app: :monad, 6 | version: "0.0.1", 7 | deps: deps ] 8 | end 9 | 10 | # Configuration for the OTP application 11 | def application do 12 | [] 13 | end 14 | 15 | # Returns the list of dependencies in the format: 16 | # { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" } 17 | defp deps do 18 | [] 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/monad_test.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | import Monad 4 | 5 | require ErrorM 6 | 7 | defmodule ErrorMonadTest do 8 | require ExUnit.DocTest 9 | use ExUnit.Case 10 | 11 | doctest ErrorM 12 | 13 | # Error Monad 14 | def error_start() do 15 | {:ok, :a_value} 16 | end 17 | def error_good() do 18 | {:ok, :another_value} 19 | end 20 | def error_bad() do 21 | {:error, :some_failure} 22 | end 23 | 24 | test "error monad basics" do 25 | assert (monad ErrorM do 26 | ErrorMonadTest.error_start() 27 | end) == {:ok, :a_value} 28 | end 29 | 30 | test "error monad bind" do 31 | assert (monad ErrorM do 32 | something <- ErrorMonadTest.error_start() 33 | end) == {:ok, :a_value} 34 | end 35 | 36 | test "error multi-step bind" do 37 | assert (monad ErrorM do 38 | _a_value <- ErrorMonadTest.error_start() 39 | b_value <- ErrorMonadTest.error_good() 40 | return b_value 41 | end) == {:ok, :another_value} 42 | end 43 | 44 | test "error monad return" do 45 | assert (monad ErrorM do 46 | return :a_value 47 | end) == {:ok, :a_value} 48 | end 49 | 50 | test "error monad fail" do 51 | assert (monad ErrorM do 52 | a_value <- ErrorMonadTest.error_start() 53 | _b_value <- ErrorMonadTest.error_bad() 54 | return a_value 55 | end) == {:error, :some_failure} 56 | end 57 | 58 | end 59 | 60 | defmodule ListMonadTest do 61 | require ExUnit.DocTest 62 | use ExUnit.Case, async: true 63 | doctest ListM 64 | end 65 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | --------------------------------------------------------------------------------