Предоставление фактических родовых параметров родовому классу для создания типа называется родовым порождением (generic derivation), а полученный в результате класс, такой как STACK [POINT], называют параметрически порожденным классом.
Родовому порождению требуется тип, родовое порождение создает новый тип:
[x]. Результат порождения STACK [POINT] является типом.
[x]. Для получения такого результата, необходим уже существующий тип, используемый в качестве фактического параметра (POINT в примере).
Фактический параметр может быть произвольным типом. Ничто не мешает выбрать тип, который сам по себе параметрически порожден. Предположим, что мы определили другой родовой класс LIST [G], тогда можно определить стек, элементы которого являются списками точек:
slp: STACK [LIST [POINT]]
или, используя STACK [POINT] как фактический родовой параметр, - стек стеков точек:
ssp: STACK [STACK [POINT]]
Нет предела глубины таких вложений, кроме естественной необходимости сохранять простоту программного текста.
Терминология
Обсуждая универсализацию, необходимо уточнить используемые термины.
[x]. Процесс порождения нового типа, такого как STACK [POINT], из типов POINT и STACK, можно было бы называть созданием экземпляра типа "generic instantiation". Но этот термин мог бы ввести в заблуждение, поскольку в названии неявно предполагается процесс периода выполнения ПО. Заметьте, родовое порождение - статический механизм, действующий на текст программы, а не на ее выполнение.
[x]. В этой книге термин "параметр" и "аргумент" используются по-разному. Первый для универсальных классов, второй - для подпрограмм. В традиционной программистской терминологии параметры и аргументы чаще всего синонимы.
Проверка типов
Используя универсализацию, можно гарантировать, что структура данных будет содержать элементы определенного типа. Допустим, класс содержит объявления:
sc: STACK [CIRCLE]; sa: STACK [ACCOUNT]; c: CIRCLE; a: ACCOUNT.
Тогда в программах этого класса допустимы следующие инструкции:
sc.put (c) -- Втолкнуть круг в стек кругов
sa.put (a) -- Втолкнуть счет в стек счетов
c := sc.item -- Сущности круг присвоить вершину стека кругов.
Но каждая из следующих инструкций недопустима и будет отвергнута:
sc.put (a); -- Попытка: Втолкнуть счет в стек кругов.
sa.put (c); -- Попытка: Втолкнуть круг в стек счетов.
c:= sa.item -- Попытка: Дать кругу значение счета.
Это исключает ошибочные операции, подобные попытке вычитания денег из круга.
Правило типизации
Правило типизации, делающее допустимым первый набор и недопустимым второй, интуитивно понятно, но его надо уточнить.
Вначале рассмотрим обычные, не родовые классы. Пусть C такой класс. Рассмотрим объявление его компонента, не использующее, естественно, никаких формальных родовых параметров:
f(a:T):U is ...
Тогда вызов вида x.f(d), появляющийся в произвольном классе B, где x типа C, будет корректен по типу, тогда и только тогда, когда:
[x]. f доступен классу B, - экспортирован всем классам или множеству классов, включающих B;
[x]. d принадлежит типу T. Если учитывать возможность наследования, то d может принадлежать потомкам T.
[x]. Результат вызова имеет тип U. В этом примере предполагается, что компонент f является функцией.
Теперь предположим, что C родовой класс с формальным родовым параметром G имеет компонент:
h (a: G): G is...
Вызов h имеет вид y.h(e), где y сущность, объявленная как
y: C [V]
Тип V - некоторый ранее определенный тип. Теперь правило типизации - двойник неродового правила - требует, чтобы e имело тип V или при наследовании было потомком V. Аналогичное требование к результату выполнения функции h.
Требования правила понятны: V - фактический параметр, заменяющий формальный родовой параметр G параметризованного класса C, поэтому он заменяет все вхождения G при вызове компонент класса. Все предыдущие примеры следовали этой модели: вызов s.put(z) требует параметра z типа POINT, если s типа STACK [POINT]; INTEGER если s типа STACK [INTEGER]; и s.item возвращает результат типа POINT в первом случае и типа INTEGER во втором.
Операции над сущностями родового типа
В родовом классе C [G, H, ...] рассмотрим сущность, чей тип - один из формальных родовых параметров, например x типа G. Когда класс используется клиентом для объявления сущностей, G, разумеется, может представлять любой тип. Поэтому любая операция, которую выполняют подпрограммы C над x, должна быть применима ко всем типам. Это ограничение позволяет выполнять только пять видов операций:
Использование сущностей формального родового типа
Корректно использовать сущность x, чей тип задан формальным родовым параметром G, можно следующим образом.
1 Слева от оператора присваивания x := y, где выражение y также имеет тип G.
2 Справа от оператора присваивания y := x, где сущность y также типа G.
3 В логических выражениях вида x = y или x /= y, где y также типа G.
4 Как фактический аргумент в вызове подпрограммы на месте формальных параметров типа G, или типа ANY.
5 Как цель вызова компонента класса ANY.
В частности, инструкция создания вида create x неприменима, так как нам ничего неизвестно о процедурах создания, если таковые есть, для класса, определенного возможным фактическим родовым параметром, соответствующим G.