Обобщенные модули
Практически все современные языки программирования содержат конструкции для определения обобщенных (generic) типов и функций. Одна из важнейших областей применения обобщенных конструкций - библиотечная реализация полиморфных контейнеров: стек, очередь, деревья, словари (map).
Как правило, семантика обобщенных типов и их реализации весьма нетривиальна, и тем самым не подходит для Тривиля.
Для языка Тривиль выбрано решение предельно простое во всех отношениях. Обобщение в нем сделано на уровне модулей, то есть только модуль целиком может быть обобщенным или, другими словами, параметризованным. Причем, он может быть параметризован не только типом, а также любым языковым объектом, например, константой или функцией.
Обобщенный модуль – это модуль, в котором опущено то определение (или определения), которым он параметризован. Другими словами - обобщенный модуль является недопределенным.
Рассмотрим, в качестве примера, обобщенный модуль Стек (полный текст см. стд/контейнеры/стек):
модуль стек
тип Элементы = []Элемент
тип Стек* = класс {
элементы = Элементы[]
верх: Цел64 := 0 // ... [0..верх[
}
фн (с: Стек) положить*(э: Элемент) {
если с.верх = длина(с.элементы) {
с.элементы.добавить(э)
} иначе {
с.элементы[с.верх] := э
}
с.верх++
}
/* другие методы */
Тип элемента стека – это Элемент, и этот тип не определен в самом модуле.
Для использования конкретного стека, например, стека целых чисел, необходимо определить настройку. Настройка - отдельный модуль, в котором указан путь к обобщенному модулю и заданы параметры обобщенного модуля:
настройка "стд/контейнеры/стек"
модуль стек-цел
тип Элемент = Цел64
Настроенный модуль используется обычным образом. Например, если стек-цел размещен в папке модули/стек-цел, то использование его выглядит так:
модуль пример
импорт "модули/стек-цел"
вход {
пусть с = стек-цел.Стек{}
с.положить(1)
}
Несколько настроек можно использовать одновременно, например, если модель стек настроен также на Строку:
модуль пример
импорт "стд/вывод"
импорт "модули/стек-цел"
импорт "модули/стек-строк"
вход {
пусть с1 = стек-цел.Стек{}
пусть с2 = стек-строк.Стек{}
с1.положить(777)
с2.положить("привет")
Последствия принятого решения
Сравнение подхода с другими известными подходами выходит за рамки описания языка. Замечу только, что главной причиной выбора такого подхода является тривиальность реализации и простота использования.
Прямым (и несколько непривычным) следствием подхода является то, что обобщенный модуль не может быть использован без настройки. Попытка импорта обобщенного модуля приведет к ошибкам компиляции.
Перед компиляцией компилятор соберет модуль, например, стек-цел, из двух частей:
- из обобщенного модуля, в котором параметры не определены
- и из модуля с настройкой, в котором параметры определены
Такой собранный модуль является полностью определенным, он компилируется обычным образом. Каждый настроенный модуль компилируется отдельно и для него строится отдельный код. Так код для стек-цел никак не связан с кодом для стек-строк.
Говоря в принятых терминах, Тривиль использует мономорфизацию на уровне исходного текста, тем самым получая оптимальный по скорости код за счет увеличения размера кода. Вся работа с обобщенными модулями происходит во время компиляции, во время исполнения все модули являются полностью определенными.
Замечу, что никаких синтаксических конструкций для ограничения параметров (constraints) в языке не предусмотрено, они накладываются неявно. Например, если в обобщенном модуле использовать операцию <, то настройка на арифметический тип будет работать, а настройка на тип, для которого эта операция не определена, приведет к ошибкам компиляции.
Уточненный синтаксис модуля
Уточненный синтаксис модуля с добавлением опциональной настройки (см. исходное определение):
Модуль: Исходный-файл+
Исходный-файл:
('настройка' Путь-импорта)?
Заголовок-модуля
Список-импорта
Описание-или-вход*