This library builds on annotations to provide the ability to generate wrapper/delegate functions dynamically, based on annotated targets.
Take a basic example of boilerplate wrapper functions:
info(Msg, Args) ->
log(info, Msg, Args).
warn(Msg, Args) ->
log(warn, Msg, Args).
error(Msg, Args) ->
log(error, Msg, Args).
log(Level, Message, Args) ->
case erlang:get({simple_log, loglevel}) of
Level -> io:format(Message, Args);
_ -> ok
end.Instead of writing lots of wrapper functions by hand, we can generate these
at compile time and have delegate forward the required arguments to the
base log/3 function on our behalf.
-module(simple_log).
-export([log/3]).
-compile({no_auto_import, [error/2]}).
-include_lib("annotations/include/annotations.hrl").
-delegate([{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']},
{arity, 2}]).
log(Level, Message, Args) ->
case erlang:get({?MODULE, loglevel}) of
Level ->
io:format(Message, Args);
_ ->
ok
end.The delegate annotation takes a list of properties, whose elements have the
following meaning:
delegatecontains a list of function names that you wish to generatearityspecifies the arity you wish the exported function(s) to haveargscontains a specification for the input arguments you wish to forward to the target (i.e., annotated) function
We will look at the args specification format in more detail later on, but
for now we need to know that $T refers to the target/annotated function
and that $I refers to the complete list of input arguments to the wrapper
function. After build/processing, this application of the delegate attribute
will provide the following modifications to the simple_log module:
-annotation({annotation, delegate,
{function, {simple_log, log, 3}},
[{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']}, {arity, 2}]}).
-export([info/2]).
-export([warn/2]).
-export([error/2]).
info(V73, V45) ->
delegate:delegate_advice({annotation, delegate,
{function, {simple_log, log, 3}},
[{target, info},
{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']}, {arity, 2}]},
simple_log, log, [V73, V45]).
warn(V51, V95) ->
delegate:delegate_advice({annotation, delegate,
{function, {simple_log, log, 3}},
[{target, warn},
{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']}, {arity, 2}]},
simple_log, log, [V51, V95]).
error(V60, V32) ->
delegate:delegate_advice({annotation, delegate,
{function, {simple_log, log, 3}},
[{target, error},
{delegate, ["info", "warn", "error"]},
{args, ['$T', '$I']}, {arity, 2}]},
simple_log, log, [V60, V32]).
log(Level, Message, Args) ->
case erlang:get({simple_log, loglevel}) of
Level -> io:format(Message, Args);
_ -> ok
end.The delegate_advice implementation simply forwards the call to your
target (log/3) function with the arguments formatted as per your
specification.
Another example of this pattern can be seen in the base function bin_op
(taken from the ogql library), which has
numerous wrappers for specific operator names:
-delegate([{args, ['$T', '$I']},
{arity, 2},
{delegate, [
"eq", "gt",
"gteq", "lt",
"lteq", "like",
"contains", "starts_with",
"ends_with", "matches", "path_exists"
]}]).
binop(Op, Axis, {{_,_,_}, _}=Literal) ->
binop(Op, Axis, {literal, Literal});
binop(Op, Axis, Literal) when is_integer(Literal) orelse
is_float(Literal) orelse
is_list(Literal) orelse
is_record(Literal, semver) ->
binop(Op, Axis, {literal, Literal});
binop(Op, Axis, {literal, _}=Literal) ->
{Axis, {operator, Op}, Literal}.The following atoms have special meaning when used in an argument spec:
$XaTheannotationrecord passed to the codegen call$XdThedatafield from theannotationrecord$MThe module in which theannotationwas applied$FThe name of the generated function currently executing$AThe arity of the generated function currently executing$TThe name of the target (annotated) function$IThe complete set of input arguments to the generated function
In addition to these, the atoms [$0, $1 .. $N] refer to each input argument
in position. If you refer to an input argument whose index is greater than or
equal to the arity of the generated function, then codegen will fail.
If you specify the $I input arguments by themselves, then will be
concatenated with the rest of the specification, such that [$T, $I] will
produce the following arguments when $T resolves to foo and $I resolves
to ["Hello ~p~n", [world]]: [foo, "Hello ~p~n", [world]].
If you want to have the input arguments provided as a single (list) input,
then you must specify it like so: [$T, [$I]]: [foo, ["Hello ~p~n", [world]]].
You can also supply a tuple instead of a list, in which case the tuple may contain the same special atoms in any of its fields:
{args, {call, {'$M', '$T', ['$I']}}}
The call atom is treated as a literal, whereas the other elements are
resolved prior to forwarding the call to your target/annotated function.
You can also combine the use of lists and tuples in specifying the way in which you want the input arguments transformed prior to delegation taking place.
This work is distributed under a permissive BSD-style license.
This project will adhere to the principles of semantic versioning once a first public API is declared.