AlgStr / Библиотека / POSIBNIK / Реализация структур данных
.DOCФункция абстракции
Любая реализация абстракции данных должна определять, как представляются объекты, принадлежащие к этому типу. Выбирая представление, тот, кто занимается реализацией, учитывает связь между объектами представления и абстрактными объектами. Предполагается, что определенные объекты представления будут соответствовать, определенным абстрактным объектам.
Эта взаимосвязь может быть определена функцией, которая называется функцией абстракции. Эта функция отображает объекты представления в абстрактные объекты:
А: rep А
Здесь А обозначает набор абстрактных объектов. Для каждого объекта представления r, А(r) является абстрактным объектом аA, который представляет r.
Рис. 1. Пример функции абстракции.
Функция абстракции – это важнейшая информация о реализации. Она определяет конкретное представление, т.е. то, каким образом объекты представления реализуют абстрактные объекты. Эта функция должна быть обязательно представлена в комментариях реализации. Описывая функцию реализации, мы иногда сталкиваемся с проблемой, связанной с тем, что, если спецификация типа неформальная, область изменения функции абстракции не может быть точно определена. Сейчас мы будем преодолевать эту проблему с помощью неформального описания «типичного» абстрактного объекта.
Для определения функции абстракции прежде всего определяем типичный элемент абстрактного типа. Это дает нам возможность говорить об абстрактных объектах. Затем можем определить функцию абстракции в терминах этого типичного объекта. Например, для наборов intset мы можем дать следующее описание:
% Типичный набор intset есть {х1, ... хn}
Здесь для описания наборов intset используем понятие математического набора, точно так же, как делали это в спецификации intset. Затем пишем
% Функция абстракции есть
% А(r) = {r[i] | low(r) i high(r)},
где {x | p(x)} есть набор всех таких х, что р(х) имеет значение true.
В качестве другого примера рассмотрим полиномы. Мы решаем представлять полином массивом, в котором i-й элемент содержит i-й коэффициент, если этот коэффициент не относится к нулевым коэффициентам справа или слева от значащих членов полинома. Это представление описываем следующим образом:
% Типичный полином есть: c0 + c1x + c2x2+ ...
% Функция абстракции для коэффициента с1 есть
% ci = г [i], если low(r) i high (r)
% = 0 в противном случае
Эта функция абстракции определена только для массивов с неотрицательной нижней границей. Функции абстракции часто имеют заданные области определения, как, например, в рассматриваемом случае.
Функция абстракции, в частности, удобна тем, что устраняет двусмысленности в интерпретации представления. Например, предположим, что с помощью массивов мы реализуем стеки. Мы можем выбирать, каким образом увеличивать массив, когда в стек добавляется новый элемент. Этот наш выбор будет отражен в функции абстракции. Если мы решаем увеличивать старший индекс массива и по этому индексу записывать элемент, то функция абстракции будет следующая:
% Типичный стек – это последовательность [е1,...,еn], где
% еn – элемент со старшим индексом
% Функция абстракции есть
% А(г) = [r[low (r)], ..., r [high (r)]]
Если мы решаем уменьшать младший индекс и по этому индексу записывать элемент, то
% А(r) = [r[high(r)], ..., r[low(r)]]
Заметим, что реализации стеков, о которых мы только что говорили, используют одно и то же представление, которое интерпретируется, однако, по-разному.
Инвариант представления
В ориентированных на работу с типами языках (как, например, в языке CLU) контроль типов гарантирует, что если абстрактный объект типа передается как аргумент операции типа, то этот абстрактный объект представляется объектом типа представления. Во многих случаях, однако, не все объекты представления являются законными представлениями абстрактных объектов. Например, для кластера intset каждый массив независимо от значений является элементом R; например, массивы [1: 1, 7,6], [1: 1, 6, 6] и [1: 6, 1, 7]. Однако в реализации intset мы решили, что каждый элемент набора записывается в массив только один раз. Следовательно, законные представления наборов intset в этом кластере не содержат дублирующих друг друга элементов; [1: 1, 6, 6], например — незаконное представление.
Условие, которому удовлетворяют все законные объекты, называется инвариантом представления. Инвариант представления I есть предикат
I: гер bool,
который принимает значение true для законных объектов представления. Например, для кластера intset мы можем дать следующий инвариант представления:
% Инвариант представления есть
% Для всех целых i, j, таких, что low(r) i < j high(r)
% r[i] ~ = r[j]
Заметим, что для массива [1: 1, 6, 6] I принимает значение false, а для массивов [1: 1, 7, 6] и [1: 6, 1, 7] – значение true. Заметим также, что мы не включаем в I условия, которые принимают значение true для всех массивов (например, size(r) = high(r) - low(r) + 1), потому что это гарантируется процедурами работы с массивами и соответственно предполагается в реализации intset.
В качестве второго примера инварианта представления рассмотрим альтернативное представление наборов intset, состоящее из булева массива размерности 100 и массива целых:
гер = record [els: array [bool], other_els: array [int], size: int]
Идея заключается в том, что для целых от 1 до 100 мы отмечаем их принадлежность набору, записывая значение true в r.els [i]. Не принадлежащие этому диапазону целые записываются в other_els точно так же, как в нашей предыдущей реализации intset. Так как невыгодно вычислять размер набора intset, просматривая все элементы массива els, мы записываем этот размер в представление в явном виде. Это представление хорошо, если почти все члены набора принадлежат диапазону 1 – 100. (В противном случае пространство, требующееся для массива els, будет использоваться неэффективно.) Таким образом, мы имеем .
% Функция абстракции есть
% А (г) = {r.other.els [i] | low (r.other_els) i high (r.other_els)}
%
%{j | 1 j 100 & r.els[j]}
Здесь обозначает математическое объединение двух наборов. Итак,
% Инвариант представления есть
% size (r.els) = 100 & low (r.els) = 1
% & все элементы r.other не принадлежат диапазону 1- 100
% & в r.other нет дубликатов элементов
% & r.size = size (r.other_els) +
% (число элем. со знач. true в r.els)
Заметим, что поле size в этом представлении излишне: оно содержит информацию, которая может быть получена из остальной части представления путем вычислений. Но, поскольку такая излишняя информация все же содержится в представлении, взаимосвязь этой информации с остальной частью представления должна быть объяснена в инварианте представления (например, последняя строка нашего инварианта представления).
Иногда удобно использовать в инварианте представления или в функции абстракции вспомогательную функцию. Например, последняя строка инварианта представления может быть переписана следующим образом:
% r.size = size (r.other_els) + cnt (r.els, low(r.els))
% где cnt (a, i) = if i > high(a) then 0
% else if r[i] then 1 + cnt (a, i + 1)
% else cnt (a, i + 1)
Вспомогательная функция cnt определена здесь рекурсивно. Интересный инвариант представления имеет реализация полиномов poly. Вспомним, что мы решили не записывать нулевые коэффициенты справа и слева от значащих членов полинома и представлять нулевой полином пустым массивом. Следовательно, мы предполагаем, что старший и младший элементы массива – не нули. Кроме того, эти массивы всегда имеют неотрицательную нижнюю границу. И наконец, пустой массив (представляющий пустой полином) должен иметь верхнюю границу, равную нулю. Таким образом, мы имеем
% Инвариант представления есть
% low (r) 0
% & if empty(r) then high (r) = 0
% else r [low (r) ] ~ = 0 & r [high (r) ] ~ = 0
Вспомним, что реализация операции degree предполагает, что верхняя граница массива равна степени полинома. Теперь мы видим это требование в инварианте представления.
Иногда все объекты представления законны. Тогда мы просто имеем
% Инвариант представления есть
% true
Инвариант представления должен быть явно задан также и в этом случае. Он освобождает осуществляющего реализацию от возможной зависимости от какого-то более сильного инварианта.
Инвариант представления – это «инвариант» потому, что он всегда сохраняется для представлений абстрактных объектов, т.е. он сохраняется, если объект используется вне его реализации. Инвариант представления не должен, однако, обязательно сохраняться всегда: он может быть нарушен при выполнении одной из операций типа. Например, операция poly$add может получить массив с нулевым старшим и младшим элементом, но такие элементы удаляются из массива перед возвратом из операции add. Инвариант представления должен сохраняться при возврате из операций.
Между функцией абстракции и инвариантом представления имеется определенная взаимосвязь. Функций абстракции имеет смысл только для законных представлений, так как только они представляют абстрактные объекты. Следовательно, нет необходимости определять ее для незаконных представлений. Например, как мы упоминали раньше, функция абстракции для полиномов имеет некую область определения – она определена только в случае, если массив имеет неотрицательную нижнюю границу. Этому требованию удовлетворяют все законные представления полиномов, что отражено в инварианте представления.
Существует соглашение о том, что должно быть отражено в инварианте представления. Инвариант представления должен отражать все требования, от которых зависят операции, но может не отражать дополнительные требования. Хорошо представлять себе, что операции реализуют различные люди, которые не общаются друг с другом. Тогда понятно, что инвариант представления должен содержать все требования, которые необходимо знать этим различным людям.
При реализации абстракции данных инвариант представления — одна из первых вещей, о которых должен подумать программист. Он должен быть выбран до реализации операций, в противном случае различные реализации не будут согласованы друг с другом. Чтобы гарантировать согласованность, инвариант представления должен быть написан и включен в программу как комментарий. Написание инварианта представления вынуждает того, кто занимается реализацией, ясно выразить то, что он хочет, и увеличивает тем самым вероятность того, что операции будут реализованы корректно.
Все операции должны реализовываться с учетом инварианта представления. Например, предположим, что мы реализуем операцию insert так:
insert = proc(s: cvt, x: int)
rep$addh (s, x)
end insert
Эта реализация может создать объект с дублирующимися элементами. Если мы знаем, что инвариант представления запрещает такие объекты, то эта реализация очевидно некорректна.
Инвариант представления также полезен для того, кто изучает реализацию. Например, в альтернативной реализации набора intset мы можем решить подвергать массив представления сортировке. В этом случае мы будем иметь
% Инвариант представления есть
% Для всех i, j, таких, что low (г) i < j high (г)
% r[i] < r[j]
Инвариант представления говорит тому, кто изучает реализацию, почему операции реализованы именно так, как они реализованы.
Сохранение инварианта представления
Для того чтобы продемонстрировать корректность реализации типа, мы должны, в частности, показать, что инвариант представления сохраняется для всех законных объектов представления. Мы можем сделать это следующим образом. Прежде всего мы покажем, что инвариант сохраняется для объектов, возвращаемых операциями (как, например, операцией poly$create), которые возвращают объект типа, но не имеют аргументов типа. Для всех других операций мы можем предположить, что при их вызове инвариант сохраняется для всех аргументов, являющихся объектами типа; тогда мы должны показать, что он сохраняется при возврате как для аргументов типа, так и для возвращаемых объектов типа.
Например, реализация набора intset имеет инвариант
% Для всех целых i, j, таких, что low (г) i < j high (г)
%r[i] ~ = r[j]
Операция intset$create отвечает этому инварианту потому, что заново созданный массив пуст. Операция member также сохраняет этот инвариант, так как мы знаем, что инвариант сохраняется для аргумента s и что эта операция не модифицирует s. Операция insert также сохраняет инвариант, так как
-
инвариант сохраняется для ее аргумента s при обращении;
-
при вызове операции member из операции insert инвариант сохраняется потому, что он сохраняется в операции member; и
-
операция insert добавляет х к s, только если выражение member (s, х). имеет значение true; следовательно, так как набор s удовлетворяет инварианту при вызове, он также удовлетворяет инварианту после добавления к нему х.
В качестве второго примера рассмотрим реализацию полиномов poly. Вспомним, что инвариант есть
% low (r) 0
% & if empty (s) then high (r) = 0
% else r[low (r)l ~ = 0 & r[high (r)] ~ = 0
Операция poly$create сохраняет инвариант потому, что она создает нулевой полином. Операция mul сохраняет инвариант потому, что
-
при вызове инвариант сохраняется для р и q;
-
случай, когда р или q — нулевой полином, распознается и создается соответствующее представление; и
-
в противном случае ни р, ни дне содержат нулей ни в старшем, ни в младшем члене; следовательно, ни младший элемент возвращаемого массива (bottom (р) * bottom (q)), ни старший элемент (top (р) * top (q)) не может быть нулем.
Здесь мы предполагаем, что абстрактные объекты нельзя модифицировать вне их реализации. Если это возможно, то мы не можем предполагать, что инвариант сохраняется при вызове операции, так как какие-то модули могут его изменить.
Изменяемые представления
Изменяемость (или неизменяемость) — это свойство абстракции, которое должно сохраняться реализацией. Изменяемые абстракции должны иметь изменяемые представления, иначе будет невозможно обеспечить требуемую изменяемость. Однако неизменяемые абстракции не обязательно должны иметь неизменяемые представления. Например, полиномы неизменяемы, но имеют изменяемое представление. Изменяемые представления приемлемы, если модификации, сделанные в представлении, не могут быть замечены пользователями абстракций.
Иногда полезно создавать объект, изменяя его представление, однако, когда объект создан, его представление больше не модифицируется. Так, например, созданы полиномы. Изменяемость также полезна для благоприятных побочных эффектов, являющихся модификациями, которые не видны вне реализации. Например, давайте представим рациональное число парой целых:
rep = record [num, denom: int]
Функция абстракции есть
% типичное рациональное число есть n/d
% Функция абстракции есть
% А (r) == r.num / r.denom
Исходя из этого представления, мы должны принять несколько решений: что делать с нулевым знаменателем, как хранить отрицательные рациональные числа и хранить ли рациональные числа в сокращенной форме (т.е. когда числитель и знаменатель не имеют общего делителя). Мы решаем: исключить нулевой знаменатель, представляй отрицательные рациональные числа с помощью отрицательного числителя и не хранить представления в сокращенной форме (для ускорения таких операций, как умножение). Таким образом, имеем
% Инвариант представления есть
% r.denom > 0
Давайте также решим находить сокращенную форму при проверке двух рациональных чисел на равенство. Это мы можем сделать, используя процедуру gcd.
gcd = proc (n, d: int) returns (int)
requires n и d должны быть положительными
effects Возвращает макс. общий делитель n и d. ,
Реализация операции equal представлена на рис.2. Однажды найденные, сокращенные формы записываются в представление, так как это ускоряет работу при последующих обращениях к операции equal.
rep = record [num, denom: int] equal = proc (rl,. r2: cvt) returns (bool)
if rl.num = 0 then return (r2.num = 0)
elseif r2.num = 0 then return (false)
end
reduce (rl)
reduce (r2)
return (rl.num = r2.num cand rl.denom = r2.denom)
end equal
% reduce — внутренняя программа, приводящая ее аргумент к сокращенной
% форме
reduce proc (r: гер)
g: int := gcd (int$abs (r.num), r.denom)
r.num := r.num/g
r.denom := r.denom/g
end reduce
Рис. 2. Благоприятный побочный эффект.
Модификация представления, осуществляемая операцией equal, — благоприятный побочный эффект. Такие побочные эффекты часто используются для улучшения эффективности. Они возможны, если функция абстракции отображает различные элементы в один, так как тогда имеется много объектов представления, которые представляют один и тот же абстрактный объект. Иногда полезно внутри реализации переходить от одного такого объекта представления к другому. Такой переход законен, так как новое представление соответствует тому же самому абстрактному объекту.