Мы говорили о классах типов, которые определяют операции для проверки двух элементов на равенство и для сравнения двух элементов по размещению их в каком-либо порядке. Это очень абстрактное и элегантное поведение, хотя мы не воспринимаем его как нечто особенное, поскольку нам доводилось наблюдать его большую часть нашей жизни. В главе 7 были введены функторы – они являются типами, значения которых можно отобразить. Это пример полезного и всё ещё довольно абстрактного свойства, которое могут описать классы типов. В этой главе мы ближе познакомимся с функторами, а также с немного более сильными и более полезными их версиями, которые называются
Функторы возвращаются
Как вы узнали из главы 7, функторы – это сущности, которые можно отобразить, как, например, списки, значения типа Maybe
и деревья. В языке Haskell они описываются классом типов Functor
, содержащим только один метод fmap
. Функция fmap
имеет тип fmap :: (a –> b) –> f a –> f b
, который говорит: «Дайте мне функцию, которая принимает a
и возвращает b
и коробку, где содержится a
(или несколько a), и я верну коробку с b
(или несколькими b
) внутри». Она применяет функцию к элементу внутри коробки.
Мы также можем воспринимать значения функторов как значения с добавочным контекстом. Например, значения типа Maybe
обладают дополнительным контекстом того, что вычисления могли окончиться неуспешно. По отношению к спискам контекстом является то, что значение может быть множественным либо отсутствовать. Функция fmap
применяет функцию к значению, сохраняя его контекст.
Если мы хотим сделать конструктор типа экземпляром класса Functor
, он должен иметь сорт *
–>
*
; это значит, что он принимает ровно один конкретный тип в качестве параметра типа. Например, конструктор Maybe
может быть сделан экземпляром, так как он получает один параметр типа для произведения конкретного типа, как, например, Maybe Int
или Maybe String
. Если конструктор типа принимает два параметра, как, например, конструктор Either
, мы должны частично применять конструктор типа до тех пор, пока он не будет принимать только один параметр. Поэтому мы не можем написать определение Functor Either where
, зато можем написать определение Functor (Either a) where
. Затем, если бы мы вообразили, что функция fmap
предназначена только для работы со значениями типа Either a
, она имела бы следующее описание типа:
fmap :: (b –> c) –> Either a b –> Either a c
Как видите, часть Either
a
– фиксированная, потому что частично применённый конструктор типа Either a
принимает только один параметр типа.
Действия ввода-вывода в качестве функторов
К настоящему моменту вы изучили, каким образом многие типы (если быть точным, конструкторы типов) являются экземплярами класса Functor: []
и Maybe
, Either a
, равно как и тип Tree
, который мы создали в главе 7. Вы видели, как можно отображать их с помощью функций на всеобщее благо. Теперь давайте взглянем на экземпляр типа IO
.
Если какое-то значение обладает, скажем, типом IO String
, это означает, что перед нами действие ввода-вывода, которое выйдет в реальный мир и получит для нас некую строку, которую затем вернёт в качестве результата. Мы можем использовать запись <–
в синтаксисе do
для привязывания этого результата к имени. В главе 8 мы говорили о том, что действия ввода-вывода похожи на ящики с маленькими ножками, которые выходят наружу и приносят нам какое-то значение из внешнего мира. Мы можем посмотреть, что они принесли, но после просмотра нам необходимо снова обернуть значение в тип IO
. Рассматривая эту аналогию с ящиками на ножках, вы можете понять, каким образом тип IO
действует как функтор.
Давайте посмотрим, как же это тип IO
является экземпляром класса Functor
… Когда мы используем функцию fmap
для отображения действия ввода-вывода с помощью функции, мы хотим получить обратно действие ввода-вывода, которое делает то же самое, но к его результирующему значению применяется наша функция. Вот код:
instance Functor IO where
fmap f action = do
result <– action
return (f result)
Результатом отображения действия ввода-вывода с помощью чего-либо будет действие ввода-вывода, так что мы сразу же используем синтаксис do
для склеивания двух действий и создания одного нового. В реализации для метода fmap
мы создаём новое действие ввода-вывода, которое сначала выполняет первоначальное действие ввода-вывода, давая результату имя result
. Затем мы выполняем return (f result)
. Вспомните, что return
– это функция, создающая действие ввода-вывода, которое ничего не делает, а только возвращает что-либо в качестве своего результата.