tags:

views:

878

answers:

3

Im developing an Erlang system and having reoccurring problems with the fact that record's are compile time pre-processor macros (almost), and that they cant be manipulated at runtime... basically Im 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 datatype, 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)?

thanks

A: 

I'm not sure I fully understand your Problem but I have moved from records to proplists in most cases. They are much more flexible and much slower. Using (d)ets I usually use a few record fields for coarse selection and then check the proplists on the remaining records for detailed selection.

mdorseif
Proplists are handy if you never need to pattern-match them (my main use is options lists as `proplists:get_value/3` allows you to retrieve a value or use a default in one operation.If you ever want to pattern match however, you need records for named slot access.
archaelus
+2  A: 

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)),
    lists:zip(Keys,Values).

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) ->
    list_to_tuple([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.


archaelus
+1  A: 

I solve this problem (in development) by use the parse transform tools to read the .hrl files and generate helper functions.

I wrote a tutorial on it at Trap Exit.

We use it all the time to generate match specs. The beauty is that you don't need to know anything about the current state of the record at development time.

However once you are in production things change! If your record is the basis of a table (as opposed to the definition of a field in a table) then changing an underlying record is more difficult (to put it mildly!).

Gordon Guthrie