Saturday, August 19, 2006

Erlang Metaprogramming Breakthrough: Metacurrying!

Quick download: smerl.erl

This is pretty big.



As I've been hacking away with Smerl last night, I had the following realization: in most cases, the form for a metafunction is mostly known in compile-time, and it only requires a small injection of runtime data to attain its final form.



This is conceptually very similar to currying.



Once these thoughts have fully materialized in my mind, I again sat down by my MacBook and frantically hacked into Smerl the very capability of runtime metacurrying!



What is metacurrying? In regular currying, you bind a value to a function's parameter(s), which gives you a new, simpler, function. In metacurrying, you bind a value to a function form, which gives you a new, simpler, function form!



This is a huge step forward for Smerl. Why? Because it finally frees the programmer from having to know the Erlang abstract format. With metacurrying, Erlang programmers finally get full compile-time checking for their metafunctions, which finally truly blurs the line between compile-time and runtime metaprogramming in Erlang.



I added a few new functions to Smerl:




  • smerl:curry/2 takes a function's abstract form and a parameter value (or list of paramter values) and returns the new, curried form. This is great for debugging and understanding what happens behind the scenes in metacurrying.

  • smerl:curry/4 takes a MetaCtx object or a module name, a function name, arity and parameter value (or list of parameter values) and returns the curried for for the function. This is essentially a smerl:get_func followed by a smerl:curry/2.
  • smerl:curry_replace/3 takes a MetaCtx object, a function form and a parameter value (or list of parameter values) and replaces the old function with the function's curried form

  • smerl:curry_replace/4 takes a MetaCtx object, a function name, arity and parameter value (or list of parameter values) and replaces the old function form with the function's curried form.

  • smerl:rename/2 is a convenience function that takes a function form and returns its renamed form.

  • smerl:curry/5 take a MetaCtx object or a module name, a function name, arity, value and new function name, and returns the curried form after renaming it.



You can read the source for the full documentation.



Ok, enough talk. Let's show an example. Remember my last demo of Smerl's capabilities? Here's the new version, which uses metacurrying:




-module(func_recs).
-export([generate/1]).

generate(Filename) ->
{ok, Forms} = epp:parse_file(Filename, [], []),
lists:foreach(
fun({attribute, _Line, record, {RecName, RecFields}}) ->
case smerl:for_module(RecName) of
{ok, C1} ->
process_module(C1, RecFields);
_Err ->
process_module(smerl:new(RecName), RecFields)
end;
(_Other) -> undefined
end, Forms).

process_module(MetaCtx, RecFields) ->
{_, C2} = lists:foldl(
fun({record_field, _Line,
{atom, _Line2, FieldName}},
{Idx, MetaCtx1}) ->
{Idx+1, process_field(MetaCtx1,
FieldName, Idx)};
({record_field, _Line,
{atom, _Line2, FieldName}, _Def},
{Idx, MetaCtx1}) ->
{Idx+1, process_field(MetaCtx1,
FieldName, Idx)}
end, {2, MetaCtx}, RecFields),
smerl:compile(C2).

get(Idx, Obj) ->
element(Idx, Obj).

set(Idx, Obj, Val) ->
setelement(Idx, Obj, Val).

process_field(MetaCtx, FieldName, Idx) ->
{ok, Getter} =
smerl:curry(func_recs, get, 2, Idx, FieldName),
{ok, Setter} =
smerl:curry(func_recs, set, 3, Idx, FieldName),
{ok, MetaCtx1} = smerl:add_func(MetaCtx, Getter),
{ok, MetaCtx2} = smerl:add_func(MetaCtx1, Setter),
MetaCtx2.


As you can see, there's not a single abstract format for a function in sight :)



What's happening here? The get/2 and set/3 functions are mere skeletons. They are not used directly anywhere in the code. (In fact, when you compile this module, the compiler complains that these functions are unused. Little does it know... :) ) When we process the file person.erl, -- in runtime -- we finally have data we need to embed get/2 and set/3 with the index parameters as well as give them their final names. We achieve this in 1 line of code by calling smerl:curry/5. Not bad, huh?



With metacurrying, I feel that Erlang metaprogramming is finally where it should be. I had thought salvation has arrived in the form (no pun intended : ) ) of fun expressions, but I was wrong (fun expressions are very convenient, but their abstract form is not available in runtime unless you're in the Erlang shell). Metacurrying has finally given us freedom from knowing the Erlang abstract format.



Now Erlang code is just the way I like it: like clay :)



As always, please get the code, give it a test drive, and tell me if you find any problems.



Enjoy!

1 comment:

Peter Arrenbrecht said...

This rocks! Template-based meta-programming is what people most often need anyway.