适当使用Monad`unrupt`与MonadPlus`mzero`

时间:2011-02-17 00:50:06

标签: haskell monads typeclass

这个问题在设计代码中已经多次出现过,特别是库。似乎有some interest,所以我认为它可能会成为一个很好的社区维基。

Monad中的fail方法被某些人视为疣;这个课程有点武断,不属于原始范畴理论。但是当然在当前状态下,许多Monad类型都具有逻辑和有用的fail实例。

MonadPlus类是Monad的一个子类,它提供了一个mzero方法,它在逻辑上封装了monad中失败的概念。

因此,想要编写一些执行某种失败处理的monadic代码的库设计者可以选择让他的代码在Monad中使用fail方法或者将他的代码限制为MonadPlus类,这样他就可以了使用mzero会感觉很好,即使他根本不关心幺正组合mplus操作。

关于此主题的一些讨论在此Wiki页面中有关proposals to reform the MonadPlus class.

的内容

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

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

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


编辑:最后一个想法发生在我身上。我最近学到了(即使它在fail的文档中就是正确的),monadic“do”符号以模式匹配失败的方式被去除,如(x:xs) <- return []中调用monad的fail }。

似乎语言设计者必须受到一些自动故障处理的前景的强烈影响,因为haskell的语法在Monad中包含fail

2 个答案:

答案 0 :(得分:10)

想想Either。它的monadic实例看起来像这样:

{-# 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的各个方面,特别是与(>>)形成对比的{{1}}方面。

答案 1 :(得分:1)

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


虽然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放入monad定义中。例如,对于Maybefail只是Nothing,对于Either String的实例,它是Left。这样,编写monad独立的monadic代码就很容易了。例如,很长一段时间lookup被定义为(Eq a,Monad b) => a -> [(a, b)] -> m blookup如果没有匹配则返回fail

现在,Haskell社区仍然存在一个重要问题:添加完全独立于Monad类型类fail类型的东西并不是一个坏主意吗?我不能回答这个问题,但恕我直言这个决定是正确的,因为其他地方对fail不太舒服。