Chuma has asked for the wisdom of the Perl Monks concerning the following question:
I have in my program a number of parameters that can be set by the user, which I store in a hash %par. Now the thing is, some of these parameters come in more and less specific versions.
An example: We have a parameter "margin-left". In some cases, the user hasn't set a specific value for it; it then defers to the more general "margin-horizontal". If the user hasn't set a specific value for that, it in turn defers to the more general "margin".
In my current program, those "unspecified" parameters are set to "a" (for "automatic"). So when the time comes to actually draw the left margin, the program does something like
if($par{'margin-left'} ne 'a'){
$actual = $par{'margin-left'}
}elsif($par{'margin-horizontal'} ne 'a'){
$actual = $par{'margin-horizontal'}
}
else{$actual = $par{'margin'}}
As you can see, it's a bit of a bunch every time I just need to read the margin-left value. I could of course pack all that into a "get" sub, that would save on typing, but it still seems like a slow and clumsy method.
There is a "set" sub, so in principle I could use that to track and update everything, checking whenever a parameter is set whether it has any parent / dependant parameters, but that's also quite a lot of code, and it's redundant for all the parameters that don't have any such behaviour.
Is there a better way? Maybe something with referencing / aliasing? Ideally keeping all the inner working in the "set" sub, so I can just use $par{'margin-left'} and get the right thing.
Re: Deferring variables
by Athanasius (Archbishop) on Jun 25, 2022 at 14:24 UTC
|
Hello Chuma,
A quick observation: if you change the “unspecified” value from 'a' to undef, you can simplify this:
if($par{'margin-left'} ne 'a'){
$actual = $par{'margin-left'}
}elsif($par{'margin-horizontal'} ne 'a'){
$actual = $par{'margin-horizontal'}
}
else{$actual = $par{'margin'}}
to this:
$actual = $par{'margin-left'} // $par{'margin-horizontal'} // $par{'ma
+rgin'};
which is easier to comprehend as well as to type. See Logical Defined Or.
Hope that helps,
| [reply] [d/l] [select] |
Re: Deferring variables -- Getopt::Long
by Discipulus (Canon) on Jun 25, 2022 at 16:41 UTC
|
Hello Chuma,
> number of parameters that can be set by the user
maybe I'm confused by your post but, if I understand it correctly, you are handling user provided params, if so I'd go totally other way: set defaults, overwrite them with user provided values, validate evrything with your own logic.
Then in the rest of your program you can safely use just one variable with the right name because it was a default or user provided or set by you following your logic
use 5.0010;
use strict;
use warnings;
use Data::Dumper;
use Getopt::Long;
# provide sane defaults
my %defaults = (
'margin-left' => undef,
'margin-horizontal' => undef,
'margin' => 5,
);
# use then user provided parameters
unless ( GetOptions (
'margin-left=i' => \$defaults{'margin
+-left'},
'margin-horizontal=i' => \$defaults{'margin
+-horizontal'},
'margin=i' => \$defaults{'margin
+'},
)) {die "$0 usage..."}
# validate them once for all
validate_parameters();
print Dumper \%defaults;
sub validate_parameters{
# here the logic of precedence for all paramenters
$defaults{'margin-left'} //= $defaults{'margin-horizontal'} // $de
+faults{'margin'};
}
__END__
io@COMP:C>perl params.pl
$VAR1 = {
'margin-horizontal' => undef,
'margin' => 5,
'margin-left' => 5
};
io@COMP:C>perl params.pl --margin-left 33
$VAR1 = {
'margin-horizontal' => undef,
'margin-left' => 33,
'margin' => 5
};
io@COMP:C>perl params.pl --margin-horizontal 4242
$VAR1 = {
'margin-horizontal' => 4242,
'margin-left' => 4242,
'margin' => 5
};
L*
There are no rules, there are no thumbs..
Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
| [reply] [d/l] |
|
| [reply] |
Re: Deferring variables
by kcott (Archbishop) on Jun 25, 2022 at 21:51 UTC
|
G'day Chuma,
I'd define your parameters and default values in a separate file.
For this example, I've used JSON:
$ cat pm_11145036_parm_defaults.json
{
"margin-left" : [ null, "margin-horizontal" ],
"margin-right" : [ null, "margin-horizontal" ],
"margin-top" : [ null, "margin-vertical" ],
"margin-bottom" : [ null, "margin-vertical" ],
"margin-horizontal" : [ null, "margin" ],
"margin-vertical" : [ null, "margin" ],
"margin" : [ 10, null ]
}
The following code is a rough example of how that might be used.
#!/usr/bin/env perl
use strict;
use warnings;
use autodie;
use constant {
VALUE => 0,
DEFER => 1,
};
use JSON;
my $json_file = 'pm_11145036_parm_defaults.json';
my $par = decode_json(
do { local $/; open my $fh, '<', $json_file; <$fh>; }
);
sub _get {
my ($key) = @_;
if (! defined $par->{$key}[VALUE]) {
_get($par->{$key}[DEFER])
}
else {
return $par->{$key}[VALUE];
}
}
sub _set {
my ($key, $value) = @_;
if (! defined $par->{$key}[DEFER]) {
warn "Can't change factory default: '$key'.\n";
}
else {
$par->{$key}[VALUE] = $value;
}
return;
}
# Some examples of usage:
print 'margin-left = ', _get('margin-left'), "\n";
_set('margin-horizontal', 20);
print 'margin-left = ', _get('margin-left'), "\n";
_set('margin-left', 30);
print 'margin-left = ', _get('margin-left'), "\n";
print 'margin = ', _get('margin'), "\n";
_set('margin', 50);
print 'margin = ', _get('margin'), "\n";
Output:
margin-left = 10
margin-left = 20
margin-left = 30
margin = 10
Can't change factory default: 'margin'.
margin = 10
Notes:
-
Keeping your data separate means it can reused in all of your programs:
no need to tweak %par in multiple scripts
(potentially a lot of work; certainly error-prone).
-
When a user has set preferences, these can be stored in a separate file for reuse across all programs;
e.g. parm_USERNAME.json.
-
For further reuse, put most of that code in a module;
then each program just needs something like use Chuma::Layouts;.
-
I only used JSON as an example.
It's not intended as a recommendation:
pick whatever you want.
| [reply] [d/l] [select] |
|
May be able to leverage Validate::Tiny to also define these parameters in a formalized/structured way.
| [reply] |
Re: Deferring variables
by LanX (Saint) on Jun 25, 2022 at 13:40 UTC
|
We need more details for a good answer, e.g. you didn't show us your set() routine.
From the example demonstrated I'd say use $par{margin} and set it to the users input, like -horizontal
another approach is to grep a list of parameters, and take the first positive
($actual) = grep { $par{$_} ne "a" } qw/margin-left margin-horizontal margin/
(that's actually better done with first from List::Util )
update
debugger-demo with perl -de0
DB<21> $par{"margin-left"} = "a"
DB<22> ($actual) = grep { $par{$_} ne "a" } qw/margin-left margin-ho
+rizontal margin/
DB<23> p $actual
margin-horizontal
update
If you need to do it for different head-parameters like "padding" and so on, try
$chain{margin} = [qw/margin-left margin-horizontal margin/ ];
# etc
($actual{$head}) = grep { $par{$_} ne "a" } @{ $chain{$head} };
and let $head loop over "margin", "padding", a.s.o. | [reply] [d/l] [select] |
Re: Deferring variables
by Yaribz (Beadle) on Jun 25, 2022 at 16:52 UTC
|
Isn't it a typical use case of tied hash ? This code for example should do exactly what you want, without requiring any get or set function:
use warnings;
use strict;
package ParamsHash;
require Tie::Hash;
our @ISA='Tie::StdHash';
use constant { UNDEF_VAL => 'a' };
# configure your parameter inheritance here:
our @GLOBAL_PARAMS=(qw'margin padding');
our %SUB_PARAMS=(horizontal => [qw'left right'],
vertical => [qw'top bottom']);
my %INHERIT;
foreach my $param (@GLOBAL_PARAMS) {
foreach my $subp (keys %SUB_PARAMS) {
map {$INHERIT{"$param-$_"}="$param-$subp"} @{$SUB_PARAMS{$subp}};
$INHERIT{"$param-$subp"}=$param;
}
}
sub FETCH {
my ($this,$key)=@_;
my $val=$this->{$key};
return $val if(defined $val && $val ne UNDEF_VAL);
my $inherited=$INHERIT{$key};
return UNDEF_VAL unless(defined $inherited);
return $this->FETCH($inherited);
}
You can test it by adding this code at the end of the file for example:
package main;
my %par;
tie(%par,'ParamsHash');
print "Initial values:\n";
printSomeParams();
print "Setting 'margin' to 4:\n";
$par{margin}=4;
printSomeParams();
print "Setting 'margin-horizontal' to 8:\n";
$par{'margin-horizontal'}=8;
printSomeParams();
print "Setting 'margin-left' to 3:\n";
$par{'margin-left'}=3;
printSomeParams();
sub printSomeParams {
map {printParam($_)} (qw'
margin
margin-horizontal
margin-left
margin-right
')
}
sub printParam { print " $_[0] = $par{$_[0]}\n" }
This should give you following result:
Initial values:
margin = a
margin-horizontal = a
margin-left = a
margin-right = a
Setting 'margin' to 4:
margin = 4
margin-horizontal = 4
margin-left = 4
margin-right = 4
Setting 'margin-horizontal' to 8:
margin = 4
margin-horizontal = 8
margin-left = 8
margin-right = 8
Setting 'margin-left' to 3:
margin = 4
margin-horizontal = 8
margin-left = 3
margin-right = 8
| [reply] [d/l] [select] |
Re: Deferring variables
by tybalt89 (Monsignor) on Jun 25, 2022 at 19:17 UTC
|
#!/usr/bin/perl
use strict; # https://perlmonks.org/?node_id=11145036
use warnings;
my %par = map { split ' ', $_, 2 } split /\n/, <<END;
margin-left defaults to margin-horizontal
margin-horizontal defaults to margin
margin 20
margin-right 3
margin-bottom defaults to margin-right
END
use Data::Dump 'dd'; dd 'dump of %par', \%par;
sub get
{
my $want = shift;
$want = $1 while ($par{$want} // '') =~ /^defaults to (.+)/;
return $par{$want} // 'not defined';
}
for my $parameter ( sort 'margin-top', keys %par )
{
printf "%30s => %s\n", $parameter, get($parameter);
}
Outputs:
(
"dump of %par",
{
"margin" => 20,
"margin-bottom" => "defaults to margin-right",
"margin-horizontal" => "defaults to margin",
"margin-left" => "defaults to margin-horizontal",
"margin-right" => 3,
},
)
margin => 20
margin-bottom => 3
margin-horizontal => 20
margin-left => 20
margin-right => 3
margin-top => not defined
UPDATE: fixed typo in regex
| [reply] [d/l] [select] |
Re: Deferring variables
by LanX (Saint) on Jun 25, 2022 at 21:25 UTC
|
Sorry, I initially misunderstood your question, but reading some other replies helped.
(my main processor is (st)ill suffering from a virus attack ;-)
I tried aliasing, but no joy, after the first level you have far too much trouble maintaining.
I tried refs on hash elements, it worked but was a bit too fickle, if the data structure changed.
I tried just one data structure, denoting the links with special values like \"margin" , but again too much trouble maintaining such a mangled structure.
My suggestion is to keep it simple, an additional hash %def maps to the default and you use a simple recursive get-function.
use v5.12;
use warnings;
use Data::Dump qw/pp dd/;
my %def =
(
'margin-left' => 'margin-horizontal',
'margin-horizontal' => 'margin',
# ...
);
my %par =
(
'margin' => 42,
);
sub get {
my $key = shift
// return "UNDEFINED"; # or whatever error mecha
+nism you prefer
$par{$key} // get( $def{$key} )
}
say get("margin-left"); # 42
$par{'margin-horizontal'} = 666;
say get("margin-left"); # 666
undef $par{'margin-horizontal'};
say get("margin-left"); # 42
say get("TYPO"); # UNDEFINED
please note that I also preferred undef over "a"
Otherwise you can easily change the get code with a ternary
$par{$key} ne "a" ? $par{$key} : get( $def{$key} );
| [reply] [d/l] [select] |
|
|