> 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