Функциональное программирование. Конспект лекций
.pdf
которую можно прочитать следующим образом: список таких элементов x, которые принадлежат списку s и при этом удовлетворяют условию, для которых выражение f x истинно.
В общем виде обобщенный конструктор списков выглядит следующим образом:
[ элемент | источники,... условия,... ]
где элемент – произвольное выражения для построения элементов нового списка из образцов, заданных в источниках; источники – перечисленные через запятую конструкции вида образец <- список; а условия – перечисленные через запятую логические выражения, определяющие условия на выбираемые элементы. Например, конструктор
[ (x,y) | x <- [1..10], y <- [1..10] ]
представляет список всех пар чисел с элементами из списка от 1 до
10.
Функция zip принимает в качестве аргументов два списка и возвращает список пар (кортежей). Данная функция имеет тип
zip :: [a] -> [b] -> [(a, b)] zip [] [] = []
zip (x1:s1) (x2:s2) = (x1, x2) : zip s1 s2
Если списки имеют разную длину, то длина результирующего списка определяется длиной наименьшего списка.
Функция zipWith последовательно выполняет операцию, заданную функцией f :: (a -> b -> c) над элементами двух списков, формируя третий список. Определение функции zipWith выглядит следующим образом:
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] zipWith _ [] [] = []
zipWith f (x1:s1) (x2:s2) = (f x1 x2) : zipWith f s1 s2
– 51 –
Если списки имеют разную длину, то длина результирующего списка определяется длиной наименьшего списка.
С помощью почленного сложения двух списков (используя zipWith с операцией (+)) можно легко сформировать бесконечную последовательность (список) чисел Фибоначчи. Если имеются два списка вида:
[1, 1, 2, 3, 5, 8, 13, 21, …] [0, 1, 1, 2, 3, 5, 8, 13,… ],
первый из которых представляет числа Фибоначчи, а второй – это первый список, сдвинутый на одну позицию вправо, и в его начальной позиции находится нуль. Складывая почленно эти две последовательности, можно сформировать бесконечный список чисел Фибоначчи, а именно:
fib = 1 : zipWith (+) fib (0 : fib) --или
fib where
fib0 = 0:fib
fib1 = zipWith (+) fib0 fib fib = 1:fib1
Эту строчку можно прочитать так: «Последовательность (список) чисел Фибоначчи fib получается, если приписать единицу к результату почленного сложения самой этой последовательности и той же последовательности, к которой приписан в начале нулевой элемент». Данное определение рекурсивно определяет последовательность Фибоначчи и действительно порождает бесконечный список чисел Фибоначчи. Это возможно только благодаря ленивым вычислениям в программах на языке Haskell.
5.4. Операции с матрицами
В [3] приведены интересные примеры использования функций высших порядков для выполнения матричных операций. Определим тип матрицы как список списков:
type Matrix = [[Float]]
– 52 –
Если матрица состоит только из одной строки-списка, в которой n элементов, то операция транспонирования должна преобразовать эту строку-список в n строк-списков, в каждом из которых будет по одному элементу. Так матрица-строка [[1,2,3]] должна быть преобразована в [[1],[2],[3]]. Это можно реализовать с помощью функции map:
transpose [row] = map singleton row
where singleton x = [x]
Для реализации рекурсии представим, что все строки, за исключением первой, транспонированы. Для завершения транспонирования следует добавить каждый элемент первой строки в уже сформированные строки матрицы, транспонированной на основе (n-1) строки. Это можно сделать с помощью функции zipWith, которой предан конструктор списков в качестве бинарной операции:
transpose (xs:xss) = zipWith (:) xs (transpose xss)
Объединяя все вместе, получим следующую программу транспонирования матрицы:
module Matrix where
type Matrix = [[Float]]
transpose :: Matrix -> Matrix transpose [row] = map singleton row
where singleton x = [x]
transpose (xs:xss) = zipWith (:) xs (transpose xss)
Работа программы будет выглядеть следующим образом:
*Matrix> transpose [[1,2,3], [4,5,6], [7,8,9]] [[1,4,7],[2,5,8],[3,6,9]]
– 53 –
В [3] приведена также нерекурсивная функция транспонирования матрицы на основе функции foldr, zipWith и repeat::a- >[a] (функция repeat строит бесконечный список из одних и тех же значений).
module Matrix where type Matrix = [[Float]]
transp :: Matrix -> Matrix transp m = foldr f e m
where f = zipWith (:) e = repeat []
--или
transp m = foldr zipWith (:) (repeat []) m
Рассмотрим пример транспонирования матрицы вида
M = |
|
|
|
1 |
2 |
3 |
|
|
|
|
M T = |
|
|
|
1 |
4 |
7 |
|
|
|
|
|
|
|
|
|
|
|
|
||||||||||||||
|
|
|
4 |
5 |
6 |
|
|
|
; |
|
|
|
2 |
5 |
8 |
|
|
|
. |
||
|
|
|
|
7 |
8 |
9 |
|
|
|
|
|
|
|
|
3 |
6 |
9 |
|
|
|
|
Исходная матрица задается в виде списка списков, и для ее транспонирования вызывается функция:
transp [[1,2,3], [4,5,6], [7,8,9]]
Разберем работу данной функции:
1.Функция (repeat []) сформирует список пустых списков [[], [], []], т.е. три пустых списка.
2.Функция foldr выберет последнюю строку матрицы, а
именно [7,8,9].
3.Функция zipWith (:) применит к каждому элементу списка [[], [], []], каждый элемент из списка [7,8,9], операцию
(:). В результате получится список списков [[7], [8], [9]].
4.Функция foldr выберет предпоследнюю строку матрицы, а
именно [4,5,6].
5.Функция zipWith (:) применит к каждому элементу списка [[7], [8], [9]], каждый элемент из списка [4,5,6], опера-
–54 –
цию (:). В результате получится список списков [[4,7], [5,8],
[6,9]].
6. Функция foldr выберет первую строку матрицы, а именно
[1,2,3].
7. Функция zipWith (:) применит к каждому элементу спис-
ка [[4,7], [5,8], [6,9]], каждый элемент из списка [1,2,3],
операцию (:). В результате получится список списков [[1,4,7],
[2,5,8], [3,6,9]].
Таким образом, при использовании функции высших порядков выполняется транспонирование матрицы.
– 55 –
Лекция 6. Деревья и множества в Hаskell
Описывается построение сложных структур данных – деревьев и множеств. Рассматриваются примеры использования функций высших порядков для работы с деревьями и множествами.
Ключевые слова: деревья; множества; характеристические функции; разглаживание деревьев.
Введение. Перейдем к рассмотрению данных сложной структуры, а именно – к деревьям и множествам. В Haskell существует удобный механизм создания пользовательских структур данных. Рассмотрение и анализ программ начнем с деревьев, далее перейдем к множествам.
6.1. Двоичные деревья
Двоичным деревом называют структуру данных, содержащую элементы, связанные друг с другом таким образом, что каждый элемент (узел дерева) связан с единственным другим элементом (его непосредственным предком), причем этот предок является предком для не более чем двух других элементов, называемых его
непосредственными потомками [1]. Один узел из всей совокупно-
сти называется корнем дерева и является исключением из общего правила: у него нет непосредственного предка. Узлы, не имеющие потомков, называются листьями дерева.
Если в дереве имеется n узлов, то около половины из них будут листьями, при этом хотя бы один лист обязательно имеется в любом дереве. Дерево, не содержащее узлов, называется пустым. Двоичное можно определить рекурсивно следующим образом: двоичным деревом называется либо пустое дерево, либо совокупность из одного узла (корня дерева) и двух других деревьев, называемых левым и правым поддеревьями этого узла. Каждое из поддеревьев может в свою очередь быть пустым деревом или узлом со своими поддеревьями.
Структуры данных для описания дерева будет выглядеть следующим образом:
– 56 –
data Tree a = Null | Tree a (Tree a) (Tree a)
Здесь используется два конструктора. Конструктор Null будет порождать пустое дерево, а конструктор Tree будет иметь в качестве параметров узел a и два поддерева (левое и правое).
Следует иметь в виду, что в выражении Tree a (Tree a) (Tree a) первый идентификатор Tree является конструктором, а два других (Tree a) – рекурсивные ссылки на идентификатор определяемого типа данных.
6.1.1. Функция вычисления количества листьев в дереве
Рассмотрим функцию вычисления количества листьев в дереве:
leafCount :: Tree a -> Int
--пустое дерево не содержит листьев: leafCount Null = 0
--если дерево состоит только из листа: leafCount (Tree _ Null Null) = 1
--корень не является листом:
leafCount (Tree _ tl tr) = leafCount tl + leafCount tr
В приведенном примере проявляется удобство функционального стиля программирования. Подсчет выполняется с помощью определения трех функций leafCount. Если дерево пустое (т.е. Null), то количество листьев равно нулю. Если дерево состоит только из листа (без поддеревьев), то количество листьев равно 1. И, наконец, если имеются левое и правое поддеревья, то надо рекурсивно подсчитать количество листьев в этих поддеревьях и сложить.
6.1.2 Функция разглаживания дерева
Рассмотрим выстраивания элементов дерева в список (или разглаживание дерева). Функция производит обход всех узлов дерева
– 57 –
и строит список из этих узлов. Порядок обхода узлов и помещения их в список важен для решения задач. Например, иногда важно, чтобы любой узел попадал в список только после своего непосредственного предка (обходы сверху вниз), иногда наоборот. В рассматриваемой функции осуществляется рекурсивный левосторонний обход, при котором любой узел попадал в список после узлов левого поддерева и до узлов правого поддерева.
flatten :: Tree a -> [a] flatten Null = []
flatten (Tree node tl tr) = flatten tl ++ [node] ++ flatten tr
Как видно, реализация функции очень проста и не очень эффективна, так как здесь используется функция соединения (конкатенации) списков. Рекурсивно вызывается функция разглаживания для левого поддерева, к полученному списку добавляется узел node, заключенный в квадратные скобки, так как используется операция
++и вызывается функция разглаживания для правого поддерева. Если исходное дерево – упорядоченное, т.е. величина любого
узла больше значений, хранящихся в левом поддереве этого узла и не меньше значений, хранящихся в правом поддереве, то при его разглаживании список получится упорядоченным по возрастанию. Это можно использовать для сортировки элементов списка. Сначала элементы исходного списка просматриваются и вставляются по очереди в упорядоченное дерево, после чего отсортированный список получается путем разглаживания сформированного дерева:
treeSort :: (Ord a)=> [a] -> [a] treeSort s = flatten (formTree s)
insert :: (Ord a)=> a -> Tree a -> Tree a insert elem Null = Tree elem Null Null
insert elem (Tree n tl tr) | elem < n = Tree n (insert elem tl) tr
| otherwise = Tree n
tl (insert elem tr)
formTree :: (Ord a)=> [a] -> Tree a
– 58 –
formTree [] = Null
formTree (x:s) = insert x (formTree s)
flatten :: Tree a -> [a] flatten Null = []
flatten (Tree |
node |
tl tr) = flatten tl ++ [node] |
|
++ flatten tr |
|
|
|
Здесь treeSort |
– основная функция сортировки списка с по- |
||
мощью дерева; |
insert – |
функция вставки элемента в упорядо- |
|
ченное дерево; |
formTree |
– функция формирования упорядочен- |
|
ного дерева из исходного списка последовательными вставками элементов; flatten – функция разглаживания дерева.
Эффективность работы данной функции сортировки невысока. Проблема состоит в скорости работы рекурсивной функции разглаживания дерева flatten. Основное уравнение, определяющее работу этой функции, содержит не только два рекурсивных обращения к этой же функции, но и обращение к операции соединения списков, которая будет просматривать заново уже разглаженную часть дерева только для того, чтобы присоединить ее к другим частям получающегося списка. В результате для дерева, содержащего n узлов, скорость разглаживания будет в среднем пропорциональ-
на n logg2n , а в худшем случае будет достигать n2.
Скорость работы функции разглаживания дерева можно увеличить, если использовать дополнительный накапливающий аргумент функции flatten'. Дополнительным накапливающим аргументом для функции разглаживания flatten' является список, к «голове» которого присоединяется результат разглаживания дерева:
flatten' :: Tree a |
-> [a] -> |
[a] |
flatten' Null list |
= list |
|
flatten' (Tree node tl tr) |
list = flatten' tl |
|
(node : (flatten' tr list)) |
|
|
flatten tree = flatten' tree |
[] |
|
– 59 –
6.1.3. Свертка двоичных деревьев
Для двоичного дерева существует несколько вариантов обхода. Соответственно, можно написать несколько вариантов функции свертки дерева согласно алгоритмам его прохождения. Например, если выбран правосторонний порядок прохождения узлов, при котором любой узел проходится до прохождения узлов его левого поддерева и после прохождения узлов правого поддерева, то функция свертки дерева foldTree может быть написана как:
foldTree :: (a -> b -> b) -> b -> Tree a -> b
foldTree _ seed Null = seed
foldTree f seed (Tree a tl tr) = foldTree f (f a (foldTree f seed tr)) tl
Так как в каждом узле дерева имеются ссылки на два поддерева, то и в определении функции свертки имеются два рекурсивных обращения – соответственно для свертки отдельно левого и правого поддеревьев. Каждый узел дерева просматривается ровно один раз. Рекурсия разворачивается до достижения нулевого поддерева. В этом случае foldTree возвращает начальное значение seed. В рекурсивном выражении foldTree f seed tr обрабатывает правое поддерево, далее применяется функция f a (foldTree f seed tr) и полученный результат выступает в качестве начального значения при обработке левого поддерева.
С помощью функции foldTree можно легко вычислить сумму или произведение значений узлов, хранящихся в дереве, или определить максимальное значение узла.
Кроме того, с помощью функции свертки можно определить функцию разглаживания дерева. Если сворачивать двоичное дерево с помощью стандартного конструктора списков (:), то в результате получится список всех узлов дерева в порядке левостороннего обхода:
flatten :: Tree a -> [a] flatten t = foldTree (:) [] t
– 60 –
