-
-
Save russelldb/e24858face262e83321a to your computer and use it in GitHub Desktop.
IF / ELSE in erlang, via parse_transform. EVIL, DO NOT USE
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
%% @doc A VERY EVIL parse_transform that allows two things: | |
%% | |
%% 1) The use of 'else' instead of 'true' in the final clause of an if | |
%% expression. | |
%% | |
%% 2) Automatic extraction of non-guard expressions into | |
%% anonymous variables that can then be used directly in the clause | |
%% guards. | |
%% | |
%% Until Erlang actually implements the 'cond' construct, this is a | |
%% compromise. | |
-module(ifelse). | |
-export([parse_transform/2]). | |
parse_transform(AST, _Options) -> | |
parse_trans:plain_transform(fun do_transform/1, AST). | |
do_transform({'if', Line, Clauses}) -> | |
%% First, recurse into the AST, transforming 'else' clauses and | |
%% nested ifs. | |
Rewritten = parse_trans:plain_transform(fun do_transform/1, Clauses), | |
%% Now transform each clause by extracting non-guard expressions | |
%% into pre-evaluated expressions that are assigned to placeholder | |
%% variables. | |
{NewClauses, {Bindings, _}} = lists:mapfoldr(fun transform_clause/2, {[], Line}, Rewritten), | |
if Bindings == [] -> | |
{'if', Line, NewClauses}; | |
true -> | |
%% Finally, wrap all the new bindings into a block | |
%% expression that includes the new bindings. | |
erl_syntax:revert(erl_syntax:block_expr(Bindings++[{'if', Line, NewClauses}])) | |
end; | |
do_transform({'clause', Line, [], [[{atom, GLine, else}]], Exprs}) -> | |
%% Where clauses use the guard 'else' (not likely in non-if | |
%% expressions), rewrite it to 'true'. | |
NewExprs = parse_trans:plain_transform(fun do_transform/1, Exprs), | |
{'clause', Line, [], [[{atom, GLine, true}]], NewExprs}; | |
do_transform(_) -> | |
%% We don't care about other constructs, so just recurse. | |
continue. | |
transform_clause({'clause', CLine, [], Guards, Exprs}=Clause, {Bindings, IfLine}=Acc) -> | |
%% Transform guards that aren't guard-safe into matches whose | |
%% LHS variables are used in the guard instead. | |
{NewGuards, NewBindings} = lists:mapfoldl(transform_guard(IfLine), [], Guards), | |
%% If we didn't create any new bindings, no need to recreate the clause. | |
if NewBindings == [] -> | |
{Clause, Acc}; | |
true -> | |
%% Otherwise, replace the clause and add the appropriate | |
%% bindings to the list. | |
{{'clause', CLine, [], NewGuards, Exprs}, {NewBindings ++ Bindings, IfLine}} | |
end. | |
transform_guard(IfLine) -> | |
%% This fun is used in mapfoldl and thus must return a two-tuple | |
%% of the transformed guard and the accumulator. | |
fun(Guard, BindingsAcc) -> | |
case lists:all(fun erl_lint:is_guard_test/1, Guard) of | |
true -> | |
%% If it's a proper guard test, we don't have to | |
%% do anything. | |
{Guard, BindingsAcc}; | |
false -> | |
%% If it's not, we need to create a variable | |
%% binding, join conjunctions in the guard with | |
%% 'andalso' and replace the non-guard expression | |
%% with the new variable. | |
%% | |
%% NB: This doesn't handle the case where a guard | |
%% expression can crash and result in the clause | |
%% not being called. The crash would happen before | |
%% if expression was entered. | |
Name = new_guard_name(IfLine), | |
NewBinding = {match, IfLine, Name, join_conjunctions(Guard, IfLine)}, | |
{[Name], [NewBinding|BindingsAcc]} | |
end | |
end. | |
%% Joins conjunctions from a guard with 'andalso'. In the case where | |
%% there's only one expression, just return it. | |
join_conjunctions([Guard],_) -> | |
Guard; | |
join_conjunctions(Guards, IfLine) -> | |
lists:foldr(fun(Op, Acc) -> | |
{op, IfLine, 'andalso', Op, Acc} | |
end, {atom, IfLine, true}, Guards). | |
%% Generates a new guard variable name. | |
new_guard_name(Line) -> | |
Key = {?MODULE, guard}, | |
Counter = case get(Key) of | |
undefined -> | |
put(Key, 1), | |
0; | |
Num -> | |
put(Key, Num+1), | |
Num | |
end, | |
{var, Line, list_to_atom("__Guard__"++integer_to_list(Counter))}. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-module(ifelse_example). | |
-compile([export_all, {parse_transform, ifelse}]). | |
simple_else(B) -> | |
if B -> | |
ok; | |
else -> | |
error | |
end. | |
simple_extract(A) -> | |
if | |
A ! true -> | |
ok; | |
true -> | |
error | |
end. | |
extract_with_else(A) -> | |
if | |
A ! true -> | |
ok; | |
else -> | |
error | |
end. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15> ifelse_example:simple_else(false). | |
error | |
16> ifelse_example:simple_else(true). | |
ok | |
17> ifelse_example:simple_extract(self()). | |
ok | |
18> flush(). | |
Shell got true | |
ok | |
19> ifelse_example:extract_with_else(self()). | |
ok | |
20> flush(). | |
Shell got true | |
ok | |
21> ifelse_example:extract_with_else(0). | |
** exception error: bad argument | |
in function ifelse_example:extract_with_else/1 (src/ifelse_example.erl, line 22) | |
%% [snip -- abstract code of compile module below] | |
4> rp(Code). | |
[{attribute,1,file,{"src/ifelse_example.erl",1}}, | |
{attribute,1,module,ifelse_example}, | |
{attribute,2,compile,[export_all]}, | |
{function,4,simple_else,1, | |
[{clause,4, | |
[{var,4,'B'}], | |
[], | |
[{'if',5, | |
[{clause,5,[],[[{var,5,'B'}]],[{atom,6,ok}]}, | |
{clause,7,[],[[{atom,7,true}]],[{atom,8,error}]}]}]}]}, | |
{function,12,simple_extract,1, | |
[{clause,12, | |
[{var,12,'A'}], | |
[], | |
[{block,0, | |
[{match,13, | |
{var,13,'__Guard__0'}, | |
{op,14,'!',{var,14,'A'},{atom,14,true}}}, | |
{'if',13, | |
[{clause,14,[],[[{var,13,'__Guard__0'}]],[{atom,15,ok}]}, | |
{clause,16,[],[[{atom,16,true}]],[{atom,17,error}]}]}]}]}]}, | |
{function,20,extract_with_else,1, | |
[{clause,20, | |
[{var,20,'A'}], | |
[], | |
[{block,0, | |
[{match,21, | |
{var,21,'__Guard__1'}, | |
{op,22,'!',{var,22,'A'},{atom,22,true}}}, | |
{'if',21, | |
[{clause,22,[],[[{var,21,'__Guard__1'}]],[{atom,23,ok}]}, | |
{clause,24,[],[[{atom,24,true}]],[{atom,25,error}]}]}]}]}]}, | |
{eof,27}] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment