Мы представляем, что функция id
играет роль параметра f
в этой реализации. Нам видно, что если мы применяем fmap id
к значению Just x
, то результатом будет Just (id x)
, и поскольку id
просто возвращает свой параметр, мы можем сделать вывод, что Just (id x)
равно Just x
. Теперь нам известно, что если мы применим функцию id
к значению типа Maybe
, созданному с помощью конструктора данных Just
, обратно мы получим то же самое значение.
Видно, что применение функции id
к значению Nothing
возвращает то же самое значение Nothing
. Поэтому из этих двух равенств в реализации функции fmap
нам видно, что закон fmap id = id
соблюдается.
Закон 2
Второй закон гласит, что композиция двух функций и последующее применение результирующей функции к функтору должны давать тот же результат, что и применение первой функции к функтору, а затем применение другой. В формальной записи это выглядит так: fmap (f . g) = fmap f . fmap g
. Или если записать по-другому, то для любого значения функтора x
должно выполняться следующее: fmap
(f
.
g)
x
=
fmap
f
(fmap
g
x)
.
Если мы выявили, что некоторый тип подчиняется двум законам функторов, надо надеяться, что он обладает такими же фундаментальными поведениями, как и другие функторы, когда дело доходит до отображения. Мы можем быть уверены, что когда мы применяем к нему функцию fmap
, за кулисами ничего не произойдёт, кроме отображения, и он будет действовать как сущность, которая может быть отображена – то есть функтор.
Можно выяснить, как второй закон выполняется по отношению к некоторому типу, посмотрев на реализацию функции fmap
для этого типа, а затем использовав метод, который мы применяли, чтобы проверить, подчиняется ли тип Maybe
первому закону. Итак, чтобы проверить, как второй закон функторов выполняется для типа Maybe
, если мы применим выражение fmap
(f . g)
к значению Nothing
, мы получаем то же самое значение Nothing
, потому что применение любой функции к Nothing
даёт Nothing
. Если мы выполним выражение fmap f (fmap g Nothing)
, то получим результат Nothing
по тем же причинам.
Довольно просто увидеть, как второй закон выполняется для типа Maybe
, когда значение равно Nothing
. Но что если это значение Just
? Ладно – если мы выполним fmap (f . g) (Just x)
, из реализации нам будет видно, что это реализовано как Just ((f . g) x)
; аналогичной записью было бы Just (f (g x))
. Если же мы выполним fmap f (fmap g (Just x))
, то из реализации увидим, что fmap g (Just x)
– это Just (g x)
. Следовательно, fmap f (fmap g (Just x))
равно fmap f (Just (g x))
, а из реализации нам видно, что это равнозначно Just (f (g x))
.
Если вы немного смущены этим доказательством, не волнуйтесь. Убедитесь, что вы понимаете, как устроена композиция функций. Часто вы можете интуитивно понимать, как выполняются эти законы, поскольку типы действуют как контейнеры или функции. Вы также можете просто проверить их на нескольких разных значениях типа – и сумеете с определённой долей уверенности сказать, что тип действительно подчиняется этим законам.
Нарушение закона
Давайте посмотрим на «патологический» пример конструктора типов, который является экземпляром класса типов Functor
, но не является функтором, потому что он не выполняет законы. Скажем, у нас есть следующий тип:
data CMaybe a = CNothing | CJust Int a deriving (Show)
Буква C
здесь обозначает счётчик. Это тип данных, который во многом похож на тип Maybe a
, только часть Just
содержит два поля вместо одного. Первое поле в конструкторе данных CJust
всегда имеет тип Int
; оно будет своего рода счётчиком. Второе поле имеет тип a
, который берётся из параметра типа, и его тип будет зависеть от конкретного типа, который мы выберем для CMaybe a
. Давайте поэкспериментируем с нашим новым типом:
ghci> CNothing
CNothing
ghci> CJust 0 "ха-ха"
CJust 0 "ха-ха"
ghci> :t CNothing
CNothing :: CMaybe a
ghci> :t CJust 0 "ха-ха"
CJust 0 "ха-ха" :: CMaybe [Char]
ghci> CJust 100 [1,2,3]
CJust 100 [1,2,3]
Если мы используем конструктор данных CNothing
, в нём нет полей. Если мы используем конструктор данных CJust
, первое поле является целым числом, а второе может быть любого типа. Давайте сделаем этот тип экземпляром класса Functor
, так чтобы каждый раз, когда мы используем функцию fmap
, функция применялась ко второму полю, а первое поле увеличивалось на 1
:
instance Functor CMaybe where
fmap f CNothing= CNothing