Статические структуры: классы
Квалифицированные и неквалифицированные вызовы
Выше было отмечено, что ОО-вычисления основаны на вызове компонентов. Как следствие этого положения исходные тексты в действительности содержат гораздо больше вызовов, чем может показаться на первый взгляд. До сих пор рассматривались две формы вызовов:
x.f x.f (u, v, ...)
Подобные вызовы используют так называемую точечную нотацию и их называют квалифицированными (qualified), так как точно указана цель вызова, идентификатор которой расположен перед точкой.
Однако другие вызовы могут быть неквалифицированны, поскольку их цель не указана. В качестве примера предположим, что необходимо в класс POINT добавить процедуру transform, которая будет комбинацией процедур translate и scale точки. Текст такой процедуры может обращаться к процедурам translate и scale:
transform (a, b, factor: REAL) is -- Сместиться на a по горизонтали, на b по вертикали, -- затем изменить расстояние до начала координат в factor раз. do translate (a, b) scale (factor) end
Тело процедуры содержит вызовы translate и scale. В отличие от предыдущих примеров здесь не указана точная цель и не применяется точечная нотация. Такие вызовы называют неквалифицированными (unqualified).
Неквалифицированные вызовы не нарушают пункта F2 принципа вызова компонент, так как тоже имеют цель. В данном случае целью является текущий экземпляр. Когда процедура transform вызывается по отношению к определенной цели, вызовы translate и scale имеют ту же цель. Фактически приведенный выше код эквивалентен следующему
do Current.translate (a, b) Current.scale (factor)
Можно переписать любой вызов как квалифицированный, указав Current в качестве цели (строго говоря, это справедливо только для экспортированных компонент). Форма неквалифицированного вызова конечно проще и вполне понятна.
Приведенные неквалифицированные вызовы являются вызовами процедур. Аналогичные соображения можно распространить и на атрибуты, хотя наличие вызовов в этом случае возможно менее очевидно. Ранее было отмечено, что в теле процедуры translate присутствие x в выражении x + a означает поле x текущего экземпляра. Можно истолковать это иначе - как вызов компонента x и выражение в полной форме примет вид Current.x+a.
В общем случае любые инструкции или выражения вида:
f
или:
f (u, v, ...)
фактически являются неквалифицированными вызовами и могут быть переписаны в форме квалифицированных вызовов:
Current.f Current.f (u, v, ...)
хотя неквалифицированная форма является более удобной. Если подобная нотация используется как инструкция, то f представляет процедуру (без параметров в первом случае или с соответствующим числом параметров определенного типа - во втором). В выражениях f может быть функцией или атрибутом (в первом варианте записи).
Компоненты-операции
Рассмотрение выражения:
x + a
приводит к важному понятию компонента-операции (operator feature). Это понятие может восприниматься как чисто косметическое, имеющее только синтаксическую значимость, и реально не вносящее ничего нового в ОО-метод. Но именно такие синтаксические свойства способны существенно облегчить жизнь разработчика, если они существуют, и сделать ее убогой, если их нет. Компоненты-операции являются хорошим примером успешного использования ОО-парадигмы в давно известных областях.
Для реализации этой идеи нужно догадаться, что выражение x + a содержит не один вызов (компонента x ), а два. В вычислениях, не использующих объектный подход, + рассматривается как операция сложения двух значений x и a типа REAL. Как уже отмечалось, в чистой ОО-модели единственным механизмом вычислений является вызов компонентов. Следовательно, можно считать, по крайней мере теоретически, что и сложение является вызовом соответствующего компонента.
Для лучшего понимания необходимо обсудить определение типа REAL. Сформулированное ранее объектное правило ( "Статические структуры: классы" ) подразумевает, что каждый тип основан на каком-то классе. Это в равной мере относится к предопределенным классам, аналогичным REAL, и к классам, определенным разработчиком, таким как POINT. Предположим, что необходимо описать REAL как класс. Нетрудно определить набор существенных компонентов: арифметические операции (сложение, вычитание, изменение знака...), операции сравнения (меньше чем, больше чем...). Итак, первый набросок будет выглядеть так:
indexing description: "Действительные числа (не окончательная версия!)" class REAL feature plus (other: REAL): REAL is -- Сумма текущего значения и other do ... end minus (other: REAL) REAL is -- Разность между текущим значением и other do ... end negated: REAL is -- Текущее значение, взятое с противоположным знаком do ... end less_than (other: REAL): BOOLEAN is -- Текущее значение меньше чем other? do ... end ... Другие компоненты ... end
При использовании такого описания класса уже нельзя более записывать арифметическое выражение в виде: x + a. Вместо этого надо использовать следующий вызов:
x.plus (a)
По аналогии, вместо привычного -x следует теперь писать x.negated.
Можно попытаться оправдать такой отход от привычной математической нотации стремлением к последовательной реализации ОО-модели и призвать в качестве примера Lisp для обоснования возможности отхода от стандартной нотации в сообществе разработчиков ПО. Но такой аргумент нельзя считать убедительным: использование Lisp было всегда весьма ограниченным. Отход от нотации, существующей уже много столетий и знакомой всем с начальной школы, чрезвычайно опасен. Тем более что в этой нотации нет ничего неправильного.
Простой синтаксический прием позволяет сохранить последовательность подхода (требование унификации вычислительного механизма, основанного на вызове компонент) и обеспечивает совместимость с традиционной нотацией. Достаточно рассматривать выражение вида
x + a
как вызов дополнительного компонента класса REAL. Для реализации такого подхода необходимо переписать компоненту plus таким образом, чтобы для ее вызовов использовать знак операции, а не точечную нотацию. Вот описание класса, реализующее эту цель:
indexing description: "Real numbers" class REAL feature infix "+" (other: REAL): REAL is -- Сумма текущего значения и other do ... end infix "-" (other: REAL) REAL is -- Разность между текущим значением и other do ... end prefix "-": REAL is -- Текущее значение, взятое с противоположным знаком do ... end infix "<" (other: REAL): BOOLEAN is -- Текущее значение меньше чем other? do ... end ... Other features ... end
Введены два новых ключевых слова - infix и prefix. Единственное синтаксическое новшество заключается в том, что имена компонент не являются идентификаторами (такими как distance или plus ), а записываются в одной из двух форм (В следующей лекции будет показано, как определить "развернутый класс". См. "Роль развернутых типов".)
где заменяется конкретным знаком операции (+, -, *, <, <= и др.). Компонент может иметь имя в инфиксной форме только если является функцией с одним аргументом, примерами могут служить plus, minus и less_than в первоначальной версии класса REAL. Префиксная форма может использоваться только для функций без аргументов или атрибутов.
Инфиксные и префиксные компоненты, называемые далее компоненты-операции (operator features), используются аналогично именованным компонентам (identifier features). Существуют лишь два синтаксических различия. Для имен компонентов-операций при их объявлении используются формы infix " " или prefix " ", а не идентификаторы. Вызов компонентов-операций в случае инфиксных компонент имеет вид:
для префиксных:
Компоненты-операции поддерживают только квалифицированные вызовы. Неквалифицированный вызов plus (y) в подпрограмме первой версии класса REAL во второй версии должен быть записан в виде Current + y. Для именованных компонентов аналогичная нотация Current.plus (y) допустима, но обычно не используется.
Кроме указанных отличий во всем остальном компоненты-операции полностью синтаксически эквиваленты именованным компонентам, в частности могут наследоваться обычным образом. Не только базовые классы аналогичные REAL, но и любые другие, могут использовать компоненты-операции, например для функции сложения двух векторов в классе VECTOR вполне допустимо использовать инфиксную компоненту "+".
Операции, используемые в компонентах-операциях, должны подчиняться следующим правилам. Знак операции - последовательность из одного или более отображаемых символов, не содержащая пробелов и переводов строки, причем первым символом может быть только один из ниже перечисленных:
+ - a / < > = \ ^ @ # | &
Ограничения, налагаемые на первый символ, облегчают распознавание инфиксных и префиксных операций.
Кроме того, для совместимости с традиционной нотацией для булевых выражений следующие ключевые слова используются для обозначения операций:
not and or xor and then or else implies
Базовые классы ( INTEGER и другие) используют так называемые стандартные операции:
- префиксные: + - not
- инфиксные: + - a / < > <= >= = // \\ ^ and or xor and then or else implies .
Здесь // обозначает целочисленное деление, \\ - остаток при целочисленном делении, ^ - операцию возведения в степень, xor - исключающее "или". В классе BOOLEAN and then и or else являются вариантами and и or (отличия обсуждаются далее), implies обозначает импликацию: выражение a implies b эквивалентно ( not a ) or else b . |
Операции, не входящие в число "стандартных", называют свободными операциями. Приведем два примера свободных операций.
- Далее в классе ARRAY будет использован инфиксный компонент-операция "@" для функции, возвращающей указанный элемент массива. Обращение к i -ому элементу массива будет выглядеть как a @ i.
- В класс POINT вместо функции distance можно ввести компонент-операцию " |-| " и расстояние между точками p1 and p2 будет записываться в виде p1 |-| p2, а не как p1.distance(p2).
Все операции имеют фиксированный приоритет, стандартные операции имеют свой обычный приоритет, а все свободные операции обладают более высоким приоритетом.
Использование компонентов-операций позволяет использовать общепринятую нотацию для выражений и одновременно отвечает требованиям полной унификации системы типов. Реализация арифметических и булевых операций как компонентов класса INTEGER вовсе не должна быть причиной снижения производительности. Концептуально a + x является вызовом компонента, но хороший компилятор может создать в результате обработки такого вызова код не менее эффективный, чем компиляторы C, Pascal, Ada или других языков, в которых "+" это жестко зафиксированная языковая конструкция.
В большинстве случаев мы можем забыть о том, что использование операций в выражениях фактически является вызовом процедур, поскольку конечный эффект будет таким же, как и при традиционном подходе. В то же время приятно сознавать, что и в этом случае не допущено отхода от принципов ОО-подхода.