微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

实际上在不创建 GOTO

如何解决实际上在不创建 GOTO

查看我的 Raku 代码后,我意识到我几乎从不使用 CATCH 块来实际捕获/处理错误。相反,我使用 try 块处理错误并测试未定义的值;我使用 CATCH 块的唯一目的是以不同的方式记录错误。有这种习惯的似乎并不是我一个人——看看 Raku docs 中的 CATCH 块,除了打印消息之外,几乎没有人处理任何意义上的错误。 (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 运算符 [编辑:或者可能不是那个宇宙的替代,因为我们显然有 a NYI goto method . 每天学习新东西……]

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

我意识到一些对异常 say 的批评者认为它们可以简化为 GOTO 语句,但这似乎有点扯远了。

我可以(主要)避免使用 GOTO 方法模拟 .resume,但我无法按照我想要的方式进行。具体来说,我不能写:

    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'»

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

解决方法

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

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

是的。在一般情况下,使用 tryif 等特征,而不是 CATCH

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

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

是否有我在所有这些中遗漏的关于错误处理的更深入的见解?

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

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

亲吻

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

我也没有。

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

这更像是。

使用包罗万象的 CATCH 记录错误

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

没错。一个明智的定位全部。这是一个用例,我认为CATCH 很适合。

文档

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

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

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

  • 替代方案;和/或

  • 什么是惯用的(imo 不是CATCH 用于代码,其中 try 更合适(现在我的新 catch 函数也是?)).

那将是不幸的。

CATCH 块在 Rakudo 编译器源代码中

(Rakudo 中的大多数 CATCH 块也是如此。

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

为什么是 phasers 语句?

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

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

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

(我原以为……CATCH 块 [本应] 从控制流中移除)

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


引用文档:

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

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

但是回到文档:

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

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


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

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

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


总体而言,我在以下方面支持您:

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

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

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

好吧,够公平的。这个怎么样:

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

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

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

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

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

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


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

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

(否则 .resume 将不起作用。)

这个好像有点远

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

.resume

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

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

.resume 不接受参数

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

即使 CATCH 块试图进行干预,它也无法以简单、自包含的方式进行干预,即通过设置其赋值引发异常的变量的值,然后.resumeing。参见Should this Raku CATCH block be able to change variables in the lexical scope?

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

关于 CATCH 块的更多花絮

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

更深入地了解错误处理

是否有我在所有这些中遗漏的关于错误处理的更深入的见解?

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

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

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

    • 条件:ifwhiletry//

    • 谓词:.so.defined.DEFINITE

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

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

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

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

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

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

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

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

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

  • 语言/代码库之间的互操作。只有 Raku 提供高水平的控制和多样化的选项,才能发挥作用。

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

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

,

CATCH 是该语言的一个非常古老的特性。
它曾经只存在 inside of a try 块。
(这不是很乐于助人。)

它也是 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();
}

使用不带 tryCATCH 以及定义的或 // 来获得非常相似的东西要容易得多。

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 } }

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

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

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

这仍然不能解释为什么您的代码无法编译。 当涉及到语句结尾的分号时,您希望 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。)

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。