Current section
Files
Jump to
Current section
Files
lib/ex_flow.ex
defmodule ExFlow do
@arg Macro.var(:"arg name for anonymous function", __MODULE__)
@moduledoc """
Module to make life with workflows easier
Helps to compose functions with `|>` to not handle errors explicitly but just to bypass them.
If you hate making names for variables with `Kernel.SpecialForms.with/1` this module is for you
```
defmodule MyModule do
import ExFlow
def action do
workflow do
validate_input_data(params)
~> save_user
~> generate_plans(2)
~>> send_confirmation
~> redirect_to_user_page
end
end
end
```
Operations in previous example might be anything. It's a bogus code to show how to to pipe operations
inside workflows and not care about failures.
`~>` is for operations which transform data and `~>>` is for operations which don't transform data and can be built
manually without metaprogramming with the help of `ExFlow.bind/1`, `ExFlow.bind/2`, `ExFlow.then/1` and
`ExFlow.then/2`.
"""
@moduledoc since: "0.1.0"
@doc ~S"""
Binds an argument and produces one-argument function
Returns new anonymous function which either returns input parameter
if it looks like error or calls the function being bound with that parameter.
Inspired by `>>=` in Haskell.
Parameter is considered to be error if it is equal to `:error` or a tuple which first element is `:error`.
Helps to compose functions with `|>` to not handle errors explicitly but just to bypass them.
## Examples
iex> ExFlow.bind(fn _ -> 42 end).({:error, :reason})
{:error, :reason}
iex> ExFlow.bind(fn _ -> 42 end).(:error)
:error
iex> ExFlow.bind(& &1).(42)
42
iex> {:error, :reason} |> ExFlow.bind(&(&1 * 2)) |> ExFlow.bind(&(&1 + 2))
{:error, :reason}
iex> :error |> ExFlow.bind(&(&1 * 2)) |> ExFlow.bind(&(&1 + 2))
:error
iex> 20 |> ExFlow.bind(&(&1 * 2)) |> ExFlow.bind(&(&1 + 2))
42
"""
@doc since: "0.1.0"
def bind(fun)
def bind(fun) do
fn
arg
when is_tuple(arg) and elem(arg, 0) == :error
when arg == :error ->
arg
arg ->
fun.(arg)
end
end
def bind(arg, fun) do
bind(fun).(arg)
end
@doc ~S"""
Binds an argument and produces one-argument function which ignores function return value
Returns new anonymous function which either returns input parameter
if it looks like error or calls the function being bound with that parameter and returns that very parameter.
Usefull for sideffect functions which don't transform input e.g. scheduling job to send email when user is created/
Inspired by `>>` in Haskell.
Parameter is considered to be error if it is equal to `:error` or a tuple which first element is `:error`.
Helps to compose functions with `|>` to not handle errors explicitly but just to bypass them.
## Examples
iex> ExFlow.then(fn a -> a + 2 end).({:error, :reason})
{:error, :reason}
iex> ExFlow.then(fn a -> a + 2 end).(:error)
:error
iex> ExFlow.then(& &1).(42)
42
iex> {:error, :reason} |> ExFlow.then(&(&1 * 2)) |> ExFlow.then(&(&1 + 2))
{:error, :reason}
iex> :error |> ExFlow.then(&(&1 * 2)) |> ExFlow.then(&(&1 + 2))
:error
iex> 20 |> ExFlow.then(&(&1 * 2)) |> ExFlow.then(&(&1 + 2))
20
"""
@doc since: "0.1.0"
def then(fun)
def then(fun) do
fn
arg
when is_tuple(arg) and elem(arg, 0) == :error
when arg == :error ->
arg
arg ->
fun.(arg)
arg
end
end
def then(arg, fun) do
then(fun).(arg)
end
@doc ~S"""
Creates workflow
In simple words helps to compose functions thinking only about happy path and return error as soon as possible.
Actually speaking it is the same as `Kernel.SpecialForms.with/1` but doesn't make you think about the errors
and allows just to write the transformations on data instead.
It uses `~>` operator which is the same as `|>` in context of workflow. So
```
workflow do
operation ~> operation2
end
```
is the same as
```
operation |> ExFlow.bind(&(operation2(&1)))
```
Also uses `~>>` operator where is the same as `|>` in context of workflow but ignores the return or right parameter
function. So
```
workflow do
operation ~>> operation2
end
```
is the same as
```
operation |> ExFlow.then(&(operation2(&1)))
```
which will return the result of `operation` not `operation2`. Useful for side-effect oe async operations which
shouldn't transform input data
"""
defmacro workflow(do: body) do
Macro.prewalk(body, &walk/1)
end
%{~>: :bind, ~>>: :then} |> Enum.each(fn {operator, mapping} ->
defp walk({unquote(operator), context, [left, right]}) do
{:|>, context, [left, quoted(unquote(mapping), right)]}
end
end)
defp walk(a), do: a
defp quoted(type, {name, cont, args}) do
quote do
ExFlow.unquote(type)(fn unquote(@arg) -> unquote({name, cont, args_for_anonymous_fn(args)}) end)
end
end
defp args_for_anonymous_fn(nil), do: [@arg]
defp args_for_anonymous_fn(args), do: [@arg | args]
end