Изучай Haskell во имя добра! - Миран Липовача
Шрифт:
Интервал:
Закладка:
ghci> :t fmap (*2)
fmap (*2) :: (Num a, Functor f) => f a –> f a
ghci> :t fmap (replicate 3)
fmap (replicate 3) :: (Functor f) => f a –> f [a]
Выражение fmap (*2) – это функция, которая получает функтор f над числами и возвращает функтор над числами. Таким функтором могут быть список, значение Maybe, Either String или что-то другое. Выражение fmap (replicate 3) получит функтор над любым типом и вернёт функтор над списком элементов данного типа. Это становится ещё очевиднее, если мы частично применим, скажем, fmap (++"!"), а затем привяжем её к имени в GHCi.
Вы можете рассматривать fmap двояко:
• как функцию, которая принимает функцию и значение функтора, а затем отображает это значение функтора с помощью данной функции;
• как функцию, которая принимает функцию и втягивает её в функтор, так чтобы она оперировала значениями функторов.
Обе точки зрения верны.
Тип fmap (replicate 3) :: (Functor f) => f a –> f [a] означает, что функция будет работать с любым функтором. Что именно она будет делать, зависит от функтора. Если мы применим fmap (replicate 3) к списку, будет выбрана реализация fmap для списка, то есть просто map. Если мы применим её к Maybe a, она применит replicate 3 к значению внутри Just. Если это значение равно Nothing, то оно останется равным Nothing. Вот несколько примеров:
ghci> fmap (replicate 3) [1,2,3,4]
[[1,1,1],[2,2,2],[3,3,3],[4,4,4]]
ghci> fmap (replicate 3) (Just 4)
Just [4,4,4]
ghci> fmap (replicate 3) (Right "ля")
Right ["ля","ля","ля"]
ghci> fmap (replicate 3) Nothing
Nothing
ghci> fmap (replicate 3) (Left "фуу")
Left "фуу"
Законы функторов
Предполагается, что все функторы проявляют определённые свойства и поведение. Они должны надёжно вести себя как сущности, которые можно отобразить. Применение функции fmap к функтору должно только отобразить функтор с помощью функции – ничего более. Это поведение описано в законах функторов. Все экземпляры класса Functor должны следовать этим двум законам. Язык Haskell не принуждает, чтобы эти законы выполнялись автоматически, поэтому вы должны проверять их сами, когда создаёте функтор. Все экземпляры класса Functor в стандартной библиотеке выполняют эти законы.
Закон 1
Первый закон функторов гласит, что если мы применяем функцию id к значению функтора, то значение функтора, которое мы получим, должно быть таким же, как первоначальное значение функтора. В формализованной записи это выглядит так: fmap id = id. Иными словами, если мы применим fmap id к значению функтора, это должно быть то же самое, что и просто применение функции id к значению. Вспомните, что id – это функция тождества, которая просто возвращает свой параметр неизменным. Она также может быть записана в виде x –> x. Если воспринимать значение функтора как нечто, что может быть отображено, то закон fmap id = id представляется довольно очевидным.
Давайте посмотрим, выполняется ли он для некоторых значений функторов:
ghci> fmap id (Just 3)
Just 3
ghci> id (Just 3)
Just 3
ghci> fmap id [1..5]
[1,2,3,4,5]
ghci> id [1..5]
[1,2,3,4,5]
ghci> fmap id []
[]
ghci> fmap id Nothing
Nothing
Если посмотреть на реализацию функцию fmap, например, для типа Maybe, мы можем понять, почему выполняется первый закон функторов:
instance Functor Maybe where
fmap f (Just x) = Just (f x)
fmap f Nothing= Nothing
Мы представляем, что функция 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
fmap f (CJust counter x) = CJust (counter+1) (f x)
Это отчасти похоже на реализацию экземпляра для типа Maybe, только когда функция fmap применяется к значению, которое не представляет пустую коробку (значение CJust), мы не просто применяем функцию к содержимому, но и увеличиваем счётчик на 1. Пока вроде бы всё круто! Мы даже можем немного поиграть с этим: