Запись

Запись — способ оформления кортежа с помощью добавления имён элементам, а также определения их дефолтных значений. То же: record.

Чтобы пользоваться записью, её надо определить (в начале модуля). Для этого мы придумываем ей название, а также названия элементам. Если угодно, можно сразу определить дефолтное значение элемента.

-record(имя_записи, {имя_элемента1 [= Значение1],
    ...
    имя_элементаN [= ЗначениеN]}).

Как имя записи, так и имена элементов должны быть атомами (называться со строчной буквы).

Предположим, нам надо создать базу данных наших студентов. Для каждого студента будет создаваться запись. Дадим ей имя — stud. Нас интересует: возраст студентов, их имена и любимый язык программирования. Элемент с возрастом назовём age. Поскольку многим студентам по 18 лет, зададим это дефолтное значение. Имена у всех разные, поэтому бессмысленно тут задавать значение по умолчанию. А раз Эрланг — лучший в мире язык, зададим его в качестве дефолтного значения для элемента lang.

-module(opit).
-export([main/0]).
-record(stud, {age=18, name, lang="Erlang"}).

main() ->
    Student1 = #stud{},
    io:format("~p~n", [Student1]),
    Student2 = #stud{name="Vasya"},
    io:format("~p~n", [Student2]),
    Student3 = #stud{age=64, name="Fedor Ivanovich", lang="PL/I"},
    io:format("~p~n", [Student3]).

Для первого студента мы задействуем только дефолтные значения, поэтому создаём ему запись так: #stud{}. Фигурные скобки нам напоминают, что мы, на самом деле, создаём кортеж. Если нам не хватает дефолтных значений, внутри фигурных скобок через запятую перечисляем элементы: имя_элемента=Значение.

Скомпилируем, запустим наш модуль и получим результат:

{stud,18,undefined,"Erlang"}
{stud,18,"Vasya","Erlang"}
{stud,64,"Fedor Ivanovich","PL/I"}

Ко всем трём переменным привязаны именно кортежи. Дальше с ними можно делать всё то же, что мы любим делать с кортежами. К атому stud можно относиться просто как к метке (тегу), что в этом кортеже содержатся сведения по студенту.

В первом случае мы не стали определять элемент name, который не имеет дефолтного значения. Как мы видим, этот элемент получил атом undefined. Наличие такого атома может говорить о том, что мы чего-то не учли. Возможно, в нашей программе надо отлавливать все undefined.

При создании записи можно пользоваться только теми полями, которые были перечислены в определении записи. Следующая строка в нашей программе вызовет ошибку:

Student4 = #stud{rost=180},

Создавая (или анализируя) запись, совсем не обязательно соблюдать порядок полей. Поля именованы, значит, их порядок не важен.

В оболочке можно сначала определить запись, а потом её “разопределить” с помощью команды rf(имя_записи).

Работа препроцессора epp не затрагивает записи.

Создание одной записи с помощью другой

Есть возможность на основании одной переменной, к которой привязан кортеж-запись, сделать другую, слегка (а может, и не слегка) подправив значения элементов. Для этого перед знаком решётки надо указать переменную, которую мы используем, а в фигурных скобках — те элементы, которые меняем.

Предположим, у одного нашего студента есть брат-близнец. Они отличаются только именами. Добавляем ещё одного студента:

Student5 = Student3#stud{name="Roman Ivanovich"},

Все элементы, кроме name, должны быть скопированы у Student3. Проверим:

{stud,64,"Fedor Ivanovich","PL/I"}
{stud,64,"Roman Ivanovich","PL/I"}

Да. Как мы видим, поменялся только элемент name. Элементы age и lang были взяты у брата-близнеца, а не дефолтные.

Обращение к полю записи

Если к некоторой переменной привязана запись, к отдельному элементу её можно легко получить доступ. Для этого есть такая форма: Имя_переменной#имя_записи.имя_поля.

Создадим определение записи для учёта наших пользователей, включающую три поля. Создадим запись для конкретного пользователя. Попробуем прочитать значение элемента кортежа по его имени. Поупражняться можно прямо в оболочке.

1> -record(user, {id, name, email}). 
ok
2> User1 = #user{id=1,name="Alex",email="alex@karmanov.su"}.
#user{id = 1,name = "Alex",email = "alex@karmanov.su"}
3> User1#user.id. 
1

Анализ записей с помощью паттернов

Запись это и запись, и кортеж. Поэтому можно анализировать запись с помощью паттернов можно двумя способами. Не забываем при этом, что у записки как у записи N полей, а у записи как у кортежа — N+1 элемент. Первым элементом будет атом названия записи.

Проанализируем сначала запись как кортеж.

2> User1 = #user{id=1,name="Alex",email="alex@karmanov.su"}.
#user{id = 1,name = "Alex",email = "alex@karmanov.su"}
3> {A,B,C,D} = User1. 
#user{id = 1,name = "Alex",email = "alex@karmanov.su"}
4> A. 
user
5> B.
1
6> C.
"Alex"
7> D.
"alex@karmanov.su"

Оболочка видит кортеж, состоящий из четырёх элементов, первый из которых — атом user. Она помнит, что до этого мы определяли запись с именем user. Поэтому она и даёт обратную связь в таком виде: #user{id = 1,name = "Alex",email = "alex@karmanov.su"}. Если же количество элементов будет другим, оболочка признает это просто кортежем:

8> {user, a, b, c}. 
#user{id = a,name = b,email = c}
9> {user, a, b, c, d}.
{user,a,b,c,d}

Чтобы проанализировать запись именно как запись, потребуется иной паттерн: #имя_записи{поле1=X, поле2=Y...}. Вместо икса и игрека будут подставлены значения полей.

2> User1 = #user{id=1,name="Alex",email="alex@karmanov.su"}.
#user{id = 1,name = "Alex",email = "alex@karmanov.su"}
3> #user{id=X, email=Y} = User1. 
#user{id = 1,name = "Alex",email = "alex@karmanov.su"}
4> X. 
1
5> Y. 
"alex@karmanov.su"

Если мы анализируем именно кортежи, то в паттерне должно быть отражено количество элементов кортежа. Например, так: {_, X, _, Y}. Паттерн записи позволяет указать только те поля, которые нам интересны. На были интересны поля id и email, мы их и использовали в паттерне, а про name умолчали.

Нам не нужно помнить, в каком порядке поля указаны в определении записи. В паттерне их можно переставлять как угодно. На результат это не влияет.

Как извлечь имя записи?

Допустим, наша функция может принимать записи с разными определениями. Скажем, записи user (пользователи) и записи stud (студенты). Мы бы хотели узнать, какого рода запись мы получили.

Это легко реализовать, если не забывать, что запись это тоже кортеж. Просто надо извлечь его первый элемент. Это можно сделать с помощью паттерна.

6> {Tip,_,_,_} = User1.
#user{id = 1,name = "Alex",email = "alex@karmanov.su"}
7> Tip. 
user

Но тут, однако, есть проблема. У наших записей могут быть разные количества полей. Поэтому более универсальным является извлечь первый элемент с помощью бифа element/2. Первый аргумент у него — номер элемента кортежа (1, 2…), второй — сам кортеж.

8> element(1, User1).
user

Вложенные записи

Одну запись можно использовать внутри другой записи. При создании конкретной записи просто присваиваем какому-то полю терм записи: имя_поля = #имя_вложенной_записи{поле1 = Значение1, поле2 = Значение2...}. Если мы желаем задать дефолтное значение для этой вложенной записи, аналогично добавляем в определение более сложной записи. Пример:

-module(opit).
-export([main/0]).
-record(stud, {age=18, group=101, lang="Erlang"}).
-record(user, {id, name, status=#stud{}}). 

main() ->
    User1 = #user{},
    io:format("~p~n", [User1]),
    User2 = #user{id=2, name='Alex', status=#stud{age=21}},
    io:format("~p~n", [User2]),
    User3 = #user{id=3, name='Roma', status=good},
    io:format("~p~n", [User3]).

Сначала мы определяем запись для студентов, в следующей строке она будет использоваться. Затем мы определяем более сложную запись user. Для поля status нам сразу захотелось задать дефолтное значение, поэтому мы и написали не status, а status=#stud{}. Дефолтным здесь будет то, что дефолтно в определении stud (age=18, group=101, lang=“Erlang”). Программа нам выдаст следующее:

{user,undefined,undefined,{stud,18,101,"Erlang"}}
{user,2,'Alex',{stud,21,101,"Erlang"}}
{user,3,'Roma',good}

Для Ромы нам захотелось поставить статус good, и мы это сделали. В качестве статуса, конечно, могли бы использовать какой-либо иной терм.

Конечно, вложенные записи можно реализовать на любом количестве уровней. Если надо, мы бы могли и в записи stud использовать другую запись. В той записи — ещё какую-нибудь. И так далее.

Импорт определений записей

Одни и те же определения записей мы можем использовать в разных модулях. Если это так, то имеет смысл вынести все такие определения в один файл — с расширением .hrl. Оттуда их легко импортировать в текущий модуль.

Создадим файл records.hrl, который содержит (пока что) лишь одну строку — с одним определением:

-record(stud, {age=18, name, lang="Erlang"}).

То есть мы взяли эту строку из нашего модуля и без изменений перенесли. А в том месте, где эта строка была, поставим инклюд:

-include("records.hrl").

Препроцессор потом заменит это место на содержимое файла records.hrl. Вот и всё. Наша программа так же компилируется и выдаёт прежний результат.


© Алексей Карманов, 2024.