5ab5traction5 - World Wide and Wonderful

I 🫀 Raku - Easy subroutine shortcuts to class constructors

Context

I decided to write a simple (but funky) dice roller over the holidays. This led to a number of fun diversions in Raku code that all deserve some highlighting.

Today I'd like to share a pending PR I have for GTK::Simple that I believe highlights one of Raku's strength: compositional concision. That is, code that does a lot in very few words thanks to the composition of various concise-on-their-own details of the language.

GTK::Simple is pretty chill already

This module does a good job of translating the C experience of writing a GTK application into an idiomatic Raku version. It is both easily usable as well as quickly extensible, should you find some corner case of the (massive) GTK that isn't covered.

I'll be updating with some more in depth discussion of recent changes that have been merged recently.

(Note to self: Fully implementing the afore-linked GTK application in Raku would make for an interesting exercise.)

... but what if it became even chiller?

In GTK::Simple, we map GTK classes into top-level GTK::Simple definitions. So MenuItem becomes GTK::Simple::MenuItem.

This leads to code such as:

use GTK::Simple;

my $app = GTK::Simple::App.new(title => "A fresh new app");
$app.set-content(
    GTK::Simple::HBox.new(
        GTK::Simple::Label.new(text => "Looking good in that GTK theme")
    )
);
$app.run;

Should my PR be accepted, it will allow a short-hand syntax that makes things almost a bit Shoes-like in simplicity.

use GTK::Simple :subs;

my $app = app :title("An alternative");
$app.set-content(
    h-box(
        label(:text("Shiny slippers"))
    )
);
$app.run;

Adding the mappings

The mapping rule is a simple CamelCase to kebab-case conversion of the class names.

The entire code for adding this feature is as follows:

# Exports above class constructors, ex. level-bar => GTK::Simple::LevelBar.new
my module EXPORT::subs {
    for GTK::Simple::.kv -> $name, $class {
        my $sub-name = '&' ~ ($name ~~ / (<:Lu><:Ll>*)* /).values.map({ .Str.lc }).join("-");
        OUR::{ $sub-name } := sub (|c) { $class.new(|c) };
    }
}

my module EXPORT::subs {

This line of code utilizes a Raku convention that allows for custom behavior through specific instructions from the importer of a module, provided in the form of Pair objects (:subs is shorthand for subs => True).

When the importer specifies use GTK::Simple :subs, it looks for a module with the pair's key as the name inside of the imported module. This is often a generated module thanks to the export trait. sub foo() is export :named-option {...} generates a my-scoped module Module::named-option that instructs the importer to include foo in its own my scope.

for GTK::Simple::.kv -> $name, $class {

This is an example of compositional concision right here. The dangling :: is shorthand for .WHO, which when called on a module returns the package meta-object of the module.1 Holding this meta-object, we can call .kv to get the keys (names) and values (package objects) of the packages within that scope.

Because we have loaded all (and only) our relevant classes into the GTK::Simple package scope, the meta-object for that package handily provides a .kv method that delivers the class names as keys and the actual class objects as keys, zipped together.

If there were anything else our scoped in GTK::Simple, it would be in this list too (module and class definitions are our scoped by default). So depending on how complex your module is, you might have to write some filters or guards to make sure you were only processing the actual class objects.

Thanks to a meta-object system designed to be both easy to use and transparently hidden from view until needed, there's nothing to make things feel complicated here.2

my $sub-name = '&' ~ ($name ~~ / (<:Lu><:Ll>) /).values.map({ .Str.lc }).join("-");

This line makes me happy. The crux of the code lies in the advanced Raku regex syntax. <:Lu> and <:Li> are built in character classes in Raku that represent uppercase and lowercase characters, respectively. These are Unicode aware, so no worries there.

The rest is straight-forward: take the .values of the Match object, map them to strings while also lower-casing them and then join the resulting list of strings into a compound string in kebab-case.

We prepend the & sigil to the name in order to register it as a subroutine when it is brought into the importer's scope.

OUR::{ $sub-name } := sub (|c) { $class.new(|c) };

Here's where the actual mapping takes place. OUR::{ $sub-name } is creating a (name for a) sub dynamically in the shared OUR scope between the importer and the imported modules. The import process is such that these dynamically defined modules become available in the scope of the importing module (it's MY scope).

sub (|c) { $class.new(|c) } says create an anonymous subroutine that passes it's arguments (which are, of course, an object themselves) exactly as-is to the constructor of $class.

Not necessarily obvious

Now, I do understand that there are aspects of this code that are not obvious: the dangling :: syntax, the necessity of making a my scoped module with a special name of EXPORT, and perhaps the use of the OUR:: package accessor.

It's probably not code that I will necessarily write directly from memory next time I want to dynamically load a bunch of subs into an importer's scope.

At the same time, I would argue that all of these examples very idiomatic to the language: little 'escape hatches' into deeper layers of the language that work as expected once you encounter them. All of this without muddying the waters of what's "in" a module by, for instance, providing methods directly on package objects that would provide similar functionality.3

Raku really is a well-thought over programming language. It's also massive, which can be intimidating. It helps to manage this massivity when pieces fit together in carefully planned ways. In other words, once I had that dangling ::/WHO object to call .kv on, everything else I had to do just fell together.

Testing this

In order to ensure that everything was indeed working in a systematic way, I needed to write some tests.

use GTK::Simple :subs;

# Other modules are pulled into GTK::Simple namespace by now that we do not want to test
sub skip-test($name) {
    state $skip-set = set '&' X~ <app simple raw native-lib g-d-k common property-facade>;
    $name (elem) $skip-set
}

for GTK::Simple::.kv -> $name, $class {
    my $sub-name = '&' ~ ($name ~~ / (<:Lu><:Ll>*)* /).values.map({ .Str.lc }).join("-");
    next if skip-test($sub-name);

    my $widget;
    lives-ok { $widget = ::{$sub-name}(:label("For Button(s)"), :uri("For LinkButton")) },
        "There is a subroutine in scope called '$sub-name'";
    ok $widget ~~ $class, "'$sub-name' returns a { $class.^name } object";
}

Here I created a state variable (only defined once per scope, so we don't re-do the variable assignment with each call) to hold a set of names that we want to skip. If the argument is an element of said set, skip the test.

::{$sub-name}

Once again :: shows up to be a Do-What-I-Mean shortcut. This time it is for the package of our current lexical scope. So it means essentially the same thing: let me access the package as if it were a "stash", giving us semantics equivalent to hashes (eg, ::{$sub-name}). A lexical lookup is begun until the subroutine is found in lexical scope. Since the import process implanted it there in lexical scope, it returns with the sub stored with name $sub-name.

We pass two positional arguments to the constructor because they are either required for some constructors and or ignored by those that don't require them.

Conclusion

That wraps up a fairly lengthy post about a very short snippet of code. Please stay tuned for other posts about changes that were made such that my awesome, funky dice roller could exist in ease and comfort.

Footnotes

  1. When .WHO is called on an initialized object, it returns it's respective class object.↩

  2. Compare that to this Java or even that Java without the Reflections library.↩

  3. In Ruby, the same would be done with MyModule.constants.select {|c| MyModule.const_get(c).is_a? Class}. It's MyModule.constants and MyModule.const_get that in my opinion shows a muddier abstraction between package objects and the meta-objects that govern and represent them.↩