Подтвердить что ты не робот

Почему Аппликативный должен быть суперкласс Монады?

Дано:

Applicative m, Monad m => mf :: m (a -> b), ma :: m a

он считается законным, что:

mf <*> ma === do { f <- mf; a <- ma; return (f a) }

или более кратко:

(<*>) === ap

Документация для Control.Applicative говорит, что <*> является "последовательным приложением", и это говорит о том, что (<*>) = ap. Это означает, что <*> должен последовательно оценивать эффекты слева направо, для соответствия с >>=... Но это неправильно. МакБрайд и оригинальная статья Патерсона, кажется, подразумевают, что последовательность слева направо произвольна:

Мода IO, и даже любая Монада, могут быть сделаны Аппликативными, взяв pure = return и <*>= ap. В качестве альтернативы мы могли бы использовать вариант ap, который выполняет вычисления в обратном порядке, но мы будем придерживаться порядка слева направо в этой статье.

Таким образом, существуют два законных нетривиальных дифференцирования для <*>, которые следуют из >>= и return, с отличным поведением. И в некоторых случаях ни один из этих двух выводов не является желательным.

Например, закон (<*>) === ap устанавливает Data.Validation для определения двух разных типов данных: Validation и AccValidation. Первый имеет экземпляр Monad, похожий на ExceptT, и последовательный экземпляр Applicative, который имеет ограниченную полезность, поскольку он останавливается после первая ошибка. Последнее, с другой стороны, не определяет экземпляр Monad и поэтому свободно реализует Applicative, который гораздо полезнее накапливает ошибки.

В StackOverflow было несколько обсуждений об этом, но я не думаю, что это действительно дошло до мяса вопроса:

Почему это должен быть закон?

Другие законы для функторов, аппликаций и монад, таких как идентичность, ассоциативность и т.д., выражают некоторые фундаментальные, математические свойства этих структур. Мы можем реализовать различные оптимизации с использованием этих законов и доказать, что они используют наш собственный код. Напротив, мне кажется, что закон (<*>) === ap накладывает произвольное ограничение без соответствующей выгоды.

Для чего это стоит, я предпочел бы отказаться от закона в пользу чего-то вроде этого:

newtype LeftA m a = LeftA (m a)
instance Monad m => Applicative (LeftA m) where
  pure = return
  mf <*> ma = do { f <- mf; a <- ma; return (f a) }

newtype RightA m a = RightA (m a)
instance Monad m => Applicative (RightA m) where
  pure = return
  mf <*> ma = do { a <- ma; f <- mf; return (f a) }

Я думаю, что правильно фиксирует взаимосвязь между ними, без чрезмерного ограничения.

Итак, несколько углов, чтобы подойти к вопросу:

  • Существуют ли другие законы, относящиеся к Monad и Applicative?
  • Есть ли какая-либо неотъемлемая математическая причина для эффектов для последовательности для Applicative так же, как и для Monad?
  • Может ли GHC или какой-либо другой инструмент выполнять преобразования кода, которые предполагают/требуют, чтобы этот закон был истинным?
  • Почему предложение Functor-Applicative-Monad считается такой превосходно хорошей? (Цитаты были бы высоко оценены здесь).

И один бонусный вопрос:

  • Как Alternative и MonadPlus подходят ко всему этому?

Примечание: главное изменение, чтобы прояснить мясо вопроса. Ответ, отправленный @duplode, цитирует более раннюю версию.

4b9b3361

Ответ 1

Хорошо, я не очень доволен ответами, которые были даны до сих пор, но я думаю, что комментарии, прилагаемые к ним, немного более убедительны. Поэтому я опишу здесь:


Я думаю, что существует только один разумный экземпляр Functor, который следует из Applicative:

fmap f fa = pure f <*> fa

Предполагая, что это единственное, имеет смысл, что Functor должен быть суперклассом Applicative, с этим законом. Аналогично, я думаю, что существует только один разумный экземпляр Functor, который следует из Monad:

fmap f fa = fa >>= return . f

Итак, имеет смысл, что Functor должен быть суперклассом Monad. У возражения, которое у меня было (и, по-прежнему, есть), есть два разумных экземпляра Applicative, которые следуют из Monad, а в некоторых конкретных случаях - еще более законные; так зачем задавать один?

pigworker (первый автор статьи оригинал Applicative) пишет:

"Конечно, это не следует. Это выбор".

(на twitter): "do-notation - несправедливое наказание за работу в монаде, мы заслуживаем аппликативной нотации"

duplode аналогичным образом пишет:

"... справедливо сказать, что pure === return и (<*>) === ap не являются законами в сильном смысле, например, как законы монады..."

"В идее LeftA/RightA существуют сопоставимые случаи в других стандартных библиотеках (например, Sum и Product в Data.Monoid). Задача сделать то же самое с Applicative равна что отношение мощности к весу слишком низкое, чтобы оправдать дополнительную точность/гибкость. Новые типы приложений стали бы менее приятными в использовании.

Итак, я рад видеть, что этот выбор определен явно, оправданным простой аргументацией, что он облегчает наиболее распространенные случаи.

Ответ 2

Помимо прочего, вы спрашиваете, почему предложение Functor-Applicative-Monad - это хорошо. Одна из причин заключается в том, что отсутствие единства означает, что существует много дублирования API. Рассмотрим стандартный модуль Control.Monad. Ниже перечислены функции в этом модуле, которые по существу используют ограничение Monad (для MonadPlus):

(>>=) fail (=<<) (>=>) (<=<) join foldM foldM_

Ниже перечислены функции в этом модуле, где ограничение Monad/MonadPlus могло бы, насколько я могу сказать, легко расслабиться до Applicative/Alternative:

(>>) return mzero mplus mapM mapM_ forM forM_ sequence sequence_ forever
msum filterM mapAndUnzipM zipWithM zipWithM_ replicateM replicateM_ guard
when unless liftM liftM2 liftM3 liftM4 liftM5 ap

Многие из последних групп имеют версии Applicative или Alternative либо в Control.Applicative, Data.Foldable, либо Data.Traversable - но почему нужно сначала изучить все это дублирование?

Ответ 3

и в моей собственной (возможно, ошибочной) интуиции, заданной pure f <*> ma <*> mb, не должно быть никакого предопределенного последовательности, поскольку ни одно из значений не зависит друг от друга.

Значения нет, но эффекты действительно. (<*>) :: t (a -> b) -> t a -> t b означает, что вам нужно каким-то образом объединить эффекты аргументов, чтобы получить общие эффекты. Будет ли комбинация коммутативной или нет, зависит от того, как определяется этот экземпляр. Например, экземпляр для Maybe является коммутативным, а экземпляр "cross join" для списков по умолчанию - нет. Поэтому есть случаи, когда вы не можете избежать наложения какого-либо порядка.

Какие законы, если таковые имеются, касаются Монады и Аппликации?

Хотя справедливо сказать, что pure === return и (<*>) === ap (цитирование Control.Applicative) не являются законами сильного что, например, законы монады таковы, что они помогают держать случаи неудивительными. Учитывая, что каждый Monad вызывает экземпляр Applicative (фактически два экземпляра, как вы указываете), естественно, что фактический экземпляр Applicative соответствует тому, что нам дает Monad. Что касается соглашения слева направо, следуя порядку ap и liftM2 (который уже существовал, когда был введен Applicative, и какое зеркало порядок, налагаемый (>>=)), был разумным решением. (Заметим, что если на какой-то момент игнорировать вопрос о том, насколько (>>=) имеет значение, противоположный выбор был бы оправданным, так как это сделало бы (<*>) и (=<<), которые имеют аналогичные типы, эффекты последовательности в том же порядок.)

Выполняет ли GHC или любой другой инструмент преобразования кода, которые предполагают/требуют, чтобы этот закон был истинным?

Это звучит очень маловероятно, учитывая, что Applicative не является даже суперклассом Monad (пока). Тем не менее, эти "законы" позволяют читателям кода делать преобразования, что так же важно.

Примечание. Если вам нужно отменить последовательность действий в экземпляре Applicative, есть Control.Applicative.Backwards, так как Габриэль Гонсалес указал. Кроме того, (<**>) переворачивает аргументы, но все же последовательности эффектов слева направо, поэтому его также можно использовать для изменения последовательности. Аналогично, (<*) не flip (*>), так как оба эффекта последовательности слева направо.

Ответ 4

Только для записи ответ на вопрос в заголовке: рассмотрим

sequenceA :: Applicative f, Traversable t => t (f a) -> f (t a)
join :: Monad m => m (m a) -> m a

Каков тип join . sequenceA?

  • ATP: Monad m, Traversable m => m (m a) -> m a
  • Текущая ситуация: Applicative m, Monad m, Traversable m => m (m a) -> m a

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

  • Наличие двух отдельных ограничений для захвата обеих операций вызывает раздражение.
  • Имена Applicative (IMHO) лучше, чем у традиционных операций монады.
  • Имеет два разных имени, например. ap, >>, << и т.д. раздражает ( "о, вы не можете использовать <*> там, a Monad not a Applicative"; "oh, вы должны использовать <*> там, что Applicative не a Monad" ).
  • В реальных монадах порядок действительно, очень важен, а это означает, что если >> и *> выполняют разные вещи, вы не можете использовать синтаксис Applicative, потому что он сделает что-то, что вы не ожидайте.

Итак, прагматично, имея Applicative для каждого Monad, который с ним совместим (в смысле (<*>) = ap), действительно, действительно хорошая идея.