List breaks the chain

September 25, 2020 Leave a comment

While watching RaycatWhoDat liking Raku, I realised that a List of Matches is not a MatchList.

say 'a1'.match(/ \d /).replace-with('#');
say 'a1b2'.match(/ \d /, :g).replace-with('#');
# OUTPUT: a#
#         No such method 'replace-with' for invocant of type 'List'
            in block <unit> at /home/dex/tmp/tmp-2.raku line 8

The 2nd call of replace-with will fail because .match with :g returns a List of Match. Of course we can play the easy game and just use subst which does the right thing when called with :g. This wouldn’t make a good blog post though.

To make replace-with work with a List we can use a where-clause. Any Match will have a copy of the original Str but not to the original Regex so we actually have to build a list of Str of everything that was not matched. This can be done by using the indices stored in .from and .to.

multi sub replace-with(\l where (.all ~~ Match), \r --> Str) {
    my $orig := l.head.orig;
    my @unmatched;

    @unmatched.push: $orig.substr(0, l.head.from);
    for ^(l.elems - 1) -> $idx {
        @unmatched.push: $orig.substr(l[$idx].to, l[$idx+1].from - l[$idx].to);
    }

    @unmatched.push: $orig.substr(l.tail.to);

    (@unmatched Z (|(r xx l.elems), |'')).flat.join;
}

say 'a1vvvv2dd3e'.match(/ \d /, :g).&replace-with('#');
# OUTPUT: a#vvvv#dd#e

If the original string does not end with a match, the list of matches will be one short to be just zipped in. That’s why I have to extend the list of replacements by an empty string before feeding it to Z.

So if subst is doing it right why bother with .replace-with? Because sometimes we have to use $/.

if 'a1bb2ccc3e' ~~ m:g/ \d / {
    say $/.&replace-with('#');
}

Often we could change the code but when a routine from a module returns Match or a list thereof, we are out of luck. For completeness we need a few more multies.

multi sub replace-with(Match \m, \r --> Str) {
    m.replace-with(r);
}

multi sub replace-with(Match \m, &r --> Str) {
    m.replace-with(r(m));
}

multi sub replace-with(\l where (.all ~~ Match), &r) {
    my $orig := l.head.orig;
    my @unmatched;

    @unmatched.push: $orig.substr(0, l.head.from);
    for ^(l.elems - 1) -> $idx {
        @unmatched.push: $orig.substr(l[$idx].to, l[$idx+1].from - l[$idx].to);
    }

    @unmatched.push: $orig.substr(l.tail.to);

    (@unmatched Z (|l.map(&r), |'')).flat.join;
}

Even if the problem is solvable it still bugs me. We have :g in many places in Raku to provide quite a lot of DWIM. In some places that concept breaks and almost all of them have to do with lists. Often ».comes to the rescue. When we actually have to work on the list and not the individual elements, even that doesn’t work. The methods on strings just work because in this case we deliberately avoid breaking the list of characters apart.

If you follow this blog you know that I’m leaning strongly towards operators. Sadly ., .? and ». are not real infixes which we can overload. Nor can we declare infixes that start with a . or we could introduce an operator that turns it’s LHS to a list and then does a dispatch to multis that can handle lists of a certain type.

Without that we need to change .match to return a subclass of List that got the method .replace-with. We could stick it into Cool but that is a crowded place already.

We don’t really have a nice way to augment the return values of builtin methods. So this will have to be fixed in CORE.

Categories: Raku

Releasing on github

September 10, 2020 1 comment

In my last post I lamented the lack of testing metadata. Just a few days later it got in my way when I played with creating releases on github. My normal workflow on github is to commit changes and push them to trigger travis. When travis is fine I bump the version field in META6.json so the ecosystem and zef can pick up the changes. And there is a hidden trap. If anybody clones the repo via zef just before I bump the version, there will be a mismatch between code and version. CPAN doesn’t got that problem because there is always a static tarball per version. With releases we can get the same on github.

It’s a fairly straight forward process.

  • build a tag-name from the github-project-name and a version string
  • generate the URL the tarball will get in the end (based on tag-name) and use that as source-url in the META6.json
  • commit the new META6.json locally
  • create a git tag locally
  • push the local commit with the changed META6.json to github
  • push the git tag to github
  • use the github API to create the release, which in turn creates the tarball

This is so simple that I immediately automated that stuff with META6::bin so I can mess it up. (Not released yet, see below.)

The result is an URL like so: https://github.com/gfldex/raku-release-test/archive/raku-release-test-0.0.19.tar.gz. When we feed that to zef it will check if the version is not already installed and then proceed to test and install.

And there is a catch. Even though zef is fine with the URL, Test::META will complain because it doesn’t end in .git and fail the test. This in turn will stop zef from installing the module. We added that check to make sure zef always gets a proper link to a clone-able repo for modules hosted on github. This assumption is clearly wrong and needs fixing. I will send a PR soon.

Having releases on github (other gitish repo-hosting sites will have similar facilities or will get them) can get us one step closer to a proper RPAN. Once I got my first module into the ecosystem this way I will provide an update here.

Categories: Raku

How does lizmat know?

September 5, 2020 2 comments

I didn’t know so I asked her.

15:25 <gfldex> How do you gather info for "Updated Raku Modules"?
17:40 <lizmat> https://twitter.com/raku_cpan_new
17:59 <gfldex> thx
23:06 <lizmat> well volunteered  :-)

That’s what you get for being nosey. So off I went into the land of mostly undocumented infrastructure.

The objective is simple. Generate two lists of modules where the first contains all modules that are newly added to the ecosystem and the second got all updated modules. For both the timespan of interest is Monday of this week until Monday of last week. Currently we got two collections of META-files. Our ecosystem and CPAN. The latter does not know about META6 and that sucks. But we will manage. Conveniently both lists are provided by ugexe at github. Since there are commits we can travel back in time and get a view of the ecosystem from when we need it. To do so we first need to get a list of commits.

sub github-get-remote-commits($owner, $repo, :$since, :$until) is export(:GIT) {
    my $page = 1;
    my @response;
    loop {
        my $commits-url = $since && $until ?? „https://api.github.com/repos/$owner/$repo/commits?since=$since&until=$until&per_page=100&page=$page“ !! „https://api.github.com/repos/$owner/$repo/commits“;
        my $curl = Proc::Async::Timeout.new('curl', '--silent', '-X', 'GET', $commits-url);
        my $github-response;
        $curl.stdout.tap: { $github-response ~= .Str };

        await my $p = $curl.start: :$timeout;
        @response.append: from-json($github-response);

        last unless from-json($github-response)[0].<commit>;
        $page++;
    }

    if @response.flat.grep(*.<message>) && @response.flat.hash.<message>.starts-with('API rate limit exceeded') {
        dd @response.flat;
        die „github hourly rate limit hit.“;
    }

    @response.flat
}

my @ecosystems-commits = github-get-remote-commits(‚ugexe‘, ‚Perl6-ecosystems‘, :since($old), :until($young));

Now we can get a whole bunch of ex-json which was compiled of the META6.json and *.meta files. Both file formats are not compatible. The auth field of a CPAN module will differ from the auth of the upstream META6.json, there is no authors field and no URL to the upstream repo. Not pretty but fixable because tar is awesome.

my @meta6;
px«curl -s $source-url» |» px<tar -xz -O --no-wildcards-match-slash --wildcards */META6.json> |» @meta6;

my $meta6 = @meta6.join.chomp.&from-json;

(Well, GNU tar is awesome. BSD tar doesn’t sport --no-wildcards-match-slash and there is one module with two META6.json-files. I think I can get around this with a 2 pass run.)

This works nicely for all but one module. For some reason a Perl 5 module sneaked into the list of Raku modules on CPAN. It’s all just parsed JSON so we can filter those out.

my @ecosystems = fetch-ecosystem(:commit($youngest-commit)).grep(*.<perl>.?starts-with('6'));

Some modules don’t contain an auth field, some got an empty name. Others don’t got the authors field set. We don’t enforce proper meta data even though it’s very easy to add quality control. Just use Test::META in your tests. Here is an example.

I can’t let lizmat down though and github knows almost all authors.

sub github-realname(Str:D $handle) {
    my @github-response;

    my $url = 'https://api.github.com/users:' ~ $handle;
    px«curl -s -X GET $url» |» @github-response;

    @github-response.join.&from-json.<name>
}

If there is more then one author they wont show up with this hack. I can’t win them all. I’m not the only one who suffers here. On modules.raku.org at least one module shows up twice with the same author. My guess is that happens when a module is published both in our ecosystem and on CPAN. I don’t know what zef does if you try to nail a module down by author and version with such ambiguity.

I added basic html support and am now able to give you a preview of next weeks new modules.

New Modules

Updated Modules

If your module is in the list and your name looks funny, you may want to have a look into the META6.json of you project.

Yesterday we had a discussion about where to publish modules. I will not use CPAN with the wrong language. Don’t get me wrong. I like CPAN. You can tie an aircraft carrier to it and it wont move. But it’s a Comprehensive Perl Archive Network. It’s no wonder it doesn’t like our metadata.

Kudos to tony-o for taking on a sizeable task. I hope my lamentation is helpful in this regard.

The script can be found here. I plan to turn it into a more general module to query the ecosystem. Given I spend the better part of a week on a 246 lines file the module might take a while.

Categories: Raku

Tripping over variables

August 25, 2020 Leave a comment

I was wondering where lizmat gets the info for changed modules from. She kindly answered with a link. I learned that updates to modules only show up, when we put them on CPAN. Since most modules are hosted on github, changing a module there does not mean that the world will be informed. I believe a better way to do that would be to fetch the ecosystems (we got two) once a week and check if version in any META6.json has changed.

Anyway, the reason I started this post is the documentation for FixedInt. It reads:

One major caveat to be aware of when using this module. The class instance may not be instantiated in a $ sigiled variable.

Raku $ sigiled scalar variables do not implement a STORE method, but instead do direct assignment; and there doesn’t seem to be any easy way to override that behaviour.

An implication of that is that classes that do implement a STORE method can not be held in a $ sigiled variable. (Well, they can, they just won’t work correctly. The first time you try to store a new value, the entire class instance will be overwritten and disappear.)

That is not true.

class Changing {
    has $!var handles <Str gist raku> is default(Nil);
    method STORE(\v) { note 'storing'; $!var = v }
    method FETCH { note 'fetching'; $!var }
}

constant term:<$a> := Changing.new;

$a = 42;
put $a;
# OUTPUT: storing
          42

The problem here is that the docs talk about variables while Raku don’t got any. It got containers with mutable content and values which are immutable. The language also got symbols that we can actually point at in source code. (Values we can point at in source code are called literals.) In the example above I created a symbol that looks like a variable but is a “reference” to a value of type Changing. The assignment operator can not be overloaded so we can protect immutable values. We can implement the method STORE instead. In fact we must, because there is no container in-between the symbol $a and the instance of Changing. (We get X::Assignment::RO if we try to assign without a STORE.) Since Rakudo does not recognise Changing as a container, it will refuse to call FETCH.

Thundergnat wrote a neat module with very little effort. Quite useful to do calculations with integers of fixed bit size.

my \fixedint = FixedInt.new(:8bit);

say fixedint; # 0

say fixedint -= 12;   # 244
say fixedint.signed;  # -12
say fixedint.bin;     # 0b11110100
say fixedint.hex;     # 0xF4

He achieved all that in just 36 lines of code. The trick is to force the user to bind and thus avoid the creation of a container while using STORE and FETCH to change the object in place. I doubt this is thread safe. Also the user of the module loses the ability to use some forms of quote interpolation and data dumper functions/modules will have less to display.

my \i = Int.new(10);
my $i = Int.new(10);

dd i;
dd $i;
# OUTPUT: 10
          Int $i = 10

We don’t have to define many operators to make custom types work because of plenty of meta-programming that is done in CORE. Many of those constructs assume immutable values. Autothreading is planned and will make the use of ». “interesting”. Thundergnat did not specify a language version for his module. The module itself is not hard to make safe. But – acutally BUT – this will change the interface for the user.

The flexibility of the language bites us here. Even thought the docs explain the difference between different sigils nobody is forced to read it. Also, nobody is forced to stick use v6.d at the beginning of a module. Please do so or the compiler wont be able to help you in the future. While naming immutable constructs quite often, the docs don’t explain why we use them. Concurrency and thus threading is very easy to add to a program. Testing it is hard.

I don’t have a solution to those problems but I’m pretty sure we need one or they will haunt us the next 100 years.

Categories: Raku

Exceptionally colourful

August 23, 2020 4 comments

STDERR is often (ab)used for printing debug or status information. This can create clutter which in turn hides the important stuff. I want to print the essential stuff in exceptions in red unless a dynvar or environment variable is set.

class Explode is Exception {
    method message {
        put "$*dynvar is bad‼";
    }
}

sub e() {
    await start {
        Explode.new.throw;
    }
    CATCH { default { put .message } }
}

my $*dynvar = 'foo';
e();

# OUTPUT: foo is bad‼

We can access a dynvar inside the method of an exception from within an exception handler. In Shell::Piping error handling is a bit more involved. The biggest issue is fail because the enclosed exception is thrown by some routine in CORE about two steps down the call tree seen from the implicit or explicit MAIN sub. The dynvar is simply not there at this point in time. Luckily instances of Exception tend not to be long lived so we can get away with capturing the state of a dynvar at object creation. A good place to do so is TWEAK.

sub infix:<///>(\a, \b) is raw {
    my $dyn-name = a.VAR.name;
    my $has-outer-dynvar = CALLER::CALLERS::{$dyn-name}:exists;
    CALLER::{$dyn-name} = $has-outer-dynvar ?? CALLER::CALLERS::{$dyn-name} !! b
}

role Exception::Colored is Exception is export {
    has $.color;
    submethod TWEAK {
        my $*colored-exceptions /// on;
        $!color = $*colored-exceptions ~~ on && $env-color ?? 31 !! 0;
    }
    method RED($str) {
        $*ERR.t ?? ("\e[" ~ $.color ~ 'm' ~ $str ~ "\e[0m") !! $str
    }
}

Now I can use $.RED in .message of any exception that is Exception::Colored.

To have a look at the full stack was very helpful to figure out why the dynvar wasn’t there in some cases. For such cases I have a context sensitive binding in my .vimrc.

nmap <F1> :w<CR>:!raku -I ./lib %<CR>
imap <F1> <esc>:w<CR>:!raku --ll-exception -I ./lib %<CR>

In insert mode F1 will write the file and run Rakudo with an additional parameter. This results in a full stack trace.

foo failed
   at SETTING::src/core.c/Exception.pm6:62  (/usr/local/src/rakudo/install/share/perl6/runtime/CORE.c.setting.moarvm:throw)
 from SETTING::src/core.c/Failure.pm6:56  (/usr/local/src/rakudo/install/share/perl6/runtime/CORE.c.setting.moarvm:throw)
 from SETTING::src/core.c/Failure.pm6:111  (/usr/local/src/rakudo/install/share/perl6/runtime/CORE.c.setting.moarvm:sink)
 from /home/dex/tmp/tmp-2.raku:56  (<ephemeral file>:<unit>)
 from /home/dex/tmp/tmp-2.raku:1  (<ephemeral file>:<unit-outer>)
 from gen/moar/stage2/NQPHLL.nqp:1948  (/usr/local/src/rakudo/install/share/nqp/lib/NQPHLL.moarvm:eval)
 from gen/moar/stage2/NQPHLL.nqp:2153  (/usr/local/src/rakudo/install/share/nqp/lib/NQPHLL.moarvm:evalfiles)
 from gen/moar/stage2/NQPHLL.nqp:2113  (/usr/local/src/rakudo/install/share/nqp/lib/NQPHLL.moarvm:command_eval)
 from gen/moar/Compiler.nqp:60  (/usr/local/src/rakudo/install/share/perl6/lib/Perl6/Compiler.moarvm:command_eval)
 from gen/moar/stage2/NQPHLL.nqp:2038  (/usr/local/src/rakudo/install/share/nqp/lib/NQPHLL.moarvm:command_line)
 from gen/moar/rakudo.nqp:116  (/usr/local/src/rakudo/install/share/perl6/runtime/perl6.moarvm:MAIN)
 from gen/moar/rakudo.nqp:1  (/usr/local/src/rakudo/install/share/perl6/runtime/perl6.moarvm:<mainline>)
 from <unknown>:1  (/usr/local/src/rakudo/install/share/perl6/runtime/perl6.moarvm:<main>)
 from <unknown>:1  (/usr/local/src/rakudo/install/share/perl6/runtime/perl6.moarvm:<entry>)

As you can see there are quite a few things called before your script will be executed. Luckily Rakudo is implementing Raku in Raku so we have a chance to see what is going on.

Categories: Raku

Defined or dynvar

August 17, 2020 1 comment

While adding dynvars to Shell::Piping to reduce the risk of finger injury I made a typo lizmat kindly corrected. She suggested to use the defined-or operator to test if a given dynamic variable is declared.

($*FOO // '') eq 'yes'

This is not equivalent to test if a dynvar was declared down the call tree. For that we need to check CALLERS.

say CALLERS::<$*colored-exceptions>:exists;
dd CALLERS::<$*colored-exceptions>;
# OUTPUT: False
#         Nil

In case the dynvar is declared we get a different result.

sub dyn-receiver {
    say CALLERS::<$*colored-exceptions>:exists;
    dd CALLERS::<$*colored-exceptions>;
}
my $*colored-exceptions;
dyn-receiver();
# OUTPUT : True
#          Any $*colored-exceptions = Any

For a module author that means we can have somebody sneak some undefined value into a dynvar we use, that has a type we don’t expect. Composebility is not the same thing as correctness. If we want do deal with this situation properly we need to check if the caller declared the dynvar and use a proper default value if they don’t.

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

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

sub dyn-receiver {
    my $*colored-exceptions = CALLERS::<$*colored-exceptions>:exists 
        ?? CALLERS::<$*colored-exceptions>
        !! off
}

In this example there are just two possible values but if there are more and they can be undefined we need to be more careful. However, this is quite a bit of typing. Can we use a deboilerplater here?

sub infix:<///>(\a, \b) is raw {
    my $dyn-name = a.VAR.name;
    my $has-outer-dynvar = CALLER::CALLERS::{$dyn-name}:exists;
    CALLER::{$dyn-name} = $has-outer-dynvar ?? CALLER::CALLERS::{$dyn-name} !! b
}

sub c {
    my $*colored-exceptions /// Int;
    dd $*colored-exceptions;
}

sub d {
    my $*colored-exceptions = Str;
    c();
}

c();
d();
# OUTPUT: Int $*colored-exceptions = Int
          Str $*colored-exceptions = Str

This operator takes two bound arguments. If we call it with a dynvar a contains the container that is the dynvar. We can query the name of that container and us it to check if down the call tree the dynvar was already declared. If so we use its value and assign it directly into the dynvar declared in c. Otherwise we assign b to the dynvar. In both cases we might return something naughty so we better do so raw.

Poking across the stack is risky. This could be done better with proper macros. I am quite sure we can do so after Christmas*.

*) For any value greater then Christmas last year.

Categories: Raku

Guarding dynamics

August 14, 2020 1 comment

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.

Categories: Raku

Whereceptions

August 9, 2020 1 comment

I have a sub that takes a file and tries to guard itself against a file that does not exist. Where clauses don’t make good error messages.

sub run-test(IO() $file where .e & .f) { };
run-test('not-there.txt');
# OUTPUT:
# Constraint type check failed in binding to parameter '$file'; expected anonymous constraint to be met but got IO::Path (IO::Path.new("not-th...)

The signature of that sub is quite expressive. Often we don’t have time to read code to hunt down mistakes. That’s why bad error messages are LTA. We can extend any where clause with a die or fail to provide a better message.

sub run-test(IO() $file where .e && .f || fail("file $_ not found")) { };
run-test('not-there.txt');
# OUTPUT:
# file not-there.txt not found

We can also throw exceptions of cause. With just one parameter the signature has gotten quite long already. Also, when working with many files we will write the same code over and over again. Since we don’t code in Java we would rather not to.

my &it-is-a-file = -> IO() $_ {
    .e && .f || fail (X::IO::FileNotFound.new(:path(.Str)))
}
sub run-test(IO::Path $file where &it-is-a-file) { }

That’s much better. Since exceptions are classes we can reuse code amongst them. Like coloured output and checking for broken symlinks.

class X::Whereception is Exception is export {
    has $.stale-symlink is rw;
    method is-dangling-symlink {
        $.stale-symlink = do with $.path { .IO.l & !.IO.e };
    }
}

class X::IO::FileNotFound is X::Whereception is export {
    has $.path;
    method message {
        RED $.is-dangling-symlink ?? „The file ⟨$.path⟩ is a dangling symlink.“ !! „The file ⟨$.path⟩ was not found.“
    }
}

I have added a few to Shell::Piping. Suggestions what else to check for are very welcome.

Categories: Raku

Dropin replacement

August 1, 2020 1 comment

Today I learned that whereis can take multiple commands to look for in $PATH.

$ whereis not-there raku-test-all zef
not-there:
raku-test-all: /home/dex/bin/raku-test-all
zef: /usr/local/src/rakudo/install/share/perl6/site/bin/zef

The results are always in the requested order. So we can use a shell spell to find a candidate to be used as our tester in a Makefile.

TESTER := $(shell whereis raku-test-all zef | cut -d ' ' -f 2 -s | head -n 1)

install-deps:
        zef --depsonly install .

test: install-deps
        $(TESTER) --verbose test .

install:
        zef install .

all: test

push: test
        git push

I had to do a few changes to raku-test-all so it mimics the interface of zef. The idea behind this Makefile is to be able to hit F6 and have all tests run and then push to github. Our current ecosystem simply distributes links to github repos. As a result pushing without testing can lead to somebody else cloning a broken repo (given the timing is bad enough). As you might imagine any speed up to testing is very welcome. Travis is quite slow. I think I should work in that area next.

Categories: Raku, shell

Wrapping Exceptions

August 1, 2020 1 comment

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.

Categories: Raku