Search code examples
moduleexportraku

How to set a named parameter to an exported sub when importing a module?


unit module My::Show;

sub show (:$indent = False) is export {
    if $indent {
        say "    Showing with indentation.";
    } else {
        say "Showing without indentation.";
    }
}

When importing this code, I would like to specify any of these three:

use My::Show :indent;
use My::Show :!indent;
use My::Show;

Clarification (thanks @raiph): The :$indent parameter of the show sub has a default value. The user of the module should have the option to specify that default value on the use line.

How to do so?


Solution

  • TL;DR You need to write and suitably invoke an EXPORT subroutine.¹²³⁴⁵⁶⁷

    Proof-of-concept

    • used.rakumod:

      sub EXPORT ([:indent($default-indent) = False]) {
      
        sub show (:$indent = $default-indent) is export { $indent }
      
        Map.new: '&show' => &EXPORT::DEFAULT::show;
      
      }
      
    • main.raku:

      use lib '.';
      
      use used [:indent],;
      
      say show; # True
      

    Explanation of used.rakumod

    If a module⁹ is used, and it has an EXPORT sub (at the compunit⁹ level), then:

    • The EXPORT sub is called.

    • Any positional arguments provided with the use statement are passed to the EXPORT sub, and this can be used to achieve the effect you're after, within reason.⁴⁵⁶⁷


    The used module is nothing but an EXPORT subroutine. It starts:

    sub EXPORT ([:indent($default-indent) = False]) {
    

    The signature and its behavior need explanation:

    • EXPORT subs only bind to positional arguments.⁴

    • It's obviously desirable to be able to specify the default value for the named argument of show by using what looks as close to passing that same named argument as possible. I'm mimicking that by writing [:indent] in a use used [:indent],; statement.⁴

    • Given that the passed positional argument is an array, the EXPORT sub needs to deconstruct that positional argument to extract the inner argument inside the outer positional argument. That's what the [...] does; it behaves as a sub-signature that "deconstructs" the positional array argument.

    • The deconstructing parameter :indent is a named parameter, turning what we had to pass as an element of a positional array back into the form we want in the EXPORT sub.

    • In addition, I take advantage of now having a true named parameter to also give the parameter an alias: :$default-index. (You write aliased parameters by writing them in the form :foo(:bar(:$baz)) such that only the inner identifier/alias has a sigil, though the upshot is that you get $foo, $bar, and $baz aliases.)

    • Specifying an alias tidies up a wrinkle that has to be addressed anyway: while my approach allows writing :indent as part of the use statement, there needs to be an alias for :$indent because otherwise we can't make it the default value for the show sub's :$indent parameter:

      sub show (:$indent = $default-indent) is export { $indent }
      

      Hopefully you can see why it wouldn't work if I wrote :$indent = $indent as the show sub's signature.


    The above may seem pretty complex. (Because it is.) I've written another run down in yet another footnote that might help.⁸


    Finally there's:

    Map.new: '&show' => &EXPORT::DEFAULT::show
    

    I will rely on the relevant doc to guide you to understanding what you need to understand about that line. I suggest that you ignore it on a first read of this answer.

    (In particular you may find the dual mentions of defaults -- $default-index and DEFAULT confusing. Suffice to say they've got nothing to do with each other.)

    Explanation of main.raku

    All that's left is to use the module⁹ and call show.

    Hopefully you know the use lib '.'; line in my main.raku is just so the code works without me needing to explain (to someone cutting and pasting my code to run it) how to set up and use a library directory.

    The next line is a use statement that passes a positional argument, as previously discussed:

    use used [:indent],;
    

    Finally proof it works (though it's only very lightly tested!):

    say show; # True
    

    Footnotes

    ¹ I'm writing this answer with an air of authority, as if it was definitely useful and right. I find that helps make it easier to read. However, the reality is that, if my guess in my comment on your Q about what you wanted was wrong, this answer will likely be useless. And even if my guess was about right, this answer may still be useless, because what it deals with is close to the edge of my understanding and competence, and I may have stepped over the edge, and may even be wildly spinning my legs halfway across the cartoon canyon, without yet realizing I am.

    ² I'm just providing the simplest proof-of-concept I can think of to do what I think you want to be able to do, along with copious footnotes to cover caveats etc.

    ³ An EXPORT sub is the most powerful of the standard mechanisms for controlling "exporting and selective importing". It may be a surprise I'm guiding you to use an export tool, especially a heavy weight one. What you want doesn't seem to be about exporting -- you've already got an is export. Shouldn't that be enough? Nor does what you want seem to be about selective importing. You just want to import what the module had available for export, like normal, albeit declaring a parameter default for one of the exported subs. Can't be that difficult, right? Suffice to say, sub EXPORT is the mechanism via which one can do the fanciest things that can be done during export/import, and, as I currently understand things, what you want to do requires sufficiently fancy footwork that indeed a sub EXPORT is required.

    ⁴ In standard Raku, any pair (named argument) supplied at the top level of a use statement (eg use module :indent;) will be interpreted as an export/import "tag". (This explains why only positional parameters at the top level of the signature of EXPORT subs can ever bind to anything.) Furthermore, the usual ways to stop Raku treating a pair as a named argument and instead treat it as a positional argument (eg by putting it in parens) don't work with a use statement. Instead you have to do something like I did: putting the pair in an array literal ([:indent]) plus adding an extra comma outside the array literal to put it into an outer list ([:indent],)).⁵

    ⁵ You could just give up on the idea of passing a pair, and instead just pass a non-pair value eg. use module 99;. (And modify the receiving EXPORT sub to match.) But I can see why you wanted to use a pair whose name/key exactly matched the named parameter you wanted to specify the default for, so I've shown you what you have to do to make that work in ordinary circumstances.⁶

    ⁶ Raku is in principle an arbitrarily programmable language (with support for userland modification of Raku's syntax and/or semantics). So you could in principle create a module/pragma (or use an existing one if one existed) that changed Raku so that use statements following use of the posited module/pragma would allow passing of top level named arguments to the EXPORT sub. So then you could presumably write use module :indent;, or something similar, instead of what I came up with. But I consider further discussion of such an approach is way beyond both the scope of a reasonable answer to your Q and my $paygrade.

    ⁷ The flip side to named arguments of a use statement being export/import tags is that any positional arguments of a use statement are passed to the used module's EXPORT sub.

    ⁸ The arrangements I've made to be able to specify the default value for the show sub's :$indent parameter are surprisingly complex. Here's another run down as a refresher:

    1. The use statement use used [:indent],; passes the positional argument [:indent] to the EXPORT sub.

    2. The sub-signature in the EXPORT routine's signature deconstructs the incoming array, extracting the :indent pair and transforming the positional argument [:indent] into the named parameter :$indent.

    3. The named parameter has an alias :$default-indent. This is so it can be referred to as $default-indent without the ambiguity of referring to it as $indent. It has a default default value (False) so that if no value is provided in a use statement for the used module, there's still a default value to pass on to the show sub.

    4. The default value for the named $indent parameter of the show sub is $default-indent.

    ⁹ When I use the term "module" in this answer I don't (necessarily) mean a module. A module might be available as a "module" but what I mean by "module" in this answer is what a use statement uses, namely a compunit.