成群的异常

Swarms of Exceptions

在拉里看来,懒惰是一种美德。让我们看看 Rakudo 是否同意。

sub virtue { }; lazy virtue;

# OUTPUT: Nil

几篇文章之前,我曾对 runshellqx{} 缺乏 Exceptions 感到惋惜。后来我改变了主意。这两个构造都不知道它们在什么操作系统上运行,也不知道 shell 命令是什么。程序员却知道(傲慢)。

这让我很不耐烦。当 Proc.exitcode 为非零时,编译器拒绝为我创建一个异常。在 Raku 中,这是一个可以解决的问题。

my $Exception = Metamodel::ClassHOW.new_type(:name($cmd-name));
$Exception.HOW.add_parent($Exception, Exception);
$Exception.HOW.add_attribute($Exception, Attribute.new(:name<$.exitcode>, :type(Int), :package($Exception), :has_accessor));
$Exception.HOW.add_method($Exception, 'message', my method { 'Shell command ⟨' ~ self.^name ~ '⟩ finished with non-zero exitcode of ' ~ self.exitcode ~ '.' } );

$Exception.HOW.compose($Exception);

我们使用 MOP 在运行时创建一个类型,并在其中填充所有 Exceptions 所需要的属性和消息方法。为了能够在程序的其余部分使用这个新类型,我们需要在程序的其余部分被编译之前让这个运行时发生。要做到这一点,我们可以使用一个 BEGIN phaser。然后我们可以将新类型塞进一个合适的包中。

BEGIN {
    package X::Shell {}
    X::Shell::<ls> := $Exception;
}

由于我们在 BEGIN 时可以访问 OUR,所以我们也可以在脚本的作用域中添加子程序,可以在程序的其他部分使用,或者将我们的shell命令添加到一个专门的包中,以避免冲突。

Shell::«$cmd-name» := sub (|c) {
    my $proc = run $cmd-name, |c, :out, :err;
    my $e = X::Shell::«$cmd-name»;
    $e.new(:exitcode($proc.exitcode), :stderr($proc.err.slurp)).throw if $proc.exitcode != 0;

    $proc.out.slurp;
}

因为我们把异常放到了 X::Shell 中,所以我们可以处理所有shell命令的异常,也可以只处理某个特定的异常。

Shell::ls('/home/not there', '-l');

CATCH {
    when X::Shell::ls { warn .stderr, .backtrace }
    when X::Shell { warn .^name, ': ', .message; .resume }
}
# OUTPUT: ls: cannot access '/home/not there': No such file or directory
#
#      in block  at exceptional-run.raku line 48

通过捕获 STDERR 并将其塞入异常中,我可以选择将shell命令发出的错误信息暴露给我的程序用户,如果我愿意的话,还可以显示栈跟踪。后者对调试有很大的帮助。由于我非常擅长写 bug,这将会很方便

整个示例脚本可以在这里找到。

动态语言会有一个明显的速度惩罚。当我们执行它们的时候。当我们写它们的时候,我们可以进步得更快。

by gfldex

comments powered by Disqus