实际上捕获异常而不创建GOTO [英] Actually CATCHing exceptions without creating GOTO

查看:67
本文介绍了实际上捕获异常而不创建GOTO的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

查看我的Raku代码,我意识到我几乎从不使用 CATCH 块来实际捕获/处理错误.相反,我使用 try 块处理错误并测试未定义的值.我使用 CATCH 块的唯一目的是以不同的方式记录错误.我似乎并不孤单,只是看着CATCH 块> Raku docs ,除了打印消息外,几乎没有任何其他方式可以处理错误.(Rakudo中的大多数 CATCH 块都是一样的.)

尽管如此,我还是想更好地了解如何使用 CATCH 块.让我研究一些示例函数,所有这些函数均基于以下基本思想:

  sub may-die($ n){$ n %% 2 ??活着"!死418} 

现在,正如我所说的,我通常会将此功能与类似的功能一起使用

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

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

  sub may-die1($ n){$ n %% 2 ??活着"!死于418CATCH {默认{'default'}}} 

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

  sub mays-die2($ n){ln1:CATCH {默认{'default'}}ln2:$ n %% 2?活着"!死于418} 

(这些行号为 <代码>标签 .是的,这是有效的Raku,是的,它们在这里没有用.但是SO没有给出行号,我想要一些.)

这至少可以编译,但是并没有达到我的意思.

  saymay-die2(3);#输出:«无» 

对于DWIM,我可以将其更改为

  sub mays-die3($ n){ln1:CATCH {默认{return'default'}}ln2:$ n %% 2?活着"!死于418}说maye-die3(3);#输出:«'默认'» 

这两个揭示的是,正如我希望的那样, CATCH 块的结果不是 not ,而是插入到发生异常的控制流中.而是,异常导致控制流跳到封闭范围的 CATCH 块.就像我们写的那样(在一个替代宇宙中,Raku具有 GOTO 运算符

  sub mays-die4($ n){ln0:转到ln2;ln1:返回默认";ln2:$ n %% 2?活着"!转到ln1;} 

我意识到一些对异常的批评家

因为 .resume 不带参数.我可以

  sub mays-die6($ n){ln1:CATCH {默认{.resume}}ln2:$ n %% 2?活着"!做{死418;'默认' }}说威武地死6 3;#输出:«'默认'» 

这至少在此特定示例中有效.但是我不禁感到这不是真正的解决方案,而是更多的hack,而且推广效果不佳.确实,我不禁感到自己缺少在Raku中进行错误处理的更深入的了解,而这会使所有这些更好地结合在一起.(也许是因为我花了太多时间在处理没有例外的错误的语言上进行编程?)对于如何用惯用的Raku编写上述代码有任何见解,我将不胜感激.以上方法之一基本正确吗?我有没有考虑过其他方法?在所有这些方面,我是否缺少对错误处理的更深入的了解?

关于错误处理的更多见解"

(在我的问题中)其中一种方法基本正确吗?

是的.在一般情况下,请使用诸如 try if 之类的功能,而不要使用 CATCH .

我有没有考虑过其他方法?

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

在所有这些方面我是否缺少对错误处理的更深入的了解?

我将在此答案的最后讨论我的一些想法.

但是,现在让我们按编写顺序依次讲解.

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

我也不是.

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

那更像它.

使用全部 CATCH

记录错误

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

对.位置合理的渔获物全部.这是一个用例,我会说 CATCH 很合适.

文档

查看 Raku文档中的 CATCH 块,除了打印消息外,他们几乎没有任何其他方式可以处理错误.

如果文档对以下内容产生误导:

  • CATCH / CONTROL 块的功能和适用范围的限制;和/或

  • 替代方案;和/或

  • 惯用语是什么(不是imo不是 使用 CATCH 的代码,而 try 更合适(现在是我的新 catch 函数吗?)).

那将是不幸的.

Rakudo编译器源文件中的

CATCH

(Rakudo中的大多数 CATCH 块都一样.)

猜测这些将被明智地摆放.在我看来,在调用堆栈用尽之前放置一个以指定默认的异常处理(例如警告加上 .resume die 或类似名称),似乎很合理.都是这些吗?

为什么 phasers 语句?

  sub may-die1($ n){$ n %% 2 ??活着"!死了418CATCH {默认{'default'}}} 

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

.o(这是因为您在第一条语句末尾忘记了分号)

(我本以为... CATCH 块[本来应该从控制流中删除的)

加入俱乐部.其他人则在归档的bug,SO Q和A中表达了相关的观点.我曾经认为目前的情况以您表达的方式是错误的.我想我现在可以很容易地被论点的两边说服了-但是jnthn的观点对我来说是决定性的.


引用文档:

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

这表明相位器不是不是语句,至少不是普通意义上的,并且可能会假定将其从普通控制流中删除.

但是返回文档:

相量[可能]具有运行时值,如果[在]周围的表达式中进行求值,则它们仅将其结果保存以供表达式使用... ...在对表达式的其余部分求值时.

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


也许将移相器从其在普通控制流中的位置中删除,然后如果不返回值则求值为 Nil 的理由是像:

  • 诸如 INIT do 之类的返回值.编译器可能会坚持要求将结果分配给一个变量,然后显式返回该变量.但这将是非常不乐世的.

  • Raku的哲学是,通常,开发人员告诉编译器做什么或不做什么,而不是相反.移相器是一个声明.如果将语句放在末尾,则希望它是其封闭块返回的值.(即使它是 Nil .)


总的来说,我在以下意义上与您在一起:

  • 很自然地认为,普通控制流不包括不返回值的相位器.为什么要这样?

  • 如果IWBNI看到一个不返回值的相位器用作包含其他返回值的语句的块的最后一个语句,则似乎至少警告了 .

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

好吧,很公平.怎么样:

  sub mays-die2($ n){ln1:CATCH {默认{'default'}}ln2:$ n %% 2?活着"!死于418}说maye-die2(3);#输出:«无» 

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

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

  • CATCH 移相器返回一个值.但是他们没有.我隐约记得jnthn已经在SO上解释了为什么.我会把它保留下来作为一种练习,供读者阅读.或者相反:

  • 编译器会警告说,未打算将返回值的移相器放置在可能要返回值的地方.


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

Raku(do)不仅仅是在进行非结构化跳转.

(否则, .resume 无效.)

这似乎承载了一些距离

我同意,您所进行的工作有点过头了.:P

.resume

可恢复的异常肯定不是我发现自己在Raku中所追求的.我认为我没有在用户空间"中使用它们.完全没有代码.

(来自 jnthn对我何时想恢复Raku例外? 的回答.)

.resume 不带参数

对.在引发异常的那条语句之后,它只是在该语句处恢复执行. .resume 不会更改失败语句的结果.

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

(我尝试了几种与 CATCH 相关的方法,然后得出结论,仅使用 try 是实现 catch 函数主体的方法,一开始是链接的.如果您尚未查看 catch 代码,建议您这样做.)

有关 CATCH 块的其他花絮

出于一些原因,他们有些烦躁.一种似乎是对其预期功能和适用性的故意限制.另一个是错误.考虑例如:

对错误处理有更深入的了解

在所有这些方面我是否缺少对错误处理的更深入的了解?

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

  • 吻#1 您已经处理了其他PL中没有例外的错误.有效.您已经在Raku中完成了.有用.仅在需要 想要使用例外时使用例外.对于大多数代码,您不会.

  • KISS#2 .忽略某些本机类型用例,几乎所有结果都可以表示为有效或无效,而不会导致真值,它提供了符合人体工程学的方法来区分非错误值和错误:

    • 条件: if while try //等>

    • 谓词: .so .defined .DEFINITE

    • 值/类型:失败,零长度复合数据结构,:D vs :U类型约束等

在遇到错误异常时,我认为值得考虑的几点:

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

  • 其他PL支持例外.Raku的超级大国之一就是能够与所有其他PL进行互操作.因此,如果出于其他目的而不是为了启用正确的互操作,它会支持异常.

  • Raku包含 Failure (故障)的概念,这是一个延迟的异常.这个想法是您可以同时兼顾两全其美.小心处理后, Failure 只是一个错误值.如果不小心处理,它会像常规异常一样爆炸.

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

  • 快速编码.原型,探索性代码,一次性等.

  • 鲁棒性控制.逐步缩小或扩大错误处理范围.

  • 各种选项.应该指出什么错误?什么时候?通过哪个代码?如果使用代码要发信号表示生成代码应该更严格怎么办?还是更放松?如果是另一种方式-产生 代码想要表明使用 的代码应该更加谨慎或可以放松,该怎么办?如果产生和使用代码的理念相互矛盾,该怎么办?如果无法更改生产代码(例如,它是一个库或用另一种语言编写)怎么办?

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

  • 这些场景之间的便捷重构.

所有这些因素以及更多因素构成了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 }

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

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

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

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
    }

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

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

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

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
    }

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

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?

解决方案

"Larger insight about error handling"

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

Yes. In the general case, use features like try and if, not CATCH.

Is there a different approach I haven't considered?

Here's a brand new one: catch. I invented the first version of it a few weeks ago, and now your question has prompted me to reimagine it. I'm pretty happy with how it's now settled; I'd appreciate readers' feedback about it.

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

I'll discuss some of my thoughts at the end of this answer.

But let's now go through your points in the order you wrote them.

KISS

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

Me neither.

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

That's more like it.

Logging errors with a catchall CATCH

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

Right. A judiciously located catchall. This is a use case for which I'd say CATCH is a good fit.

The doc

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

If the doc is misleading about:

  • The limits of the capabilities and applicability of CATCH / CONTROL blocks; and/or

  • The alternatives; and/or

  • What's idiomatic (which imo is not use of CATCH for code where try is more appropriate (and now my new catch function too?)).

then that would be unfortunate.

CATCH blocks in the Rakudo compiler source

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

At a guess those will be judiciously placed catchalls. Placing one just before the callstack runs out, to specify default exception handling (as either a warning plus .resume, or a die or similar), seems reasonable to me. Is that what they all are?

Why are phasers statements?

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 ( Well that's because you forgot a semi-colon at the end of the first statement )

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

Join the club. Others have expressed related sentiments in filed bugs, and SO Q's and A's. I used to think the current situation was wrong in the same way you express. I think I could now easily be persuaded by either side of the argument -- but jnthn's view would be decisive for me.


Quoting the doc:

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

That suggests that a phaser is not a statement, at least not in an ordinary sense and would, one might presume, be removed from ordinary control flow.

But returning to the doc:

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.

That suggests that they can have a value in an ordinary control flow sense.


Perhaps the rationale for not removing phasers from holding their place in ordinary control flow, and instead evaluating to Nil if they don't otherwise return a value, is something like:

  • Phasers like INIT do return values. The compiler could insist that one assigns their result to a variable and then explicitly returns that variable. But that would be very un Raku-ish.

  • Raku philosophy is that, in general, the dev tells the compiler what to do or not do, not the other way around. A phaser is a statement. If you put a statement at the end, then you want it to be the value returned by its enclosing block. (Even if it's Nil.)


Still, overall, I'm with you in the following sense:

  • It seems natural to think that ordinary control flow does not include phasers that do not return a value. Why should it?

  • It seems IWBNI the compiler at least warned if it saw a non-value-returning phaser used as the last statement of a block that contains other value-returning statements.

Why don't CATCH blocks return/inject a value?

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»

As discussed above, many phasers, including the exception handling ones, are statements that do not return values.

I think one could reasonably have expected that:

  • CATCH phasers would return a value. But they don't. I vaguely recall jnthn already explaining why here on SO; I'll leave hunting that down as an exercise for readers. Or, conversely:

  • The compiler would warn that a phaser that did not return a value was placed somewhere a returned value was probably intended.


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

Raku(do) isn't just doing an unstructured jump.

(Otherwise .resume wouldn't work.)

this seems to be carrying things a bit far

I agree, you are carrying things a bit too 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.

(from jnthn's answer to When would I want to resume a Raku exception?.)

.resume doesn't take an argument

Right. It just resumes execution at the statement after the one that led to an exception being thrown. .resume does not alter the result of the failed statement.

Even if a CATCH block tries to intervene, it won't be able to do so in a simple, self-contained fashion, by setting the value of a variable whose assignment has thrown an exception, and then .resumeing. cf Should this Raku CATCH block be able to change variables in the lexical scope?.

(I tried several CATCH related approaches before concluding that just using try was the way to go for the body of the catch function I linked at the start. If you haven't already looked at the catch code, I recommend you do.)

Further tidbits about CATCH blocks

They're a bit fraught for a couple reasons. One is what seems to be deliberate limits of their intended capability and applicability. Another is bugs. Consider, for example:

Larger insight about error handling

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

Perhaps. I think you already know most of it well, but:

  • KISS #1 You've handled errors without exceptions in other PLs. It worked. You've done it in Raku. It works. Use exceptions only when you need or want to use them. For most code, you won't.

  • KISS #2 Ignoring some native type use cases, almost all results can be expressed as valid or not valid, without leading to the semi-predicate problem, using simple combinations of the following Raku Truth value that provide ergonomic ways to discern between non-error values and errors:

    • Conditionals: if, while, try, //, et al

    • Predicates: .so, .defined, .DEFINITE, et al

    • Values/types: Nil, Failures, zero length composite data structures, :D vs :U type constraints, et al

Sticking with error exceptions, some points I think worth considering:

  • One of the use cases for Raku error exceptions is to cover the same ground as exceptions in, say, Haskell. These are scenarios in which handling them as values isn't the right solution (or, in Raku, might not be).

  • Other PLs support exceptions. One of Raku's superpowers is being able to interoperate with all other PLs. Ergo it supports exceptions if for no other reason than to enable correct interoperation.

  • Raku includes the notion of a Failure, a delayed exception. The idea is you can get the best of both worlds. Handled with due care, a Failure is just an error value. Handled carelessly, it blows up like a regular exception.

More generally, all of Raku's features are designed to work together to provide convenient but high quality error handling that supports all of the following coding scenarios:

  • Fast coding. Prototyping, exploratory code, one-offs, etc.

  • Control of robustness. Gradually narrowing or broadening error handling.

  • Diverse options. What errors should be signalled? When? By which code? What if consuming code wants to signal that producing code should be more strict? Or more relaxed? What if it's the other way around -- producing code wants to signal that consuming code should be more careful or can relax? What can be done if producing and consuming code have conflicting philosophies? What if producing code cannot be altered (eg it's a library, or written in another language)?

  • Interoperation between languages / codebases. The only way that can work well is if Raku provides both high levels of control and diverse options.

  • Convenient refactoring between these scenarios.

All of these factors, and more, underlie Raku's approach to error handling.

这篇关于实际上捕获异常而不创建GOTO的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆