Поприветствуйте аппликативные функторы




ЗНАЕТЕ ЛИ ВЫ?

Поприветствуйте аппликативные функторы



 
 

Итак, встречайте класс типов Applicative, находящийся в модуле Control.Applicative!.. Он определяет две функции: pure и <*>. Он не предоставляет реализации по умолчанию для какой-либо из этих функций, поэтому нам придётся определить их обе, если мы хотим, чтобы что-либо стало аппликативным функтором. Этот класс опре- делён вот так:

class (Functor f) => Applicative f where pure :: a –> f a

 
 

(<*>) :: f (a –> b) –> f a –> f b

Простое определение класса из трёх строк говорит нам о мно- гом!.. Первая строка начинается с определения класса Applicative; также она вводит ограничение класса. Ограничение говорит, что если мы хотим определить для типа экземпляр класса Applicative, он, прежде всего, уже должен иметь экземпляр класса Functor. Вот почему, когда нам известно, что конструктор типа принадлежит классу Applicative, можно смело утверждать, что он также прина- длежит классу Functor, так что мы можем применять к нему функ- цию fmap.

Первый метод, который он определяет, называется pure. Его сигнатура выглядит так: pure :: a –> f a. Идентификатор f играет здесь роль нашего экземпляра аппликативного функтора. Посколь- ку язык Haskell обладает очень хорошей системой типов и притом всё, что может делать функция, – это получать некоторые парамет- ры и возвращать некоторое значение, мы можем многое сказать по объявлению типа, и данный тип – не исключение.

Функция pure должна принимать значение любого типа и воз- вращать аппликативное значение с этим значением внутри него. Словосочетание «внутри него» опять вызывает в памяти нашу ана- логию с коробкой, хотя мы и видели, что она не всегда выдержива- ет проверку. Но тип a –> f a всё равно довольно нагляден. Мы берём значение и оборачиваем его в аппликативное значение, которое содержит в себе это значение в качестве результата. Лучший спо- соб представить себе функцию pure – это сказать, что она берёт


 

значение и помещает его в некий контекст по умолчанию (или чис- тый контекст) – минимальный контекст, который по-прежнему воз- вращает это значение.

 
 

Оператор <*> действительно интересен. У него вот такое опре- деление типа:

f (a –> b) –> f a –> f b

Напоминает ли оно вам что-нибудь? Оно похоже на сигнатуру fmap :: (a –> b) –> f a –> f b. Вы можете воспринимать оператор <*> как разновидность расширенной функции fmap. Тогда как функция fmap принимает функцию и значение функтора и применяет функцию внутри значения функтора, оператор <*> принимает значение фун- ктора, который содержит в себе функцию, и другой функтор – и из- влекает эту функцию из первого функтора, затем отображая с её помощью второй.

 

Аппликативный функтор Maybe

 
 

Давайте взглянем на реализацию экземпляра класса Applicative для типа Maybe:

instance Applicative Maybe where pure = Just

Nothing <*> _ = Nothing

 
 

(Just f) <*> something = fmap f something

Опять же из определения класса мы видим, что идентифика- тор f, который играет роль аппликативного функтора, должен при- нимать один конкретный тип в качестве параметра. Поэтому мы пишем instance Applicative Maybe where вместо instance Applicative (Maybe a) where.

Далее, у нас есть функция pure. Вспомните, что функция долж- на что-то принять и обернуть в аппликативное значение. Мы на- писали pure = Just, потому что конструкторы данных вроде Just являются обычными функциями. Также можно было бы написать pure x = Just x.

Наконец, у нас есть определение оператора <*>. Извлечь функ- цию из значения Nothing нельзя, поскольку внутри него нет функции. Поэтому мы говорим, что если мы пробуем извлечь функцию из значения Nothing, результатом будет то же самое значение Nothing.


 

В определении класса Applicative есть ограничение класса Functor – значит, мы можем считать, что оба параметра операто- ра <*> являются значениями функтора. Если первым аргументом выступает не значение Nothing, а Just с некоторой функцией внутри, то мы говорим, что с помощью данной функции хотим отобразить второй параметр. Этот код также заботится о случае, когда вторым аргументом является значение Nothing, потому что его отображе- ние с помощью любой функции при использовании метода fmap вернёт всё то же Nothing. Итак, в случае с типом Maybe оператор <*> извлекает функцию из значения слева, если это Just, и отображает с её помощью значение справа. Если какой-либо из параметров яв- ляется значением Nothing, то и результатом будет Nothing.

 
 

Теперь давайте это опробуем:

ghci> Just (+3) <*> Just 9

Just 12

ghci> pure (+3) <*> Just 10

Just 13

ghci> pure (+3) <*> Just 9

Just 12

ghci> Just (++"ха-ха") <*> Nothing Nothing

 
 

ghci> Nothing <*> Just "во-от" Nothing

Вы видите, что выполнение выражений pure (+3) и Just (+3) в данном случае – одно и то же. Используйте функцию pure, если имеете дело со значениями типа Maybe в аппликативном контекс- те (если вы используете их с оператором <*>); в противном случае предпочитайте конструктор Just.

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

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


 

стороны, позволяют вам работать с несколькими функторами, ис- пользуя одну функцию.

 

Аппликативный стиль

 
 

При использовании класса типов Applicative мы можем последо- вательно задействовать несколько операторов <*> в виде цепочки вызовов, что позволяет легко работать сразу с несколькими аппли- кативными значениями, а не только с одним. Взгляните, например, на это:

ghci> pure (+) <*> Just 3 <*> Just 5 Just 8

ghci> pure (+) <*> Just 3 <*> Nothing Nothing

 
 

ghci> pure (+) <*> Nothing <*> Just 5 Nothing


 

 

pure (+) <*> Just 3 <*> Just 5 то же самое, что и вот это: (pure (+) <*> Just 3) <*> Just 5


Мы обернули оператор + в ап- пликативное значение, а затем ис- пользовали оператор <*>, чтобы вызвать его с двумя параметрами, оба из которых являются апплика- тивными значениями.

Давайте посмотрим, как это происходит, шаг за шагом. Опера- тор <*> левоассоциативен; это зна- чит, что


Сначала оператор + помещается в аппликативное значение – в данном случае значение типа Maybe, которое содержит функцию. Итак, у нас есть pure (+), что, по сути, равно Just (+). Далее проис- ходит вызов Just (+) <*> Just 3. Его результатом является Just (3+). Это из-за частичного применения. Применение только значения 3 к оператору + возвращает в результате функцию, которая прини- мает один параметр и добавляет к нему 3. Наконец, выполняется Just (3+) <*> Just 5, что в результате возвращает Just 8.


 

Ну разве не здорово?! Аппликативные функторы и апплика- тивный стиль вычисления pure f <*> x <*> y <*> ... позволяют взять функцию, которая ожидает параметры, не являющиеся апплика- тивными значениями, и использовать эту функцию для работы с несколькими аппликативными значениями. Функция может при- нимать столько параметров, сколько мы захотим, потому что она всегда частично применяется шаг за шагом между вхождениями оператора <*>.

 
 

Это становится ещё более удобным и очевидным, если мы примем во внимание тот факт, что выражение pure f <*> x равно fmap f x. Это один из законов аппликативных функторов, которые мы более подробно рассмотрим чуть позже; но давайте подумаем, как он применяется здесь. Функция pure помещает значение в кон- текст по умолчанию. Если мы просто поместим функцию в контекст по умолчанию, а затем извлечем её и применим к значению внут- ри другого аппликативного функтора, это будет то же самое, что просто отобразить этот аппликативный функтор с помощью дан- ной функции. Вместо записи pure f <*> x <*> y <*> ..., мы можем написать fmap f x <*> y <*> ... Вот почему модуль Control.Applicative экспортирует оператор, названный <$>, который является просто синонимом функции fmap в виде инфиксного оператора. Вот как он определён:

(<$>) :: (Functor f) => (a –> b) –> f a –> f b f <$> x = fmap f x

ПРИМЕЧАНИЕ.Вспомните, что переменные типов не зависят от имён параметров или имён других значений. Здесь идентифи- катор f в сигнатуре функции является переменной типа с огра- ничением класса, которое говорит, что любой конструктор типа, который заменяет f, должен иметь экземпляр класса Functor. Идентификатор f в теле функции обозначает функцию, с помо- щью которой мы отображаем значение x. Тот факт, что мы ис- пользовали f для представления обеих вещей, не означает, что они представляют одну и ту же вещь.

При использовании оператора <$> аппликативный стиль про- являет себя во всей красе, потому что теперь, если мы хотим приме- нить функцию f к трем аппликативным значениям, можно просто написать f <$> x <*> y <*> z. Если бы параметры были обычными значениями, мы бы написали f x y z.


 

 
 

Давайте подробнее рассмотрим, как это работает. Предполо- жим, что мы хотим соединить значения Just "johntra" и Just "volta" в одну строку, находящуюся внутри функтора Maybe. Сделать это вполне в наших силах!

ghci> (++) <$> Just "johntra" <*> Just "volta" Just "johntravolta"

 
 

Прежде чем мы увидим, что происходит, сравните предыдущую строку со следующей:

ghci> (++) "johntra" "volta" "johntravolta"

Чтобы использовать обычную функцию с аппликативным функ- тором, просто разбросайте вокруг несколько <$> и <*>, и функция будет работать с аппликативными значениями и возвращать аппли- кативное значение. Ну не здорово ли?

Возвратимся к нашему выражению (++) <$> Just "джонтра" <*> Just "волта": cначала оператор (++), который имеет тип (++) :: [a] –

> [a] –> [a], отображает значение Just "джонтра". Это даёт в резуль- тате такое же значение, как Just ("джонтра"++), имеющее тип Maybe ([Char] –> [Char]). Заметьте, как первый параметр оператора (++) был «съеден» и идентификатор a превратился в тип [Char]! А те- перь выполняется выражение Just ("джонтра"++) <*> Just "волта", которое извлекает функцию из Just и отображает с её помощью значение Just "волта", что в результате даёт новое значение – Just "джонтраволта". Если бы одним из двух значений было значение Nothing, результатом также было бы Nothing.

 

Списки

 
 

Списки (на самом деле конструктор типа списка, []) являются ап- пликативными функторами. Вот так сюрприз! Вот как [] является экземпляром класса Applicative:

instance Applicative [] where pure x = [x]

 
 

fs <*> xs = [f x | f <– fs, x <– xs]

Вспомните, что функция pure принимает значение и помещает его в контекст по умолчанию. Другими словами, она помещает его


 

в минимальный контекст, который всё ещё возвращает это значе- ние. Минимальным контекстом для списков был бы пустой список, но пустой список означает отсутствие значения, поэтому он не мо- жет содержать в себе значение, к которому мы применили функ- цию pure. Вот почему эта функция принимает значение и помещает его в одноэлементный список. Подобным образом минимальным контекстом для аппликативного функтора Maybe было бы значение Nothing – но оно означает отсутствие значения вместо самого значе- ния, поэтому функция pure в реализации экземпляра для типа Maybe реализована как вызов конструктора данных Just.

 
 

Вот функция pure в действии:

ghci> pure "Эй" :: [String] ["Эй"]

 
 

ghci> pure "Эй" :: Maybe String Just "Эй"

Что насчёт оператора <*>? Если бы тип оператора <*> огра- ничивался только списками, мы получили бы (<*>) :: [a –> b] –> [a] –> [b]. Этот оператор реализован через генератор списков. Он должен каким-то образом извлечь функцию из своего левого пара- метра, а затем с её помощью отобразить правый. Но левый спи- сок может не содержать в себе функций или содержать одну либо несколько функций, а правый список также может содержать не- сколько значений. Вот почему мы используем генератор списков для извлечения из обоих списков. Мы применяем каждую возмож- ную функцию из левого списка к каждому возможному значению из правого. Результирующий список содержит все возможные комбинации применения функции из левого списка к значению из правого.

 
 

Мы можем использовать оператор <*> со списками вот так:

ghci> [(*0),(+100),( 2)] <*> [1,2,3]

 
 

[0,0,0,101,102,103,1,4,9]

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


 

 
 

В следующем примере применяются две функции между двумя списками:

ghci> [(+),(*)] <*> [1,2] <*> [3,4]

 
 

[4,5,5,6,3,4,6,8]

Оператор <*> левоассоциативен, поэтому сначала выполняется [(+),(*)] <*> [1,2], результатом чего является такой же список, как [(1+),(2+),(1*),(2*)], потому что каждая функция слева применя- ется к каждому значению справа. Затем выполняется [(1+),(2+), (1*),(2*)] <*> [3,4], что возвращает окончательный результат.

 
 

Как здорово использовать аппликативный стиль со списками!

ghci> (++) <$> ["хa","хeх","хм"] <*> ["?","!","."]

 
 

["хa?","хa!","хa.","хeх?","хeх!","хeх.","хм?","хм!","хм."]

Ещё раз: мы использовали обычную функцию, принимающую две строки, между двумя списками строк, просто вставляя соответс- твующие аппликативные операторы.

Вы можете воспринимать списки как недетерминированные вычисления. Значение вроде 100 или "что" можно рассматривать как детерминированное вычисление, которое имеет только один результат. В то же время список вроде [1,2,3] можно рассматри- вать как вычисление, которое не в состоянии определиться, ка- кой результат оно желает иметь, поэтому возвращает нам все воз- можные результаты. Поэтому когда вы пишете что-то наподобие (+) <$> [1,2,3] <*> [4,5,6], то можете рассматривать это как объеди- нение двух недетерминированных вычислений с помощью опера- тора + только для того, чтобы создать ещё одно недетерминирован- ное вычисление, которое ещё меньше уверено в своём результате. Использование аппликативного стиля со списками часто яв- ляется хорошей заменой генераторам списков. В главе 1 мы хо- тели вывести все возможные комбинации произведений [2,5,10]

 
 

и [8,10,11] и с этой целью предприняли следующее:

ghci> [x*y | x <– [2,5,10], y <– [8,10,11]]

 
 

[16,20,22,40,50,55,80,100,110]

Мы просто извлекаем значения из обоих списков и применяем функцию между каждой комбинацией элементов. То же самое мож- но сделать и в аппликативном стиле:


 

ghci> (*) <$> [2,5,10] <*> [8,10,11]

 
 

[16,20,22,40,50,55,80,100,110]

Для меня такой подход более понятен, поскольку проще по- нять, что мы просто вызываем оператор * между двумя недетерми- нированными вычислениями. Если бы мы захотели получить все возможные произведения элементов, больших 50, мы бы исполь- зовали следующее:

ghci> filter (>50) $ (*) <$> [2,5,10] <*> [8,10,11]

 
 

[55,80,100,110]

Легко увидеть, что вызов выражения pure f <*> xs при исполь- зовании списков эквивалентен выражению fmap f xs. Результат вы- числения pure f – это просто [f], а выражение [f] <*> xs применит каждую функцию в левом списке к каждому значению в правом; но в левом списке только одна функция, и, следовательно, это похоже на отображение.

 

Тип IO – тоже аппликативный функтор

 
 

Другой экземпляр класса Applicative, с ко- торым мы уже встречались, – экземпляр для типа IO. Вот как он реализован:

instance Applicative IO where pure = return

a <*> b = do f <– a

 
 

x <– b return (f x)

Поскольку суть функции pure состоит в помещении значения в минимальный контекст, который всё ещё содержит зна- чение как результат, логично, что в случае с типом IO функция pure – это просто вызов return. Функция return создаёт действие ввода-вывода, которое ничего не делает. Оно просто возвращает некое значение в качестве своего результата, не производя


 

никаких операций ввода-вывода вроде печати на терминал или чте- ния из файла.

Если бы оператор <*> ограничивался работой с типом IO, он бы имел тип (<*>) :: IO (a –> b) –> IO a –> IO b. В случае с типом IO он принимает действие ввода-вывода a, которое возвращает функ- цию, выполняет действие ввода-вывода и связывает эту функцию с идентификатором f. Затем он выполняет действие ввода-выво- да b и связывает его результат с идентификатором x. Наконец, он применяет функцию f к значению x и возвращает результат этого применения в качестве результата. Чтобы это реализовать, мы ис- пользовали здесь синтаксис do. (Вспомните, что суть синтаксиса do заключается в том, чтобы взять несколько действий ввода-вывода и «склеить» их в одно.)

 
 

При использовании типов Maybe и [] мы могли бы восприни- мать применение функции <*> просто как извлечение функции из её левого параметра, а затем применение её к правому параметру. В отношении типа IO извлечение остаётся в силе, но теперь у нас появляется понятие помещения в последовательность, поскольку мы берём два действия ввода-вывода и «склеиваем» их в одно. Мы должны извлечь функцию из первого действия ввода-вывода, но для того, чтобы можно было извлечь результат из действия ввода- вывода, последнее должно быть выполнено. Рассмотрите вот это:

myAction :: IO String myAction = do

a <– getLine b <– getLine

 
 

return $ a ++ b

Это действие ввода-вывода, которое запросит у пользователя две строки и вернёт в качестве своего результата их конкатенацию. Мы достигли этого благодаря «склеиванию» двух действий ввода- вывода getLine и return, поскольку мы хотели, чтобы наше новое

 
 

«склеенное» действие ввода-вывода содержало результат выполне- ния a ++ b. Ещё один способ записать это состоит в использовании аппликативного стиля:

myAction :: IO String

 
 

myAction = (++) <$> getLine <*> getLine

Это то же, что мы делали ранее, когда создавали действие вво- да-вывода, которое применяло функцию между результатами двух


 

других действий ввода-вывода. Вспомните, что функция getLine – это действие ввода-вывода, которое имеет тип getLine :: IO String. Когда мы применяем оператор <*> между двумя аппликативными значениями, результатом является аппликативное значение, так что всё это имеет смысл.

Если мы вернёмся к аналогии с коробками, то можем предста- вить себе функцию getLine как коробку, которая выйдет в реаль- ный мир и принесёт нам строку. Выполнение выражения (++) <$> getLine <*> getLine создаёт другую, бо´льшую коробку, которая по- сылает эти две коробки наружу для получения строк с терминала, а потом возвращает конкатенацию этих двух строк в качестве свое- го результата.

 
 

Выражение (++) <$> getLine <*> getLine имеет тип IO String. Это означает, что данное выражение является совершенно обычным действием ввода-вывода, как и любое другое, тоже возвращая ре- зультирующее значение, подобно другим действиям ввода-вывода. Вот почему мы можем выполнять следующие вещи:

main = do

a <– (++) <$> getLine <*> getLine

 
 

putStrLn $ "Две строки, соединённые вместе: " ++ a

 





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

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