Records in function heads
Erlang records get a bit more backlash than they deserve. I like using them quite a bit, and in fact it’s Erlang maps that haven’t won me over yet.
The reason I like them, is they force my modules to adhere to rigid data models. This has the benefit of making me slow down and think a bit ahead of time. There is only one small issue I have with them: records are slightly insane. Records behave a tad differently when used in function bodies than in function heads.
The Erlang mailing list addresses this in dozens of threads, and there are blog-posts that have brought it up, too. But I wanted to address it again, and also share a workaround I use when it comes to default record values as they play-out in function heads.
Record refresher
To set the stage here, a record can have default values assigned. Not unlike how a SQL database field can have default values like ‘NULL’ or a time-stamp, so too can an Erlang record.
A record upon construction can either take a value on that field, and if none given, take the one pre-specified.
Some code
Let’s play with a module that has two records. The ‘animals’ record will have one default value, and ‘plants’ will have none.
-module(the_living_pond).
-export([info/1]).
-record(animals, {fish, foul=[ducks]}).
-record(plants, {aquatic, marshland}).
info(#animals{} = Y) ->
erlang:display({animal_results, Y});
info(#plants{aquatic=A} = P) ->
erlang:display({plants_results, A, P}).
It’s important to understand that records when located in function heads, loosely translate from the above into the following:
...
info({animals, _, _} = Y) ->
erlang:display({animal_results, Y});
info({plants, A, _} = P) ->
erlang:display({plants_results, A, P}).
N.B. In order to capture the value for ‘aquatic plants’ — ie. ‘A’ — we have to know that it sits in the second position in the tuple. This is not cool. Luckily, records save us from this pedantic issue.
To get to the crux of the matter — and the point of this post — it’s
often desirable to explicitly match on default record values in the
function head. Not so obvious at first, is that #animals{}
when in a
function head does not mean {animals, undefined, [ducks]}
. Rather,
it equates to something like {animals, _Foo, _Bar}
.
Now, how could we re-write the first clause of the ‘info’ routine so
it only matches when ‘foul’ equates to the default value of [ducks]
?
It ain’t pretty, but I often go about it this way:
-module(the_living_pond).
-export([info/1]).
-record(animals, {fish, foul=[ducks]}).
-record(plants, {aquatic, marshland}).
info(#animals{} = Y) when
Y#animals.foul =:= #animals{}#animals.foul -> % Wham!
erlang:display({animal_results, Y});
info(#plants{aquatic=A} = P) ->
erlang:display({plants_results, A, P}).
The only thing unsophisticated about this solution is its verbosity. But, alas, it gets us to where we need to be: we ensure that the parameter is an animal record, and enforce a match to the default field value (as desired).
The heuristic here is we don’t want to hard-code default values in
the guard. We want to let it change dynamically should someone want a
new default value for the ‘foul’ field. By guarding the clause with
the strange #animals{}#animals.foul
we accomplish just that.