"Dynamic" record access functions with EMP1
Brian Olsen (over at Programming Experiments) wrote a small set of functions to make record accesses/updates in Erlang nicer. Ayrnieu wrote a detailed response to this in a comment on Reddit.
Brian wanted to hide some of the (admittedly pretty ugly) syntax of Erlang records in a simple way. He used some run-time list-searching to find the position in the record tuple that a particular field name occurs at, and then located the desired value at that position.
Now that we have EMP1 working I thought that perhaps I might see how I would use this particular tool to solve the same problem.
First of all we need to figure out what the functions we want should look like. I think something like this would do nicely:
recval(FieldName, Record) -> Value.
setrecval(FieldName, Record, Value) -> Updated Record.
Of course under the covers recval and setrecval would examine the record given and work out which field to retrieve / update.
Both Brian and Ayrneiu have this work done at run-time. With EMP1 we can build the supporting functions at compile-time based on the record information (which is already known at compile-time).
In detail, recval and company would look something like this:
recval(FieldName, Record) -> recval(element(1, Record), FieldName, Record).
recval(record1, field1, Record) -> element(2, Record);
recval(record1, field2, Record) -> element(3, Record);
recval(record2, field1, Record) -> element(2, Record);
...
...and similarly for the setrecval versions.
These functions can all be created at compile-time with EMP1, like this:
-module(dyrec_macro).
-export([recval_generate/1]).
recval_field(NameRecord, NameField, Posn) ->
io_lib:format("recval(~w, ~w, Record) -> element(~B, Record)",
[NameRecord, NameField, Posn]).
setrecval_field(NameRecord, NameField, Posn) ->
io_lib:format(
"setrecval(~w, ~w, Record, Value) -> setelement(~B, Record, Value)",
[NameRecord, NameField, Posn]).
recval_record(RecordDetails) -> recval_record(RecordDetails, 2, []).
recval_record({_NameRecord, []}, _Posn, Text) -> Text;
recval_record({NameRecord, [NameField|NameFieldsRest]}, Posn, Text) ->
recval_record({NameRecord, NameFieldsRest}, Posn + 1,
Text ++ "; " ++ recval_field(NameRecord, NameField, Posn)).
setrecval_record(RecordDetails) -> setrecval_record(RecordDetails, 2, []).
setrecval_record({_NameRecord, []}, _Posn, Text) -> Text;
setrecval_record({NameRecord, [NameField|NameFieldsRest]}, Posn, Text) ->
setrecval_record({NameRecord, NameFieldsRest}, Posn + 1,
Text ++ "; " ++ setrecval_field(NameRecord, NameField, Posn)).
recval_generate(ListRecordDetails) ->
[$;,32|CodeGet] = lists:flatten(
lists:map(fun(E) -> recval_record(E) end, ListRecordDetails)),
[$;,32|CodeSet] = lists:flatten(
lists:map(fun(E) -> setrecval_record(E) end, ListRecordDetails)),
"recval(Field, Record) -> recval(element(1, Record), Field, Record). "
"setrecval(Field, Record, Value) -> "
"setrecval(element(1, Record), Field, Record, Value). " ++
io_lib:format("~s. ~s.", [CodeGet, CodeSet]).
And here is a test program:
-module(dyrec_test).
-export([start/0]).
-compile({parse_transform, emp1}).
-record(data1, {this, that}).
-record(data2, {this, the_other}).
-macro({dyrec_macro, recval_generate,
[[{data1, [this, that]}, {data2, [this, the_other]}]]}).
start() ->
D1 = #data1{this=a, that=b},
D2 = #data2{this=c, the_other=d},
D3 = setrecval(this, D1, e),
io:format("~p~n~p~n~p~n~p~n~p~n",
[recval(this, D1), recval(that, D1),
recval(this, D2), recval(the_other, D2),
D3]).
After compiling both of them, we can run this at the REPL:
1> dyrec_test:start().
a
b
c
d
{data1,e,b}
ok
2>
Personally I would not use EMP1 for this (particular) purpose. I do not mind Erlang's record syntax, but if I really did not want to use it I would rather build a parse-transformation (a la Yariv's recless module) to convert a different syntax into the record tuples Erlang uses behind the scenes.
By layering function calls on top of record/tuple field accesses we destroy the ability of Erlang's compiler to convert the usual record syntax into direct tuple element lookups at the point of reference. With this approach the runtime now has to perform a pattern match on record and field names before finding the appropriate value. (Possibly this overhead could be removed by the use of the compiler's 'inline' option, though.)
So my verdict on this jaunt into using EMP1 for layering function calls on record accesses, is "certainly possible, but not necessarily practical". Wait for EMP2 and use that instead. :-)
No comments:
Post a Comment