Записи
Записи похожи на кортежи, но их элементы (называемые полями) именованы и доступ к элементам осуществляется по имени.
Записи не могут быть анонимными, они должны быть определены явно.
Записи формируются с помощью нотации { имя1=значение1, ..., имяN=значениеN }, а доступ к полям осуществляется через конструкцию expr.имя_поля. Записи также можно распаковывать:
type rect_t = {x: int; y: int; width: int; height: int}
val r = rect { x=10, y=5, width=30, height=60 }
val r_area = r.width*r.height
type object_t // '=' можно пропустить перед '{'
{
box: rect_t // разделяйте поля символом ';' или переходом на новую строку
velocity: (int, int)
id: int=-1 // некоторым полям записей можно задать значения по умолчанию
label=(string, (uint8*3))
tracked: bool=true
}
// Порядок полей может быть произвольным при создании записи
val obj = object_t {
box=r, id=5, velocity=(0, 0),
label=("", (255u8, 255u8, 255u8))
// используем значение по умолчанию для поля "tracked"
}
// Игнорируем метку и флаг отслеживания,
// считывая только рамку и скорость
// Сокращение "id" вместо "id=id"
val { box=r, id, velocity=(vx, vy) } = obj
Заметим, что при создании записи нужно явно указывать имя типа, так как это позволяет компилятору однозначно определить, какую конкретно запись вы имеете в виду, какие поля она содержит и какая реальная последовательность полей используется.
Но при распаковке записи компилятор уже знает тип распаковываемого значения, поэтому тип записи опускается.
Изменение/обновление записи
Хотя кортежи обычно невелики и редко нуждаются в частичных изменениях, для записей это утверждение неверно, и простое обновление отдельного поля может потребовать написания большого количества кода, особенно если меняется только одно поле. Для решения этой проблемы в Фикус предусмотрено удобное средство обновления записей – оператор .{...}:
type Rect = {x: int; y: int; width: int; height: int}
val r0 = Rect {x=0, y=0, width=100, height=50}
val r1 = r0.{x = r0.x + 10} // r1 совпадает с r0, за исключением изменённой координаты x
type object_t = {
id: int=-1;
box: rect_t;
velocity: (int, int)
}
val obj = object_t {box=r1, velocity=(10, 5)}
// здесь обновление записи распространяется на вложенные записи
var moved_obj = obj.{
box = obj.box.{
x = obj.box.x + obj.velocity.0,
y = obj.box.y + obj.velocity.1
}
}
// запись rec .= {обновляемые_элементы} — это сокращение для
// rec = rec . {обновляемые_элементы}
moved_obj .= {velocity=moved_obj.velocity/2}
Оператор .= – это сокращенная форма записи для выражения rec = rec.{поле = значение}
Также отдельные элементы записи могут быть изменены в тех же ситуациях, что и кортежи:
- если запись хранится в переменной
- если запись находится в массиве записей
- если имеется ссылка на запись
Можно считать это своего рода оптимизацией.
Изменяемые поля записи
Для примера предположим, что с помощью записи реализован трекер объектов,
состоящий из множества элементов. Часть полей могут не меняться после инициализации, другие – меняться периодически, остальные вслед за состоянием трекера меняются постоянно.
Такая запись может храниться в переменной (var), что позволяет менять её. Однако, если вы захотите реализовать функцию обновления трекер, такая функция не может менять свои параметры, так как они неизменяемы. Она может лишь построить свежую копию с обновлениями структуры и возвратить её. Это замечательный чисто функциональный стиль, но это немного неэффективно и многословно.
Можно создать ссылку на запись и использовать ссылку для обновления записи. Но тогда можно менять и поля, не предназначенные для изменения Кроме того, вместо доступа через точку . нужно пользоваться более длинной нотацией ->. И так далее…
Частично эта задача может быть решена полями-ссылками, но это требует дополнительных затрат на типизацию
В приведенном примере предварять поля записи ключевым словом (оно здесь выступает модификатором), что делает поле изменяемым:
type Tracker =
{
// некоторые параметры алгоритма
eps: double
search_radius: int = 20
// текущее состояние
var objs: object_t []
}
fun track(tracker: Tracker, detector: Detector, image: uint8 [,])
{
...
for obj@i <- tracker.objs {
// обновляем местоположение i-го объекта
...
}
if size(newly_tracked_obj) != 0 {
// конкатенируем два массива;
// подробности см. в разделе 'Массивы'
tracker.objs = [\tracker.obj, \newly_detected_objs]
}
}
Важно отметить, что если в записи есть хотя бы одно изменяемое поле, запись всегда размещается в куче, что приводит к дополнительным издержкам при использовании изменяемых полей. Но эти затраты ниже в сравнении со ссылками на поля.
Можно трактовать запись с изменяемыми полями как ссылку на запись, где поля без модификатора var защищены от изменения. Можно также представить ссылку как запись с единственным изменяемым полем (так сделано в OCaml).
Подробнее об этом см в разделе ссылки.