R – Erlang and run-time record limitations


I'm developing an Erlang system and having reoccurring problems with the fact that records are compile-time pre-processor macros (almost), and that they cant be manipulated at runtime…
basically, I'm working with a property pattern, where properties are added at run-time to objects on the front-end (AS3). Ideally, I would reflect this with a list on the Erlang side, since its a fundamental data type, but then using records in QCL [to query ETS tables] would not be possible since to use them I have to specifically say which record property I want to query over… I have at least 15 columns in the larges table, so listing them all in one huge switch statement (case X of) is just plain ugly.

does anyone have any ideas how to elegantly solve this? maybe some built-in functions for creating tuples with appropriate signatures for use in pattern matching (for QLC)?


Best Solution

It sounds like you want to be able to do something like get_record_field(Field, SomeRecord) where Field is determined at runtime by user interface code say.

You're right in that you can't do this in standard erlang as records and the record_info function are expanded and eliminated at compile time.

There are a couple of solutions that I've used or looked at. My solution is as follows: (the example gives runtime access to the #dns_rec and #dns_rr records from inet_dns.hrl)

%% Retrieves the value stored in the record Rec in field Field.
info(Field, Rec) ->
    Fields = fields(Rec),
    info(Field, Fields, tl(tuple_to_list(Rec))).

info(_Field, _Fields, []) -> erlang:error(bad_record);
info(_Field, [], _Rec) -> erlang:error(bad_field);
info(Field, [Field | _], [Val | _]) -> Val;
info(Field, [_Other | Fields], [_Val | Values]) -> info(Field, Fields, Values).

%% The fields function provides the list of field positions
%% for all the kinds of record you want to be able to query
%% at runtime. You'll need to modify this to use your own records.
fields(#dns_rec{}) -> fields(dns_rec);
fields(dns_rec) -> record_info(fields, dns_rec);
fields(#dns_rr{}) -> fields(dns_rr);
fields(dns_rr) -> record_info(fields, dns_rr).

%% Turns a record into a proplist suitable for use with the proplists module.
to_proplist(R) ->
    Keys = fields(R),
    Values = tl(tuple_to_list(R)),

A version of this that compiles is available here: rec_test.erl

You can also extend this dynamic field lookup to dynamic generation of matchspecs for use with ets:select/2 or mnesia:select/2 as shown below:

%% Generates a matchspec that does something like this
%% QLC psuedocode: [ V || #RecordKind{MatchField=V} <- mnesia:table(RecordKind) ]
match(MatchField, RecordKind) ->
    MatchTuple = match_tuple(MatchField, RecordKind),
    {MatchTuple, [], ['$1']}.

%% Generates a matchspec that does something like this
%% QLC psuedocode: [ T || T <- mnesia:table(RecordKind),
%%                        T#RecordKind.Field =:= MatchValue]
match(MatchField, MatchValue, RecordKind) ->
    MatchTuple = match_tuple(MatchField, RecordKind),
    {MatchTuple, [{'=:=', '$1', MatchValue}], ['$$']}.

%% Generates a matchspec that does something like this
%% QLC psuedocode: [ T#RecordKind.ReturnField
%%                   || T <- mnesia:table(RecordKind),
%%                        T#RecordKind.MatchField =:= MatchValue]
match(MatchField, MatchValue, RecordKind, ReturnField) 
  when MatchField =/= ReturnField ->
    MatchTuple = list_to_tuple([RecordKind
           | [if F =:= MatchField -> '$1'; F =:= ReturnField -> '$2'; true -> '_' end
              || F <- fields(RecordKind)]]),
    {MatchTuple, [{'=:=', '$1', MatchValue}], ['$2']}.

match_tuple(MatchField, RecordKind) ->
           | [if F =:= MatchField -> '$1'; true -> '_' end
              || F <- fields(RecordKind)]]).

Ulf Wiger has also written a parse_transform, Exprecs, that more or less does this for you automagically. I've never tried it, but Ulf's code is usually very good.