A native record is a data structure for storing a fixed number of elements in named fields. Unlike traditional tuple-based records described in the previous section, a native record is a distinct type.
Warning
Native records are considered experimental in Erlang/OTP 29; that is, if necessary, there may be incompatible changes in Erlang/OTP 30.
Change
Native records were introduced in Erlang/OTP 29.
Defining Native Records
A native-record definition consists of the # character followed by
the record name and a set of named fields. Field names must be
atoms. Each field can have an optional default value and type
annotation.
-record #Name{Field1 [= Expr1],
...
FieldN [= ExprN]}.The #Name syntax denotes a native record definition. Name is an
atom. As opposed to tuple-based records, it is not necessary to
quote atoms that look like variable names or keywords.
Examples:
-record #div{class}.
-record #State{}.
-record #'42'{}.A default value must be a literal or a simple expression evaluable at compile time. The expression must not contain variables, function calls, or record constructions.
A native record definition can be placed anywhere among the attributes and function declarations of a module, but the definition must appear before any usage of the record.
By default, the fields of a native record are only accessible within the defining module. To make them accessible to other modules, the record must be exported.
A native record should never be defined in a header file.
Exporting Records
To allow the fields of a record to be used outside its defining module, the record needs to be exported.
-export_record([Name1, Name2, ..., NameN]).Name1, Name2, and so on are record names (atoms) defined in the
module that are to be accessible from other modules.
Example:
-module(vector_lib).
-record #vector{x = 0.0, y = 0.0}.
-record #position{x = 0.0, y = 0.0}.
-export_record([vector, position]).Importing Records
To use a native record defined in another module without fully
qualifying its name every time, use -import_record:
-import_record(Module, [Name1, Name2, ..., NameN]).Module, an atom, specifies which module to import records from.
Name1, Name2, and so on are record names (atoms) to be imported.
Example:
-module(example).
-import_record(vector_lib, [vector, position]).Constructing Native Records
The following expression constructs a new Name record where the
value of each field FieldI is the result of evaluating the
corresponding expression ExprI:
#Name{Field1=Expr1, ..., FieldK=ExprK}
#Module:Name{Field1=Expr1, ..., FieldK=ExprK}The fields can be given in any order, not necessarily the same order as in the record definition, and fields can be omitted. Omitted fields are assigned their default values.
It is an error if not all fields are given values either explicitly or through default values. How the error is manifested depends on whether the record construction is local or external.
Local Record Construction
Record construction is local if the first form of the syntax is used
(without a module name) and a native-record definition for Name
appears earlier in the module than the construction expression.
Example:
-module(example).
-export([make_pair/2]).
-record #pair{a, b}.
make_pair(A, B) ->
#pair{a=A, b=B}.1> example:make_pair(1, 2).
#example:pair{a = 1,b = 2}Not giving values to all fields (explicitly or through default values) for a local record results in a compilation error.
Example:
-module(example).
-export([make_empty_pair/0]).
-record #pair{a, b}.
make_empty_pair() ->
#pair{}.Attempting to compile this module results in the following errors:
$ erlc example.erl
example.erl:7:5: field a is not initialized in native record pair
% 7| #pair{}.
% | ^
example.erl:7:5: field b is not initialized in native record pair
% 7| #pair{}.
% | ^
External Record Construction
Record construction is external if either of the following conditions is true:
- The
#Module:Name{...}syntax is used. - The
#Name{...}syntax is used andNamehas been imported.
Examples:
-module(pair_library).
-record #pair{a, b}.
-export_record([pair]).Given the previous record definition, a record value can be constructed
from the example module by prefixing the record name pair with
the module name pair_library:
-module(example).
-export([make_pair/2]).
make_pair(A, B) ->
#pair_library:pair{a=A, b=B}.Alternatively, the pair record can be imported from pair_library:
-module(example).
-export([make_pair/2]).
-import_record(pair_library, [pair]).
make_pair(A, B) ->
#pair{a=A, b=B}.External record construction will fail with an exception if one of the following conditions is true:
The referenced module is not loaded.
The referenced record is not exported.
Giving a value to a field not present in the record definition.
Not giving values to all fields.
Example:
-module(example).
-export([make_empty_pair/0]).
make_empty_pair() ->
#pair_library:pair{}.Record construction fails with an exception at runtime because no
values are given to fields a and b:
1> example:make_empty_pair().
** exception error: no value provided for field a in #pair_library:pair{}Capturing of the Record Definition
When constructing a native record, the record definition is "captured"; that is, included in the created record value. The export status (whether it was exported) of the record at the time of construction is also captured.
Subsequent operations on the record (updating, matching, and accessing individual fields) use the captured record definition, not the definition in the code. These operations will succeed even if the module is unloaded, modified and then reloaded, or executed on another Erlang node where the module containing the record definition has never been loaded.
In other words, the record definition in the module is used only when creating a record. All other record operations use the record definition captured at the time the record was constructed.
Record Field Access
Expr#Name.Field
Expr#Module:Name.FieldExpr is expected to evaluate to a record value.
If the operation is local (no module name given), the record value
must have the name Name and its module must be the same as the name
of the currently executing module.
If the access operation is external (module name given explicitly or
imported), the record value must be named Name and be defined in
module Module. Furthermore, at the time the record was constructed,
the record must have been exported.
In either case, the Field must have existed at the time the record
was constructed.
If the conditions just described are fulfilled, the value of the specified field is returned. Otherwise, the operation fails. If the access operation is in a guard, the guard fails; if it is in a function body, an exception is raised.
Example:
-record #person{name, phone, address}.
get_person_name(Person) ->
Person#person.name.
get_vec_x(Vec) ->
Vec#geom_2d:vec.x.Anonymous Record Field Access
Expr#_.FieldReturns the value of the specified field. Expr is expected to
evaluate to a native record.
The operation will fail if the field does not exist in the captured record definition.
If the module name is not the same as the currently executing module, the operation will only succeed if the record was exported at the time the record was constructed.
Updating Native Records
Updating is not construction, so the captured definition will be used.
Expr#Name{Field1=Expr1, ..., FieldK=ExprK}
Expr#Module:Name{Field1=Expr1, ..., FieldK=ExprK}Expr is expected to evaluate to a record value.
If the operation is local (no module name given), the record value
must have the name Name and its module must be the same as the name
of the currently executing module.
If the update operation is external (module name given explicitly or
imported), the record value must have the name Name and be defined
in the module Module.
A copy of this record is returned, with the value of each specified
field FieldI changed to the value of evaluating the corresponding
expression ExprI. All other fields retain their old values.
The operation will fail with an exception if any of the named fields do not exist in the record definition captured when the record was constructed.
An external record update operation will fail with an exception if the record was not exported from its owning module when the record was constructed.
Anonymous Update
Expr#_{Field1=Expr1, ..., FieldK=ExprK}Expr is expected to evaluate to a native record.
A copy of this record is returned, with the value of each specified
field FieldI changed to the value of evaluating the corresponding
expression ExprI. All other fields retain their old values.
The update operation will fail if any of the fields do not exist in the record definition captured when the record was constructed.
If the module name is not the same as the currently executing module, the operation will only succeed if the record was exported at the time of construction.
The Guard BIF is_record/1
The guard BIF is_record(Term) tests whether Term is a native
record.
If Term is a tuple-based record, is_record/1 returns false.
The Guard BIF is_record/2
The guard BIF is_record(Term, Name), where Name is an atom, tests
whether Term is either a native or a tuple-based record with the name
Name, defined in the current module or (in the case of a native record)
imported from another module.
Example:
-record #vec{x, y}.
increment(Vec) when is_record(Vec, vec) ->
... .It is often more convenient to use matching instead of using
is_record/2:
-record #vec{x, y}.
increment(#vec{}=Vec) ->
... .Matching only the record name succeeds regardless of whether the record was exported at the time of construction.
The Guard BIF is_record/3
The guard BIF is_record(Term, Module, Name), where Module and
Name are atoms, tests whether Term is a native record with the
name Name constructed from a definition in module Module. This BIF
checks the module name and record name captured in the record at the
time it was constructed.
Example:
increment(Vec) when is_record(Vec, geom_2d, vec) ->
... ;
increment(Vec) when is_record(Vec, geom_3d, vec) ->
... .It is often more convenient to use matching instead of is_record/3:
increment(#geom_2d:vec{}=Vec) ->
... ;
increment(#geom_3d:vec{}=Vec) ->
... .Native Records in Guards
Field access is the only native-record operation allowed in guards.
Example:
handle(Msg, State) when State#state.running =:= true ->
...Records in Patterns
Pattern matching uses the same syntax as construction:
#Name{Field1=Expr1, ..., FieldN=ExprN}
#Module:Name{Field1=Expr1, ..., FieldN=ExprN}In this case, one or more of Expr1 ... ExprN can contain unbound
variables.
Matching will fail if one or more of the field names are not present in the record definition captured at the time the record was constructed.
Examples:
-record #vec{}.
len(#vec{x=X, y=Y}) ->
math:sqrt(X * X + Y * Y).For an external matching operation, matching can also fail if at least one field is being matched and, at the time of construction, the record was not exported. If the list of fields to match is empty, the match succeeds as long as the module and record names match.
In the following example, the first clause will match if the module
name geom_2d and the record name vec match, regardless of whether
the record was exported at construction time or not:
is_vec(#geom_2d:vec{}) -> true;
is_vec(_) -> false.Anonymous Pattern Matching
#_{Field1=Pattern1, ..., FieldN=PatternN}If the module name of the native record being matched is the same as that of the currently executing module, it does not matter whether the record was exported at the time the record was constructed. As long as all fields exist and all patterns match, the match succeeds.
If the module name of the native record being matched is not the same as that of the currently executing module, if at least one field is being matched, in order for the match operation to succeed, the record must have been exported at construction time.
Reflection: The records Module
The records module contains functions for constructing and
inspecting native records. The main purpose of the functions in the
records module is for debugging, implementing library functions
(such as printing of native records), and implementing tools (such as
the Debugger). Use with care in production code.
Native Records in the Erlang Shell
Native records can be defined and constructed in the Erlang shell
using the same syntax as in module code. All records constructed
in the shell belongs to the shell_default module.
Examples:
1> -record #pair{a=1, b=1}.
ok
2> #pair{a=42}.
#shell_default:pair{a = 42,b = 1}The shell does not enforce the privacy of non-exported records. That is, the shell will print non-exported records and it also allows constructing non-exported records.
For example, consider the following module that defines the
non-exported record vec:
-module(geometry).
-export([make_vec/2, origin/0, add_vec/2]).
-record #vec{x, y}.
make_vec(X, Y) ->
#vec{x=X, y=Y}.
origin() ->
#vec{x=0.0, y=0.0}.
add_vec(#vec{x=X1, y=Y1}, #vec{x=X2,y=Y2}) ->
#vec{x=X1+X2, y=Y1+Y2}.From the shell, it is possible to both show and construct vec
records:
1> geometry:make_vec(1.0, 7.0).
#geometry:vec{x = 1.0,y = 7.0}
2> #geometry:vec{x = 100.0, y = 99.0}.
#geometry:vec{x = 100.0,y = 99.0}Nested Native Records
Assume the following native record definitions:
-record #nrec0{name = "nested0"}.
-record #nrec1{name = "nested1", nrec0}.
-record #nrec2{name = "nested2", nrec1}.
N2 = #nrec2{nrec1 = #nrec1{nrec0 = #nrec0{}}},Accessing or updating nested records can be written without parentheses:
"nested0" = N2#nrec2.nrec1#nrec1.nrec0#nrec0.name,
N0n = N2#nrec2.nrec1#nrec1.nrec0#nrec0{name = "nested0a"},which is equivalent to:
"nested0" = ((N2#nrec2.nrec1)#nrec1.nrec0)#nrec0.name,
N0n = ((N2#nrec2.nrec1)#nrec1.nrec0)#nrec0{name = "nested0a"},Advanced: Hot Code Updating
When doing hot code updating, it can be necessary to do term conversion. For example, one might want to add or remove fields in a native record.
How to add a field to a native record
Assume that we start out with the following record definition:
-module(add_one_field).
-record #rec{a, b, c, d}.
-export([make_rec/4]).
make_rec(A, B, C, D) ->
#rec{a=A, b=B, c=C, d=D}.Our task is to keep all the existing fields and add the new field.
Here is one way to implement an update/1 function that will take an
old version of the record and return its content in a new version of
the record:
-module(add_one_field).
-record #rec{a, b, c, d, new}.
-export([update/1]).
update(#rec{new=_}=AlreadyUpdated) ->
AlreadyUpdated;
update(#rec{a=A, b=B, c=C, d=D}) ->
#rec{a=A, b=B, c=C, d=D, new=new_value}.Remember that the captured definition is used when matching a record. That makes it possible to access all fields in a record constructed by a previous version of a module.
Let us look at this code in action. First compile the original version
of the module and construct a rec record:
1> c(add_one_field).
{ok,add_one_field}.
2> R = make_rec(1, 2, 3, 4).
#add_one_field:rec{a = 1,b = 2,c = 3,d = 4}Next compile the modified version of the module and call
add_one_field:update(R) to update the record:
3> c(add_one_field).
{ok,add_one_field}.
4> NewR = add_one_field:update(R).
#add_one_field:rec{a = 1,b = 2,c = 3,d = 4,new = new_value}If the update/1 function is called on the already updated record, it
will just return its argument:
5> NewR = add_one_field:update(NewR).
#add_one_field:rec{a = 1,b = 2,c = 3,d = 4,new = new_value}How to delete one field from a native record
Removing a field from a record is not really any more difficult than adding a field, except that the compiler by default will emit a warning.
Assume that we start out with the following record definition:
-module(delete_one_field).
-record #rec{a, b, c, d, opts=[]}.
-export([make_rec/4]).
make_rec(A, B, C, D) ->
#rec{a=A, b=B, c=C, d=D}.
Our task is to remove the d field and incorporate its content into
opts. Here is one way to implement an update/1 function that will
take an old version of the record and return its content in a new
version of the record:
-module(delete_one_field).
-record #rec{a, b, c, opts=[]}.
-export([update/1]).
update(#rec{a=A, b=B, c=C, d=D, opts=Opts}) ->
#rec{a=A, b=B, c=C, opts=[{d,D} | Opts]};
update(#rec{}=AlreadyUpdated) ->
AlreadyUpdated.The first clause refers to the d field, despite it not being present
in the record definition. That will cause a compilation warning, but if
the field exists in the captured record definition matching will succeed.
$ erlc delete_one_field.erl
delete_one_field.erl:5:28: Warning: field d undefined in record rec
% 6| update(#rec{a=A, b=B, c=C, d=D, opts=Opts}) ->
% | ^The warning can be disabled using the nowarn_undefined_field option:
-module(delete_one_field).
-compile(nowarn_undefined_field).
-record #rec{a, b, c, opts=[]}.
-export([update/1]).
update(#rec{a=A, b=B, c=C, d=D, opts=Opts}) ->
#rec{a=A, b=B, c=C, opts=[{d,D} | Opts]};
update(#rec{}=AlreadyUpdated) ->
AlreadyUpdated.Let us look at this code in action. First compile the original
version of the module and construct a rec record:
1> c(delete_one_field).
{ok,delete_one_field}.
2> R = make_rec(1, 2, 3, 4).
#delete_one_field:rec{a = 1,b = 2,c = 3,d = 4,opts = []}Next compile the modified version of the module and call
delete_one_field:update(R) to update the record:
3> c(delete_one_field).
{ok,delete_one_field}.
4> NewR = delete_one_field:update(R).
#delete_one_field:rec{a = 1,b = 2,c = 3,opts = [{d,4}]}If the update/1 function is called on the already updated record, it
will just return its argument:
5> NewR = delete_one_field:update(NewR).
#delete_one_field:rec{a = 1,b = 2,c = 3,opts = [{d,4}]}