Wrapping Exceptions
As stated before I would like to reduce simple error handling in Shell::Piping
. The following is just way to long for putting a single line into a terminal.
CATCH {
when X::Shell::CommandNotFound {
when .cmd ~~ ‚commonmarker‘ {
put ‚Please install commonmarker with `gem install commonmarker`.‘;
exit 2;
}
default {
.rethrow;
}
}
}
We have a conditional in line 3 and a print statement in line 4. The rest is boilderplate. The shortest syntax I came up with looks like the following.
X::Shell::CommandNotFound.refine({.cmd eq ‚commonmarker‘}, {„Please install ⟨{.cmd}⟩ in ⟨{.path}⟩.“});
X::Shell::CommandNotFound.refine({.cmd eq ‚raku‘}, {„Please run `apt install rakudo`.“});
I want to modify the type object behind CommandNotFound
because the module will create instances of them and throw them before I can intercept them. Of cause I could catch them. But the whole excercise is done to get rid of the CATCH
block. Adding a method to any exception both inside a module, and thus at compile time, and at runtime would be nice. We can do so with a trait.
multi sub trait_mod:<is>(Exception:U $ex, :$refineable) {
$ex.HOW.add_method($ex, ‚refine‘, my method (&cond, &message) {
$ex.WHO::<@refinements>.append: &cond, &message;
state $wrapped;
$ex.HOW.find_method($ex, ‚message‘).wrap(my method {
my @refinements := self.WHO::<@refinements>;
for @refinements -> &cond, &message {
if cond(self) {
return message(self);
}
}
nextsame
}) unless $wrapped;
$wrapped = True;
});
}
A trait is a fancy sub that takes type objects or other stat-ish (Raku is a dynamic language without static things) objects and does stuff with it. That can be wrapping – a MOP operation in disguise – or actual MOP-calls. This redefining trait adds a class attribute named @refinements
via autovification on the package-part of the type. This is hidden behind the pseudo method .WHO
. Then it adds a method that takes a pair of Callables
and stores them in @refinements
. The type object’s original .message
method is wrapped unless already done so. This is controlled by a state variable.
The wrapped .message
will call the first callable and expects a Bool
in return. If that one is True
is will call the 2nd callable. Both are called with an exception instance.
The trait does not .^compose
the type object it is called on. This is (for now) needed because of the following case.
class X::Shell::CommandNotFound is Exception is refineable { ... }
The trait is actually called on Exception
. That works because the added method .refine
is not shadowed by anything. We can therefore use this method to wait for it to be called to finish what we need to do after compile time. The wrapper has a fall through via nextsame
if no condition has matched.
I don’t think I got all corner cases covered yet. That will need further testing. The user facing syntax can stay the way it is because traits are deboilerplaters. The trait itself is likely going to get more complex. But that is fine. The burden is supposed to be on the side of the implementer.
-
August 3, 2020 at 15:172020.31 TwentyTwenty – Rakudo Weekly News