- •4.9.1. Изменяемость
- •4.9.2. Классы операций
- •4.9.3. Полнота
- •4.9.5. Операции egual, similar и copy
- •5. Исключительные ситуации
- •6.2.1. Сигнализация об исключительных ситуациях
- •5.2.2. Обработка исключительных ситуаций
- •5.2.3. Предложение resignal
- •6.2.4. Необрабатываемые исключительные ситуации
- •6.2.1. Реализация итераторов
- •6.2.2. Использование итераторов
- •7.2. Абстракция данных
- •7.4. Генераторы
- •8.2.2. Обобщенность
- •9.1.3. Пример
- •9.6. Заключение
7.4. Генераторы
В языке Паскаль имеется оператор for, который позволяет выполнить итерацию в некотором поддиапазоне типов скалярных величин (за исключением типа real), но в нем совсем нет механизма для реализации итераторов, определенных пользователем. Как отмечено в гл. 6, это может вызвать неудобства. Например, чтобы суммировать элементы в очереди целых чисел intqueue, мы могли бы написать программу, аналогичную программе, приведенной на рис. 7.9. Эта реализация была бы несколько менее неуклюжа, если бы мы к очереди целых чисел intqueue добавили функцию q.copy, но все равно она была бы неудовлетворительной. В этой программе имеются потери и по времени, и по пространству. По крайней мере они состоят из копирования всей очереди.
sum := О t := q.new; while not (qJsempty (q)) do
begin
i := q_remfirst (q); sum := sum + i; q_append (t, i) end;
while not (isempty (t)) do q_append (q, q_remfirst (t));
Рис. 7.9. Суммирование элементов очереди без использования итератора,
Чтобы избежать таких потерь, мы создадим тип некоторого специального вида, называемый генератором. Генераторы имитируют работу итераторов. У каждого генератора есть три функции и одна процедура. Эти три функции создают объект генератора, получают следующий элемент и проверяют, все ли элементы были выданы. Процедура уничтожает объект генератора.
Например, для очереди целых чисел мы могли бы создать генератор qelems. Функция qelems.create выполняет некоторую работу, которая соответствует той инициализации, которая делается до выполнения первого оператора yield в итераторе языка CLU. Эта функция возвращает некоторый объект с типом qelems. Вторая функция qelems_next выдает каждый раз один элемент при обращении к ней и модифицирует объект генератора qelems, чтобы указать, что этот элемент уже был выдан. Последняя функция qelems.done используется для определения того, были ли выданы все элементы. Процедура qelems, destroy освобождает пространство, которое было выделено при работе функций qe-lems.create и qelems_next.
Спецификация генератора qelems приведена на рис. 7.10. Обе функции qelems_done и qelems_next требуют, «чтобы очередь, используемая для создания элементов qi, не модифицировалась после того, как создан элемент qi». Это аналогично общему требованию предложения requires итераторов языка CLU, в которых требуется, чтобы объект, по которому совершается итерация, не модифицировался в теле данного цикла.
qelems = generator type is qelems_create, qelems_done,
qelerns-next, qelems_destroy
Описание
Генераторы qelems используются для выполнения итераций по элементам очередей целых чисел intqueue. Реализация может зависеть от представления очереди целых чисел intqueue.
Операции
function qelems_create (qJntqueue): qelems
effects Возвращается некоторый объект qelems, который может быть использован для выдачи всех элементов в очереди q. function qelems_done (qi: qelems): boolean
requires Чтобы очередь целых чисел intqueue, используемая для создания элементов qi, не модифицировалась после того, как создан элемент qi.
effects Возвращается значение true, если все элементы в очереди целых чисел intqueue, используемой для создания элементов qi, были «выданы» после того, как был создан элемент qi. В противном случае возвращается значение false. function qelems.next (qi: qelems): integer
requires Чтобы очередь целых чисел intqueue, используемая для создания элементов qi, не модифицировалась после того, как создан элемент qi, и чтобы существовал некоторый элемент в очереди целых чисел intqueue, который еще не был выдан. modifies oi
effects Еозгращается некоторый элемент в очереди чисел intqueiie, используемый для создания элемента qi. Этот элемент не был выдан после того, как был создан элемент qi. Элемент qi модифицируется для того, чтобы отметить тот факт, что данный элемент был «выдан». proctewe qelems_destroy (qi: qelems) modifies qi
effects Освобождается вся память из неупорядоченного массива, занимаемая элементом qi. end qelems
Рис. 7,i0. Спецификация типа данных генератор для очередей.
Используя тип данных qelems, мы можем заново реализовать суммирование следующим образом:
sum := 0
qi := qelems_create (q); tt-hi'le not (qelems. done (qi)) do
sum := sum + qelems. next (qi); qelems- destroy (qi)
Другие циклы реализуются аналогичным образом.
На рис. 7. II приводится реализация операций над типом данных qelems. Эта реализация совместима с реализацией очереди целых чисел intqueue, приведенной на рис. 7.2. На самом деле осуществляется доступ к представлению очереди целых чисел intqueue. Такой доступ типичен для генераторов, использующих такие совокупносчи, как очереди целых чисел intqueue. Они в действительности являются частью данного типа совокупности, как итераторы являются частью языка CLU. Отметим, что пред ставление имеет дополнительный уровень косвенности дла того, чтобы гарантировать, что совместимое использование работает правильно.
qelems = {qelemsJ-ep; qelems_rep == ^intqueue_elem; function qelems_create (q: intqlieue): qelems; var qi: qelems; begin new (qi); qit := qf; qelems .create :== qi end {функция qelems_create}; lunction qelems .done (qi: qelems): boolean; begin
qelems_done := (qi^ == nil) end {функция qelems_done}; function qelems_next (qi: qelems): integer; begin if q^ = nil then failure ("К функции qelems_next обратились с пустой очередью
генератора") else begin qelems_next := qi-("l-.val; ФТ ••= qiTt-next end
end {функция qelems_next}; procedure qelems_destroy (qi: qelems); begin
dispose (qi); end {процедура qelems_destroy};
Рис. 7.11. Реализация генератора для типа данных очередь целых чисел intqueue.
7.5. Полный пример
В этом разделе приводится конкретная иллюстрация того, как подготовить программу на языке Паскаль, используя подход, который мы описали. Предположим, что у нас есть некоторая библиотека абстракций, в которую включен тип данных intbintree, приведенный на рис. 7.12, а мы хотим реализовать некоторую функцию, которая организует поиск по бинарному дереву целых чисел методом в ширину.
Поиск в ширину обычно реализуется с использованием очереди деревьев. Сначала помещаем в пустую очередь то дерево, по которому будет выполняться поиск. Затем проверяем, содержит ли корневой узел этого дерева тот элемент, который мы ищем. Если он не содержит его, удаляем из очереди это дерево и помещаем в очередь его правое и левое поддеревья. Затем этот процесс повторяется для каждого из этих деревьев. Если в конце мы получим пустую очередь, то в дереве нужного элемента не было.
intbintree == data type is bt_new, bt_isempty, bt_append_left, bt_append_right, bt.left, bt_right, bt_rootval, bt_destroy
Описание
Бинарные деревья целых чисел intbintree используются для хранения значений типа целые числа. Элемент может присутствовать в нескольких узлах такого дерева. И промежуточные узлы, и листья имеют значения. Бинарные деревья целых чисел intbintree изменяемы.
Операции
function bt_new; intbintree
effects Возвращается .пустое бинарное дерево.
function btJsempty (bt: intbintree): boolean
effects Возвращается значение true, если дерево bt пусто, и значение
false — в противном случае.
procedure bt_append_left (bt: intbintree; е: integer)
modifies bt effects Добавляется некоторый узел со значением е как самый левый
узел дерева bt.
procedure bt.append_right (bt: intbintree; е: integer)
modifies bt effects Добавляется некоторый узел со значением е как самый правый
узел дерева bt.
function btJeft (bt: intbintree): intbintree requires Чтобы дерево bt было не пустым.
effects Возвращается поддерево дерева bt, чей корень является левым ближайшим потомком корня дерева bt. Это поддерево может быть пустым.
function bt_right (bt: intbintree): intbintree requires Чтобы дерево bt было не пустым,
effects Возвращается поддерево дерева bt, чей корень является правым ближайшим потомком корня дерева bt. Это поддерево может быть пустым.
function bt_rootval (bt: intbintree): integer
requires Чтобы дерево bt было не пустым.
effects Возвращается значение корневого узла дерева bt.
procedure bt_destroy (bt; intbintree)
modifies bt
effects Освобождается вся память неупорядоченного массива, занимаемая деревом bt. end intbintree
Рис. 7.12. Спецификация бинарного дерева целых чисел intbintree.
На рис. 7.13 приведена реализация функции breadth.first. search. При этом предполагается, что была выполнена конкретная реализация абстракции queue [etype] при помощи замены etype на intbintree и задания имени btq получившемуся типу.
Отметим, что функция breadth. firsLsearch использует процедуру btq.destroy для освобождения пространства, занимаемого создаваемой очередью. Если это не будет сделано, то память, занимаемая элементами локальной переменной q, была бы поте ряна. С другой стороны, нет обращения к процедуре bt.destroy (t). Прежде чем выйти из операции, мы должны вызвать процедуру destroy для освобождения тех объектов, которые были выделены динамически в процессе работы этой функции. Хотя функция breadth.first. search делает объявление локальной переме « ной типа бинарного дерева целых чисел intbintree, она не создает ни одного объекта данного типа.
lunction breadth Jiist ^search (bt: intbiiitree; е: integer.): boolean; var q: btq; t: intbintree; found: boolean; begin found ;= false; if not (btJsempty (bt)) then begin q := btq_new; btq_append (q, btq);
while (not (btqJsempty (q)) and not (found)) do begin t := btq_re;nfirst (q); if bt.rootval (t) == e then found :== true else begin if not (btJsempty (bt.left (t))) then btq_append (q, btJeft (t)); if not (btJsempty (bt_right (t))) then btq_append (q, bt_right (t))! end end btq .destroy (q) end;
breadthJirst-search :== found end {функция breadthJirst_search}
Рис. 7.13. Реализация функции breadth, first, search,
Теперь у нао есть все части, необходимые для получения функции поиска в ширину, которую можно было бы вставить в большую программу. Способ, которым мы объединим эти части, зависит от используемого языка Паскаль. Во многих диалектах языка Паскаль (включая и стандартный язык Паскаль) порядок объявления переменных внутри блока имеет ограничения — сперва делаются все объявления const. затем все объявления type, далее все объявления var и, наконец, все объявления операций. Из этого еледует, что объявление представления некоторого абстрактного типа не будет соседствовать с реализациями операций над данным типом.
Далее мы предполагаем, что используем язык Паскаль, в котором снято это ограничение и допускается любой порядок объявлений. Если для языка Паскаль, у которого более строгие ограничения, надо затратить большие усилия по программированию, то, вероятно, выгодно написать некоторую программу, которая переводит программы, написамые как здесь без ограничений, в программы, соответствующие данному компилятору. Такая программа сама может быть быстро написана на языке Паскаль.
В языке Паскаль требуется, щобы все идентификаторы были объявлены до того, как они используются. Для написания взаимно рекурсивных операций исильзуют^я формальные объявления. Формальное объявление задает заголовок некоторой операции, не задавая ее тела. Обвдц формат программы на языке Паскаль, соответствующий ограничениям на объявление, а затем на использование, всегда будет следующим: 1) реализация процедуры failure; 2) реализации всех абстрактных типов, которые будут использоваться в данной программе, включая и генераторы; 3) реализации всех операций, которые не являются частью не-когарого абстрактного тела, а также объявления констант, типов и переменных и формальныесбъявления; 4) тело данной программы.
Структура программы, соответствующей нашему примеру, была бы следующей: 1) реализация процедуры failure; 2) реализация бинарного дерева целых чисе.т intbintree; 3) реализация очереди queue [intbintree]; 4) реализация функции breadth.first. serch; 5) другие объявления и тцо программы.
76. Обзор главы
Подход к программированию, рассматриваемый бэтой книге, в большой степени независим от используемого языка программирования. Мы предполагаем, что, хотя выбор языка программирования для реализации программы окажет некоторое влияние на проектирование самой программы, это влияние не будет доминирующим. В частности, нет необходимости ограничивать типы йостракций, используемые при проектировании, теми типами, ко-тсрые непосредственно поддерживаются в данном языке программирования.
В нашем случае мы обратились к языку Паскаль — языку, который имеет относительно примитивные механизмы абстракций. Мы определили, как надо модифицировать абстракции процедур, итераций и данных, чтобы соответствовать языку Паскаль, и предложили соглашения по программированию, которые могут быть использованы для реализации абстракций данных, генераторов, полиморфных абстракций и обработки исключительных ситуаций па языке Паскаль. Наш подход 8 языку Паскаль может быть относительно просто адаптирован для других языков программирования (в течение нескольких лет мы читали курс, в котором использовался язык ПЛ/1). Мы рассмотрим такую адаптацию ч гл. 15.
Дополнительная литература
Garland, Stephen J. 1986. Introduction to Computer Science with Applications in Pascal. Mass.: Addison-Wesley Publishing Co.
Упражнения
7.1. Реализуйте тип бинарного дерева целых чисел intbintree, заданный на рис. 7.12.
7.2. Специфицируйте некоторую абстракцию bintree [type], основанную на абстракции бинарного дерева целых чисел intbintree, заданную на рис. 7,12.
7.3. Реализуйте очередь целых чисел intqueue (рис. 7.1) так, чтобы доступ к первому и последнему элементам и их удаление могли быть сделаны за постоянное время. Надо ли изменять реализацию функции breadth-first-search для того, чтобы использовать преимущества этой лучшей реализации очереди целых чисел inttjueue?
7.4. Специфицируйте и реализуйте некоторую абстракцию обобщенного дерева, т. е. такого дерева, в котором может быть любое конечное число ветвей у каждого узла. (Указание: в своей реализации вы можете рассмотреть для каждого узла использование очереди его детей.)
7.5. Модифицируйте функцию breadth-first-search (рис. 7.13) так, чтобы она работала с обобщенным деревом, которое специфицировано в результате Вашего ответа на упражнение 7.4.
7.6. 'Специфицируйте и реализуйте некоторую абстракцию стека и используйте ее для реализации некоторой функции, которая выполняет поиск в ширину.
7.7. Специфицируйте и реализуйте некоторый генератор, который возвращает все листья в бинарном дереве целых чисел intbintree (рис. 7.12).
7.8. Что реализовано в программе, приведенной на рис. 7,14? (Предполагается, что сделаны спецификации, приведенные на рис, 7.1 и 7.10.)
program test: var i, sum: integer; q: intqueue; qi, qil: qelems; begin q := q-new;
for i := I to 3 do q.append (q, i); sum :== 0 qi := qelems_create (q); while not (qelems-done (qi)) do begin qil '.= qelems-create (q); while not (qelems-done (qil)) do
sum := sum + qelems-next (qil); qelems-destroy (qil); sum :== sum+ qelems_next (qi); end; qelems-destroy (qi); q .destroy (q); write Ln (sum) end {программа test)
Рис. 7.14. Программа на языке Паскаль»
8. Спецификации
В данной книге мы указывали важность.спецификаций на всех этапах разработки программного обеспеченияГПри этом подчеркивался тот факт, что корректное^прлменение спецификаций является ключевым моментом на пуйГ к эффективному программированию. Без спецификаций абстракции слишком сложны для того, чтобы оказаться действительно полезными^ В данной главе мы рассмотрим смысл спецификаций и некоторые критерии, которые необходимо учитывать при их написании. Мы также рассмотрим два основных назначения спецификаций.
8.1. Спецификации и необходимые наборы спецификаций
Назначение спецификации состоит в определении поведения абстракции. Пользователи полагаются на.это поведение, а разработчики должны обеспечить его. Говорят, что реализация, обеспечивающая описываемое поведение, удовлетворяет заданной спецификации.
Определим значение спецификации через набор всех программных модулей, ей удовлетворяющих. Назовем это необходимым набором спецификаторов для данной спецификации. В качестве примера рассмотрим спецификацию
р = ргос (у: int) returns (х: int)
requires у > 0
effects x >у
Она удовлетворяется любой процедурой, с именем р, которая при вызове с аргументом, большим нуля, возвращает значение, большее этого аргумента. Члены необходимого набора включают в себя:
р = ргос (у: int) returns (int) return (у + 1) end р
р = ргос (у: int) returns (int) return (у * 2) end р
р = ргос (у: int) returns (int) return (у + 3) end р
Подобно каждой спецификации, данной спецификации удовлетворяет неограниченное число программ.
Важно пешмняь, что спецификация, ее набор спецификаторов и некоторый член этого набора представляют собой весьма отличные друг от друга вещи, так же отличающиеся друг от друга, . как отличается программа от набора всех выполняемых ею возможных вычислений и работы этой программы с каким-то одним набором значений.
8.2. Некоторые критерии, применяемые к спецификациям
Хорошие спецификации могут иметь различные формы, однако все они имеют схожие атрибуты. В данном разделе рассматриваются три важных атрибута — ограниченность, обобщенность и простота.
8.2.1. Ограниченность
Имеется большое различие между знанием о пригодности некоторых членов набора спецификаторов, и знанием того, что подходящими являются все члены. Это аналогично отличию знания о возможности работы программы с некоторыми наборами входных значений, от знания возможности ее работы со всеми входными значениями. Мы будем говорить о подобном отличии при обсуждении тестирования программ (см. гл. 9).'./Хорошая спецификация должна быть ограничена настолько, чтобы позволить выявить любую реализацию, не устраивающую пользователей данной абстракции. Это требование лежит в основе почти всех применений спецификаций.
В общем случае обсуждение того, является ли спецификация достаточно ограниченной, предполагает рассмотрение задач, возлагаемых на членов набора спецификаторов,^меется ряд распространенных ошибок, которые почти всегда приводят к неадекватно ограниченным абстракциям^? К одной из таких ошибок' относитс^неполное задание требований в предложении requires.^/ Например, на рис. 8.1 приведены три спецификации итератора elems для портфеля целых чисел. (Портфель (bag) представляет собой обычный набор данных за тем исключением, что элементы в нем могут повторяться. Например, портфель bag [int] может содержать, число 3 дважды. Портфели иногда называют мульти-наборами.) В первой спецификации не дается ответа на вопрос, что произойдет, если b изменится в цикле, использующем elems. Она, следовательно, допускает реализации с резко отличающимся поведением. Например, приведет ли изменение b к изменению значений, возвращаемых elems?
elems = iter (b: bag [t]) yields (e: t) effects Поочередно выдает каждый элемент из Ь.
elems == iter (b: bag [t]) yields (e: t)
requires b не изменяется в цикле, использующем elems. effects Поочередно выдает каждый элемент из b.
elems = iter (b: bag [t]) yields (e: t)
requires b не изменяется в цикле, использующем elems. effects Выдает все элементы из b в произвольном порядке. Каждый элемент из b выдается столько же раз, сколько раз он встречается в b.
Рнс. 8.1. Три спецификации для elems.
Одним из способов решения данной проблемы является введение требования о том, чтобы переменная b не изменялась внутри использующего elems цикла. Это требование задано во второй спецификации. Эта спецификация может бьпь, а может и не быть достаточно ограниченной, поскольку она не накладывает ограничений на порядок, в котором возвращаются элементы. Лучше, если спецификация будет определять порядок или содержать фразу «в произвольном порядке». Кроме того, в специфшка-ции не указано, что делается в том случае, если какой-нибудь элемент встречается в b более одного раза. В ней даже не говорится явно, что elems возвращает только те элементы, которые содержатся в b. В третьей спецификации рассмотренные недостатки устранены.
К другим ошибкам относится отсутствие полного списка исключительных ситуаций и описания .поведения программы для граничных случаев. Например, пусть операция string^indexs получает в качестве аргументов строки si и s2, и, если si является подстрокой в s2, возвращает индекс вхождения в s2 первого символа из si. Например: &iring,$'ndexs ("ab", "abc"») == 2
Спецификаиия, которая содержит только эту информацию, не будет достаючно ограниченной, поскольку она не объясняет, что произойдет в том случае, если si не является подстрокой в s2, пли если она встречается в s2 несколько раз, или если si или s2 пусты. Спецификация на рис. 8.2 является достаточно полной.
Сказанное позволяет сделать вывод о том, что написание достаточно эффективных спецификацнй должно производиться с большой осторожностью. Полезную роль здесь может сыграть язык формальных спецификаций (формальные спецификации рассматриваются в гл. 10).
iiidexs = proc (si, s2: string) returns (i: int)
effects Если si входит в s2 как подстрока, то i есть последний индекс вхождения si в s2. i равно 0, если si не входит в s2, и рапно 1, если si есть пустая строка. indexs ("be", "abcbc") = 2 indexs (" ", "a") = I
Рис, 8.2, Спецификация операции strin^indexs,