Если это звучит немного непонятно, не беспокойтесь. Всё прояснится, когда мы рассмотрим несколько примеров.
Гм-м… что-то мне напоминает объявление функции 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)
Проверим: