Функции в качестве функторов 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Функции в качестве функторов



Другим экземпляром класса Functor, с которым мы всё время имели дело, является (–>) r. Стойте!.. Что, чёрт возьми, означает (–>) r? Тип функции r –> a может быть переписан в виде (–>) r a, так же как мы можем записать 2 + 3 в виде (+) 2 3. Когда мы воспринимаем его как (–>) r a, то (–>) представляется немного в другом свете. Это просто конструктор типа, который принимает два параметра типа, как это делает конструктор Either.

Но вспомните, что конструктор типа должен принимать в точ- ности один параметр типа, чтобы его можно было сделать экземп- ляром класса Functor. Вот почему нельзя сделать конструктор (–>) экземпляром класса Functor; однако, если частично применить его до (–>) r, это не составит никаких проблем. Если бы синтаксис позволял частично применять конструкторы типов с помощью се- чений – подобно тому как можно частично применить оператор +, выполнив (2+), что равнозначно (+) 2, – вы могли бы записать (–>) r как (r –>).

 
 

Каким же образом функции выступают в качестве функторов? Давайте взглянем на реализацию, которая находится в модуле Control.Monad.Instances.

instance Functor ((–>) r) where fmap f g = (\x –> f (g x))

 
 

Сначала подумаем над типом метода fmap:

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

 
 

Далее мысленно заменим каждое вхождение идентификато- ра f, являющегося ролью, которую играет наш экземпляр функто- ра, выражением (–>) r. Это позволит нам понять, как функция fmap должна вести себя в отношении данного конкретного экземпляра. Вот результат:

fmap:: (a –> b) –> ((–>) r a) –> ((–>) r b)

 
 

Теперь можно записать типы (–>) r a и (–>) r b в инфиксном виде, то есть r –> a и r –> b, как мы обычно поступаем с функциями:

fmap:: (a –> b) –> (r –> a) –> (r –> b)

Хорошо. Отображение одной функции с помощью другой должно произвести функцию, так же как отображение типа Maybe


 

 
 

с помощью функции должно произвести тип Maybe, а отображение списка с помощью функции – список. О чём говорит нам предыду- щий тип? Мы видим, что он берёт функцию из a в b и функцию из r в a и возвращает функцию из r в b. Напоминает ли это вам что-ни- будь? Да, композицию функций!.. Мы присоединяем выход r –> a ко входу a –> b, чтобы получить функцию r –> b, чем в точности и яв- ляется композиция функций. Вот ещё один способ записи этого экземпляра:

instance Functor ((–>) r) where fmap = (.)

Код наглядно показывает, что применение функции fmap к фун- кциям – это просто композиция функций.

 
 

В исходном коде импортируйте модуль Control.Monad.Instances, поскольку это модуль, где определён данный экземпляр, а затем за- грузите исходный код и попробуйте поиграть с отображением фун- кций:

ghci>:t fmap (*3) (+100)

fmap (*3) (+100):: (Num a) => a –> a ghci> fmap (*3) (+100) 1

ghci> (*3) `fmap` (+100) $ 1

ghci> (*3). (+100) $ 1

 
 

ghci> fmap (show. (*3)) (*100) 1 "300"

Мы можем вызывать fmap как инфиксную функцию, чтобы сходство с оператором. было явным. Во второй строке ввода мы отображаем (+100) с помощью (*3), что даёт функцию, которая примет ввод, применит к нему (+100), а затем применит к этому ре- зультату (*3). Затем мы применяем эту функцию к значению 1.

Как и все функторы, функции могут восприниматься как зна- чения с контекстами. Когда у нас есть функция вроде (+3), мы мо- жем рассматривать значение как окончательный результат фун- кции, а контекстом является то, что мы должны применить эту функцию к чему-либо, чтобы получить результат. Применение fmap (*3) к (+100) создаст ещё одну функцию, которая действует так же,


 

как (+100), но перед возвратом результата к этому результату будет применена функция (*3).

Тот факт, что функция fmap является композицией функций при применении к функциям, на данный момент не слишком нам полезен, но, по крайней мере, он вызывает интерес. Это несколь- ко меняет наше сознание и позволяет нам увидеть, как сущности, которые действуют скорее как вычисления, чем как коробки (IO и (–>) r), могут быть функторами. Отображение вычисления с по- мощью функции возвращает тот же самый тип вычисления, но ре- зультат этого вычисления изменён функцией.

Перед тем как перейти к законам, которым должна следовать

 
 

fmap, давайте ещё раз задумаемся о типе fmap:

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

Если помните, введение в каррированные функции в главе 5 на- чалосьс утверждения, чтовсефункции вязыке Haskellнасамомделе принимают один параметр. Функция a –> b –> c в действительности

берёт только один параметр типа a, после чего возвраща- ет функцию b –> c, которая принимает один параметр типа b и возвращает значе- ние типа c. Вот почему вызов функции с недостаточным количеством параметров (её частичное применение) воз- вращает нам обратно функ- цию, принимающую несколь- ко параметров, которые мы пропустили (если мы опять воспринимаем функции так, как если бы они принимали

несколько параметров). Поэтому a –> b –> c можно записать в виде

a –> (b –> c), чтобы сделать каррирование более очевидным.

Аналогичным образом, записав fmap:: (a –> b) –> (f a –> f b), мы можем воспринимать fmap не как функцию, которая принимает одну функцию и значение функтора и возвращает значение функ- тора, но как функцию, которая принимает функцию и возвращает


 

 
 

новую функцию, которая такая же, как и прежняя, за исключением того, что она принимает значение функтора в качестве параметра и возвращает значение функтора в качестве результата. Она прини- мает функцию типа a –> b и возвращает функцию типа f a –> f b. Это называется «втягивание функции». Давайте реализуем эту идею, ис- пользуя команду:t в GHCi:

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. Пока вроде бы всё круто! Мы даже можем немного поиграть с этим:

ghci> fmap (++"-ха") (CJust 0 "хо") CJust 1 "хо-ха"


 

ghci> fmap (++"-хе") (fmap (++"-ха") (CJust 0 "хо")) CJust 2 "хо-ха-хе"

 
 

ghci> fmap (++"ля") CNothing CNothing

Подчиняется ли этот тип законам функторов? Для того чтобы увидеть, что что-то не подчиняется закону, достаточно найти всего одно исключение.

ghci> fmap id (CJust 0 "ха-ха") CJust 1 "ха-ха"

 
 

ghci> id (CJust 0 "ха-ха") CJust 0 "ха-ха"

Как гласит первый закон функторов, если мы отобразим зна- чение функтора с помощью функции id, это должно быть то же са- мое, что и просто вызов функции id с тем же значением функтора. Наш пример показывает, что это не относится к нашему функтору CMaybe. Хотя он и имеет экземпляр класса Functor, он не подчиняет- ся данному закону функторов и, следовательно, не является функ- тором.

Поскольку тип CMaybe не является функтором, хотя он и при- творяется таковым, использование его в качестве функтора может привести к неисправному коду. Когда мы используем функтор, не должно иметь значения, производим ли мы сначала композицию нескольких функций, а затем с её помощью отображаем значение функтора, или же просто отображаем значение функтора после- довательно с помощью каждой функции. Но при использовании типа CMaybe это имеет значение, так как он следит, сколько раз его отобразили. Проблема!.. Если мы хотим, чтобы тип CMaybe подчи- нялся законам функторов, мы должны сделать так, чтобы поле ти- па Int не изменялось, когда используется функция fmap.

Вначале законы функторов могут показаться немного запутан- ными и ненужными. Но если мы знаем, что тип подчиняется обоим законам, мы можем строить определённые предположения о том, как он будет действовать. Если тип подчиняется законам функто- ров, мы знаем, что вызов функции fmap со значением этого типа только применит к нему функцию – ничего более. В результате наш код становится более абстрактным и расширяемым, потому что мы можем использовать законы, чтобы судить о поведении, кото-


 

рым должен обладать любой функтор, а также создавать функции, надёжно работающие с любым функтором.

В следующий раз, когда вы будете делать тип экземпляром клас- са Functor, найдите минутку, чтобы убедиться, что он удовлетворя- ет законам функторов. Вы всегда можете пройти по реализации строка за строкой и посмотреть, выполняются ли законы, либо попробовать найти исключение. Изучив функторы в достаточном количестве, вы станете узнавать общие для них свойства и поведе- ние и интуитивно понимать, следует ли тот или иной тип законам функторов.

 



Поделиться:


Последнее изменение этой страницы: 2017-02-17; просмотров: 158; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.145.183.137 (0.066 с.)