Created
June 6, 2018 08:50
-
-
Save damncabbage/b3de0cd72a345fab7ba63c2898e00b63 to your computer and use it in GitHub Desktop.
Elixir "Traversing Error Mountain" Code Samples
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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