Свёртка — одна из самых важных интегральных характеристик во многих областях прикладной математики: сумма, среднее, максимум, норма — всё это свёртки коллекции данных в одно число. В Haskell свёртку можно провернуть на любом типе, обладающем экземпляром , который держится на одном методе:
class Foldable t where
foldMap :: Monoid w => (a -> w) -> t a -> w
-- (в классе есть и другие методы, но foldMap достаточно)
-- контейнер t реализует Foldable
data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)
instance Semigroup (Tree a) where
Empty <> s = s
l <> Empty = l
Leaf x <> s = Node Empty x s
Node l x r <> s = Node l x (r <> s)
instance Monoid (Tree a) where mempty = Empty
instance Foldable Tree where
foldMap _ Empty = mempty
foldMap f (Leaf x) = f x
foldMap f (Node l x r) = foldMap f l <> f x <> foldMap f r
newtype Sum a = Sum a
instance Num a => Semigroup (Sum a) where Sum x <> Sum y = Sum (x + y)
instance Num a => Monoid (Sum a) where mempty = Sum 0
-- свернём дерево 1,2,3 моноидом Sum:
t = Node (Leaf 1) 2 (Leaf 3)
foldMap Sum t -- Sum 6 (форма дерева схлопнулась в одно число)На самом деле одного достаточно для вывода: , , , , … Но сводит всю структуру к одному значению. А если на каждом элементе нужно выполнить вычисление — которое может провалиться, сходить в конфиг, накопить лог — и при этом собрать результаты, не потеряв форму структуры? Это умеет .
Обратите внимание на классы и в объявлении ниже: настолько общий, что и — его частные случаи. Подставьте вместо контекста обёртку без эффекта — и вырождается в , так что контейнер обязан быть . Подставьте (аппликатив, когда — моноид) — и вырождается в , значит, всякий заодно и . Оттого оба класса и вынесены в суперклассы:
class (Functor t, Foldable t) => Traversable t where
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
sequenceA :: Applicative f => t (f a) -> f (t a)
traverse g = sequenceA . fmap g -- traverse и sequenceA взаимовыразимы
-- f = Identity (обёртка без эффекта):
(a -> Identity b) -> t a -> Identity (t b) ≅ (a -> b) -> t a -> t b -- fmap
-- f = Const w (Applicative, когда w — моноид):
(a -> Const w b) -> t a -> Const w (t b) ≅ (a -> w) -> t a -> w -- foldMap
-- половинка чётного, иначе провал:
half :: Int -> Maybe Int
half x = if even x then Just (x `div` 2) else Nothing
traverse half [2, 4, 6] -- Just [1,2,3]
traverse half [2, 3] -- NothingПонятно, что даёт список коробок , а выворачивает его в коробку списка ; и стоит одному элементу вернуть , как весь результат — .
Осталось понять, откуда в сигнатуре взялся именно . В примере выше склеивала контексты соседних элементов: с , а любой обнулял результат. Для такой склейки нужны ровно два умения: — начать обход с «пустого» эффекта — и — присоединить эффект очередного элемента к уже накопленному. Это в точности интерфейс : здесь слаб ( не умеет сливать два контекста в один), а избыточен ( разрешает следующему шагу зависеть от результата предыдущего, но маршрут обхода и так жёстко задан формой структуры).
У есть и законы. Самый наглядный — тождество: , обход без эффекта не меняет ни форму, ни значения. Остальные два — композиция (два обхода подряд можно слить в один) и натуральность (обход согласован с заменой одного аппликатива на другой). Вместе они гарантируют, что обходит каждый элемент ровно один раз и сохраняет форму структуры.
Так шестёрка классов и замыкается: комбинирует значения, / / применяют функции во всё более сильных контекстах, / обходят структуры — и практически весь «бойлерплейт» обработки данных сводится к ним.