Record
Functions for working with Records.
A record is a tagged tuple which contains one or more elements where the first element is an atom. We can manually create a record by simply defining such a tuple:
iex> record = { User, "José", 25 }
iex> is_record(record, User)
true
However, manually constructing tuples can be quite error prone. If we need to add a new field to our User, it would require us to carefully change all places where the tuple is used. Furthermore, as more items are added to the tuple, they lose semantic value.
This module solves these problems by allowing us to name each element and encapsulate the generation and manipulation of such tuples. Much of the functionality provided by this module happens at compilation time, meaning they don't add any runtime overhead while considerably improving the quality of our code.
For these reasons, Records are frequently used in Elixir and are also very useful when combined with Protocols. This module provides different mechanisms for working with records and we are going to explore them in the following sections.
defrecordp
The simplest way of working with records is via defrecordp:
defmodule User do
defrecordp :user, name: "José", age: 25
end
In the example above, defrecordp is going to generate a set of
macros named user that allows us to create, update and match
on a record. Our record is going to have two fields, a name with
default value of "José" and age with default value 25.
Let's see some examples:
# To create records
user() #=> { :user, "José", 25 }
user(age: 26) #=> { :user, "José", 26 }
By using the user macro, we no longer need to explicitly create
a tuple with all elements. It also allows us to create and modify
values by name:
# Create a new record
sample_user = user()
# And now change its age to 26
user(sample_user, age: 26)
Since user is a macro, all the work happens at compilation time.
This means all operations, like changing the age above, work
as a simple tuple operation at runtime:
# This update operation...
user(sample_user, age: 26)
# Literally translates to this one:
set_elem(sample_user, 2, 26)
For this reason, the following operation is not allowed as all values need to be explicit:
new_values = [age: 26]
user(sample_user, new_values)
As the name says, defrecordp is useful when you don't want to
expose the record definition. The user macro used above, for
example, is only available inside the User module and nowhere else.
You can find more information in Kernel.defrecordp/3 docs.
defrecord
By using defrecord, a developer can make a Record definition
available everywhere within Elixir code. Let's see an example:
defrecord User, name: "José", age: 25
User[] #=> User[name: "José", age: 25]
User[age: 26] #=> User[name: "José", age: 26]
All the functionality discussed above happens at compilation time.
This means that both user(age: 26) and User[age: 26] are expanded
into a tuple at compile time.
However, there are some situations where we want to set or update
fields dynamically. defrecord (and not defrecordp ) supports
this behaviour out of the box:
defrecord User, name: "José", age: 25
opts = [name: "Hello"]
user = User.new(opts)
#=> User[name: "Hello", age: 25]
user.update(age: 26)
#=> User[name: "Hello", age: 26]
All the calls above happen at runtime. It gives Elixir records flexibility at the cost of performance since there is more work happening at runtime.
The above calls (new and update) can accept both atom and string
keys for field names, however not both at the same time. This feature
allows to "sanitize" untrusted dictionaries and initialize/update
records without using Kernel.binary_to_existing_atom/1.
To sum up, defrecordp should be used when you don't want to expose
the record information while defrecord should be used whenever you
want to share a record within your code or with other libraries or
whenever you need to dynamically set or update fields.
The standard library contains excellent examples of both use cases,
with HashDict being implemented with defrecordp
and Range with defrecord.
You can learn more about records in the Kernel.defrecord/3 docs. Now
let's discuss the usefulness of combining records with protocols.
Protocols
Developers can extend existing protocols by creating their own records and implementing the desired protocols. For instance, imagine that you have created a new representation for storing date and time, represented by the year, the week of the year and the week day:
defrecord WeekDate, year: nil, week: nil, week_day: nil
Now we want this date to be represented as a string and this
can be done by implementing the String.Chars protocol for
our record:
defimpl String.Chars, for: WeekDate do
def to_string(WeekDate[year: year, week: week, week_day: day]) do
"#{year}-W#{week}-#{day}"
end
end
Now we can explicitly convert our WeekDate:
to_string WeekDate[year: 2013, week: 26, week_day: 4]
"2013-W26-4"
A protocol can be implemented for any record, whether
generated with defrecordp or defrecord.
Summary
| deffunctions(values, env) | Define record functions skipping the module definition |
| defmacros(name, values, env, tag \\ nil) | Define macros for manipulating records |
| deftypes(values, types, env) | Define types and specs for the record |
| extract(name, opts) | Extract record information from an Erlang file |
Functions
Define record functions skipping the module definition.
This is called directly by Kernel.defrecord/3. It expects the record
values, a set of options and the module environment.
Examples
defmodule CustomRecord do
Record.deffunctions [:name, :age], __ENV__
Record.deftypes [:name, :age], [name: :binary, age: :integer], __ENV__
end
Define macros for manipulating records.
This is called
directly by Kernel.defrecordp/3. It expects the macro name, the
record values and the environment.
Examples
defmodule CustomRecord do
Record.defmacros :user, [:name, :age], __ENV__
end
Extract record information from an Erlang file.
Returns the fields as a list of tuples.
Examples
Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
#=> [size: :undefined, type: :undefined, access: :undefined, atime: :undefined,
mtime: :undefined, ctime: :undefined, mode: :undefined, links: :undefined,
major_device: :undefined, minor_device: :undefined, inode: :undefined,
uid: :undefined, gid: :undefined]
defrecord FileInfo, Record.extract(:file_info, from_lib: "kernel/include/file.hrl")