Monad (Monaditto v0.4.0)

View Source

Introduction

Hi there! This is a library for monads in Elixir - not with new structures, but with idiomatic Elixir tuples

You have already seen monads in Elixir:

jose = %{first_name: "Jose", last_name: "Valim"}
{:ok, "Jose"} = Map.fetch(jose, :first_name)    # Oh gosh! Monad `Maybe` - `Just "Jose"`
:error = Map.fetch(jose, :age)                  # Holly moly! Monad `Maybe` - `Nothing`

So, don't be afraid of monads, you use it every day I don't want you to change your entire codebase and break simplicity of elixir, so you could use any function from this library without wrapping it in a new structure like %Just{}, %Nothing{} etc

Example

You can chain operations naturally using pipelines, just like you do with regular Elixir code:

# Success path
{:ok, "john"}
|> Monad.map(&String.upcase/1)
|> Monad.map(&String.reverse/1)
# => {:ok, "NHOJ"}

# Error path - stops at first error
{:error, :not_found}
|> Monad.map(&String.upcase/1)
|> Monad.map(&String.reverse/1)
# => {:error, :not_found}

# Real-world example
user_id
|> fetch_user()           # {:ok, %User{}} | {:error, :not_found}
|> Monad.map(&User.email/1)
|> Monad.map(&send_email/1)
# => {:ok, :email_sent} | {:error, :not_found}

Other examples you could find in the documentation for dedicated functions

Summary

Functions

Checks if all values are successful ({:ok, ...}), without nested values checking

Checks if there is any error in the data, without nested values checking

Applies a function if data is successful ({:ok, ...}), otherwise applies another function

Applies a function for value part of successful data, otherwise returns the data as is

Applies a function if data is successful ({:ok, ...}), otherwise returns the data as is

Applies a function if data is error, otherwise returns the data as is

Calls a function with the data and returns the data as is

Wraps a function in a try/rescue block and returns {:error, error} if an error is raised

Unwraps a list of results

Combines map and sequence functions

Unwraps a data and returns the default value if the data is error

Unwraps a data, similar to Monad.unwrap/2 but raises an error if the data is error

Types

error()

@type error() :: {:error, any()} | :error

ok()

@type ok() :: {:ok, any()} | :ok

result()

@type result() :: ok() | error()

Functions

all_ok?(data)

@spec all_ok?([result()] | result()) :: boolean()

Checks if all values are successful ({:ok, ...}), without nested values checking

Examples

iex> Monad.all_ok?({:ok, "John"})
...> true

iex> Monad.all_ok?([{:ok, "John"}, {:ok, "Brother Tom"}])
...> true

iex> Monad.all_ok?([{:ok, "John"}, {:error, :not_found}])
...> false

iex> Monad.all_ok?([{:ok, "John"}, {:error, :not_found}, {:ok, "Jane"}])
...> false

any_error?(data)

@spec any_error?([result()] | result()) :: boolean()

Checks if there is any error in the data, without nested values checking

Examples

iex> Monad.any_error?({:ok, "John"})
...> false

iex> Monad.any_error?([{:ok, "John"}, {:ok, "Brother Tom"}])
...> false

iex> Monad.any_error?({:error, :not_found})
...> true

iex> Monad.any_error?([{:error, :not_found}, {:ok, "John"}])
...> true

bimap(data, success_fun, error_fun)

@spec bimap(result(), (any() -> any()), (any() -> any())) :: result()

Applies a function if data is successful ({:ok, ...}), otherwise applies another function

Similar to Monad.map/2, but for both success and error cases

Examples

iex> greet = fn name -> "Hello, #{name}!" end
iex> error = fn reason -> "Error: #{reason}" end

iex> Monad.bimap({:ok, "John"}, greet, error)
...> {:ok, "Hello, John!"}

iex> Monad.bimap({:error, :not_found}, greet, error)
...> {:error, "Error: not_found"}

flat_map(data, fun)

@spec flat_map([result()], (any() -> result())) :: result()

Applies a function for value part of successful data, otherwise returns the data as is

Examples

iex> Monad.flat_map({:ok, "John"}, fn name -> {:ok, "Hello, #{name}!"} end)
...> {:ok, "Hello, John!"}

iex> Monad.flat_map({:error, :not_found}, fn reason -> {:ok, "Hello, #{reason}!"} end)
...> {:error, :not_found}

iex> [{:ok, "John", 25}, {:ok, "Brother Tom", 30}]
...> |> Monad.flat_map(fn {name, age} -> {:ok, name, age * 2} end)
...> |> Monad.flat_map(fn {name, age} -> {:ok, "#{name} is #{age} years old"} end)
...> {:ok, ["John is 50 years old", "Brother Tom is 60 years old"]}

iex> [{:ok, "John", 25}, {:error, :not_found}, {:ok, "Brother Tom", 30}]
...> |> Monad.flat_map(fn {name, age} -> {:ok, name, age * 2} end)
...> |> Monad.flat_map(fn {name, age} -> {:ok, "#{name} is #{age} years old"} end)
...> {:error, :not_found}

map(data, fun)

@spec map(result(), (any() -> any())) :: result()

Applies a function if data is successful ({:ok, ...}), otherwise returns the data as is

Examples

Most common {:ok, value} | {:error, reason} | :error case:

iex> greet = fn name -> "Hello, #{name}!" end

iex> Monad.map({:ok, "John"}, greet)
...> {:ok, "Hello, John!"}

iex> Monad.map({:error, :not_found}, greet)
...> {:error, :not_found}

iex> Monad.map(:error, greet)
...> :error

Tuples with more than one value are passed as tuple in the mapping function:

iex> Monad.map({:ok, "John", "John's info"}, fn {name, info} -> "Hello, #{name}! #{info}" end)
...> {:ok, "Hello, John! John's info"}

map_error(data, fun)

@spec map_error(result(), (any() -> any())) :: result()

Applies a function if data is error, otherwise returns the data as is

Examples

iex> Monad.map_error({:error, :not_found}, fn reason -> "Error: #{reason}" end)
...> {:error, "Error: not_found"}

iex> Monad.map_error({:ok, "John"}, fn reason -> "Error: #{reason}" end)
...> {:ok, "John"}

iex> Monad.map_error(:error, fn reason -> "Error: #{reason}" end)
...> :error

peek(data, fun)

@spec peek(result(), (any() -> any())) :: result()

Calls a function with the data and returns the data as is

Examples

iex> Monad.peek({:ok, "John"}, fn {:ok, name} -> IO.puts("Hello, #{name}!") end)
...> {:ok, "John"}

iex> Monad.peek({:error, :not_found}, fn {:error, reason} -> IO.puts("Error: #{reason}") end)
...> {:error, :not_found}

safe(fun, after_fun \\ fn -> nil end)

@spec safe((-> any()), (-> any())) :: result()

Wraps a function in a try/rescue block and returns {:error, error} if an error is raised

Optionally accepts an after_fun that will be executed in the after block regardless of success or failure.

Examples

iex> Monad.safe(fn -> 1 / 0 end)
...> {:error, %ArithmeticError{message: "bad argument in arithmetic expression"}}

iex> Monad.safe(fn -> "Hello" end)
...> {:ok, "Hello"}

iex> Monad.safe(fn -> "Success" end, fn -> IO.puts("Cleanup") end)
...> {:ok, "Success"}

sequence(data, strategy \\ :until_error)

@spec sequence([result()], :until_error) :: result()

Unwraps a list of results

During unwrapping, any values after :ok or :error will be grouped into a tuple if there are more than one, but if there is only :error atom instead of tuple, it will be treated as {:error, :error}:

  • {:ok, :here, :there} -> {:here, :there}
  • {:error, :here, :there} -> {:here, :there}
  • {:ok, :here} -> :here
  • {:error, :here} -> :here
  • :error -> {:error, :error}

Strategies:

  • :until_error - stops at the first error (default)

Examples

Common {:ok, value} | {:error, reason} list:

  data = [
    {:ok, "John"},
    {:ok, "Brother Tom"},
    {:error, :not_found},
    {:ok, "Jane"},
    {:error, :bad_query}
  ]

  iex> Monad.sequence(data)
  ...> {:error, :not_found}

Mixed values (yes, it's possible, but remember that God watches you):

  data = [
    {:ok, "John", "John's info"},
    :error,
    {:error, :not_found, "meta info"},
    {:ok, "Brother Tom"},
    {:error, :not_found}
  ]

  iex> Monad.sequence(data)
  ...> :error

  iex> Monad.sequence([{:ok, "John"}, {:ok, "Brother Tom"}])
  ...> {:ok, ["John", "Brother Tom"]}

traverse(data, fun)

@spec traverse([result()] | result(), (any() -> result())) :: result()

Combines map and sequence functions

Examples

iex> Monad.traverse(1..5, & {:ok, &1 * 2})   # map + sequence
...> {:ok, [2, 4, 6, 8, 10]}

iex> map_even = fn x ->
       if rem(x, 2) == 0,
          do: {:ok, x * 2},
          else: {:error, :odd}
     end

iex> Monad.traverse(1..5, map_even)
...> {:error, :odd}

iex> Monad.traverse([2, 4, 6, 10], map_even)
...> {:ok, [4, 8, 12, 20]}

unwrap(data, default \\ nil)

@spec unwrap(result(), any()) :: any()

Unwraps a data and returns the default value if the data is error

Examples

iex> Monad.unwrap({:ok, "John"}, "Default")
...> "John"

iex> Monad.unwrap({:error, :not_found}, "Default")
...> "Default"

iex> Monad.unwrap(:error)
...> nil

unwrap!(data)

@spec unwrap!(result()) :: any()

Unwraps a data, similar to Monad.unwrap/2 but raises an error if the data is error

Examples

iex> Monad.unwrap!({:ok, "John"})
...> "John"

iex> Monad.unwrap!({:error, :not_found})
...> ** (RuntimeError) Error: not_found