http://qs1969.pair.com?node_id=11145036

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.

Replies are listed 'Best First'.
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,

    Athanasius <°(((><contra mundum Iustus alius egestas vitae, eros Piratica,

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.

    — Ken

      May be able to leverage Validate::Tiny to also define these parameters in a formalized/structured way.
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.
      I think the OP was talking about a CGI or similar web code.

      Though that can also be called from CLI ...

      Cheers Rolf
      (addicted to the Perl Programming Language :)
      Wikisyntax for the Monastery

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 )

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    Wikisyntax for the Monastery

    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.

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
Re: Deferring variables
by tybalt89 (Monsignor) on Jun 25, 2022 at 19:17 UTC

    Keeping the default in the %par hash could be one way. Seems relatively clean to me...

    #!/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

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} );

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    Wikisyntax for the Monastery