Ответ 1
Извините, я действительно не знаю свою математику, поэтому мне любопытно, как произносить функции в Applicative typeclass
Зная вашу математику или нет, я думаю, в значительной степени это не имеет значения. Как вам известно, Haskell заимствует несколько терминов из различных областей абстрактной математики, в первую очередь Теория категорий, откуда мы получаем функторы и монады. Использование этих терминов в Haskell несколько отличается от формальных математических определений, но они обычно достаточно близки, чтобы в любом случае быть хорошими описательными терминами.
Класс типа Applicative
находится где-то между Functor
и Monad
, поэтому можно ожидать, что он будет иметь аналогичную математическую основу. Документация для модуля Control.Applicative
начинается с:
Этот модуль описывает структуру, промежуточную между функтором и монадой: он обеспечивает чистые выражения и последовательность, но не привязывает. (Технически, сильный слабый моноидальный функтор.)
Хм.
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
Не так привлекательно, как Monad
, я думаю.
Все это в основном сводится к тому, что Applicative
не соответствует какой-либо концепции, которая особенно интересна математически, поэтому нет готовых терминов, лежащих вокруг этого, захватив способ, которым он пользовался в Haskell. Итак, отложите математику на время.
Если мы хотим знать, что называть (<*>)
, это может помочь узнать, что это означает в основном.
Так что же с Applicative
, во всяком случае, и почему мы это называем?
То, что Applicative
на практике, является способом поднять произвольные функции на a 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
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 type. В ней авторы делают по существу два наблюдения:
- Подъемные функции с несколькими аргументами в
Functor
- это очень естественная вещь. - Для этого не требуются полные возможности
Monad
Приложение с нормальной функцией написано простым сопоставлением терминов, поэтому, чтобы сделать "отмененное приложение" максимально простым и естественным, в документе представлены операторы infix, которые могут стоять в приложении, подняты в Functor
и тип класса чтобы обеспечить то, что необходимо для этого.
Все это приводит нас к следующему пункту: (<*>)
просто представляет собой приложение-приложение - так зачем произносить его иначе, чем вы выполняете "оператор сопоставления"?
Но если это не очень удовлетворительно, мы можем заметить, что модуль Control.Monad
также предоставляет функцию, которая делает то же самое для монад:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Где ap
, конечно, не подходит для "apply". Поскольку любой 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
как 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" или "apply", или полностью исключить его, как обычное функциональное приложение. -
(<*>)
также грубо обобщаетzipWith
в списках, поэтому вы можете прочитать его как "zip-функторы с", аналогично чтениюfmap
как "сопоставить функтор с".
Первое ближе к намерению класса типа Applicative
- как следует из названия - так, что я рекомендую.
На самом деле, я рекомендую либеральное использование и не-произношение всех поднятых операторов приложений:
-
(<$>)
, который поднимает однопараметрическую функцию вFunctor
-
(<*>)
, который объединяет функцию с несколькими аргументами черезApplicative
-
(=<<)
, который связывает функцию, которая вводитMonad
в существующее вычисление
Все три, в основе, просто регулярное приложение функций, немного приправлено.