Introduction
This is a post about effectively using Erlang records in Elixir code.
Lately I've been working on an Elixir project that uses a few Erlang libraries that represent data using the record data type.
Here's how Erlang's documentation describes records:
A record is a data structure for storing a fixed number of elements. It has named fields and is similar to a struct in C. [...] record expressions are translated to tuple expressions during compilation
Records exist in Elixir too but are mostly there for compatibility with Erlang (for a variety of reasons you're better off using a struct or a map than a record in pure Elixir code). The official Elixir docs about records are brief and left me unsure about how to actually use them, so after some experimenting I've made this brief guide for reference. I hope you find it useful!
Prerequisites
This guide assumes basic familiarity with Elixir's syntax, pattern matching, and tooling. While no prior Erlang experience is required, you should be comfortable working with Elixir's basic data types like tuples and maps.
Overview of Records in Elixir
Broadly, Elixir's Record module lets you do two things:
1. Extract Definitions
Extract definitions of records from existing Erlang header files into an Elixir module as tuples. The functions extract/2
and extract_all/1
are used for this.
2. Generate Helpers
Generate helpers for creating, accessing, and pattern matching record data types from tuples describing records. The macros defrecord/3
and defrecordp/3
are used for this.
It's crucial to note that you need to do both steps (extract and define) to actually use the records defined in Erlang files inside Elixir code. Extracting record definitions from Erlang headers results in tuples describing the records, but does not generate the helpers for creating, accessing, and pattern matching on records.
Simply put, you need to plumb the output of extract/2
or extract_all/1
into defrecord/2
(or defrecordp/3
) to use Erlang records in Elixir code.
Examples of Extracting and Defining Records
As an example, the code below extracts the :gtp
record type from the library gtp_packet.hrl
and puts a tuple defining the record into gtp_def
. Then, :gtp
and gtp_def
are passed to Record.defrecord/3
to generate code for using the gtp
record type:
gtp_def = Record.extract(:gtp, from_lib: "gtplib/include/gtp_packet.hrl")
Record.defrecord(:gtp, gtp_def) # generates the macros gtp/0, gtp/1, gtp/2
Extracting and defining records can get really tedious if you find yourself working with many different record types. Thankfully, Elixir also provides a way to extract records in bulk from Erlang headers. Unfortunately, there's no way to define record helpers in bulk. However, the Enum
module and a simple lambda function comes to the rescue!
# Define helpers dynamically based on the extracted records
Enum.each(Record.extract_all(from_lib: "gtplib/include/gtp_packet.hrl"), fn {name, fields} ->
Record.defrecord(name, fields)
end)
Each tuple from extract_all/2
of {name, fields}
, where name
is simply the name of the record and fields
is a tuple describing the record's fields is passed into defrecord/3
just as in the manually-done first example we saw earlier. I think it's kind of strange there's no extract_and_define
function in Elixir that does this all for you, but whatever, this way gives you a little more control over things (for example, you could modify the names, such as putting a prefix on them, to avoid a collision if you're importing records from several different Erlang libraries into the same Elixir module).
Using Records in Elixir
After you've managed to generate code for working with records using defrecord/3
you can finally use them! If you called defrecord(:foo, foo_defs)
(where foo_defs
was returned from extract/2
) you'd end up with three new macros all named foo
. Let's imagine the record type foo
contains the fields bar
and baz
. In fact, you can do all this in iex
as long as you wrap in inside a module definition:
iex> defmodule Foo do
...> Record.defrecord(:foo, [bar: :undefined, baz: :undefined])
...> end
Creating Records
Now let's use foo/0
to create a foo
record with default values (spoiler: it's boring, but it's a start):
iex> Foo.foo()
{:foo, :undefined, :undefined}
Notice the result is a tuple starting with :foo
.
To make things a little more interesting we can set values in the foo
we're creating:
iex> Foo.foo(bar: :asdf, baz: 1234)
{:foo, :asdf, 1234}
Inspecting Records
Let's say we have a record like {:foo, :asdf, 1234}
and that we're forgetful and can't remember the order of bar
and baz
inside the record. We can use the foo/1
macro generated by defrecord/2
earlier to see the keys and the values laid out clearly:
iex> Foo.foo({:foo, :asdf, 1234})
[bar: :asdf, baz: 1234]
Accessing a Field within Records
You can access individual fields within a record using square brackets, similarly to a Map. To do this, you pass the tuple representation of a record into the generated function with 1-arity, here foo/1
.
iex> my_foo = Foo.foo(bar: :asdf, baz: 1234)
{:foo, :asdf, 1234}
iex> Foo.foo(my_foo)[:bar]
:asdf
iex> Foo.foo(my_foo)[:baz]
1234
The generated 2-arity function foo/2
allows for the same with slightly different syntax:
iex> Foo.foo(my_foo, :bar)
:asdf
iex> Foo.foo(my_foo, :baz)
1234
Now we can clearly see that bar
is set to asdf
and baz
is set to 1234
. This technique can come in really handy for working with records in the repl if you want to inspect them.
Pattern Matching on Records
However, we can go further and even pattern match on fields within records with similar syntax to pattern matching on Maps. Taking the same foo
example record as before, bar
and baz
can be pattern matched into their own variables:
iex> my_foo = Foo.foo(bar: :asdf, baz: 1234)
{:foo, :asdf, 1234}
iex> Foo.foo(bar: bar, baz: baz) = my_foo
{:foo, :asdf, 1234}
iex> bar
:asdf
iex> baz
1234
Pattern matching is very useful if an Erlang library hands you a record from which you want to concisely extract multiple values. Pattern matching can also be done in function heads to select a specific implementation of a function based on the values of its inputs. For more details on pattern matching see the Elixir docs.
Updating Records
Finally, you can update fields in an existing record (leaving other fields untouched) using the 2-arity version of the generated macro, again foo/2
here:
iex> my_foo = Foo.foo(bar: :asdf, baz: 1234)
{:foo, :asdf, 1234}
iex> Foo.foo(my_foo, bar: :qwerty)
{:foo, :qwerty, 1234}
iex> Foo.foo(my_foo, bar: :qwerty, baz: 5678)
{:foo, :qwerty, 5678}
Summary
I've tried to show in detail with plenty of examples how to use the functionality of Elixir's Record
module. This includes extracting record definitions from Erlang header files using extract/2
and extract_all/2
, generating macros for working with records via defrecord/3
and defrecordp/3
, and examples of how to read, write, and update record data types in the previous few subsections.
Hopefully this guide fills in the gaps in the official documentation and makes at least one other person's life a little easier.