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.

Source

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

Types

t :: tuple

Functions

deffunctions(values, env)

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
Source
defmacros(name, values, env, tag \\ nil)

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
Source
deftypes(values, types, env)

Define types and specs for the record.

Source
extract(name, opts)

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")
Source