Erlang Central

Difference between revisions of "Match Specifications And Records (Dynamically!)"

From ErlangCentral Wiki

(Working Code)
(Dynamic Match Spec Library)
(2 intermediate revisions by one user not shown)
Line 1:Line 1:
 
== Author ==
 
== Author ==
 
Gordon Guthrie
 
Gordon Guthrie
 
  
 
== Overview ==
 
== Overview ==
Line 232:Line 231:
 
build([],N,Acc)          -> build([],N-1,['_'|Acc]).
 
build([],N,Acc)          -> build([],N-1,['_'|Acc]).
 
</code>
 
</code>
 +
[[Category:HowTo]]

Revision as of 12:53, 3 September 2008

Contents

Author

Gordon Guthrie

Overview

This How To shows how dynamic match specifications can be built using records.

This approach allows record definitions to be extended by adding new fields, or the order of fields in records to be amended, without affecting the dynamic match specifications.

It works by generating a helper library function at compile time which enables a match_specifications module to introspect the record structure and build dynamic match specs.

The Problem

Records are a great way of making tuples easier to handle - they are the Erlang equivalent of a C struct. The ability to reference an element in a structure without having to know it is the third element is great, but...

...the problem is that sometimes you actually need to know the structure - and the main time is when you want to write match specifications.

There is a 'work around' for writing transforming code that has records in it but which generates valid match specs. This uses the function fun2ms from stdlib (see the documentation [1]).

But the problems with fun2ms is that it can't take a variable so you *can* use it to make writing static match specifications easier but you *can't* use it to generate them dynamically.

You might think the answer is to use fun2ms within an eval but you don't have access to the record definition in an eval so you are snookered there too..

The Solution

The solution is to use the Erlang Preprocessor to parse the .hrl file that has the record definition and output a helper function.

This helper function must be generated everytime the header file is changed as part of the make procedures for you application.

Dynamic match specifications written with the library will then 'just work'!

Working Code

The working code for this How To is appended at the end.

Worked Example

The Record Definition

My .hrl file contains a record definition like this:

%% Test Macros
-define(HN_URL1,   "http://127.0.0.1:9000").
-define(HN_URL2,   "http://127.0.0.1:9001").

-record( index,
{
    site,
    path,
    column,
    row
}).

At run time I need to be able to introspect this structure and ask questions like:

  • how many fields does record index have?
  • where is the field path in record index?

Generating The Helper Function

We use the Erlang preprocesser function to parse the header file:

{ok,Tree}=epp:parse_file("myheader.hrl",["./"],[]),

which produces output like:

{attribute,1,file,{"myheader.hrl",1}},
{attribute,18,record,
  {index,
   [{record_field,20,{atom,20,site}},
    {record_field,21,{atom,21,path}},
    {record_field,22,{atom,22,column}},
    {record_field,23,{atom,23,row}}]}},
    [...]

We step through this output and generate a helper file which looks like this:

%% This module automatically generated - do not edit

%%% This module provides utilities for use in building
%%% match specifications from records

-module(ms_util2).

-export([get_index/2,no_of_fields/1]).

no_of_fields(index) -> 4;

get_index(index,site)-> 1;
get_index(index,path)-> 2;
get_index(index,column)-> 3;
get_index(index,row)-> 4;
get_index(index,F) -> exit({error,"Record: index has no field called "++atom_to_list(F)}).

Writing Dynamic Match Specs

We can now write a little library that enables us to write dynamic match specs and we can use the atom names of records (index and path) and the function with make_ms to make a dymamic match spec which will always be right at run time:

ms_util:make_ms(index,[{path,"Match Me!"}]).

will return

{index,'_','"Match Me!",'_','_'}

Source Code

Here is the source code for the example

Record In .hrl File

%% Test Macros
-define(HN_URL1,   "http://127.0.0.1:9000").
-define(HN_URL2,   "http://127.0.0.1:9001").

-record( index,
{
    site,
    path,
    column,
    row
}).

Preprocess Functions

%%%-------------------------------------------------------------------
%%% File        : record_util.erl
%%% Author      : Gordon Guthrie gordon@hypernumbers.com
%%% Description : utilities for manipulating records
%%%
%%% Created     :  2 Sep 2008 by Gordon Guthrie 
%%%-------------------------------------------------------------------
-module(make_ms_util).

-include("myheader.hrl").

-export([make/0]).

-define(MODULENAME,"ms_util2").

make() ->
    {ok,Tree}=epp:parse_file("myheader.hrl",["./"],[]),
    Src=make_src(Tree),
    ok=file:write_file(?MODULENAME++".erl",list_to_binary(Src)).

make_src(Tree) -> make_src(Tree,[]).

make_src([],Acc)                              -> make_src2(Acc,[],[]);
make_src([{attribute,_,record,Record}|T],Acc) -> make_src(T,[Record|Acc]);
make_src([_H|T],Acc)                          -> make_src(T,Acc).

make_src2([],Acc1,Acc2)    -> top_and_tail(Acc1,Acc2);
make_src2([H|T],Acc1,Acc2) -> {NewAcc1,NewAcc2}=expand_rec(H),
			      make_src2(T,[NewAcc1|Acc1],[NewAcc2|Acc2]).

expand_rec({Name,Def}) -> expand_fields(Name,Def,1,[]).

expand_fields(Name,[],N,Acc) -> {mk2(Name,N-1),lists:reverse([mk(Name)|Acc])};
expand_fields(Name,[{record_field,_,{atom,_,F},_}|T],N,Acc) -> 
    expand_fields(Name,T,N+1,[mk(Name,F,N)|Acc]);
expand_fields(Name,[{record_field,_,{atom,_,F}}|T],N,Acc) -> 
    expand_fields(Name,T,N+1,[mk(Name,F,N)|Acc]);
expand_fields(Name,[H|T],N,Acc) -> expand_fields(Name,T,N+1,Acc).

%% mk2/1 builds the no of fields fns
mk2(Name,N) -> "no_of_fields("++atom_to_list(Name)++") -> "++
		   integer_to_list(N)++";\n".

%% mk/1 builds an error line
mk(Name) -> "get_index("++atom_to_list(Name)++",F) -> "++
		"exit({error,\"Record: "++atom_to_list(Name)++
		" has no field called \"++atom_to_list(F)});\n".

mk(Name,Field,N) -> 
    "get_index("++atom_to_list(Name)++","++
	atom_to_list(Field)++")-> "++integer_to_list(N)++";\n".

top_and_tail(Acc1,Acc2)->
    Top="%% This module automatically generated - do not edit\n"++
	"\n"++
	"%%% This module provides utilities for use in building\n"++
	"%%% match specifications from records\n"++
	"\n"++
	"-module("++?MODULENAME++").\n"++
	"\n"++
	"-export([get_index/2,no_of_fields/1]).\n"++
	"\n",
    Tail1="no_of_fields(Other) -> exit({error,\"Invalid Record Name: \""++
	"++Other}).\n\n\n",
    Tail2="get_index(Record,_Field) -> exit({error,\""++
	"Invalid Record Name: \"++Record}).\n",
    Top++lists:flatten(Acc1)++Tail1++lists:flatten(Acc2)++Tail2.

Dynamic Match Spec Library

%%%-------------------------------------------------------------------
%%% File        : ms_util.erl
%%% Author      : Gordon Guthrie gordon@hypernumbers.com
%%% Description : this is the match spec utilities module
%%%               it works closely with ms_util2.erl which is
%%%               the generated module that 'introspects'
%%%               the record structures of myheader.hrl
%%%
%%% Created     :  3 Sep 2008 by Gordon Guthrie 
%%%-------------------------------------------------------------------
-module(ms_util).

-export([make_ms/2]).

%%%
%%% External Functions (API)
%%%

make_ms(Rec,List) when is_atom(Rec), is_list(List) ->
    NoFields=ms_util2:no_of_fields(Rec),
    NewList=proc_list(Rec,List),
    Return=list_to_tuple([Rec|build(NewList,NoFields)]),
    Return.

%%%
%%% Internal Functions
%%%

proc_list(Rec,List) -> proc_list(Rec,List,[]).

%% bit funky - return the list sorted in reverse order
proc_list(Rec,[],Acc)            -> lists:reverse(lists:keysort(1,Acc));
proc_list(Rec,[{Field,B}|T],Acc) -> 
    proc_list(Rec,T,[{ms_util2:get_index(Rec,Field),B}|Acc]).

build(List,NoFields) -> build(List,NoFields,[]).

build([],0,Acc)           -> Acc;
build([{N,Bits}|T],N,Acc) -> build(T,N-1,[Bits|Acc]);
build([H|T],N,Acc)        -> build([H|T],N-1,['_'|Acc]);%don't drop H - will match later
build([],N,Acc)           -> build([],N-1,['_'|Acc]).