Month with Raku
, a 10 minute read
Raku was language that I was interested in for a while. I was on a hype train for a while, mostly thanks to Hillel Wayne and their two excellent posts on Raku: Raku: A Language for Gremlins and Five Unusual Raku Features. I decided that Advent of Code would be a perfect playground to get a taste of the language. And dare I say it, I was impressed!
As always with Advent of Code I gradually loose interest. I managed to complete up to Day 8 and used quite a few mechanics of Raku. However, I only scratched the surface of this language and I wish that one day I could properly embrace this weird language. I don't quite know what to use it for yet, perhaps some quick parsing and text processing where AWK wouldn't be enough and large Python standard library wouldn't be necessary. I also didn't figure out Raku package system yet which gives another point to Python for now.
First Impressions
This is the scripting language. A love child of a various random features that somehow feels quite coherent. AWK and Python with some declarative and array programming put together to directly express your intent, regardless of your programming background. Which is great for small scripts and programs that you create every day, especially when you do a lot of parsing and weird data processing.
The scripting language feel is enforced by how it doesn't seem easy to scale. It may have the same issue as C++, when you or your team need to create per project conventions that must be upheld. Since language is large you need to choose which subset of the language to use. This may be hard if the libraries don't follow your conventions. C++ may be the worst case of this and I may be just traumatized - I would really like to meet a dedicated Raku programmer to chat about it.
Notable features
Program organization
Whole Advent of Code is contained in one file: aoc.raku. It can be called as a script on Linux due to shebang on the top. Entrypoint is chosen based on command line arguments by using multimethods. First, the entrypoint prototype validates that arguments match expected input: two integers for day number and day part, then a string beeing a file path:
proto MAIN(Int $day, Int $part, Str $file where *.IO.f, |) {*}
The most interesting part of this signature is the hole: {*}
.
It can be used to control when the dispatch to multimethod is handled:
proto dispatch_only_when_even(Int $value) {
if ($value % 2 == 0) {
return {*};
}
return "$value is odd";
}
multi dispatch_only_when_even(Int $value) {
return "$value is even";
}
say dispatch_only_when_even(4); # prints "4 is even"
say dispatch_only_when_even(3); # prints "3 is odd"
Then each multi method definition specifies which day and which part it is. For example for day 4, part 2 the definition is:
multi MAIN(4, 2, $filename) {
}
Control flow
Supercharged conditionals with when
Raku has a few extensions of standard set of control flow constructs.
A small one is when
,
a variant of if
that on false skips all the following statements in the surrounding block and goes to the outer block.
sub test($x) {
{
when $x == 0 { say "$x equal to 0" }
say "$x not equal to 0";
}
say "end of test";
}
test(0);
test(1);
Code above produces this output:
0 equal to 0
end of test
1 not equal to 0
end of test
Another difference with if
is the test that it's performed.
It uses smartmatch against a topic variable
to provide comfy contextual matching.
For example when iterating over the lines of file using for $file.IO.lines {}
statement, the topic is current line of file.
By smartmatching against regular expression we can select only those lines that contain a number:
for $file.IO.lines {
when /\d+/ { say "$_ contains a number"; }
}
With the given
one can reproduce switch
in Raku,
without explicit support for it.
My favourite use of when
(in conjuction with the regular expressions described below)
is in solution of day 3 part 2 with parsing of simple expressions.
This code is so beautiful and self expressive that one could immediately deduce the task from what is written (if they know Raku regexes of course):
given $file.IO.slurp {
for m:exhaustive{ ("do" "n't"? "()")
|| "mul("( \d ** 1..3) "," (\d**1..3) ")"
} {
when "do()" { $enabled = True }
when "don't()" { $enabled = False }
default { $total += $_[0] * $_[1] if $enabled }
}
}
Statement modifiers
Conditional statements can be turned into statement modifiers, controlling the execution of single statement. This allows for readable and quick early exits and conditions that are just a joy to write. My favourite use in the Advent of Code solutions is this one:
last unless inbounds @grid, $x, $y;
which would translate to this C code:
if (!inbounds(grid, x, y)) { break; }
and could be split by formatter to up to 4 lines!
Also naming continue/break
, next/last
is a smart move, if C syntax wouldn't hold entire programming world hostage we should switch.
gather/take
In Raku loops already behave like list comprehansions in other languages:
say (for 1..10 -> $v { $v if $v % 2 == 0 });
# printed (2 4 6 8 10)
Raku allows this mechanism of value collection to be used explicitly with gather
that creates a block where values will be gathered and take
that will send values to gather
. The simplest use would be like this:
say gather {
take 1;
take 2;
};
# printed (1 2)
What makes it unique in the languages that support similar features (like Python or JavaScript generators) is that take
can send from function to caller without any special syntax:
sub count($n) {
for 0..$n -> $i { take $i; }
}
say gather {
count(5);
count(4);
}
# prints (0 1 2 3 4 5 0 1 2 3 4)
It's superpowers doesn't end there.
Since Raku has builtin pair type, gather that produces sequence of pairs could be easily coerced into a dictionary data structure.
There is also a lazy variant which would stop the execution between each produced take depending on consumption.
Fun doesn't stop there - Raku provides also an asynchronous stream like variant called supply/emit
.
Grammars and regular expressions
I've written a lot of lexers, parsers and data extractors. Both in context of programming languages and outside of it. I'm used to writing standard recursive descent or mocking some abomination of loops and regular expressions in Python.
Raku feels like a very welcome revolution by improving the syntax of regular expressions and introducing grammars as a primary composition methods of them. This is pure comfort and joy. Parsing no longer feels like encoding some automata that you have in your head and instead is provided as is, declarative, in the language, without special EDSLs, compilers or generators. I don't like most parser libraries, I find them not worth the effort. Having support for parsing right in the language is a bliss.
Regular expressions in Raku make a few key changes that improve readability.
By introducing insignificant whitespace, comments and regexes composition via interpolation one could easily structure regex in clear way.
Escaping special characters isn't necessary when you can put literals in quotes: matching suffix .com
can be easily done with regex /".com"$/
.
Quantifiers can be modified with the separator.
%
modifies quantifier of the left to expect values separated by the value on the right.
%%
does the same but allows trailing separators.
So if you want to specify regex for a CSV file that contains some numbers you could do this like this: /((\d+)+ % ",")* % \n/
.
The documentation for Regexes in Raku is massive. I think that with my usage I only scratched the surface of what can be accomplished. For Advent of Code it was however enough enough.
When you grasp Raku Regexes, Grammars offer a way to structure them nicely. For me, the words aren't enough and would only muddy the picture. The best showcase is this simple JSON grammar in Raku that can deserialize JSON in 40 lines of code. Impressive.
Declarative programming
Raku has a bunch of sugar that allows to specify directly what one thinks. In this matter it's very similar to languages like Haskell or APL.
Raku has a lot of non-standard operators, including different array and hash indexing operators (array[idx]
and hash{idx}
),
replication operators (x
for strings, xx
for lists) and flip-flop operators (ff
; the wildest of them all).
The most impressive ones are metaoperators - operators that are parametrized by other operators.
It puts Haskell-style list processing to shame and is the closest that I felt a „normal language” has approach concatenative paradigm.
Metaoperators are operators that enhance or modify the behaviour of normal operators.
The simplest case is the negation of the operator: !=
can be viewed as the usage of metaoperator !
with operator ==
.
In Raku this is correct interpretation, the cannonical spelling of not equals is !==
with !=
as an alias.
Another simple operation is reversing the sides of binary operators.
This is done in Raku with R
: 10 - 9 == 9 R- 10
.
Why one would need to reverse a binary operator? For reductions of course.
You could turn any binary operator into a reduction by putting it into square brackets.
This makes it a unary reduction operator!
For example [*] 1..5
would calculate 5 factorial.
If you want partial results, just add one character: [\*] 1..5 == (1, 2, 6, 24, 120)
.
In conjuction with operator reversing this allows for any common kind of fold:
Math | Haskell | Raku |
---|---|---|
(((0-1)-2)-3)-4 |
foldl (-) 0 [1..4] |
[-] (0, 1..4).flat |
((1-2)-3)-4 |
foldl1 (-) [1..4] |
[-] 1..4 |
0-(1-(2-(3-4))) |
foldr (-) 0 [1..4] |
[R-] (0, 1..4).flat |
1-(2-(3-4)) |
foldr1 (-) [1..4] |
[R-] 1..4 |
Lack of value types
As with many languages, cracks begin to show when builtin primitive types aren't enough.
For example, let's say you want to map two dimensional vector to some numeric value.
In Python this can easily be done using tuples: {(1, 2): 3, (4, 5): 6}
.
In Raku however, if one would like to index with array, Raku would respond by indexing by each element of array:
> {3 => 4, 4 => 5}{3,4,5}
(4 5 (Any))
> {[3,4] => 10, [5,6] => 20}{[3,4]}
((Any) (Any))
> {[3,4] => 10, [5,6] => 20}{"3 4"}
10
Listing above shows what really happens. If we index with arrays during hash creation, then they are automatically converted to strings. If we index after creation with an array, then it uses array elements as indices. It was for a moment a breaking point for me with Raku. One could use builtin pair type, however it also has a set of quirks.
C Foreign Function Interface
C ABI on given platform is the interface of programming languages. If your language doesn't support it the use of it is quite limited. Raku shows with what would I categorize as the modern standard for C interoperability by using Raku normal syntax for classes and functions to define foregin ones with a bit of traits magic. You can find documentation here.
In C:
void InitWindow(int width, int height, char const* title);
In Raku:
LIBRAYLIB = "/path/to/raylib.so"
sub InitWindow(int32 $width, int32 $height, Str $title) is native(LIBRAYLIB) {*}
Structures can be also defined with one to one match:
class Color is repr('CStruct') {
has uint8 $.r is rw;
has uint8 $.g is rw;
has uint8 $.b is rw;
has uint8 $.a is rw;
}
However, as in most scripting languages there is requirement to pass structures by reference, not by value. This is fine for some C APIs but not all of them, especially Raylib. Since Color class can fit inside the 32bit value workaround is easy:
class Color is repr('CStruct') {
has uint8 $.r is rw;
has uint8 $.g is rw;
has uint8 $.b is rw;
has uint8 $.a is rw;
method asInt {
$.r +< (3 * 8) +|
$.g +< (2 * 8) +|
$.b +< (1 * 8) +|
$.a +< (0 * 8)
}
}
sub ClearBackgroundNative(int64 $color)
is native(LIBRAYLIB)
is symbol("ClearBackground") {*}
sub ClearBackground(Color $c) {
ClearBackgroundNative($c.asInt);
}
Sadly, I couldn't get multimethods to work here, since int64
isn't a real type that Raku could dispatch on.