raku

Separating operator definitions for a class to other files and using them


I have 4 files all in the same directory: main.rakumod, infix_ops.rakumod, prefix_ops.rakumod and script.raku:

Why 3 files not 1? Since class definition might be long and separating overloaded operator definitions in files seemed like a good idea for writing tidier code (easier to manage).

e.g.,

# main.rakumod
class A {
    has $.x is rw;
}
# prefix_ops.rakumod
use lib ".";
use main;

multi prefix:<++>(A:D $obj) {
    ++$obj.x;
    $obj;
}

and similar routines in infix_ops.rakumod. Now, in script.raku, my aim is to import main module only and see the overloaded operators also available:

# script.raku
use lib ".";
use main;

my $a = A.new(x => -1);
++$a;

but it naturally doesn't see ++ multi for A objects because main.rakumod doesn't know the *_ops.rakumod files as it stands. Is there a way I can achieve this? If I use prefix_ops in main.rakumod, it says 'use lib' may not be pre-compiled perhaps because of circular dependentness


Solution

  • it says 'use lib' may not be pre-compiled

    This has now been fixed per @codesections++'s comment below.

    perhaps because of circular dependentness

    No. use lib can only be used by the main program file, the one directly run by Rakudo.

    Is there a way I can achieve this?

    Here's one way.

    We introduce a new file that's used by the other packages to eliminate the circularity. So now we have four files (I've rationalized the naming to stick to A or variants of it for the packages that contribute to the type A):

    1. A-sawn.rakumod that's a role or class or similar:

      unit role A-sawn;
      
    2. Other packages that are to be separated out into their own files use the new "sawn" package and does or is it as appropriate:

      use A-sawn;
      
      unit class A-Ops does A-sawn;
      
      multi  prefix:<++>(A-sawn:D $obj) is export { ++($obj.x) }
      multi postfix:<++>(A-sawn:D $obj) is export { ($obj.x)++ }
      
    3. The A.rakumod file for the A type does the same thing. It also uses whatever other packages are to be pulled into the same A namespace; this will import symbols from it according to Raku's standard importing rules. And then relevant symbols are explicitly exported:

      use A-sawn;
      use A-Ops;
      sub EXPORT { Map.new: OUTER:: .grep: { .key ~~ /'fix:<'/ } }
      
      unit class A does A-sawn;
      has $.x is rw;
      
    4. Finally, with this setup in place, the main program can just use A;:

      use lib '.';
      use A;
      
      my $a = A.new(x => -1);
      say $a++; # A.new(x => -1)
      say ++$a; # A.new(x => 1)
      say ++$a; # A.new(x => 2)
      

    The two main things here are:

    Footnotes

    [1] As things stands this technique means coming up with a new name that's different from the one used by the consumer of the new type, one that won't conflict with any other packages. This suggests a suffix. I think -sawn is a reasonable choice for an unusual and distinctive and mnemonic suffix. That said, I imagine someone will eventually package this process up into a new language construct that does the work behind the scenes, generating the name and automating away the manual changes one has to make to packages with the shown technique.

    [2] A critically important point is that, if a sub EXPORT is to do what you want, it must be placed outside the package definition to which it applies. And that in turn means it must be before a unit package declaration. And that in turn means any use statement relied on by that sub EXPORT must appear within the same or outer lexical scope. (This is explained in the doc but I think it bears summarizing here to try head off much head scratching because there's no error message if it's in the wrong place.)

    [3] As with the circularity saw aspect discussed in footnote 1, I imagine someone will also eventually package up this import-and-export mechanism into a new construct, or, perhaps even better, an enhancement of Raku's built in use statement.