Каким же образом функции выступают в качестве функторов? Давайте взглянем на реализацию, которая находится в модуле Control.Monad.Instances
.
instance Functor ((–>) r) where
fmap f g = (\x –> f (g x))
Сначала подумаем над типом метода fmap
:
fmap :: (a –> b) –> f a –> f b
Далее мысленно заменим каждое вхождение идентификатора f
, являющегося ролью, которую играет наш экземпляр функтора, выражением (–>) r
. Это позволит нам понять, как функция fmap
должна вести себя в отношении данного конкретного экземпляра. Вот результат:
fmap :: (a –> b) –> ((–>) r a) –> ((–>) r b)
Теперь можно записать типы (–>) r a
и (–>) r b
в инфиксном виде, то есть r
–>
a
и r
–>
b
, как мы обычно поступаем с функциями:
fmap :: (a –> b) –> (r –> a) –> (r –> b)
Хорошо. Отображение одной функции с помощью другой должно произвести функцию, так же как отображение типа Maybe
с помощью функции должно произвести тип Maybe
, а отображение списка с помощью функции – список. О чём говорит нам предыдущий тип? Мы видим, что он берёт функцию из a
в b
и функцию из r
в a
и возвращает функцию из r
в b
. Напоминает ли это вам что-нибудь? Да, композицию функций!.. Мы присоединяем выход r –> a
ко входу a –> b
, чтобы получить функцию r –> b
, чем в точности и является композиция функций. Вот ещё один способ записи этого экземпляра:
instance Functor ((–>) r) where
fmap = (.)
Код наглядно показывает, что применение функции fmap
к функциям – это просто композиция функций.
В исходном коде импортируйте модуль Control.Monad.Instances
, поскольку это модуль, где определён данный экземпляр, а затем загрузите исходный код и попробуйте поиграть с отображением функций:
ghci> :t fmap (*3) (+100)
fmap (*3) (+100) :: (Num a) => a –> a
ghci> fmap (*3) (+100) 1
303
ghci> (*3) `fmap` (+100) $ 1
303
ghci> (*3) . (+100) $ 1
303
ghci> fmap (show . (*3)) (*100) 1
"300"
Мы можем вызывать fmap
как инфиксную функцию, чтобы сходство с оператором .
было явным. Во второй строке ввода мы отображаем (+100)
с помощью (*3)
, что даёт функцию, которая примет ввод, применит к нему (+100)
, а затем применит к этому результату (*3
). Затем мы применяем эту функцию к значению 1
.
Как и все функторы, функции могут восприниматься как значения с контекстами. Когда у нас есть функция вроде (+3)
, мы можем рассматривать значение как окончательный результат функции, а контекстом является то, что мы должны применить эту функцию к чему-либо, чтобы получить результат. Применение fmap (*3)
к (+100)
создаст ещё одну функцию, которая действует так же, как (+100)
, но перед возвратом результата к этому результату будет применена функция (*3)
.
Тот факт, что функция fmap
является композицией функций при применении к функциям, на данный момент не слишком нам полезен, но, по крайней мере, он вызывает интерес. Это несколько меняет наше сознание и позволяет нам увидеть, как сущности, которые действуют скорее как вычисления, чем как коробки (IO
и (–>) r
), могут быть функторами. Отображение вычисления с помощью функции возвращает тот же самый тип вычисления, но результат этого вычисления изменён функцией.
Перед тем как перейти к законам, которым должна следовать fmap
, давайте ещё раз задумаемся о типе fmap
:
fmap :: (a –> b) –> f a –> f b
Если помните, введение в каррированные функции в главе 5 началось с утверждения, что все функции в языке Haskell на самом деле принимают один параметр. Функция a –> b –> c
в действительности берёт только один параметр типа a
, после чего возвращает функцию b –> c
, которая принимает один параметр типа b
и возвращает значение типа c
. Вот почему вызов функции с недостаточным количеством параметров (её частичное применение) возвращает нам обратно функцию, принимающую несколько параметров, которые мы пропустили (если мы опять воспринимаем функции так, как если бы они принимали несколько параметров). Поэтому a –> b –> c
можно записать в виде a –>
(b
–> c)
, чтобы сделать каррирование более очевидным.