9

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

  • main module has a class definition (class A)
  • *_ops modules have some operator routine definitions to write, e.g., $a1 + $a2 in an overloaded way.
  • script.raku tries to instantaniate A object(s) and use those user-defined operators.

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

  • I admit I am a beginner to the language and very confused about `use`, `import`, `require`, `need`, `module` etc even after looking documentation multiple times. I don't know what to use when. It may be that it assumes some Perl5 knowledge for these but perlmod is also confusing for me –  Sep 01 '21 at 17:22
  • I've got an answer ready, but am working on improving it. Do you want me to publish what I have already or wait a while (maybe a half hour)? – raiph Sep 01 '21 at 17:25
  • @raiph however you wish, thank you. –  Sep 01 '21 at 17:27

2 Answers2

3

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

  • The word "may" is ambiguous. Actually it cannot be precompiled.

  • The message would be better if it said something to the effect of "Don't put use lib in a module."

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: /'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:

  • Introducing an (empty) A-sawn package

    This type eliminates circularity using the technique shown in @codesection's answer to Best Way to Resolve Circular Module Loading.

    Raku culture has a fun generic term/meme for techniques that cut through circular problems: "circular saws". So I've used a -sawn suffix of the "sawn" typename as a convention when using this technique.[1]

  • Importing symbols into a package and then re-exporting them

    This is done via sub EXPORT { Map.new: ... }.[2] See the doc for sub EXPORT.

    The Map must contain a list of symbols (Pairs). For this case I've grepped through keys from the OUTER:: pseudopackage that refers to the symbol table of the lexical scope immediately outside the sub EXPORT the OUTER:: appears in. This is of course the lexical scope into which some symbols (for operators) have just been imported by the use Ops; statement. I then grep that symbol table for keys containing fix:<; this will catch all symbol keys with that string in their name (so infix:<..., prefix:<... etc.). Alter this code as needed to suit your needs.[3]

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.

raiph
  • 31,607
  • 3
  • 62
  • 111
  • @Hanselmann I've now added the main improvement I was working on, but knew it would take a while to complete. I've gotta go away now, perhaps for several days, but please ask questions about any of it via comments here and I or others will follow up in due course. I enjoyed writing this answer; thanks for asking your question. – raiph Sep 01 '21 at 18:52
  • 1
    I agree that the error message there is Less Than Awesome, so I submitted [a PR](https://github.com/rakudo/rakudo/pull/4510) to add the info you mentioned. @Hanselmann, thanks for bringing this up – that's how the error messages get better :) – codesections Sep 01 '21 at 20:21
  • @raiph thanks for the answer. I didn't know or read `use lib` pragma shouldn't be used everywhere. As for the solution, do you know if I *have to* use another class to make this work? (by the way, I'm happy to use this solution). Also, I add the operators to Map manually right? Though I can read & parse the *_ops files and programatically construct the Map I think. –  Sep 02 '21 at 13:06
  • @codesections Oh that was a fast development, thanks for making the error message more descriptive for the future. I still need to read things for the "why" of the error message, but I wont hopefully attempt to `use lib` in a module. By the way, by "module", does it mean either a .rakumod file or a file with `module` keyword in it? –  Sep 02 '21 at 13:14
  • 1
    @Hanselmann I've switched to using a `role` as per .@codesection's original SO, suggested a naming convention, and provided one way to generate the symbol reexporting programmatically. The word "module" in the context of `use lib` means any `.rakumod` which is *any* package -- declared with `package`, `module` , `class`, `role`, `grammar` etc. For this Gordian Knot cutting technique, the module must be one that can `does` a CutKnot role or `is` a `CutKnot` class (which counts out plain `package`s or `module`s). – raiph Sep 02 '21 at 21:04
  • (I've switched to "sawn" instead of "cut knot" as the metaphor used in the answer.) – raiph Sep 14 '21 at 20:29
  • @raiph thanks. Do you know if I *have to* use another class to make this work? (by the way, I'm happy to use this solution)? A reason for asking is `Ops` module don't compile on its own and says `Could not find Super in: ...` where "Super" is `A-sawn` here. I then add `use lib "."` to it and it compiles. –  Sep 15 '21 at 07:47
  • ... I'm using VSCode with [this "perl 6 language support"](https://github.com/scriplit/vscode-languageserver-perl6) extension that *"shows errors as you type"*. But without `use lib` in it, it stucks at that "couldn't find" error and don't see possible others. But when I need to run main program I need to manually remove "use lib" in `Ops.rakumod`. Similar issue arises when I also need to check `A.pm6` but then `use lib cannot be precompiled` error shows up, and then I remove `use lib` in `Ops.pm6` to see mistakes in this, etc... –  Sep 15 '21 at 07:50
  • (and also I don't understand why you switched to a role instead of a class as it was the case, if it isn't too much to ask) –  Sep 15 '21 at 07:52
  • That's *another* circularity, a [*precompilation*](https://docs.raku.org/language/faq#index-entry-Precompile_(FAQ)) one. Some packages have been precompiled with old dependencies, but Rakudo doesn't realize. To fix *that* circularity problem in one go, find and delete all the files in [the relevant `.precomp` directory or directories](https://docs.raku.org/language/compilation#Precomp_stores) . (They're hidden by default for most directory list tools, but hopefully you know about that.) Things will then be slow for a while, then steadily speed up, as the precompilation cache refills over time. – raiph Sep 15 '21 at 10:21
  • I recognize it would have been annoying to hit a second circularity, but please forgive me for finding it amusing. I used the "cut knot" metaphor in a prior version of this answer. It was a reference to the story of Alexander the Great solving the riddle of how to undo the "Gordian Knot", something clever folk had failed to solve. He thought up and delivered his simple solution in a single second. I decided to drop the "cut knot" metaphor for my answer for various reasons, but the solution for this *precomp* circularity is indeed a "cut knot" one that's just a single, simple stroke! :) – raiph Sep 15 '21 at 10:33
  • The insight underlying [composition over inheritance](https://www.google.com/search?q=%22composition+over+inheritance%22) suggests it's generally best to declare most "objecty" types as *roles*, not *classes*, because declaring and using *classes* requires inheritance whereas a role cannot be inherited *from*, only composed, and can declare a completely working "model" of an object, without inheriting from any other, only composing them. You can still ["type pun" a role into a class](https://docs.raku.org/syntax/role#Automatic_role_punning) to create an instance, so you don't lose anything. – raiph Sep 15 '21 at 10:55
  • @Hanselmann My last few comments are simplifications of complex topics. Deleting a precomp directory is a simple solution but the consequence is that *all* your code will now have to be recompiled, which might take a lot of time. But once that's done, both the original problem (compile fails) and consequent problems (slow compilation) will be history. (This distils what was supposedly great, and definitely terrible, about Alexander. Winning wars is "great". Wars are terrible.) To *use* a role, one must `.new` it (or mix it into an existing instance) -- which will inherit from `Any` and `Mu`. – raiph Sep 15 '21 at 11:08
  • thanks again. But after deleting .precomp dirs, compilation of e.g., `Ops.pm6` on its own is still failing. And I fail to see how deleting .precomp files helps. My understanding: When I say `use A-sawn`, it looks some predetermined directories (e.g., C:\rakudo\...) but it's not there and it complains. Only when I say `use lib`, the module on its own is free of that error and I (or the aforementioned extension) can compile it and see other syntax errors etc. –  Sep 15 '21 at 12:05
  • also, do you happen to know if I *have to* use another role/class to make this work? (by the way, I'm happy to use this solution). –  Sep 15 '21 at 12:06
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/237132/discussion-between-raiph-and-hanselmann). – raiph Sep 15 '21 at 14:05
0

Hi @hanselmann here is how I would write this (in 3 files / same dir):

Define my class(es):

# MyClass.rakumod
unit module MyClass;

class A is export {
    has $.x is rw; 
}

Define my operators:

# Prefix_Ops.rakumod
unit module Prefix_Ops;

use MyClass;

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

Run my code:

# script.raku
use lib ".";
use MyClass;
use Prefix_Ops;

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

say $a.x;   #0

Taking my cue from the Module docs there are a couple of things I am doing different:

  • Avoiding the use of main (or Main, or MAIN) --- I am wary that MAIN is a reserved name and just want to keep clear of engaging any of that (cool) machinery
  • Bringing in the unit module declaration at the top of each 'rakumod' file ... it may be possible to use bare files in Raku ... but I have never tried this and would say that it is not obvious from the docs that it is even possible, or supported
  • Now since I wanted this to work first time you will note that I use the same file name and module name ... again it may be possible to do that differently (multiple modules in one file and so on) ... but I have not tried that either
  • Using the 'is export' trait where I want my script to be able to use these definitions ... as you will know from close study of the docs ;-) is that each module has it's own namespace (the "stash") and we need export to shove the exported definitions into the namespace of the script
  • As @raiph mentions you only need the script to define the module library location
  • Since you want your prefix multi to "know" about class A then you also need to use MyClass in the Prefix_Ops module

Anyway, all-in-all, I think that the raku module system exemplifies the unique combination of "easy things easy and hard thinks doable" ... all I had to do with your code (which was very close) was tweak a few filenames and sprinkle in some concise concepts like 'unit module' and 'is export' and it really does not look much different since raku keeps all the import/export machinery under the surface like the swan gliding over the river...

librasteve
  • 6,832
  • 8
  • 30
  • oh and now I see that from your comment to Ralph answer that you want to NOT explicitly import the overloaded operators ... but have these grandfathered into your script namespace via the class import ... this will not work since you need to explicitly use the module where you export the methods in the scope where they are used... AFAIK that is a language design trade off intended to avoid overloading action at a distance that can lead to confusion – librasteve Sep 01 '21 at 20:55
  • the way I solve this in cf. [Physics::Measure](https://github.com/p6steve/raku-Physics-Measure/blob/master/lib/Physics/Measure.rakumod) line 492 is to define the operators in the same module which defines the class ... so you use the class once and get the operators thrown in – librasteve Sep 01 '21 at 20:56
  • so reading @raiphs new version, I see that he has solved the grandfathering 'problem' by introducing a SuperClass and then 'forcibly' EXPORTing the prefix op across from one module to the other --- well this is the "makes the hard things doable" in action and raku will give you all the rope you want --- in contrast mine is the vanilla way that sticks to the basics ... you have the power and responsibility ;-) – librasteve Sep 01 '21 at 21:19
  • 1
    Hi @p6steve (and .@codesection). I originally wrote the same code as in this answer. But I wanted to add the `SuperClass` solution below it. Then I wondered if .@Hanselmann would appreciate seeing the simpler 3 file answer. I ended up posting that as [my original answer](https://stackoverflow.com/revisions/69018308/1). .@Hanselmann commented on it, seeking the `SuperClass` solution, without upvoting the 3 file solution. So I upvoted their comment, deleted my answer, and posted just the 4 file solution. And guess what my `SuperClass` solution was derived from? An earlier .@codesection answer! – raiph Sep 01 '21 at 23:21
  • 1
    Imo the re-exporting is too hard and error prone, and there ought to be a much simpler way to do it than the manual way I did it. I'm confident there *will* be much simpler ways to do it one day, but there's no hurry. As you note, and I've shown, there pretty much always is *a* way to do anything, and Raku has all the right pieces in place to make it easy for devs to nicely package anything up in a convenient way by creating modules and/or evolving the language, as they see fit when they have tuits / consensus. All in good time... – raiph Sep 02 '21 at 00:01
  • 1
    hi p6steve, thanks for the answer. As noted, I'd like the user to only `use A;` and see `++` etc. avaialble (the "user" is again me, of course...). I (try to) learn from your items: 1) I see MAIN is special, will keep in mind. 2) [Documentation](https://docs.raku.org/language/module-packages#What_happens_if_I_omit_module?) says something about it, perhaps accessing is different, not sure. 4-6) thanks for these remarks. –  Sep 02 '21 at 08:46
  • 1
    @Hanselmann thanks for the fb... i will leave my answer in for later readers to compare to the right one. imo this kind of action at a distance stuff ought to be a bit hard (but doable) to nudge meat + potatoes coders like me to stay (lexically) local – librasteve Sep 02 '21 at 17:28