也许monad vs例外


Answers:


57

使用Maybe(或其Either工作原理基本相同,但可以让您返回任意值代替的表亲Nothing)的用途与异常的用途略有不同。用Java术语来说,就像有一个检查异常而不是运行时异常。它代表您必须处理的某些期望,而不是您没有预期的错误。

这样的函数indexOf会返回一个Maybe值,因为您希望该项目不在列表中。这类似于null从函数返回,只是以类型安全的方式强制您处理null情况。Either除了您可以返回与错误情况相关的信息外,其他方法的工作方式相同,因此实际上与异常更类似Maybe

那么Maybe/ Either方法的优点是什么?首先,它是该语言的一等公民。让我们将一个函数Either与一个引发异常的函数进行比较。对于例外情况,您唯一的真实诉求是try...catch声明。对于此Either功能,您可以使用现有的组合器使流量控制更加清晰。以下是几个示例:

首先,假设您要尝试几种可能连续出错的功能,直到获得没有出错的功能。如果您没有得到任何正确的消息,则想返回一条特殊的错误消息。这实际上是一个非常有用的模式,但使用会造成极大的痛苦try...catch。令人高兴的是,由于Either这只是一个正常值,因此您可以使用现有函数使代码更清晰:

firstThing <|> secondThing <|> throwError (SomeError "error message")

另一个示例是具有可选功能。假设您有多个要运行的功能,其中包括一个试图优化查询的功能。如果失败,则您希望其他所有内容继续运行。您可以编写如下代码:

do a <- getA
   b <- getB
   optional (optimize query)
   execute query a b

与使用相比try..catch,这两种情况都更清楚,更短,更重要的是,还具有更多的语义。与始终处理异常相比,使用类似<|>或之类的功能optional可以使您的意图更加清晰try...catch

另请注意,您不必在代码中乱码if a == Nothing then Nothing else ...!治疗的整点MaybeEither一个单子为的是避免这种情况。您可以将传播语义编码到bind函数中,以便免费获得null /错误检查。唯一需要显式检查的是,是否要返回Nothing给定以外的内容Nothing,即使这样也很容易:有很多标准库函数可以使代码更好。

最后,另一个优点是Maybe/ Either类型更简单。无需使用其他关键字或控件结构来扩展语言-一切都只是一个库。由于它们只是普通值,因此使类型系统更简单-在Java中,您必须区分throws不使用的类型(例如,返回类型)和效果(例如,语句)Maybe。它们的行为也与任何其他用户定义的类型一样—不需要在语言中嵌入特殊的错误处理代码。

另一个胜利是Maybe/ Either是函子和monad,这意味着它们可以利用现有的monad控制流功能(其中有一个相当大的数目),并且通常可以与其他monad一起很好地玩耍。

也就是说,有一些警告。其一,既不Maybe也不Either更换未经检查的异常。您将需要其他方法来处理诸如除以0之类的事情,这仅仅是因为让每个除法都返回一个Maybe值会很痛苦。

另一个问题是有多种类型的错误返回(仅适用于Either)。使用异常,您可以在同一函数中引发任何不同类型的异常。使用Either,您只会得到一种类型。这可以通过子类型化或包含所有不同类型错误的ADT作为构造函数来克服(第二种方法是Haskell中通常使用的方法)。

总体而言,我还是更喜欢Maybe/ Either方法,因为我发现它更简单,更灵活。


大多数人都同意,但是我不认为“可选”的例子有意义-似乎您可以做到以下几点也一样:void Optional(Action act){try {act(); } catch(Exception){}}
Errorsatz

11
  1. 异常可以携带有关问题根源的更多信息。OpenFile()可以抛出FileNotFoundNoPermissionTooManyDescriptors等。没有不携带这种信息。
  2. 可以在缺少返回值的上下文中使用异常(例如,使用具有返回值的语言的构造函数)。
  3. 异常使您可以非常轻松地将信息发送到堆栈,而无需使用很多if None return None-style语句。
  4. 异常处理几乎总是比返回值带来更高的性能影响。
  5. 最重要的是,异常和Maybe monad具有不同的用途-异常用于表示问题,而Maybe不是。

    “护士,如果 5室有病人,您能请他等吗?”

    • 也许莫纳德:“医生,5号房没有病人。”
    • 例外:“医生,没有5号房间!”

    (请注意“如果”-这表示医生正在期待 Maybe monad)


7
我不同意第2点和第3点。特别是,在使用monad的语言中2实际上并没有造成问题,因为这些语言的约定不会产生在构造过程中必须处理失败的难题。关于3,具有monad的语言具有适当的语法,可以以透明的方式处理monad,无需显式检查(例如,None值可以被传播)。您的观点5只是一种权利……问题是:哪些情况无疑是例外?事实证明…… 不多
康拉德·鲁道夫2012年

3
关于(2),您可以这样编写bind:测试None不会产生语法开销。一个非常简单的示例,C#只是Nullable适当地重载了运算符。None即使使用类型,也无需检查。当然,检查仍然可以完成(它是安全的类型),但是在幕后并不会使您的代码混乱。从某种意义上说,您对我对(5)的反对也同样适用,但我同意这可能并不总是适用。
康拉德·鲁道夫2012年

4
@Oak:关于(3),将其Maybe视为单子的全部要点是使传播None隐式。这意味着,如果您要返回None给定None,则根本不必编写任何特殊代码。您唯一需要匹配的时间是如果您想对进行特殊操作None。您永远不需要if None then None某种陈述。
蒂洪·耶维斯

5
@Oak:是的,但这不是我的意思。我的意思是,您可以免费获得null完全一样的检查(例如if Nothing then Nothing),因为它是monad。它在的bind()定义中编码。Maybe>>=Maybe
蒂洪·耶维斯

2
另外,关于(1):您可以轻松编写一个monad,该monad可以携带Either行为类似的错误信息(例如)Maybe。实际上,两者之间的切换非常简单,因为Maybe实际上只是的一种特殊情况Either。(在Haskell,你能想到的Maybe作为Either ()。)
吉洪Jelvis

6

“也许”不能替代例外。异常应在特殊情况下使用(例如:打开数据库连接,但应该有数据库服务器不存在)。“也许”用于为您可能具有或没有有效值的情况建模;说您正在从字典中获得某个键的值:它可能存在或可能不存在-这些结果中的任何一个都没有“例外”。


2
实际上,我有很多涉及字典的代码,其中缺少键意味着某些地方发生了严重错误,很可能是我的代码中或将输入转换为当时所用内容的代码中的错误。

3
@delnan:我并不是说缺失的值永远不能表示异常状态-并非必须如此。也许可以真正模拟一个变量可能具有或不具有有效值的情况-考虑一下C#中的可为空类型。
Nemanja Trifunovic

6

我是蒂洪的第二个回答,但我认为每个人都缺少一个非常重要的实际要点:

  1. 至少在主流语言中,异常处理机制与各个线程紧密耦合。
  2. Either机制根本不耦合到线程。

因此,我们今天在现实生活中看到的是,许多异步编程解决方案都采用了Either-style错误处理的变体。考虑以下任何链接中详细介绍的Javascript Promise

Promise的概念允许您编写这样的异步代码(摘自最后一个链接):

var greetingPromise = sayHello();
greetingPromise
    .then(addExclamation)
    .then(function (greeting) {
        console.log(greeting);    // 'hello world!!!!’
    }, function(error) {
        console.error('uh oh: ', error);   // 'uh oh: something bad happened’
    });

基本上,promise是一个对象:

  1. 表示异步计算的结果,可能尚未完成。
  2. 允许您链接进一步的操作以对其结果执行操作,该结果将在该结果可用时触发,并且其结果又可作为承诺使用;
  3. 允许您挂钩失败处理程序,如果promise的计算失败,该处理程序将被调用。如果没有处理程序,则错误将传播到链中的更高版本的处理程序。

基本上,由于当跨多个线程进行计算时,语言的本机异常支持不起作用,因此promises实现必须提供错误处理机制,并且这些结果类似于Haskell的Maybe/ Either类型。


它与线程无关。浏览器中的JavaScript总是在一个线程中运行,而不是在多个线程中运行。但是您仍然不能使用异常,因为您不知道将来何时调用函数。异步并不自动意味着线程的参与。这也是为什么您不能使用异常的原因。如果调用函数并立即执行,则只能获取异常。但是异步的全部目的是它在将来运行,通常是在其他事情完成后立即运行。这就是为什么您不能在其中使用异常的原因。
大卫·拉布

1

Haskell类型的系统将要求用户承认的可能性Nothing,而编程语言通常不需要捕获异常。这意味着我们将在编译时知道用户已经检查了错误。


1
Java中有检查的异常。人们常常甚至对他们都不好。
弗拉基米尔

1
@ DairT'arg ButJava需要检查IO错误(例如),但不检查NPE。在Haskell中,则是另一种方法:检查了等效的空值(也许),但IO错误仅引发(未检查的)异常。我不确定会有什么不同,但是我没有听到Haskellers抱怨Maybe,我想很多人都希望对NPE进行检查。

1
@delnan人们想检查NPE吗?他们一定很生气。我的意思是。仅当您首先有一种避免使用 NPE的方法时才进行NPE检查(通过使用非空引用,Java没有这种引用)。
康拉德·鲁道夫2012年

5
@KonradRudolph是的,人们可能不希望对其进行检查,因为他们必须throws NPE在每个单个签名和catch(...) {throw ...} 每个方法主体中添加一个。但我确实相信,与Maybe一样,检查市场也是如此:可空性是可选的,并在类型系统中进行跟踪。

1
@delnan嗯,那我同意。
康拉德·鲁道夫2012年

0

可能单子与大多数主流语言对“空意味着错误”检查的使用基本相同(除了它需要检查空值),并且在很大程度上具有相同的优点和缺点。


8
那么,它具有相同的缺点,因为它可以静态类型正确使用检查。当可能使用monad时,没有等效的null指针异常(同样,假设它们使用正确)。
康拉德·鲁道夫2012年

确实。您认为应该如何澄清?
Telastyn

@Konrad那是因为要使用Maybe中的值(可能),您必须检查None(当然,可以自动执行很多检查)。其他语言则将这种事情保持手动状态(主要是出于我认为的哲学原因)。
Donal Fellows

2
@Donal是的,但请注意,大多数实现monad的语言的确提供了适当的语法糖,因此通常可以完全隐藏此检查。例如,您可以只Maybe写两个数字a + b 无需检查None,结果又是一个可选值。
康拉德·鲁道夫2012年

2
与可以为null的类型进行比较对于该Maybe 类型仍然适用,但是Maybe用作monad会添加语法糖,从而可以更优雅地表达null-ish逻辑。
tdammers 2012年

0

异常处理可能是分解和测试的真正难题。我知道python提供了不错的“ with”语法,使您无需严格的“ try ... catch”块即可捕获异常。但是例如,在Java中,尝试catch块很大,很冗长,既冗长又极其冗长,并且很难分解。最重要的是,Java添加了围绕检查和未检查异常的所有干扰。

相反,如果您的monad捕获异常并将其视为Monadic空间的属性(而不是某些处理异常),那么您可以自由地混合和匹配绑定到该空间的函数,而不管它们抛出或捕获的内容是什么。

如果更好的是,您的monad阻止了可能发生异常的情况(例如将空检查强加到Maybe中),那就更好了。如果...那么,比尝试...捕获要容易得多,那么分解和测试就容易得多。

从我所看到的来看,Go通过指定每个函数返回(答案,错误)采取了类似的方法。这有点类似于将函数“提升”到monad空间,在该空间中核心答案类型装饰有错误指示,并有效地避免了引发和捕获异常的发生。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.