В.Ш. Кауфман - Языки программирования - концепции и принципы (1990) (1160787), страница 20
Текст из файла (страница 20)
изобретения Вирта программисты явно кодировали такие компоненты (обычно
целыми числами). В сущности, Вирт предложил полезную абстракцию от
конкретной кодировки, резко повышающую надежность, понятность и
модифицируемость программ без заметной потери эффективности.
Упражнение. Обоснуйте последнее утверждение.
Подсказка. См. ниже пункт 4.7.1.1.о сравнении с Алголом-60.
Шаг 3. Как формализовать понятие "курс"?
Нетрудно догадаться, что "курс" должен быть перечисляемым типом:
type курс is (север, восток, юг, запад);
Ведь снова, как и для типа "команда", с точки зрения решаемой задачи
абсолютно несущественно внутреннее строение этих значений. Поэтому
невозможно вводить "курс" каким-либо конструктором составного типа.
Действительно, из чего состоит "север" или "восток"? Важно лишь, что это
разные сущности, связанные между собой только тем, что направо от севера -
восток, а налево от востока - север.
Таким образом, значения типа "курс", также как и типа "команда",
следует считать просто именами компонент модели задачи (точнее, той модели
внешнего мира, на которой мы решаем нашу содержательную задачу).
Существенные связи этих имен - непосредственное отражение содержательных
связей между именуемыми компонентами внешней модели. Мы пришли еще к одной
причине, по которой нам нужны в программе все такие имена-значения - иначе
не запрограммировать базовые функции (ведь нет никаких внутренних
зависимостей между значениями, только внешние, а их-то и нужно отражать).
Когда мы программировали базовую функцию "связать" в пакете
управление_сетью, нам, наоборот, были абсолютно безразличны индивидуальные
имена узлов - можно было программировать, опираясь на внутреннее строение
именуемых объектов (на строение записей_об_узле в массиве "сеть"). Это
внутреннее строение создавалось пользователем, работающим с пакетом,
посредством других базовых операций. Когда же будем программировать,
например, операцию "налево", никакое внутреннее строение не подскажет нам,
что налево от юга находится восток. Это следует только из модели внешнего
мира, создаваемой нами самими, т.е. в данном случае создателем пакета, а не
пользователем. Поэтому мы обязаны явно сопоставить восток - югу, север -
востоку, юг - западу.
[С ситуацией, когда строение значений скрыто от пользователя, но отнюдь
не безразлично для реализатора, мы уже встречались, когда изучали приватные
типы данных. Теперь строение не скрыто, но существенно лишь постольку,
поскольку обеспечивает идентификацию значений. В остальном можно считать,
что его просто нет - перед нами список имен объектов внешнего мира, играющих
определенные роли в решаемой задаче.
Перечисляемые типы похожи на приватные тем, что также создают для
пользователя определенный уровень абстракции - пользователь вынужден
работать только посредством операций, явно введенных для этих типов. Однако
если операции приватных типов обычно обеспечивают доступ к скрытым
компонентам содержательных "приватных" объектов, то операции перечисляемых
типов отражают связи между теми содержательными объектами, которые названы
явно перечисленными в объявлении типа именами. Вместе с тем приватный тип
вполне может оказаться реализованным некоторым перечисляемым типом.]
Завершая шаг детализации, определим перечисляемый тип "курс" вместе с
базовыми операциями-поворотами. Сделаем это с помощью спецификации пакета
"движение":
package движение is
type курс is (север, восток, юг, запад) ;
function налево (старый : курс) return курс ;
function направо (старый : курс) return курс ;
function назад (старый : курс) return курс ;
end движение ;
Шаг 4. Тело функции "маневр".
Идея в том, чтобы понять, каким поворотом можно добиться движения в
нужном направлении, и выдать соответствующую команду.
function маневр (старый, новый : курс) return команда ;
begin
if новый = старый then return прямо ;
elsif новый = налево(старый) then return налево ;
elsif новый = направо(старый) then return направо ;
else return назад ;
end if ;
end маневр ;
Мы свободно пользовались сравнением имен-значений на равенство.
Условный оператор с ключевым словом elsif можно считать сокращением обычного
условного оператора. Например, оператор
if B1 then S1 ;
elsif B2 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 ;
Такое сокращение удобно, когда нужно проверять несколько условий
последовательно.
Тем самым программирование функции маневр завершено.
Еcли считать, что "маневр" - лишь одна из предоставляемых пользователю
услуг, можно включить ее в пакет со следующей спецификацией:
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 налево ;
Замечание (о согласовании абстракций). Перед нами - наглядное
подтверждение принципа цельности. Раз в Аде есть способ явно описывать
"малые" множества (вводить перечисляемые типы), то должно быть и средство,
позволяющее непосредственно сопоставить определенное действие каждому
элементу такого множества. Таким средством и служит ВЫБИРАЮЩИЙ ОПЕРАТОР
(case). Между ключевыми словами case и of записывается УПРАВЛЯЮЩЕЕ
ВЫРАЖЕНИЕ некоторого перечисляемого типа (точнее, любого ДИСКРЕТНОГО типа -
к последним относятся и перечисляемые, и целые типы с ограниченным
диапазоном значений). Между of и end case записываются так называемые
ВАРИАНТЫ. Непосредственно после then (когда) записывается одно значение,
несколько значений или диапазон значений указанного типа, а после "=>" -
последовательность операторов, которую нужно выполнить тогда и только тогда,
когда значение управляющего выражения равно указанному значению (или
попадает в указанный диапазон).
Выбирающий оператор заменяет условный оператор вида
if старый = север then return запад ;
elsif старый = восток then return север ;
elsif старый = юг then return восток;
elsif старый = запад then return юг ;
end if ;
[Условный и выбирающий операторы - частные случаи развилки (одной из
трех основных управляющих структур: последовательность, развилка, цикл),
используемых в структурном программировании.]
По сравнению с условным, выбирающий оператор, во-первых, компактнее (не
нужно повторять выражение); во-вторых, надежнее - и это главное.
Дело в том, что варианты обязаны охватывать все допустимые значения
анализируемого типа и никакое значение не должно соответствовать двум
вариантам. Все задаваемые после when значения (и диапазоны) должны быть
вычисляемы статически (т.е. не должны зависеть от исходных данных программы,
с тем чтобы компилятор мог их вычислить). Так что компилятор в состоянии
проверить указанные требования к вариантам выбирающего оператора и
обнаружить ошибки.
Наконец, статическая вычислимость обеспечивает и третье преимущество
выбирающего оператора - его можно эффективно реализовать (значение
выбирающего выражения может служить смещением относительно начала
вариантов).
Предоставим возможность читателю самостоятельно запрограммировать
функции "направо" и "назад", завершив тем самым решение нашей "морской"
задачи.
4.7.1.1. Морская задача и Алгол-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 маневр ;
Конечно, "настоящие" программисты постарались бы "подогнать" кодировку
курсов и команд с тем, чтобы заменить прямой перебор "вычислением" команды.
Однако такой прием неустойчив по отношению к изменению условий задачи и в
общем случае с большой вероятностью может оказаться ошибочным. К тому же оно
менее понятно по сравнению с решением на Аде. Нужно "держать в голове"
кодировку, чтобы понимать программу.