Native Records — Erlang System Documentation v29.0.3

Search documentation of Erlang System Documentation

# Native Records

Copy Markdown View Source

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 and `Name` has 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.Field

`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 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#_.Field

Returns 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}]}

← Previous Page Records
Next Page → Errors and Error Handling