В.Ш. Кауфман - Языки программирования - концепции и принципы (1990) (1160787), страница 21
Текст из файла (страница 21)
Таким образом, отсутствие средств, поддерживающих нужные абстракции (в
частности, в процессе пошаговой детализации), вполне может помешать и
наиболее творческим моментам в программировании, помешать увидеть изящное,
надежное, понятное и эффективное решение.
Надежность. Внимательнее сравним программы на Аде и Алголе 60 с точки
зрения надежности предоставляемой услуги. Чтобы воспользоваться операцией
"маневр", на Аде можно написать, например,
маневр (север, восток) ;
а на Алголе-60 -
маневр (1, 2) ;
Ясно, что первое - нагляднее, понятнее (а, значит, и надежнее). Но
высокий уровень надежности гарантируется не только наглядностью, но и
контролем при трансляции. На Аде нельзя написать
маневр (1, 2)
так как транслятор обнаружит несоответствие типов аргументов и параметров!
На Алголе-60 можно написать
маневр (25, 30) ;
и получить 4. А чтобы получить тот же уровень контроля, который
автоматически обеспечивает Ада-компилятор, нужно добавить в программу явные
проверки диапазона целых значений и обращение к соответствующим
диагностическим процедурам. И все это будет работать динамически, а в Аде -
статически. Так что надежность при программировании на Алголе-60 может быть
обеспечена усилиями только самого программиста и только за счет снижения
эффективности целевой программы.
Можно постараться добиться большей наглядности, введя переменные
"север", "восток", "юг" и "запад" (постоянных в Алголе-60 нет). Им придется
присвоить значения (1, 2, 3, 4) - также во время работы объектной программы,
но зато окажется возможным писать столь же понятно, как и на Аде:
маневр (север, восток) ;
Однако в отличие от имен-значений перечисляемого типа в Аде, которые по
определению - константы, эти переменные не защищены от случайных
присваиваний. Не говоря уже о защите от применения к ним других операций (в
Аде к значениям определенного перечисляемого типа применимы, конечно, только
операции, параметры которых соответственно специфицированы).
Итак, мы выделили технологическую потребность определять небольшие
множества имен и работать с ними на таком уровне абстракции, когда
указываются лишь связи этих имен между собой и с другими программными
объектами. Эта потребность и удовлетворяется в Аде перечисляемыми типами
данных. Важно, что удовлетворяется она комплексно, в соответствии с
важнейшими общими принципами (такими, как принцип цельности) и
специфическими требованиями к ЯП (надежность, понятность и эффективность
программ). Показано также, что перечисляемые типы не могут быть полностью
заменены аппаратом классических ЯП.
4.6.2. Дискретные типы
Перечисляемые типы - частный случай так называемых ДИСКРЕТНЫХ типов.
Дискретным называется тип, класс значений которого образует ДИСКРЕТНЫЙ
ДИАПАЗОН, т.е. конечное линейно упорядоченное множество. Это значит, что в
базовый набор операций для дискретных типов входит, во-первых, операция
сравнения "меньше", обозначаемая обычно через "<"; во-вторых, функции
"первый" и "последний", вырабатывающие в качестве результатов соответственно
минимальный и максимальный элементы диапазона и, в-третьих, функции
"предыдущий" и "последующий" с очевидным смыслом. Эти операции для всех
дискретных типов предопределены в языке Ада.
Кроме перечисляемых, дискретными в Аде являются еще и ЦЕЛЫЕ типы. Класс
значений любого целого типа считается конечным. Для предопределенного типа
INTEGER он фиксируется реализацией языка (т.е. различные компиляторы могут
обеспечивать различный диапазон предопределенных целых; этот диапазон должен
быть указан в документации на компилятор; кроме того, его границы
доставляются (АТРИБУТНЫМИ) функциями "первый" и "последний"). Для
определяемых целых типов границы диапазона значений явно указываются в
объявлении целого типа (см. объявление типа узел).
Для типа INTEGER предопределены также унарные операции "+", "-", "abs"
и бинарные "+", "-", "*", "/", "**" (возведение в степень) и др.
Любые дискретные типы можно использовать для индексации и управления
циклами. Мы уже встречались и с тем, и с другим в пакете управление_сетями.
В "морской" задаче мы не воспользовались предопределенными базовыми
операциями для типа "курс". Но в соответствии с его объявлением
север < восток < юг < запад
причем
последующий (север) = восток;
предыдущий (восток) = север ;
курс'первый = север ;
курс'последний = запад ;
Так что функцию "налево" можно было реализовать и так:
function налево (старый: курс) return курс is
begin
case старый of
when север => return запад ;
when others => return предыдущий (старый) ;
end case ;
end налево ;
Обратите внимание, функция "предыдущий" не применима к первому элементу
диапазона (как и функция "последующий" - к последнему элементу).
Вариант when others в выбирающем операторе работает тогда, когда
значение выбирающего выражения (в нашем случае - значение параметра
"старый") не соответствует никакому другому варианту. Выбирающее выражение
должно быть дискретного типа (вот еще одно применение дискретных типов,
кроме индексации и управления циклами) и каждое допустимое значение должно
соответствовать одному и только одному варианту. Такое жесткое правило было
бы очень обременительным без оборота when others. С его помощью можно
использовать выбирающий оператор и тогда, когда границы диапазона изменяются
при выполнении программы, т.е. являются динамическими. Конечно, при этом
изменяется не тип выбирающего выражение, а лишь его подтип - динамические
границы не могут выходить за рамки статических границ, определяемых типом
выражения.
Вообще, если D - некоторый дискретный тип, то справедливы следующие
соотношения. Пусть X и Y - некоторые значения типа D. Тогда
последующий (предыдущий (X)) = X, если X /= D'первый;
предыдущий (последующий (X)) = X, если X /= D'последний;
предыдущий (X) < X, если X /= D'первый.
Для дискретных типов предопределены также операции "<=", ">", ">=", "="
и "/=".
Вот еще несколько примеров дискретных типов. Предопределены дискретные
типы BOOLEAN, CHARACTER. При этом считается, что тип BOOLEAN введен
объявлением вида
type BOOLEAN is (true, false) ;
так что true < false.
Для типа CHARACTER в определении языка явно перечислены 128 значений-
символов, соответствующих стандартному коду ASCII, среди которых первые 32 -
управляющие телеграфные символы, вторые 32 - это пробел, за которым следуют
!"#$%&'()*+,-./0123456789:;<=>?, третьи 32 - это коммерческое at (@), за
которым идут прописные латинские буквы, затем [\]^_; наконец, последние 32 -
знак ударения '; затем строчные латинские буквы, затем {|}, затем тильда ~ и
символ вычеркивания.
Для типов BOOLEAN, CHARACTER и INTEGER предопределены обычные операции
для дискретных типов. (Мы привели не все такие операции). Кроме того, для
типа BOOLEAN предопределены обычные логические операции and, or, xor и not с
обычным смыслом (xor - исключительное "или").
Вот несколько примеров определяемых дискретных типов.
type день_недели is (пн, вт, ср, чт, пт, сб, вс) ;
type месяц is (январь, февраль, март, апрель, май, июнь, июль,
август, сентябрь, октябрь, ноябрь, декабрь) ;
type год is new INTEGER range 0..2099 ;
type этаж is new INTEGER range 1..100 ;
4.6.3. Ограничения и подтипы
Проанализируем еще одну технологическую потребность - потребность
ограничивать множество значений объектов по сравнению с полным классом
значений соответствующего типа. Рассмотрим фрагмент программы, меняющей знак
каждого из десяти элементов вектора A:
for j in 1..10 loop
A(j) := -A(j) ;
end loop ;
Перед нами фрагмент, который будет правильно работать только при
условии, что вектор A состоит ровно из десяти элементов. Иначе либо
некоторые элементы останутся со старыми знаками, либо индекс выйдет за
границу массива. К тому же такой фрагмент способен работать только с
вектором A и не применим к вектору B. Другими словами, это очень конкретный
фрагмент, приспособленный для работы только в специфическом контексте, плохо
защищенный от некорректного использования.
Вопрос. В чем это проявляется?
Допустим, что потребность менять знак у элементов вектора возникает
достаточно часто. Вместо того, чтобы каждый раз писать аналогичные
фрагменты, хотелось бы воспользоваться принципом обозначения повторяющегося
и ввести подходящую подпрограмму, надежную и пригодную для работы с любыми
векторами. Другими словами, мы хотим обозначить нечто общее, характерное для
многих конкретных действий, т.е. ввести операционную абстракцию.
От чего хотелось бы отвлечься? По-видимому, и от конкретного имени
вектора, и от конкретной его длины. Возможно, от конкретного порядка
обработки компонент или от конкретной размерности массива. Можно ли это
сделать и целесообразно ли, зависит от многих причин. Но прежде всего - от
возможностей применяемого ЯП. Точнее, от возможностей встроенного в него
аппарата развития (аппарата абстракции-конкретизации).
Здесь уместно сформулировать весьма общий принцип проектирования (в
частности, языкотворчества и программирования). Будем называть его принципом
реальности абстракций.
4.7.3.1. Принцип реальности абстракций
Назовем реальной такую абстракцию, которая пригодна для конкретизации в
используемой программной среде. Тогда принцип реальности абстракций можно
сформулировать так:
в программировании непосредственно применимы лишь реальные абстракции.
Иначе говоря, создавая абстракцию, не забудь о конкретизации. Следует
создавать возможность абстрагироваться только от таких характеристик,
которые применяемый (создаваемый) аппарат конкретизации позволяет указывать
в качестве параметров настройки.
В нашем примере попытаемся построить ряд все более мощных абстракций,
следуя за особенностями средств развития в Аде. Скажем сразу - Ада позволяет
явно построить первую из намеченных четырех абстракций (мы ведь собрались
отвлечься от имени, от длины, от порядка и от размерности); со второй
придется потрудиться (здесь-то и понадобятся ограничения и подтипы); третья
потребует задачного типа и может быть построена лишь частично, а четвертая
вообще не по силам базисному аппарату развития Ады (т.е. для Ады - это
нереальная абстракция).
Абстракция от имени. Достаточно ввести функцию с параметром и
результатом нужного типа.
Что значит "нужного типа"? Пока мы абстрагируемся только от имени
вектора, сохраняя все остальные его конкретные характеристики. Поэтому нужен
тип, класс значений которого - десятиэлементные векторы. Объявим его:
type вектор is array (1..10) of INTEGER ;
Теперь нетрудно объявить нужную функцию.
function "-" (X : вектор) return вектор is
Z: вектор ;
begin
for j in (1..10) loop
Z(j) := - X(j) ;
end loop ;
return Z ;
end "-" ;
Обратите внимание: такая функция перекрывает предопределенную операцию
"-". Становится вполне допустимым оператор присваивания вида
А := -А ;
где А - объект типа "вектор", а знак "-" в данном случае идентифицирует не
предопределенную операцию над числами, а определенную нами операцию над
векторами.
Замечание (о запрете на новые знаки операций). В Аде новые знаки
операций вводить нельзя. Это сделано для того, чтобы синтаксический анализ
текста программы не зависел от ее смысла (в частности, от результатов
контекстного анализа). Скажем, знак "[" нельзя применять для обозначения
новой операции, а знак "-" - можно.
Именно для того, чтобы продемонстрировать перекрытие знака "-", мы
ввели функцию, а не процедуру, хотя в данном случае последнее было бы
эффективнее. Действительно, ведь наш исходный фрагмент программы создает
массив-результат непосредственно в массиве-аргументе. А функция "-" создает
новый массив, сохраняя аргумент неизменным. Так что более точной и