> On Mar 14, 2024, at 21:57, Mark Devine <m...@markdevine.com> 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

Reply via email to