在不创建 GOTO 的情况下实际捕获异常

Actually CATCHing exceptions without creating GOTO

查看我的 Raku 代码,我意识到我几乎从不使用 CATCH 块来实际 catch/handle 错误。相反,我使用 try 块处理错误并测试未定义的值;我使用 CATCH 块的唯一目的是以不同的方式记录错误。我似乎不是唯一有这种习惯的人——查看 Raku docs 中的 CATCH 块,其中几乎 none 处理错误的方式不只是打印一条消息。 (Rakudo 中的大多数 CATCH 块也是如此。)。

不过,我想更好地了解如何使用 CATCH 块。让我完成几个示例函数,所有这些函数都基于以下基本思想:

sub might-die($n) { $n %% 2 ?? 'lives' !! die 418 }

现在,正如我所说,我通常将此功能与

say try { might-die(3) } // 'default';

但我想在这里避免这种情况,并在函数内部使用 CATCH 块。我的第一直觉是写

sub might-die1($n) {
    $n %% 2 ?? 'lives' !! die 418
    CATCH { default { 'default' }}
}

但这不仅不起作用,而且(非常有帮助!)甚至无法编译。显然,CATCH 而不是 从控制流中删除(正如我所想的那样)。因此,该块而不是三元表达式是函数中的最后一条语句。好,可以。这个怎么样:

    sub might-die2($n) {
ln1:    CATCH { default { 'default' }}
ln2:    $n %% 2 ?? 'lives' !! die 418
    }

(那些行号是 Lables。是的,它是有效的 Raku,是的,它们在这里没用。但是 SO 没有给出行号,我想要一些。)

这至少可以编译,但它不符合我的意思。

say might-die2(3);  # OUTPUT: «Nil»

对于DWIM,我可以把这个改成

    sub might-die3($n) {
ln1:    CATCH { default { return 'default' }}
ln2:    $n %% 2 ?? 'lives' !! die 418
    }
say might-die3(3);  # OUTPUT: «'default'»

这两个揭示的是 CATCH 块的结果是 而不是 ,正如我所跳的那样,被插入到发生异常的控制流中。相反,异常导致控制流跳转到封闭范围的 CATCH 块。就好像我们写了(在另一个宇宙中,Raku 有一个 GOTO 运算符 [编辑:或者可能没有 that 宇宙的替代,因为我们显然有 a NYI goto method。每天学点新东西…]

    sub might-die4($n) {
ln0:    GOTO ln2;
ln1:    return 'default';
ln2:    $n %% 2 ?? 'lives' !! GOTO ln1;
    }

我意识到一些异常的批评者 say 他们可以减少到 GOTO 语句,但这似乎有点过头了。

我可以(大部分)避免使用 .resume 方法模拟 GOTO,但我无法按照自己喜欢的方式进行。具体写不出来:

    sub might-die5($n) {
ln1:    CATCH { default { .resume('default') }}
ln2:    $n %% 2 ?? 'lives' !! die 418
    }

因为.resume不接受争论。我可以

    sub might-die6($n) {
ln1:    CATCH { default { .resume }}
ln2:    $n %% 2 ?? 'lives' !! do { die 418; 'default' }
    }
say might-die6 3;  # OUTPUT: «'default'»

这有效,至少在这个特定的例子中是这样。但我忍不住觉得它更像是一种 hack,而不是实际的解决方案,而且它不能很好地概括。事实上,我不禁觉得我错过了 Raku 错误处理背后的一些更深入的见解,这些见解可以使所有这些更好地结合在一起。 (也许是因为我花了太多时间用无一例外地处理错误的语言进行编程?)如果能深入了解如何用惯用的 Raku 编写上述代码,我将不胜感激。上述方法之一是否基本正确?有没有我没有考虑过的不同方法?我在所有这些中都缺少关于错误处理的更深入的见解吗?

“更深入地了解错误处理”

Is one of the approaches [in my question] basically correct?

是的。在一般情况下,使用像 tryif 这样的特征,而不是 CATCH.

Is there a different approach I haven't considered?

这是一个全新的:catch。几周前我发明了它的第一个版本,现在你的问题促使我重新构想它。我对现在的解决方式很满意;我非常感谢读者对此的反馈。

is there a larger insight about error handling that I'm missing in all of this?

我会在这个回答的最后讨论我的一些想法。

但是现在让我们按照你写的顺序来过一遍你的观点。

I pretty much never use CATCH blocks to actually catch/handle error.

我也没有。

Instead, I handle errors with try blocks and testing for undefined values

差不多吧

使用包罗万象记录错误 CATCH

the only thing I use CATCH blocks for is to log errors differently.

对。位置巧妙的 catchall。这是一个用例,我会说 CATCH 很合适。

文档

looking at the CATCH blocks in the Raku docs, pretty much none of them handle the error in any sense beyond printing a message.

如果文档误导了:

  • CATCH/CONTROL块的能力和适用性的限制; and/or

  • 备选方案; and/or

  • 什么是惯用的(在我看来 不是 使用 CATCH 代码 try 更合适(现在我的新catch 也有功能?)).

那就不幸了。

CATCH Rakudo 编译器源中的块

(The same is true of most of the CATCH blocks in Rakudo.).

猜测这些将被明智地放置在笼子里。在调用堆栈用完之前放置一个,以指定默认异常处理(作为警告加 .resume,或 die 或类似),对我来说似乎是合理的。他们都是这样吗?

为什么是phasers个报表?

sub might-die1($n) {
    $n %% 2 ?? 'lives' !! die 418
    CATCH { default { 'default' }}
}

this not only doesn't work, it also (very helpfully!) doesn't even compile.

.oO(那是因为你忘记在第一个语句末尾添加分号)

(I would have thought ... the CATCH block [would have been] removed from the control flow)

加入俱乐部。其他人已在提交的错误以及 SO Q 和 A 中表达了相关观点。我曾经认为现在的情况和你表达的一样是错误的。我想我现在很容易被争论的任何一方说服——但 jnthn 的观点对我来说将是决定性的。


引用文档:

A phaser block is just a trait of the closure containing it, and is automatically called at the appropriate moment.

这表明移相器 不是 语句,至少不是普通意义上的语句,并且可能会被从普通控制流中删除。

但是 return正在查看文档:

Phasers [may] have a runtime value, and if evaluated [in a] surrounding expression, they simply save their result for use in the expression ... when the rest of the expression is evaluated.

这表明它们可以普通控制流意义上具有


也许移除移相器在普通控制流中的位置,而不是评估为Nil,否则return 一个值,类似于:

  • Phasers 喜欢 INIT do return 值。编译器可以坚持将其结果分配给一个变量,然后显式 returns 该变量。但那将是非常不 Raku-ish。

  • Raku 的哲学是,一般来说,开发人员告诉编译器该做什么或不做什么,而不是相反。移相器是一个声明。如果你把一个语句放在最后,那么你希望它是它的封闭块的值 returned。 (即使是 Nil。)


不过,总的来说,我在以下意义上支持你:

  • 似乎很自然地认为普通控制流不包括没有 return 值的移相器。为什么要这样做?

  • IWBNI 编译器似乎至少警告 如果它看到一个非值-returning 移相器用作包含其他值-returning 语句的块。

为什么 CATCH 不阻止 return/inject 一个值?

Ok, fair enough. How about this:

    sub might-die2($n) {
ln1:    CATCH { default { 'default' }}
ln2:    $n %% 2 ?? 'lives' !! die 418
    }

    say might-die2(3);  # OUTPUT: «Nil»

如上所述,许多移相器(包括异常处理移相器)都是没有 return 值的语句。

我认为人们可以合理地预期:

  • CATCH 移相器 return 一个值。但他们没有。我依稀记得 jnthn 已经在 SO 上解释了为什么;我将把它作为读者的练习。或者,反过来:

  • 编译器会警告说,没有 return 值的移相器被放置在可能需要 returned 值的地方。


It's as though we'd written ... a GOTO operator

Raku(do) 不只是进行非结构化跳跃。

(否则.resume不行。)

this seems to be carrying things a bit far

我同意,你有点过分了。 :P

.resume

Resumable exceptions certainly aren't something I've found myself reaching for in Raku. I don't think I've used them in "userspace" code at all yet.

(来自 jnthn's answer to When would I want to resume a Raku exception?。)

.resume doesn't take an argument

对。它只是在导致抛出异常的语句之后的语句处恢复执行。 .resume 不会改变失败语句的结果。

即使 CATCH 块试图进行干预,它也无法通过设置赋值引发异常的变量的值来以简单、独立的方式进行干预,然后 .resumeing。比照 .

(我尝试了几种 CATCH 相关的方法,然后得出结论,仅使用 try 是我在开始时链接的 catch 函数主体的方法。如果你还没有看过 catch 代码,我建议你看一下。)

关于 CATCH 个块的更多花絮

出于几个原因,他们有点担心。其一似乎是故意限制它们的预期能力和适用性。另一个是错误。例如考虑:

更深入地了解错误处理

is there a larger insight about error handling that I'm missing in all of this?

也许吧。我认为您已经了解其中的大部分内容,但是:

  • KISS #1 您已经在其他 PL 中毫无例外地处理了错误。有效。你已经在 Raku 做到了。有用。仅当您需要想要使用它们时才使用异常。对于大多数代码,您不会。

  • KISS #2忽略一些native类型的用例,几乎所有的结果都可以表示为有效或无效,不会导致semi-predicate problem, using simple combinations of the following Raku Truth value 提供符合人体工程学的方法来区分非错误值和错误:

    • 条件:ifwhiletry//

    • 谓词:.so.defined.DEFINITE、等

    • Values/types:NilFailures、零长度复合数据结构、:D vs :U 类型约束等阿尔

坚持错误异常,我认为有几点值得考虑:

  • Raku 错误异常的一个用例是覆盖与 Haskell 中的异常相同的基础。在这些情况下,将它们作为值处理不是正确的解决方案(或者,在 Raku 中,可能 不是)。

  • 其他 PL 支持异常。 Raku 的超能力之一是能够与所有其他 PL 互操作。因此,如果除了启用正确的互操作之外没有其他原因,它支持异常。

  • Raku 包含 Failure 的概念,即延迟异常。这个想法是您可以两全其美。如果小心处理,Failure 只是一个错误值。处理不慎,它会像常规异常一样爆炸。

更一般地说,Raku 的所有功能都旨在协同工作以提供方便但高质量的错误处理,支持以下所有编码方案:

  • 快速编码。原型制作、探索性代码、一次性等

  • 稳健性控制。逐渐缩小或扩大错误处理范围。

  • 多种选择。应该发出什么错误信号?什么时候?通过哪个代码?如果消费代码想要发出信号,生产代码应该更严格怎么办?还是比较放松?如果反过来——producing code 想要发出信号 consuming code 应该更加小心或可以放松怎么办?如果生产代码和使用代码的理念存在冲突怎么办?如果生成的代码无法更改(例如,它是一个库,或者用另一种语言编写)怎么办?

  • 语言/代码库之间的互操作。唯一可行的方法是 Raku 提供高水平的控制和多样化的选择。

  • 这些场景之间的方便重构。

所有这些因素以及更多因素构成了 Raku 错误处理方法的基础。

CATCH 是该语言的一项非常古老的功能。
它曾经只存在 inside of a try 块。
(这不是很 Rakuish。)

也是Raku中很少用到的部分。
这意味着没有多少人提出该功能的“痛点”
所以很少有人做任何工作让它变得更 Rakuish。

这两者的结合使得 CATCH 成为该语言中相当无特色的部分。
如果您查看该功能的测试文件,您会注意到当测试套件仍然是 Pugs 项目的一部分时,其中大部分是 written in 2009
(其余大部分是对多年来发现的错误的测试。)


很少有人尝试向 CATCH 添加新行为,这是有充分理由的,还有许多其他功能更好用。

如果要在出现异常时替换结果

sub may-die () {
  if Bool.pick {
    return 'normal'
  } else {
    die
  }
}
my $result;
{
  CATCH { default { $result = 'replacement' }}
  $result = may-die();
}

只使用 try 而不使用 CATCH 以及 defined-or // 以获得非常相似的效果要容易得多。

my $result = try { may-die } // 'replacement';

如果您处理的是软故障而不是硬异常,那就更容易了,因为您可以只使用定义或本身。

sub may-fail () {
  if Bool.pick {
    return 'normal'
  } else {
    fail
  }
}
my $result = may-fail() // 'replacement';

事实上,将 CATCH 与软故障一起使用的唯一方法是将其与 try

结合使用
my $result;
try {
  CATCH { default { $result = 'replacement' }}
  $result = may-fail();
}

如果您的软故障是所有故障对象的基础 Nil,您可以使用 //is default

my $result = may-return-nil // 'replacement';
my $result is default<replacement> = may-return-nil;

但是 Nil 不会只与 CATCH 一起工作,无论你 try


我通常唯一一次使用 CATCH 是当我想以不同的方式处理几个不同的错误时。

{
  CATCH {
    when X::Something { … }
    when X::This      { … }
    when X::That      { … }

    default           { … }
  }

  # some code that may throw X::This
  …
  # some code that may throw X::NotSpecified (default)
  …
  # some code that may throw X::Something
  …
  # some code that may throw X::This or X::That
  …

  # some code that may fail instead of throw
  # (sunk so that it will throw immediately)
  sink may-fail;
}

或者,如果我想向您展示如何编写这个[可怕的] Visual Basic 行

On Error Resume Next

CATCH { default { .resume } }

当然,这并不能真正回答你的问题。

您说您希望 CATCH 从控制流程中删除。 CATCH 的全部意义在于将其自身插入异常控制流中。

实际上这并不准确。它并没有将 insert 自身插入到控制流中,而是在继续进行 caller/outside 块之前在进行一些处理时结束控制流。大概是因为当前区块的数据处于错误状态,不应该再被信任。

这仍然不能解释为什么您的代码无法编译。 当涉及到语句结束的分号时,您希望 CATCH 有自己的特殊语法规则。 如果它按照您预期的方式工作,它将无法通过 Raku 中的重要 [语法] 规则之一,“应该有尽可能少的特殊情况”。它的语法在任何方面都不像您所期望的那样特殊。

CATCH 只是众多相位器中的一个,它具有一个重要的额外功能,它可以阻止异常在调用堆栈中传播。

您似乎要求它改变可能抛出的表达式的结果。

这似乎不是个好主意。

$a + may-die() + $b

您希望能够用一个值替换 may-die 中的异常。

$a + 42 + $b

基本上,您要求能够将远距离动作添加为一项功能。

还有一个问题,如果你真的想把$a + may‑die换成

怎么办?
42 + $b

你的想法中没有办法让你具体说明。

更糟糕的是,有一种方法可能会意外发生。如果 may‑die 开始返回失败而不是异常怎么办。然后它只会在您尝试使用它时导致异常,例如将它添加到 $a.


如果某些代码抛出异常,则该块处于不可恢复状态,需要停止执行。到此为止。

如果表达式抛出异常,则执行它所在语句的结果是可疑的。
其他语句可能依赖于那个损坏的语句,所以整个块也是可疑的。

我认为如果它允许代码继续但当前表达式的结果不同,那将不是一个好主意。特别是如果该值可以远离块内其他地方的表达式。 (远距离动作)

如果您能想出一些可以用 .resume(value) 大大改进的代码,那么也许可以添加它。
(我个人认为leave(value)在这种情况下会更有用。)

我同意 .resume(value) 看起来它可能对控制异常有用。
(捕捉到 CONTROL 而不是 CATCH。)