Guarding dynamics
Dynamic variables are a nice way to get the benefits of global variables without taking the drawbacks. They pass information up the call tree without forcing a declaration upon the caller. However, dynvars share a burden with exceptions. The callee knows how to do what the caller might not expect.
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
In this example sub-from-B()
will silently fail until it tries to open the file named “42”. While sub-from-C()
will try to coerce 42 to become a file handle and throw. So there lies a danger in dynvars expected to be set by independent modules. Worse, the behaviour might suddenly pop up after any zef --install
. Raku is a dynamic language that will try its best to convert values automatically and fail at runtime. It is good advice to support the compiler by failing early.
I want to use dynvars in a few modules to provide semi-global flags to remove boilerplate. The following seems to provide protection against unintentional dynvar-spill over.
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")
I derive from Mu
to get rid of all the coercers from Cool
and overload Str
to take that one out of the loop as well. Using say
for anything but debugging is a bug anyway, so I might support it properly with .gist
. Since I create a type with the class I can then go and protect my subs and methods with a whereception.
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
Using the DYNAMIC::($name)
in a where clause will slow down any call on MMD. So pulling the check into the sub might be reasonable.
With this measure I feel better to add $*always-capture-stderr
to Shell::Piping
to get rid of :stderr(Capture)
on pretty much any pipe I start. And I feel a lot better when adding $*debug
in everywhere.
Raku was not designed with type checks on dynvars in mind. It was designed to be a Perl. That gives us the flexibility to fix problems as they occur.
-
August 17, 2020 at 16:012020.33 Function Types – Rakudo Weekly News