Monads in Elixir
This post is not about what monads are, I expect that you already know about them. The post is to see how you can use monads in Elixir using already created libraries.
Monads are a great way of handling side effect in functional language. It makes the code much more readable, maintainable and composable. Elixir language is not bundled with monads but it has an even more powerful construct i.e. meta programming. Using metaprogramming you can add monads in your program and use it in such a way that they feel like part of the language itself.
There are multiple monad libraries available in Elixir. I explored two libraries which I think had the most complete implementation (relatively) of Monads those were Monad and MonadEx. Both are great libraries. I think the MonadEx is a more complete library with the required functions to use monad. I've been using rmies/monad because the macro provided by rmies/monad feels more like part of the language as they chain with |>
, in case of rob-bron/MonadEx you have to create anonymous functions to chain.
We will explore rmies/monad library in this post and see how to use the following monads
Getting dependency: There is some issue in the rmies/monad library, so the examples here won't work with that for now. I've fixed the issue, you should include the following dependency for now till I send the pull request and fix goes to the main repo.
{:monad, git: "https://github.com/zabirauf/monad.git", branch: "develop"}
Operators
Bind
The bind operator is not >>=
like Haskell but instead it's |>
. The pipe operator only works as bind operator in a specific block (which I'll discuss). The type of bind operator is
f a -> (a -> f b) -> f b
It takes a wrapped value f a
and a function which takes a value and returns a wrapped value a -> f b
. Bind applies value in the wrapper to that function and gets another wrapped value.
It currently does not have operators for Functor.fmap and Applicative.apply but if you are interested in that, it's available in the MonadEx library.
Monads
Lets see some example of all the monads provided by MonadEx library
Maybe:
Maybe monad is the simplest where the return value is either something or nothing. It is represented as
# In case of something
{:just, some_value}
# In case of nothing
:nothing
But you don't have to create those structs yourself, there are functions to do that.
defmodule ExampleMaybe do
require Monad.Maybe, as: Maybe
import Maybe
# The division operator which return something if successfull
# otherwise in case of failure returns nothing
def my_div(_numerator,0) do
# Returns nothing
Maybe.fail(nil)
end
def my_div(numerator, denominator) do
# Returns {:something, result}
Maybe.return(numerator/denominator)
end
# Sum always returns something
def my_sum(a,b) do
# Returns {:just, result}
Maybe.return(a+b)
end
def output(x) do
case is_nothing(x) do
true -> IO.puts "No value gotten"
false -> x |> from_just |> IO.puts
end
end
# A successful scenario
def scenario1() do
# The |> opeartor in Maybe.p acts as bind
val =
Maybe.p do
Maybe.return(100)
|> my_sum(50)
|> my_div(2)
end
output(val) # Outputs "75.0"
end
# A scenario where division by zero is done
def scenario2() do
val =
Maybe.p do
Maybe.return(0)
|> my_sum(0)
|> (&my_div(100, &1)).()
end
output(val) # Outputs "No value gotten"
end
end
You use the Monad.p do .. end
block to use the bind operator and all the functions are chained using |>
which becomes bind operator in this block. The functions used in it should return either {:just, something}
created by Maybe.return(something)
or return :nothing
using Maybe.fail(nil)
.
Error
The Error monad is very similar to Maybe monad but instead of nothing you return an error with a reason. It is represented as
# In case of a valid value
{:ok, value}
# In case of error
{:error, error_reason}
The Error monad is missing required functions to extract the value or error from the structure, so we have to depend directly on the structure.
Lets see and example of it which is very similar to Maybe monad example.
defmodule ExampleError do
require Monad.Error, as: Error
import Error
# The division operator which return ok and result if successfull
# otherwise in case of failure returns error with reason
def my_div(_numerator,0) do
# Returns {:error, reason}
Error.fail("Division by zero is not allowed")
end
def my_div(numerator, denominator) do
# Returns {:ok, result}
Error.return(numerator/denominator)
end
def my_sum(a,b) do
# Sum always returns {:ok, result}
Error.return(a+b)
end
def output(x) do
case x do
{:error, reason} -> IO.puts "Error: #{reason}"
{:ok, value} -> IO.puts value
end
end
# Successful scenario
def scenario1() do
# Here the |> opeartor in Error.p acts as bind operator
val =
Error.p do
Error.return(100)
|> my_sum(50)
|> my_div(2)
end
output(val) # Outputs "75.0"
end
# Scenario where division fails due to division by zero
def scenario2() do
val =
Error.p do
Error.return(0)
|> my_sum(0)
|> (&my_div(100, &1)).()
end
output(val) # Outputs "Error: Division by zero is not allowed"
end
end
The functions used for Error monad should return either {:ok, something}
created by Error.return(something)
or return {:error, reason}
using Error.fail(reason)
.
This monad is same as the one that I recreated in the post Railway Oriented Programming in Elixir. You can use this instead of recreating and hence making error handling so much easier and elegant.
Reader
Reader monad is used to pass around a context across all of your function composition. Examples of reader monad include
- Dependency injection, where you want to pass an external dependency across all your functions.
- Passing in configuration
- Passing in user request
The reader monad passes whatever you want silently i.e. you don't have to make it an argument and you can get its value anytime in the function. Lets see an example of it
defmodule ExampleReader do
require Monad.Reader, as: Reader
import Reader
# We create a greeting string by getting name as argument
# and getting greeting from the reader monad
def greeting(name) do
# The value for the reader can be read in Reader.m block
Reader.m do
# Getting the greeting by calling ask function
greeting <- ask
return "#{greeting}, #{name}"
end
end
# If the greeting is hello it puts exclamation mark
# otherwise adds . at end of string
def done(input) do
Reader.m do
greeting <- ask
case (greeting == "Hello") do
true -> return "#{input} !!!"
false -> return "#{input}."
end
end
end
# Adds a new line to whatever string it gets
def add_newline(input) do
return("#{input}\n")
end
# Outputs "Hello, Zohaib !!!\n"
def scenario1() do
# You use the run function to call put the value and
# execute the functions with that value in context.
#
# You compose the functions by using |> operator
# which acts as bind operator in Reader.p block
run("Hello",
Reader.p do
return("Zohaib")
|> greeting
|> done
|> add_newline
end)
end
# Outputs "Welcome, Zohaib.\n"
def scenario2() do
run("Welcome",
Reader.p do
return("Zohaib")
|> greeting
|> done
|> add_newline
end)
end
end
Here we use return
from the functions which we want in the composition. If we want to read the value of the context then under the Reader.m do .. end
block we ask for its value by variable_name <- ask
, simple as that. To compose the functions we use the Reader.p do .. end
block and use the |>
pipe operator to compose the functions. We use the run
function where the first argument is what is passed across the functions and the second argument is a function or composition of functions.
Writer
The writer monad is just what the name suggests i.e. it allows you to write some values. A good example of using writer monad is to have logs with each operation that you do.
Lets see the example
defmodule ListWriter do
use Monad.Writer
# Called when the writer monad is started
# to initialize
def initial do
[]
end
# Called whenever you put new value to the writer
def combine(new, acc) do
acc ++ new
end
end
defmodule ExampleWriter do
import ListWriter
# We return the sum and write logs to the writer
def my_sum(a,b) do
# If you want to write then you have to
# call tell in your created writers m block,
# As we defined ListWriter so in this
# case its ListWriter.m
ListWriter.m do
tell ["Adding #{a} and #{b}"]
return a+b
end
end
# We return the subtraction and write logs to the writer
def my_subtraction(a,b) do
ListWriter.m do
tell ["Subtracting #{a} and #{b}"]
return a-b
end
end
# Outputs "{8, ["Adding 5 and 10", "Subtracting 15 and 7"]}"
def scenario1() do
# We run the writer monad by calling run
# and pass in the function or composition of function
#
# The |> operators becomes bind in p block of your
# writer. As we defined ListWriter so its ListWriter.p
# block here
run(ListWriter.p do
return(5)
|> my_sum(10)
|> my_subtraction(7)
end)
end
end
Here we have to create a module that implements some methods required by the writer monad. In the above example we have created ListWriter
and that implements two required functions initial
and combine(new, acc)
. In initial
you initialize the structure to which values will be written and in combine(new,acc)
you write the new
value to the acc
accumulated values. This module should be defined external to the module in which it is being used.
The way to use it is that in the functions you use the ListWriter.m do .. end
block and in that you use tell
to write the value, this in turn will call the combine
function where you add that value to your structure.
The functions should use return
to return whatever they want. You can compose function by chaining them using |>
pipe operator in ListWriter.p do .. end
block.
The output of scenario1
is {8, ["Adding 5 and 10", "Subtracting 15 and 7"]}
where the first elment is the result of your functions and second element is the list of logs you wrote.
State
State monad is similar to Reader monad but in it along with reading the value you can also write the value hence maintaining side effect across functions in a more functional and maintainable way.
defmodule ExampleState do
require Monad.State, as: State
import State
# Returns sum and increments state
def my_sum(a,b) do
# Use get to read the state and
# use put to write the state. These
# functions should be called in State.m block
State.m do
x <- get
put x+1
return a+b
end
end
# Returns subtraction and increments state
def my_subtraction(a,b) do
State.m do
x <- get
put x+1
return a-b
end
end
# Outputs "{8, 2}"
def scenario1() do
# Call run to run the state monad
run(0, State.p do
return(5)
|> my_sum(10)
|> my_subtraction(7)
end)
end
end
In this monad we use the State.m do .. end
block in the functions where we want to read and write the state. We call variable_name <- get
to get the state and put new_state
to update the state. In the above example we pass 0 and increment it as the functions are called. In the end the output of scenario1
is {8, 2}
where the first value is the output of the composition and second is the state at the end.
You can get all these example from the git repo