Skip to content

Instantly share code, notes, and snippets.

@damncabbage
Created June 6, 2018 08:50
Show Gist options
  • Save damncabbage/b3de0cd72a345fab7ba63c2898e00b63 to your computer and use it in GitHub Desktop.
Save damncabbage/b3de0cd72a345fab7ba63c2898e00b63 to your computer and use it in GitHub Desktop.
Elixir "Traversing Error Mountain" Code Samples
defmodule Examples do
def zero do
files = ["abc.json", "def.json", "ghi.json"]
contents = files
|> Enum.map(fn (file) ->
File.read(file)
end)
# => [{:ok, "..."}, {:error, :enoent}, {:ok, "..."}]
end
def one do
files = ["abc.json", "def.json", "ghi.json"]
contents = files
|> Result.traverse(fn (file) ->
File.read(file)
end)
# => {:error, :enoent}
end
def two do
files = ["abc.json", "def.json", "ghi.json"]
contents = files
|> Result.traverse(fn (file) ->
file
|> File.read()
|> Result.map(&String.trim/1)
end)
# => {:error, :enoent}
end
def three do
files = ["abc.json", "def.json", "ghi.json"]
contents = files
|> Result.traverse(fn (file) ->
file
|> File.read()
|> Result.map(&String.trim/1)
|> Result.map_error(fn (e) ->
{file, e}
end)
end)
# => {:error, {"def.json", :enoent}}
end
def four do
files = ["abc.json", "def.json", "ghi.json"]
contents = files
|> Result.traverse(fn (file) ->
file
|> File.read()
|> Result.bimap(
&String.trim/1,
fn (e) -> {file, e} end
)
end)
# => {:error, {"def.json", :enoent}}
end
def five do
files = ["abc.json", "def.json", "ghi.json"]
contents = files
|> Result.traverse(fn (file) ->
file
|> File.read()
|> Result.map(&String.trim/1)
|> Result.map_error(fn (e) ->
{file, e}
end)
end)
# => {:error, {"def.json", :enoent}}
end
def six do
files = ["abc.json", "def.json", "ghi.json"]
contents = files
|> Result.traverse(fn (file) ->
file
|> File.read()
|> Result.and_then(fn (contents) ->
contents
|> Poison.decode()
end)
|> Result.map_error(fn (e) ->
{file, e}
end)
end)
# => {:error, {"def.json", :enoent}}
# => {:error, {"ghi.json", {:invalid, "{", 23}}
# => {:ok, [%{"hello": "world"}, ...]}
end
end
defmodule Result do
@typedoc """
A type representing `{:ok, ...}` or `{:error, ...}`.
"""
@type t(ok, err) :: result(ok, err)
# Private alias for module readability.
@typep result(ok, err) ::
{:ok, ok}
| {:error, err}
@doc """
Create an `{:ok, ...}` value.
Useful to prevent typos (`:okay`).
"""
@spec ok(any()) :: result(any(), any())
def ok(x), do: {:ok, x}
@doc """
Create an `{:error, ...}` value.
Useful to prevent typos (`:err`).
"""
@spec error(any()) :: result(any(), any())
def error(x), do: {:error, x}
@doc """
This is a pattern-match except in function form.
(We use this to take a `Result.t()`, and either do something if it's :ok, or
do something else if it's an :error. This is a "fold" on a `Result.t()`,
much like Enum.reduce/3 is a fold on lists.)
## Examples
iex> Result.match({:ok, 1}, &(&1 + 2), &(raise &1))
3
iex> Result.match({:error, 1}, &(raise &1), &(&1 + 2))
3
"""
@spec match(
result(any(), any()),
(any() -> any()),
(any() -> any())
) :: any()
def match(x, ok_func, error_func)
when is_function(ok_func) and is_function(error_func)
do
case x do
{:ok, val} ->
ok_func.(val)
{:error, err} ->
error_func.(err)
end
end
@doc """
Operate on the values in a `{:ok, ...}` or `{:error, ...}` container.
This is like match/3, but keeps the value in the container. This is
a convenience function.
## Examples
iex> Result.map({:ok, 1}, &(&1 + 2), &(raise &1))
{:ok, 3}
iex> Result.map({:error, 1}, &(raise &1), &(&1 + 2))
{:error, 3}
"""
@spec bimap(
result(any(), any()),
(any() -> any()),
(any() -> any())
) :: result(any(), any())
def bimap(x, ok_func, error_func) do
match(
x,
&(ok_func.(&1) |> ok()),
&(error_func.(&1) |> error())
)
end
@doc """
Operate on the `{:error, ...}` part of a `Result.t()`.
## Examples
iex> Result.map_error({:error, 1}, &(&1 + 2))
{:error, 3}
iex> Result.map_error({:ok, 1}, &(raise &1))
{:ok, 1}
"""
@spec map_error(
result(any(), any()), (any() -> any())
) :: result(any(), any())
def map_error(x, error_func), do: bimap(x, &(&1), error_func)
@doc """
Operate on the `{:ok, ...}` part of a `Result.t()`.
(This is sometimes called a "map".)
## Examples
iex> Result.map({:ok, 1}, &(&1 + 2))
{:ok, 3}
iex> Result.map({:error, 1}, &(raise &1))
{:error, 1}
"""
@spec map(
result(any(), any()), (any() -> any())
) :: result(any(), any())
def map(x, ok_func), do: bimap(x, ok_func, &(&1))
@doc """
Chain together a bunch of actions on an `Result.t()`, passing it down
the chain, and short-circuiting when one of the action fails.
Takes an `{:ok, <thing>}` or `{:error,...}` (`Result.t()`), and
a function that takes a `thing` and returns a result, and gives you
a `Result.t()` back.
We usually handle this kind of thing with a `with {:ok, foo} <- ...`
sort of structure, but sometimes it's more convenient (or less
weird-looking) to chunk a `|> Result.and_then(...)` on the bottom of
a block instead.
## Examples
iex> Result.and_then({:ok, 1}, &({:ok, &1 + 2}))
{:ok, 3}
iex> Result.and_then({:error, 1}, &({:ok, &1 + 2}))
{:error, 1}
iex> {:ok, "12:34:56"}
...> |> Result.and_then(&Time.from_iso8601/1)
...> |> Result.and_then(&(Time.convert(&1, Calendar.ISO)))
{:ok, ~T[12:34:56]}
"""
@spec and_then(
result(any(), any()),
(any() -> result(any(), any()))
) :: result(any(), any())
def and_then(x, func) when is_function(func) do
match(x, func, &error/1)
end
@doc """
Flatten a result by a layer of {:ok, ...}s. Leaves everything else
intentionally preserved.
## Examples
iex> Result.flatten({:ok, {:ok, 123}})
{:ok, 123}
iex> Result.flatten({:ok, {:error, 123}})
{:error, 123}
iex> Result.flatten({:error, {:ok, 123}})
{:error, {:ok, 123}}
iex> Result.flatten({:error, {:error, 123}})
{:error, {:error, 123}}
iex> Result.flatten({:ok, {:ok, {:ok, 123}}})
{:ok, {:ok, 123}}
"""
@spec flatten(
result(result(any(), any()), any())
) :: result(any(), any())
def flatten(x) do
and_then(x, &(&1))
end
@doc """
Take a list of values, and a function from that value to an `{:ok, ...}`
or `{:error, ...}`, and get back either an `{:ok, [...]}` list of
results, or an `{:error, ...}` of the first failure.
This is handy for walking map a list of things, doing something
that may fail for each item, and then collecting up the results at the
end. Like a bunch of Ecto.Repo.insert/2 calls, or using
DateTime.from_iso8601/1 on a bunch of dates.
## Examples
iex> Result.traverse([1,2,3], &({:ok, &1 * 2}))
{:ok, [2,4,6]}
iex> times = ["12:34:56", "23:45:01"]
iex> Result.traverse(times, &Time.from_iso8601/1)
{:ok, [ ~T[12:34:56], ~T[23:45:01] ]}
iex> times = ["12:34:56", "25:00:00", "23:45:01"]
iex> Result.traverse(times, &Time.from_iso8601/1)
{:error, :invalid_time}
# Compared to:
iex> times = ["12:34:56", "23:45:01"]
iex> Enum.map(times, &Time.from_iso8601/1)
[{:ok, ~T[12:34:56]}, {:ok, ~T[23:45:01]}]
"""
@spec traverse(
Enum.t(result(any(), any())),
(any() -> result(any(), any()))
) :: result(list(any()), any())
def traverse(results, func) when is_function(func) do
Enum.reduce_while(results, {:ok, []}, fn(element, {:ok, okays}) ->
case func.(element) do
{:ok, v} ->
{:cont, {:ok, [v | okays]}}
{:error, e} ->
{:halt, {:error, e}}
end
end)
|> map(&Enum.reverse/1) # ... and flip it.
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment