Brex.Result

Build Status hex.pm version Coverage Status license

This library provides tools to handle three common return values in Elixir

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

Table of Contents

Overview

Brex.Result is split into three main components:

  • Brex.Result.Base - Base provides tools for creating and passing around ok/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 in error tuples. The functions in this module always propogate the success value.
  • Brex.Result.Mappers - Mappers includes tools for applying functions that return :ok | {:ok, val} | {:error, reason} over Enumerables.

Usage

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.

Differences from Similar Libraries

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} over enumerables
  • 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

Definitions

  • 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

Types

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)

Style Recommendation

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()

Base

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}

Style Recommendation

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 propogates 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}

Style Recommendation

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()

Helpers

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

Mappers

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.

Known Problems

  • Credo complains pipe chain is not started with raw value when preceeded by ~>.

Installation

The package can be installed by adding brex_result to your list of dependencies in mix.exs:

def deps do
  [
    {:brex_result, "~> 0.4.0"}
  ]
end