I use Erlang and Elixir regularly, and I often get hung up on the differences between Erlang records and Elixir structs. Both serve the same purpose most of the time but are implemented differently.
In this blog post I will document the differences between these two constructs.
Similarities
The Erlang documentation defines records as:
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.
I couldn’t find a succinct definition of Elixir structs on the Elixir website, but the Elixir School website sums them up nicely:
Structs are special maps with a defined set of keys and default values. A struct must be defined within a module, which it takes its name from.
Both records and structs are defined within a module, but Elixir structs take on the name of the module they are defined in, whereas Erlang records are defined with a name of their own. Both are defined with a set of named fields and, optionally, default values for those fields.
Differences
The significant differences between the two constructs are shown below. I’m ignoring syntax here because it doesn’t affect how these constructs are used or behave.
Erlang records | Elixir structs | |
---|---|---|
Field Default Value |
|
|
Evaluation of Field Default |
Runtime |
Compile-time |
Underlying Data Structure |
Tuple |
Map |
Performance |
Same as tuples |
~30% slower than records |
Field Default Value
Erlang typically uses the undefined
atom as an empty value so records use it as their field default value. Structs were created with the Elixir programming language and they use Elixir’s empty value of nil
for their fields.
Evaluation of Field Default Expression
The field default value for records and structs can be defined as any valid expression. However, field default value expressions in Elixir structs are evaluated at compile time. This means that the compiled struct only contains literal values for field defaults. It is not possible to have a field default that is evaluated at runtime. If we define a struct like this:
defmodule Person do
defstruct [
:name,
# This expression is evaluated at compile time
created_at: DateTime.now!("Etc/UTC")
]
end
The created_at
field will be populated with the compilation time and not the time the instance of the person struct was created. Erlang records behave differently. Default field value expressions are evaluated at runtime. This makes it possible to have dynamic defaults. In this case, to have a created_at
field that is initialized to the current timestamp when the record is created:
-record(person, {
name,
% This expression is evaluated at the time the record is initialized
created_at = os:timestamp(),
}).
When an instance of this person record is created the os:timestamp/0
function would be invoked and the created_at
field would be populated with the return value of the os:timestamp/0
function call. Default value expressions like this are not often used but I think the way Erlang records behave with dynamic defaults is more useful.
Tip
|
Throw As A Default Value
Since Erlang field default value expressions are evaluated when a record is created we can use this behavior to create "required" fields that must be populated with a value. Set the field default expression to a
|
Underlying Data Structure
Elixir structs are maps with a struct
key that contains the name of the struct. Erlang records are translated into tuples during compilation with the first tuple item being the name of the record and any expressions using the record syntax are converted to the equivalent tuple expressions. Both records and structs inherit the performance characteristics of their underlying data structures. This brings us to the next difference.
Performance
I copied and modified a benchmark script posted by OvermindDL1 on the Elixir Forum. My final benchmark script benchmarks two operations on three different sizes of records and structs. The script is fairly long so I didn’t include it in this post but it is available here. The output from the benchmarking script is also available here.
Erlang records are faster by about 50%, but neither construct is particularly costly. Only in rare circumstances should the difference in performance merit choosing records over struct. Performance characteristics for updates and limits are similar, with records averaging about 50% faster overall.
Conclusion
There are differences between records and structs that you need to be aware of. The most significant difference between the two is the underlying data structure, but as we have seen the difference in performance isn’t enough to justify choosing one over the other in most cases. It’s also good to be aware of the subtle difference in the way field default value expressions are used but this shouldn’t dictate what construct you use. Records and structs serve the same purpose and in most cases you should simply use whichever construct is more convenient. In Elixir structs are more widely supported and you should use them in most cases. Erlang doesn’t have any built in support for structs, so you would be better off sticking with records when in Erlang.