AlgStr / Библиотека / ЛЕКЦИИ / PZ00 / Тищенко Андрей
.doc
Вопрос № 16 :
Реализация абстракции данных. Функция инвариант представления. Сохранения инварианта представления.
Для реализации типа данных мы выбираем представление для объектов и реализуем операции в терминах этого представления. Выбранное представление должно предоставлять возможности для довольно простой и эффективной реализации всех операций. Кроме того, если некоторые операции должны выполняться быстро, представление должно предоставлять и эту возможность. Часто представление, обеспечивающее быструю работу некоторых операций, приводит к тому, что другие операции выполняются медленно. В этом случае мы должны использовать несколько различных реализаций одного и того же типа.
Например, возможное представление для объекта intset — это массив целых чисел, где каждое целое число набора intset соответствует элементу массива. Мы должны решить — должен ли каждый элемент набора встречаться в массиве только один раз или же он может встречаться много раз. В последнем случае операция insert будет работать быстрее, однако операции delete и member будут выполняться медленнее. Если операция member используется часто, мы должны остановиться на первом случае.
Заметим, что здесь мы говорим о двух разных типах: новом абстрактном типе intset, который мы реализуем, и массиве целых, который используется как представление. Каждая реализация будет иметь два таких типа: абстрактный тип и тип представления (rep type). Предполагается, что с типом представления мы имеем дело только при реализации. Все, что мы можем делать с объектами абстрактного типа, — это применять к ним соответствующие данному типу операции. Соблюдение этого ограничения в языке CLU осуществляется с помощью контроля типов. Язык CLU удобен, поскольку в нем тип может быть реализован как отдельный программный модуль. Эта программная единица языка CLU называется кластер. Отсюда и название (CLU от слова CLUster.)
Внутри кластера должны быть доступны как абстрактный тип, так и тип представления. Более того, должна иметься возможность перехода от одного типа к другому. Например, операция insert получает в качестве аргумента набор целых чисел, но для реализации insert необходимо использовать массив, который представляет этот набор.
Возможность перехода от абстрактного типа к типу представления и обратно в языке CLU обеспечивают две специальные операции: up и down. Операция up использует в качестве аргумента объект представления и в качестве результата выдает абстрактный объект, а операция down осуществляет обратное действие. Для того чтобы обеспечить преобразование абстрактного типа в тип представления и обратно, каждый кластер имеет свою собственную версию операций up и down. Эти операции автоматически определяются компилятором CLU. Например, в кластере процедуры intset компилятор порождает операции up и down со следующими заголовками:
up = proc (a: array [int]) returns (intset) down = proc (s: intset) returns (array [int])
Операции up и down могут использоваться только в кластерах и всегда осуществляют переход от абстрактного типа к типу представления и обратно только для кластера, в котором они появились. Поэтому операции up и down не могут отрицательно повлиять на контроль типов языка CLU.
В ориентированных на работу с типами языках (как, например, в языке CLU) контроль типов гарантирует, что если абстрактный объект типа передается как аргумент операции типа, то этот абстрактный объект представляется объектом типа представления. Во многих случаях, однако, не все объекты представления являются законными представлениями абстрактных объектов. Например, для кластера intset имеем массивы
[1: 1, 7,6], [1: 1, 6, 6] и [1:6, 1,7]. Однако в реализации intset мы решили, что каждый элемент набора записывается в массив только один раз.
Следовательно, законные представления наборов intset в этом кластере не содержат дублирующих друг друга элементов; [1: 1, 6, 6], например — незаконное представление.
Условие, которому удовлетворяют все законные объекты, называется инвариантом представления. Инвариант представления 1 есть предикат1: ( rep bool. ) который принимает значение true для законных объектов представления. Например, для кластера intset мы можем дать следующий инвариант представления:
Функция ИНВАРИАНТ ПРЕДСТАВЛЕНИЯ есть
( Для всех целых i и j таких, что
low (r)<= i < j <= high (r):(r[i] <> r[j]) )
Заметим, что для массива II:[ 1, 6, 6] 1 принимает значение false, а для массивов [1: 1, 7, 6] и [1: 6, 1, 7] — значение true. Заметим также, что мы не включаем в 1 условия, которые принимают значение true для всех массивов (например, size (r) = high (r) — low (r) + 1), потому что это гарантируется процедурами работы с массивами и соответственно предполагается в реализации intset.
В качестве второго примера инварианта представления рассмотрим альтернативное представление наборов intset, состоящее из булева массива размерности 100 и массива целых:
гер = record leis: array [bool], other.els: array [int], size: int
Идея заключается в том, что для целых от 1 до 100 мы отмечаем их принадлежность набору, записывая значение true в r.els [i] . Не принадлежащие этому диапазону целые записываются в other.els точно так же, как в нашей предыдущей реализации intset. Так как невыгодно вычислять размер набора intset, просматривая все элементы массива els, мы записываем этот размер в представление в явном виде. Это представление хорошо, если почти все члены набора принадлежат диапазону 1—100. (В противном случае пространство, требующееся для массива els, будет использоваться неэффективно.) Таким образом, мы имеем
Функция абстракции есть : А (r) = {i-.other-els [i ] I low (r. other- els) i s$high (r.other, els)}
Инвариант представления есть size (r.els) = 100 & low (r.els) = I & все элементы r.other не принадлежат диапазону I—100 & в r.other нет дубликатов элементов & r.size = size (r.other, els) - (число элементов со значением true в r.els)
Заметим, что поле size в этом представлении излишне: оно содержит информацию, которая может быть получена из остальной части представления путем вычислений. Но, поскольку такая излишняя информация все же содержится в представлении, взаимосвязь этой информации с остальной частью представления должна быть объяснена в инварианте представления (например, последняя строка нашего инварианта представления).
Иногда удобно использовать в инварианте представления или в функции абстракции вспомогательную функцию. Например, последняя строка инварианта представления может быть переписана следующим образом:
r.size= size (r.othei_els)+ cnt (r.els, low (т.els)) где cnt (a, i) = if i > high (a) then 0
else if r [i] then I+cnt (a, i+1) else cnt (a, i + 1)
Вспомогательная функция cnt определена здесь рекурсивно.
Инвариант представления должен быть явно задан также и в этом случае. Он освобождает осуществляющего реализацию от возможной зависимости от какого-то более сильного инварианта.
Инвариант представления — это «инвариант» потому, что он всегда сохраняется для представлений абстрактных объектов, т. е. он сохраняется, если объект используется вне его реализации. Инвариант представления не должен, однако, обязательно сохраняться всегда: он может быть нарушен при выполнении одной из операций типа. Инвариант представления должен сохраняться при возврате из операций.
Между функцией абстракции и инвариантом представления имеется определенная взаимосвязь. Функция абстракции имеет смысл только для законных представлений, так как только они представляют абстрактные объекты. Следовательно, нет необходимости определять ее для незаконных представлений. Существует соглашение о том, что должно быть отражено в инварианте представления.
Инвариант представления должен отражать все требования, от которых зависят операции, но может не отражать дополнительные требования. Хорошо представлять себе, что операции реализуют различные люди, которые не общаются друг с другом. Тогда понятно, что инвариант представления должен содержать все требования, которые необходимо знать этим различным людям.
При реализации абстракции данных инвариант представления — одна из первых вещей, о которых должен подумать программист. Он должен быть выбран до реализации операций, в противном случае различные реализации не будут согласованы друг с другом. Чтобы гарантировать согласованность, инвариант представления должен быть написан и включен в программу как комментарий. Написание инварианта представления вынуждает того, кто занимается реализацией, ясно выразить то, что он хочет, и увеличивает тем самым вероятность того, что операции будут реализованы корректно.
Все операции должны реализовываться с учетом инварианта представления. Например, предположим, что мы реализуем операцию insert так:
insert == proc (s: cvt, x: inl) rep$addh (s, x) end insert
Эта реализация может создать объект с дублирующимися элементами. Если мы знаем, что инвариант представления запрещает такие объекты, то эта реализация очевидно некорректна. Инвариант представления также полезен для того, кто изучает реализацию. Например, в альтернативной реализации набора intset мы можем решить подвергать массив представления сортировке. В этом случае мы будем иметь:
Инвариант представления есть: для всех i, j, таких, что low (r) < i < j < high (r): r [i] < r [j]
Все операции тогда будут реализовываться иначе. Инвариант представления говорит тому, кто изучает реализацию, почему операции реализованы именно так, как они реализованы.
Для того чтобы продемонстрировать корректность реализации типа, мы должны, в частности, показать, что инвариант представления сохраняется для всех законных объектов представления. Мы можем сделать это следующим образом. Прежде всего мы покажем, что инвариант сохраняется для объектов, возвращаемых операциями, которые возвращают объект типа, но не имеют аргументов типа. Для всех других операций мы можем предположить, что при их вызове инвариант сохраняется для всех аргументов, являющихся объектами типа; тогда мы должны показать, что он сохраняется при возврате как для аргументов типа, так и для возвращаемых объектов типа.
Например, реализация набора intset имеет инвариант
для всех целых i, j, таких, что low (r) < i < j <high (r) : r [i] <> r [j]
Операция intset$create отвечает этому инварианту потому, что заново созданный массив пуст. Операция member также сохраняет этот инвариант, так как мы знаем, что инвариант сохраняется для аргумента s и что эта операция не модифицирует s. Операция insert также сохраняет инвариант, так как
1) инвариант сохраняется для ее аргумента s при обращении;
2) при вызове операции member из операции insert инвариант сохраняется потому, что он сохраняется в операции member;
3) операция insert добавляет х к s, только если выражение member (s, х) имеет значение true; следовательно, так как набор s удовлетворяет инварианту при вызове, он также удовлетворяет инварианту после добавления к нему х.