Типы

Ficus – строгий статически типизированный язык. Каждое значение обладает типом, определяемым во время компиляции. Для значений и переменных типы часто выводятся автоматическии и обычно описание типа для них может быть опущено. Иногда требуется указать типы явно, например, при объявлении функции требуется явно указать типы аргументов. Если значение не типизировано явно и компилятор не может вывести его тип, будет выдана ошибка. Это значит, что если программа скомпилировалась без ошибок, все значения получили тип.

Тип определяется следующим образом (кроме классов и интерфейсов, у них свой синтаксис):

type typename = <тело типа>

Здесь тело типа это синоним типа, запись или вариант, например:

type point = (int, int)
type DComplex = {re: double; im: double}
type contour_t = point vector

Для обобщенных типов синтаксис чуть более сложный (см. Обобщенное программирование:

// тип с одним параметром
type 't typename = <тело типа>
или
// тип с несколькими параметрами
type ('t1, ..., 'tN) typename = <тело типа>

Например:

type 't Complex = {re: 't, im: 't}
type ('key, 'data) hashtable_t =
{
    hash_func: 'key -> uint64;
    table: (uint64, 'key, 'data) [] ref;
}

Краткое замечание относительно нотации 't: идентификатор с префиксом ' в рамках объявления типа задает обозначает так называемую типовую переменную. В терминах C++ это параметр шаблона typename. Когда тип инстанцируется, вместо 't должен подставляться конкретный тип. Если одно и то же имя в описании типа появляется несколько раз, это означает, что должен использоваться один и тот же тип во всех указанных местах. Если используются разные идентификаторы,они могут соответствовать разным конкретным типа, а могут быть и одним типом. Такая нотация немного отличается от других языков, где типовые аргументы идут за обобщенным типом в угловых скобках, например vector<int> в C++. В Ficus аргумент(ы) помещается спереди: point vector, float Complex и т.д.

Вот как можно использовать ранее определенные не-обобщенные типы:

fun convexhull(c: contour_t): contour_t { ... }

// Контур представляется вектором пар целых.
val contour = vector([(0, 0), (10, 0), (1, 1), (0, 10)])

// Корректно: contour_t === point vector === (int, int) vector
// Тип cHull выводится автоматически
val cHull = convexhull(contour)
// Корректно: переопределяется значение 'contour' как (float Complex) vector.
// Предыдущий 'contour' сокрыт.
// Также нет необходимости объявлять тип
val contour =
      vector([Complex {re=0.f, im=0.f}, Complex {re=10.f, im=0.f},
              Complex {re=1.f, im=1.f}, Complex {re=0.f, im=10.f}])
// Некорректно: convexhull ожидает (int, int) vector.
val cHull_f = convexhull(contour)

fun conj(c: DComplex) = DComplex {re=c.re, im=-c.im}

// Неверно: Ожидается DComplex, предоставлено 'double Complex'.
// Обе структуры имеют одинаковый состав,
// но разные наименования.
val xc = conj(Complex {re=5., im=1.})

// Задается синоним экземпляра hashtable_t
type phonebook_t = (string, int) hashtable_t

// Задается функция, используютщая другой экземпляр hashtable_t
fun translate(word: string, dict: (string, string) hashtable_t)
{ ... }

Типы в Ficus делятся на 3 категории:

  1. Примитивные или скалярные типы: численные типы, bool, string, char, void, cptr
  2. Безымянные составные типы, конструируемые рекурсивно из скалярных типов и других составных типов, именованных или безымянных. Такие типы определяются своей структурой. Таким типам можно давать псевдонимы, но псевдонимы будут трактоваться одинаково, пока будут структурно эквивалентны. Эта категория включает в себя ref, list, array, tuple, vector, функции и экземпляры обобщенных типов (например float Complex или (string, int) hashtable_t из предыдущего примера).
  3. Именованные составные типы. Каждый из таких типов требует явного объявления с присвоением имени. Такие типы сравниваются по имени. Два типа с разными именами считаются различающимися, даже если у них совершенно одинаковое содержимое (кроме случаев создания псевдонимов, тогда типы считаются эквивалентными). Составные именованные типы – это record, variant (тип-сумма), class и interface.

Список типов

В Ficus есть встроенные типы и пользовательские типы. Встроенные типы являются предопределенными.

  • целые числа и числа с плавающей точкой разной размерности:

    Тип Знаковый? Размерность Диапазон Суффикс
    int1 Да указатель
    uint8 Нет 8 бит 0…255 u8
    int8 Да 8 бит −128…127 i8
    uint16 Нет 16 бит 0…65535 u16
    int16 Да 16 бит −32768…32767 i16
    uint32 Нет 32 бит 0…232−1 u32
    int322 Да 32 бит −231…231−1 i32
    uint64 Нет 64 бит 0…264−1 u64
    int64 Да 64 бит −263…263−1 i64
    half3 Да 16 бит 2−14…65504 h
    float Да 32 бит −3.4e−38…3.4e&plus;38 f
    double Да 64 бит −1.7E−308…1.7E&plus;308
    long4 Да

Числа обсуждаются далее в разделе Числа

  • логический тип bool содержит только два значения: true и false. Неявное преобразование между типом bool и численными типами не производится, это нужно делать явно:
val nz_x = x != 0
val x = if nz_x {1} else {0}
val y = int(nz_x) // то же самое, что и условное выражение выше
val x = (nz_x :> int) // явное приведение типа, также соответствует выражению выше
  • тип void. Не является первоклассным типом как в других функциональных языках. Используется только для указания типа возврата функции или для указания, что функция не принимает параметры.
  • Тип string представляет строки Юникода. Строковые литералы ранее обсуждались и подробнее рассмотрены в разделе Текстовые строки.
  • Тип char представляет одиночный символ Юникода.
  • Тип cptr представляет непрозрачный указательь на структуру C. Это не просто указатель, это “ящик”, содержащий указатель, счетчик ссылок и деструктор, вызываемый, когда счетчик ссылок становится равным 0. Деструктор представляет собой функцию, задаваемую пользователем. Этот тип используется для хранения и обработки различных структур С, используемых программами на Ficus, например файловые дескрипторы, мьютексы и т.д. См. Взаимодействие с C.
  • функция: 't1 -> 'rt (одиночный параметр), ('t1, 't2, ..., 'tn) -> 'rt (множественные параметры) или void -> rt (нет параметров). Если функция не возвращает значения, то rt равен void. Функции рассматриваются в разделе Функции. Вот краткий пример для демонстрации использования типа:
fun tabulate(a: double, b: double,
           n: int, f: double -> double) =
[for i <- 0:n {
    val x = (b - a) * i / (n - 1) + a
    f(x)
}]
val cosine_tab = tabulate(0., 6.28, 256, cos)
  • кортеж: ('t1, 't2, ..., 'tn). Экземпляры кортежа создаются, когда несколько значений, разделенных запятыми оборачиваются круглыми скобками. Нет необходимости дополнительно явно определять тип кортежа перед созданием его экземпляра. Индивидуальные элементы кортежа пожно получить с помощью нотации tuple_val.X, где X – целочисленные литерал или с помощь сопоставления с образцом:
val label_color = (0u8, 255u8, 0u8)
val detected_object_info=((100, 200, 50, 100),
                        label_color, "пешеход")
val bounding_box = detected_object_info.0
val (_, _, label) = detected_object_info // получаем label

Краткая нотация вида ('t * N), где N – целочисленный литерал, может использоваться для определения кортежа с N элементами одного типа, например:

fun cmul(a: (float*2), b: (float*2)) =
 (a.0 * b.0 - a.1 * b.1, a.0 * b.1 + a.1 * b.0)
  • запись: type type_params record_name = { name1: 't1; name2: 't2; ... }. Записи можно рассматривать как кортежи с именованными элементами. В отличие от кортежей, тип записи должен явно быть объявлен.
type 't point = {x: 't; y: 't}

type rect = { x: int; y: int; width: int; height: int }

type detected_object_info_t // '=' при определении записи можно опустить
{
    bounding_box: rect // newline can be used as a separator
    label_color: (uint8*3)
    object_class: string
}

val detected_object_info = detected_object_info_t {
    bounding_box=rect{x=100, y=200, width=50, height=100},
    label_color=(0u8, 255u8, 0u8),
    object_class="pedestrian" }

val bbox = detected_object_info.bounding_box
val center = (bbox.x + bbox.width/2,
            bbox.y + bbox.height/2)
  • ссылка: 't ref. Ссылка это структура, выделяемая на куче, содержащее значение определенного типа и счетчик ссылок. Можно читать и изменять сохраненное значение:
var a = "to be" // Инициализируем переменную
val b = ref a   // Создаем ссылку с тем же значением
a += " or not to be" // изменяем a, но *b остается тем же самым
println(*b)     // выводит "to be"
b = ref "hello" // ошибка: b нельзя присвоить, это значение
val c = b     // копируем ссылку, делим содержимое

fun append(sr: string ref, delta: string) = *sr += delta

append(c, " or not to be") // корректно, меняем содержимое c (и b)
println(*b)   // выводит "to be or not to be"

Программисты C/C++ могут трактоват ссылку как константный указатель (int ref в Ficus ≈ int* const в C++). Стоит также отметить, что в Ficus нет унарного оператора &, так что взять адрес существующего объекта не получится. Вместо этого, оператор ref всегда располагает новый объект на куче и присваивает ему переданное начальное значение, которое впоследствии можно менять.

  • опция: 't?, например int?, (string, string) list? и т.д. Преставляет собой тип “значение или ничего”. Строго говоря, это частный случай варианта, обсуждаемый далее. Но этот случай очень часто используется и так удобен, что он получил свой собственный синтаксис (?). Если x – это значение типа T, тогда Some(x) будет значением типа T?. А как быть с “ничего”? Для этого используется None. В большинстве выражений Ficus правильно распознает, к какому типу T? относится None, но компилятору можно подсказать, явно указав тип (None : int?):
// Если возможно, преобразовать строку в число
fun str2int(s: string): int? {
    var result = 0, sign = 1, start=0
    if s.startswith('-') {sign = -1; start=1}
    for i <- start:s.length() {
        val c = s[i]
        if '0' <= c <= '9' { result = result * 10 + (ord(c) - '0') }
        else { result = -1; break }
    }
    if result >= 0 { Some(result * sign) } else { None }
}

val x = str2int("-123") // Some(-123)
val y = str2int("abc")  // None
println(x.issome()) // печатает 'true'
println(y.isnone()) // печатает 'true'
println(x.value_or(0)) // печатает '-123'
println(str2int("1h").value_or(-1)) // печатает '-1'
// бросает исключение 'OptionError'
val z = str2int("123456789").value() + // 123456789 + ...
        str2int("not a number").value() // здесь будет исключение
  • массив: 't [] – одномерный массив, 't [,] – двухмерный массив, 't [,,] – трехмерный массив и т.д. Тип представляет собой компактный одно- или многомерный массив (сейчас поддерживается до 5 измерений). Все элементы массива должны иметь одинаковый тип. Размер массива может быть произвольным, но известным на момент создания:
// создаем небольшой одномерный массив (типа int[])
// перечислением его элементов
val small_arr = [1, 2, 3, 4, 5]
// ошибка: все элементы должны быть одного типа
val err = [1, 2, 3, 4, 5.]
// можно создавать массив массивов
// его тип будет int[][]
// (можно также указывать как (int [])[] )
val pascal_triangle =
[
    [ 1 ],
    [ 1, 1 ],
    [ 1, 2, 1 ],
    [ 1, 3, 3, 1 ],
    [ 1, 4, 6, 4, 1 ]
]
val alpha = 30*M_PI/180
// двухмерые массивы также можно создавать перечислением элементов, 
// разделяя строки точкой с запятой ';'
// у rotation_mtx тип 'float [,]'
val rotation_mtx =
    [ cos(alpha), -sin(alpha);
        sin(alpha), cos(alpha) ]
// ошибка: все строки двухмерного массива должны иметь один размер
val pascal_triangle_err =
[ 1; 1, 1; 1, 2, 1; 1, 3, 3, 1; 1, 4, 6, 4, 1 ]

// создаем двухмерный массив, все элементы которого
// инициализируем одним значением ((0,0,0) в данном случае)
// у image тип '(uint8, uint8, uint8) [,]' 
val image = array((480, 640), (0u8, 0u8, 0u8))

// доступ к элементу строка=1, столбец=2.
image[1, 2] = sat_uint8(image[1,2] + (10, 10, 10))

// инвертируем прямоугольник 30 <= строка < 200, 50 <= столбец <200 внутри массива
image[30:200,50:200] ^= (255u8, 255u8, 255u8)
  • список: 't list. Односвязный неизменяемый список. Все элементы списка должны быть одного типа. Есть 4 базовых операции над списками: list.hd() (взятие “головы”), list.tl() (взятие “хвоста”), :: (операция присоединения, Cons) и list.empty() (проверка на пустоту). Списочные литералы строятся с помощью синтаксиса [:: elem0, elem1, ..., elemN ]:
// строим список из пяти последовательных натуральных чисел
val mylist1 = [:: 1, 2, 3, 4, 5]
val hd1 = mylist1.hd() // 1
                       // mylist1.hd() то же самое, что и
                       // List.hd(mylist1)
val tl1 = mylist1.tl() // [:: 2, 3, 4, 5]
val mylist2 = 100 :: tl1 // [:: 100, 2, 3, 4, 5]

// можно строить список с помощью оператора '::'.
val mylist3 = "a" :: "b" :: "c" :: "d" :: []
val phone_book = ("Peter", 12345678) ::
                ("Ann", 131415926) :: []

// вот как могут выглядеть функции компьютерного зрения --
// на вход подается изображение (представленное
// двухмерным массивом байт), на выходе получается список
// обнаруженных объектов 
fun detect_objects(image: uint8 [,]) :
    detected_object_info_t list = { ... }

Обратите внимание, что изменять список нельзя, можно лишь декомпозировать его и собрать новый список из частей старого и новых элементов, всегда добавляемых в начало списка.

  • вектор: 't vector. Вектор – это неизменяемый одномерный массив, построенный поверх структуры данных Relaxed Radix Balanced Tree с хорошей эффективностью произвольного доступа, обхода, деления на части и конкатенации:
val small_vector = vector([1, 2, 3, 4, 5])

// создаем большой вектор vector([1, 2, 3, ..., N])
val N = 1000000
// используется построение вектора; [] внутри () можно опустить
val big_vector = vector([for i <- 0 : N {i + 1}])

// произвольный доступ -- это операция сложности ~O(1),
// так что этот цикл достаточно быстрый.
var sum = 0.
val rng = RNG(0x123u64)
for i <- 0:100000 { sum += big_vector[rng.uniform(0, N-1)] }

// создаем новый вектор, включающий в себя первые 100К
// и последние 200к элементов вектора big_vector
// это операция тоже быстрая
val removed_middle = big_vector[:100000] +
                    big_vector[800000:]

// создаем список с тем же содержимым, что и big_vector
val big_list = vector([for i <- 0:N {i+1}])

// займет вечность, поскольку доступ к n-му элементу
// занимает O(n) времени
for i <- 0:100000 { sum -= big_list.nth(rng(0, N)) }

В принципе, i-й элемент вектора может быть “изменен” более-менее эффективно с помощью вызова вида vec[:i] + vector([new_values ...]) + vec[i+1:], но если приходится часто менять элементы, стоит выбрать массив как более производительную альтернативу (10-100 раз быстрее).

  • вариант, также известный как “тип-сумма”: Tag1: 't1 | Tag2: 't2 | .... Варианты используются для представления различных структур данных от простых перечислений до сложнейших иерархических структур. Вариантам посвящен соответствующий раздел.

  • инстанцирование обобщенного типа: 't generic_type_name (если типовой параметр один) или ('t1, 't2, ..., 'tN) generic_type_name (если типовых параметров несколько). Некоторые встроенные типы вроде массивов, списков, векторов или ссылок, ведут себя как обобщенные типы и “инстанцируются” одинаково. Инстанцирование может выполняться рекурсивно, то есть некоторые из tj также могут быть элементами обобщенного типа:

// двухмерный массив ссылок на целые числа
type cell_matrix = int ref [,]

// граф, представленный списком пар (вершина,
// список связанных вершин с весами ребер)
type graph_t = (int, (int, double) list) list

import Map
// ассоциативный контейнер со строковыми ключами и целочисленными значениями
type str2int_map_t = (string, int) Map.t

In the second example it may look like the outer list is instantiated with 2 type arguments. But since the compiler always knows how many parameters each generic type has (the list has 1), it correctly interpreters (int, (int, double) list) as the single type argument. Во втором примере может показаться, что внешний списко инстанцируется с двумя типовыми аргументами. Это не так, поскольку компилятор всегда знает, сколько типовых параметров есть у обобщенного типа (у списка один) и корректно понимает, что (int, (int double) list) – это один типовой параметр.

  • классы и интерфейсы. Представляют собой классы и абстрактные интерфейсы соответственно. Подробнее смотри раздел Объектно-ориентированное программирование.

  • Исключения. Тип exn. Исключения – это классический механизм обработки ошибок, часто используемый вместо ручного управления возвратом из функции. Также, как и в других функциональных языках вроде OCaml или F#, но в отличие от некоторых “традиционных” языков вроде C++ или Python, исключения в Ficus не являются экземплярами классов (возможно, унаследованных от какого-то общего предка). Вместо этого они являются экземплярами особого типа exn. Новые исключения объявляются с помощью ключевого слова exception с указанием имени исключения и, возможно, дополнительных атрибутов исключения (информация, которой вы хотите сопроводить выбрасываемое исключение). Подробнее в разделе “Исключения”.

  1. Самый часто используемый тип. Это целый знаковый тип, совпадающий по размеру с указателем, то есть 32 бита на 32-битной машине, 64 бита на 64-битной машине и т.д. 

  2. Обратите внимание, int32 и int друг в друга неявно не преобразуются. 

  3. В настоящий момент тип в Ficus не поддерживается, зарезервировано для будущих выпусков. 

  4. Знаковый целый тип неограниченной (ограниченной потреблением памяти) точности.