- Notifications
You must be signed in to change notification settings - Fork2
Erlang Parse Transforms Including Fold (MapReduce) comprehension, Elixir-like Pipeline, and default function arguments
License
saleyn/etran
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Author: Serge Aleynikov <saleyn(at)gmail.com>
License: MIT License
This library includes useful parse transforms including Elixir-like pipeline operator forcascading function calls.
Module | Description |
---|---|
defarg | Support default argument values in Erlang functions |
erlpipe | Elixir-like pipe operator for Erlang |
listcomp | Fold Comprehension and Indexed List Comprehension |
iif | Ternary if function includingiif/3 ,iif/4 ,nvl/2 ,nvl/3 parse transforms |
str | Stringification functions includingstr/1 ,str/2 , andthrow/2 parse transforms |
Presently the Erlang syntax doesn't allow function arguments to have defaultparameters. Consequently a developer needs to replicate the functiondefinition multiple times passing constant defaults to some parameters offunctions.
This parse transform addresses this shortcoming by extending the syntaxof function definitions at the top level in a module to have a defaultexpression such that forA / Default
argument theDefault
will beused if the function is called in code without that argument.
Though it might seem more intuitive for programmers coming from otherlanguages to use the assignment operator=
for defining default arguments,using that operator would change the current meaning of pattern matching ofarguments in function calls (i.e.test(A=10)
is presently a valid expression).Therefore we chose the/
operator for declaring default arguments becauseit has no valid meaning when applied in declaration of function arguments,and presently without thedefarg
transform, using this operator(e.g.test(A / 10) -> ...
) would result in a syntax error detected by thecompiler.
-export([t/2]).test(A/10,B/20)->A+B.
The code above is transformed to:
-export([t/2]).-export([t/0,t/1]).test()->test(10);test(A)->test(A,20);test(A,B)->A+B.
The arguments with default values must be at the end of the argument list:
test(A,B,C/1)->%% This is valid ...test(A/1,B,C)->%% This is invalid ...
NOTE: The default arguments should be constant expressions. Function calls in defaultarguments are not supported!
test(A/erlang:timestamp())->%% !!! Bad syntax ...
Inspired by the Elixir's|>
pipeline operator.This transform makes code with cascading function calls much more readable by using the/
as thepipeline operator. In theLHS / RHS / ... Last.
notation, the result of evaluation of the LHSexpression is passed as an argument to the RHS expression. This process continues until theLast
expression is evaluated. The head element of the pipeline must be either a term to which thearithmetic division/
operator cannot apply (i.e. not integers, floats, variables, functions),or if you need to pass an integer, float, variable, or a result of a function call, wrap it in alist brackets.
It transforms code from:
print(L)whenis_list(L)-> [3,L]%% Multiple items in a list become arguments to the first function/lists:split%% In Module:Function calls parenthesis are optional/element(1,_)%% '_' is the placeholder for the return value of a previous call/binary_to_list/io:format("~s\n", [_]).test1(Arg1,Arg2,Arg3)-> [Arg1,Arg2]%% Arguments must be enclosed in `[...]`/fun1%% In function calls parenthesis are optional/mod:fun2/fun3()/fun4(Arg3,_)%% '_' is the placeholder for the return value of a previous call/funff/1%% Inplace function references are supported/funerlang:length/1%% Inplace Mod:Fun/Arity function references are supported/fun(I) ->Iend%% This lambda will be evaluated as: (fun(I) -> I end)(_)/io_lib:format("~p\n", [_])/fun6([1,2,3],_,other_param)/fun7.test2()->% Result = Argument / Function3=abc/atom_to_list/length,%% Atoms can be passed to '/' as is3="abc"/length,%% Strings can be passed to '/' as is"abc"= <<"abc">>/binary_to_list,%% Binaries can be passed to '/' as is"1,2,3"= {$1,$2,$3}/tuple_to_list%% Tuples can be passed to '/' as is/ [[I] ||I<-_]%% The '_' placeholder is replaced by the return of tuple_to_list/1/string:join(","),%% Here a call to string:join/2 is made"1"= [min(1,2)]/integer_to_list,%% Function calls, integer and float value"1"= [1]/integer_to_list,%% arguments must be enclosed in a list."1.0"= [1.0]/float_to_list([{decimals,1}]),"abc\n"="abc"/ (_++"\n"),%% Can use operators on the right hand side2.0=4.0/max(1.0,2.0),%% Expressions with lhs floats are unmodified2=4/max(1,2).%% Expressions with lhs integers are unmodifiedtest3()->A=10,B=5,2=A/B,%% LHS variables (e.g. A) are not affected by the transform2.0=10/5,%% Arithmetic division for integers, floats, variables is unmodified2.0=A/5,%% (ditto)5=max(A,B)/2.%% Use of division on LHS function calls is unaffected by the transform
to the following equivalent:
test1(Arg1,Arg2,Arg3)->fun7(fun6([1,2,3],io_lib:format("~p\n", [ (fun(I) ->Iend)(erlang:length(ff(fun4(Arg3,fun3(mod2:fun2(fun1(Arg1,Arg2)))))))]),other_param)).print(L)whenis_list(L)->io:format("~s\n", [binary_to_list(element(1,lists:split(3,L)))]).test2()->3=length(atom_to_list(abc)),3=length("abc"),"abc"=binary_to_list(<<"abc">>),"1,2,3"=string:join([[I] ||I<-tuple_to_list({$1,$2,$3})],","),"1"=integer_to_list(min(1,2)),"1"=integer_to_list(1),"1.0"=float_to_list(1.0, [{decimals,1}]),"abc\n"="abc"++"\n",2.0=4.0/max(1.0,2.0),2=4/max(1,2).
Similarly to Elixir, a specialtap/2
function is implemented, whichpasses the given argument to an anonymous function, returning the argumentitself. The following:
f(A)->A+1....test_tap()-> [10]/tap(f)/tap(funf/1)/tap(fun(I) ->I+1end).
is equivalent to:
...test_tap()->beginf(10),beginf(10),begin (fun(I) ->I+1end)(10),10endendend.
Some attempts to tackle this pipeline transform have been done by other developers:
- https://github.com/fenollp/fancyflow
- https://github.com/stolen/pipeline
- https://github.com/oltarasenko/epipe
- https://github.com/clanchun/epipe
- https://github.com/pouriya/pipeline
Yet, we subjectively believe that the choice of syntax in this implementation of transformis more succinct and elegant, and doesn't attempt to modify the meaning of the/
operatorfor arithmetic LHS types (i.e. integers, floats, variables, and function calls).
Why didn't we use|>
operator instead of/
to make it equivalent to Elixir?Parse transforms are applied only after the Erlang source code gets parsed to the ASTrepresentation, which must be in valid Erlang syntax. The|>
operator is not known tothe Erlang parser, and therefore, using it would result in the compile-time error. Wehad to select an operator that the Erlang parser would be happy with, and/
was our choicebecause visually it resembles the pipe|
character more than the other operators.
Occasionally the body of a list comprehension needs to know the indexof the current item in the fold. Consider this example:
[{1,10}, {2,20}]=element(1,lists:foldmapl(fun(I,N) -> {{N,I},N+1}end,1, [10,20])).
Here theN
variable is tracking the index of the current itemI
in the list.While the same result in this specific case can be achieved withlists:zip(lists:seq(1,2), [10,20])
, in a more general case, there is no way to havean item counter propagated with the current list comprehension syntax.
TheIndexed List Comprehension accomplishes just that through the use of an unassignedvariable immediately to the right of the||
operator:
[{Idx,I} ||Idx,I<-L].% ^^^% |% +--- This variable becomes the index counter
Example:
[{1,10}, {2,20}]= [{Idx,I} ||Idx,I<- [10,20]].
To invoke the fold comprehension transform include the initial stateassignment into a list comprehension:
[S+I ||S=1,I<-L].% ^^^ ^^^^^% | |% | +--- State variable bound to the initial value% +----------- The body of the foldl function
In this example theS
variable gets assigned the initial state1
, andtheS+I
expression represents the body of the fold function thatis passed the iteration variableI
and the state variableS
:
lists:foldl(fun(I,S) ->S+Iend,1,L).
A fold comprehension can be combined with the indexed list comprehensionby using this syntax:
[do(Idx,S+I) ||Idx,S=10,I<-L].% ^^^^^^^^^^^^ ^^^ ^^^^^^% | | |% | | +--- State variable bound to the initial value (e.g. 10)% | +--------- The index variable bound to the initial value of 1% +--------------------- The body of the foldl function can use Idx and S
This code is transformed to:
element(2,lists:foldl(fun(I, {Idx,S}) -> {Idx+1,do(Idx,S+I)}end, {1,10},L)).
Example:
33= [S+Idx*I ||Idx,S=1,I<- [10,20]],30= [print(Idx,I,S) ||Idx,S=0,I<- [10,20]].% Prints:% Item#1 running sum: 10% Item#2 running sum: 30print(Idx,I,S)->Res=S+I,io:format("Item#~w running sum:~w\n", [Idx,Res]),Res.
This transform improves the code readability for cases that involve simple conditionalif/then/else
tests in the formiif(Condition, Then, Else)
. Since this is a parsetransform, theThen
andElse
expressions are evaluatedonly if theCondition
evaluates totrue
orfalse
respectively.
E.g.:
iif(tuple_size(T)==3,good,bad).%% Ternary ifiif(some_fun(A),match,ok,error).%% Quaternary ifnvl(L,undefined).nvl(L,nil,hd(L))
are transformed to:
casetuple_size(T)==3oftrue ->good;_ ->badend.casesome_fun(A)ofmatch ->ok;nomatch ->errorend.caseLof [] ->undefined;false ->undefined;undefined ->undefined;_ ->Lend.caseLof [] ->nil;false ->nil;undefined ->nil;_ ->hd(L)end.
This module implements a transform to stringify an Erlang term.
str(Term)
is equivalent tolists:flatten(io_lib:format("~p", [Term]))
forterms that are not integers, floats, atoms, binaries and lists.Integers, atoms, and binaries are converted to string using*_to_list/1
functions. Floats are converted usingfloat_to_list/2
where the secondargument is controled bystr:set_float_fmt/1
andstr:reset_float_fmt/0
calls. Lists are converted to string usinglists:flatten(io_lib:format("~s", [Term]))
and if that fails, then usinglists:flatten(io_lib:format("~p", [Term]))
format.str(Fmt, Args)
is equivalent tolists:flatten(io_lib:format(Fmt, Args))
.bin(Fmt, Args)
is equivalent tolist_to_binary(lists:flatten(io_lib:format(Fmt, Args)))
.throw(Fmt,Args)
is equivalent tothrow(list_to_binary(io_lib:format(Fmt, Args)))
.error(Fmt,Args)
is equivalent toerror(list_to_binary(io_lib:format(Fmt, Args)))
.
Two other shorthand transforms are optionally supported:
b2l(Binary)
is equivalent tobinary_to_list(Binary)
(enabled by giving{d,str_b2l}
)compilation option.i2l(Integer)
is equivalent tointeger_to_list(Binary)
(enabled by giving{d,str_i2l}
)compilation option.
E.g.:
erlc +debug_info -Dstr_b2l -Dstr_i2l +'{parse_transform, str}' -o ebin your_module.erl
$ make
To use the transforms, compile your module with the+'{parse_transform, Module}'
command-lineoption, or include-compile({parse_transform, Module}).
in your source code, whereModule
is one of the transform modules implemented in this project.
To use all transforms implemented by theetran
application, compile your module with thiscommand-line option:+'{parse_transform, etran}'
.
erlc +debug_info +'{parse_transform, etran}' -o ebin your_module.erl
If you are usingrebar3
to build your project, then add torebar.config
:
{deps, [{etran, "0.5.1"}]}.{erl_opts, [debug_info, {parse_transform, etran}]}.
About
Erlang Parse Transforms Including Fold (MapReduce) comprehension, Elixir-like Pipeline, and default function arguments