Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Языки программирования. Концепции и принципы

.pdf
Скачиваний:
2
Добавлен:
08.09.2026
Размер:
2 Мб
Скачать
☆
Данные и типы
111
чего состоит «север» или «восток»? Важно лишь, что это разные сущности, свя занные между собой только тем, что направо от севера – восток, а налево от восто ка – север.
Таким образом, значения типа «курс», так же как и типа «команда», следует
считать просто именами компонент модели задачи (точнее, той модели внешнего мира, на которой мы решаем нашу содержательную задачу). Существенные связи этих имен – непосредственное отражение содержательных связей между именуе мыми компонентами внешней модели. Мы пришли еще к одной причине, по кото рой нам нужны в программе все такие именазначения, – иначе не запрограмми ровать базовые функции (ведь нет никаких внутренних зависимостей между значениями, только внешние, а ихто и нужно отражать).
Когда мы программировали базовую функцию «связать» в пакете управле
ние_сетью, нам, наоборот, были абсолютно безразличны индивидуальные имена узлов – можно было программировать, опираясь на внутреннее строение именуе мых объектов (на строение записей_об_узле в массиве «сеть»). Это внутреннее строение создавалось пользователем, работающим с пакетом, посредством других базовых операций.
Когда же мы будем программировать, например, операцию «налево», никакое
внутреннее строение не подскажет нам, что налево от юга находится восток. Это следует только из модели внешнего мира, создаваемой нами самими, то есть в дан ном случае создателем пакета, а не пользователем. Поэтому мы обязаны явно со поставить восток – югу, север – востоку, юг – западу.
С ситуацией, когда строение значений скрыто от пользователя, но отнюдь не без
различно для реализатора, мы уже встречались, когда изучали приватные типы
данных. Теперь строение не скрыто, но существенно лишь постольку, поскольку
обеспечивает идентификацию значений. В остальном можно считать, что его про
сто нет, – перед нами список имен объектов внешнего мира, играющих определен
ные роли в решаемой задаче.
Перечисляемые типы похожи на приватные тем, что также создают для пользова
теля определенный уровень абстракции – пользователь вынужден работать только
посредством операций, явно введенных для этих типов. Однако если операции
приватных типов обычно обеспечивают доступ к скрытым компонентам содержа
тельных «приватных» объектов, то операции перечисляемых типов отражают свя
зи между теми содержательными объектами, которые названы явно перечисленны
ми в объявлении типа именами. Вместе с тем приватный тип вполне может
оказаться реализованным некоторым перечисляемым типом.
Завершая шаг детализации, определим перечисляемый тип «курс» вместе
с базовыми операциямиповоротами. Сделаем это с помощью спецификации па кета «движение»:
package движение is
type курс is(север, восток, юг, запад); function налево(старый : курс) return курс; function направо(старый : курс) return курс; function назад(старый : курс) return курс;
end движение;
112
Современное состояние языков программирования
Шаг 4. Тело функции «маневр».
Идея в том, чтобы понять, каким поворотом можно добиться движения в нуж ном направлении, и выдать соответствующую команду:
function маневр(старый, новый : курс) return команда; begin
if новый = старый then return прямо;
elsif новый = налево(старый) then return налево;
elsif новый = направо(старый) then return направо;
else return назад;
end if; end маневр;
Мы свободно пользовались сравнением имензначений на равенство.
Условный оператор с ключевым словом elsif можно считать сокращением обычного условного оператора. Например, оператор
if B1 then S1;
elsif Â2 then S2;
elsif B3 then S3; end if;
эквивалентен оператору
if B1 then S1 else
if B2 then S2
else
if B3 then S3 end if;
end if; end if;
Такое сокращение удобно, когда нужно проверять несколько условий последо вательно.
Программирование функции «маневр» завершено.
Если считать, что «маневр» – лишь одна из предоставляемых пользователю услуг, можно включить ее в пакет со следующей спецификацией:
package услуги is
type команда is(прямо, налево, направо, назад);
package движение is
type курс is(север, восток, юг, запад); function налево(старый : курс) return курс; function направо(старый : курс) return курс; function назад(старый : курс) return курс;
end движение;
use движение;
function маневр(старый, новый : курс) return команда; end услуги;
Замечания о конструктах. Вопервых, видно, что пакет («движение») может быть объявлен в другом пакете. Чтобы воспользоваться объявленными во внутреннем
Данные и типы
пакете именами при объявлении функции «маневр», указание контекста (with) не
нужно. Но указание сокращений (use) обязательно, если желательно пользоваться
сокращенными именами. Иначе пришлось бы писать
function маневр(старый,новый : движение.курс) return команда;
Вовторых, имена функций совпадают с именами команд (обратите внимание на
тело функции «маневр»). Это допустимо. Даже если бы возникла коллизия наиме
нований, имена функций всегда можно употребить с префиксом – именем пакета.
Например, движение.налево, движение.назад, а имена команд – употребить с так
называемым КВАЛИФИКАТОРОМ. Например, команда (налево), команда (на
право), команда (назад). На самом деле в нашем случае ни префиксы, ни квалифи
каторы не нужны, так как успешно действуют правила перекрытия – по контексту
понятно, где имена команд, а где – функции.
Втретьих, функция «маневр» применима к типу данных «курс», но не входит в на
бор его базовых операций. Ведь определяющий пакет для этого типа – «движение».
Зато для типа «команда» функция «маневр» – базовая операция.
Шаг 5. Функции пакета «движение»
function налево(старый : курс) return курс is begin
case старый of
when север => return запад;
when восток => return север;
when þã => return восток;
when запад => return þã;
end case;
end налево;
113
Замечание (о согласовании абстракций). Перед нами – наглядное подтверждение
принципа цельности. Раз в Аде есть способ явно описывать «малые» множества
(вводить перечисляемые типы), то должно быть и средство, позволяющее непос
редственно сопоставить определенное действие с каждым элементом множества.
Таким средством и служит ВЫБИРАЮЩИЙ ОПЕРАТОР (case). Между ключе
выми словами case и of записывается УПРАВЛЯЮЩЕЕ ВЫРАЖЕНИЕ некото
рого перечисляемого типа (точнее, любого ДИСКРЕТНОГО типа – к последним
относятся и перечисляемые, и целые типы с ограниченным диапазоном значений).
Между of и end case записываются так называемые ВАРИАНТЫ. Непосредствен
но после when (когда) записывается одно значение, несколько значений или диапа
зон значений указанного типа, а после «=>» – последовательность операторов, ко
торую нужно выполнить тогда и только тогда, когда значение управляющего
выражения равно указанному значению (или попадает в указанный диапазон).
Выбирающий оператор заменяет условный оператор вида
if старый = север then return запад; elsif старый = восток then return север; elsif старый = юг then return восток; elsif старый = запад then return юг ; end if;
114
Условный и выбирающий операторы – частные случаи развилки (одной из трех основных управляющих структур: последовательность, развилка, цикл, – исполь зуемых в структурном программировании).
Современное состояние языков программирования
По сравнению с условным, выбирающий оператор, вопервых, компактнее (не нужно повторять выражение); вовторых, надежнее – и это главное.
Дело в том, что варианты обязаны охватывать все допустимые значения анали зируемого типа и никакое значение не должно соответствовать двум вариантам. Все задаваемые после when значения (и диапазоны) должны быть вычисляемы статически (то есть не должны зависеть от исходных данных программы, с тем чтобы компилятор мог их вычислить). Так что компилятор в состоянии прове рить указанные требования к вариантам выбирающего оператора и обнаружить ошибки.
Наконец, статическая вычислимость обеспечивает и третье преимущество вы бирающего оператора – его можно эффективно реализовать (значение выбираю щего выражения может служить смещением относительно начала вариантов).
Предоставим возможность читателю самостоятельно запрограммировать функции «направо» и «назад», завершив тем самым решение нашей «морской» задачи.
Морская задача и Алгол60. Читателям, привыкшим к Паскалю, где имеются перечисляемые типы и выбирающий оператор, не так просто оценить достижение Вирта. Чтобы подчеркнуть его связь с перспективной технологией программиро вания (и заодно лишний раз подтвердить принцип технологичности), посмотрим на «морскую» задачу из другой языковой среды. Представим, что в нашем распо ряжении не Ада, а Алгол60.
Технология. Уже на первом шаге детализации нам не удалось бы ввести подхо дящую операционную абстракцию. Помните, нам была нужна уверенность в воз можности определить подходящие типы для понятий «курс» и «команда». В Ал голе60 вообще нет возможности определять типы, в частности перечисляемые. Поэтому пришлось бы «закодировать» курсы и команды целыми числами. Ска жем, север – 1, восток – 2, юг – 3, запад – 4; команда «прямо» – 1, «налево» – 2, «направо» – 3, «назад» – 4. Заголовок функции «маневр» выглядел бы, например, так:
integer procedure маневр(старый, новый); integer старый, новый; value старый, новый;
Приступая к проектированию тела функции, мы не имели бы случая предвари тельно создать абстрактный тип «курс» с операциями поворота. Но ведь именно с операциями поворота связана основная идея реализации функции «маневр» на Аде! Вспомните, чтобы подобрать подходящую команду, мы проверяли возмож ность получить новый курс из старого определенным поворотом. Если бы при меняемая технология программирования не требовала вводить абстрактный тип «курс», то и идея реализации функции «маневр» вполне могла оказаться совсем другой. Не было бы удивительным, если бы ее тело было запрограммировано «в лоб», например так:
Данные и типы
begin маневр :=
if старый = новый then 1 else if старый = 1 & новый = 4 v старый = 2 & новый = 1 v
старый = 3 & новый = 2 v старый = 4 & новый = 3 then 2 else if старый = 1 & новый = 2 v старый = 2 & новый = 3 v
старый = 3 & новый = 4 v старый = 4 & новый = 1 then 3 else if старый = 1 & новый = 3 v старый = 2 & новый = 4 v
старый = 3 & новый = 1 v старый = 4 & новый = 2 then 4;
end маневр;
115
Конечно, «настоящие» программисты постарались бы «подогнать» кодировку
курсов и команд, с тем чтобы заменить прямой перебор «вычислением» команды. Однако такой прием неустойчив по отношению к изменению условий задачи, и в общем случае решение с большой вероятностью может оказаться ошибочным. К тому же оно менее понятно по сравнению с решением на Аде. Нужно «держать в голове» кодировку, чтобы понимать программу.
Таким образом, отсутствие средств, поддерживающих нужные абстракции
(в частности, в процессе пошаговой детализации), вполне может помешать и наи более творческим моментам в программировании, помешать увидеть изящное, на дежное, понятное и эффективное решение.
Надежность. Внимательнее сравним программы на Аде и Алголе60 с точки
зрения надежности предоставляемой услуги. Чтобы воспользоваться операцией «маневр», на Аде можно написать, например,
маневр(север, восток);
а на Алголе60
маневр(1,2);
Ясно, что первое – нагляднее, понятнее (а значит, и надежнее). Но высокий
уровень надежности гарантируется не только наглядностью, но и контролем при трансляции. На Аде нельзя написать маневр (1,2), так как транслятор обнаружит несоответствие типов аргументов и параметров! А на Алголе60 можно написать
маневр(25,30);
и получить... неизвестно что.
А чтобы получить тот же уровень контроля, который автоматически обеспечи
вает Адакомпилятор, нужно добавить в программу явные проверки диапазона целых значений и обращение к соответствующим диагностическим процедурам. И все это будет работать динамически, а в Аде – статически. Так что надежность при программировании на Алголе60 может быть обеспечена усилиями только самого программиста и только за счет снижения эффективности целевой про граммы.
Можно постараться добиться большей наглядности, введя переменные «се
вер», «восток», «юг» и «запад» (постоянных в Алголе60 нет). Им придется при своить значения 1, 2, 3, 4 также во время работы объектной программы, но зато окажется возможным писать столь же понятно, как и на Аде:
маневр(север, восток);
116
Однако в отличие от имензначений перечисляемого типа в Аде, которые по определению – константы, эти переменные не защищены от случайных присваи ваний. Не говоря уже о защите от применения к ним других операций (в Аде к значениям определенного перечисляемого типа применимы, конечно, только опе рации, параметры которых соответственно специфицированы).
Итак, мы выделили технологическую потребность определять небольшие мно жества имен и работать с ними на таком уровне абстракции, когда указываются лишь связи этих имен между собой и с другими программными объектами. Эта по требность и удовлетворяется в Аде перечисляемыми типами данных. Важно, что удовлетворяется она комплексно, в соответствии с важнейшими общими принци пами (такими, как принцип цельности) и специфическими требованиями к ЯП (на дежность, понятность и эффективность программ). Показано также, что перечис ляемые типы не могут быть полностью заменены аппаратом классических ЯП.
Современное состояние языков программирования
4.6.2. Дискретные типы
Перечисляемые типы – частный случай так называемых ДИСКРЕТНЫХ типов.
Дискретным называется тип, класс значений которого образует ДИСКРЕТ НЫЙ ДИАПАЗОН, то есть конечное линейно упорядоченное множество. Это значит, что в базовый набор операций для дискретных типов входит, вопервых, операция сравнения «меньше», обозначаемая обычно через «<»; вовторых, функ ции «первый» и «последний», вырабатывающие в качестве результатов соответ ственно минимальный и максимальный элементы диапазона, и, втретьих, функ ции «предыдущий» и «последующий» с очевидным смыслом. Эти операции для всех дискретных типов предопределены в языке Ада.
Кроме перечисляемых, дискретными в Аде являются еще и ЦЕЛЫЕ типы. Класс значений любого целого типа считается конечным. Для предопределенного типа INTEGER он фиксируется реализацией языка (то есть различные компиля торы могут обеспечивать различный диапазон предопределенных целых; этот диапазон должен быть указан в документации на компилятор; кроме того, его границы доставляются (АТРИБУТНЫМИ) функциями «первый» и «послед ний»). Для определяемых целых типов границы диапазона значений явно указы ваются в объявлении целого типа (см. объявление типа узел).
Для типа INTEGER предопределены также унарные операции «+», «–», «abs» и бинарные «+», «–», «*», «/», «**» (возведение в степень) и др.
Любые дискретные типы можно использовать для индексации и управления циклами. Мы уже встречались и с тем, и с другим в пакете управление_сетями.
В «морской» задаче мы не воспользовались предопределенными базовыми операциями для типа «курс». Но в соответствии с его объявлением
север < восток < юг < запад
причем
последующий(север) = восток; предыдущий(восток) = север; курс'первый = север;
Данные и типы
курс'последний = запад;
117
Так что функцию «налево» можно было реализовать и так:
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 предопре делены обычные операции для дискретных типов. (Мы привели не все такие опе
118
рации.) Кроме того, для типа BOOLEAN предопределены обычные логические операции and, or, хоr и not с обычным смыслом (хоr – исключительное «или»).
Вот несколько примеров определяемых дискретных типов:
type день_недели is(пн, вт, ср, чт, пт, сб, вс); type месяц is(январь, февраль, март, апрель, май, июнь, июль, август, сентябрь, октябрь, ноябрь, декабрь); type год is new INTEGER range 0..2099; type этаж is new INTEGER range 1..100;
Современное состояние языков программирования
4.6.3. Ограничения и подтипы
Проанализируем еще одну технологическую потребность – потребность ограни чивать множество значений объектов по сравнению с полным классом значений соответствующего типа. Рассмотрим фрагмент программы, меняющей знак каж дого из десяти элементов вектора А:
for j in 1..10 loop
A(j) :=-A(j); end loop;
Перед нами фрагмент, который будет правильно работать только при условии, что вектор А состоит ровно из десяти элементов. Иначе либо некоторые элементы останутся со старыми знаками, либо индекс выйдет за границу массива. К тому же такой фрагмент способен работать только с вектором А и не применим к вектору В.
Другими словами, это очень конкретный фрагмент, приспособленный для ра боты только в специфическом контексте, плохо защищенный от некорректного использования.
Вопрос. В чем это проявляется?
Допустим, что потребность менять знак у элементов вектора возникает доста точно часто. Вместо того чтобы каждый раз писать аналогичные фрагменты, хоте лось бы воспользоваться принципом обозначения повторяющегося и ввести подхо дящую подпрограмму, надежную и пригодную для работы с любыми векторами. Другими словами, мы хотим обозначить нечто общее, характерное для многих конкретных действий, то есть ввести операционную абстракцию.
От чего хотелось бы отвлечься? Повидимому, и от конкретного имени векто ра, и от конкретной его длины. Возможно, от конкретного порядка обработки ком понент или от конкретной размерности массива. Можно ли это сделать и целесо образно ли, зависит от многих причин. Но прежде всего – от возможностей применяемого ЯП. Точнее, от возможностей встроенного в него аппарата разви тия (аппарата абстракцииконкретизации).
Здесь уместно сформулировать весьма общий принцип проектирования (в ча стности, языкотворчества и программирования). Будем называть его принципом реальности абстракций.
Принцип реальности абстракций. Назовем реальной такую абстракцию, кото рая пригодна для конкретизации в используемой программной среде. Тогда прин цип реальности абстракций можно сформулировать так:
Данные и типы
119
в программировании непосредственно применимы лишь реальные абстракции.
Иначе говоря, создавая абстракцию, не забудь о конкретизации. Следует со
здавать возможность абстрагироваться только от таких характеристик, которые применяемый (создаваемый) аппарат конкретизации позволяет указывать в каче стве параметров настройки.
В нашем примере попытаемся построить ряд все более мощных абстракций,
следуя за особенностями средств развития в Аде. Скажем сразу: Ада позволяет явно построить первую из намеченных четырех абстракций (мы ведь собрались отвлечься от имени, от длины, от порядка и от размерности); со второй придется потрудиться (здесьто и понадобятся ограничения и подтипы); третья потребует задачного типа и может быть построена лишь частично, а четвертая вообще не по силам базисному аппарату развития Ады (то есть для Ады это – нереальная абст ракция).
Абстракция от имени. Достаточно ввести функцию с параметром и результа
том нужного типа.
Что значит «нужного типа»? Пока мы абстрагируемся только от имени векто
ра, сохраняя все остальные его конкретные характеристики. Поэтому нужен тип, класс значений которого – 10элементные векторы. Объявим его:
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 «-»;
Обратите внимание, такая функция перекрывает предопределенную опера
цию «–». Становится вполне допустимым оператор присваивания вида
À := -À;
где А – объект типа «вектор», а знак «–» в данном контексте обозначает не пре допределенную операцию над числами, а определенную нами операцию над век торами.
Замечание (о запрете на новые знаки операций). В Аде новые знаки операций
вводить нельзя. Это сделано для того, чтобы синтаксический анализ текста програм
мы не зависел от ее смысла (в частности, от результатов контекстного анализа). Ска
жем, знак I нельзя применять для обозначения новой операции, а знак «–» можно.
Именно для того, чтобы продемонстрировать перекрытие знака «–», мы ввели функ
цию, а не процедуру, хотя в данном случае последнее было бы эффективнее. Дей
ствительно, ведь наш исходный фрагмент программы создает массиврезультат,
изменяя массиваргумент. А функция создает новый массив, сохраняя аргумент
неизменным. Так что более точной и эффективной была бы абстракцияпроцедура
следующего вида:
120
procedure минус(X : in out вектор) is begin
for j in(1..10) loop
X(j) := – X(j);
end loop;
end минус;
Уже при такой слабой абстракции (только от имени вектора) мы оказались пе ред необходимостью согласовывать операционную абстракцию и абстракцию данных (обратите внимание на диапазон 1…10 и в объявлении функции, и в объяв лении типа). Так что принцип согласования абстракций работает и при создании языковых конструктов (при создании ЯП, на метауровне), и на уровне их приме нения. При этом согласование на метауровне призвано всячески облегчать согла сование на уровне применения.
Вопрос. Нельзя ли упростить последнее в нашем случае? Подсказка. Следует полнее использовать тип данных.
За счет согласованного объявления управляющей переменной цикла и диапа зона допустимых индексов мы повысили надежность программы. Применяя фун кцию «–», невозможно выйти за границы массива – ведь ее аргументами могут быть только 10элементные векторы.
Абстракция от длины вектора (начало). Пойдем дальше по пути абстракции. Как написать функцию, применимую к вектору любой длины? Уникальность типа требует снабдить определенным типом каждый параметр. Поэтому возника ют два согласованных вопроса (снова действует принцип согласования абстрак ций!): «как объявить нужный тип?» и «как написать тело процедуры, работающей с массивом произвольной длины?».
Здесь полезно на время оторваться от нашего примера и вспомнить об общем контексте, в котором эти вопросы возникли.
Современное состояние языков программирования
4.6.4. Квазистатический контроль
Мы продолжаем заниматься в основном данными – одной из трех важнейших аб стракций программирования. Исходя из потребности прогнозирования и контро ля поведения объектов (в свою очередь выводимой из более общей потребности писать надежные и эффективные программы), мы пришли к концепции уникаль ности типа данных. А исходя из назначения системы типов, выделили динамиче ские, статические и относительно статические (говоря короче, квазистатические) ЯП в зависимости от степени гибкости прогнозированияконтроля. Отметили, что в Аде концепция собственно типа ориентирована на прогнозированиеконт роль статических характеристик поведения объектов, а концепция подтипа – на прогнозированиеконтроль квазистатических (или, если угодно, квазидинами ческих) характеристик.
Наша ближайшая цель – обосновать полезность концепции подтипа.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]