Skip to content

Instantly share code, notes, and snippets.

@seancribbs
Last active December 4, 2020 05:37
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save seancribbs/2eba3e249e05bc257380 to your computer and use it in GitHub Desktop.
Save seancribbs/2eba3e249e05bc257380 to your computer and use it in GitHub Desktop.
IF / ELSE in erlang, via parse_transform. EVIL, DO NOT USE
%% @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))}.
-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.
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