ghci> fmap (*2) EmptyTree
EmptyTree
ghci> fmap (*4) (foldr treeInsert EmptyTree [5,7,3])
Node 20 (Node 12 EmptyTree EmptyTree) (Node 28 EmptyTree EmptyTree)
Впрочем, тут следует быть внимательным! Если тип Tree
используется для представления бинарного дерева поиска, то нет никакой гарантии, что дерево останется таковым после применения к каждому его узлу некоторой функции. Проход по дереву функцией, скажем, negate
превратит дерево поиска в обычное дерево.
И тип Either является функтором
Отлично! Ну а теперь как насчёт Either a b
? Можно ли сделать его функтором? Класс типов Functor
требует конструктор типов с одним параметром, а у типа Either
их два. Гм-м… Придумал – мы частично применим конструктор Either
, «скормив» ему один параметр, и таким образом он получит один свободный параметр. Вот как для типа Either
определён экземпляр класса Functor
в стандартных библиотеках:
instance Functor (Either a) where
fmap f (Right x) = Right (f x)
fmap f (Left x) = Left x
Что же здесь происходит? Как видно из записи, мы сделали экземпляр класса не для типа Either
, а для Either a
. Это потому, что Either
– конструктор типа, который принимает два параметра, а Either a
– только один. Если бы функция fmap
была только для Either
a
, сигнатура типа выглядела бы следующим образом:
(b –> c) –> Either a b –> Either a c
поскольку это то же самое, что
(b –> c) –> (Either a) b –> (Either a) c
В реализации мы выполняем отображение в конструкторе данных Right
, но не делаем этого в Left
. Почему? Вспомним, как определён тип Either
a
b
:
data Either a b = Left a | Right b
Если мы хотим применять некую функцию к обеим альтернативам, параметры a
и b
должны конкретизироваться одним и тем же типом. Если попытаться применить функцию, которая принимает строку и возвращает строку, то b
у нас – строка, а a
– число; это не сработает. Также, когда мы смотрели на тип функции fmap
для типа Either a
, то видели, что первый параметр не изменяется, а второй может быть изменён; первый параметр актуализируется конструктором данных Left
.
Здесь можно продолжить нашу аналогию с коробками, представив часть Left
как пустую коробку, на которой сбоку записано сообщение об ошибке, поясняющее, почему внутри пусто.
Отображения из модуля Data.Map
также можно сделать функтором, потому что они хранят (или не хранят) значения. Для типа Map k v
функция fmap
будет применять функцию v –> v'
на отображении типа Map k v
и возвращать отображение типа Map
k
v'
.
ПРИМЕЧАНИЕ. Обратите внимание: апостроф не имеет специального значения в типах (как не имеет его и в именовании значений). Этот символ используется для обозначения схожих понятий, незначительно отличающихся друг от друга.
Попытайтесь самостоятельно догадаться, как для типа Map k
определён экземпляр класса Functor
!
На примере класса типов Functor
мы увидели, что классы типов могут представлять довольно мощные концепции высокого порядка. Также немного попрактиковались в частичном применении типов и создании экземпляров. В одной из следующих глав мы познакомимся с законами, которые должны выполняться для функторов.
Сорта и немного тип-фу
Конструкторы типов принимают другие типы в качестве параметров для того, чтобы рано или поздно вернуть конкретный тип. Это в некотором смысле напоминает мне функции, которые принимают значения в качестве параметров для того, чтобы вернуть значение. Мы видели, что конструкторы типов могут быть частично применены, так же как и функции (Either String
– это тип, который принимает ещё один тип и возвращает конкретный тип, например, Either String Int
). Это очень интересно. В данном разделе мы рассмотрим формальное определение того, как типы применяются к конструкторам типов. Точно так же мы выясняли, как формально определяется применение значений к функциям по декларациям типов. Вам не обязательно читать этот раздел для того, чтобы продолжить своё волшебное путешествие в страну языка Haskell, и если вы не поймёте, что здесь изложено, – не стоит сильно волноваться. Тем не менее, если вы усвоили содержание данного раздела, это даст вам чёткое понимание системы типов.