Current section
Files
Jump to
Current section
Files
src/partial.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% A parse transform implementing partial function application.
%%% @end
%%%-------------------------------------------------------------------
-module(partial).
% -define(PARTIAL_DEBUG, true).
-ifdef(PARTIAL_DEBUG).
-define(IF_DEBUG(Expression), Expression).
-else.
-define(IF_DEBUG(Expression), (ok)).
-endif.
-define(MATCH_REMOTE(Module, Name),
{remote, _, {atom, _, Module}, {atom, _, Name}}).
%% API
-export([cut/1,
cute/1,
parse_transform/2]).
-include_lib("syntax_tools/include/merl.hrl").
%%%===================================================================
%%% API
%%%===================================================================
%% @doc
%% A dummy function used as a marker by parse_transform/2 to convert
%% calls to functions to partially applied functions. The special
%% variable '_' is used as a marker for unevaluated arguments, as it
%% is usually illegal to use on the right hand side of a match.
%%
%% All arguments are evaluated when the partially applied function is
%% called.
%%
%% The parse transform is only able to detect and rewrite simple
%% literal calls to this function. Other uses will result in an error
%% being thrown at runtime.
%% @end
%% @see parse_transform/2
cut(_Fun) ->
missing_parse_transform().
%% @doc
%% A dummy function used as a marker by parse_transform/2 to convert
%% calls to functions to partially applied functions. The special
%% variable '_' is used as a marker for unevaluated arguments, as it
%% is usually illegal to use on the right hand side of a match.
%%
%% Given arguments are evaluated when the partially applied function
%% is constructed. This can be used as an easy way to cache expensive
%% computation in a closure.
%%
%% The parse transform is only able to detect and rewrite simple
%% literal calls to this function. Other uses will result in an error
%% being thrown at runtime.
%% @end
%% @see parse_transform/2
cute(_Fun) ->
missing_parse_transform().
%% @doc
%% A parse transformation function which converts calls to special
%% dummy functions in this module.
%%
%% Add:
%% -compile({parse_transform, partial}).
%%
%% to the top of any module to enable.
%% @end
parse_transform(Forms, _Options) ->
transform_forms(Forms).
%%%===================================================================
%%% Internal Functions
%%%===================================================================
missing_parse_transform() ->
throw({missing_parse_transform,
"This function requires that the partial:parse_trans/2 "
"function be listed as a parse transformation for your "
"module as \"-compile({parse_transform, partial}).\" "
"If that is already true, this function has been called "
"indirectly in a way the parse transform can not "
"recognize, such as via apply/3."}).
transform_forms(Forms) ->
% If any errors occured, we replace the entire form with them.
% This allows the standard compiler chain to pick them up as if
% they were erl_parse errors and display a standard error message
% to the user instead of a traceback.
erl_syntax:revert_forms(
lists:append(
[case erl_syntax_lib:mapfold(fun transform/2, [], Form) of
{Transformed, []} ->
[Transformed];
{_Transformed, Errors} ->
Errors
end || Form <- Forms])).
transform(Form, Errors) ->
Line = erl_syntax:get_pos(Form),
Transformed = case Form of
?Q("partial:cut(_@@Args)") ->
?IF_DEBUG(ok = io:format(
"partial:parse_transform/2 cut Args ~p~n",
[Args])),
Cut = cut_function(Line, Args),
?IF_DEBUG(ok = io:format(
"partial:parse_transform/2 cut Transform ~p~n",
[Cut])),
Cut;
?Q("partial:cute(_@@Args)") ->
?IF_DEBUG(ok = io:format(
"partial:parse_transform/2 cute Args ~p~n",
[Args])),
Cute = cute_function(Line, Args),
?IF_DEBUG(ok = io:format(
"partial:parse_transform/2 cute Transform ~p~n",
[Cute])),
Cute;
_ ->
{ok, Form}
end,
case Transformed of
{ok, NewForm} ->
{NewForm, Errors};
{error, Error} ->
{Form, [Error | Errors]}
end.
transform_error(Line, Message) ->
% I'm probably abusing the error system by using erl_parse here,
% but it allows my errors to show up as usual in the compiler
% output.
Prefix = "Error: In partial:parse_transform/2, ",
{error, erl_syntax:error_marker({Line, erl_parse, Prefix ++ Message})}.
%% @doc
%% Transform the AST for:
%%
%% F = partial:cut(f(_, x, y, z)).
%%
%% into:
%%
%% F = fun (Arg1) ->
%% f(Arg1, x, y, z)
%% end.
%% @end
cut_function(MarkerLine, [MarkerArgument])->
case MarkerArgument of
?Q("_@Name(_@@Args)") ->
Line = erl_syntax:get_pos(MarkerArgument),
CutVariables = cut_variables(Line, Args),
CutArguments = cut_arguments(Args, CutVariables),
CutFun = merl:qquote(Line,
"fun (_@@variables) ->"
" _@name(_@@arguments)"
" end",
[{variables, CutVariables},
{name, Name},
{arguments, CutArguments}]),
{ok, CutFun};
_ ->
transform_error(
MarkerLine,
"partial:cut/1 requires a function call as an argument")
end;
cut_function(MarkerLine, MarkerArguments) ->
transform_error(
MarkerLine,
io_lib:format(
"partial:cut/1 requires a single argument, got ~b",
[length(MarkerArguments)])).
%% @doc
%% Transform the AST for:
%%
%% F = partial:cute(f(_, x, y, z)).
%%
%% into:
%%
%% F = (fun () ->
%% Arg2 = x,
%% Arg3 = y,
%% Arg4 = z,
%% fun (Arg1) ->
%% f(Arg1, Arg2, Arg3, Arg4)
%% end
%% end)().
%% @end
cute_function(MarkerLine, [MarkerArgument])->
case MarkerArgument of
?Q("_@Name(_@@Args)") ->
Line = erl_syntax:get_pos(MarkerArgument),
CutVariables = cut_variables(Line, Args),
CuteMatches = cute_matches(Line, Args),
CuteVariables = cute_variables(CuteMatches),
CuteArguments = cute_arguments(Args, CutVariables, CuteVariables),
CuteFun = merl:qquote(Line,
"(fun () ->"
" _@@matches,"
" fun (_@@variables) -> _@name(_@@arguments) end"
" end)()",
[{matches, CuteMatches},
{variables, CutVariables},
{name, Name},
{arguments, CuteArguments}]),
{ok, CuteFun};
_ ->
transform_error(
MarkerLine,
"partial:cute/1 requires a function call as an argument")
end;
cute_function(MarkerLine, MarkerArguments)->
transform_error(
MarkerLine,
io_lib:format(
"partial:cute/1 requires a single argument, got ~b",
[length(MarkerArguments)])).
is_cut_variable(Var) ->
case Var of
?Q("_") ->
true;
_ ->
false
end.
variable_name(Type) ->
Name = io_lib:format("PartialArgument_~s_~w", [Type, make_ref()]),
erlang:list_to_atom(lists:flatten(Name)).
variable(Line, Type) ->
erl_syntax:set_pos(erl_syntax:variable(variable_name(Type)), Line).
cut_variables(Line, Args) ->
[variable(Line, cut) || Arg <- Args, is_cut_variable(Arg)].
cut_arguments([], []) ->
[];
cut_arguments([Arg | Args], Variables) ->
case is_cut_variable(Arg) of
true ->
[hd(Variables) | cut_arguments(Args, tl(Variables))];
false ->
[Arg | cut_arguments(Args, Variables)]
end.
cute_matches(Line, Args) ->
MatchExpr = fun (Arg) ->
Pattern = variable(Line, cute),
merl:qquote(Line,
"_@pattern = _@arg",
[{pattern, Pattern}, {arg, Arg}])
end,
[MatchExpr(Arg) || Arg <- Args, not is_cut_variable(Arg)].
cute_variables(Matches) ->
lists:map(
fun (Match) -> ?Q("_@Pattern = _@_") = Match, Pattern end,
Matches).
cute_arguments([], [], []) ->
[];
cute_arguments([Arg | Args], CutVariables, CuteVariables) ->
case is_cut_variable(Arg) of
true ->
[hd(CutVariables) | cute_arguments(Args,
tl(CutVariables),
CuteVariables)];
false ->
[hd(CuteVariables) | cute_arguments(Args,
CutVariables,
tl(CuteVariables))]
end.