22

这是我在设计代码中多次出现的问题,尤其是库。似乎有人对它感兴趣,所以我认为它可能会成为一个很好的社区 wiki。

Monad 中的fail方法被一些人认为是一个缺点。不是来自原始范畴论的类的有点武断的添加。但当然,在当前状态下,许多 Monad 类型都有合乎逻辑且有用的fail实例。

MonadPlus 类是 Monad 的子类,它提供了一种mzero方法,该方法在逻辑上将失败的想法封装在 monad 中。

因此,想要编写一些执行某种故障处理的 monadic 代码的库设计者可以选择让他的代码使用failMonad 中的方法或将他的代码限制为 MonadPlus 类,这样他就可以对使用感到满意mzero,即使他根本不关心幺半群组合mplus操作。

关于这个主题的一些讨论在这个 wiki 页面中关于改革 MonadPlus 类的建议。


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

哪些 monad 实例(如果有)具有自然fail方法,但不能是 MonadPlus 的实例,因为它们没有逻辑实现mplus

但我最感兴趣的是关于这个主题的讨论。谢谢!


编辑:我想到了一个最后的想法。我最近了解到(即使它就在 for 的文档中fail)单子“do”符号以模式匹配失败的方式去糖,就像(x:xs) <- return []调用 monad 的fail.

似乎语言设计者一定受到了一些内置在 Haskell 语法中的自动故障处理前景的强烈影响,这些自动故障处理包含fail在 Monad 中。

4

2 回答 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对比的方面(>>)

于 2011-02-17T13:29:27.243 回答
2

在这个答案中,我想讨论这个话题,为什么failMonad. 我不想将此添加到我的其他答案中,因为它涵盖了另一个主题。


尽管 monad 的数学定义不包含fail,但 Haskell 98 的创建者将其放入Monad类型类中。为什么?

为了简化 monad 的使用并更容易抽象出 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了 monads 定义。例如,对于Maybe,fail是简单Nothing的,对于 的实例Either String,它是Left。这样,很容易编写独立于 monad 的 monadic 代码。例如,很长一段时间lookup被定义为(Eq a,Monad b) => a -> [(a, b)] -> m b,如果没有匹配则lookup返回。fail

Monad现在,在 Haskell 社区中仍然存在一个大问题:向类型类添加完全独立的东西不是一个坏主意fail吗?我无法回答这个问题,但恕我直言,这个决定是正确的,因为其他地方对fail.

于 2011-02-17T15:43:11.717 回答