Сообщений 5    Оценка 131 [+0/-1]         Оценить  
Система Orphus

Абстрактные типы данных в программировании

Автор: Лапшин Владимир Анатольевич
Введение
Использование АТД в языках программирования
АТД как алгебраические системы
Языки алгебраических спецификаций
Заключение
Список литературы

Введение

В данной работе рассматривается концепция абстрактных типов данных (АТД). Со времени формулирования этой концепции в 1974 году [1] абстрактные типы данных играют важную роль в теории программирования. Концепция АТД является, наряду с объектно-ориентированным подходом, наиболее популярной в настоящее время методологией для составления программ. В процессе декомпозиции программы на составляющие компоненты доступ к ним организуется посредством т.н. кластера операций, который представляет собой конечный список операций, которые могут быть использованы для модификации данных, предоставляемых данным компонентом.

Отличительной особенностью абстрактных типов данных как механизма абстракции является тот факт, что функциональность компонента программы, описываемая кластером операций, может быть реализована различными способами. Различные реализации абстрактных типов данных взаимозаменяемы благодаря механизму абстракции АТД, позволяющему скрыть детали реализации с помощью набора предопределенных операций.

Концепция абстрактных типов данных хорошо описывается с помощью математической теории алгебраических систем [2]. Алгебраическую систему или, проще говоря, алгебру (абстрактную алгебру), неформально можно определить как множество с набором операций, действующих на элементах данного множества. Операции реализуются как функции от одного или более параметров, действующие на элементах данного множества (для операции с одним аргументом) или на декартовых произведениях множества (для операций с несколькими аргументами). Описание операций, включающее в себя описание типов аргументов (рассуждение имеет смысл только для статически типизированных языков – прим.ред.) и возвращаемых значений, называется сигнатурой алгебраической системы. Сигнатуры, очевидно, представляют математическую модель абстрактного типа данных. Это обстоятельство дает возможность описывать программные сущности, заданные посредством АТД, как алгебраические системы. Этот подход будет подробнее рассмотрен ниже.

Типы данных впервые были описаны Д. Кнутом в его книге «Искусство программирования» [3]. В главе 2, «Информационные структуры», Кнут описывает т.н. структуры данных, определяемые как способы организации данных внутри программы. Кнут описывает такие типы данных, как списки, деревья, стеки, очереди, деки и т.д. Рассмотрим, например, как Кнут описывает тип данных «стек».

Кроме собственно описания самой структуры данных, Кнут описывает «алгоритмы обработки» этой структуры с помощью словаря специальных терминов [3, стр. 281]. Для стека этот словарь содержит термины: push (втолкнуть), pop (вытолкнуть) и top (верхний элемент стека). Таким образом, типы данных описываются Кнутом с помощью специального языка, задающего определенную терминологию и толкование этой терминологии. Эта особенность описания была замечена Стивеном Жиллем в работе [4] и, таким образом, явилась одним из побудительных мотивов для осознания важности концепции АТД.

В 1972 году была напечатана работа Дэвида Парнаса (David Parnas) [5], в которой впервые был сформулирован принцип разделения программы на модули. Модули – это компоненты программы, которые имеют два важных свойства:

  1. Модули скрывают в себе детали реализации.
  2. Модули могут быть повторно использованы в различных частях программы.

Парнас представлял модули как абстрактные машины, хранящие внутри себя состояния и позволяющие изменять это состояние посредством определенного набора операций. Эта концепция является базовой, как для концепции абстрактных типов данных, так и для объектно-ориентированного программирования.

Понятие абстрактного типа данных впервые в явном виде было сформулировано в совместной работе Стивена Жиля и Барбары Лисков [1]. В разделе «Смысл понятия абстракции» авторы обсуждают, каким образом понятие абстракции может быть применимо к программному коду. Абстракция – это способ отвлечься от неважных деталей и, таким образом, выбрать наиболее важные признаки для рассмотрения. В процессе создания программы разработчик строит программную модель решаемой задачи. В процессе построения программной модели разработчик оперирует элементами этой модели. Программный код структурируется соответствующим образом. Для выделения программных сущностей в коде программы естественно использовать механизм абстракции. В работе Жиля и Лисков рассматривался механизм т.н. поведенческой абстракции или, в терминологии авторов, функциональной абстракции.

Функциональная абстракция подразумевает выделение набора операций (функций) для работы с элементами программной модели. Таким образом, сущности программной модели представляются с помощью набора операций. Так осуществляется поведенческая абстракция сущности в программном коде. Сами авторы использовали термин «operational cluster», т.е. набор операций, и назвали такой набор операций абстрактным типом данных (АТД).

Рассмотрим определение абстрактного типа данных на языке, синтаксис которого унаследован преимущественно от языка PASCAL (детали можно найти в оригинальной работе). Этот язык поддерживает абстракцию модулей, доступ к которым задается посредством набора операций. Итак, определение типа «стек».

stack: cluster(element_type: type) is push, pop, top, erasetop, empty;

  rep(type_param: type) = (tp: integer; e_type: type; stk: array[1..] of type_param;

  create
    s: rep(element_type);
    s.tp := 0;
    s.e_type := element_type;
    return s;
  end

  push: operation(s: rep, v: s.e_type);
    s.tp := s.tp+1;
    s.stk[s.tp] := v;
    return;
  end

  pop: operation(s: rep) returns s.e_type;
    if s.tp = 0 then error;
    s.tp := s.tp-1;
    return s.stk[s.tp];
  end

  top: operation(s: rep) returns s.e_type;
    if s.tp = 0 then error;
    return s.stk[s.tp];
  end

  erasetop: operation(s: rep);
    if s.tp = 0 then error;
    s.tp := s.tp-1;
    return;
  end

  empty: operation(s: rep) returnsboolean;
    return s.tp = 0;
  endend stack

Здесь задается модуль с именем «stack» и для него определяется набор (cluster) операций: push, pop, top, erasetop, empty. От привычного всем программистам нашего времени набора операций со стеком этот кластер отличается только наличием операции erasetop, которая аналогична операции pop, но не возвращает значения. В библиотеке STL языка C++ операция pop шаблонного класса stack определена как erasetop, т.е. не возвращает удаляемого значения. Таким образом, это определение, сделанное в 1974 году, практически аналогично используемым сейчас определениям типа стек.

Для представления типа содержимого стека используется параметрический тип rep. Название унаследовано от термина «object representation» (представление объекта). По идее авторов, пользователи не имеют доступа к внутренним «объектам» модуля, но работают только с его «представлением», т.е. с типом rep. В данном случае rep является представлением внутренней реализации объектов «стека». Для каждого объекта стека фиксируется тип его элементов (e_type), поддерживается счетчик числа элементов в стеке (tp) и хранилище элементов в виде массива (stk).

Заметим, что процедура create не входит в набор операций АТД stack. Это специальная операция, используемая исключительно для целей конструирования объектов абстрактного типа данных stack. Таким операции в теории АТД называются конструкторами. Чуть более строго определим терминологию, для чего опишем операции в математической нотации:

create: -> Stack[Elem]
push: Stack[Elem] x Elem -> Stack[Elem]
pop: Stack[Elem] -> Elem
top: Stack[Elem] -> Elem
erasetop: Stack[Elem] -> Stack[Elem]
empty: Stack[Elem] -> Bool

Операции без аргументов, возвращающие объекты стека, как это делает create, называются конструкторами АТД. В теории алгебраических систем такие операции называют еще константами. Операции вида push и erasetop, возвращающие в качестве результата объекты стека, называют командами. Операции, в которых справа от стрелки не встречается представление АТД, как pop, top и empty, называют запросами.

Таким образом, согласно классическому определению, абстрактный тип данных представляется как кластер операций. Операции получают и возвращают аргументы определенных типов, поэтому для полноценного описания АТД необходимо еще эти типы определить. В большинстве случаев этого вполне достаточно, но можно было бы описать еще условия, которым должны удовлетворять все правильные реализации данного абстрактного типа данных. Эти условия легко можно сформировать в виде композиции операций, применяемых к объектам стека. Например, совершенно ясно, что вставив элемент в стек с помощью операции push, а затем, взяв элемент с вершины стека с помощью операции top, мы должны получить тот же элемент. Это утверждение можно сформулировать следующим образом:

top(push(s,x))=x

Аналогичным образом можно сформулировать другие условия:

pop(push(s,x))=x
empty(create())=true
not empty(push(s,x))=true

Все эти утверждения сформулированы в виде аксиом специального вида, т.н. аксиом в виде равенств. Кроме аксиом-равенств для абстрактных типов данных можно вводить еще т.н. предусловия, которые должны выполняться при применении операций к АТД. Для стека можно было бы ввести два таких предусловия, которые можно увидеть выше:

pop(s) require not empty(s)
top(s) require not empty(s)

Подобное условие можно было бы ввести и для операции erasetop, но мы здесь этого делать не будем, чтобы не отступать от используемых в наше время определений.

Таким образом, абстрактный тип данных можно определить как четверку (типы, операции, аксиомы-равенства, предусловия). Подобные определения используются в языках алгебраических спецификаций, о которых будет рассказано чуть подробнее в одном из разделов ниже.

В следующем разделе на некоторых примерах будет показано, как концепция АТД используется в современных языках программирования.

Использование АТД в языках программирования

В большинстве современных императивных языков основной концепцией, используемой для описания абстракций в программном коде, является объектно-ориентированный подход. Объектно-ориентированное программирование (ООП) также, как и подход к программированию на основе АТД, является, в некоторой степени, развитием тех идей о модульном программировании, которые были заложены в упоминавшейся выше работе Дэвида Парнаса [4].

В ООП результатом абстракции являются объекты, представляющие собой абстрактные машины (устройства), обладающие внутренним состоянием, которое можно изменять посылкой сообщений через методы объекта. Объекты обмениваются сообщениями друг с другом и, таким образом, реализуется алгоритм программной модели. В этом смысле объект есть почти то, что подразумевал Парнас в своей работе, когда рассуждал о модульном программировании.

Одно из наиболее значимых отличий АТД от ООП заключается в том, что абстрактный тип не содержит данных и, тем более, каких либо состояний этих данных. Свойство абстракции АТД как раз и заключается в том, что абстрактный тип представляет собой поведенческую абстракцию, т.е. кластер операций, посредством которых производится взаимодействие с данными. Поведенческая абстракция целиком и полностью описывается спецификацией абстрактного типа данных. Реализация АТД должна соответствовать спецификации данного абстрактного типа, в этом и заключается абстракция на основе АТД. Подробнее о различиях концепций АТД и ООП можно прочитать в работе [6].

На практике, в языках программирования, которые поддерживают концепцию ООП, часто также реализуется и концепция АТД. Чтобы, согласно "бритве Оккама", не «множить сущее без необходимости», для реализации АТД используются средства ООП. Для этого абстрактный тип определяют как класс с определенным интерфейсом. После чего в качестве реализаций данного абстрактного типа выступают его объекты-наследники (в смысле наследования ООП). Работа с реализациями АТД ведется через интерфейс базового объекта, т.е. через операции данного АТД. Для этого обычно используется принцип подстановки Лисков [7], согласно которому объекты-наследники можно подставлять в аргументы, имеющие базовый тип.

Рассмотрим в качестве примера, как АТД реализуются в языке программирования C++. C++ является языком, поддерживающим ООП на основе т.н. механизма классов. Абстрактный тип данных определяется как класс, предоставляющий т.н. «чисто виртуальные методы». Для иллюстрации рассмотрим пример реализации АТД стека целочисленных значений на C++:

      class Stack 
{
  public:
    virtualvoid push(unsigned int elem) = 0;
    virtualunsignedint top() = 0;
    virtualvoid pop() = 0;
    virtualbool empty() = 0;
};

Здесь, ввиду особенностей реализации объектов на C++, объекты АТД Stack передаются в операции и возвращаются из них неявно, в виде скрытых параметров. Поэтому аргументы операций почти ничем не отличаются от тех, что описаны в спецификации АТД "стек" выше.

В языке C++ любой класс, объявляющий чисто виртуальный метод (т.е. приравненный нулю при объявлении), называется «абстрактным типом». Это, конечно, еще не абстрактный тип данных, но уже нечто методологически на него похожее. Экземпляры абстрактных типов создавать нельзя. Таким образом, абстрактные типы используются в C++ для определения интерфейсов (кластеров операций), т.е. реализуют в несколько усеченном виде концепцию АТД.

Для задания реализации абстрактных типов в языке C++ используется механизм наследования и переопределения, объявленных в базовом классе методов-операций. Вот, например, как будет выглядеть реализация стека на основе массива:

      class ArrayStack : public Stack 
{
  // Массив реализуется на основе типа std::vector стандартной библиотеки C++
  std::vector<unsigned int> array_;
  public:
    void push(unsigned int elem) 
{
      array_.push_back(elem);
    }

  unsigned int top() 
{
    if (array_.empty()) 
{
      throw std::invalid_argument(“the stack is empty”);
    }
    return array_.back();
  }

  void pop() 
{
    if (array_.empty()) 
{
      throw std::invalid_argument(“the stack is empty”);
    }
    array_.pop_back();
  }

  bool empty() 
{
    return array_.empty();
  }
};

В реализации используется тип std::vector стандартной библиотеки языка C++. Это параметризованный тип, реализующий концепцию «динамического массива значений». Тип предоставляет методы для вставки/удаления значений в конец массива. Также имеются методы для доступа к последнему элементу массива и проверки массива на непустоту. При невыполнении предусловий на операции pop и top генерируется исключение.

В языке C++ принцип подстановки Лисков реализуется на типах «указателей на объекты классов» и на ссылках. Указатель представляет собой адрес объекта данного типа в памяти программы, но также является специфическим типов, с которым можно производить определенные операции. В частности, указатели на объекты классов-наследников можно приводить к указателям на их базовые классы. Рассмотрим на примере, каким образом это делается.

Для этого определим функцию, проверяющую одну из аксиом-равенств АТД "стек", а именно, аксиому

top(push(s,x))=x

Функция будет выглядеть так:

      bool CheckFirstAxiom(Stack* stack) 
{
  stack->push(123);
  if (stack.top() != 123) 
    returnfalse;
  
  returntrue;
}

Данная функция получает в качестве параметра указатель на абстрактный тип Stack. В качестве этого параметра можно подставлять его реализации в класса-наследниках. Например:

ArrayStack stack_impl;
if (CheckFirstAxiom(&stack_impl)) 
  std::cout << “first axiom works!\n”;
else 
  std::cerr << “first axiom does not works.\n”;

Здесь работает принцип подстановки Лисков, т.е. вместо типа Stack* был подставлен тип ArrayStack*. Кроме того, благодаря тому, что методы базового класса объявлены как виртуальные, производится вызов методов класса-наследника ArrayStack, т.е. работает реализация АТД Stack, выполненная на основе массива.

В описанном примере используется встроенная в язык C++ возможность реализации абстрактных типов данных. При этом реализации АТД подставляются динамически, во время исполнения программы. Однако, в C++ также можно реализовать концепцию АТД по другому, используя механизм шаблонов и, таким образом, связывая АТД с его реализацией на этапе компиляции.

Механизм, реализующий эту возможность, называется в C++ «сuriously recurring template pattern» (CRTP) [8]. Для реализации статического (на этапе компиляции) полиморфизма эта концепция выглядит следующим образом:

  1. Создается шаблонный класс B, который берет в качестве параметра шаблона некоторый тип T.
  2. При объявлении класса-наследника D этот класс наследуется от класса B и, кроме того, передается себя в качестве параметра шаблона класса B.

В виде C++ кода это будет выглядеть следующем образом:

      template <class T>
class B {
…
};

class D : public B<D> {
…
};

Для реализации АТД с помощью статического полиморфизма базовый шаблонный класс B реализует операции данного АТД, но вызывает при этом соответствующие методы класса-наследника, переданного классу B в качестве параметра шаблона. Приведем конкретный пример:

      template <class T>
class Stack {
public:
  void push(unsigned int elem) {
    static_cast<T*>(this)->push_impl(elem);
  }unsignedint top() {
    return static_cast<T*>(this)->top_impl();
  }

  void pop() {
    static_cast<T*>(this)->pop_impl();
  }

  bool empty() {
    returnstatic_cast<T*>(this)->empty_impl();
  }
};

Это, фактически, спецификация операций абстрактного типа данных Stack. Каждая операция вызывает внутри себя операцию класса-параметра шаблона, представляющего реализацию данного абстрактного типа.

ПРИМЕЧАНИЕ

В силу особенностей языка C++ в реализации абстрактного типа на основе статического полиморфизма лучше объявлять имена реализации методов отличными от имен объявлений этих методов в шаблонном интерфейсе. В противном случае, если, по какой-то причине, в классе-реализации не будет метода с вызываемым именем, произойдет рекурсивный вызов метода шаблонного интерфейса и программа войдет в бесконечную рекурсию. Именно по этой причине в вызовах методов добавлены суффиксы «impl».

Теперь достаточно чуть изменить объявление класса ArrayStack, оставив реализацию операций без изменений, только добавив к именам методов суффиксы «_impl», чтобы заработал статический полиморфизм:

      class ArrayStack : public Stack<ArrayStack> {
  // Массив реализуется на основе типа std::vector стандартной библиотеки C++
  std::vector<unsigned int> array_;
public:
  void push_impl(unsigned int elem) {
    array_.push_back(elem);
}

  unsigned int top_impl() {
    if (array_.empty()) {
      throw std::invalid_argument(“the stack is empty”);
    }
    return array_.back();
  }

  void pop_impl() {
    if (array_.empty()) {
      throw std::invalid_argument(“the stack is empty”);
    }
    array_.pop_back();
  }

  bool empty_impl() {
    return array_.empty();
  }
};

Полиморфизм разрешается на этапе компиляции программы, поэтому функция, реализующая подстановку реализации АТД, также должна быть шаблонной:

      template <class StackImpl>
bool CheckFirstAxiom(Stack<StackImpl>* stack) {
  stack->push(123);
  if (stack.top() != 123) {
    returnfalse;
  }
  returntrue;
}

В качестве параметра функции по-прежнему передается указатель на объект класса ArrayStack, но разрешение реализации операций производится на этапе компиляции программы.

К сожалению, в языке C++ не реализована эффективным образом возможность связывания реализации АТД с его объявлением на этапе линкования модулей программы. Это полезно при разработке библиотек функций, которые требуют от клиента реализации специфицированного в библиотеке АТД. Например, некая библиотека проводит разбор файлов определенного формата и ожидает от пользователя реализации АТД Interpreter, чтобы вызывать операции этого типа для передачи сведений о результатах анализа. На этапе компиляции библиотеки ее разработчики не знают о том, каким образом информация, предоставляемая разборщиком формата, будет использоваться клиентом. Кроме того, не всегда возможно показывать код библиотеки клиенту, что придется делать при реализации концепции АТД на основе шаблонов. Поэтому связывание на основе шаблонов здесь не годится.

В языках, основанных на парадигмах, отличных от ООП, концепция АТД, очевидно, должна быть реализована по-другому. Рассмотрим, например, как концепция АТД реализуется в языке Haskell. В этом языке АТД можно задавать в модулях. Модуль позволяет описывать типы и набор операций, который виден пользователям модуля. Вот, например, как можно описать абстрактный тип Stack:

      module Stack (Stack, create, push, top, pop, empty) where

create :: Stack a
push :: Stack a -> a -> Stack a
top :: Stack a -> a
pop :: Stack a -> Stack a
empty :: Stack a -> Bool

Синтаксис языка Haskell позволяет описывать абстрактные типы данных более ясно. Задается модуль Stack, предоставляющий операции, и тип Stack, параметризуемый типом его содержимого. Для операций описывают их арность, типы аргументов и возвращаемых значений. В языке Haskell типы аргументов задаются не в виде декартового произведения соответствующих типов, а с помощью функций, берущих в качестве аргументов другие функции. Соответствие между этими двумя типами описания аргументов называется карированием. Здесь мы на этом не будем подробно останавливаться.

Реализацию АТД Stack также можно задать в модуле Stack следующим образом:

      newtype Stack a = StackImpl [a]
create = StackImpl []
push (StackImpl s) x = StackImpl (x:s)
top (StackImpl s) = head s
pop (StackImpl (s:ss)) = StackImpl ss
empty (StackImpl s) = null s

Здесь, очевидно, описана реализация АТД Stack на основе списка, причем при помещении элементов в стек они добавляются в начало списка. Таким образом, мы имеем еще один пример реализации АТД стека, но уже на основе списка. Если позже изменить реализацию данного абстрактного типа, заменив ее, например, реализацией на основе конкатенации строкового представления элементов стека, то пользователи данного АТД ничего не заметят, т.к. работают только со спецификацией этого типа, заданной в заголовке модуля.

АТД как алгебраические системы

После того, как была осознана важность понятия АТД, была также осознана необходимость в его строгом математическом описании. Этой проблемой занялась группа ADJ.

ПРИМЕЧАНИЕ

Идейным вдохновителем деятельности этой группы был Джозеф Гоген (Joseph Goguen). В статье «Воспоминания об ADJ» [10] Гоген пишет, что основной целью работы данной группы было показать связь между теорией категорий и информатикой, а в качестве задачи максимум – построить информатику на новом основании, теории категорий. Название «ADJ» намекает на термин «adjunction», обозначающий в теории категорий сопряжение двух категорий.

Как уже говорилось выше, неформально алгебраическую систему можно определить, как «множество с операциями». Например, множество натуральных чисел с операцией сложения образует алгебру (алгебраическую систему). В большинстве случаем дело обстоит несколько сложнее, операции определяются не на одном множестве, но на нескольких. Например, можно выделить множество строк и множество натуральных чисел, и определить операцию конкатенации (соединения) двух строк, а также операцию конкатенации конечного числа раз строки с собой. Последняя операция будет брать в качестве аргументов (операндов) строку и число, представляющее количество конкатенаций. В теории алгебраических систем отделяют описание операций и их свойств от конкретных реализаций этих операций на конкретных множествах. Далее в разделе будет описано, как это делается.

Для описания операций задается конечное множество имен сортов (типов). Каждое имя сорта представляет соответствующее множество. Множество имен сортов будем обозначать через S. Часто на множестве имен сортов вводят отношение подчинения (моделирующее отношение тип-подтип), которое образует алгебраическую структуру решетки, но здесь мы этого делать не будем.

Задается также множество OP имен или сигнатур операций. Сигнатурой (именем) операции назовем выражение вида

σ : s1 x s2 x .. x sn -> s

где все si, 1 ≤ i ≤ n, и s принимают значения в множестве имен сортов S. Элементы s1, s2, …, sn представляют типы аргументов операции, а s – тип значения. Имя операции обозначается здесь греческой буквой σ. Количество аргументов операции называют ее арностью. Множество

Сигнатурой многосортной алгебры назовем пару Σ=(S,OP), где S – множество имен сортов, а OP представляет множество имен операций.

Сигнатура алгебры представляет описание операций этой алгебры (алгебраической системы). Это описание включает в себя описание имен сортов и имен операций, но без указания конкретных множеств и операций на них. Сигнатура многосортной алгебры является математическим представлением абстрактного типа данных, если его трактовать только как кластер операций, без задания аксиом-равенств и предусловий.

Заметим, что арность операции может быть нулевая. Операции с нулевой арностью называют еще константами. Вот, например, как будет выглядеть определение натурального числа в стиле Пеано:

S={Nat}, OP={0,s}
0 : -> Nat
s : Nat -> Nat

Задается константа 0 как нульарная операция и унарная операция s увеличения числа на единицу.

Для задания алгебраической системы необходимо определить еще множества для каждого имени сорта и отображение на этих множествах для каждого имени операции. Множества, задаваемые для имен сортов, называются множествами-носителями этих сортов. Мы будем обозначать их через As для имени сорта s. Таким образом, приходим к следующему определению.

Многосортной Σ-алгеброй над сигнатурой Σ=(S,OP) будем называть пару A=(AS,α), где AS – набор множеств-носителей имен сортов S сигнатуры Σ, а α – семейство отображений вида

ασ : AS1 x AS2 x .. x ASn -> AS

заданных для каждого имени операции

σ : s1 x s2 x .. x sn -> s

Очевидно, что при таком определении каждой нульарной операции соответствует ровно один элемент множества-носителя сорта значения этой операции. Иначе говоря, нульарная операция просто выбирает некоторый элемент во множестве-носителе сорта своего значения. Это и объясняет тот факт, что такие операции называют константами.

Каждому сорту соответствует его множество-носитель, а каждой операции – отображение, определенное на декартовом произведении множеств-носителей аргументов операции, и имеющее значения в множестве-носителе сорта значения данной операции.

Для описанной выше сигнатуры определения множества натуральных чисел в стиле Пеано можно задать сколь угодно много многосортных Σ-алгебр. Основная такая Σ-алгебра – это множество натуральных чисел N, задаваемое как носитель сорта Nat, а также отображения: 0 для константы 0 и увеличения значения числа на единицу – для отображения s. Но можно задать и другие Σ-алгебры. Например, в качестве множества-носителя сорта Nat выбрать множество A = {x} из одного элемента x и задать x как значение отображения 0, а также в качестве значения отображения s. Это задание вполне корректно т.к. никаких требований к свойствам операций мы пока не предъявляем.

Определение многосортной алгебры очень напоминает задание абстрактного типа данных и его реализации. Только при задании АТД обычно имеется ввиду, что абстрактный тип может иметь несколько реализаций. Следовательно, для строгого математического описания АТД необходимо каким-то образом связать сигнатуры типа со всеми его реализациями.

На многосортных Σ-алгебрах можно задать отображения посредством задания отображений их множеств-носителей. В алгебре обычно интересны такие отображения, которые сохраняют свойства операций, т.е. инвариантны относительно отображений, определенных для операций сигнатуры Σ=(S,OP). Эта инвариантность выражается следующим образом. Пусть имеются две Σ-алгебры: A=(AS,α) и A'=(A’S,α’). Гомоморфизмом j : A -> A’ будем называть такое отображение j множеств-носителей AS в множества-носители A’S, которое сохраняет типы элементов и обладает следующим свойством: для любой операции σ из OP и для любого результата отображения ασ(a1, a2, …, an) выполняется условие:

j(ασ(a1, a2, …, an))= α’σ(j(a1), j(a2), …, j(an)) (*)

Взаимооднозначный гомоморфизм называют изоморфизмом.

Свойство (*) просто говорит о том, .что применяя отображение ασ, а затем переводя результат в алгебру A' отображением j, мы получим тоже самое, если переведем все аргументы a1, a2, …, an отображением j в алгебру A', а потом применим отображение, соответствующее операции σ.

Для выражения аксиом-равенств вводится множество переменных X. Элементы множества X типизированы сортами из S, т.е. каждая переменная принадлежит некоторому сорту. Из переменных и операций можно построить множество типизированных термов следующим образом:

  1. Имя нульарной операции есть терм, тип которого совпадает с типом значения операции.
  2. Имя любой переменной типа s есть терм типа s.
  3. Для любого имени операции вида σ : s1 x s2 x .. x sn -> s и термов t1, t2, …tn типов s1 , s2 , .. , sn, соответственно, выражение вида σ(t1, t2, …tn) есть терм типа s.
  4. Ничто другое не является термом типа s.

Например, пусть X={x,y} и рассматриваем сигнатуру определения натуральных чисел в стиле Пеано, определенную выше. В это алгебре только один тип – Nat, обе переменные x и y, также являются термами типа Nat. Нульарная операция 0 является термом типа Nat. С помощью операции s можно образовать термы вида: s(0), s(x), s(s(0)), s(s(s(y))) и т.д.

Множество термов, построенных из сигнатуры Σ=(S,OP) и множества переменных X, будем обозначать через TΣ(X). Множество термов в свою очередь образует Σ-алгебру следующим образом:

Эта алгебра, построенная на множестве термов, играет важную роль в теории алгебраических систем. С помощью этой алгебры можно производить вычисления «синтаксически», на множестве термов, не имея какой-либо конкретной реализации сигнатуры Σ=(S,OP). В логике это построение называется Эрбрановским универсумом [11], используемым, в частности, для вычислений в языке Prolog.

Σ-алгебра, построенная на множестве термов без переменных (т.е. с одними константами в качестве атомарных термов), обладает одним важным свойством: из нее в каждую другую Σ-алгебру имеется единственный гомоморфизм. Таким Σ-алгебры называются инициальными алгебрами. Посредством инициальных Σ-алгебр можно описывать семантику программ, а также производить вычисления.

Для задания аксиом-равенств можно также использовать термы. Внимательный читатель вероятно уже заметил, что аксиомы, приведенные в примере описания АТД стек, на самом деле являются термами, между которыми стоит знак равенства. Более конкретно, равенством в сигнатуре Σ=(S,OP) будем называть тройку e=(X, t’, t’’), где X – некоторое множество переменных, используемых в термах равенства, а t' и t'' – термы, имеющие один и тот же тип.

Таким образом, мы приходим к следующему математически строгому определению абстрактного типа данных:

Абстрактным типом будем называть тройку ADT=(N, Σ, E), где N – имя абстрактного типа данных, Σ=(S,OP) – сигнатура многосортной алгебры, а E = {e1, e2, …,en} – конечный набор в сигнатуре Σ.

Каждая Σ-алгебра над сигнатурой Σ=(S,OP), в которой выполняются равенства E, называется реализацией абстрактного типа данных ADT=(N, Σ, E).

Таким образом, абстрактные типы данных определяются как сигнатуры многосортных алгебр, включающие в себя задание аксиом в виде равенств. Каждая реализация абстрактного типа данных математически представляет собой Σ-алгебру над сигнатурой данного абстрактного типа данных. Для каждого АТД можно построить алгебру термов, которая является «синтаксическим» представлением данного абстрактного типа данных, а также является инициальной алгеброй в классе всех Σ-алгебр данного АТД.

Языки алгебраических спецификаций

В предыдущем разделе мы рассмотрели основы математической теории многосортных алгебраических систем. Было показано, что абстрактные типы данных также описываются этой теорией. Абстрактный тип данных представляется сигнатурой Σ-алгебры, содержащей имена сортов и операций. Также, абстрактный тип данных снабжается конечным набором аксиом-равенств, выражающих закономерности, присущие данному типу данных.

В классе всех Σ-алгебр сигнатуры Σ=(S,OP) имеется т.н. инициальная алгебра, из которой имеется единственный гомоморфизм в каждую Σ-алгебру данного класса. Инициальная алгебра строится синтаксически как множество термов, составленных из констант сигнатуры Σ, и имен операций этой сигнатуры.

Инициальная алгебра позволяет производить проверку корректности спецификации АТД, а также проверять спецификацию на наличие ошибок другого рода. Следует отметить, что подобным способом (в виде сигнатур Σ-алгебр) можно описывать не только абстрактные типы данных. Сигнатуры с аксиомами в виде равенств можно использовать для различного рода описаний. Такие описания, выполненные в виде сигнатур, называются алгебраическими спецификациями.

За прошедшие три с лишним десятка лет было придумано множество языков алгебраических спецификаций. Одним из первых языков такого рода был OBJ [12], реализованный Джозефом Гогеном в середине 70-х годов прошлого века. Вот как выглядит спецификация конечного автомата на этом языке.

      th AUTOMATON issorts Input State Output .
  op s0 : -> State .
  op f : Input State -> State .
  op g : State -> Output .
endth

Абстрактный тип данных «конечный автомат» вводится с помощью ключевого слова «th» или «theory». Затем идет спецификация сортов и операций. Поясним их смысл.

Конечный автомат неформально можно представить как конечное множество состояний и функцию переходов, которая берет на вход пару (символ, состояние) и возвращает состояние в качестве результата. В конечном автомате выделяются начальное состояние и множество допускающих состояний. В начале работы конечный автомат устанавливается в начальное состояние, после чего по входной последовательности символов производятся переходы.

В данной выше спецификации конечного автомата задается три сорта. Сорт Input представляет множество символов, которые подаются на вход автомату. Сорт State представляет множество состояний, а сорт Output представляет множество символов, которые выдаются на печать для каждого состояния. Описываемый тип конечного автомата, который печатает значения для каждого состояния, называется трансдьюсером. Для трансдьюсеров допускающие состояния не столь важны, поэтому здесь они не заданы.

Операция s0 – это константа, задающая начальное состояние. Двухместная операция f представляет функцию переходов автомата. Операция g описывает функцию вывода трансдьюсера.

Одним из наиболее популярных на данный момент языков алгебраических спецификаций является CASL [13]. Язык CASL является результатом разработки инициативной группы CoFI (Common Framework Initiative for algebraic specification and development of software) [14], организованной в 1995 году специально для создания языка алгебраических спецификаций.

Приведу пример спецификации моноида на языке CASL. Моноид – это алгебраическая структура, с одной операцией, которая удовлетворяет свойству ассоциативности. Также в моноиде имеется нейтральный элемент, который при взаимодействии с другими элементами оставляет их без изменений. Примером моноида является множество натуральных чисел, включающее нуль, который играет роль нейтрального элемента относительно операции сложения натуральных чисел. Итак, приведем пример спецификации.

      spec Monoid = 
  sort Elem
  ops  n     : Elem;
       __*__ : Elem x Elem -> Elem;
forall x,y,z : Elem
  * n*x=x
  * x*n=x
  * (x*y)*z=x*(y*z)

Как видно, определяет спецификация типа Monoid, в котором имеется один сорт с именем Elem и две операции: константа n и «*». Также задаются три аксиомы-равенства. Первые две аксиомы описывают взаимодействие с нейтральным элементом, а третья аксиома – ассоциативность операции «*».

Заключение

В работе рассматривается понятие абстрактного типа данных – одного из ключевых понятий в программировании. Абстрактные типы данных используются как метод абстракции данных в программном коде, а также как метод выделения объектов в программной модели.

Для описания абстрактных типов данных используется т.н. поведенческая абстракция. Объекты рассматриваются как черные ящики, доступ к которым обеспечивается посредством т.н. кластера операций. Это позволяет подменять реализации абстрактного типа данных без необходимости что-то менять в коде взаимодействия.

Для абстрактных типов данных имеется строгое математическое описание – теория сигнатур многосортных алгебраических систем. Абстрактный тип данных описывается как сигнатура Σ-алгебры с конечным набором аксиом в виде равенств.

Для каждого класса Σ-алгебр имеется т.н. инициальная Σ-алгебра, из которой имеется в каждую алгебру этого класса единственный гомоморфизм. Инициальную Σ-алгебру можно построить как множество термов, построенных из операций сигнатуры. При этом роль атомарных термов играют нульарные операции, называемые также константами. Инициальные алгебры позволяют производить вычисления на сигнатуре Σ-алгебры: получать ответы на вопросы, проверять корректность задания АТД и т.д. В статье даются примеры алгебраических спецификаций, записанные на специально созданных для этих целей языках OBJ и CASL.

Автор надеется, что данная работа послужит внесению некоторой ясности в вопросы, связанные с понятием абстрактного типа данных. Математическая теория абстрактных типов данных вообще мало известна в программистском сообществе. Донести информацию об этом именно разработчикам представляется автору в высшей степени полезным делом.

Список литературы

  1. Liskov B., Zilles S.Programming with abstract data types // SIGPlan Notices, vol. 9, no. 4, 1974.
  2. Мальцев А.И. Алгебраические системы: М.: Наука, 1970. 392 с.
  3. Кнут Д. Искусство программирования. Том 1. Основные алгоритмы. Вильямс, 2010.
  4. Zilles S. Procedure encapsulation: a linguistic protection technoque // SIGPlan Notices, vol. 8, no. 9, 1973.
  5. Parnas D. On the criteria to be used in decomposing systems into modules // Communications of the ACM, 15, 1972.
  6. Cock W.R. Object-Oriented Programming Versus Abstract Data Types. ed. by de Bakker J.W., de Roever W.P. and Rozenberg G., in Foundations of Object Oriented Languages, Lecture Notes in Computer Science #489, 1990. pp. 151-178.
  7. http://ru.wikipedia.org/wiki/Принцип_подстановки_Лисков.
  8. http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
  9. http://gcc.gnu.org/wiki/LinkTimeOptimization.
  10. Goguen J. Memories of ADJ // Bulletin of the European Association. for Theoretical Computer Science, 36:96–102, October 1989.
  11. Чень Ч., Ли Р. Математическая логика и автоматическое доказательство теорем. Пер. с англ. М.: Наука, 1983.
  12. http://cseweb.ucsd.edu/~goguen/sys/obj.html.
  13. http://www.informatik.uni-bremen.de/cofi/wiki/index.php/CASL.
  14. http://www.informatik.uni-bremen.de/cofi/wiki/index.php/About_CoFI.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 5    Оценка 131 [+0/-1]         Оценить