Если вы видите что-то необычное, просто сообщите мне. Skip to main content

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

Добро пожаловать во вторую часть серии о монадах и других функциональных структур. Мы продолжим готовить собирать нашу базу изучая идеи апликативных функторов. Если вы всё еще не имеете твердое понимание функторов, пересмотрите первую часть этой серии. Если вы считаете, что уже готовы к монадам, то можете смело переходить к части 3.

В этой части приведенные примеры можно будет опробовать на GHCI.

Функторы становятся короткими

В первой части, мы обсудили функтор типа класса. Мы нашли, то что это позволяет нам запустить преобразования данных в зависимости от того, во что обернуты данные. Не важно, являются ли наши данные List, Maybe, Either или даже свой собственный тип, мы можем просто вызывать fmap. Однако, что случится когда мы попробуем объединить обернутые данные? Для примера, если мы попробуем произвести эти вычисления с помощью GHCI, мы получим ошибку типа:

>> (Just 4) * (Just 5)
>> Nothing * (Just 2)

Могут ли функции помочь нам тут? Мы можем исопльзовать fmap чтобы обернуть умножение с помощью частичной обертывания Maybe значения:

>> let f = (*) <$> (Just 4)
>> :t f
f :: Num a => Maybe (a -> a)
>> (*) <$> Nothing
Nothing

Это дает частичную функци обернутую в Maybe. Но мы до сих пор не может развернуть это и применить к Just 5 в общем стиле. Поэтому нам нужно обратиться к коду специально для типа Maybe:

funcMaybe :: Maybe (a -> b) -> Maybe a -> Maybe b
funcMaybe Nothing _ = Nothing
funcMaybe (Just f) val = f <$> val

Это очевидно не будет работать с другими типами функторов.

Приложения в помощь

То что такое апликативные типы классов, говорят две главные функции:

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

Чистая фнукция принимает какое-то значение и обертывает его в минимальный контекст. Функция <*> вызывает последующиее приложение, которое принимает 2 параметра. Первый - принимает функци обернутую в конекст. Второе - обернутое значение. Вывод - результат применения функции к значению, преобразованное в контексте. Экземпляр называется аппликативный функтор так как он позволяет нам применять обернутую функцию. Так как последюущее применение принимает обернутую функцию, мы ычасто начинаем с обертки чего-то чистого или fmap. Это будет понятнее на примерах.

Для начала представим перемножение Maybe значений. Если мы умножаем на постоянное значение, мы можем использовать функторный подход. Но мы можем так же использовать аппликативный подход обернув постоянную функцию в чистую и затем использовать последовательное применение:

>> (4 *) <$> (Just 5)
Just 20
>> (4 *) <$> Nothing
Nothing
>> pure (4 *) <*> (Just 5)
Just 20
>> pure (4 *) <*> Nothing
Nothing

Теперь если мы хотим умножить 2 Maybe значения, мы начинаем оборачивать простую функцию произведения в чистую. Затем последовательно применяем оба Maybe значения:

>> pure (*) <*> (Just 4) <*> (Just 5)
Just 20
>> pure (*) <*> Nothing <*> (Just 5)
Nothing
>> pure (*) <*> (Just 4) <*> Nothing
Nothing

Реализация аппликативов

По этим примерам, мы можем сказать, что экземпляры Аппликативов для Maybe реализованны точно, как мы ожидаем. Чистая функция просто оборачивает начение с помощью Just. Затем связывает веще вмете, если другие функции или знаения будут Nothing, мы просто выводим Nothing. В противном случае применяем функцию к значение и переоборачиваем с помощью Just.

instance Applicative Maybe where
  pure = Just
  (<*>) Nothing _ = Nothing
  (<*>) _ Nothing = Nothing
  (<*>) (Just f) (Just x) = Just (f x)

Экземпляр аппликатива для List будет немного интереснее. Он может вести себя на так как мы ожидаем.

instance Applicative [] where
  pure a = [a]
  fs <*> xs = [f x | f <- fs, x <- xs]

Чистая функция - то что мы ожидаем. Мы принимаем значение и оборачиваем его как одиночку в список. Когда мы связываем операции, мы принимаем LIST функций. Мы должны ожидать применения каждой функции к значению в соответствующей позиции. Однако, на самом деле мы применяем функцию из первого списка к каждому значению из второго. Когда у нас олько одна функция, этот резаультат имеет понятноее поведение. Но когда у нас несколько функций, появляется отличие.

>> pure (4 *) <*> [1,2,3]
[4,8,12]
>> [(1+), (5*), (10*)] <*> [1,2,3]
[2,3,4,5,10,15,10,20,30]

Тут легко сделать определенные операции, как нахождение попарных результатов двух списков:

>> pure (*) <*> [1,2,3] <*> [10,20,30]
[10,20,30,20,40,60,30,60,90]

Вы возможно гадаете, как мы сделаем паралельное применение функторов. Например, мы можемт хотеть использовать второй список из примера выше, но иметь результат [2,10,30]. Для этого есть конструкт под названием Ziplist, это новый тип вокрут списка, для которого поведение экземпляр аппликатива и предусмотренно.

>> import Control.Applicative
>> ZipList [(1+), (5*), (10*)] <*> [5,10,15]
ZipList {getZipList = [6,50,150]}

Выводы

Если все это кажется непонятным, не бойтесь вернуться к части 1 и убедиться, что у вас есть четкое понимание того, что такое функторы. Если вам всё ясно, вы готовы перейти к части 3, где мы наконец испачкаемся монадами.

Все эти идея гораздно проще понять если попытаться исполнить код из примеров самостоятельно.