为什么使用MonadPlus而不是Monad + Monoid?

42

我试图理解MonadPlus背后的动机。如果已经有了类型类MonadMonoid,为什么还需要它呢?

可以承认,Monoid的实例是具体类型,而Monad的实例需要单个类型参数。(请参见Monoid vs MonadPlus以获得有用的解释。) 但是,你不能将任何类型约束重写为

(MonadPlus m) => ...
作为Monad和Monoid的组合?
(Monad m, Monoid (m a)) => ...

Control.Monad中的guard函数为例。它的实现如下:

guard :: (MonadPlus m) => Bool -> m ()
guard True = return ()
guard False = mzero

我只使用MonadMonoid就实现了它:

guard' :: (Monad m, Monoid (m ())) => Bool -> m ()
guard' True = return ()
guard' False = mempty

请问有没有人能够澄清MonadPlusMonad+Monoid之间的真正区别?

4个回答

38

但是你不能重写任何类型约束

(MonadPlus m) => ...

不是的。在你提供的问题的最佳答案中,已经有一个很好的解释关于MonadPlus和Monoid的法则。但即使我们忽略类型类法则,它们之间仍然存在差异。

Monoid (m a) => ...意味着m a必须是由调用者选择的特定a的幺半群,但MonadPlus m意味着m a必须对于所有a都是一个幺半群。因此,MonadPlus a更灵活,在以下四种情况下很有帮助:

  1. 如果我们不想告诉调用者我们打算使用哪个a
    MonadPlus m => ...而不是Monoid (m SecretType) => ...

  2. 如果我们想使用多个不同的a
    MonadPlus m => ...而不是(Monoid (m Type1), Monoid (m Type2), ...) => ...

  3. 如果我们想要使用无限多的不同的a
    MonadPlus m => ...而不是不可能。

  4. 如果我们不知道我们需要哪个a
    MonadPlus m => ...而不是不可能。


1
如果您不介意的话,我真的很想在这里看到一个具体的例子。您能否提供一些使用特定Monad的实例,其中MonadPlus比Monoid更有用或更清晰? - Fresheyeball
1
@Fresheyeball:不好意思,我不能提供“使用特定Monad的一些实例”,如果已知monad,则无需在m上进行抽象,并且无需使用任何类型类来指定m的预期接口,因此如果您的代码仅适用于一个特定的monad,则MonadPlusMonoid之间的区别并不重要。 - Toxaris
1
我理解这一点。但是如果没有看到特定的单子如何与通用代码配合工作,很难理解实践过程。 - Fresheyeball

6

你的guardMonoid m a类型不匹配。

如果你的意思是Monoid (m a),那么你需要定义memptym ()中的值。一旦你完成这个定义,你就定义了一个MonadPlus

换句话说,MonadPlus定义了两个操作:mzeromplus,满足两个规则:mzero对于mplus是中性的,而mplus是结合的。这满足了Monoid的定义,所以mzeromemptymplusmappend

区别在于,MonadPlus m是对于任何am a都是一个monoid,但Monoid m只为m定义了一个monoid。你的guard'有效是因为你只需要m对于()是一个Monoid。但是MonadPlus更强大,它声称对于任何am a都是一个monoid。


5
使用QuantifiedConstraints语言扩展,可以表达对于所有a的选择,Monoid (m 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 []实际上与[]相同; 但是对于MaybeMonoid实例通过人为地赋予其标识元素来提升某些基础半群,而MonadPlus实例是左偏选择; 因此,我们必须使用MonoidMonad First而不是MonoidMonad Maybe来获得正确的实例。

0

编辑: 灯泡亮了,一切都恍然大悟。我完全误解了Toxaris的答案。现在我理解了,除了一些支持性的例子和一个观察结果外,我没有什么可补充的了,AlternativeMonad约束似乎在MonadPlus类型类定义中是不必要的。

monadPlusExample :: (MonadPlus m) => m Int
monadPlusExample = do
  x <- idM 7
  f <- idM (* 6)
  return $ f x
  where
    idM a = return a `mplus` mzero

-- a Monoid constraint for each lifted type
monoidExample :: (Monad m, Monoid (m Int), Monoid (m (Int -> Int))) => m Int
monoidExample = do
  x <- idM 7
  f <- idM (* 6)
  return $ f x
  where
    idM a = return a <> mempty

-- yes, QualifiedConstraints unifies the Monoid constraint quite nicely
monoidExample2 :: (Monad m, forall a. Monoid (m a)) => m Int
monoidExample2 = do
  x <- idM 7
  f <- idM (* 6)
  return $ f x
  where
    idM a = return a <> mempty

为了被调用,它们必须使用具体类型进行定义(例如,monoidExample2 :: [Int])。

有了相对较新的“QuantifiedConstraints”扩展,你可以编写像(Monad m,forall a。Monoid(m a))这样的约束条件。如果没有该扩展,仅使用Monoid(m a)可能会遇到多态递归问题。当需要多态递归时,许多事情都变得重要! - dfeuer
需要扩展来正确地使用数字类型类是另一个完全不同的话题... - Sledge
1
大多数这些扩展都非常好。QuantifiedConstraints 还有一点粗糙。但我真的认为你在这里的回答忽略了我的观点。假设你有像 data Foo m a = Stop [m a] | Go (Foo m (a, a)) 这样的东西。现在想象一下,出于某种原因,您需要能够组合 Stop 构造中列表的元素。除非使用 QuantifiedConstraints,否则没有办法给出一个 Monoid 约束,让您这样做 - 您将需要类似于 MonadPlus 的东西。QuantifiedConstraints 是一个改变游戏规则的东西吗?嗯,我们会看到的;它还太新了。 - dfeuer
我的问题不在于扩展本身,而是在于类型类需要它们。 - Sledge
2
@Sledge,我不明白你对需要扩展的类型类的问题在这里为什么有关联。(Monad m, Monoid (m a))MonadPlus m并不完全等价,因为前者可以通过调用者以依赖于特定a的方式满足,而后者则不能(这两种情况都不比另一种更好,它们只是不同)。使用QuantifiedConstraints,我们现在可以编写一个更接近MonadPlus等效性的不同约束,但是我们所讨论的任何类都不需要任何扩展即可正常工作。 - Ben
@Ben 纯属离题。当提到类型类和扩展时,我想起了像 MonadState 和 Parsec 的 Stream 这样的类型类,它们需要多个扩展来使它们工作。然而,正如你所暗示的那样,在这里并非如此,因此我评论说“完全是另一个话题”。 - Sledge

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接