Actually CATCHing exceptions without creating GOTO

cod*_*ons 10 goto exception rakudo raku

Looking over my Raku code, I've realized that I pretty much never use CATCH blocks to actually catch/handle error. Instead, I handle errors with try blocks and testing for undefined values; the only thing I use CATCH blocks for is to log errors differently. I don't seem to be alone in this habit – looking at the CATCH blocks in the Raku docs, pretty much none of them handle the error in any sense beyond printing a message. (The same is true of most of the CATCH blocks in Rakudo.).

Nevertheless, I'd like to better understand how to use CATCH blocks. Let me work through a few example functions, all of which are based on the following basic idea:

sub might-die($n) { $n %% 2 ?? 'lives' !! die 418 }
Run Code Online (Sandbox Code Playgroud)

Now, as I've said, I'd normally use this function with something like

say try { might-die(3) } // 'default';
Run Code Online (Sandbox Code Playgroud)

But I'd like to avoid that here and use CATCH blocks inside the function. My first instinct is to write

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

But this not only doesn't work, it also (very helpfully!) doesn't even compile. Apparently, the CATCH block is not removed from the control flow (as I would have thought). Thus, that block, rather than the ternary expression, is the last statement in the function. Ok, fair enough. How about this:

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

(those line numbers are Lables. Yes, it's valid Raku and, yes, they're useless here. But SO doesn't give line numbers, and I wanted some.)

This at least compiles, but it doesn't do what I mean.

say might-die2(3);  # OUTPUT: «Nil»
Run Code Online (Sandbox Code Playgroud)

To DWIM, I can change this to

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

What these two reveal is that the result of the CATCH block is not, as I'd hopped, being inserted into control flow where the exception occurred. Instead, the exception is causing control flow to jump to the CATCH block for the enclosing scope. It's as though we'd written (in an alternate universe where Raku has a GOTO operator [EDIT: or maybe not that alternate of a universe, since we apparently have a NYI goto method. Learn something new every day…]

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

I realize that some critics of exceptions say that they can reduce to GOTO statements, but this seems to be carrying things a bit far.

I could (mostly) avoid emulating GOTO with the .resume method, but I can't do it the way I'd like to. Specifically, I can't write:

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

Because .resume doesn't take an argument. I can write

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

This works, at least in this particular example. But I can't help feeling that it's more of a hack than an actual solution and that it wouldn't generalize well. Indeed, I can't help feeling that I'm missing some larger insight behind error handling in Raku that would make all of this fit together better. (Maybe because I've spent too much time programming in languages that handle errors without exceptions?) I would appreciate any insight into how to write the above code in idiomatic Raku. Is one of the approaches above basically correct? Is there a different approach I haven't considered? And is there a larger insight about error handling that I'm missing in all of this?

rai*_*iph 9

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

[在我的问题中]其中一种方法基本上正确吗?

是的。在一般情况下,使用try和这样的功能if,而不是CATCH

有没有我没有考虑过的不同方法?

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

在所有这些中,我是否对错误处理有更深入的了解?

我将在这个答案的最后讨论我的一些想法。

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

我几乎从不使用CATCH块来实际捕获/处理错误。

我也不。

相反,我使用try块处理错误并测试未定义的值

这还差不多。

使用包罗万象记录错误 CATCH

我使用CATCH块的唯一目的是以不同的方式记录错误。

对。一个明智位于捕获所有。这是一个用例,我会说CATCH 一个不错的选择。

文档

查看Raku 文档中的CATCH块,除了打印消息之外,几乎没有任何一个可以处理任何意义上的错误。

如果文档在以下方面具有误导性:

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

  • 替代品;和/或

  • 什么是习惯(这海事组织使用的CATCH代码,其中try是比较合适的(现在我的新catch功能吗?))。

那将是不幸的。

CATCH Rakudo 编译器源代码中的块

CATCHRakudo 中的大多数街区也是如此。)

猜测这些将被明智地放置。放置一个调用堆栈耗尽之前,指定默认的异常处理(无论是作为警告加.resumedie或类似的),似乎是合理的我。他们都是这样吗?

为什么是相位器语句?

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

这不仅不起作用,而且(非常有用!)甚至无法编译。

.oO(那是因为你忘记了第一条语句末尾的分号)

(我原以为……该CATCH块 [本来] 从控制流中删除了)

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


引用文档:

移相器块只是包含它的闭包的一个特征,并在适当的时候自动调用。

这表明移相器不是一个语句,至少不是普通意义上的语句,并且可以假定,它会从普通控制流中删除。

但是回到文档:

移相器 [可能] 有一个运行时值,如果 [在] 周围表达式中进行计算,它们只需保存其结果以供在表达式中使用......当表达式的其余部分被计算时。

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


也许移除移相器在普通控制流中的位置,而是评估Nil它们是否不返回值的基本原理是这样的:

  • 移相器喜欢INIT 的返回值。编译器可以坚持将他们的结果分配给一个变量,然后显式返回该变量。但这将是非常不乐的。

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


尽管如此,总的来说,我在以下意义上支持你:

  • 认为普通控制流不包括不返回值的移相器似乎很自然。为什么要呢?

  • 如果 IWBNI 编译器看到一个不返回值的移相器用作包含其他返回值语句的块的最后一条语句,它似乎至少会发出警告

为什么CATCH块不返回/注入值?

好,可以。这个怎么样:

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

    say might-die2(3);  # OUTPUT: «Nil»
Run Code Online (Sandbox Code Playgroud)

如上所述,许多移相器,包括异常处理器,都是不返回值的语句。

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

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

  • 编译器会警告不返回值的移相器被放置在可能需要返回值的地方。


就好像我们写了...一个GOTO运算符

Raku(do) 不仅仅是在做一个非结构化的跳跃。

(否则.resume将无法工作。)

这似乎把东西搬得有点远

我同意,你把东西带得太远了。:P

.resume

可恢复的异常当然不是我在 Raku 中发现的东西。我认为我根本没有在“用户空间”代码中使用它们。

(来自jnthn 对我何时要恢复 Raku 异常的回答。)

.resume 不争论

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

即使CATCH块试图进行干预,它也无法以简单的、自包含的方式进行干预,即通过设置其赋值引发异常的变量的值,然后执行.resumeing。cf这个 RakuCATCH块应该能够改变词法范围内的变量吗?.

(我尝试了几种CATCH相关的方法,然后得出结论,只是使用trycatch我在开始时链接的函数体的方法。如果您还没有看过catch代码,我建议您这样做。)

关于CATCH块的更多花絮

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

更深入地了解错误处理

在所有这些中,我是否对错误处理有更深入的了解?

也许。我想你已经很了解其中的大部分内容了,但是:

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

  • KISS #2忽略一些原生类型用例,几乎所有结果都可以表示为有效或无效,而不会导致半谓词问题,使用以下 Raku Truth 值的简单组合,提供符合人体工程学的方法来区分非错误值和错误:

    • 条件:if, while, try, //, 等

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

    • 值/类型:NilFailures、零长度复合数据结构、:Dvs:U类型约束等

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

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

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

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

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

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

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

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

  • 语言/代码库之间的互操作。如果 Raku 提供高水平的控制和多样化的选择,那么唯一的方法是有效的。

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

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


Bra*_*ert 5

CATCH是该语言的一个非常古老的功能。
它曾经只存在于一个try块内。
(这不是很乐于助人。)

它也是 Raku 很少使用的部分。
这意味着没有多少人想出该功能的“痛点”
因此,很少有人做过任何工作来使其更加 Rakuish。

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


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

如果要在发生异常时替换结果

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

使用trywithoutCATCH和 defined? 或者//得到一些工作非常相似的东西要容易得多。

my $result = try { may-die } // 'replacement';
Run Code Online (Sandbox Code Playgroud)

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

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

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

my $result;
try {
  CATCH { default { $result = 'replacement' }}
  $result = may-fail();
}
Run Code Online (Sandbox Code Playgroud)

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

my $result = may-return-nil // 'replacement';
Run Code Online (Sandbox Code Playgroud)
my $result is default<replacement> = may-return-nil;
Run Code Online (Sandbox Code Playgroud)

但不管你有多少,Nil都不会工作。CATCHtry


实际上,我通常唯一一次使用的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;
}
Run Code Online (Sandbox Code Playgroud)

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

On Error Resume Next
Run Code Online (Sandbox Code Playgroud)

在乐

CATCH { default { .resume } }
Run Code Online (Sandbox Code Playgroud)

这当然丝毫不能真正回答您的问题。

您说您希望CATCH从控制流中删除。重点CATCH是将自己插入到异常控制流中。

其实这并不准确。它并没有自己插入到控制流中,而是在继续执行调用者/外部块之前进行一些处理时结束控制流。大概是因为当前区块的数据处于错误状态,不应该再被信任。

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

CATCH 只是具有一项重要的额外功能的众多移相器之一,它停止了调用堆栈中的异常传播。

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

这似乎不是一个好主意。

$a + may-die() + $b
Run Code Online (Sandbox Code Playgroud)

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

$a + 42 + $b
Run Code Online (Sandbox Code Playgroud)

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

还有一个问题,如果你真的想$a + may?die被替换呢。

42 + $b
Run Code Online (Sandbox Code Playgroud)

在您的想法中,您无法指定这一点。

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


如果某些代码抛出异常,则块处于不可恢复状态,需要停止执行。这么远,再也不远了。

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

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

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

我承认它.resume(value)似乎对控制异常有用。
(抓住CONTROL而不是CATCH。)