Два пути к Data.Foldable: структуры, допускающие свёртку
Интуиция⌗
Класс Data.Foldable
представляет собой абстракцию над такими (часто
контейнерными) типами данных, которые допускают свёртку (fold). Для этих типов
осмыслена и определена операция коллапса, то есть разрушения и схлопывания всей структуры в одно
единственное итоговое значение (дайджест, резюме).
> foldr (+) 100 $ Node 2 [Node 1 [], Node 3 []]
106
> let f = foldr (.) id [(+1), (*2), (+10)]
> :t f
f :: Num b => b -> b
> f 1
23
Если некоторый тип t
реализует интерфейс тайпкласса Data.Foldable
, то это означает, что
для данного типа t
указана механика осуществления его свёртки.
Механику коллапса структуры данных можно выразить одной из двух функций:
-- Независимое описание процесса свёртки вручную
foldr :: (a -> b -> b) -> b -> t a -> b
-- Упрощённое описание: установление связи каждого элемента структуры
-- с некоторым моноидом (свёртка происходит уже внутри моноида)
foldMap :: Monoid m => (a -> m) -> t a -> m
Создадим реализацию Data.Foldable
для следующего рекурсивного типа:
-- Тип для нашего списка
data List a = Cons a (List a) | Nil
-- Пример списка
li = Cons 1 (Cons 2 (Cons 3 (Cons 4 (Cons 5 Nil))))
Реализация Foldable при помощи некоторого моноида⌗
В самом простом случае мы можем описать механику ассоциирования элементов List
с данным (на вход)
моноидом, написав функцию foldMap
.
Чтобы отобразить все элементы List
в моноид m
необходимо:
- отобразить в
m
голову списка - рекурсивно выполнится для оставшегося хвоста списка (отобразить голову хвоста списка и т.д.)
- при этом склеивать между собой все элементы внутри моноида
- конец нашего списка (конструктор
Nil
) соответствует нейтральному элементу моноида
instance Foldable List where
-- foldMap :: Monoid m => a -> m -> List a -> m
foldMap f (Cons x xs) = f x <> foldMap f xs
foldMap _ Nil = mempty
Моноид recap⌗
Напомним, что фундаментальное свойство любого моноида – наличие бинарной операции mappend
(синоним: <>
) для склеивания двух элементов моноида в один, а также наличие нейтрального элемента mempty
,
не меняющего результат склейки. Например, вот как происходит склейка внутри моноида по сложению
Sum
:
> Sum 10 <> mempty <> Sum 6 <> Sum 4
Sum {getSum = 20}
Реализации foldMap
достаточно, чтобы для нашего типа List a
стал доступен весь арсенал интерфейса
Foldable
(используем список li
, определённый выше):
> foldMap Sum li
Sum {getSum = 15}
> maximum li
5
> minimum li
1
> mapM_ (\x -> putStrLn ("Element " ++ show x)) li
Element 1
Element 2
Element 3
Element 4
Element 5
> find (\x -> x == 3) li
Just 3
> find (\x -> x == 6) li
Nothing
И мы получили это всё нахаляву, написав лишь одну функцию foldMap
!
Реализация Foldable при помощи описания процесса свёртки⌗
Более сложный, но независимый путь, не требующий готовых моноидов, – это создание механики
разрушения структуры посредством foldr
:
foldr :: (a -> b -> b) -> b -> t a -> b
Функция foldr
имеет сложную сигнатуру, которую можно разложить на три компонента (справа-налево):
t a
– собственно наша структура данных, которую мы сворачиваемb
– начальное значение аккумулятора(a -> b -> b)
– функция от одного элемента нашей структуры и от текущего значения аккумулятора, возвращающая новое значение аккумулятора
Аккумулятор нужен, как не трудно догадаться, для “протаскивания” (и, возможно, попутной модификации)
дайджеста-аккумулятора через всю структуру: мы схлопываем t a
в финальное значение аккумулятора b
.
Правоассоциативная операция foldr
последовательно разрушает (редуцирует) структуру справа-налево:
-- Ассоциативность справа, т.е. выражение ветвится вправо: f x1 (f x2 (f xn z))
foldr f z [x1, x2, ..., xn] == x1 `f` (x2 `f` ... (xn `f` z)...)
Идея нашей реализации проста: отсекаем голову списка и производим свёртку хвоста
(рекурсивно). При этом начальное значение аккумулятора start
окажется в самой
глубине выражения, а при редукции каждый последующий foldr
будет возвращать
новое значение аккумулятора, вплоть до сокращения всего выражения:
instance Foldable List where
-- foldr :: (a -> b -> b) -> b -> t a -> b
foldr f start (Cons x xs) = (x `f` (foldr f start xs))
foldr _ acc Nil = acc
Теперь мы можем осуществлять свёртку без помощи сторонних моноидов. Написание foldr
достаточно
для включения нашего List
в тайпкласс Data.Foldable
!
-- разрушение происходит справа (сначала 5, потом 4, ...)
> foldr (\e (s, log) -> (s + e, log ++ " added " ++ show e)) (0, "") li
(15," added 5 added 4 added 3 added 2 added 1")
-- разрушение происходит слева (сначала 1, потом 2, ...)
> foldl (\(s, log) e -> (s + e, log ++ " added " ++ show e)) (0, "") li
(15," added 1 added 2 added 3 added 4 added 5")
Кстати, при помощи foldr
можно написать foldl
, но не наоборот (для бесконечных структур)!