Foldable и Traversable

Свёртка — одна из самых важных интегральных характеристик во многих областях прикладной математики: сумма, среднее, максимум, норма — всё это свёртки коллекции данных в одно число. В Haskell свёртку можно провернуть на любом типе, обладающем экземпляром Foldable\mathtt{Foldable}, который держится на одном методе:

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   (форма дерева схлопнулась в одно число)

На самом деле одного foldMap\mathtt{foldMap} достаточно для вывода: sum\mathtt{sum}, length\mathtt{length}, maximum\mathtt{maximum}, elem\mathtt{elem}, toList\mathtt{toList} Но foldMap\mathtt{foldMap} сводит всю структуру к одному значению. А если на каждом элементе нужно выполнить вычисление — которое может провалиться, сходить в конфиг, накопить лог — и при этом собрать результаты, не потеряв форму структуры? Это умеет Traversable\mathtt{Traversable}.

Обратите внимание на классы Functor\mathtt{Functor} и Foldable\mathtt{Foldable} в объявлении ниже: traverse\mathtt{traverse} настолько общий, что fmap\mathtt{fmap} и foldMap\mathtt{foldMap} — его частные случаи. Подставьте вместо контекста ff обёртку без эффекта Identity\mathtt{Identity} — и traverse\mathtt{traverse} вырождается в fmap\mathtt{fmap}, так что контейнер обязан быть Functor\mathtt{Functor}. Подставьте Const  w\mathtt{Const}\;w (аппликатив, когда ww — моноид) — и traverse\mathtt{traverse} вырождается в foldMap\mathtt{foldMap}, значит, всякий Traversable\mathtt{Traversable} заодно и Foldable\mathtt{Foldable}. Оттого оба класса и вынесены в суперклассы:

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

Понятно, что fmap  half\mathtt{fmap\;half} даёт список коробок [Maybe  Int][\mathtt{Maybe}\;\mathtt{Int}], а sequenceA\mathtt{sequenceA} выворачивает его в коробку списка Maybe  [Int]\mathtt{Maybe}\;[\mathtt{Int}]; и стоит одному элементу вернуть Nothing\mathtt{Nothing}, как весь результат — Nothing\mathtt{Nothing}.

traverse half превращает список значений в Maybe-список: [2,4,6] в Just [1,2,3], а один Nothing обнуляет всё
Рис. 2.8. traverse  half\mathtt{traverse\;half} превращает список в Maybe\mathtt{Maybe}-список: один Nothing\mathtt{Nothing} обнуляет всё.

Осталось понять, откуда в сигнатуре traverse\mathtt{traverse} взялся именно Applicative\mathtt{Applicative}. В примере выше sequenceA\mathtt{sequenceA} склеивала контексты соседних элементов: Just\mathtt{Just} с Just\mathtt{Just}, а любой Nothing\mathtt{Nothing} обнулял результат. Для такой склейки нужны ровно два умения: pure\mathtt{pure} — начать обход с «пустого» эффекта — и <>\mathbin{\mathtt{<*>}} — присоединить эффект очередного элемента к уже накопленному. Это в точности интерфейс Applicative\mathtt{Applicative}: Functor\mathtt{Functor} здесь слаб (fmap\mathtt{fmap} не умеет сливать два контекста в один), а Monad\mathtt{Monad} избыточен (>>=\mathbin{\mathtt{>>=}} разрешает следующему шагу зависеть от результата предыдущего, но маршрут обхода и так жёстко задан формой структуры).

У traverse\mathtt{traverse} есть и законы. Самый наглядный — тождество: traverse  Identity=Identity\mathtt{traverse\;Identity = Identity}, обход без эффекта не меняет ни форму, ни значения. Остальные два — композиция (два обхода подряд можно слить в один) и натуральность (обход согласован с заменой одного аппликатива на другой). Вместе они гарантируют, что traverse\mathtt{traverse} обходит каждый элемент ровно один раз и сохраняет форму структуры.

Так шестёрка классов и замыкается: Monoid\mathtt{Monoid} комбинирует значения, Functor\mathtt{Functor} / Applicative\mathtt{Applicative} / Monad\mathtt{Monad} применяют функции во всё более сильных контекстах, Foldable\mathtt{Foldable} / Traversable\mathtt{Traversable} обходят структуры — и практически весь «бойлерплейт» обработки данных сводится к ним.