Ну разве не здорово?! Аппликативные функторы и аппликативный стиль вычисления pure f <*> x <*> y <*>
… позволяют взять функцию, которая ожидает параметры, не являющиеся аппликативными значениями, и использовать эту функцию для работы с несколькими аппликативными значениями. Функция может принимать столько параметров, сколько мы захотим, потому что она всегда частично применяется шаг за шагом между вхождениями оператора <*>
.
Это становится ещё более удобным и очевидным, если мы примем во внимание тот факт, что выражение pure f <*> x
равно fmap f x
. Это один из законов аппликативных функторов, которые мы более подробно рассмотрим чуть позже; но давайте подумаем, как он применяется здесь. Функция pure
помещает значение в контекст по умолчанию. Если мы просто поместим функцию в контекст по умолчанию, а затем извлечём её и применим к значению внутри другого аппликативного функтора, это будет то же самое, что просто отобразить этот аппликативный функтор с помощью данной функции. Вместо записи pure f <*> x <*> y <*>
…, мы можем написать fmap
f
x
<*>
y
<*>
… Вот почему модуль Control.Applicative
экспортирует оператор, названный <$>
, который является просто синонимом функции fmap
в виде инфиксного оператора. Вот как он определён:
(<$>) :: (Functor f) => (a –> b) –> f a –> f b
f <$> x = fmap f x
ПРИМЕЧАНИЕ. Вспомните, что переменные типов не зависят от имён параметров или имён других значений. Здесь идентификатор f
в сигнатуре функции является переменной типа с ограничением класса, которое говорит, что любой конструктор типа, который заменяет f
, должен иметь экземпляр класса Functor
. Идентификатор f
в теле функции обозначает функцию, с помощью которой мы отображаем значение x
. Тот факт, что мы использовали f
для представления обеих вещей, не означает, что они представляют одну и ту же вещь.
При использовании оператора <$>
аппликативный стиль проявляет себя во всей красе, потому что теперь, если мы хотим применить функцию f
к трем аппликативным значениям, можно просто написать f <$> x <*> y <*> z
. Если бы параметры были обычными значениями, мы бы написали f x y z
.
Давайте подробнее рассмотрим, как это работает. Предположим, что мы хотим соединить значения Just "johntra"
и Just "volta"
в одну строку, находящуюся внутри функтора Maybe
. Сделать это вполне в наших силах!
ghci> (++) <$> Just "johntra" <*>
Just "volta" Just "johntravolta"
Прежде чем мы увидим, что происходит, сравните предыдущую строку со следующей:
ghci> (++) "johntra" "volta"
"johntravolta"
Чтобы использовать обычную функцию с аппликативным функтором, просто разбросайте вокруг несколько <$>
и <*>
, и функция будет работать с аппликативными значениями и возвращать аппликативное значение. Ну не здорово ли?
Возвратимся к нашему выражению (++) <$> Just "джонтра" <*> Just "волта"
: сначала оператор (++)
, который имеет тип (++) :: [a] – > [a] –> [a]
, отображает значение Just "джонтра"
. Это даёт в результате такое же значение, как Just ("джонтра"++)
, имеющее тип Maybe ([Char] –> [Char])
. Заметьте, как первый параметр оператора (++)
был «съеден» и идентификатор a
превратился в тип [Char]
! А теперь выполняется выражение Just ("джонтра"++) <*> Just "волта"
, которое извлекает функцию из Just
и отображает с её помощью значение Just "волта"
, что в результате даёт новое значение – Just "джонтраволта"
. Если бы одним из двух значений было значение Nothing
, результатом также было бы Nothing
.
Списки
Списки (на самом деле конструктор типа списка, []
) являются аппликативными функторами. Вот так сюрприз! Вот как []
является экземпляром класса Applicative
:
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <– fs, x <– xs]