Если это звучит немного непонятно, не беспокойтесь. Всё прояснится, когда мы рассмотрим несколько примеров.
Гм-м… что-то мне напоминает объявление функции fmap
! Если вы не знаете сигнатуру функции map
, вот она:
map :: (a –> b) –> [a] –> [b]
О, как интересно! Функция map
берёт функцию из a
в b
и список элементов типа a
и возвращает список элементов типа b
. Друзья, мы только что обнаружили функтор! Фактически функция map
– это функция fmap
, которая работает только на списках. Вот как список сделан экземпляром класса Functor
:
instance Functor [] where
fmap = map
И всё! Заметьте, мы не пишем instance Functor [a] where
, потому что из определения функции
fmap :: (a –> b) –> f a –> f b
мы видим, что параметр f
должен быть конструктором типов, принимающим один тип. Выражение [a]
– это уже конкретный тип (список элементов типа a
), а вот []
– это конструктор типов, который принимает один тип; он может производить такие конкретные типы, как [Int]
, [String]
или даже [[String]]
.
Так как для списков функция fmap
– это просто map
, то мы получим одинаковые результаты при их использовании на списках:
map :: (a –> b) –> [a] –> [b]
ghci>fmap (*2) [1..3]
[2,4,6]
ghci> map (*2) [1..3]
[2,4,6]
Что случится, если применить функцию map
или fmap
к пустому списку? Мы получим опять же пустой список. Но функция fmap
преобразует пустой список типа [a]
в пустой список типа [b]
.
Экземпляр класса Functor для типа Maybe
Типы, которые могут вести себя как контейнеры по отношению к другим типам, могут быть функторами. Можно представить, что списки – это коробки с бесконечным числом отсеков; все они могут быть пустыми, или же один отсек заполнен, а остальные пустые, или несколько из них заполнены. А что ещё умеет быть контейнером для других типов? Например, тип Maybe
. Он может быть «пустой коробкой», и в этом случае имеет значение Nothing
, или же в нём хранится какое-то одно значение, например "ХА-ХА"
, и тогда он равен Just
"ХА-ХА"
.
Вот как тип Maybe
сделан функтором:
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing = Nothing
Ещё раз обратите внимание на то, как мы записали декларацию instance Functor Maybe where
вместо instance Functor (Maybe m) where
– подобно тому как мы делали для класса YesNo
. Функтор принимает конструктор типа с одним параметром, не конкретный тип. Если вы мысленно замените параметр f
на Maybe
, функция fmap
работает как (a –> b) –> Maybe a –> Maybe b
, только для типа Maybe
, что вполне себя оправдывает. Но если заменить f
на (Maybe m)
, то получится (a –> b) –> Maybe m a –> Maybe m b
, что не имеет никакого смысла, так как тип Maybe
принимает только один тип-параметр.
Как бы то ни было, реализация функции fmap
довольно проста. Если значение типа Maybe
– это Nothing
, возвращается Nothing
. Если мы отображаем «пустую коробку», мы получим «пустую коробку», что логично. Точно так же функция map
для пустого списка возвращает пустой список. Если это не пустое значение, а некоторое значение, упакованное в конструктор Just
, то мы применяем функцию к содержимому Just
:
ghci> fmap (++ " ПРИВЕТ, Я ВНУТРИ JUST") (Just "Серьёзная штука.")
Just "Серьёзная штука. ПРИВЕТ, Я ВНУТРИ JUST"
ghci> fmap (++ " ПРИВЕТ, Я ВНУТРИ JUST") Nothing
Nothing
ghci> fmap (*2) (Just 200)
Just 400
ghci> fmap (*2) Nothing
Nothing
Деревья тоже являются функторами
Ещё один тип, который можно отображать и сделать для него экземпляр класса Functor
, – это наш тип Tree
. Дерево может хранить ноль или более других элементов, и конструктор типа Tree
принимает один тип-параметр. Если бы мы хотели записать функцию fmap
только для типа Tree
, её сигнатура выглядела бы так: (a
–>
b)
–>
Tree
a
–>
Tree
b
.
Для этой функции нам потребуется рекурсия. Отображение пустого дерева возвращает пустое дерево. Отображение непустого дерева – это дерево, состоящее из результата применения функции к корневому элементу и из правого и левого поддеревьев, к которым также было применено отображение.
instance Functor Tree where
fmap f EmptyTree = EmptyTree
fmap f (Node x left right) = Node (f x) (fmap f left) (fmap f right)
Проверим: