【问题标题】:Returning values from exception handlers in Perl 6从 Perl 6 中的异常处理程序返回值
【发布时间】:2019-01-09 16:03:33
【问题描述】:

我一直在尝试编写一个执行以下逻辑的 Perl 6 表达式:评估子表达式并返回其值,但如果这样做会引发异常,则捕获异常并返回固定值。

例如,假设我想将两个数字相除,如果发生错误,则让表达式计算为 -1。在 Ruby 中我可能会写:

quotient = begin; a / b; rescue; -1; end

在 Emacs Lisp 中可能写成:

(setq quotient (condition-case nil (/ a b) (error -1))

我的第一次 Perl 6 尝试是这样的:

sub might-throw($a, $b) { die "Zero" if $b == 0; $a / $b }
my $quotient = do { might-throw($a, $b); CATCH { default { -1 } } };

但是这里$quotient 最终是未定义的,无论$b 是否为零。

似乎CATCH 返回的值被忽略了,或者至少在描述异常如何工作的doc page 上,所有CATCH 主体只做有副作用的事情,比如日志记录。

该页面提到try 作为替代。例如,我可能会写:

my $quotient = try { might-throw($a, $b) } // -1;

我发现它是一个相当平庸的解决方案。一方面,我正在评估的表达式可能确实具有未定义的值,我无法将其与引发异常的情况区分开来。另一方面,我可能想根据抛出的异常的类回退到不同的值,但try 只是将它们全部吞下。我可以将自己的CATCH 块放在try 中以区分异常,但随后我又回到了上面的第一种情况,其中CATCH 的值被忽略了。

Perl 6 的异常处理可以像我在上面所说的那样做吗?

编辑:

当前的答案提供了丰富的信息,但过于狭隘地关注除法运算符的语义。我稍微重写了这个问题,以使异常捕获的主要问题更加核心。

【问题讨论】:

  • 0 分母有理数用于无穷大Inf.Rat.nude.join('/').say1/0
  • 如果你只是在玩,你可能想使用div而不是/

标签: exception try-catch raku resume


【解决方案1】:

TL;DR此答案的最后一部分讨论了您尝试中发生的其他人未能解决的问题[1 2]。其余部分介绍trys,这是一个全面解决您的 Q 所展示的整体问题的解决方案。

trys总结

几个非常简单的例子:

say trys { die }, { -1 }                          # -1

say trys { die }, { when X::AdHoc { 42 } }        # 42

trys:

  • 不是拼写错误。[3]

  • 获取一个或多个Callables(函数、lambda 等)的列表。

  • 将环境异常作为其topic 传递给每个Callable

  • trys 每个Callable 依次直到:

    • 所有“失败”(抛出异常或以其他方式拒绝结果)。如果是这样,trys 返回一个 Failure,它包装了最后一个 Callable 抛出的异常(或者如果传递了可选的 :$list-throws,则所有这些异常);

    或者

    • 一个成功。如果是这样,trys 会返回该成功结果。

trys 代码

unit module X2;

our sub trys ( **@blocks,                 #= List of code blocks.
               :$reject = (),             #= Value(s) to be rejected.
               :$HANDLED = True,          #= Mark `Failure` as handled?
               :$list-throws is copy      #= List *all* throws in final `Failure` payload?
                         = False,
             ) is export {

  $! = CLIENT::<$!>;                      # Set argument of first block to caller's `$!`.
  if $! and $list-throws                  # Include caller's `$!` in list of throws.
    { $list-throws = [].push: $! }        # (Reuse `$list-throws` for store. Why not? :))

  my $result is default(Nil);             # At least temporarily preserve a `Nil` result.

  for @blocks -> &block {
    $result = try { block $! }            # Try block with `$!` from previous try as topic.
    if not $! and $result ~~ $reject.any  # Promote result to exception.
      { $! = X::AdHoc.new: payload => "Rejected $result.gist()" }
    if $! and $list-throws
      { $list-throws .push: $! }
    return $result unless $!;             # Return result if block didn't throw.
  }

  if $list-throws
    { $! = X::AdHoc.new: payload => $list-throws }

  given Failure.new: $! {                 # Convert exception(s) to `Failure`.
    .handled = True if $HANDLED;
    .return
  }
}

Code on glot.io(包括此答案中的所有trys 代码)。

trys详细介绍

use X2;

# `trys` tries a list of callables, short circuiting if one "works":
say trys {die}, {42}, {fail}                  # 42

# By default, "works" means no exception thrown and result is not a `Failure`:
say trys {die}, {fail}, {42}                  # 42

# An (optional) `:reject` argument lets you specify
# additional value(s) you want rejected if they match via infix `~~`:
say trys :reject(Nil,/o/), {Nil}, {'no'}, {2} # 2

# If no callable works, the last error is converted into a `Failure` and returned:
say trys :reject(Nil), {Nil}                  # (HANDLED) Rejected Nil
say trys {die}                                # (HANDLED) Died
say trys {(42/0).Str}                         # (HANDLED) Attempt to divide by zero
# To stop last error `Failure`s being handled, specify optional argument `:!HANDLED`:
say (trys {(42/0).Str}, :!HANDLED) .handled;  # False

# The first callable is passed the caller's current exception as its topic:
$! = X::AdHoc.new: payload => 'foo';
trys {.say}                                   # foo

# Subsequent callables are passed the exception from the prior callable as their topic:
trys {die 'bar'}, *.say;                      # bar
trys {fail 'bar'}, {die "$_ baz"}, *.say;     # bar baz

# Unless there's an exception in the guts of `trys`, the caller's `$!` is left alone:
say $!;                                       # foo

# To make final `Failure` payload be a *list*, specify optional argument `:list-throws`:
say trys {die 'bar'}, :list-throws;           # (HANDLED) foo bar
# (`list-throws` includes the caller's original `$!` if it was defined.)

trys“陷阱”

# Some "traps" are specific to the way `trys` works:

say trys { ... } // 42;                   # "(HANDLED) Stub code executed"
say trys { ... }, { 42 }                  # 42   <-- Do this instead.

#trys 22;                                 # Type check failed ... got Int (22)
#trys {}                                  # Type check failed ... got Hash ({})
say trys {;}                              # Nil   <-- Use blocks instead.

# Other "traps" are due to the way Raku works:

# Surprise `False` result if callable has `when`s but none match:
say do   {when rand { 42 }}               # False   <-- It's how Raku works.
say trys {when rand { 42 }}               # False   <-- So same with `trys`.
say trys {when rand { 42 }; Nil}          # Nil     <-- Succinct fix.
say trys {when rand { 42 }; default {}}   # Nil     <-- Verbose fix.

# Surprise `(Any)` result if callable's last/return value is explicitly `$!`:
$! = X::AdHoc.new: payload => 'foo';
say try {$!}                              # (Any)   <-- It's how Raku works.
say $!;                                   # (Any)   <-- Clears `$!` *before* return!
$! = X::AdHoc.new: payload => 'foo';
say trys {$_}                             # (Any)   <-- `trys` has same return behaviour.
say $!;                                   # foo     <-- (But doesn't clear caller's `$!`.)
$! = X::AdHoc.new: payload => 'foo';
say try {$!.self}                         # foo     <-- One way to fix with `try`.
say $!;                                   # (Any)   <-- (Still clears caller's `$!`.)
$! = X::AdHoc.new: payload => 'foo';
say trys {.self}                          # foo     <-- Suggested fix with `trys`.
say $!;                                   # foo     <-- (Doesn't clear caller's `$!`.)

trys 与内置插件 tryCATCH

trys 结合了tryCATCH 的选定部分:

  • 就像try 一样,它的可调用对象对他们的代码执行try但是:

    • 确实支持语句形式; trys 42 将不起作用。

    • 它将异常转换为Failures

    • 它允许用户指定他们想要提升为Failures 的值列表。

  • 就像CATCH 一样,每个可调用对象都被传递一个异常作为其主题。 但是:

    • 可调用对象不是 phaser 块;它们不会自动调用。

    • 它返回一个值(与CATCH 块不同)。

每个trys callable 可以扮演try 角色、CATCH 角色,或两者兼而有之。

讨论你的尝试

我的第一次 Raku 尝试是这样的:

sub might-throw($a, $b) { die "Zero" if $b == 0; $a / $b }
my $quotient = do { might-throw($a, $b); CATCH { default { -1 } } };

CATCH 块始终返回 Nil。它是闭包主体中的最后一条语句,因此始终返回 Nil。 (这是一把似乎应该修理的足枪。请参阅Actually CATCHing exceptions without creating GOTO 中的进一步讨论)

我可能会写例如:

my $quotient = try { might-throw($a, $b) } // -1;

我正在评估的表达式可能确实有一个未定义的值,我无法将这与引发异常的情况区分开来。

你可以改为:

my $quotient is default(-1) = try { might-throw($a, $b) }

这里发生了什么:

  • is default trait 声明变量的默认值是什么,如果它没有被初始化并且如果有尝试分配Nil 时使用它。 (虽然Nil 在技术上是一个未定义的值,但其目的是表示“没有值或良性故障”。)

  • try 定义为在评估过程中抛出异常时返回 Nil


如果想要区分由于抛出异常而返回的Nil 和由于Nil 的普通返回,这可能仍然不能令人满意。或者,也许更重要的是:

我可能想根据抛出的异常的类别回退到不同的值,但 try 只是将它们全部吞下。

这需要解决方案,但不是CATCH

我可以将自己的 CATCH 块放在 try 中以区分异常,但我又回到上面的第一个案例

相反,现在我创建了 trys 函数。

脚注

[1] 正如您所指出的:“当前的答案......过于狭隘地关注除法运算符的语义。”。所以我在脚注下对这方面的总结进行了注释:为了支持advanced math,Raku 不会自动将理性除以零(例如1/0)视为异常/错误。 Raku 随之而来的双重延迟异常处理是一个红鲱鱼。

[2]CATCH 也是红鲱鱼。即使与.resume 一起使用,它也不会返回值或注入值,因此它是完成需要完成的工作的错误工具。

[3] 有些人可能认为trys 最好拼写为tries。但我故意拼写为trys。为什么?因为:

  • 在英语中,就tries 这个词与try 的关系而言,它非常 密切相关。单词选择trys 的奇怪之处在于提醒人们它不仅仅是复数try。也就是说,粗略的含义与try 密切相关有点,所以拼写为trys 在imo 中仍然有意义。

  • 我喜欢奇思妙想。显然,在阿尔巴尼亚语中,trys 的意思是“按压、压缩、挤压”。与try 一样,trys 函数“按下”代码(“按下”在“压力”意义上),并“压缩”它(与不使用 trys 的冗长相比),以及“将所有与异常相关的错误机制——Exceptions、Failures、Nils、tryCATCH.resume——合二为一。

  • 在立陶宛语中,trys 的意思是“三”。 trys:

    1. 拒绝三种结果:Exceptions; Failures;和用户指定的:reject 值。

    2. 以三种方式保持滚动:将调用者的$! 传递给第一个可调用对象;调用带有最后一个异常的后续可调用对象作为他们的主题;将最后一个块中抛出的异常转换为Failure

    3. 解决编程中最难的事情之一——命名:trystry 明显相似但不同;我在此预测很少有开发者会在他们的代码中使用阿尔巴尼亚语或立陶宛语trys;选择trys 而不是tries 可以减少与现有代码发生冲突的可能性。 :)

【讨论】:

  • 等等,你有没有把它发布到某个地方?
  • @jjmerelo 就在这个 SO 中。我认为这可能是我写的第一件事,也许应该以其他形式广泛发布,但我目前认为它不值得作为生态系统包中的唯一内容这样做。
  • @jjmerelo 我没有详细说明为什么不这样做。但评论# No idea if this is reliable. 是其中的一部分。它不再起作用了。目前有一个更简单的咒语起作用。但同样,我不知道它是否可靠。有一天我可能会发布关于那个的帖子,但我可能会等到 jnthn 完成他的调度和 rakuast 工作并有时间休息一段时间(一年?)之后。一切都很好......
【解决方案2】:

我认为创建一个中缀运算符是有道理的。

sub infix:<rescue> ( $l, $r ) {
  # return right side if there is an exception
  CATCH { default { return $r }}

  return $r if $l ~~ Nil; # includes Failure objects
  return $r if $l == NaN;

  # try to get it to throw an exception
  sink $l;
  sink ~$l; # 0/0

  # if none of those tests fail, return the left side
  return $l;
}

使用这个新运算符的快速副本:

my ($a,$b) = 0,0;

my $quotient = do { try { $a / $b } rescue -1 };

当然可以简化为:

my $quotient = $a / $b rescue -1;

这不包括像 Ruby 那样进行多个类型化的rescue 测试的能力。 (如果是这样,它就不适合 Raku,而且 CATCH 已经处理了。)

它实际上也不会捕获异常,因此如果它可能导致异常,您必须用 try {…} 包裹左侧。
当然,一旦我们获得了宏,那可能就是另一回事了。

(解决问题的最佳方法是创建一种易于解决问题的语言。)


如果实现了leave(value),您也许可以在CATCH 块中使用它。 据我所知,leave(value) 应该类似于return(value),这也是我使用sub 的部分原因。

do { might-throw($a, $b); CATCH { default { leave(-1) } } };

虽然它也可能不起作用,因为CATCH {…}default {…} 创建了两个块。 无论如何,这都是假设的,因为它没有实现。


如果要将rescue 实际添加到 Raku,则可能需要一种新方法。

use MONKEY-TYPING;

augment class Any {
  proto method NEEDS-RESCUE ( --> Bool ){*}
  multi method NEEDS-RESCUE ( --> False ){} # includes undefined
}
augment class Nil { # includes Failure objects
  multi method NEEDS-RESCUE ( --> True ){}
}
# would be in the Rational role instead
augment class Rat {
  multi method NEEDS-RESCUE (Rat:D: ){
    $!denominator == 0
  }
}
augment class FatRat {
  multi method NEEDS-RESCUE (FatRat:D: ){
    $!denominator == 0
  }
}
augment class Num {
  multi method NEEDS-RESCUE (Num:D: ){
    self.isNAN
  }
}

sub infix:<rescue> ( $l, $r ){
  $l.NEEDS-RESCUE ?? $l !! $r
}

say 0/0 rescue -1; # -1
say 0/1 rescue -1; # 0
say NaN rescue -1; # -1

【讨论】:

    【解决方案3】:

    其他答案有助于关注“为什么”,所以这里只关注“如何”。

    你问如何重写

    sub might-throw($a, $b) { die "Zero" if $b == 0; $a / $b }
    my $quotient = do { might-throw($a, $b); CATCH { default { -1 } } };
    

    以便在$b == 0 时将$quotient 设置为提供的默认值。这里有两种方法:

    选项 1

    sub might-throw($a, $b) { die "Zero" if $b == 0; $a / $b }
    my $quotient = sub { might-throw($a, $b); CATCH { default { return -1 } } }();
    

    选项 2

    sub might-throw1($a, $b) { die "Zero" if $b == 0; $a / $b }
    my $quotient = do with try might-throw1($a, $b) { $_ } elsif $! { -1 };
    

    一些解释性说明:CATCH 块(以及更普遍的移相器)不会隐式返回它们的最后一个表达式。您可以使用return 函数显式返回,但只能从Routine(即方法或子)内返回。选项 1 将您提供的块包装在一个立即调用的匿名子中,这为您提供了一个返回范围。

    选项 2(这将是我的首选)切换到 try,但利用 try$! 的值设置为最后一个这一事实解决了您使用 try {…} // $default 方法注意到的两个问题它捕获的异常。

    您提到了try {…} // $default 的两个问题。首先,您要区分异常和真正未定义的值。选项 2 通过测试 try 是否捕获异常来实现这一点——如果 &amp;might-throw 返回未定义的值而不抛出异常,$quotient 将是未定义的。

    其次,您说您“可能希望根据抛出的异常的类回退到不同的值”。选项 2 可以通过匹配 elsif 块内的 $! 来扩展。

    【讨论】:

      【解决方案4】:

      所以我们有了一个函数。有时它返回 Any (undef) 否则返回 $a / $b 除非 $b0 在这种情况下它会引发异常。

      sub might-throw($a, $b) { 
          return Any if (True, False, False, False, False).pick();
          die "Zero" if $b == 0;  
          $a / $b;
      }
      

      我们希望 quotient 是函数调用的值,除非它抛出异常,在这种情况下我们希望 -1。

      让我们随机制作 20 对并尝试一下:

      for 1..20 {
          my $a = (0..2).pick;
          my $b = (0..2).pick;
          my $quotient = -1;
          try {
              let $quotient = might-throw($a, $b);
              $quotient ~~ Any|Numeric;
          }
          say "{$a}/{$b} is {$quotient} maybe..";
      }
      

      所以我们开始预定义错误状态的商。然后在一个 try 块中,我们使用let 调用函数来设置它。如果函数错误或块返回 undef,let 将被回滚...因此我们测试$quotientAnyNumeric

      【讨论】:

        【解决方案5】:

        我得到了以下工作:

        use v6;
        
        my $a = 1;
        my $b = 0;
        my $quotient = $a / $b;
        try {
            #$quotient;   # <-- Strangely, this does not work
            "$quotient"; 
            CATCH {
                when X::Numeric::DivideByZero {
                    $quotient = -1;
                }
                default { fail }
            }
        }
        say "Value of quotient: ", $quotient;
        

        输出

        Value of quotient: -1
        

        但是,如果我不在try 子句中对$quotient 进行字符串化,则会给出

        Useless use of $quotient in sink context (line 9)
        Attempt to divide 1 by zero using div
          in block <unit> at ./p.p6 line 18
        

        我不确定这是否是一个错误..

        编辑

        解决CATCH 块的返回值问题。您可以通过调用resume 方法来解决它不向外部范围返回值的问题:

        my $a = 1;
        my $b = 0;
        my $quotient = do {
            my $result = might-throw($a, $b);
            CATCH {
                default {
                    say "Caught exception: ", .^name;
                    .resume;
                }
            }
            $result;  #<-- NOTE: If I comment out this line, it does not work
                      #          A bug?
        };
        
        sub might-throw($a, $b) {
            if $b == 0 {
                die "Zero";
                -1;  # <-- the resume method call from CATCH will continue here
            }
            else {
                $a / $b
            }
        }
        

        【讨论】:

        • $result; #&lt;-- NOTE: If I comment out this line, it does not work ... A bug? 在我看来是这样。我只是searched rt for resumeGH issues for resume 并没有看到匹配。我还在 rt 和 GH 中搜索了 catch 和 return。因此,我查看了很多错误,但没有一个与此匹配。如果你有时间,请报告。试着打高尔夫球。它需要 CATCH/resume 吗?
        • #$quotient; # &lt;-- Strangely, this does not work。 Aiui,这不是错误。 $quotient 是一个 Scalar 容器。它在接收器上下文中,所以它只是显示一个关于无用使用的警告,但不需要查看 $quotient 内部,将其值拉出,转到“omg”,然后抛出异常。所以这些都不涉及投掷:my $foo = Failure.new; $foomy $foo = Failure.new; say try { $foo; 42 } 而这两个都涉及 my $foo = Failure.new; "$foo"say try { Failure.new; 42 }
        • @raiph 感谢 cmets!我在 GitHub 上添加了一个错误报告:Missing return value from do when calling resume and CATCH is the last statement in a block
        • 谢谢。回想起来,我必须知道这个错误,因为我知道不要将 CATCH 块放在最后。但是当我在写评论之前测试它时,我没有复制一个问题。或者,也许我把自己弄糊涂了。无论如何,感谢您将其记录在案,假设我在查看所有这些错误时没有错过它。 :)
        【解决方案6】:

        这似乎是设计和/或实现缺陷:

        Rakudo 愉快地将Int 除以0,返回Rat。你可以.Num它(产生Inf)和.perl它,但如果你尝试.Str.gist它,它会爆炸。

        相反,除以Num 0e0 将立即失败。

        为了保持一致性,整数除以零也可能会失败。另一种方法是返回一个在字符串化时不会爆炸的常规值,但我反对它......

        【讨论】:

        • 一个一致的argument总是一个合理的argumentTimToady Bicarbonate。我对此有太多话要说,无法在此处发表评论。同样,我对其他答案和 OP 也有其他回应。所以我正在写一个答案。但这可能需要我几天时间。同时,我想注册一个 tentative -1 以将其视为设计/实现缺陷,+1 将其视为关于 Rational 和浮点语义之间根本差异的可教时刻; P6 异常处理的优点;和其他好东西。
        • @raiph:如果它应该产生一个常规值,那么在调用.gist 时它不应该爆炸,就像你不期望.gistNum 上爆炸一样那是Inf 或(非信令)NaN;这就是我提到的实现缺陷
        • OP 的问题非常简单。我为尝试一个简单的答案而鼓掌,从某种意义上说,希望我们中的一个人已经成功了。但我看到 OP 的问题涉及从例外到语言设计的许多主题(尤其是 language version detection(请注意关于 IEEE / 的最终评论))。和数字。我的部分观点是,除以零的任何东西都不是常规值。但我听到了。我只是在您的答案的第一句话中提请注意限定词“似乎”的双方含义。我认为这不仅仅是表面上看到的。
        • @p6steve。因此say 1/0 死了。这个话题是一个红鲱鱼。我花了一些时间来写它,但我现在已经发布了一个专注于 OP 观点的答案。回到1/0,它是一个数字字面意思1超过zero。这有一个非常有用的地方in mathematics 和计算。浮点数的 IEEE 标准要求 n/0-n/0 不是死亡,而是 +∞ and −∞。 P6 还没有这样做,但这是标准。 1/0 的功能远不止眼前所见。
        • (1/0).Num === Inf;Inf.Rat === 1/0;(-1/0).Num === -Inf;-Inf.Rat === -1/0;(0/0).Num === NaN;NaN.Rat === 0/0;
        【解决方案7】:

        您的 catch 块不起作用的原因是除以零本身并不是错误。 Perl6 很乐意让您除以零并将该值存储为 Rat。当您想以有用的方式显示所述 Rat 时,就会出现问题(IE say it)。那时你会得到一个返回的失败,如果不处理,就会变成异常。

        所以你有几个选择。你可以在$q之前检查$b

        $q = $b == 0 ?? -1 !! $a / $b; 
        

        或者,如果您想在say 时保留实际值(请注意,您可以同时检查 Rat 的分子和分母而不会导致除以零错误),您可以使用 .perl 或 @987654327 @ 版本。

        当你有一个0 分母时,两者都给你Rat 的十进制表示,.perl&lt;1/0&gt;.NumInf

        【讨论】:

          猜你喜欢
          • 2016-05-24
          • 1970-01-01
          • 2014-03-17
          • 2021-08-08
          • 1970-01-01
          • 1970-01-01
          • 2022-07-29
          • 2018-05-20
          • 2019-04-04
          相关资源
          最近更新 更多