Если мы отображаем список символов с помощью функции compare
, которая имеет тип (Ord a) => a –> a –> Ordering
, то получаем список функций типа Char –> Ordering
, потому что функция compare
частично применяется с помощью символов в списке. Это не список функций типа (Ord a) => a –> Ordering
, так как первый идентификатор переменной типа a
имел тип Char
, а потому и второе вхождение a
обязано принять то же самое значение – тип Char
.
Мы видим, как, отображая значения функторов с помощью «многопараметрических» функций, мы получаем значения функторов, которые содержат внутри себя функции. А что теперь с ними делать?.. Мы можем, например, отображать их с помощью функций, которые принимают эти функции в качестве параметров – поскольку, что бы ни находилось в значении функтора, оно будет передано функции, с помощью которой мы его отображаем, в качестве параметра.
ghci> let a = fmap (*) [1,2,3,4]
ghci> :t a
a :: [Integer –> Integer]
ghci> fmap (\f –> f 9) a
[9,18,27,36]
Но что если у нас есть значение функтора Just (3 *)
и значение функтора Just 5
, и мы хотим извлечь функцию из Just (3 *)
и отобразить с её помощью Just 5
? С обычными функторами у нас этого не получится, потому что они поддерживают только отображение имеющихся функторов с помощью обычных функций. Даже когда мы отображали функтор, содержащий функции, с помощью анонимной функции \f –> f 9
, мы делали именно это и только это. Но используя то, что предлагает нам функция fmap
, мы не можем с помощью функции, которая находится внутри значения функтора, отобразить другое значение функтора. Мы могли бы произвести сопоставление конструктора Just
по образцу для извлечения из него функции, а затем отобразить с её помощью Just 5
, но мы ищем более общий и абстрактный подход, работающий с функторами.
Поприветствуйте аппликативные функторы
Итак, встречайте класс типов Applicative
, находящийся в модуле Control.Applicative
!.. Он определяет две функции: pure
и <*>
. Он не предоставляет реализации по умолчанию для какой-либо из этих функций, поэтому нам придётся определить их обе, если мы хотим, чтобы что-либо стало аппликативным функтором. Этот класс определён вот так:
class (Functor f) => Applicative f where
pure :: a –> f a
(<*>) :: f (a –> b) –> f a –> f b
Простое определение класса из трёх строк говорит нам о многом!.. Первая строка начинается с определения класса Applicative
; также она вводит ограничение класса. Ограничение говорит, что если мы хотим определить для типа экземпляр класса Applicative
, он, прежде всего, уже должен иметь экземпляр класса Functor
. Вот почему, когда нам известно, что конструктор типа принадлежит классу Applicative
, можно смело утверждать, что он также принадлежит классу Functor
, так что мы можем применять к нему функцию fmap
.
Первый метод, который он определяет, называется pure
. Его сигнатура выглядит так: pure :: a –> f a
. Идентификатор f
играет здесь роль нашего экземпляра аппликативного функтора. Поскольку язык Haskell обладает очень хорошей системой типов и притом всё, что может делать функция, – это получать некоторые параметры и возвращать некоторое значение, мы можем многое сказать по объявлению типа, и данный тип – не исключение.
Функция pure
должна принимать значение любого типа и возвращать аппликативное значение с этим значением внутри него. Словосочетание «внутри него» опять вызывает в памяти нашу аналогию с коробкой, хотя мы и видели, что она не всегда выдерживает проверку. Но тип a –> f a
всё равно довольно нагляден. Мы берём значение и оборачиваем его в аппликативное значение, которое содержит в себе это значение в качестве результата. Лучший способ представить себе функцию pure
– это сказать, что она берёт значение и помещает его в некий контекст по умолчанию (или чистый контекст) – минимальный контекст, который по-прежнему возвращает это значение.
Оператор <*>
действительно интересен. У него вот такое определение типа:
f (a –> b) –> f a –> f b
Напоминает ли оно вам что-нибудь? Оно похоже на сигнатуру fmap
::
(a
–>
b)
–>
f
a
–>
f
b
. Вы можете воспринимать оператор <*>
как разновидность расширенной функции fmap
. Тогда как функция fmap
принимает функцию и значение функтора и применяет функцию внутри значения функтора, оператор <*>
принимает значение функтора, который содержит в себе функцию, и другой функтор – и извлекает эту функцию из первого функтора, затем отображая с её помощью второй.
Аппликативный функтор Maybe
Давайте взглянем на реализацию экземпляра класса Applicative
для типа Maybe
: