defmodule Rustic.Maybe do
@moduledoc """
Implementation of the Maybe monad, inspired by Rust's Option type.
"""
@typedoc "Describe an empty Maybe monad."
@type nothing :: :nothing
@typedoc "Describe a Maybe monad containing some value."
@type some :: {:some, any()}
@typedoc "Describe a Maybe monad."
@type t :: nothing() | some()
@typedoc "Describe an Ok result."
@type ok :: {:ok, any()}
@typedoc "Describe an Error result."
@type error :: {:error, any()}
@typedoc "Describe a Result monad."
@type result :: ok() | error()
@doc "Returns an empty Maybe monad."
@spec nothing() :: nothing()
def nothing(), do: :nothing
@doc "Encaspulate a value into a Maybe monad."
@spec some(any()) :: some()
def some(v), do: {:some, v}
@doc """
Check if a value is an empty Maybe monad.
iex> Rustic.Maybe.nothing() |> Rustic.Maybe.is_nothing()
true
iex> Rustic.Maybe.some(1) |> Rustic.Maybe.is_nothing()
false
iex> 1 |> Rustic.Maybe.is_nothing()
false
"""
defguard is_nothing(v) when v == :nothing
@doc """
Check if a value is a non-empty Maybe monad.
iex> Rustic.Maybe.nothing() |> Rustic.Maybe.is_some()
false
iex> Rustic.Maybe.some(1) |> Rustic.Maybe.is_some()
true
iex> 1 |> Rustic.Maybe.is_some()
false
"""
defguard is_some(v) when is_tuple(v) and elem(v, 0) == :some
@doc """
Check if a value is a Maybe monad.
iex> Rustic.Maybe.nothing() |> Rustic.Maybe.is_maybe()
true
iex> Rustic.Maybe.some(1) |> Rustic.Maybe.is_maybe()
true
iex> 1 |> Rustic.Maybe.is_maybe()
false
"""
defguard is_maybe(v) when is_nothing(v) or is_some(v)
@doc """
Get the value of a Maybe monad or raise an exception if it were empty.
iex> Rustic.Maybe.some(1) |> Rustic.Maybe.unwrap!()
1
iex> Rustic.Maybe.nothing() |> Rustic.Maybe.unwrap!()
** (ArgumentError) trying to unwrap an empty Maybe monad
"""
@spec unwrap!(t()) :: any()
def unwrap!({:some, value}), do: value
def unwrap!(:nothing) do
raise ArgumentError, message: "trying to unwrap an empty Maybe monad"
end
@doc """
Get the value of a Maybe monad or a default value if it were empty.
iex> Rustic.Maybe.some(1) |> Rustic.Maybe.unwrap_or(2)
1
iex> Rustic.Maybe.nothing() |> Rustic.Maybe.unwrap_or(2)
2
"""
@spec unwrap_or(t(), any()) :: any()
def unwrap_or(:nothing, default_value), do: default_value
def unwrap_or({:some, value}, _default_value), do: value
@doc """
Get the value of a Maybe monad or compute a default value if it were empty.
iex> Rustic.Maybe.some(1) |> Rustic.Maybe.unwrap_or_else(fn -> 2 end)
1
iex> Rustic.Maybe.nothing() |> Rustic.Maybe.unwrap_or_else(fn -> 2 end)
2
"""
@spec unwrap_or_else(t(), (() -> any())) :: any()
def unwrap_or_else(:nothing, default_func), do: default_func.()
def unwrap_or_else({:some, value}, _default_func), do: value
@doc """
Returns the Maybe monad contained in a Maybe monad, or nothing.
iex> Rustic.Maybe.some(Rustic.Maybe.some(1))
...> |> Rustic.Maybe.flatten()
{:some, 1}
iex> Rustic.Maybe.some(Rustic.Maybe.nothing())
...> |> Rustic.Maybe.flatten()
:nothing
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.flatten()
:nothing
"""
@spec flatten(t()) :: t()
def flatten(:nothing), do: :nothing
def flatten({:some, mval}) when is_maybe(mval), do: mval
@doc """
Boolean AND operation on 2 Maybe monads which returns the right one.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.and_other(Rustic.Maybe.some(2))
{:some, 2}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.and_other(Rustic.Maybe.some(2))
:nothing
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.and_other(Rustic.Maybe.nothing())
:nothing
"""
@spec and_other(t(), t()) :: t()
def and_other(:nothing, _), do: :nothing
def and_other({:some, _}, other_mval), do: other_mval
@doc """
Maps the value of a non-empty Maybe monad to a new Maybe monad.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.and_then(fn n -> Rustic.Maybe.some(n + 1) end)
{:some, 2}
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.and_then(fn _ -> :nothing end)
:nothing
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.and_then(fn n -> Rustic.Maybe.some(n + 1) end)
:nothing
"""
@spec and_then(t(), (any() -> t())) :: t()
def and_then(:nothing, _), do: :nothing
def and_then({:some, val}, func), do: func.(val)
@doc """
Boolean OR operation on 2 Maybe monads which returns the left one.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.or_other(Rustic.Maybe.some(2))
{:some, 1}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.or_other(Rustic.Maybe.some(2))
{:some, 2}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.or_other(Rustic.Maybe.nothing())
:nothing
"""
@spec or_other(t(), t()) :: t()
def or_other(:nothing, other_mval), do: other_mval
def or_other({:some, _} = mval, _), do: mval
@doc """
Returns the Maybe monad or execute a function returing a Maybe monad.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.or_else(fn -> Rustic.Maybe.some(2) end)
{:some, 1}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.or_else(fn -> Rustic.Maybe.some(2) end)
{:some, 2}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.or_else(fn -> Rustic.Maybe.nothing() end)
:nothing
"""
@spec or_else(t(), (() -> t())) :: t()
def or_else(:nothing, func), do: func.()
def or_else({:some, _} = mval, _), do: mval
@doc """
Boolean XOR operation on 2 Maybe monad which returns some value if and only if
one of them is non empty.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.xor_other(Rustic.Maybe.some(2))
:nothing
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.xor_other(Rustic.Maybe.nothing())
:nothing
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.xor_other(Rustic.Maybe.nothing())
{:some, 1}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.xor_other(Rustic.Maybe.some(2))
{:some, 2}
"""
@spec xor_other(t(), t()) :: t()
def xor_other(:nothing, :nothing), do: :nothing
def xor_other({:some, _}, {:some, _}), do: :nothing
def xor_other({:some, val}, :nothing), do: {:some, val}
def xor_other(:nothing, {:some, val}), do: {:some, val}
@doc """
Apply a function to the value of a Maybe monad.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.map(fn n -> n + 1 end)
{:some, 2}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.map(fn n -> n + 1 end)
:nothing
"""
@spec map(t(), (any() -> any())) :: t()
def map(:nothing, _), do: :nothing
def map({:some, val}, func), do: {:some, func.(val)}
@doc """
Return the default result for an empty Maybe monad, or Apply a function to
its value and return the result.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.map_or(3, fn n -> n + 1 end)
2
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.map_or(3, fn n -> n + 1 end)
3
"""
@spec map_or(t(), any(), (any() -> any())) :: any()
def map_or(:nothing, default_val, _), do: default_val
def map_or({:some, val}, _, func), do: func.(val)
@doc """
Compute the default result for an empty Maybe monad, or Apply a function to
its value and return the result.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.map_or_else(fn -> 3 end, fn n -> n + 1 end)
2
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.map_or_else(fn -> 3 end, fn n -> n + 1 end)
3
"""
@spec map_or_else(t(), (() -> any()), (any() -> any())) :: any()
def map_or_else(:nothing, default_func, _), do: default_func.()
def map_or_else({:some, val}, _, func), do: func.(val)
@doc """
Returns the Maybe monad only if predicate returns true for its contained
value.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.filter(fn n -> n > 0 end)
{:some, 1}
iex> Rustic.Maybe.some(-1)
...> |> Rustic.Maybe.filter(fn n -> n > 0 end)
:nothing
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.filter(fn n -> n > 0 end)
:nothing
"""
@spec filter(t(), (any() -> boolean())) :: t()
def filter(:nothing, _), do: :nothing
def filter({:some, val}, predicate) do
if predicate.(val) do
{:some, val}
else
:nothing
end
end
@doc """
Transform a Maybe monad into a Result monad, turning nothing into an error.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.ok_or(:no_value)
{:ok, 1}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.ok_or(:no_value)
{:error, :no_value}
"""
@spec ok_or(t(), any()) :: result()
def ok_or(:nothing, err), do: {:error, err}
def ok_or({:some, val}, _), do: {:ok, val}
@doc """
Transform a Maybe monad into a Result monad turning nothing into a computed
error.
iex> Rustic.Maybe.some(1)
...> |> Rustic.Maybe.ok_or_else(fn -> :no_value end)
{:ok, 1}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.ok_or_else(fn -> :no_value end)
{:error, :no_value}
"""
@spec ok_or_else(t(), any()) :: result()
def ok_or_else(:nothing, err_func), do: {:error, err_func.()}
def ok_or_else({:some, val}, _), do: {:ok, val}
@doc """
Transform a Maybe monad containing a Result monad to a Result monad containing
a Maybe monad.
iex> Rustic.Maybe.some({:ok, 1})
...> |> Rustic.Maybe.transpose()
{:ok, {:some, 1}}
iex> Rustic.Maybe.some(:ok)
...> |> Rustic.Maybe.transpose()
{:ok, {:some, nil}}
iex> Rustic.Maybe.some({:error, :no_value})
...> |> Rustic.Maybe.transpose()
{:error, :no_value}
iex> Rustic.Maybe.nothing()
...> |> Rustic.Maybe.transpose()
{:ok, :nothing}
"""
@spec transpose(t()) :: result()
def transpose(:nothing), do: {:ok, :nothing}
def transpose({:some, :ok}), do: {:ok, {:some, nil}}
def transpose({:some, {:ok, val}}), do: {:ok, {:some, val}}
def transpose({:some, {:error, reason}}), do: {:error, reason}
end