Re: MAIN(*magic)

[email protected] (Bruce Gray)
Newsgroups perl.perl6.users
Message-ID <[email protected]>

> On Mar 14, 2024, at 21:57, Mark Devine <[email protected]> wrote:
> 
> Rakoons,
>  
> I keep running into a space with Raku where it would be nice to have some magic to reduce boilerplate.  I often make roles & classes with interface options that flow from a consuming script’s MAIN arguments, then the first bit of code at the top the MAIN block, then into instantiation.  If a person makes nice utility libraries with lots of options for the user & employs them a lot, the time consumed typing out the boilerplate adds up & bloats the code.  Is there any way to shorten this pattern?  I was thinking about a ‘switchable’ trait for attributes (with the naivety of a schoolboy).
>  
> my class Output {
>     has $.csv      is switchable;
>     has $.html     is switchable;
>       .
>       .
>       .
>     has $.xml     is switchable;
> }
>  
> my class Timer {
>     has $.count    is switchable;
>     has $.expire   is switchable;
>     has $.interval is switchable;
> }
>  
> sub MAIN (
>     Output.switchables,     #= switchables from Class 'Output'
>     Timer.switchables,      #= switchables from Class 'Timer'
> ) {
>     my Timer $t    .= new: :$expire, :$interval, :$count;   # variables magically appear in scope
>     my Output $o   .= new: :$csv, :$html, … , :$xml;        # variables magically appear in scope
> }
>  
> I estimate 10-50 lines of boilerplate could be removed from most of my Raku scripts with something like that.  Unfortunately, I don’t possess any dark magic for such things or I’d put forward an attempt.
>  
> Thanks,
>  
> Mark

Like lizmat, I also see no way to achieve your goal using the "is switchable" approach you have posited.
However, the thought of removing so much boilerplate pricks my thumbs, Ostara approaches, and it just turned midnight here, so Dark Magic will be attempted.

Thoughts:
* You don't need to have a $expire variable, or materialize any variable to allow feeding a CLI argument to a class' `.new()` constructor. You can skim off the arguments from @*ARGS, the same way many of the Getopt:: modules do. If you need them later, you can get them from the built object, e.g. `$t.count`.
* I expect is is harmless to expose *all* of the BUILDable attributes of the interfaced object types (which we can get via introspection!), so no need to mark individual attributes; you could mark the class itself with `is switchable`. 
* We could automate that `is switchable` (now for the whole class instead of its attributes) with a role, but why? More flexible to wrap (via module) *any* class/module with an OO API, even if not your own code! Proof-of-concept below.

Outline:
	use Getopt::Attributes; # Not a real module (yet!)
	my $sc = auto-cli( SomeClass );
	sub MAIN ( ...just normal params, no need to list the params for all the objects built with `auto-cli`...  ) {
		... code here can use `$sc`, which was created using args from the command-line ...
	}


Test runs of code below:
$ raku poc_01.raku --count=5 --xml=foo.xml myfile.txt 
    Timer  object  Timer.new(count => 5, expire => Any, interval => Any)
    Output object  Output.new(csv => Any, html => Any, xml => "foo.xml", logger => "localhost")
    Remaining ARGS [myfile.txt]
    Filename       myfile.txt

$ raku poc_01.raku --count=5 --xml=foo.xml --logger=abc myfile.txt 
    Timer  object  Timer.new(count => 5, expire => Any, interval => Any)
    Output object  Output.new(csv => Any, html => Any, xml => "foo.xml", logger => "abc")
    Remaining ARGS [myfile.txt]
    Filename       myfile.txt

$ raku poc_01.raku myfile.txt 
    Timer  object  Timer.new(count => Any, expire => Any, interval => Any)
    Output object  Output.new(csv => Any, html => Any, xml => Any, logger => "localhost")
    Remaining ARGS [myfile.txt]
    Filename       myfile.txt



Actual proof-of-concept in a single file: poc_01.raku

# XXX In a fully-realized solution, this part would be in its own module.
#     If it was Getopt::Attributes , it would live in lib/Getopt/Attributes.rakumod
# unit class Getopt::Attributes:ver<0.0.1>;

# Given a class, `auto-cli` introspects that class for its public accessors, knowing that those are valid named parameters for the class constructor. It then does a scan on @*ARGS, removing arguments that could be intended for that class. This must happen *before* MAIN runs, or MAIN will throw a USAGE error on the unexpected (from MAIN's perspective) arguments. The removed command-line arguments are merged with any provided defaults, and an instance of that class with the merged named arguments.
sub auto-cli ( Any:U $class, *%defaults ) is export {
    my %opt = %defaults;

    my @attrs = $class.^attributes
                      .grep( *.has_accessor )
                      .map(  *.name.subst(/^\S\S/) );
    my $attr_re = /@attrs/;

    for @*ARGS.keys.reverse -> $i {
        if @*ARGS[$i] ~~ / ^ '--' ($attr_re) '=' (\S+) $ / {
            my ($key, $value) = ~$0, (+$1 // ~$1);
            @*ARGS.splice($i, 1);
            %opt{$key} = $value;
        }
    }

    return $class.new: |%opt;
}



# Works with imported and in-line classes/modules, but I commented out since we are keeping this demo all-in-one-file.
# use lib '.';
# use lib './lib';
# use Timer;

class Timer {
    has $.count;
    has $.expire;
    has $.interval;
}
my class Output {
    has $.csv;
    has $.html;
    has $.xml;
    has $.logger;
}

# XXX This would import the `auto-cli` function.
# use Getopt::Attributes;

# These must run before MAIN.
my $t = auto-cli( Timer );
my $o = auto-cli( Output, :logger('localhost') ); # default if --logger not specified on command-line

sub MAIN ( $filename ) {
    say 'Timer  object  ', $t;
    say 'Output object  ', $o;
    say 'Remaining ARGS ', @*ARGS;
    say 'Filename       ', $filename;
}

-- 
Hope this helps,
Bruce Gray (Util of PerlMonks)
https://www.enotes.com/shakespeare-quotes/by-pricking-my-thumbs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.