Почему MonadPlus, а не Monad + Monoid?
Я пытаюсь понять мотивацию MonadPlus
. Почему это необходимо, если уже есть классы Monad
и Monoid
?
Конечно, экземпляры Monoid
являются конкретными типами, тогда как экземпляры Monad
требуют одного параметра типа. (См. Monoid vs MonadPlus для получения полезного объяснения.) Но вы не могли бы переписать ограничение типа
(MonadPlus m) => ...
как комбинация Monad
и Monoid
?
(Monad m, Monoid (m a)) => ...
Возьмем, например, функцию guard
из Control.Monad
. Его реализация:
guard :: (MonadPlus m) => Bool -> m ()
guard True = return ()
guard False = mzero
Мне удалось реализовать его, используя только Monad
и Monoid
:
guard' :: (Monad m, Monoid (m ())) => Bool -> m ()
guard' True = return ()
guard' False = mempty
Может кто-то прояснить реальную разницу между MonadPlus
и Monad
+ Monoid
?
Ответы
Ответ 1
Но вы не могли бы переписать ограничение типа
(MonadPlus m) => ...
как комбинация Monad и Monoid?
Нет. В верхнем ответе на вопрос, который вы связываете, уже есть хорошее объяснение законов MonadPlus против Monoid. Но есть различия, даже если мы игнорируем законы типа оформления.
Monoid (m a) => ...
означает, что m a
должен быть моноидом для одного конкретного a
, выбранного вызывающим, но MonadPlus m
означает, что m a
должен быть моноидом для всех a
. Таким образом, MonadPlus a
является более гибким, и эта гибкость полезна в четырех ситуациях:
-
Если мы не хотим сообщить вызывающему, что a
мы намерены использовать.
MonadPlus m => ...
вместо Monoid (m SecretType) => ...
-
Если мы хотим использовать несколько разных a
.
MonadPlus m => ...
вместо (Monoid (m Type1), Monoid (m Type2), ...) => ...
-
Если мы хотим использовать бесконечно много разных a
.
MonadPlus m => ...
вместо невозможного.
-
Если мы не знаем, что нам нужно a
.
MonadPlus m => ...
вместо невозможного.
Ответ 2
Ваш guard'
не соответствует типу Monoid m a
.
Если вы имеете в виду Monoid (m a)
, то вам нужно определить, что mempty
для m ()
. Как только вы это сделали, вы определили MonadPlus
.
Другими словами, MonadPlus
определяет два исключения: mzero
и mplus
, удовлетворяющие двум правилам: mzero
является нейтральным относительно mplus
, а mplus
ассоциативным. Это удовлетворяет определению a Monoid
, так что mzero
есть mempty
и mplus
есть mappend
.
Разница в том, что MonadPlus m
является моноидом m a
для любого a
, но Monoid m
определяет моноид только для m
. Ваш guard'
работает, потому что вам нужно m
быть Monoid
только для ()
. Но MonadPlus
сильнее, он утверждает, что m a
является моноидом для любого a
.
Ответ 3
С в QuantifiedConstraints
расширении языка можно выразить, что Monoid (ma)
экземпляр должен быть равномерным во всех выборах: a
{-# LANGUAGE QuantifiedConstraints #-}
class (Monad m, forall a. Monoid (m a)) => MonadPlus m
mzero :: (MonadPlus m) => m a
mzero = mempty
mplus :: (MonadPlus m) => m a -> m a -> m a
mplus = mappend
В качестве альтернативы, мы можем реализовать "настоящий" класс MonadPlus
для всех таких моноад-монад:
{-# LANGUAGE GeneralizedNewtypeDeriving, DerivingStrategies, QuantifiedConstraints #-}
{-# LANGUAGE UndecidableInstances #-}
import Control.Monad
import Control.Applicative
newtype MonoidMonad m a = MonoidMonad{ runMonoidMonad :: m a }
deriving (Functor, Applicative, Monad)
instance (Applicative m, forall a. Monoid (m a)) => Alternative (MonoidMonad m) where
empty = MonoidMonad mempty
(MonoidMonad x) <|> (MonoidMonad y) = MonoidMonad (x <> y)
instance (Monad m, forall a. Monoid (m a)) => MonadPlus (MonoidMonad m)
Обратите внимание, что в зависимости от вашего выбора m
, это может или не может дать вам MonadPlus
; например, MonoidMonad []
действительно совпадает с []
; но для Maybe
экземпляр Monoid
поднимает некоторую нижележащую полугруппу, искусственно придавая ей элемент идентичности, тогда как экземпляр MonadPlus
является левосторонним выбором; и поэтому мы должны использовать MonoidMonad First
вместо MonoidMonad Maybe
чтобы получить правильный экземпляр.