Pipe Macro

This package provides an Erlang macro to mimic the Elixir pipe operator, but with some additional capabilities compared to its Elixir counterpart.

Usage

The main file is the header include/pipe_macro.hrl which defines the macro; include it in any module where it will be used with the usual directive:

-include_lib("pipe_macro/include/pipe_macro.hrl").

The macros defined here are ?PIPE and ?PARG--they work together to achieve pipeline functionality similar to |> in Elixir. The main difference is that while |> implicitly passes results, ?PARG must explicitly be used wherever a result is to be passed. The basic form is:

?PIPE(Expr1, Expr2, ..., ExprN)

where each Expr parameter is any valid Erlang expression. Any Exprafter the first can include any number of ?PARG. Any ?PARG used in the first expression is an error.

💡 ?PIPE overloads have been defined up to an arity of 10. If you want to use more than 10 expressions, simply add overloads in the header file following the established pattern.

All ?PARG are replaced by the result of the previous expression. For example:

[1, 2, 3] = ?PIPE([1, [2], 3], lists:flatten(?PARG))

See Examples for more.

Examples

Passing results

The result of an expression must be explicitly passed with ?PARG to use it in the expression that follows:

%% Same as lists:flatten([1, [2], 3])
[1, 2, 3] = ?PIPE([1, [2], 3], lists:flatten(?PARG)),

%% Same as lists:map(fun(X) -> X * 2 end, lists:flatten([1, [2], 3]))
[2, 4, 6] = ?PIPE([1, [2], 3],
                  lists:flatten(?PARG),
                  lists:map(fun(X) -> X * 2 end, ?PARG)),

If ?PARG is not used, the previous expression's result is ignored, and the compiler will give a warning about an unused variable:

%% This will cause a compiler warning due to no `?PARG' in the 2nd expression
[8, 10, 12] = ?PIPE([1, [2], 3], % this goes unused and triggers a warning
                    lists:flatten([4,[5],6]),
                    lists:map(fun(X) -> X * 2 end, ?PARG)), % uses flatten result

Variables

Variables bound outside of the pipeline can be used in the expressions:

%% Bound variables used in pipeline expressions
Me = pid_to_list(self()),
[$I,$\ ,$a,$m,$:|Me] = ?PIPE(Me, lists:append(["I am:", ?PARG])),

Composing

As its arguments can be any valid expressions, ?PIPE can also be used to compose arbitrary terms and values:

%% No func calls, just compose something
#{one := 1, 1 := one} = ?PIPE(one, #{?PARG => 1, 1 => ?PARG}),

Including function values:

%% Create a nested anonymous function
Upcase_word = ?PIPE(fun(X) when $a =< X,  X =< $z -> X + $A - $a; (X) -> X end,
                    fun(X) -> lists:map(?PARG, X) end),

"ERLANG" = Upcase_word("Erlang"),

["I","LIKE","ERLANG"] = lists:map(Upcase_word, ["I","like","Erlang"]),

Lazy calls

The last random example of ?PIPE use is getting results from a lazy function. This is based on the function ints_from/1 in the lazy module of the Erlang programming example.

To get a list of 5 results at a time, instead of writing something like:

XX = ints_from(1),

First = hd(XX()),
Tail1 = tl(XX()),

Second = hd(Tail1()),
Tail2 = tl(Tail1()),

Third = hd(Tail2()),
Tail3 = tl(Tail2()),

Fourth = hd(Tail3()),
Tail4 = tl(Tail3()),

Fifth = hd(Tail4()),

[1, 2, 3, 4, 5] = [First, Second, Third, Fourth, Fifth],

Using ?PIPE can eliminate the intermediate variables, at the cost of making the code unreadable:

YY = ints_from(1),

%% Complicated, and may cause blindness, but works the same
[1, 2, 3, 4, 5] =
  ?PIPE([tl(YY()), hd(YY())],
        [tl((hd(?PARG))()), hd((hd(?PARG))()) | tl(?PARG)],
        [tl((hd(?PARG))()), hd((hd(?PARG))()) | tl(?PARG)],
        [tl((hd(?PARG))()), hd((hd(?PARG))()) | tl(?PARG)],
        [tl((hd(?PARG))()), hd((hd(?PARG))()) | tl(?PARG)],
        lists:reverse(tl(?PARG))),
ok.