Monad `fail` 和 MonadPlus `mzero` 的适当使用方式

22
这是一个在设计代码,尤其是库中多次出现的问题。似乎有一些兴趣,所以我想这可能会成为一个很好的社区wiki。
Monad中的fail方法被一些人认为是一个瑕疵;它是类别理论中原始概念之外的某种任意添加。但当然,在当前状态下,许多Monad类型都具有逻辑和有用的fail实例。
MonadPlus类是Monad的子类,提供了一个mzero方法,它在逻辑上封装了单子中失败的想法。
因此,一个想要编写一些单子代码来处理某种失败处理的库设计者可以选择使他的代码使用Monad中的fail方法或将他的代码限制在MonadPlus类中,只是为了能够使用mzero而感到良好,即使他根本不关心单子结合mplus操作。
关于改革MonadPlus类的提案的一些讨论在这个维基页面中。

所以我想我有一个具体的问题:

如果有的话,哪些单子实例具有自然的fail方法,但不能成为MonadPlus的实例,因为它们没有逻辑实现mplus

但我主要对这个主题进行讨论。谢谢!


编辑:我有一个最后的想法。最近我学到了(尽管在fail的文档中已经有了)单子“do”符号被解析成这样一种方式,即模式匹配失败,例如(x:xs) <- return []调用单子的fail

看起来语言设计师一定受到了某些自动故障处理内置到haskell语法中的前景的强烈影响,因此将fail包含在Monad中。


2
这是一个不太明确的问题,但回答它可能会有助于澄清事情。最重要的步骤是决定MonadPlus应该遵循哪些代数法则,但也值得考虑程序员可能期望fail和mzero做什么。 - Edward Z. Yang
我认为我只是在邀请关于在Monad中包含fail的讨论,或者在MonadPlus维基讨论页面上扩展这种讨论的脉络。也许我的回答可以被编辑以更好地达到这个目标。 - jberryman
1
哦,大多数人不喜欢失败,并认为它不应该存在于Monad中 :-) - Edward Z. Yang
将标签[class]更改为[typeclass],因为class几乎仅用于面向对象编程。Haskeller通常使用[typeclass]标签。 - fuz
2个回答

11

想一想 Either。它的单子实例看起来像这样:

{-# LANGUAGE FlexibleInstances #-}
instance Monad (Either String) where
  (Left x) >>= _   = Left x
  (Right a)  >>= f = f a
  return           = Right
  fail             = Left

我们需要 FlexibleInstances 来允许像 Either String 这样的实例。
因此,它基本上就像 Maybe ,如果发生了什么,它会有一个可选的错误消息。您无法使用mzero重新创建此内容,因为您无法向失败添加错误消息。 它与fail略有不同。

每个mplus实例都应满足以下两个规则:

mzero `mplus` a -> a
a `mplus` mzero -> a

很简单,不是吗?但这些规则让 mplus 很特别。有了它们,就可以为其编写一个合理的MonadPlus 实例:

instance MonadPlus (Either a) where
  mzero = Left undefined
  mplus (Left _) b = b
  mplus a _        = a

这是什么?它表示一种选择。如果第一次计算成功,它将被返回。否则,mplus 将返回第二个计算结果。请注意与 (>>) 的不同之处,后者不满足法律规定:

Left a   >>    Right b -> Left a
Left a `mplus` Right b -> Right b

(>>)在第一次计算时停止,而mplus则尝试进行第二次计算。[]的行为也是如此:

[] >> [1..4] -> []
[] `mplus` [1..4] -> [1,2,3,4]

这只是讨论MonadPlus的方面,特别是与(>>)相比的mplus方面。


哦,傻了。试图在 (Left a) >>= f = ... 中避免短路需要发明一个参数。即使被 (>>) 忽略,它也会使 Monad 实例变得非常邪恶。因此,虽然可能可以推导出 mplus,但不能以不引入非全性的方式(可靠地)完成,也就是说,不能明智地完成。话虽如此,我认为你在第二行代码中想要返回 x 而不是 a - barsoap
1
我认为你的意思是 x@(Left _) >>= _ = x,而不是 x@(Left a) >>= _ = a(因为a是一个字符串而不是某种类型tEither String t)。 - rampion
是的。修正了那个错字。已经使用 @ 进行了准备,但后来输入了错误的字符。 - fuz
尝试对您的Monad实例进行类型检查。它存在语法错误和类型错误。 - Peaker
@Peaker:已修复。感谢您的提示。 - fuz
在新发布的 GHC 8(base 4.9)中,“fail”被分成了一个单独的类型类MonadFail。现在我们可以定义instance MonadFail (Either String) where fail = Leftinstance String ~ a => MonadFail (Either a) 或者 无论情况如何 - Iceland_jack

2
在这个答案中,我想讨论一个话题,即为什么failMonad的成员。我不想将其添加到我的其他回答中,因为它涵盖了另一个话题。
虽然单子的数学定义不包含fail,但Haskell 98的创建者将其放入了Monad类型类中。为什么?
为了简化单子的使用并使单子的使用更容易抽象,他们引入了do符号,这是一个非常有用的语法糖。例如,这段代码:
do putStr "What's your full name? "
   [name,surname] <- getLine >>= return . words
   putStr "How old are you? "
   age <- getLine >>= return . read
   if age >= 18
      then putStrLn $ "Hello Mr / Ms " ++ surname
      else putStrLn $ "Hello " ++ name

翻译为:

putStr "What's your full name? " >>
getLine >>= return . words >>= \[name,surname] ->
putSr "How old are you? " >>
getLine >>= return . read >>= \age ->
if age >= 18
   then putStrLn $ "Hello Mr / Ms " ++ surname
   else putStrLn $ "Hello " ++ name

这里有什么问题?想象一下,你的名字中间有一个空格,比如 Jon M. Doe 。在这种情况下,整个结构将是_|_。当然,您可以通过添加一些临时函数和let来解决此问题,但这是纯粹的样板文件。在创建Haskell 98时,没有像今天这样的异常系统,您无法简单地捕获匹配失败。此外,不完整的模式被认为是不良编码风格。
解决方案是什么? Haskell 98的创建者添加了一个特殊的函数fail,在不匹配的情况下调用该函数。解糖的结果看起来有点像这样:
putStr "What's your full name? " >> let
  helper1 [name,surname] =
    putSr "How old are you? " >> let
      helper2 age =
        if age >= 18
           then putStrLn $ "Hello Mr / Ms " ++ surname
           else putStrLn $ "Hello " ++ name
      helper2 _ = fail "..."
    in getLine >>= return . read >>= helper2
  helper1 _ = fail "..."
in getLine >>= return . words >>= helper1

我不确定是否真的有一个名为helper2的东西,但我认为是有的。

如果你仔细看一下,你会发现它有多么聪明。首先,永远不会出现不完整的模式匹配,其次,您可以使fail可配置。为了实现这一点,他们只需将fail放入单子定义中。例如,对于Maybefail只是Nothing,而对于Either String的实例,则为Left。这样,编写单子无关的单子代码就很容易。例如,很长一段时间以来,lookup被定义为(Eq a,Monad b) => a -> [(a, b)] -> m b,如果没有匹配,则lookup返回fail

现在,在Haskell社区中仍然存在一个重要问题:像fail这样完全独立于Monad类型类的东西是否是一个坏主意?我无法回答这个问题,但我认为这个决定是正确的,因为其他地方并不那么适合fail


12
我仍然不相信fail必须在Monad中,而不是它自己的类中才能实现。如果出现“在<whereever>的do语句中出现了未完成的模式匹配导致(Failure Foo)没有实例”的编译器错误,我绝对不会介意。尽管这可能不太适合现有的编译器流程。 - barsoap
如果这个语法糖在另一个类中,行为将取决于这个类是否有实例。或者你会被迫声明一个实例来使用这个语法糖。如果某个混蛋在其他模块中定义了该类的一个实例,则这可能会改变您代码的含义。此外,请注意,实例就像病毒一样,一旦定义就可以在任何地方使用它们。此外,出于性能问题,类的所有实例都是静态的并且在执行之前已知。在运行时没有“我是Foo的实例”的运行时检查。这种静态类型是Haskell的基本属性。 - fuz
10
改变的不是动态行为:当实例缺失时,不完整的模式匹配将成为编译时错误,不涉及运行时检查,因此唯一的选择是让不可编译的代码编译(或者如果他引入重复的实例,则相反情况也适用于“Monad”)。如果您需要更高程度的安全性,则始终可以使用“-fwarn-incomplete-patterns”。 - barsoap
1
@barsoap:我不想引发争端。就个人而言,我喜欢它现在的样子。我认为这主要是品味问题,取决于你喜欢数学上的正确性还是实用主义。 - fuz

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