Haskell: Как произносится <*>?

Как вы произносите эти функции в Applicative typeclass:

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

(То есть, если они не являются операторами, что они могут быть вызваны?)

В качестве побочного примечания, если бы вы могли переименовать pure в нечто более дружелюбное к не математикам, что бы вы назвали его?

Ответ 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 в существующее вычисление

Все три, в основе, просто регулярное приложение функций, немного приправлено.

Ответ 2

Так как у меня нет амбиций улучшения на C. Технический ответ A. McCann, я займусь более пушистым:

Если вы могли бы переименовать pure в нечто более дружелюбное к подобным мне, как я, что бы вы назвали?

В качестве альтернативы, тем более, что нет конца постоянному заполненному и преданным изменением в ответ на версию Monad, называемую "return", я предлагаю другое имя, которое предлагает его функцию таким образом которые могут удовлетворить самые настоятельные императивные программисты и наиболее функциональные из... ну, надеюсь, каждый может жаловаться так же: inject.

Возьмите значение. "Ввести" его в Functor, Applicative, Monad или что-то-вы. Я проголосовал за "inject", и я одобрил это сообщение.

Ответ 3

Мне всегда нравилось wrap. Возьмите значение и оберните его в Functor, Applicative, Monad. Он также хорошо работает при использовании в предложении с конкретными экземплярами: [], Maybe и т.д. "Он принимает значение и обертывает его в X".

Ответ 4

(<*>) -- Tie Fighter
(*>)  -- Right Tie
(<*)  -- Left Tie
pure  -- also called "return"

Источник: Haskell Programming from First Principles, Крис Аллен и Джули Моронуки

Ответ 5

Вкратце:

  • <*> вы можете назвать его применимым. Таким образом, Maybe f <*> Maybe a может быть объявлено как применимое Maybe f над Maybe a.

  • Вы можете переименовать pure в of, как это делают многие библиотеки JavaScript. В JS вы можете создать Maybe с помощью Maybe.of(a).

Кроме того, у Haskell wiki есть страница о произношении языковых операторов здесь