Выражение (+) <$> [1,2] <*> [4,5,6]
возвращает в результате недетерминированное вычисление x
+
y
, где образец x
принимает каждое значение из [1,2]
, а y
принимает каждое значение из [4,5,6]
. Мы представляем это в виде списка, который содержит все возможные результаты. Аналогичным образом, когда мы выполняем выражение sequenceA [[1,2],[3,4],[5,6]]
, результатом является недетерминированное вычисление [x,y,z]
, где образец x
принимает каждое значение из [1,2]
, а y
– каждое значение из [3,4]
и т. д. Для представления результата этого недетерминированного вычисления мы используем список, где каждый элемент в списке является одним возможным списком. Вот почему результатом является список списков.
При использовании с действиями ввода-вывода функция sequenceA
представляет собой то же самое, что и функция sequence
! Она принимает список действий ввода-вывода и возвращает действие ввода-вывода, которое выполнит каждое из этих действий и в качестве своего результата будет содержать список результатов этих действий ввода-вывода. Так происходит, потому что чтобы превратить значение [IO a]
в значение IO [a]
, чтобы создать действие ввода-вывода, возвращающее список результатов при выполнении, все эти действия ввода-вывода должны быть помещены в последовательность, а затем быть выполненными одно за другим, когда потребуется результат выполнения. Вы не можете получить результат действия ввода-вывода, не выполнив его!
Давайте поместим три действия ввода-вывода getLine
в последовательность:
ghci> sequenceA [getLine, getLine, getLine]
эй
хо
ух
["эй","хо","ух"]
В заключение отмечу, что аппликативные функторы не просто интересны, но и полезны. Они позволяют нам объединять разные вычисления – как, например, вычисления с использованием ввода-вывода, недетерминированные вычисления, вычисления, которые могли окончиться неуспешно, и т. д., – используя аппликативный стиль. Просто с помощью операторов <$>
и <*>
мы можем применять обычные функции, чтобы единообразно работать с любым количеством аппликативных функторов и использовать преимущества семантики каждого из них.
12
Моноиды
В этой главе представлен ещё один полезный и интересный класс типов Monoid
. Он существует для типов, значения которых могут быть объединены при помощи бинарной операции. Мы рассмотрим, что именно представляют собой моноиды и что утверждают их законы. Затем рассмотрим некоторые моноиды в языке Haskell и обсудим, как они могут нам пригодиться.
И прежде всего давайте взглянем на ключевое слово newtype
: мы будем часто его использовать, когда углубимся в удивительный мир моноидов.
Оборачивание существующего типа в новый тип
Пока что вы научились создавать свои алгебраические типы данных, используя ключевое слово data
. Вы также увидели, как можно давать синонимы имеющимся типам с применением ключевого слова type
. В этом разделе мы рассмотрим, как создаются новые типы на основе имеющихся типов данных с использованием ключевого слова newtype
. И в первую очередь, конечно, поговорим о том, чем всё это может быть нам полезно.
В главе 11 мы обсудили пару способов, при помощи которых списковый тип может быть аппликативным функтором. Один из этих способов состоит в том, чтобы заставить оператор <*>
брать каждую функцию из списка, являющегося его левым параметром, и применять её к каждому значению в списке, который находится справа, что в результате возвращает все возможные комбинации применения функции из левого списка к значению в правом:
ghci> [(+1),(*100),(*5)] <*> [1,2,3]
[2,3,4,100,200,300,5,10,15]
Второй способ заключается в том, чтобы взять первую функцию из списка слева от оператора <*>
и применить её к первому значению справа, затем взять вторую функцию из списка слева и применить её ко второму значению справа, и т. д. В конечном счёте получается нечто вроде застёгивания двух списков.
Но списки уже имеют экземпляр класса Applicative
, поэтому как нам определить для списков второй экземпляр класса Applicative
? Как вы узнали, для этой цели был введён тип ZipList a
. Он имеет один конструктор данных ZipList
, у которого только одно поле. Мы помещаем оборачиваемый нами список в это поле. Далее для типа ZipList
определяется экземпляр класса Applicative
, чтобы, когда нам понадобится использовать списки в качестве аппликативных функторов для застёгивания, мы могли просто обернуть их с по мощью конструктора ZipList
. Как только мы закончили, разворачиваем их с помощью getZipList
:
ghci> getZipList $ ZipList [(+1),(*100),(*5)] <*> ZipList [1,2,3]
[2,200,15]