Erlang: records
Join the DZone community and get the full member experience.
Join For FreeErlang is mostly a functional language, and functions go hand in hand with immutable data structures that are passed around on the stack to represent the state of a process.
One of the simplest way to represent a record-like structure with a fixed number of fields is with a tuple. To create a Purchase tuple, we can link up the Name, Price and Merchant variables into a single structure:
Purchase = {Name, Price, Merchant}.
Extracting the values again is performed via pattern matching on the tuple's structure:
doSomething({Name, Price, Merchant}) -> execute(Name).
However, this structure is prone to break upon modifications. For example:
- adding a date field means all the code pattern matching the tuple must be reviewed and updated to add another field.
- The same goes if we remove merchant or another field, or if we want to reorder the fields for importance.
- The structure does not scale to even very few fields (say 6), the ones you would put into a relational table.
Thus, for representing a C-like struct, Erlang provides a record type that you can use to specify a little abstraction over plain tuples.
Abstraction
A record's underlying representation is still a tuple, but it is accessed by name instead of by index. However, it can be compared more to a C struct or a Java object than to a map, since it is not dynamic: the name of fields and their number are fixed at compile time.
Records scale the tuple model to more fields, decoupling the accessed and modifications of fields from knowing anything about unrelated fields in the same record (save the record name).
There are runtime checks builtin when you pattern match on records, so while calling a function that expects a record with the wrong argument, you'll see a function_clause error indicating a bug.
Getting our hands dirty
There's an effective way of learning new syntaxes and models for coding: try them. So I set out with O'Reilly Erlang Programming book and wrote exploration tests for Erlang's own syntax that is explained there.
Defining a record is simple: you have to provide a name and a list of fields, which are all atoms. Additionally, you can specify default values for fields; in this case I'm using an atom too as a value, but it can be anything, even another record.
-module(records_10). -include_lib("eunit/include/eunit.hrl"). -record(purchase, {name, price, merchant=default}).
Creating a record means assigning to a new variable that can contain the data structure. The syntax makes use of # to identify a record with the chosen atom.
record_creation_test() -> P = #purchase{name="Giorgio", price=10.00, merchant=bakery}, ?assertEqual("Giorgio", P#purchase.name).
Default values can of course be omitted:
record_default_value_test() -> P2 = #purchase{name="Giorgio", price=10.00}, ?assertEqual(default, P2#purchase.merchant).
Modifying a record only involves the fields whose value you want to change; since Erlang variables are immutable, you will have to assign the record to a new identifier.
record_modification_test() -> P = #purchase{name="Giorgio", price=10.00}, NewP = P#purchase{price=20.00}, ?assertEqual(#purchase{name="Giorgio", price=20.00}, NewP).
This should remind you of the classic Value Object pattern: immutable values that generate a new structure upon modification. However, behavior for these "objects" is left to functions accepting them as arguments, and there is no encapsulation of fields nor hiding of the internal structure (which for many Value Objects is really fine.)
Pattern matching can be performed not only on tuples and lists but also on records:
record_pattern_matching_test() -> P = #purchase{name="Giorgio", price=10.00}, ?assertEqual(2.0, fixed_tax(P)).
fixed_tax(#purchase{price=Price} = Purchase) -> Price * 0.20.
Like for multiple function clauses, constraints can be specified in the patterns in order to select the appropriate body:
record_multiple_pattern_matching_test() -> ?assertEqual(2.0, variable_tax(#purchase{name="Giorgio", price=10.00})), ?assertEqual(0.4, variable_tax(#purchase{name="Giorgio", price=10.00, merchant=bakery})).
variable_tax(#purchase{price=Price,merchant=bakery} = Purchase) -> Price * 0.04; variable_tax(#purchase{price=Price} = Purchase) -> Price * 0.20.
Finally, you can also introspect a bit how a record structure contains, with a reflection function:
record_info_on_fields_test() -> ?assertEqual([name, price, merchant], record_info(fields, purchase)).
Conclusions
Programming is not only building a Turing-complete platform but also satisfying many non functional requirements like the ability to evolve the code in months and years. Defining records instead of tuples helps in maintenance and in expressing concepts which could remain hidden in the variable soup.
All the code for this article is available on Github.
Opinions expressed by DZone contributors are their own.
Comments