- •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.3. Предложение resignal
- •5.2.4. Необрабатываемые исключительные ситуации
- •6.2.1. Реализация итераторов
- •6.2.2. Использование итераторов
- •7.2. Абстракция данных
- •7.4. Генераторы
- •9.1.3. Пример
6.2.3. Предложение resignal
Иногда обработка исключительной ситуации сводится просто к сигнализации о той же самой исключительной ситуации с теми же самыми результатами. Для этого случая в языке CLU имеется предложение resignal
resignal имена
По существу это является короткой формой предложения excepts в котором каждое имя сигнализируется явно и с теми же самыми результатами. Например,
i := search (a, x) resignal duplicate, not_in
является короткой формой для
i :== search (a, x) except when not_in: signal not_in when duplicate (j: int): signal duplicate (j) end
5.2.4. Необрабатываемые исключительные ситуации
Программы на языке CLU не обязательно должны включать в себя программы обработки для всех исключительных ситуаций. Вместо этого каждая процедура на языке CLU имеет еще одну дополнительную исключительную ситуацию, кроме тех, которые перечислены в списке предложений signals. Эта исключительная ситуация имеет имя failure и один аргумент, строку. Каждая исключительная ситуация, возникшая в вызванной процедуре
Исключительные ситуации
и не обрабатывающаяся явно в вызывающей процедуре, автоматически преобразуется в исключительную ситуацию failure. Это преобразование эквивалентно действиям, которые были бы произведены следующим включенным в тело процедуры предложением except:
except when failure (s: string): signal failure (s) others (s: string): signal failure («необрабатыв. искл. сит.: " Ц s) end
Если необрабатываемая исключительная ситуация не является failure, ее имя становится частью строкового аргумента; если необрабатываемая исключительная ситуация есть failure, ее строковый аргумент сохраняется. Таким образом, строковый аргумент содержит имя первой необрабатываемой исключительной ситуации
~ Реализация процедуры choose (рис. 5.2) включает в себя необрабатываемую исключительную ситуацию. Так как мы знаем, что при выполнении оператора return набор s не пуст, мы не беспокоимся о возникновении исключительной ситуации bounds. В случае же, когда эта исключительная ситуация все-таки возникает, она автоматически распространяется как исключительная ситуация failure («необрабатыв. искл. сит.: boudns»). (Заметим, что исключительная ситуация boudns может возникнуть только в случае ошибки в реализации массивов.)
Оператор others предложения except рассматривает исключительную ситуацию failure иначе, чем другие исключительные ситуации. Если исключительная ситуация, попавшая в ветвь
others (s: string): тело
есть failure, то s присваивается строковый аргумент исключительной ситуации failure, а не строка «failure». Эффект в этом случае такой же, как если бы ветвь оператора others была разбита на две следующие части:
when failure (s: string): тело others (s: string): тело
Об исключительной ситуации failure также может быть сигнализировано явно; в этом случае должен быть задан строковый аргумент.
Должны обрабатываться все исключительные ситуации, кроме тех, которые не могут возникнуть. Следовательно, ситуация failure обычно означает, что произошла программная или системная ошибка. В этом случае ситуацию обычно анализирует программист. Информация в строке исключительной ситуации failure предназначена главным образом для программистов, а не §ля программ.
116 Глава 5
6.2.5. Предложение exit
В гл. 2 мы ввели предложения break и continue; break используется для выхода из цикла, a continue — для перехода к следующей итерации цикла. Иногда необходима более общая передача управления. Эту возможность предоставляют предложения exit и except, используемые совместно.
Форма предложения exit похожа на форму предложения signal exit имя (выражение)
Имя определяет выходную точку предложения exit; это имя должно встречаться в предложении, содержащем except, и управление передается ближайшему предложению, содержащему except, с программой обработки, соответствующей этому имени. Если при выполнении предложения exit никаких значений не передается, часть (выражение) может отсутствовать.
В следующем примере предложение exit используется для завершения цикла поиска.
ai == array [int] х: int i: int :== ai$low (a) begin while i <= ai$high (a) do x:=a[i]
if special (х) then exit found end i := i + I end
x :== make. new-one () % если не находит, создает новый end except when found: end % Здесь мы имеем нужное значение для х
5.3. Использование исключительных ситуаций в программах
При реализации процедуры с исключительными ситуациями работа программиста заключается в том, чтобы обеспечить соответствующую спецификации работу процедуры. Если спецификация включает в себя исключительные ситуации, программа должна в нужное время сигнализировать о соответствующих исключительных ситуациях и передавать определенные в спецификации значений. Для выполнения этой задачи программа, возможно, должна будет обрабатывать исключительные ситуации, возникающие в процедурах, к которым она обращается,
Исключительные ситуации могут обрабатываться двумя различными способами. Иногда исключительная ситуация распространяется до другого уровня, т. е. вызывающая процедура также завершает работу сигнализацией об исключительной ситуации с тем же самым или другим именем. Перед распростране-
Исключительные ситуации
нием исключительной ситуации вызывающая процедура может осуществить некоторую локальную обработку. Такая обработка иногда необходима для достижения соответствия со спецификацией вызывающей процедуры. Например, операция типа данных должна гарантировать, что объекты перед ее завершением будут удовлетворять инварианту представления.
Другой способ заключается в том, что вызывающая процедура маскирует исключительную ситуацию, т. е. обрабатывает исключительную ситуацию сама. Например, процедура может считывать все символы потока, используя операцию stream$getc. Когда процедура getc сигнализирует о конце файла (end-ol'-file), это просто означает, что все символы считаны и пора закончить работу.
В этом разделе представлены примеры использования исключительных ситуаций при спецификации и реализации процедур. Наш первый пример — наборы целых чисел intset. Эти наборы — те же, что и раньше (рис. 5.4), за исключением того, что операция intset == cluster is create, insert, delete, member, size, choose rep -= array [int] create = proc ( ) returns (cvt) return (rep$new ( )) end create
insert == proc (s: intset, х: int) if— member (s, х) then rep$addh (down (s), х) end end insert delete == proc (s: cvt, х: int)
j: int := getind (s, х) ехсерт when not_in: return end s [j] := rep$top (s) rep$remh (s) end delete
member == proc (s: cvt, х: int) returns (bool) getir)j (s, х) except when not_in: return (false) end return (true) end member size = proc (s: cvt) returns (int) return (rep$size (s)) end size
choose = proc (s: cvt) returns (int) signals (empty) return (rep$bottom s))
except when bounds: signal empty end end choose
getind == proc (s: rep, х: int) returns (int) signals (not_in) i: int := rep$low (s) while true do if x == s [i] then return (i) end
except when bounds: signal not_in end i:=i+ I end end getind end intset Рис. 5.4. Реализация типа данных intset.
118 Глава 5
choose сигнализирует об исключительной ситуации, если ее аргумент пуст. Реализация представлена на рис. 5.4. Заметим, что мы изменили операцию getind, чтобы она сигнализировала об исключительной ситуации not-in, если х не принадлежит s. Этот сигнал перехватывается операцией delete, которая просто маскирует исключительную ситуацию и осуществляет нормальный возврат. Реализация операции member также маскирует исключительную ситуацию.
Реализация операции getind использует исключительную ситуацию bounds, о которой сигнализирует операция fetch для массивов, в случае если она просмотрела все элементы массива. Когда процедура getind получает этот сигнал, она сигнализирует об исключительной ситуации not_in, что является примером распространения исключительной ситуации. Другой пример распространения — процедура choose.
Реализация операции delete иллюстрирует использование необрабатываемых исключительных ситуаций. Если из процедуры getind осуществляется нормальный возврат, мы знаем, что индекс находится внутри заданных границ и что массив не пуст. Следовательно, нет необходимости следить за возникновением исключительной ситуации bounds при вызове операций top, fetch и remh для массивов,
Следующий пример —это упорядоченные списки. Удалив некоторые предложения requires и добавив исключительные ситуации в операциях addel, remel и least, мы получаем более устойчивую спецификацию. Эта результирующая спецификация представлена на рис. 5.5.
Реализация упорядоченных списков показана на рис. 5.6. Некоторые процедуры используют предложение resignal. Например, процедура addel рекурсивно обращается сама к себе по левому и правому поддеревьям; если в каком-то из этих обращений возникает исключительная ситуация dupl, происходит выполнение предложения resignal. Это предложение часто используется в рекурсивных реализациях, как, например, в нашем случае.
В качестве последнего примера рассмотрим процедуру summation, специфицированную и реализованную на рис. 5.7. Заметим, что в предложениях requires и where перечислены исключительные ситуации, связанные с процедурой add. Процедура summation использует списки, определенные на рис. 4.12, 'за исключением того, что процедуры first и rest сигнализируют об исключительной ситуации empty, если список, заданный как аргумент, пуст. Исключительная ситуация empty, возникающая при вызове list.$first, маскируется; когда она возникает, все элементы списка уже просмотрены. Исключительные ситуации underflow и overflow распространяются.
Исключительные ситуации
olist^data type It; type] is create, addel, remel, ii.in, empty, least
Requires t имеет операции
It, equal: proctype (t, t) returns (bool), которые определяют упорядочение t
empty
Описание
Упорядоченные списки olist — изменяемые списки элементов. Операции addel и remel модифицируют упорядоченный список. Операция least возвращает наименьший элемент упорядоченного списка,
Операции
create = ргос ( ) returns (olist [t ]) effects Возвращает новый, пустой список.
addel г= ргос (s: olist [t], х: t) signals (dup!)
modifies s
effects Если х принадлежит s, сигнализирует об исключительной ситуации dupl, иначе вставляет х в s.
remel == ргос (s: olist [t], х: t) signals (notJn)
modifies s
effects Если х не принадлежит s, сигнализирует об исключительной ситуации not_in, иначе удаляет х из s.
is_in = ргос (s: olist [tl, х: t) returns (bool)
effects Возвращает значение true, если s содержит какой-нибудь элемент, равный х, в противном случае возвращает значение false.
ргос (s: olist [t]) returns (bool)
effects Возвращает значение true, если s не содержит элементов, и значение false — в противном случае.
least =- ргос (s: olist [t]) returns (t) signals (empty) effects Если s пуст, сигнализирует об исключительной ситуации empty, иначе возвращает элемент е из s, такой, что в s не существует элемента, меньшего е (как определяется t$lt).
end olist Рис. 5.5. Спецификация упорядоченных списков.
olist == cluster [t: type] is create, addel, remel, isJn, least, empty where t has equal. It: proctype (t, t) returns (bool)
node == record [val: t, left, right: olist [t]] rep= variant [some: node, empty: null]
% Типичный упорядоченный список olist есть [el, ...,en] % Функция абстракции есть
% A (r) = [] если г пуст, % = A (n.left) [[ [n.val ] \\ A (n.right) % где n = value_some (r)
% Инвариант представления есть % if is_some (r) then % ace компоненты n.left < n.val % & n.val < всех компонентов n.right, % где n == value_some (r)
если если r не пуст,
118 Глава 5
choose сигнализирует об исключительной ситуации, если ее аргумент пуст. Реализация представлена на рис. 5,4. Заметим, что мы изменили операцию getind, чтобы она сигнализировала об исключительной ситуации not-in, если х не принадлежит s. Этот сигнал перехватывается операцией delete, которая просто маскирует исключительную ситуацию и осуществляет нормальный возврат. Реализация операции member также маскирует исключительную ситуацию.
Реализация операции getind использует исключительную ситуацию bounds, о которой сигнализирует операция fetch для массивов, в случае если она просмотрела все элементы массива. Когда процедура getind получает этот сигнал, она сигнализирует об исключительной ситуации not_in, что является примером распространения исключительной ситуации. Другой пример распространения — процедура choose.
Реализация операции delete иллюстрирует использование необрабатываемых исключительных ситуаций. Если из процедуры getind осуществляется нормальный возврат, мы знаем, что индекс находится внутри заданных границ и что массив не пуст. Следовательно, нет необходимости следить за возникновением исключительной ситуации bounds при вызове операций top, fetch и remh для массивов.
Следующий пример —это упорядоченные списки. Удалив некоторые предложения requires и добавив исключительные ситуации в операциях addel, remel и least, мы получаем более устойчивую спецификацию. Эта результирующая спецификация представлена на рис. 5.5.
Реализация упорядоченных списков показана на рис. 5.6. Некоторые процедуры используют предложение resignal. Например, процедура addel рекурсивно обращается сама к себе по левому и правому поддеревьям; если в каком-то из этих обращений возникает исключительная ситуация dupl, происходит выполнение предложения resignal. Это предложение часто используется в рекурсивных реализациях, как, например, в нашем случае.
В качестве последнего примера рассмотрим процедуру summation, специфицированную и реализованную на рис. 5.7. Заметим, что в предложениях requires и where перечислены исключительные ситуации, связанные с процедурой add. Процедура summation использует списки, определенные на рис. 4.12, 'за исключением того, что процедуры first и rest сигнализируют об исключительной ситуации empty, если список, заданный как аргумент, пуст. Исключительная ситуация empty, возникающая при вызове list$first, маскируется; когда она возникает, все элементы списка уже просмотрены. Исключительные ситуации underflow и overflow распространяются.
Исключительные ситуации
olist=data type [t: type I is create, addel, remel, is_in, empty, least
Requires t имеет операции
It, equal: proctype (t, t) returns (bool), которые определяют упорядочение t
Описание
Упорядоченные списки olist — изменяемые списки элементов. Операции addel и remel модифицируют упорядоченный список. Операция least возвращает наименьший элемент упорядоченного списка,
Операции
create = ргос () returns (olist [t]) effects Возвращает новый, пустой список.
addel = proc (s: olist [t], х: t) signals (dupl)
modifies s
effects Если х принадлежит s, сигнализирует об исключительной ситуации dupl, иначе вставляет х в s.
remel = proc (s: olist [t], х: t) signals (notJii)
modifies s
effects Если х не принадлежит s, сигнализирует об исключительной ситуации not-in, иначе удаляет х из s.
isJn = ргос (s: olist [t], х: t) returns (bool)
effects Возвращает значение true, если s содержит какой-нибудь элемент, равный х, в противном случае возвращает значение false.
empty = ргос (s: olist [t]) returns (bool)
effects Возвращает значение true, если s не содержит элементов, и значение false — в противном случае.
least = proc (s: olist [t]) returns (t) signals (empty) effects Если s пуст, сигнализирует об исключительной ситуации empty, иначе возвращает элемент е из s, такой, что в s не существует элемента, меньшего е (как определяется l$lt).
end olist Рис, 5.5. Спецификация упорядоченных списков.
olist ^cluster [t: type] is create, addel, remel, is-in, least, empty where t has equal, It; proctype (t, t) returns (bool)
node == record [val; t, left, right: olist [t]) rep == variant [some; node, empty; null I
% Типичный упорядоченный список olist есть [el, ...,en] % Функция абстракции есть
% А (г) = [] если г пуст, % == A (n.left) II [n.val ] Ц A (n.right) если г не пуст, % где п == value-some (г)
% Инвариант представления есть % if is-some (r) then % все компоненты n.left < n.val % & n.val < всех компонентов n.right, % где n = value.some (r)
120 Глава 5
create = proc ( ) returns (cvt) return (rep$make_empty (nil)) end create
addel = proc (s: cvt, v; t) signals (dupl) tagcase s fag some (n: node): it v < n.val then signal dupl elseif v < n.val then addel (n.left, v) else addel (n.right, v) end resignal dupl tag empty:
rep$change_some (s, tiode${val: v, left: create (), right: create ())) end end addel
rernel = proc (s: cvt, v: t) signals (notJn) tagcase s tag empty: signal notJn tag some (n: node): if v == n.val then if empty (n.right) then % заменить узел на лево? поддерево rep$v_get_v (s, down (n. left))
else % сделать n.val значением из правого поддерева n.val :== least (n.right) remel (n.right, n.val) end
elseif v < n.val then remel (n.left, v) else remel (n.right, v) end resignal not_in end end remel
isJn = proc (s: cvt, v: t) returns (bool) tagcase s tag empty: return (false) tag some (n: node): if v = n.val then return (true) elseif v < n.val then return (isJn (n.left, v)) else return (isJn (n.right, v)) end end end isJn
least = proc (s: cvt) returns (t) signals (empty) tagcase s tag empty: signal empty tag some (n: node): return (least (n.left)) end except when empty: return (n.val) end end least
empty = proc (s: cvt) retunis (bool) return (rep$is_empty (s)) end empty
end olist Рис. 5.6, Реализация упорядоченных списков.
Исключительные ситуации
summation^ proc [t: type] (x: iist [t], zero; t) returns (t)
signals (underflow, overflow) requires t имеет операцию добавления:
add: proctype(t, t) returns (t) signals (underflow, overflow) effects Создает сумму элементов в x, начиная с zero; сигнализирует об искл. ситуациях underflow или overflow в случае возникновения потери значимости или переполнения при вычислении суммы.
summation = proc [t: type] (x: list (t], zero: t) returns (t)
signals (underflow, overflow) where t has add: proctype (t, t) returns (t)
signals (underflow, overflow) sum: t : == zero while true do sum ;= sum-{- list [t]$first (x) % используется t$add
except when empty: return (sum) end x := list [t]$rest (x) end resignal underflow, overflow end summation
Рис. 5.7. Процедура summation.
5.4. Некоторые аспекты проектирования программ
Исключительные ситуации следует использовать для устранения большинства ограничений, перечисленных в предложениях requires. Эти предложения следует оставлять только из соображений эффективности или если контекст использования настолько ограничен, что мы можем быть уверены, что ограничения удовлетворяются. Например, процедура search будет, вероятно, требовать, чтобы массив был упорядочен, так как в этом случае она может быть реализована намного более эффективно. Точно так же процедура merge, используемая для сортировки слиянием (рис. 3.2), будет требовать, чтобы ее аргументы были упорядочены, так как, с одной стороны, это улучшает эффективность и, с другой стороны, потому что контекст ее использования ограничен.
Исключительные ситуации также следует использовать для того, чтобы избежать кодирования информации в обычных результатах. Например, процедура getind кластера набора целых чисел (рис. 5.4) сигнализирует об исключительной ситуации, если элемент не находится в массиве, а не возвращает элемент, больший, чем верхняя граница. Лучше передавать эту информацию с помощью исключительной ситуации, так как возвращаемый в этом случае результат не может быть использован как обычный результат. Используя исключительную ситуацию, мы можем легко отличать этот результат от обычного, что позволяет избежать потенциальной ошибки.
Когда процедура имеет смысл только для аргументов, принадлежащих подмножеству ее области определения, в реализации этой процедуры допускается делать с не принадлежащими этому подмножеству аргументами все, что угодно. Конечно, не все
122 Глава 5
реализации равно хороши. Самый лучший подход — сигнализировать об исключительной ситуации failure. Часто это происходит естественным образом, либо из-за того, что не обрабатывается исключительная ситуация, которая не имеет места для аргументов, принадлежащих допустимому подмножеству, либо из-за выхода на конец процедуры, которая должна возвращать результаты. (В этом случае компилятор языка CLU вставляет команды для сигнализации об исключительной ситуации failire.) В других случаях, возможно, стоит затратить некоторые усилия. Например, в процедуре merge (рис. 3.6) мы можем во время просмотра элементов массива сравнивать эти элементы с целью определить, упорядочены они или нет, и если нет, то мы сигнализируем об исключительной ситуации failure. Такие проверки — некоторая дополнительная работа, однако они существенно улучшают устойчивость программы.
Не все ошибки сызывают исключительные ситуации. Например, пусть в большом вводном файле имеется ошибочная запись и будет возможно продолжать обработку файла, пропустив эту запись. В этом случае целесообразно оповес_ти_гь_..иол.ьзователя (а не программу) о произошедшей^ошибке. Исключительные ситуа-ций^являются механизмом взаимодействия программ, а не программ с пользователями. Для взаимодействия с пользователями па какое-нибудь устройство вывода может быть выдано сообщение об ошибке. Заметим, что реакция на ошибку определяется в спецификации абстракции.
Однако исключительные ситуации не всегда связаны с ошибками. Для некоторых абстракций может быть более чем один тип обычного поведения процедур, и в этом случае исключительные ситуации — удобный инструмент. Они предоставляют средства для обеспечения нескольких типов поведения и дают возможность вызывающему процедуру различать между различными случаями.
Например, операция lookup над таблицей символов имеет две цели. Согласно заданному идентификатору, она определяет, обрабатывалось ли уже объявление этого идентификатора и если да, то возвращает информацию об этом идентификаторе. Заголовок для этой операции может быть следующим:
lookup =proc(s: symbol-table, id: string) returns (info) signals (noL in)
Здесь мы решили рассматривать случай, в котором объявление существует как обычный случай, но мы можем легко принять противоположное решение. При принятии решения следует учитывать, в частности, эффективность. Обычно исключительные ситуации требуют больших затрат, чем обычный возврат. (Это верно для языка CLU, хотя различие и мало.) Следовательно, случай, который предположительно будет возникать чаще, нужно рас-
Исключительные ситуации
сматривать как обычный случай. Если решение о том, какой случай будет обычным, принимается из соображений производительности, то очевидно, что с исключительной ситуацией не должно быть связано никаких понятий типа «ошибка».
Стоит поговорить о взаимосвязи модификации аргументов с завершением работы процедуры выходом на исключительную ситуацию. Секция modifies спецификации указывает, что аргумент может модифицироваться, но не говорит о том, когда это происходит. Если имеются исключительные ситуации, то обычно модификации происходят только для каких-нибудь из них. Что в точности происходит, должно быть описано в секции effects. Модификации должны быть описаны явно в каждом случае, в котором они совершаются; если не описано никаких модификаций, это означает, что они не совершаются вообще. Например, рассмотрим процедуру
addel == proc (s: olist [t], x: t) sinals (dupl) modifies s
effects Если x уже принадлежит s, сигнализировать об исключ. ситуации dupl, иначе включить х в s,
Так как для случая, когда процедура addel сигнализирует об исключительной ситуации dupl, не описано никаких модификаций, s модифицируется только при обычном возврате из процедуры addel,
Исключительная ситуация failure является неявной исключительной ситуацией каждой процедуры, однако она не упоминается ни в каких спецификациях. И это вполне оправданно. Спецификация описывает поведение процедуры при ее работе и для случая, когда аргументы удовлетворяют предложению requires. Исключительная ситуация failure возникает тогда, когда в работе процедуры происходит сбой и она больше не отвечает своей спецификации или когда заданы неверные аргументы. .
Обычно исключительная ситуация failure не может быть обработана программой. Эта ситуация означает либо ошибку в математическом обеспечении, либо сбой в аппаратной части. Очень устойчивая программа с анализом всевозможных ситуаций может быть способна заблокировать ошибочную часть и продолжать работать с остальным. В этом случае полезно внести ошибку в протокол, чтобы ее можно было исправить впоследствии. Менее устойчивая программа просто завершит свою работу и внесет ошибку в протокол. (Если программа выполняется в режиме отладки, пользователь у консоли может быть оповещен немедленно.)
Исключительные ситуации, о возникновении которых сигнализируют операции типов данных, связаны со специфическими операциями. Однако имена исключительных ситуаций относятся к типу в целом: либо к статусу объектов типа (например, список
124 Глава 5
может быть пуст или не пуст), либо к тому факту, что такой объект не существует (например, существует наименьшее и наибольшее целое). Следовательно, операции типа должны использовать одинаковые имена исключительных ситуаций для одинаковых случаев. Например, операции для списков first и rest сигнализируют об исключительной ситуации empty, если список, заданный в качестве аргумента, пуст, а операции для целых чисел, результатом которых является слишком большое число, сигнализируют об исключительной ситуации overflow.
Исключительные ситуации предоставляют информацию, которая обычно может быть получена прямым обращением к операциям. Например, о факте пустого списка можно узнать, либо обратившись к операции first и получив ее сигнал, либо обратившись к операции empty. Такая избыточность не необходима. Например, возможен тип, в котором узнать, пуст объект или не пуст, можно только через исключительную ситуацию.
В наших примерах реализаций мы не стремились избегать возникновения исключительных ситуаций; наоборот, мы использовали их для управления работой программ. Исключительные ситуации могут улучшить производительность за счет уменьшения количества вызовов процедур. Например, реализация процедуры choose (рис. 5.4) использует исключительную ситуацию bounds для того, чтобы избежать обращения к операции size для массивов.
5.5. Заключение
В этой главе мы обобщили процедуры, введя исключительные ситуации. Исключительные ситуации необходимы для создания устойчивых программ, так как предоставляют способ реагировать на ошибки. Если аргумент не тот, который ожидается, процедура может оповестить обратившегося к ней об этом факте, вместо того чтобы просто выйти на сбой. Так как это оповещение отличается от обычного случая, обратившийся к процедуре не может их перепутать.
Исключительные ситуации вводятся при разработке процедур. Большинство процедур должно быть задано на всей области определения; исключительные ситуации используются для обработки ситуаций, в которых «обычная» работа программы не может быть осуществлена. Частичные процедуры используются только по соображениям эффективности или когда процедура используется в ограниченном контексте, о котором точно известно, что все обращения имеют корректные аргументы. В любом случае при реализации процедуры полезно практиковать защитное программирование, сигнализируя об исключительной ситуации failure там, где только возможно,' при значениях аргументов, не принадлежащих принятому подмножеству области определения.
Исключительные ситуации
При реализации процедуры программист должен гарантировать, что она во всех ситуациях закончится в соответствии со спецификацией. Следует сигнализировать только об исключительных ситуациях, указанных в спецификации. Сигнализировать следует, кроме того, только по правильным причинам. При создании реализации разумно использовать исключительные ситуации вызываемых процедур и в зависимости от обстоятельств распространять или маскировать их.
Дополнительная литература
Goodenough, John В., 1975. Exception handling: issues and proposed notation. Communications of the ACM 18 (12): 683—696.
Liskov, Barbara, H., Alan Snyder, 1979. Exception handling in CLU. IEEE Transactions on Software Engineering SE-5 (6): 546—558.
Упражнения
5.1. Модифицируйте абстракцию данных poly, определенную в гл. 4 (рис. 4.3), чтобы воспользоваться преимуществами исключительных ситуаций. Специфицируйте новую абстракцию и затем реализуйте ее.
5.2. Модифицируйте абстракцию данных list, определенную в гл. 4 (рис. 4.12), чтобы воспользоваться преимуществами исключительных ситуаций. Специфицируйте новую абстракцию и затем реализуйте ее.
5.3. Реализуйте процедуру remove, dup! в терминах упорядоченных списков, воспользовавшись исключительными ситуациями, о которых оповещают операции типа данных olist (рис. 5.5).
5.4. Упр. 9 гл. 4 связано с абстракцией ограниченных очередей. Переопределите эту абстракцию, используя исключительные ситуации, и реализуйте модифицированную абстракцию.
5.5. Отображение — это таблица, связывающая элементы (некоторого произвольного типа) со строками. Каждая строка отображается максимально в один связанный с ней элемент. Операции над отображением включают в себя create (для создания пустого отображения), insert (для добавления строки и связанного с ней элемента), change (для изменения элемента, связанного со строкой), delete (для удаления строки и связанного с ней элемента) и eval (для нахождения элемента, связанного со строкой). Специфицируйте отображения, используя соответствующие исключительные ситуации, и сконструируйте дополнительные операции, необходимые для полноты. Затем реализуйте вашу спецификацию и обеспечьте инвариант представления и функцию абстракции. Ваша реализация должна быть эффективна: операция eval должна выполняться быстрее, чем за п шагов, где п — число входных точек отображения.
5.6. Обсудите полноту абстракции отображений, определенной в упр. 5. Возможно ли всегда избегать обращений к вызывающим исключительные ситуации операциям над отображениями при помощи, например, обращений к другим операциям над отображениями? Обсудите, правильные ли в этом отношении были приняты решения.
5.7. Процедура для вычисления минимального значения массива может требовать непустой массив. В случае пустого массива эта процедура возвращает наименьшее целое число или сигнализирует об исключительной ситуации. Обсудите, какая альтернатива лучше.
5.8. Предположим, что вместо вычисления минимального значения процедура из упр. 7 складывает элементы массива. Изменит ли это вашу идею о том, какая альтернатива лучше?
6. Абстракция итерации
В этой главе обсуждается наш последний механизм абстрак< ции — абстракция итерации, или, коротко, итератор. Итераторы являются некоторым обобщением итерационных методов, имеющихся в большинстве языков программирования. Они позволяют пользователям выполнять итерации для произвольных типов данных удобным и эффективным способом.
Например, использование некоторого набора обычно состоит в том, чтобы выполнить некоторые действия для каждого его элемента.
for all элементов набора do действие
Такой цикл мог бы быть выполнен полностью для всего набора, например чтобы напечатать все элементы набора. Или мы могли бы искать некоторый элемент, который удовлетворяет некоторому условию, и в этом случае такой цикл может остановиться, как только будет найден требуемый элемент.
Наборы intset, как мы их определили до сих пор, не дают удобного способа для выполнения таких циклов. Например, предположим, что мы хотим вычислить сумму элементов в некотором наборе intset.
setsum == proc (s: intset) returns (int) effects Выдает сумму элементов в s.
Реализация процедуры setsum, показанная на рис. 6.1, иллюстрирует два основных недостатка в нашей абстракции набора intset. Во-первых, для того чтобы выполнить цикл по всем элементам, мы удаляем каждый элемент, возвращенный операцией choose, так что этот элемент не будет снова выбран. Таким образом, на каждой итерации должны быть обращения к двум операциям — choose и delete. Этой неэффективности можно было избежать, если бы заставить операцию choose удалять выбранный элемент. Но мы все равно имеем вторую проблему, которая состоит в том, что итерация по всему набору intset уничтожит этот набор, поскольку удаляются все его элементы. Иногда такое уничтожение бывает приемлемым, но оно не может быть удовлетворительным в общем случае. Хотя мы можем собрать удаленные
Абстракция итерации
ai = array [int] setsum = proc (s: intset) returns (int) a; ai :== ai$new ( ) % Вычислить сумму sum: int := 0 while true do x: int := intset$choose (s) sum :== sum+ x intsel$delete (s, x) ai$addh (a, x) end except when empty: end % Восстановить элементы s i: int :=. I while true do
intset$insert(s,ati]) excepr when bounds: return (sum) end i:-i+ I end end setsum
Рис. 6.1. Реализация процедуры setsum.
элементы и потом их опять вставить в набор, как это делается на рис. 6.1, такой метод является неуклюжим и неэффективным.
Если бы процедура setsum являлась некоторой операцией над набором intset, мы могли бы реализовать ее эффективно при помощи операций над массивами. Однако процедура setsum не имеет реального смысла как некоторая операция над набором intset, она представляется несколько в стороне от концепции набора. Более того, даже если бы мы могли обосновать создание такой операции, то что же делать с другими аналогичными процедурами, которые нам могут понадобиться, например, с такой, как печать всех элементов? Должен быть некоторый способ реализации таких процедур помимо введения типа данных.
Для адекватного решения проблемы итерации нам нужен эффективный доступ ко всем элементам некоторого множества без уничтожения этого множества. Как мы могли бы это сделать для наборов intset? Создать такую операцию el_seg:
el.seg == proc (s: intset) returns (seg [int])
effects Возвращает некоторую последовательность, содержащую все элементы s в некотором произвольном порядке, причем каждый элемент — только один раз.
Если задана эта операция, мы можем реализовать процедуру setsum так, как показано на рис. 6.2. Поскольку операция el_seg не модифицирует свой аргумент, нам больше не надо восстанавливать набор intset после выполнения итерации.
Хотя операция el_seg упрощает использование наборов intset, она неэффективна, особенно когда набор intset большой. Во-первых, имеем две структуры данных — сам набор intset и последовательность. Во-вторых, в случае цикла поиска мы, возможно, выполняем лишнюю работу. В среднем в таком цикле не надо
128 Глава 6
Абстракция итерации
stblim == proc (s: intset) returns (iit) items: sequence [int] :== intset$el_seg(s) i: int :-= I sum: int := 0 while true do sum :== sum+ items [i]
except when bounds: return (srum) end i:=i+ I end end setsum
Рис. 6.2. Реализация процедуры setsum с использованием операции eLseg.
проверять все элементы множества, в котором организуется поиск. Например, если бы мы организовали в наборе intset поиск некоторого отрицательного элемента, то могли остановиться тогда, когда встретили бы первый отрицательный элемент. Однако, мы должны обработать все элементы множества, чтобы построить последовательность.
Некоторой альтернативой для операции eLseg является операция, которая просто выдает массив представления. Однако это решение является очень плохим, поскольку оно уничтожает абстракцию через спецификацию. Мы в действительности внесли операцию down и потеряли локальный контроль над представлением.
Требуется некоторый общий метод итерации, который удобен и эффективен и который сохраняет абстракцию через спецификацию. Итератор обеспечивает эти требования. Он вызывается как 1 процедура, но вместо окончания с выдачей всех результатов цмеет 1 много результатов, которые выдает каждый раз по одному. Порученные элементы могут использоваться в других модулях, которые задают действия, выполняемые для каждого такого элемента. Использующий итератор модуль будет содержать некоторую структуру организации цикла, например, такую:
for each для элемента результата i, выданного итератором А do выполнить над i некоторое действие
Каждый раз, когда итератор выдает некоторый элемент, над этим элементом выполняется тело цикла. Затем управление возвращается в итератор, так что он может выдавать следующий элемент.
Отметим разделение обязанностей в такой форме. Итератор ответствен за получение элемента, а модуль, содержащий цикл, определяет то действие, которое будет над ним выполняться. Итератор может использоваться в различных модулях, которые выполняют разные действия над элементами, и он может быть реализован различными способами, не оказывая влияния на эти модули.
Поскольку итератор выдает элементы по одному, исчезают проблемы пространства и времени, упоминавшиеся ранее. Нам не нужно строить потенциально большую структуру данных, которая содержит такие элементы. Более того, если модуль выполняет некоторый цикл поиска, итератор может быть остановлен, как только будет найден интересующий его элемент.
Как упоминалось ранее, итераторы являются некоторой обобщенной формой методов итерации, имеющихся в большинстве языков программирования, В дополнение к некоторой форме цикла «while» языки программирования обычно имеют некоторую конструкцию «for» для итерации по целым числам. Такая итерация полезна при обработке массивов, которые индексируются, но она неудобна для таких неиндексируемых абстракций, как наборы itset.
(•» 6.1. Спецификация
'. ' Как и другие абстракции, итераторы должны быть определены через спецификации. Форма спецификации итерации аналогична форме для процедуры. Заголовок имеет такой вид:
iname = iter (...) yields (...) signals (...)
Здесь мы используем ключевое слово iter для обозначения абстракции итератора. Итератор может совсем не выдавать объектов на каждой итерации или выдать несколько объектов. Число и тип этих объектов описываются в предложении yields. (Если для каждого yields не выдается ни одного объекта, то предложение yields может быть опущено.) Итератор может не выдавать никаких результатов, когда он заканчивается нормально, но он может заканчиваться по исключительной ситуации с именем и результатами, указанными в предложении signals. Например,
elements = iter (s: intset) yields (int) requires s не модифицируется в теле цикла.
effects Выдает элементы s в некотором произвольно?л порядке, причем каждый элемент только один раз.
Эта операция вполне правдоподобна для набора intset. Отметим, что операция elements не имеет исключительных ситуаций. Если ей будет задан в качестве аргумента пустой набор intset, она просто заканчивается, не выдавая ни одного элемента. Характерно, что использование итераторов исключает проблемы, связанные с заданием некоторых аргументов (таких, как пустой набор intset) для соответствующих процедур (таких, как процедура choose).
Мы требуем, чтобы набор s не модифицировался при использовании цикла. Подобные ограничения типичны для итераторов 5 Дисков Б,, Гатэг Дж.
130 Глава 6
Абстракция итерации
которые работают над такими изменяемыми объектами, как наборы intset, поскольку тогда станет неясно, что такое «элементы набора 8».'Эти ограничения будут еще обсуждаться в разд. 6.4.
6.2. Итераторы языка CLU
Итераторы могут быть реализованы в языке программирования CLU при помощи модуля iter. Итераторы могут также исполь-аовать оператор for, имеющийся в языке CLU.