Как мы уже выяснили, функторы позволяют втягивать “обычную” функцию в некоторый вычислительный контекст, превращая её в функцию над контекстами:

> :t (* 3)
(* 3) :: Num a => a -> a

> :t fmap (* 3)
fmap (* 3) :: (Functor f, Num b) => f b -> f b

В этом примере (* 3) – это функция “утроения” (функция от одного числа, отсюда и a -> a). Функция fmap (* 3) тогда есть “функция утроения содержимого некоторого контейнера (функтора)”. Конкретизируем пример на списковый функтор []:

-- число в контексте списка ("недетерминированного результата", т.е. принимающее несколько значений одновременно)
nums :: Num a => [a]
nums = [10, 20, 30]

-- функция над числом в контексте списка (т.е. функции)
funs :: Num a => [a -> a]
funs = [(* 3), (+1), (-) 2]

Для некоторого функтора f и каких-то типов a и b мы имеем f a и f (a -> b).

Как же нам применить функцию-в-контексте к значению-в-контексте и получить результат-в-контексте? Средств тайпкласса Functor уже не хватает, чтобы применить f (a -> b) к f a и получить f b. Хотелось-бы уметь применять функции к аргументам в f-мире, тем самым получая возможность выстраивать цепочку вычислений (компьютерную программу) внутри окружения (нужного вычислительного контекста).

-- всё просто, когда ты в безконтекстном мире
> sqrt 36
6.0

-- но как быть в f-мире? Всё это работать не будет...
> [sqrt] [36] 
> (Just sqrt) (Just 36)
> ...

Очевидно, мы не можем так просто взять и написать некий общий код для применения функции к аргументу внутри какого-то там абстрактного функтора. Ведь такой код должен будет знать, как деконструировать функтор, чтобы вытащить “чистую” функцию и её “чистый” аргумент, а также, наоборот, как конструировать функтор с результатом данного применения (аппликации).

Applicative

Крутейший тайпкласс Applicative наделяет функторы такой возможностью. Аппликативные функторы являются уточнением функтора, поэтому любой экземпляр Applicative – это также и Functor (но не наоборот). Определение аппликативного функтора:

class Functor f => Applicative f where
  pure  :: a -> f a

  (<*>) :: f (a -> b) -> f a -> f b

Сигнатура <*> точь в точь совпадает с тем, чего мы пытались получить в нашем f-мире в примере выше!

> [sqrt] <*>  [36]
[6.0]

> Just sqrt <*> Just 36
Just 6.0

> Just (+) <*> Just 36 <*> Just 14
Just 50

Блгодаря повсеместному каррированию, мы можем свободно применять функции от нескольких аргументов внутри аппликативного контекста, главное, чтобы совпадали все типы входов/выходов. Аппликативное окружение, таким образом, как-бы эволюционирует, меняется при “протаскивании” каррированием вычисления через контекст с каждым новым вызовом <*>. Например, для некоторого аппликативного функтора a:

# псевдокод
a (t1 -> t2 -> ... -> tm -> tn) <*> a t1 <*> a t2 <*> ... <*> a tm == a tn
a (t1 -> t2 -> ... -> tn) <*> a t1 == a (t2 -> ... -> tn)
a (t2 -> ... -> tn) <*> a t2 == a (t3 -> ... -> tn)
a (t3 -> ... -> tn) <*> a t3 == a (t4 -> ... -> tn)
# и так далее

Вернёмся к определению аппликативного функтора. Функция pure помещает “чистое” значение в контекст и тем самым в некоторой степени напоминает fmap. Идея здесь в том, что pure втягивает значение в некий аппликативный контекст по-умолчанию, в (пока ещё) свободный от будущих эффектов контейнер.

> Just (+) <*> Just 36 <*> Just 14
Just 50

> pure (+) <*> Just 36 <*> Just 14
Just 50

Нетрудно заметить, что аппликативные функторы связаны с функторами следующим уравнением:

fmap g x = pure g <*> x

Применение g к содержимому контейнера x для Functor идиоматически то же самое, что помещение g в аппликативный контейнер по-умолчанию и затем применение функции внутри данного аппликативного контейнера. Используя инфиксный синоним для fmap можно переписать уравнение так:

g <$> x = pure g <*> x

Упражнения

1. Implement an instance of Applicative for Maybe.

Напомню интерфейс Applicative:

class Functor f => Applicative f where
  pure  :: a -> f a

  (<*>) :: f (a -> b) -> f a -> f b

Это просто. Я определяю тип MyMaybe, делаю его сначала функтором, а затем и аппликативным функтором. Аппликативный контекст обламывается, и втянутая функция не выолняется, когда в контексте находится значение MyNothing:

data MyMaybe a = MyJust a | MyNothing

instance Functor MyMaybe where
  fmap f MyNothing = MyNothing
  fmap f (MyJust a) = MyJust (f a)

instance Applicative MyMaybe where
  pure a = MyJust a

  (<*>) (MyJust f) (MyJust a) = MyJust (f a)
  (<*>) _ _ = MyNothing

Протестируем на функции, суммирующей четыре числа:

> -- sum4 :: Int -> Int -> Int -> Int -> Int
> let sum4 a b c d = sum [a, b, c, d]
>
> pure sum4 <*> MyJust 36 <*> MyJust 14 <*> MyJust 20 <*> MyJust 30
MyJust 100
> pure sum4 <*> MyJust 36 <*> MyJust 14 <*> MyNothing <*> MyJust 30
MyNothing
> pure sum4 <*> MyNothing <*> MyJust 14 <*> MyJust 20 <*> MyJust 30
MyNothing

Вследствии повсеместного каррирования в Haskell, sum4 действует на аппликативный контекст MyMaybe, обновляя этот контекст после каждой аппликации (после каждого <*>). Мы видим, как наш контекст эволюционирует, доходя до последнего, четвёртого аргумента. Мы видим также, как аппликакции прекращаются когда <*> натыкается на MyNothing, и контекст посреди вычисления вдруг становится постоянным (принимает значение MyNothing), протаскивая “ничего” до самого конца.

Мы в самом деле можем думать об аппликативном контексте, как о некоторой упорядоченной последовательности аппликаций. Например, мы можем оборвать вычисление суммы четырёх чисел посредине и попробовать два различных “продолжения” прерванному аппликативному контексту:

> let part1 = pure sum4 <*> MyJust 36 <*> MyJust 14
> part1 <*> MyJust 20 <*> MyJust 30
MyJust 100
> part1 <*> MyNothing <*> MyJust 30
MyNothing

2.Determine the correct definition of pure for the ZipList instance of Applicative—there is only one implementation that satisfies the law relating pure and (<*>).

Определение ZipList дано такое:

newtype ZipList a = ZipList { getZipList :: [a] }
 
instance Applicative ZipList where
  pure = undefined   -- наше упражнение!
  (ZipList gs) <*> (ZipList xs) = ZipList (zipWith ($) gs xs)

Прежде чем написать pure для Applicative ZipList, посмотрим как работает аппликация с функцией zipWith внутри такого контекста:

> ZipList [(+), (+)] <*> ZipList [1,2,3] <*> ZipList [10,20,30]
ZipList {getZipList = [11,22]}
> ZipList [(+), (*), (-), (+)] <*> ZipList [1,2,3] <*> ZipList [10,20,30]
ZipList {getZipList = [11,40,-27]}

То есть аппликация просто позиционно “сшивает” список функций с их параметрами, используя обычную операцию применения функций ($) к аргументам. Для каждой функции из списков аргументов снимается по штуке, поэтому функций должно быть столько, сколько всего элементов в кратчайшем списке аргументов.

Элегантное решение с функцией repeat, которая повторяет свой параметр до бесконечности:

instance Applicative ZipList where
  pure = ZipList . repeat
  -- ...

Мы втягиваем в аппликативный контекст бесконечное количество функций, поэтому наш функтор будет работать со списками аргументов любой длины.

Аппликативные функторы

Некоторые аппликативные функторы из Prelude Haskell:

instance Applicative (Either e) -- Defined in ‘Data.Either’
instance Applicative [] -- Defined in ‘GHC.Base’
instance Applicative Maybe -- Defined in ‘GHC.Base’
instance Applicative IO -- Defined in ‘GHC.Base’
instance Applicative ((->) a) -- Defined in ‘GHC.Base’
instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’

Applicative [] реализует аппликацию не “сшиванием”, как ZipList выше, а последовательным применением функции к каждому из входов и сбором результатов:

>  [(+), (*)] <*> [1,2] <*> [100,1000]
[101,1001,102,1002,100,1000,200,2000]

То есть поддерживает интуицию списка как аппликативного контекста недетерминированного выбора – мы получаем все возможные результаты одновременно.

Но мне больше всего нравится последний аппликативный функтор. Это – пара, первый элемент которой представляет собой некоторой моноид (алгебраическую структуру с ассоциативной бинарной операцией и нейтральным элементом относительно неё). Моноидальные типы в Haskell представлены тайпклассом Monoid:

class Monoid a where
  -- нейтральный элемент операции mappend
  mempty :: a
  -- собственно, сама бинарная операция (т.н. сложение, конкатенация и т.п.)
  mappend :: a -> a -> a

Одна из возможных интуиций, связанных с этим довольно общим объектом, – это некий магазин (обойма), некий аккумулятор или разрастающаяся цепочка (строка), звенья которой лепятся друг к другу бинарной операцией mappend^[Разумеется, структура замкнута относительно введённой в ней операции: оба элемента принадлежат множеству-носителю, как и их “сумма”. Этот факт из алгебры в Haskell соответствует одинаковым типам в сигнатуре mappend, а также тому факту, что конструкторы значений a не выводят за границы типа.]. Например, Monoid [] реализует mappend обычной конкатенацией списков, нейтральным элементом выступает пустой список (склеивание с пустым списком не меняет исходного списка).

Вернёмся к нашему прекрасному аппликативному функтору. Чтобы понять его смысл, надо понять его <*>. Вот моя реализация:

instance Monoid a => Applicative ((,) a) where
  pure x (a, _) = (mempty a, x)
  (<*>) (a, f) (a', x) = (a `mappend` a', f x)

Данный аппликативный контекст умеет производить вычисления, параллельно накапливая результат в некой моноидальной структуре (первый элемент пары). Например, в списке:

> pure sum4 <*> (["add one"], 1) <*> (["add two"], 2) <*> (["add three"], 3) <*> (["add ten"], 10)
(["add one","add two","add three","add ten"],16)

Таким образом можно аккумулировать, например, логи происходящего внутри контекста: промежуточные результаты вычислений, шаги, ошибки, стек вызова и т.п. Реализация умеет деконструировать текущий (f (a -> b)) и следующий (f a) элементы в аппликативной цепочке, поэтому можно контролировать процесс вычисления, исходя из его истории (эволюции аппликативного окружения).