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:
delegate
contains a list of function names that you wish to generatearity
specifies the arity you wish the exported function(s) to haveargs
contains 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:
$Xa
Theannotation
record passed to the codegen call$Xd
Thedata
field from theannotation
record$M
The module in which theannotation
was applied$F
The name of the generated function currently executing$A
The arity of the generated function currently executing$T
The name of the target (annotated) function$I
The 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.