Обобщенные модули

Практически все современные языки программирования содержат конструкции для определения обобщенных (generic) типов и функций. Одна из важнейших областей применения обобщенных конструкций - библиотечная реализация полиморфных контейнеров: стек, очередь, деревья, словари (map).

Как правило, семантика обобщенных типов и их реализации весьма нетривиальна, и тем самым не подходит для Тривиля.

Для языка Тривиль выбрано решение предельно простое во всех отношениях. Обобщение в нем сделано на уровне модулей, то есть только модуль целиком может быть обобщенным или, другими словами, параметризованным. Причем, он может быть параметризован не только типом, а также любым языковым объектом, например, константой или функцией.

Обобщенный модуль – это модуль, в котором опущено то определение (или определения), которым он параметризован. Другими словами - обобщенный модуль является недопределенным.

Рассмотрим, в качестве примера, обобщенный модуль Стек (полный текст см. стд/контейнеры/стек):

модуль стек

тип Элементы = []Элемент

тип Стек* = класс {
    элементы = Элементы[] 
    верх: Цел64 := 0 // ... [0..верх[
}

фн (с: Стек) положить*(э: Элемент) {
    если с.верх = длина(с.элементы) {
        с.элементы.добавить(э)
    } иначе {
        с.элементы[с.верх] := э
    }
    с.верх++
}
/* другие методы */

Тип элемента стека – это Элемент, и этот тип не определен в самом модуле.

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

настройка "стд/контейнеры/стек"
модуль стек-цел

тип Элемент = Цел64

Настроенный модуль используется обычным образом. Например, если стек-цел размещен в папке модули/стек-цел, то использование его выглядит так:

модуль пример

импорт "модули/стек-цел"

вход { 
    пусть с = стек-цел.Стек{}
    с.положить(1)
}    

Несколько настроек можно использовать одновременно, например, если модель стек настроен также на Строку:

модуль пример

импорт "стд/вывод"
импорт "модули/стек-цел"
импорт "модули/стек-строк"

вход { 
    пусть с1 = стек-цел.Стек{}
    пусть с2 = стек-строк.Стек{}

    с1.положить(777)
    с2.положить("привет")

Последствия принятого решения

Сравнение подхода с другими известными подходами выходит за рамки описания языка. Замечу только, что главной причиной выбора такого подхода является тривиальность реализации и простота использования.

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

Перед компиляцией компилятор соберет модуль, например, стек-цел, из двух частей:

  • из обобщенного модуля, в котором параметры не определены
  • и из модуля с настройкой, в котором параметры определены

Такой собранный модуль является полностью определенным, он компилируется обычным образом. Каждый настроенный модуль компилируется отдельно и для него строится отдельный код. Так код для стек-цел никак не связан с кодом для стек-строк.

Говоря в принятых терминах, Тривиль использует мономорфизацию на уровне исходного текста, тем самым получая оптимальный по скорости код за счет увеличения размера кода. Вся работа с обобщенными модулями происходит во время компиляции, во время исполнения все модули являются полностью определенными.

Замечу, что никаких синтаксических конструкций для ограничения параметров (constraints) в языке не предусмотрено, они накладываются неявно. Например, если в обобщенном модуле использовать операцию <, то настройка на арифметический тип будет работать, а настройка на тип, для которого эта операция не определена, приведет к ошибкам компиляции.

Уточненный синтаксис модуля

Уточненный синтаксис модуля с добавлением опциональной настройки (см. исходное определение):

Модуль: Исходный-файл+
Исходный-файл:
    ('настройка' Путь-импорта)?
    Заголовок-модуля
    Список-импорта
    Описание-или-вход*