Функции в качестве функторов
Другим экземпляром класса Functor, с которым мы всё время имели дело, является (–>) r. Стойте!.. Что, чёрт возьми, означает (–>) r? Тип функции r –> a может быть переписан в виде (–>) r a, так же как мы можем записать 2 + 3 в виде (+) 2 3. Когда мы воспринимаем его как (–>) r a, то (–>) представляется немного в другом свете. Это просто конструктор типа, который принимает два параметра типа, как это делает конструктор Either.
Но вспомните, что конструктор типа должен принимать в точности один параметр типа, чтобы его можно было сделать экземпляром класса Functor. Вот почему нельзя сделать конструктор (–>) экземпляром класса Functor; однако, если частично применить его до (–>) r, это не составит никаких проблем. Если бы синтаксис позволял частично применять конструкторы типов с помощью сечений – подобно тому как можно частично применить оператор +, выполнив (2+), что равнозначно (+) 2, – вы могли бы записать (–>) r как (r –>).
Каким же образом функции выступают в качестве функторов? Давайте взглянем на реализацию, которая находится в модуле 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), чтобы сделать каррирование более очевидным.
Аналогичным образом, записав fmap :: (a –> b) –> (f a –> f b), мы можем воспринимать fmap не как функцию, которая принимает одну функцию и значение функтора и возвращает значение функтора, но как функцию, которая принимает функцию и возвращает новую функцию, которая такая же, как и прежняя, за исключением того, что она принимает значение функтора в качестве параметра и возвращает значение функтора в качестве результата. Она принимает функцию типа a –> b и возвращает функцию типа f a –> f b. Это называется «втягивание функции». Давайте реализуем эту идею, используя команду :t в GHCi:
ghci> :t fmap (*2)
fmap (*2) :: (Num a, Functor f) => f a –> f a
ghci> :t fmap (replicate 3)
fmap (replicate 3) :: (Functor f) => f a –> f [a]
Выражение fmap (*2) – это функция, которая получает функтор f над числами и возвращает функтор над числами. Таким функтором могут быть список, значение Maybe, Either String или что-то другое. Выражение fmap (replicate 3) получит функтор над любым типом и вернёт функтор над списком элементов данного типа. Это становится ещё очевиднее, если мы частично применим, скажем, fmap (++"!"), а затем привяжем её к имени в GHCi.
Вы можете рассматривать fmap двояко:
• как функцию, которая принимает функцию и значение функтора, а затем отображает это значение функтора с помощью данной функции;
• как функцию, которая принимает функцию и втягивает её в функтор, так чтобы она оперировала значениями функторов.
Обе точки зрения верны.
Тип fmap (replicate 3) :: (Functor f) => f a –> f [a] означает, что функция будет работать с любым функтором. Что именно она будет делать, зависит от функтора. Если мы применим fmap (replicate 3) к списку, будет выбрана реализация fmap для списка, то есть просто map. Если мы применим её к Maybe a, она применит replicate 3 к значению внутри Just. Если это значение равно Nothing, то оно останется равным Nothing. Вот несколько примеров:
ghci> fmap (replicate 3) [1,2,3,4]
[[1,1,1],[2,2,2],[3,3,3],[4,4,4]]
ghci> fmap (replicate 3) (Just 4)
Just [4,4,4]
ghci> fmap (replicate 3) (Right "ля")
Right ["ля","ля","ля"]
ghci> fmap (replicate 3) Nothing
Nothing
ghci> fmap (replicate 3) (Left "фуу")
Left "фуу"