Кортежи

Кортеж — это упорядоченная коллекция элементов фиксированного размера. Размер кортежа и типы его элементов известны на стадии компиляции. Элементами кортежа могут быть другие типы, включая сами кортежи, и кортежи могут быть вложены в другие типы:

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

Кортежи как короткие числовые векторы

В Фикус отсутствуют отдельные типы для коротких числовых векторов, точек, комплексных чисел, кватернионов, пикселей RGB и прочего. Вместо этого предлагается использовать кортежи. Например, кортеж типа (uint8, uint8, uint8) занимает 3 байта и столь же эффективен, как встроенный тип пикселя RGB, будь он доступен.

Для упрощения работы с такими типами в стандартной библиотеке Фикус определены базовые операции над подобными кортежами, в частности:

  • арифметические операции:
    • поэлементные: .+, .-, .*, ./
    • * над 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

Это не нарушает правила “неизменяемости кортежей”, а скорее служит оптимизацией с добавлением удобной синтаксической оболочки.

Эта оптимизация и удобный синтаксис применимы ко всем ситуациям, когда кортеж хранится в изменяемом расположении:

  • в переменной
  • в элементе массива
  • на кортеж указывает ref-ссылка (см. раздел ref-ссылки)