Кортежи и записи
Кортежи
Кортеж — это упорядоченная коллекция элементов фиксированного размера. Размер кортежа и типы его элементов известны на стадии компиляции. Элементами кортежа могут быть другие типы, включая сами кортежи, и кортежи могут быть вложены в другие типы:
type quaternion = (float, float, float, float) // или (float*4)
type color_t = (uint8*3) // сокращение для (uint8, uint8, uint8)
type label_t = (string, color_t)
type graph_t = (int, int list) list
Кортежи формируются путём помещения двух или более элементов, разделённых запятыми, в круглые скобки:
val q: quaternion = (1.f, 0.f, 0.f, 0.f)
// явно указали тип, но это не обязательно
val magenta : color_t = (255u8, 0, 255u8)
// другое указание типа кортежа
val yellow : (uint8*3) = (255u8, 255u8, 128u8)
val label = ("car", magenta)
Как уже отмечалось, кортежи могут использоваться анонимно, без явного указания типа. Однако можно задать удобные синонимы, как показано выше.
Доступ к элементам кортежа осуществляется с помощью нотации tuple_expr.целочисленный_литерал. Можно также распаковывать кортежи:
val (q_re, qi, qj, qk) = q
val (r, g, b) = magenta
val channel_idx = 1
// правильный способ извлечь элемент кортежа
fun channel(c: color, idx: int) =
if idx == 0 {c.0}
else if idx == 1 {c.1}
else if idx == 2 {c.2}
else {throw OutOfRangeError}
val label_name = label.0
val label_color = label.1
val i = i1.0
Кортежи как короткие числовые векторы
В Ficus отсутствуют отдельные типы для коротких числовых векторов, точек, комплексных чисел, кватернионов, пикселей RGB и прочего. Вместо этого предлагается использовать кортежи. Например, кортеж типа (uint8, uint8, uint8) занимает 3 байта и столь же эффективен, как встроенный тип пикселя RGB, будь он доступен.
Для упрощения работы с такими типами в стандартной библиотеке Ficus определены базовые операции над подобными кортежами, в частности:
- арифметические операции:
- поэлементные:
.+,.-,.*,./ *над 2-компонентными кортежами дает произведение комплексных чисел,/- деление комплексных чисел*над 4-компонентными кортежами дает произведение кватернионов
- поэлементные:
- операции сравнения: кортежи сравниваются лексикографически
norm(): вычисляет корень суммы квадратов элементов кортежаdot(): скалярное произведениеcross(): векторное произведение для 3-компонентных кортежей: cross()- печать и преобразование в строку:
print(),string() - другие операции, полный список см. Builtins.fx.
Вложенные кортежи
Кортежи могут содержать произвольные элементы, включая другие кортежи. Доступ к элементам вложенных кортежей осуществляется аналогичным способом:
fun transform(Rt: ((double*3)*2), pt: (double*2)) =
(Rt.0.0*pt.0 + Rt.0.1*pt.1 + Rt.0.2,
Rt.1.0*pt.0 + Rt.1.1*pt.1 + Rt.1.2)
val a = 30*M_PI/180
val Rt = ((cos(a), -sin(a), 10.), (sin(a), cos(a), 0.))
val pt1 = (1., 0.)
println(transform(Rt, pt1))
Несмотря на возможное замешательство, вызванное наличием дробных чисел после кортежей, парсер решает проблему корректно. Пример иллюстрирует, что кортежи могут представлять небольшие матрицы, а не только векторы.
Изменение кортежей
До сих пор мы рассматривали способы построения и чтения кортежей. Теперь разберёмся, как их изменять. Кортежи — неизменяемые структуры данных, поэтому непосредственно заменить их элементы нельзя. Однако, если у вас есть переменная типа кортежа (var), вы можете присвоить ей новый кортеж:
var vec = (1.f, 0.f, 0.f)
// "Изменим" второй элемент, создав новый кортеж
vec = (vec.0, vec.1 + 0.1f, vec.2)
Поскольку это не совсем эффективно, компилятор допускает изменение отдельных элементов, имитируя полную замену кортежа новым:
var vec = (1.f, 0.f, 0.f)
// Меняем только второй элемент
vec.1 += 0.1f
Это не нарушает правила “неизменяемости кортежей”, а скорее служит оптимизацией с добавлением удобной синтаксической оболочки.
Эта оптимизация и удобный синтаксис применимы ко всем ситуациям, когда кортеж хранится в изменяемом расположении:
- в переменной
- в элементе массива
- в ссылаемой величине (см. раздел Ссылки)
Записи
Запись похожа на кортеж, но её элементы (называемые полями) имеют имена и доступ к элементам осуществляется по имени. Записи не могут быть анонимными, они должны быть определены явно.
Записи формируются с помощью нотации { имя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
Заметим, что при создании записи нужно явно указывать имя типа, так как это позволяет компилятору однозначно определить, какую конкретно запись вы имеете в виду, какие поля она содержит и какая реальная последовательность полей используется.
Но при распаковке записи компилятор уже знает тип распаковываемого значения, поэтому тип записи опускается.
Изменение/обновление записи
Хотя кортежи обычно невелики и редко нуждаются в частичных изменениях, для записей это утверждение неверно, и простое обновление отдельного поля может потребовать написания большого количества кода, особенно если меняется только одно поле. Для решения этой проблемы в Ficus предусмотрено удобное средство обновления записей — оператор . {...}:
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 . {поле = значение}
Также отдельные элементы записи могут быть изменены в тех же ситуациях, что и кортежи:
- если запись хранится в переменной
- если запись находится в массиве записей
- если имеется ссылка на запись
Можно считать это своего рода оптимизацией.
Работа с записью и кортежем, хранящимися по ссылке
Если у нас есть ссылка на кортеж или запись, мы можем сначала разыменовать ссылку и затем применить оператор . для доступа или модификации структуры (включая оператор обновления записи):
val r = ref (Rect {x=1, y=1, width=10, height=10})
println((*r).width*(*r).height)
Но существует удобная альтернатива, аналогичная C/C++:
...
println(r->width*r->height)
*r = r->{x = r->x + 5, y = r->y + 5}
r->x -= 5
r->y -= 5
Конструкция rec->что-то эквивалентна (*rec).что-то во всех случаях, и то же справедливо для кортежей.
Изменяемые поля записи
Предположим, что запись представляет сложную структуру данных, такую как трекер объектов, состоящий из множества членов. Часть из них может быть постоянными параметрами алгоритма, другие члены могут меняться периодически, третьи отражают текущее состояние трекера, постоянно изменяющееся.
Такая запись может храниться в переменной (var), что позволяет обновить её. Однако, если вы захотите реализовать функцию, обновляющую трекер (например, реализацию самого алгоритма трекинга), сама функция не сможет изменить параметры записи, так как параметры функций неизменяемы. Она может лишь построить свежую обновленную структуру и вернуть её. Это хороший чисто функциональный стиль, но слегка неэффективный и громоздкий.
Альтернативой может стать хранение записи в виде ссылки, что упростило бы обновление. Тогда даже неизменяемые поля могли бы быть изменены, а код стал бы немного сложнее благодаря замене всех обращений через точку (.) на стрелочку (->).
Частичным решением может стать объявление некоторых полей записи как изменяемых с помощью спецификатора 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). Подробности см. в разделе Ссылки.