Легко увидеть, что вызов выражения pure f <*> xs
при использовании списков эквивалентен выражению fmap f xs
. Результат вычисления pure f
– это просто [f]
, а выражение [f] <*> xs
применит каждую функцию в левом списке к каждому значению в правом; но в левом списке только одна функция, и, следовательно, это похоже на отображение.
Тип IO – тоже аппликативный функтор
Другой экземпляр класса Applicative
, с которым мы уже встречались, – экземпляр для типа IO
. Вот как он реализован:
instance Applicative IO where
pure = return
a <*> b = do
f <– a
x <– b
return (f x)
Поскольку суть функции pure
состоит в помещении значения в минимальный контекст, который всё ещё содержит значение как результат, логично, что в случае с типом IO
функция pure
– это просто вызов return
. Функция return
создаёт действие ввода-вывода, которое ничего не делает. Оно просто возвращает некое значение в качестве своего результата, не производя никаких операций ввода-вывода вроде печати на терминал или чтения из файла.
Если бы оператор <*>
ограничивался работой с типом IO
, он бы имел тип (<*>) :: IO (a –> b) –> IO a –> IO b
. В случае с типом IO
он принимает действие ввода-вывода a
, которое возвращает функцию, выполняет действие ввода-вывода и связывает эту функцию с идентификатором f
. Затем он выполняет действие ввода-вывода b
и связывает его результат с идентификатором x
. Наконец, он применяет функцию f
к значению x
и возвращает результат этого применения в качестве результата. Чтобы это реализовать, мы использовали здесь синтаксис do
. (Вспомните, что суть синтаксиса do
заключается в том, чтобы взять несколько действий ввода-вывода и «склеить» их в одно.)
При использовании типов Maybe
и []
мы могли бы воспринимать применение функции <*>
просто как извлечение функции из её левого параметра, а затем применение её к правому параметру. В отношении типа IO
извлечение остаётся в силе, но теперь у нас появляется понятие помещения в последовательность, поскольку мы берём два действия ввода-вывода и «склеиваем» их в одно. Мы должны извлечь функцию из первого действия ввода-вывода, но для того, чтобы можно было извлечь результат из действия ввода-вывода, последнее должно быть выполнено. Рассмотрите вот это:
myAction :: IO String
myAction = do
a <– getLine
b <– getLine
return $ a ++ b
Это действие ввода-вывода, которое запросит у пользователя две строки и вернёт в качестве своего результата их конкатенацию. Мы достигли этого благодаря «склеиванию» двух действий ввода-вывода getLine
и return
, поскольку мы хотели, чтобы наше новое «склеенное» действие ввода-вывода содержало результат выполнения a ++ b
. Ещё один способ записать это состоит в использовании аппликативного стиля:
myAction :: IO String
myAction = (++) <$> getLine <*> getLine
Это то же, что мы делали ранее, когда создавали действие ввода-вывода, которое применяло функцию между результатами двух других действий ввода-вывода. Вспомните, что функция getLine
– это действие ввода-вывода, которое имеет тип getLine :: IO String
. Когда мы применяем оператор <*>
между двумя аппликативными значениями, результатом является аппликативное значение, так что всё это имеет смысл.
Если мы вернёмся к аналогии с коробками, то можем представить себе функцию getLine
как коробку, которая выйдет в реальный мир и принесёт нам строку. Выполнение выражения (++) <$> getLine <*> getLine
создаёт другую, бо́льшую коробку, которая посылает эти две коробки наружу для получения строк с терминала, а потом возвращает конкатенацию этих двух строк в качестве своего результата.
Выражение (++) <$> getLine <*> getLine
имеет тип IO String
. Это означает, что данное выражение является совершенно обычным действием ввода-вывода, как и любое другое, тоже возвращая результирующее значение, подобно другим действиям ввода-вывода. Вот почему мы можем выполнять следующие вещи:
main = do
a <– (++) <$> getLine <*> getLine
putStrLn $ "Две строки, соединённые вместе: " ++ a
Функции в качестве аппликативных функторов
Ещё одним экземпляром класса Applicative
является тип (–>) r
, или функции. Мы нечасто используем функции в аппликативном стиле, но концепция, тем не менее, действительно интересна, поэтому давайте взглянем, как реализован экземпляр функции[12].
instance Applicative ((–>) r) where
pure x = (\_ –> x)
f <*> g = \x –> f x (g x)