Menu

Zero Generics: параметры типа для функций и shapes

Как работают дженерики в Zero: объявление параметров типа на функциях и shapes, вызов дженерик-функций и паттерн псевдонимов типов, который превращает длинные параметризации в чистые имена.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Зачем дженерики

Быстро встречаются типы, которые должны работать более чем с одним типом элемента. «Пара» из двух значений не возражает, целые там, строки или какой-то ваш shape. Дженерики позволяют написать тип один раз и инстанцировать его теми типами элементов, которые нужны вызывающему.

Альтернатива — писать IntPair, StringPair, BytePair и так далее — быстро становится скучной и плохо композируется. Дженерики в Zero — стандартный инструмент для этой задачи.

Дженерик-функции

Объявите параметры типа в угловых скобках между именем функции и списком параметров:

fun makePair<T, U>(left: T, right: U) -> Pair<T, U> {
    return Pair { left: left, right: right }
}

T и U — это placeholder-типы; вызывающий решает, чем они станут.

Вызовы обычно не требуют их прописывания — компилятор выводит из типов аргументов:

let pair = makePair(40, 2_u8)

Здесь T выводится как i32 (по умолчанию для целочисленного литерала без суффикса), а U — как u8 (из суффикса _u8). Результирующая привязка pair имеет тип Pair<i32, u8>.

Если вывод выбрал бы неправильные типы — например, потому что литералы неоднозначны — параметры можно закрепить прямо в месте вызова:

let pair = makePair<u8, u8>(1, 2)

(Точный синтаксис вызова с угловыми скобками может меняться между версиями Zero — посмотрите актуальную документацию. Поведение «сначала вывод» — это стабильная часть.)

Дженерик-shapes

Shapes принимают параметры типа точно так же:

shape Pair<T, U> {
    left: T,
    right: U,
}

В типе каждого поля можно упоминать параметры. Экземпляры их фиксируют:

let intBytes: Pair<i32, u8> = Pair { left: 40, right: 2_u8 }
let words:    Pair<String, String> = Pair { left: "hi", right: "there" }

Разобранный пример с дженерик-shape и дженерик-функцией — нажмите Run и увидите вывод типов в действии:

Одно объявление дженерик-shape, одна дженерик-функция, один строго типизированный сайт вызова. Никакого boilerplate в духе IntBytePair.

Type aliases

Когда одна и та же параметризация всплывает снова и снова, дайте ей имя через type:

type BytePair = Pair<u8, u8>

Теперь BytePair взаимозаменяем с Pair<u8, u8> везде, где можно написать тип:

Псевдоним — это чисто именование, новый тип он не создаёт. Функция, принимающая BytePair, спокойно примет значение типа Pair<u8, u8> (и наоборот).

Дженерики в стандартной библиотеке

Тот же механизм питает большую часть стандартной библиотеки. Несколько из тех, что встретятся в реальном Zero-коде:

  • Maybe<T> — опциональное значение, содержит либо T, либо ничего.
  • Span<T> — заимствованный срез по T. Span<u8> — каноническое представление байтового буфера.
  • ref<T> и mutref<T> — явные типы ссылок для случаев, когда нужно делиться данными без копирования.

Учить их все сразу не надо. Смысл дженериков в том, что один и тот же shape работает с любым типом элемента, который у вас под рукой.

Когда дженерики окупаются (и когда нет)

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

  • Логика функции имеет смысл только для конкретного типа (парсер String, скажем).
  • Хочется, чтобы тип появлялся в сообщениях об ошибках, чтобы упростить отладку.
  • Характеристики производительности зависят от конкретного размера в памяти.

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

Заметка про ограничения

В некоторых системах дженериков параметр типа можно ограничить («T должен поддерживать ==», «T должен реализовывать Iterator»). В Zero pre-1.0 история с ограничениями ещё эволюционирует — примеры в официальном репозитории используют дженерики в обычной форме, без хитрых границ. По мере стабилизации языка ожидайте, что синтаксис ограничений появится в маленькой регулярной форме, согласованной с остальным языком. Пока пишите дженерики, которые работают с любым T, который вы реально в них передаёте, и доверяйте компилятору сообщать, когда какая-то операция не поддерживается.

Дальше: Enums

Дженерики позволяют параметризоваться по типам. Следующий строительный блок — на другом конце спектра: enums, простой тип перечисления Zero для случаев, когда варианты не несут дополнительных данных.

Часто задаваемые вопросы

Как работают дженерики в Zero?

Объявите параметры типа в угловых скобках после имени функции или shape: fun makePair<T, U>(left: T, right: U) -> Pair<T, U> или shape Pair<T, U> { left: T, right: U }. Вызывающие либо явно закрепляют параметры (Pair<i32, u8>), либо позволяют компилятору вывести их из аргументов вызова.

Могут ли shapes быть дженериками в Zero?

Да. Shape может принимать параметры типа в том же синтаксисе с угловыми скобками, что и функции: shape Pair<T, U> { left: T, right: U }. Каждое поле может использовать параметры в своём типе. Экземпляры формируются написанием параметризованного типа — например, Pair<i32, u8>.

Нужно ли указывать параметры типа при вызове дженерик-функции?

Обычно нет. Компилятор выводит их из типов аргументов. Вызова makePair(40, 2_u8) достаточно — T становится i32, а Uu8. Закрепить параметры явно стоит, когда вывод выбрал бы не тот тип или когда хочется задокументировать их в месте вызова.

Что такое type alias в Zero?

Type alias — это короткое имя для более длинного выражения типа. type BytePair = Pair<u8, u8> позволяет писать BytePair везде, где иначе пришлось бы писать Pair<u8, u8>. Псевдоним — чисто именовательная штука: он не вводит новый тип, а только короткий способ сослаться на существующий.

Где дженерики встречаются в стандартной библиотеке Zero?

Везде — везде, где типу нужно держать или работать с произвольным типом элемента. Maybe<T> для опциональных значений, Span<u8> для байтовых срезов, контейнеры, параметризованные типом элемента. Один и тот же механизм дженериков обрабатывает и пользовательские типы, и типы стандартной библиотеки.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ