Packages

CapturePipe exposes an extended pipe-operator that allows the usage of bare function captures.

Current section

Files

Jump to
capture_pipe lib capture_pipe.ex
Raw

lib/capture_pipe.ex

defmodule CapturePipe do
import Kernel, except: [|>: 2]
@moduledoc """
To use this operator in one of your modules, you need to add the following to it:
use CapturePipe
This does the same thing as explicitly writing:
import Capturepipe
import Kernel, except: [|>: 2]
"""
defmacro __using__(_opts) do
quote do
import CapturePipe
import Kernel, except: [|>: 2]
end
end
@doc """
Extended pipe-operator that allows the usage of bare function captures.
This is useful to insert the pipe's results into a datastructure
such as a tuple.
What this macro does, is if it encounters a `&` capture,
it wraps the whole operand in `(...).()` which is the
anonymous-function-call syntax that Elixir's normal pipe accepts,
that (argubably) is less easy on the eyes.
For instance, `10 |> &{:ok, &1}` is turned into `10 |> (&{:ok, &1}).()`
## Examples
Still works as normal:
iex> [1,2,3] |> Enum.map(fn x -> x + 1 end)
[2,3,4]
Insert the result of an operation into a tuple
iex> 42 |> &{:ok, &1}
{:ok, 42}
It also works multiple times in a row.
iex> 20 |> &{:ok, &1} |> &[&1, 2, 3]
[{:ok, 20}, 2, 3]
Besides the function-capture syntax
CapturePipe also enables you to use anonymnous functions
directly inside a pipe, performing similar wrapping:
iex> 42 |> fn val -> to_string(val) end
"42"
Even if the pipes are nested deeply
and interspersed with 'normal' pipe calls:
iex> (10
iex> |> &Kernel.div(20, &1)
iex> |> Kernel.-()
iex> |> to_string()
iex> |> &"The answer is: \#{&1}!"
iex> |> String.upcase()
iex> |> &{:ok, &1}
iex> )
{:ok, "THE ANSWER IS: -2!"}
Currently, the implementation raises errors when
captures or anonymous functions with a bad arity are used
_at runtime_. This might be improved in the future
to raise compile-time errors whenever possible instead.
"""
# The implementation is identical to
# Elixir's builtin one,
# except that we are using our overridden
# `pipe` and `unpipe` functions.
# (rather than the ones in the `Macro` module)
defmacro left |> right do
[{h, _} | t] = unpipe({:|>, [], [left, right]})
fun = fn {x, pos}, acc ->
pipe(acc, x, pos)
end
:lists.foldl(fun, h, t)
end
# Implementation is _very_ similar to Elixir's builtin
# `Macro.unpipe/1`
defp unpipe(expr) do
:lists.reverse(unpipe(expr, []))
end
defp unpipe({:|>, _, [left, right]}, acc) do
unpipe(right, unpipe(left, acc))
end
# The magic happens here:
# We rewrite `a |> (&b |> c)`
# into `a |> (&b) |> c`
# and then recurse
defp unpipe({:&, meta1, [{:|>, meta2, [left, right]}]}, acc) do
unpipe({:|>, meta2, [{:&, meta1, [left]}, right]}, acc)
end
defp unpipe(other, acc) do
[{other, 0} | acc]
end
# Bare captures are wrapped to become
# (&(...)).()
defp pipe(expr, {:&, _, _} = call_args, position) do
pipe(expr, quote do (unquote(call_args)).() end, position)
end
defp pipe(expr, {:fn, _ ,_} = call_args, position) do
pipe(expr, quote do (unquote(call_args)).() end, position)
end
# Defer the rest of the implementation
# to the one that already ships with Elixir
defp pipe(expr, other, position) do
Macro.pipe(expr, other, position)
end
end