Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
30
Добавлен:
23.03.2015
Размер:
885.76 Кб
Скачать

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. Реализация генератора для типа данных очередь целых чисел int­queue.

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.dest­roy (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 Applica­tions 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,

Соседние файлы в папке Б. Лисков