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

  1. Maybe
  2. Error
  3. Reader
  4. Writer
  5. State

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

Reference

  1. Monad
  2. MonadEx
  3. Three Useful Monads