Search Results
Plucking strings
This is a correction and follow-up of my previous post. The ever helpful vrurg provided a simplification to my transformative role. I added some evil to it, mostly for nicer looking introspection.
role Trans[::From, ::To, &c] {
has To $.value;
method COERCE(From:D $old) {
self.new(:value($old.&c))
}
unless ::?ROLE.^declares_method(my $mname = To.^name) {
::?ROLE.^add_method: $mname, ('my method ' ~ $mname ~ '(--> To) { $.value }').EVAL;
}
}
By checking if the role contains a method already, I don’t need to fool around with the method table any more. I use .EVAL
to compose the method name properly. Rakudo doesn’t care, but a human looking at the method name does not need to be confused. Please note the absence of use MONKEY;
. The method form of EVAL
doesn’t require it. It is safe to assume code not to be safe.
Task 2 can be written as a naive algorithm. Keep the stickers that contain characters that are also in the target word. Check if all letters in the target word are in the kept stickers. If so, show them or complain.
Again, I need a way to turn words into Set
s. I shall do so by invoking the coercion-protocol with a new
-method.
class Plucked {
has $.str;
has @.chars;
method new(Str:D $s) { callwith :str($s), :chars($s.comb) }
method gist { $!str ~ ' ' ~ @!chars.pairs».&{ .key ~ ' ' ~ .value } }
method Str { $.str }
method Set { @.chars.Set }
method AT-POS($pos) { @.chars.AT-POS($pos) }
method AT-KEY($key) { @.chars.grep( * eq $key ) }
}
constant PC = Plucked(Str:D);
for [
('perl','raku','python'), 'peon';
('love','hate','angry'), 'goat';
('come','nation','delta'), 'accommodation';
('come','country','delta'), 'accommodation'
] -> [ @stickers, PC $word ] {
my @keep;
for @stickers -> PC $sticker {
@keep.push($sticker) if $word ∩ $sticker;
}
if ([∪] @keep) ⊇ $word {
put „"$word" can be made with "$@keep"“;
} else {
my $missing = $word ∖ ([∪] @keep);
put „"$word" can not be made, "$missing" is missing“;
}
}
The helper class Plucked
knows how to dissect Str
s and turn them into Set
s and Str
s. Since laziness is a virtue, I reduce the amount of typing by storing the coercion-type in a constant. Then I write the naive algorithm down.
I didn’t know that I can store coercion-types the same way as other type-objects in a constant. It just DWIMed and that pleases me greatly.
Coercing coercion
Task 1 of PWC #216 is a Set
-operation.
for [('abc', 'abcd', 'bcd'), 'AB1 2CD'; ('job', 'james', 'bjorg'), '007 JB'; ('crack', 'road', 'rac'), 'C7 RA2'] -> [@words, $reg] {
say @words.grep($reg.lc.comb.grep(/<alpha>/) ⊆ *.comb);
}
The transformation of $reg
is a bit unwieldy. I could pull it out and transform it before I use it but then I would have to use is rw
. That ain’t neat. What if I could write a type that does the transformation for me? The answer is mildly insane.
role Trans[::From, ::To, &c] {
has To $.value;
method COERCE(From:D $old) {
my \instance = self.new(:value($old.&c));
my \table = instance.^method_table;
table{To.^name} = my method ::To(--> To) { $!value }
instance.^compose;
instance
}
}
constant ToSet = Trans[Str, Set, { .lc.comb.grep(/<alpha>/).Set }];
for [('abc', 'abcd', 'bcd'), 'AB1 2CD'; ('job', 'james', 'bjorg'), '007 JB'; ('crack', 'road', 'rac'), 'C7 RA2'] -> [@words, ToSet() $reg] {
say @words.grep($reg ⊆ *.comb);
}
I create a parametric role that is told how to transform a Str
to a Set
with a Block
and use that as a coercion-type. Things get tricky inside method COERCE
because I have to return the role or the coercion-protocol will throw X::Coerce::Impossible
. As a result I need to add a method called Set
to the parametrised role. Raku doesn’t have the syntax to specify an indirection for method-names (for definitions, calling them can be done with ."$foo"
). Hence the use of the MOP. Also, .^add_method
doesn’t take a :local
-adverb and thus refuses to overload methods provided by Mu
and Any
. Overwriting the name in the method table is a gnarly hack but works fine — as hacks do.
And so I got myself a way to run code at bind-time in signatures that changes arguments to what I need. I’m not sure what this could be useful for but will keep it in my toolbox nonetheless.
EVAL sadly doesn’t work, because quotes can’t form a closure over a code-object. I believe untangling this would be a nice test for RakuAST-macros and would improve readability for this example quite a bit. In fact, I wouldn’t need a parametric role but could craft a simple class.
Coercive bits
Altreus was looking for a way to convert a list of bitmask names, provided as a string by the user, to a bitmask. He wished for BUILDARGS, as provided by Perl’s Moose but was given good advise how to solve the problem without making object instantiation even more complex. Nobody can stop me from doing just that.
With Moose, BUILDARGS allows to modify values before they are bound to attributes of a class. We can do the same by using the COERCE-protocol, not just for Routine
-arguments.
role BitMask[@names] {
has int $.mask;
sub names-to-bits(@a) {
my int $mask;
for @a -> $s {
$mask = $mask +^ (1 +< @names.first($s, :k) // fail('bad‼'))
}
$mask
}
sub bits-to-names($mask) {
($mask.base(2).comb.reverse Z @names).map: -> [Int() $is-it, $name] { $is-it ?? $name !! Empty }
}
multi method COERCE(Str $s) { self.COERCE: $s.split(' ').list }
multi method COERCE(List $l) { self.new: :mask($l.&names-to-bits) }
method raku { "BitMask[<@names[]>](<{$!mask.&bits-to-names}>)" }
method bits(--> Str) { $.mask.fmt('%b') }
}
class C {
has BitMask[<ENODOC ENOSPEC LTA SEGV>]() $.attr is required;
}
my $c = C.new: :attr<ENODOC ENOSPEC SEGV>;
say $c;
say $c.attr.bits;
# OUTOUT: C.new(attr => BitMask[<ENODOC ENOSPEC LTA SEGV>](<ENOSPEC LTA SEGV>))
# 1011
Here I build a role that carries the names of bits in a bit-field and will create the bit-field when give a Str
or List
containing those names. Since I use a parametrised role, my bitfield is type-safe and the type is tied to the actual names of the bits. As a consequence a user of the module that exports C
can extend the accepted types by using that specific role-candidate.
enum IssueTypes ( ENODOC => 0b0001, ENOSPEC => 0b0010, LTA => 0b0100, SEGV => 0b1001 );
class IssueTypesBits does BitMask[<ENODOC ENOSPEC LTA SEGV>] {
method new(*@a where .all ~~ IssueTypes) {
self.bless: mask => [+|] @a
}
}
sub bitmask(*@a where .all ~~ IssueTypes) {
IssueTypesBits.new: |@a
}
my $c3 = C.new: attr => BEGIN bitmask(ENODOC, LTA, SEGV);
say $c3;
say $c3.attr.bits;
# OUTPUT: C.new(attr => BitMask[<ENODOC ENOSPEC LTA SEGV>](<ENODOC LTA SEGV>))
1101
Here I define an enum
and provide the same names in the same order as in does BitMask
. With the shim bitmask
I create the type-safety bridge between IssueTypes
and BitMask[<ENODOC ENOSPEC LTA SEGV>]
.
It is very pleasing how well the new COERCE
-protocol ties into the rest of the language, because we can use a coercing-type at any place in the source code which takes a normal type as well. What is not pleasing are the LTA-messages X::Coerce::Impossible
is producing.
my $c4 = C.new: attr => 42;
# OUTPUT: Impossible coercion from 'Int' into 'BitMask[List]': method new returned a type object NQPMu
in block <unit> at parameterised-attribute.raku line 60
This is surpring because we can get hold of the candidates for COERCE
at runtime.
CATCH {
when X::Coerce::Impossible && .target-type.HOW === Metamodel::CurriedRoleHOW {
put "Sadly, COERCE has failed for {.from-type.^name}. Available candidates are: ", .target-type.new.^lookup('COERCE').candidates».signature».params[*;1]».type.map(*.^name)
}
}
# OUTPUT: Sadly, COERCE has failed for Int. Available candidates are: Str List
What we can’t see are the candidates of IssueTypesBits
, because that is hidden behind the constructor new
. I tried to use the MOP to add another multi candidate but failed. When parametrising the underlying type-object gets cloned. Any change to the multi-chain wont propagate to any specialised types.
The COERCE-protocol is quite useful indeed. Maybe it’s time to document it.
The truth is a hard problem
In a recent article, stevied promised a detailed walk through of code. I asked him if he would be interested in a critique of his writings. He foolishly agreed.
He claims that Array
s are eager and fill all memory if given the chance. Let’s check this claim.
my @a = 1, 1, * + * … ∞;
say @a.WHAT;
# OUTPUT: (Array)
Array.STORE
goes out of it’s way to maintain iterables (not all things that are iterable do Iterable
) and only reify container slots that we actively ask for. It will cache lazy lists by default, assuming that when we use .AT-POS
(usually indirectly with a positional subscript) we want to use the values again. Dealing with lazy lists is a hard problem and many get it wrong (as my next blog post will discuss in more detail).
In the next paragraph I found another inaccurate statement. Int:D
does not check for definedness.
multi sub bar(Any:D $) { say 'defined'; }
multi sub bar(Any:U $) { say 'undefined'; }
bar(Int); # undefined
class C { method defined { False } }
my $c = C.new; # undefined
bar(C); # undefined
bar($c); # defined
say defined $c; # False
say $c.DEFINITE; # True
say $c // 'undefined'; # undefined
In Raku objects are either type objects or definite objects. The latter are the result of nqp::create
. Raku and most user code expects objects to provide a basic set of methods that can be found in Mu
or Any
. The default type-check is against Mu
. We can check if an object is definite with the macro .DEFINITE
(you can overload a method of the same name, but Rakudo will ignore it) or, if we also want to know if we got an ordinary object, with $foo ~~ Mu:D
. There is a clear distinction of definedness and being definite. Raku needs to make that differentiation to support “unusual” values and types like Nil
and Failure
. Please note, that type-objects are not definite but can be defined. Types are a hard problem.
In the same paragraph stevied writes: “defined integer object”, even though he makes a type-check against Int
.
multi sub foo(Int:D $i) { }
multi sub foo($i where { $i - $i.truncate == 0 }) { say 'lolwut‽' }
multi sub foo($) { say 'not integer'; }
foo(1.0);
foo(1/1);
foo(¼);
# OUTPUT: lolwut‽
lolwut‽
not integer
Int:D
will fail for any value that doesn’t got (Int)
in its .^mro
or is a subset
of Int
or any of Int
‘s sub-classes. Raku sports an excellent coercion protocol and there is no reason not use it.
subset PositiveInteger of Numeric:D() where { $^i > 0 && ($i - $i.truncate == 0) || fail("Expected a positive and an integer value but got $i.")}
sub get-prime(PositiveInteger $nth where * > 0) {
say ($x.grep: *.is-prime)[$nth - 1];
}
get-prime('5');
get-prime(½);
# OUTPUT: 11
Expected a positive and an integer value but got 0.5.
in block at 2021-03-08.raku line 1809
in block <unit> at 2021-03-08.raku line 1288
A value check is needed in this instance because .AT-POS
will coerce to Int
(by truncating) what may give a reasonable answer for an unreasonable question. No matter how sharp the knife, we wont get the ½th prime number. Being positive and integer is a hard problem.
Please don’t use say
unless you actually want to run .gist
.
say Nil;
put Nil;
# OUTOUT: Nil
Use of Nil in string context
in block at 2021-03-08.raku line 1818
When you say
you may miss subtile bugs, especially when running CI-tests.
Further down we find Sequence
but grep
returns a Seq
– in this example. I never had to make the distinction between Seq
and HyperSeq
but the latter is not a child of Cool
so a lot of interfaces are missing. The same is true for the role Sequence
. The build-in types are a hard problem.
If we must ask what exactly *
is (I wouldn’t, because that’s a really hard question.), it is best to provide the correct answer.
my &b = * + *;
say .WHAT, .signature with &b;
my &b2 = { $^a + $^b };
say .WHAT, .signature with &b2;
# OUTPUT: (WhateverCode)(;; $whatevercode_arg_11 is raw, $whatevercode_arg_12 is raw)
(Block)($a, $b)
A WhateverCode
-object will have all its arguments as is raw
, what means it will always have access to the containers used by the callee (There are many things we can do, but probably shouldn’t, with .VAR
). It will never have control exceptions (no return
or phasers, etc.), doesn’t have a proper scope (no use
-statement) and a few more things. The idea is to have a subclass of Code
that is so simple that the compiler can always inline it. Code-objects are a hard problem.
I would have written that code a little different.
sub get-primes(*@nth) {
(^∞).hyper.grep(-> Int:D() $_ is raw { .is-prime })[@nth »-» 1]
}
.put for get-primes(5, 50, 500, 5000, 50000);
I spend a pretty penny on that Threadripper, so I better .hyper
as often as I can. If sensible I try to use a slurpy to move the burden of dealing with plurals from the user to the implementer. We can ask .AT-POS
for a list of values, so there is no reason not to.
There are quite a few more inaccuracies in Steve’s article. Bless him, Raku is a bitch and we are not good at explaining how the language actually works. For most programs that doesn’t matter, they will run just fine even if the programmer is sloppy. I learned most of the gritty details when tracking down bugs. Granted, I stick my arm deep into the machinery, so it’s my own bloody fault when I get pinched. Since bugs happen we need to get better at explaining how to hunt them down. Raku is a hard problem.
Coercing the unspeakable
My wish for typed Supply
would be rather limited if we could not coerce to roles.
role R[::T] {}
class A {
method m(R[Int]() $_) { say $_ ~~ R[Int] }
}
class B {
method R[Int]() {}
}
# OUTPUT: Missing block
at /home/dex/projects/raku/tmp/typed-supply.raku:35
------> method R⏏[Int]() {}
So a Signature
can ask for a coercion to a parametrised role but a class can’t provide such a method because the compiler doesn’t like the name. From the standpoint of the compiler method names are just strings. The class
keyword is just veneer for the MOP.
B.^add_method('R[Int]', method {
class :: does R[Int] {
}.new
});
B.^compose;
A.new.m(B.new);
# OUTPUT: True
Having a dynamic compiler for a dynamic language does come with perks. However, using silly method names is not specced. So a problem solving issue is still in order.
Raku is a match for *
PimDaniel asked an interesting question.
How do i test match is True while matching : this does NOT work :
if my ($type,$a,$b,$c) = ($v ~~ /^ ('horiz'|'vertic') '_' (\d+) '_' (\d+) '_' (\d+) $/)>>.Str { ... }
Well i made it in 2 times 1/ capture and test the match, 2/ convert the match to Str.
There was no prompt answer and no improvement at all. I couldn’t find a nice way to do this quickly either. In fact it took me the better part of an hour to crack this nut. The main issue here is that a failed match will produce Nil
that .Str
will complain about. So lets separate boolean check of if
and the conversion to Str
.
my $a = '1 B';
if $a ~~ /(<digit>) \s (<alpha>)/ -> $_ {
my ($one, $B) = .deepmap: *.Str;
say "$one $B";
}
# OUTPUT: 1 B
By forcing the result of the condition expression into the topic, we can run any method on the result of the match, but only if Match.bool
returns true. I don’t got a degree in CS* but would be very surprised if Raku-signatures would not turn out to turing complete.
if $a ~~ /(<digit>) \s (<alpha>)/ -> Match (Str() $one, Str() $B) {
dd $one;
dd $B;
}
# OUTPUT: "1"
"B"
The signature of the if
block coerces the Match
to a list. We pick two elements of it and coerce those to Str
. Of cause we could coerce to anything we like based on the position of the captures.
Regexes in Raku are compiled to the same byte code then the rest of the program. In fact grammars are just classes with a funky syntax. That’s why we can run Raku code inside a regex with ease. That means we can turn the whole thing inside out.
my @a = <1 B 3 D 4>;
my @b;
my $hit;
for @a -> $e {
@b.push: ($e ~~ /(<alpha>) || { next } /).Str;
}
say @b;
# OUTPUT: [B D]
Here we skip the .push
if the match does not succeed by skipping the rest of the loop body with next
. We could fire any control exception inside the regex. That means we could stick the whole thing into a sub
and return the value we are looking for from within the regex.
sub cherry-pick-numeric(Str $matchee) {
$matchee ~~ m/(<digit>) && { return .Numeric }/;
Empty
}
@b = do .&cherry-pick-numeric for @a;
dd @b;
# OUTPUT: Array @b = [1, 3, 4]
Raku has been in the making for 10 years. This was an gargantuan task. Now comes the hard bit. We have to take that large language and find all the nice idioms. Good things come to those who wait (on IRC).
*) read: Don’t believe anything I write. You have been warned.
Update:
In truly lazy fashion I came up with a way to turn a match into a lazy list after the work should have been done.
$a = '1B3D4';
my \ll := gather $a ~~ m:g/
[ <alpha> && { take $/<alpha>.Str } ]
| [ <digit> && { take $/.<digit>.Numeric } ]
| [ { say 'step' } ]
/;
say ll[0];
say ll[3];
# OUTPUT: 1
step
step
step
D
The trick is force the match to run all the way to the end of the string with the :g
adverb. This run will be interrupted by a take (by throwing CX::Take
) and resumed when the next value is asked from the Seq
returned by gather
. I don’t know if this is memory efficient thought. There may be a Match
instance kept around for each take
.
Assumed predictability
Vadim does not agree with me. Nor should he. I hardly ever agree with myself. Also, I’m happy for his disagreement because it allows me to write about a topic that I got in the queue for quite some time.
The basic statement is that enforcing types allows reasoning about interfaces at compile time and maybe even earlier — at brain time. A reasonable thing to do in a statically typed language. When objects are involved, Raku does plenty of its thinking at runtime. Let’s have a look at two examples.
class Foo {
submethod shift-right($i) { }
}
sub foo(Foo() $handle) {
$handle.shift-right(4);
}
foo(Foo.new);
class Bar is Foo {
}
foo(Bar.new);
# OUTPUT: No such method 'shift-right' for invocant of type 'Bar'
We ask the compiler for a type check against Foo
that Bar
satisfied. Then we proceed to call a method on an instance of Bar
that is only supplied by Foo
. The compiler tried to help us but couldn’t. In this example our interface is not Foo
and its methods but only Foo
— excluding its child classes — and its methods. This is a subtle difference that will hurt us at runtime. With another example I can illustrate quite clearly why.
class Catchall {
has $.the-object;
method FALLBACK($name, |c) {
self.the-object."$name"(|c)
}
}
sub foo(Catchall $c) {
$c.any-name(42);
}
say foo( Catchall.new: the-object =>
class :: {
method any-name($i) { 42 ~~ $i ?? 'Universe' !! 'wut‽' }
}
);
# OUTPUT: Universe
Can you name the interface that is being used by sub foo
? That’s a mean question of course, as the class that supplies the interface got no name. The type constraint provides the interface of an accessor method to $.the-object
and anything inherited from Any
. The latter might change with new language versions btw. Consequently, the real interface is more like *.any-name()
with a type constraint of Any
. Those two are simple examples. Any module you use might use MONKEY-TYPING
or fiddle with the MOP. Runtime interfaces are utterly unpredictable in Raku and we all do well to use Test
.
That being said, Vadim is right to uphold the principle of least surprise. We start the names of roles with a capital to indicate it to be a type object and thus the wish to honour some sort of interface. I would be happy for a more general solution to the problem of “slurpy” coercion. Technically, Raku got the syntax for it already.
sub Filish(Any:D $handle where * ~~ Str|IO::Handle|IO::Path --> IO::Handle) {
# ... coerce away here
}
sub foo(&Filish() $handle) {
$handle.put: "This would make me happy!";
}
This form would basically say: “I want the coercion handled by a sub called Filish
“. It would allow the code in Filish
to be reused and as such provide flexibility, without giving the impression to promise interface. At least in this example the signature of the coercion sub contains its own documentation. There may even be room for some compile time checks, so long as we don’t use a multi. The parameter $handle
must satisfy the signature of Filish
. Having a sub would allow a module user to .wrap
it.
Being in general disagreement with myself can be challenging but does allow me to change my mind easily. Since this is likely my last blog post this year I wish you all exclusively nice reasons to change your minds in 2021.
Coercive files
Many APIs have a routine or two that take a file as an argument. In Raku we can easily turn Str
into IO::Path
and subsequently into IO::Handle
. As a module author it’s a polite move to provide MMD variants, so the user can supply what they like.
sub s($file is copy where { $_ ~~ Str|IO::Path|IO::Handle or fail(&?ROUTINE.name ~ ' likes Str, IO::Path and IO::Handle.') } ) {
given $file {
when Str { $file = .IO.open; }
when IO::Path { $file = .open; }
when IO::Handle { }
}
say ?$file;
}
This is boilerplate. Which in the kingdom of Raku is almost banned. Using the new coercion protocol, we can implement a role to happily save time ever after.
role Filish[*%mode] is IO::Handle {
proto method COERCE($) {*}
multi method COERCE(Str:D $s) {
my $handle = self.new: :path($s.IO);
$handle.open: |%mode
}
multi method COERCE(IO::Path:D $p) {
my $handle = self.new: :path($p);
$handle.open: |%mode
}
multi method COERCE(IO::Handle:D $h) {
$h
}
}
sub f(Filish[:w, :a, :!bin]() $handle) {
$handle.put: "foo" xx 42;
$handle.close;
}
f('/tmp/foo.txt');
With the coercing type constraint Filish[:w, :a, :!bin]()
we basically say: “Give me something that represents a file, that I will open for writing in the appending fashion.”. I was not aware of the possibility to use a slurpy in the parameter list of a role (*@a
works too). This seems to be an ENODOC. Since it makes my file easier I wont complain.
The new coercion protocol is very useful but got one flaw. It forces me to return the type that contains the COERCE
-method. In a role that doesn’t make much sense and it forces me to juggle with IO::Handle
. It took me 30 minutes to figure out how to successfully subclass it. There may be classes in modules that are even worse. Some programmers really like their private attributes. It would be nice to drop that restrictions on roles and/or if a return type is explicitly provided. With the current model, something easy is made hard.
Anyway, I got it working and might stick a module into the ecosystem, once I came up with a good name.
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.
Handling Failure
After some back and forth I have found a practical way to handle error conditions in Shell::Piping
. The practical way is to have more then one way. A process does have an exitinteger (called a code, because it can be quite cryptic) and text output to STDERR to indicate something went wrong. Sometimes we need sloppy error handling, sometimes we need to look into the textual output and react to it.
I found a really nice way to use a Junction and Rakus type system to remove some boilerplate from error handling. Combining both allows us to create a flexible type.
class Exitcode {
has $.value;
has $.command;
method Numeric { $.value }
method Str { $.command }
}
So this class produces objects that are both a number and a text. What is actually looked at depends on who is looking. We can use infix:<~~>
to make the decision which comparison operator to use.
say $ex ~~ 42 && $ex ~~ ‚find‘; # OUTPUT: True
That’s still quite wordy. We can use a Junction because it binds tighter then ~~
.
say $ex ~~ 42 & ‚find‘; # OUTPUT: True
Now we can CATCH
an Exception and narrow done the command in a pipe that failed easily.
CATCH {
when X::Shell::NonZeroExitcode {
given .exitcode {
when 42 & ‚find‘ {
warn ‚Oh now! We found the answer!‘;
}
}
}
}
Not all users of a module might like to use Exceptions. So we use a construct in a Shell::Pipe
object to create a Failure
to return from .sink
. If the method Shell::Pipe.exitcode
is called, we assume the user is dealing with them by hand. We can then call .handled
to “abort” the Exception. This has to be easy or it might get skipped. Hence the unusual usage of the coercer methods in the class Exitcode
.