This library provides tools to handle three common return values in Elixir
:ok | {:ok, value} | {:error, reason}
- Overview
- Usage
- Differences from Similar Libraries
- Definitions
- Types
- Base
- Helpers
- Mappers
- Known Problems
- Installation
Brex.Result
is split into three main components:
Brex.Result.Base
- Base provides tools for creating and passing aroundok
/error
tuples. The tools follow the property: if there’s a success continue the computation, if there’s an error propagate it.Brex.Result.Helpers
- Helpers includes tools for modifying the reason inerror
tuples. The functions in this module always propagate the success value.Brex.Result.Mappers
- Mappers includes tools for applying functions that return:ok | {:ok, val} | {:error, reason}
overEnumerables
.
Include the line use Brex.Result
to import the entire library or import Brex.Result.{Base, Helpers, Mappers}
to import the modules individually.
A sampling of functions and examples are provided below. For more in-depth examples see examples.
Other libraries like OK, Monad, and Ok Jose have embraced the concept of monadic error handling and have analogous functions to the ones we have in Brex.Result.Base
.
Brex.Result
separates itself by:
- Extending support beyond classic monadic functions
- support for
:ok
as a success value - support for modifying
errors
tuple reasons - support for mapping functions that return
ok | {:ok, value} | {:error, reason}
overenumerables
- support for
- Respecting Elixir idioms
- encourages use of elixir builtins like the
with
statement when appropriate - provides style guidelines
- actively avoids heavy macro magic that can turn a library into a DSL
- encourages use of elixir builtins like the
- Result tuples:
{:ok, value} | {:error, reason}
- Error tuples:
{:error, reason}
- OK/Success tuples:
{:ok, value}
- Success values:
:ok | {:ok, value}
- Propagate a value: Return a value unchanged
Parametrized types
@type s(x) :: {:ok, x} | {:error, any}
@type t(x) :: :ok | s(x)
Convenience types
@type p() :: :ok | {:error, any}
@type s() :: s(any)
@type t() :: t(any)
Write specs and callbacks usings these shorthands.
# Original
@spec my_fun({:ok, String.t} | {:error, any}, Integer) :: :ok | {:error, any}
# With shorthands
@spec my_fun(Brex.Result.s(String.t), Integer) :: Brex.Result.p()
Use Brex.Result.Base.ok/1
to wrap a value in an ok
tuple.
iex> 2
...> |> ok
{:ok, 2}
Brex.Result.Base.error/1
wraps a value in an error
tuple.
iex> :not_found
...> |> error
{:error, :not_found}
Don't use ok/1
and error/1
when tuple syntax is more explicit:
# No
ok(2)
# Yes
{:ok, 2}
Do use ok/1
and error/1
at the end of a chain of pipes:
# No
val =
arg
|> get_values()
|> transform(other_arg)
{:ok, val}
# Yes
arg
|> get_values()
|> transform(other_arg)
|> ok
Use Brex.Result.Base.fmap/2
to transform the value within a success tuple. It propagates the error value.
iex> {:ok, 2}
...> |> fmap(fn x -> x + 5 end)
{:ok, 7}
iex> {:error, :not_found}
...> |> fmap(fn x -> x + 5 end)
{:error, :not_found}
Use Brex.Result.Base.bind/2
to apply a function to the value within a success tuple. The function must returns a result tuple.
iex> {:ok, 2}
...> |> bind(fn x -> if x > 0, do: {:ok, x + 5}, else: {:error, :neg})
{:ok, 7}
iex> {:ok, -1}
...> |> bind(fn x -> if x > 0, do: {:ok, x + 5}, else: {:error, :neg})
{:error, :neg}
iex> {:error, :not_found}
...> |> bind(fn x -> if x > 0, do: {:ok, x + 5}, else: {:error, :neg})
{:error, :not_found}
Infix bind is given for convience as Brex.Result.Base.~>/2
iex> {:ok, [[1, 2, 3, 4]}
...> ~> Enum.member(2)
...> |> fmap(fn x -> if x, do: :found_two, else: :not_found end)
{:ok, :found_two}
Avoid single ~>
s and only use ~>
when the function argument is named and fits onto one line.
# No
{:ok, file}
~> File.read()
# Yes
bind({:ok, file}, &File.read/1)
# No
{:ok, val}
~> (fn x -> if x > 0, do: {:ok, x}, else: {:error, neg}).()
~> insert_amount()
# Yes
{:ok, val}
|> bind(fn x -> if x > 0, do: {:ok, x}, else: {:error, neg})
~> insert_amount()
Brex.Result.Helpers.map_error/2
allows you to transform the reason in an error
tuple.
iex> {:error, 404}
...> |> map_error(fn reason -> {:invalid_response, reason} end)
{:error, {:invalid_reponse, 404}}
iex> {:ok, 2}
...> |> map_error(fn reason -> {:invalid_response, reason} end)
{:ok, 2}
iex> :ok
...> |> map_error(fn reason -> {:invalid_response, reason} end)
:ok
Brex.Result.Helpers.mask_error/2
disregards the current reason and replaces it with the second argument.
iex> {:error, :not_found}
...> |> mask_error(:failure)
{:error, :failure}
Brex.Result.Helpers.convert_error/3
converts an error
into a success value if the reason matches the second argument.
iex> {:error, :not_found}
...> |> convert_error(:not_found)
:ok
iex> {:error, :not_found}
...> |> convert_error(:not_found, default)
{:ok, default}
Brex.Result.Helpers.log_error/3
logs on error. It automatically includes the reason in the log metadata.
iex> {:error, :not_found}
...> |> log_error("There was a problem", level: :warn)
{:error, :not_found}
Brex.Result.Helpers.normalize_error/2
converts a naked :error
atom into an error
tuple. It's good for functions from outside libraries.
iex> :error
...> |> normalize_error(:not_found)
{:error, :not_found}
iex> {:ok, 2}
...> |> normalize_error(:not_found)
{:ok, 2}
iex> :ok
...> |> normalize_error(:not_found)
:ok
Brex.Result.Mappers.map_while_success/2
, Brex.Result.Mappers.each_while_success/2
, Brex.Result.Mappers.reduce_while_success/3
all mimic the Enum functions Enum.map/2
, Enum.each/2
, Enum.reduce/3
, but take a function that returns :ok | {:ok, value} | {:error, reason}
as the mapping/reducing argument. Each of these functions produce a success value containing the final result or the first error
.
- Credo complains pipe chain is not started with raw value when preceeded by
~>
.
The package can be installed by adding brex_result
to your list of dependencies in mix.exs
:
def deps do
[
{:brex_result, "~> 0.4.1"}
]
end