Guarding Dynamics

楽土

动态变量是一种很好的方式,它可以获得全局变量的好处,但又不存在缺点。它们在调用树上传递信息,而不强制调用者进行声明。然而,动态变量与异常共享一个负担。被调用者知道如何做调用者可能想不到的事情。

use Module::By::Author::A;
use Module::By::Author::B;

my $*dynvar = 42;

sub-from-A(); # expects $*dynvar to be Int
sub-from-B(); # expects $*dynvar to be IO(Str)
sub-from-C(); # expects $*dynvar to be IO::Handle

在这个例子中,sub-from-B() 将默默地失败,直到它试图打开名为 “42” 的文件。而 sub-from-C() 会试图把 42 强转成为一个文件句柄并抛出。因此,在预期由独立模块设置的 dynvars 中存在一个危险。更糟糕的是,这种行为可能会在任何 zef --install 之后突然出现。Raku 是一种动态语言,它会尽力自动转换值,并在运行时失败。通过提前失败来支持编译器是个好建议。

我想在一些模块中使用 dynvars 来提供半全局标志,以去除模板。下面的内容似乎可以提供保护,防止 dynvar 无意间的溢出。

class Switch is Mu {
    has $.name;
    method gist { $.name }
    method Str { die('invalid coersion') }
}

constant on = Switch.new: :name<on>;
constant off = Switch.new: :name<off>;

sub s() {
    # put $*d; # this will die
    say $*d;
    dd $*d;
}
my $*d = off;
s();
# OUTPUT: off
#         Switch $*d = Switch.new(name => "off")

我从 Mu 中派生出 Cool 中的所有 coercers,并重载 Str,把那个也从循环中取出来。除了调试之外,使用 say 是一个 bug,所以我可能会用 .gist 适当地支持它。由于我用类创建了一个类型,所以我可以用 whereception 去保护我的 sub 和方法。

sub dyn-var-typecheck(Mu:U \T, $name) {
    sub {
        DYNAMIC::($name) ~~ T || die(„$name is set to an unexpected value“)
    }
}

constant &dyn-s-typecheck = dyn-var-typecheck(Switch, ‚$*d‘);

sub s($? where dyn-s-typecheck) { }
# OUTPUT: $*d is set to an unexpected value
#         in sub  at /home/dex/projects/blog/shielded-dynvars.raku line 6
#         in sub s at /home/dex/projects/blog/shielded-dynvars.raku line 22
#         in block <unit> at /home/dex/projects/blog/shielded-dynvars.raku line 29

where 子句中使用 DYNAMIC::($name) 会减慢对 MMD 的任何调用。所以把检查拉到子例程中可能是合理的。

通过这个措施,我觉得在 Shell::Piping 中添加 $*always-capture-stderr 更好,可以在我启动的几乎所有管道上摆脱 :stderr(Capture)。而在各处添加 $*debug 时,我感觉好多了。

Raku 在设计时并没有考虑到对 dynvars 的类型检查。它被设计成 Perl。这让我们可以灵活地在问题发生时进行修复。

原文链接: https://gfldex.wordpress.com/2020/08/14/guarding-dynamics/

comments powered by Disqus