【问题标题】:Should I avoid using Monad fail?我应该避免使用 Monad 失败吗?
【发布时间】:2011-12-31 02:54:13
【问题描述】:

我对 Haskell 还很陌生,并且慢慢地意识到 Monad fail 的存在有问题。 Real World Haskell warns against its use(“再一次,我们建议您几乎总是避免使用失败!”)。我今天刚刚注意到,Ross Paterson 将其称为“一个缺陷,而不是一种设计模式”back in 2008(并且似乎在该线程中达成了相当的一致)。

在观看 Ralf Lämmel 博士 talk on the essence of functional programming 时,我开始理解可能导致 Monad 失败的紧张局势。在讲座中,Ralf 谈到了将各种 monadic 效果添加到基本 monadic 解析器(日志记录、状态等)。许多效果需要更改基本解析器,有时还需要更改使用的数据类型。我认为向所有 monad 添加 'fail' 可能是一种妥协,因为 'fail' 是如此普遍,并且您希望尽可能避免更改 'base' 解析器(或其他)。当然,某种“失败”对解析器来说是有意义的,但并非总是如此,例如,状态的 put/get 或 Reader 的 ask/local。

如果我走错了路,请告诉我。

我应该避免使用 Monad 失败吗? Monad失败的替代方案是什么? 是否有任何不包含此“设计疣”的替代 monad 库? 我在哪里可以了解有关此设计决策的历史的更多信息?

【问题讨论】:

  • Real World Haskell says "[在许多 monads 中] fail 使用 error。调用 error 通常是非常不可取的,因为它会引发调用者无法捕获或不会期望的异常。"
  • 我相信当你这样处理模式匹配失败时需要“失败”:“Just x
  • @Paul Johnson 为什么? GHC 运行时将愉快地在模式匹配失败时使常规函数崩溃。仅在 monad 实例具有捕获该错误的能力的情况下才需要,这是一种并非真正专属于或普遍存在于 monad 的 hack。
  • 你应该使用MonadFail

标签: monads haskell


【解决方案1】:

一些 monad 有一个合理的失败机制,例如终端单子:

data Fail x = Fail

一些 monad 没有合理的失败机制(undefined 不合理),例如最初的单子:

data Return x = Return x

从这个意义上说,要求所有 monad 都有一个 fail 方法显然是一个缺点。如果您正在编写对 monads (Monad m) => 进行抽象的程序,那么使用通用 mfail 方法并不是很健康。这将导致您可以使用不应该真正存在 fail 的 monad 来实例化一个函数。

当在一个明确指定了良好fail 行为的特定monad 中工作时,我看到使用fail(尤其是间接地,通过匹配Pat <- computation)的反对意见较少。这样的程序有望在回归旧学科后幸存下来,在旧学科中,非平凡的模式匹配创建了对 MonadZero 的需求,而不仅仅是 Monad

有人可能会争辩说,更好的纪律总是明确地处理失败案例。我在两个方面反对这个立场:(1)monadic 编程的重点是避免这种混乱,以及(2)当前对 monadic 计算结果进行案例分析的符号是如此糟糕。 SHE 的下一个版本将支持该表示法(也可以在其他变体中找到)

case <- computation of
  Pat_1 -> computation_1
  ...
  Pat_n -> computation_n

这可能会有所帮助。

但是这整个情况是一团糟。通过它们支持的操作来表征 monad 通常是有帮助的。您可以看到failthrow 等作为某些monad 支持的操作,但其他monad 不支持。 Haskell 使得在可用操作集中支持小的本地化更改变得非常笨拙和昂贵,通过解释如何根据旧操作来处理它们来引入新操作。如果我们真的想在这里做得更整洁,我们需要重新考虑catch 的工作原理,使其成为不同本地错误处理机制之间的转换器。我经常想用一个在传递错误之前添加更多上下文信息的处理程序将可能无信息失败的计算(例如,通过模式匹配失败)括起来。我不禁感到,有时做这件事比应该做的更难。

因此,这是一个可以做得更好的问题,但至少,fail 仅用于提供合理实现并正确处理“异常”的特定 monad。

【讨论】:

  • 请问,你说的那个“SHE”是什么?
  • 这是 Strathclyde Haskell 增强功能,我的预处理器支持实验性功能(主要是假依赖类型)。但如今,由于在 GHC 中采用了大部分功能,SHE 已经变得多余了。
  • 似乎没有采用建议的语法,但您可以使用单独的函数或使用 LambdaCase(如果您不使用 do 表示法)。见stackoverflow.com/q/5395517/309483
【解决方案2】:

在 Haskell 1.4 (1997) 中没有fail。相反,有一个 MonadZero 类型类,其中包含一个 zero 方法。现在,do 表示法使用zero 表示模式匹配失败;这给人们带来了惊喜:他们的函数需要Monad还是MonadZero取决于他们如何使用其中的do符号。

当 Haskell 98 的设计稍晚一些时,他们进行了一些更改以使新手更容易编程。例如,monad 推导变成了列表推导。同样,要删除do 类型类问题,删除了MonadZero 类;对于do的使用,fail方法被添加到Monad;对于zero 的其他用途,mzero 方法已添加到MonadPlus

我认为,fail 不应明确用于任何事情,这是一个很好的论据;它的唯一用途是在do 符号的翻译中。不过,我自己也经常调皮,也明确使用fail

您可以访问原始 1.4 和 98 报告 here。我确信可以在一些电子邮件列表档案中找到导致更改的讨论,但我没有方便的链接。

【讨论】:

  • “我自己经常很调皮,并且明确地使用fail”。经常?解释一下。
  • @DanBurton:有什么要解释的?如果我在单子上下文中,我倾向于使用fail 来表示失败,除非我有特殊需要使用其他机制。
  • 谢谢。很高兴听到历史。我不知道 Haskell 最初只有 do 表示法(没有列表推导)。我在邮件列表存档mail-archive.com/haskell@haskell.org/msg03002.html 中发现了一个有趣的线程
  • @StevenShaw,列表推导比 do 表示法更早。只是有一段时间,在 Haskell 中,列表推导式已经泛化为 monad 推导式。它们仍然适用于列表,但也适用于所有其他单子。
  • “例如,monad 推导式变成了列表推导式”。 10 年后,Scala 得到了单子的理解。 :-/
【解决方案3】:

我尽量避免 Monad 失败,根据您的情况,有多种方法可以捕获失败。 Edward Yang 在他的博客上写了一篇很好的概述,标题为8 ways to report errors in Haskell revisited

总之,他指出的报告错误的不同方法是:

  1. 使用错误
  2. 使用 Maybe a
  3. 使用任一字符串 a
  4. 使用 Monad 并不能泛化 1-3
  5. 使用 MonadError 和自定义错误类型
  6. 在 IO monad 中使用 throw
  7. 使用 ioError 和 catch
  8. 用单子变压器发疯
  9. 已检查的异常
  10. 失败

其中,如果我知道如何处理错误,但需要更多上下文,我会很想使用选项 3 和 Either e b

【讨论】:

  • Maybe... 使用其中任何一个都很好。但是,在同一代码中使用使用 Either 的库和使用 Maybe 的库很烦人。 Monad 转换器不是那么好......
猜你喜欢
  • 2012-10-14
  • 2010-09-23
  • 2018-06-16
  • 2021-06-03
  • 1970-01-01
  • 2011-03-26
  • 2014-10-24
  • 2010-09-07
  • 2013-06-27
相关资源
最近更新 更多