Erlang Macro Processor (v2), Part I
EMP1 is all well and good, but it does have more than its fair share of idiosyncratic behaviour[1]:
- EMP1 can only be used to create full functions at the top level of a module. This makes it a bit more difficult to use than strictly necessary, especially if we only want to generate a term to use within a function.
- Arguments passed to the macro must be literal values - no function calls allowed!
- Macros must be defined in a separate module, which must be compiled before the macro-using module is compiled.
Today we will begin to tackle this issue with EMP2, but before we dive straight into the parse_transform code I would like to spend a few moments updating our example code. The rewrite will make the example_macro.erl and example.erl modules use the as-yet-unwritten EMP2 module functionality. I probably won't explicitly show it in these posts, but the compile errors I get from running EMP2 over example.erl will have a big influence over the direction that its development takes.
We will still need a separate macro module, but the macro function will only generate the lookup table itself rather than return a whole function definition:
-module(example_macro).
-export([lookup_binary/1]).
lookup_binary(Size) ->
[[$,,FirstVal]|NumberString] = lists:map(
fun(Offset) -> io_lib:format(",~B", [Offset * 2]) end,
lists:seq(0, Size - 1)),
"<<" ++ [FirstVal] ++ NumberString ++ ">>".
We have lost the code that produces the whole function and only kept our lookup binary creation function, which also seems to have picked up a jaunty little Size argument from somewhere. As before, each element's value is twice its offset (modulo 256: we are only storing bytes after all).
To check that the new macro code works correctly:
1> CL = fun(F) -> c(F), l(F) end.
#Fun
2> CL(example_macro).
{module,example_macro}
3> M = fun(R) -> io:format("~s~n", [lists:flatten(R)]) end.
#Fun
4> M(example_macro:lookup_binary(4)).
<<0,2,4,6>>
ok
5>
And we also have to rewrite the module that calls this lookup macro:
-module(example).
-export([lookup/1]).
-compile({parse_transform, emp2}).
-macro_modules([example_macro]).
lookup(Offset) ->
<<_:offset/binary,value:8/integer,_/binary>> =
example_macro:lookup_binary(4),
Value.
This does look a lot nicer than the EMP1 version. Only the snippet of code that needs to be dynamically generated is in the macro module; the rest of the code is in the standard module where it belongs, and the macro call is in a much more appropriate place - inside the function that uses it - than lurking within a module attribute.
With EMP1 we had to peek inside another module to see that a lookup/1 function was being generated, but here we can see that fact already in front of us. We can even guess that a binary term will be created just from the context around the macro call.
Note that 'emp1' has changed to 'emp2' in the parse_transform compiler directive, and that we need a new 'macro_modules' module attribute to tell EMP2 which remote function calls are to be expanded at compile-time.
Once we have written EMP2 and compiled all the modules,we should be able to run the lookup function and receive the same results as we did before:
1> lists:map(fun(N) -> example:lookup(N) end, lists:seq(0, 3)).
[0,2,4,6]
2>
We shall see.
[1] And I cannot have all that competition floating around out there, you know.
No comments:
Post a Comment