В продолжении разговора о функторах стандартной библиотеки Haskell, подумаем над их законами.

Функториальные законы

Все реализации Functor должны подчиняться следующим двум законам:

fmap id = id
fmap (g . h) = (fmap g) . (fmap h)

Первый закон очень ёмок, несмотря на кажущуюся простоту. Он говорит нам о том, что втягивание функции тождества (id a = a) в вычислительный контекст не может изменять контекст. Второй закон говорит нам о дистрибутивности fmap относительно композиции: применение одной функции внутри контекста вслед за другой – это то же самое, что применение композиции этих функций внутри контекста. На самом деле, как оказывается, из первого закона можно вывести второй.

Упражнения

1. Although it is not possible for a Functor instance to satisfy the first Functor law but not the second (excluding undefined), the reverse is possible. Give an example of a (bogus) Functor instance which satisfies the second law but not the first.

Мой вариант тривиален. Тем не менее, он подчиняется лишь второму функториальному закону.

data List a = End | Cons a (List a)

instance Functor List where
  fmap _ _ = End

2. Which laws are violated by the evil Functor instance for list shown above: both laws, or the first law alone? Give specific counterexamples.

Неправильный функтор определён как

-- Evil Functor instance
instance Functor [] where
  fmap _ [] = []
  fmap g (x:xs) = g x : g x : fmap g xs

Лифтинг тождественной функции меняет контекст (контейнер) – он разрастается с каждым fmap. Ровно поэтому нарушен и второй функториальный закон: два fmap вставят в два раза больше лишних элементов, чем один fmap некоторой композиции g . h.

Лифтинг?

Поскольку, как и все функции в Haskell, fmap каррируется и на самом деле не принимает “два аргумента”, а принимает ровно один, но возвращает функцию, можно преписать сигнатуру fmap в таком виде:

fmap :: (a -> b) -> (f a -> f b)

что ещё раз подчёркивает тот факт, что fmap просто втягивает (т.н. lifting) обычную функцию в контекст, фактически преобразуя её в функцию над контекстами:

g  = (< 5) -- обычная функция над числами

g' = fmap g -- функция над вычислительными контекстами с числами

Легко видеть, как fmap сделал из “простой” функции функцию “f-мира” (тип нашего функтора):

g  :: (Num a, Ord a) => a -> Bool
g' :: (Functor f, Num a, Ord a) => f a -> f Bool

Сигнатуры подсказывают, как будут вести себя g и g'.

g есть отображение чего-то одновременно численного (класс Num) и упорядоченного (класс Ord), в тип Bool значений истина/ложь.

g' есть отображение чего-то, что функтор (класс Functor) поверх численного и упорядоченного содержания, в такой-же функтор поверх булевого типа.

> g 3
True

> -- Functor []
> g' [1, 3, 6, 9]
[True,True,False,False]

> -- Functor Maybe
> g' (Just 6)
Just False

> -- Functor Tree
> g' $ Node 5 [Node 4 [Node 3 []], Node 6 [Node 7 []]]
Node {rootLabel = False, subForest = [Node {rootLabel = True, subForest = [Node {rootLabel = True, subForest = []}]},Node {rootLabel = False, subForest = [Node {rootLabel = False, subForest = []}]}]}

> -- а можно сразу и нарисовать это дерево
> (putStrLn . drawTree . fmap show . g') $ Node 5 [Node 4 [Node 3 []], Node 6 [Node 7 []]]
False
|
+- True
|  |
|  `- True
|
`- False
   |
   `- False

Просто замечательно!