defmodule Fex do
@moduledoc """
Documentation for `Fex`.
This is an experimental package - do not use in production!
Many Elixir functions are returning two types of tuples:
- {:ok, data}
- {:error, error}
Together they maps quite closely to "either" structure.
Idea is to instead of using the `case` function(s) to
check was operation successfull:
```
with {:ok, json} <- File.read("list.json"),
{:ok, data} <- Jason.decode(json)
do
data
|> Enum.sum()
else
Logger.error("Error occurred")
IO.inspect(msg)
0
end
```
we could use functions that are operating on
"either" looking structures:
```
File.read("list.json") do
|> Fex.chain(&Jason.decode/1)
|> Fex.apply(&Enum.sum/1)
|> Fex.fold(&(&1), fn msg ->
Logger.error("Error occurred")
IO.inspect(msg)
0
end)
```
Above approach gives some advatanges over the
"usual" approach:
- developer has freedom to decide when to handle
the "error" case, which makes it easier to divide
code to dirty and pure parts
- error cases can be grouped together
- code is easier to reason about with a little bit
of understanding about the "either" idea
"""
@doc """
Maps list of results using `Enum.map/2`
## Examples
iex> Fex.map({:ok, [1, 2]}, &(&1 + 2))
{:ok, [3, 4]}
iex> Fex.map({:error, "Error message"}, &(&1 + 2))
{:error, "Error message"}
"""
def map({:ok, data}, iteration_fn) do
{:ok, Enum.map(data, iteration_fn)}
end
def map({:error, error}, _iteration_fn), do: {:error, error}
@doc """
Applies function over data
## Examples
iex> Fex.apply({:ok, [1, 2]}, &Enum.sum/1)
{:ok, 3}
iex> Fex.map({:error, "Error message"}, &Enum.sum/1)
{:error, "Error message"}
"""
def apply({:ok, data}, function) do
{:ok, function.(data)}
end
def apply({:error, error}, _function), do: {:error, error}
@doc """
Like apply but doesn't wrap function in the "either" format.
It relies on the fact that applied function will return the
"either" format.
Useful when you have an "either" structure already and you
need to pass it to function that returns another "either"
structure(using apply would cause "nesting" of eithers).
## Examples
iex> Fex.chain({:ok, "{}"}, &Jason.decode/1)
{:ok, %{}}
iex> Fex.chain({:error, "Error message"}, &Jason.decode/1)
{:error, "Error message"}
"""
def chain({:ok, data}, function), do: function.(data)
def chain({:error, error}, _function), do: {:error, error}
@doc """
When you would like to extract the pure value out of the "either"
struct.
## Examples
iex> Fex.fold({:ok, 5}, &(&1), &(&1))
5
iex> Fex.fold({:error, "Error message"}, &(&1), &(&1))
"Error message"
"""
def fold({:ok, data}, success_fn, _error_fn), do: success_fn.(data)
def fold({:error, error}, _success_fn, error_fn), do: error_fn.(error)
end