Monad (Monaditto v0.4.0)
View SourceIntroduction
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
Functions
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
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
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"}
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}
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)
...> :errorTuples 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"}
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
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}
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"}
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"]}
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]}
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
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