Multi Operations : Raku Weekly Challenge : Week 331

Week 331 : 2025-07-21

Part 1

We we're to find the length of the last word in a given string. I'm defining a word as an unbroken string of non spaces. This is probably wrong now I think about it but it's done now. So the basis solution is quite simple.


sub last-word(Str $s) { $s.comb(/\S+/)[*-1].codes }
      

So use comb to pull out each set of non space characters, then use [*-1] to get the last item in the list and then get the count of codepoints in it. That's all nice, but what if we give it a empty string or a string made up of just spaces? Well then it's going to error... and that's not nice.

Let's define a subset of Str for a Blank string.


subset Blank of Str where m/^ \s* $/;
      

With that we can test our incoming string and if it's blank we return 0. (Of course there's other options for a return value but 0 is fine by me.) Now in many languages you would do something like this.


sub last-word(Str $s) {
    return 0 if $s ~~ Blank;

    return $s.comb(/\S+/)[*-1].codes;
}
      

Oh I added a return there for the final value as it's not a single line function any more, it's not needed but I think it looks nicer. Still this whole "checking stuff on the way in" is a bit messy. The more possible failure states the larger the function becomes with more gunk that doesn't have anything to do with the actual job of the function. Luckily Raku provides us with a powerful tool for dealing with this.

By making a subroutine (or method) a multi method we can define different versions to trigger depending on the arguements. Of course multi dispatch is not new, but Raku is very powerful in that it does it's dispatch based on matching not just argument count. In this case if our input string is Blank we just return 0.


multi sub last-word(Blank $) { 0 }
multi sub last-word(Str $s) { $s.comb(/\S+/)[*-1].codes }         
      

After that was can just wrap it all in a MAIN method and add some tests and we're on to part 2.

Part 2

That was quick.

So for part 2 I decided I wanted to try something else out and define an operator the buds infix operator, it returns a Bool if the two strings are buddies. Here's some example usage in the test subroutine.


multi sub MAIN(:t(:$test) where ?* ) is hidden-from-USAGE {
    use Test;
    ok !("123" buds "1"), "Different Lengths";
    ok !("1" buds "2"), "Too Short";
    ok "fcuk" buds "fuck", "Exmaple 1";
    ok "eovl" buds "love", "Swap anything";
    ok !("love" buds "love"), "Example 2";
    ok "fodo" buds "food", "Example 3";
    ok "feed" buds "feed", "Example 4";
    done-testing;
}
      

There's a few tests here beyond the examples. The challenge doesn't specifically state that you have to swap adjacent letters so I decided to allow for swapping any two letters. Also I've got a couple of tests for a couple of automatic failures (where the two strings aren't the same length or if the strings are less than two characters long.

To define an operator we us the infix category and it's name. We can start with the two failure cases I mentioned above and again we can use multi subs to define them. Note here that our definitions all have 2 arguments, but as I said Raku uses more than that to decide which version to dispatch a call to.


multi sub infix:<buds> (Str() $source,
                        Str() $target where $target.codes != $source.codes) {
    return False
}
multi sub infix:<buds> (Str() $source,
                        Str() $target where $target.codes <= 1) {
    return False
}          
      

So the first handles the case where our two strings are different lengths. Then we have the case where one of the strings is 1 or less long. (We only need to check one as they must both be the same length or the first case would have been triggered.) So with that in place we can implement our algorithm which is pretty simple.


multi sub infix:<buds> (Str() $source, Str() $target) {
    my @target = $target.comb.Array;
    my @check = $source.comb.Array;
    my $last = $target.codes;
    for 0..^$last -> $first {
        for $first^..^$last -> $second {
            @check[$first,$second] = @check[$second,$first];
            return True if @check ~~ @target;
            @check[$first,$second] = @check[$second,$first];
        }
    }
    return False;
}
      

Make arrays of our two strings. Loop through from 0 to the last index in the arrays, inside that loop we loop from the next point to the last index (using the ^..^ range operator, the ^ at each end means "skip this number"). We swap the two elements at these indices and test to see if the two Arrays match. If they don't we swap the two values back and carry on. If we drop out of out loops we have failed and Return false.

There's only one problem I have, defining custom operators currently causes a bit of a speed hit to Raku code. It's not huge in the grand scheme of things but it's noticable. Still it was too cool and idea not to do. Anyway, I hope that was enlightening. Looking forward to next week.