Извините, я плохо разбираюсь в математике, поэтому мне любопытно, как произносятся функции в классе типов Applicative
Я думаю, что знание вашей математики здесь в значительной степени не имеет значения. Как вы, вероятно, знаете, Haskell заимствует несколько терминов из различных областей абстрактной математики, в первую очередь из теории категорий , откуда мы и получаем функторы и монады. Использование этих терминов в Haskell несколько отличается от формальных математических определений, но обычно они достаточно близки, чтобы в любом случае быть хорошими описательными терминами.
В Applicative
типа находится где-то между Functor
и Monad
, поэтому можно было бы ожидать, что он имеет аналогичную математическую основу. Документация к Control.Applicative
модулю начинается с:
Этот модуль описывает промежуточную структуру между функтором и монадой: он обеспечивает чистые выражения и последовательность, но без привязки. (Технически это сильный нестрогий моноидальный функтор.)
Хм.
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
Monad
Думаю, не так запоминающе .
В основном все это сводится к тому, Applicative
что не соответствует ни одной особенно интересной концепции. математической точки зрения, поэтому нет никаких готовых терминов, которые отражали бы способ ее использования в Haskell. Итак, отложим пока математику.
Если мы хотим знать, как позвонить (<*>)
это может помочь узнать, что это в основном означает.
Так в чем дело Applicative
и почему? же мы называем это что?
На Applicative
практике получается способ поднять произвольные функции в Functor
. Рассмотрим комбинацию Maybe
(возможно, самого простого нетривиального Functor
) и Bool
(аналогично простейшего нетривиального типа данных).
maybeNot :: Maybe Bool -> Maybe Bool
maybeNot = fmap not
Эта функция fmap
позволяет нам перейти not
от работы Bool
к работе Maybe Bool
. Но что, если мы хотим поднять (&&)
?
maybeAnd' :: Maybe Bool -> Maybe (Bool -> Bool)
maybeAnd' = fmap (&&)
Ну, это не то , что мы хотим , чтобы у всех ! Фактически, это бесполезно. Мы можем попытаться быть умным и украдкой другой Bool
в Maybe
через спину ...
maybeAnd'' :: Maybe Bool -> Bool -> Maybe Bool
maybeAnd'' x y = fmap ($ y) (fmap (&&) x)
... но это нехорошо. Во-первых, это неправильно. Во-вторых, это некрасиво . Мы могли бы продолжать попытки, но оказывается, что есть никакого способа поднять функцию с несколькими аргументами для работы с произвольнымFunctor
. Раздражает!
С другой стороны, мы могли бы сделать это легко , если бы мы использовали Maybe
«s Monad
экземпляр:
maybeAnd :: Maybe Bool -> Maybe Bool -> Maybe Bool
maybeAnd x y = do x' <- x
y' <- y
return (x' && y')
Теперь, это много хлопот просто перевести простую функцию - именно поэтому Control.Monad
обеспечивает функцию , чтобы сделать это автоматически, liftM2
. Цифра 2 в названии указывает на то, что он работает с функциями ровно с двумя аргументами; аналогичные функции существуют для функций с 3, 4 и 5 аргументами. Эти функции лучше , но не идеальны, а указывать количество аргументов некрасиво и неуклюже.
Это подводит нас к статье, в которой представлен класс типа Applicative. . В нем авторы делают по существу два наблюдения:
- Преобразование функций с несколькими аргументами в
Functor
- это очень естественная вещь.
- Для этого не требуются все возможности
Monad
Обычное функциональное приложение написано простым сопоставлением терминов, поэтому, чтобы сделать «поднятое приложение» как можно более простым и естественным, в документе представлены инфиксные операторы, заменяющие приложение, перенесенные вFunctor
класс, и класс типа, чтобы предоставить все необходимое для этого. .
Все это подводит нас к следующему пункту: (<*>)
просто представляет собой приложение функции - так зачем произносить его иначе, чем вы произносите пробельный «оператор сопоставления»?
Но если это не очень удовлетворительно, мы можем заметить, что Control.Monad
модуль также предоставляет функцию, которая делает то же самое для монад:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Где ap
, конечно, короткий для «применить». Поскольку любой Monad
может быть Applicative
и ap
нуждается только в подмножестве функций, присутствующих в последнем, мы, возможно, можем сказать, что если бы он (<*>)
не был оператором, его следовало бы вызвать ap
.
Мы также можем подойти к вещам с другой стороны. Операция Functor
подъема вызывается, fmap
потому что это обобщение map
операции над списками. Какая функция в списках могла бы работать (<*>)
? Конечно, есть то, что ap
есть в списках, но само по себе это не особенно полезно.
На самом деле, списки могут интерпретироваться более естественно. Что приходит в голову, когда вы смотрите на следующую подпись типа?
listApply :: [a -> b] -> [a] -> [b]
Есть что-то настолько заманчивое в идее выстраивания списков параллельно, применяя каждую функцию в первом к соответствующему элементу второго. К несчастью для нашего старого друга Monad
, эта простая операция нарушает законы монад, если списки имеют разную длину. Но он приносит штраф Applicative
, и в этом случае он (<*>)
становится способом объединения обобщенной версии zipWith
, так что, возможно, мы можем представить себе, как это называется fzipWith
?
Эта идея застегивания на самом деле завершает круг. Помните ту математику о моноидальных функторах? Как следует из названия, это способ объединения структуры моноидов и функторов, оба из которых являются знакомыми классами типов Haskell:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Как бы они выглядели, если бы вы сложили их в коробку и немного встряхнули? Оттуда Functor
мы сохраним идею структуры, независимой от ее параметра типа , а затем Monoid
сохраним общую форму функций:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ?
mfAppend :: f ? -> f ? -> f ?
Мы не хотим предполагать, что есть способ создать действительно «пустой» Functor
, и мы не можем вызвать значение произвольного типа, поэтому мы исправим тип mfEmpty
as f ()
.
Мы также не хотим, чтобы принудительно mfAppend
требовался параметр согласованного типа, поэтому теперь у нас есть это:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f ?
Для чего нужен тип результата mfAppend
? У нас есть два произвольных типа, о которых мы ничего не знаем, поэтому у нас не так много вариантов. Самое разумное - просто сохранить оба:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f (a, b)
В этот момент mfAppend
теперь ясно обобщенный вариант zip
в списках, и мы можем реконструировать Applicative
легко:
mfPure x = fmap (\() -> x) mfEmpty
mfApply f x = fmap (\(f, x) -> f x) (mfAppend f x)
Это также показывает нам, что pure
связано с элементом идентичности a Monoid
, поэтому другими хорошими именами для него может быть что угодно, предлагающее значение единицы, нулевую операцию или что-то подобное.
Это было долго, так что резюмирую:
(<*>)
- это просто модифицированное приложение-функция, поэтому вы можете прочитать его как «ap» или «применить», или полностью исключить его, как и обычное приложение-функцию.
(<*>)
также примерно обобщает zipWith
списки, поэтому вы можете читать его как «zip-функторы с», аналогично чтениюfmap
«сопоставить функтор с».
Первый ближе к замыслу Applicative
класса типа - как следует из названия - так что я рекомендую.
Фактически, я призываю к либеральному использованию, а не к произношению всех операторов поднятого приложения :
(<$>)
, который переводит функцию с одним аргументом в Functor
(<*>)
, который связывает функцию с несколькими аргументами через Applicative
(=<<)
, который связывает функцию, которая вводит в Monad
существующее вычисление
По сути, все три являются обычным функциональным приложением, немного приправленным.