Основы функционального программирования/Конструирование функций
Материал из Викиучебника
Основы функционального программирования
- Вводная лекция
- Структуры данных и базисные операции:
Содержание |
Для конструирования функций используются разные формализмы, среди которых синтаксически-ориентированное конструирование. Чтобы применять его, можно воспользоваться методом, в свое время предложенным Хоаром.
Ниже приводится описание метаязыка, используемого для определения структур данных (в абстрактном синтаксисе):
1. Декартово произведение. Если
суть типы, а C — тип, состоящий из множества n-ок вида
,
, i = 1,n, то говорится, что C — декартово произведение типов
и обозначается как
. При этом предполагается, что определены селекторы
для типа C, что записывается как
.
Таким же образом записывается конструктор
. Конструктор — это функция, имеющая тип
, то есть для
.
Будет считаться, что справедливо равенство:
.
Это равенство называется аксиомой тектоничности. Кроме того, иногда эту аксиому записывают следующим образом:

2. Размеченное объединение. Если
— это типы, а C — это тип, состоящий из объединения типов
, при условии выполнения «размеченности», то C называется размеченным объединением типов
. Обозначается этот факт как
. Условие размеченности обозначает, что если из C взять какой-нибудь элемент ci, то однозначно определяется тип этого элемента Ci. Размеченность можно определить при помощи предикатов
таких, что:

Размеченное объединение гарантирует наличие таких предикатов. Этот факт указывается записью:
. Ещё есть части типа, которые обозначаются так:
.
Как видно, в представленном метаязыке используется два конструктора типов:
и + . Далее рассматриваются несколько примеров определения новых типов.
Пример 17. Формальное определение типа
.





Глядя на это описание (скорее — определение) типа, можно описать внешний вид функций, обрабатывающих структуры типа
:
Каждая функция должна содержать как минимум два клоза, первый обрабатывает NIL, второй — nonNIL соответственно. Этим двум частям типа
в абстрактной записи соответствуют селекторы
и (H:T). Два клоза можно объединить в один с использованием охраны. В теле второго клоза (или второго выражения охраны) обработка элемента T (или
) выполняется той же самой функцией.
Пример 18. Формальное определение типа
.


Функции над
должны иметь по крайней мере следующие клозы:
1° 
2° ![[\,] \to \operatorname{when} \Big( \operatorname{null} (L) \Big)](http://upload.wikimedia.org/math/5/4/b/54b5694f3cc5c5abd1bf4034014ee764.png)
3° 
3.1° 
3.2° 
Пример 19. Формальное определение деревьев и лесов с помеченными вершинами.




Пример 20. Формально определение деревьев с помеченными вершинами и дугами.






Утверждается, что любая функция, работающая с типом
, может быть представлена только через упомянутые шесть операций независимо от того, как она реализована. Это утверждение можно проверить при помощи диаграммы (скорее, это гиперграф), на которой ясно видно, что к любой части типа
можно «добраться», используя только эти шесть операций.
Для конструирования функций, обрабатывающих структуры данных
, необходимо ввести несколько дополнительных понятий и обозначений для них. Это делается для простоты. Начальная вершина, вершина
и вершина
(выходящая из
) обозначаются как S0, S1 и S2 соответственно. Для обработки этих вершин необходимы три функции — f0, f1 и f2, причём f0 — это начальная функция, а две последних — рекурсивные.
Рисунок 3. Гиперграф для представления структуры 
Конструирование функции f0 выглядит просто — у этой функции один параметр T, который соответствует начальной вершине S0. Две другие функции сконструировать сложнее.
Функция f1 получает следующие параметры:
- A — метка вершины;
- K — параметр, содержащий результат обработки просмотренной части дерева;
- L — лес, который необходимо обработать.
f1 A K L = g1 A K when null L
f1 A K L = f1 A (g2 (f2 A (arc (head L)) (mtree (tail L)) K) A (arc L) K) (tail L) otherwise
Эта функция организует режим просмотра дерева «сначала в глубину».
Функция f2 получает следующие параметры (и это уже должно быть ясно из её вызова во втором клозе функции f1):
- A — метка вершины;
- B — метка дуги;
- T — поддерево для обработки;
- K — результат обработки просмотренной части дерева.
f2 A B T K = f1 (mroot T) (g3 A B K) (mlist T)
Необходимо отметить, что это общий вид функций для обработки структур данных
. Реализация дополнительных функций g1, g2 и g3 зависит от конкретной задачи. Теперь можно сконструировать и общий вид функции f0:
f0 T = f1 (root T) k (mlist T)
где k — это начальное значение параметра K.
Для более глубокого закрепления методики конструирования функций можно рассмотреть конкретную реализацию функций работы с B-деревьями. Пусть для структуры данных BTree существует набор базисных операций, а сами деревья представляются в виде списков (особой роли представление не играет). Базисные операции следующие:
1° cbtree A Left Right = [A, Left, Right]
2° ctree = []
3° root T = head T
4° left T = head (tail T)
5° right T = head (tail (tail T))
6° empty T = (T == [])
Пример 21. Функция insert для вставки элемента в дерево.
insert (A:L) T = cbtree (A:L) ctree ctree when (empty T)
insert (A:L) T = cbtree (root T) (insert (A:L) (left T)) (right T) when (A < head (root T))
insert (A:L) T = cbtree (A:(L:tail (root T))) (left T) (right T) when (A == head (root T))
insert (A:L) T = cbtree (root T) (left T) (insert (A:L) (right T)) otherwise
Это реализация на абстрактном уровне.
Пример 22. Функция access для поиска элементов в B-дереве.
access A Emptic = []
access A ((A1:L) × Left × Right) = access A Left when (A < A1)
access A ((A1:L) × Left × Right) = access A Right when (A > A1)
access A ((A:L) × Left × Right) = L
access A (Root × Left × Right) = access A Right otherwise
В этом примере приведено две новых конструкции — абстрактный элемент Emptic, представляющий собой, по сути, пустое дерево, а также знак ×, при помощи которого абстрагируется декартово произведение, которое используется здесь вместо списочного представления. Но надо помнить, что это только абстрактный функциональный язык.
В представленных двух примерах существует одна проблема. При использовании написанных функций совершается огромное количество лишних копирований из одного места в памяти в другое. По сути дела это воссоздание нового дерева с новыми элементами (речь идет о функции insert). Этого можно избежать при использовании деструктивного присваивания.
[править] Упражнения
1. Сконструировать функцию insert для вставки элемента в B-дерево, использующую деструктивное присваивание.
[править] Ответы для самопроверки
1. Один из возможных вариантов функции insert с деструктивным присваиванием:
-- «Псевдо-функции» для деструктивного присваивания.
-- В строгом функциональном языке (Haskell) так делать нельзя.
-- В Лиспе есть возможность использовать деструктивное присваивание.
replace_root A T – функция добавления элемента в корень дерева
replace_left K (Root × Emptic × Right) => (Root × (K × Emptic × Emptic) × Right)
replace_right K (Root × Left × Emptic) => (Root × Left × (K × Emptic × Emptic))
-- Функция insert
insert K Emptic = cbtree K ctree ctree
insert (A:L) ((A1:L1) × Left × Right) = insert (A:L) Left when ((A < A1) & nonEmpty Left)
insert (A:L) ((A1:L1) × Emptic × Right) = replace_left (A:L) ((A1:L1) × Emptic × sRight) when (A < A1)
insert (A:L) ((A1:L1) × Left × Right) = insert (A:L) Right when ((A > A1) & nonEmpty Right)
insert (A:L) ((A1:L1) × Left × Emptic) = replace_right (A:L) ((A1:L1) × Left × Emptic) when (A > A1)
insert A T = replace_root A T otherwise