Beefy Boxes and Bandwidth Generously Provided by pair Networks
Come for the quick hacks, stay for the epiphanies.
 
PerlMonks  

Question regarding proper handling of a Hash/HashRef structure in Moose-variants

by atcroft (Abbot)
on Dec 22, 2021 at 05:44 UTC ( [id://11139816]=perlquestion: print w/replies, xml ) Need Help??

atcroft has asked for the wisdom of the Perl Monks concerning the following question:

To the members of the body monktorate, greetings.

I seek your wisdom. I am trying to teach myself to use Moose (or the associated variations, such as Mouse, Moo, Mo, etc.). For my learning project, I wanted to do something I felt more useful than the Animal example, so for my purposes I wanted to represent a particle. I created a StationaryParticle, then extended it to a MoveableParticle. Because I want it to move within a limited rectangular space, I began trying to add a Boundary object to the system, which I added as a singleton for now (because in most cases particles would have the same boundaries). This is when I came to my problem.

In my way of thinking I would either group a boundary based on aggregation (min/max) then dimension (x/y), or vice versa, so I might define either of the following:

my $boundary_aggregation_first = ( min => { x => 0, y => 0, }, max => { x => 639, y => 479, }, ); my $boundary_dimension_first = ( x => { min => 0, max => 639, }, y => { min => 0, max => 479, }, );
(For the remainder of the code, I will go with the aggregation-first model, although the issue occurs either way.)

When I try to code that into Moo (or Moose), I'm not sure how best to handle that object. I would think I should be able to add the limit through a method, but I cannot seem to reason out what that method should look like. If I break encapsulation I could it could be as easy as $wall->limit->{min}{x} = 0, but I don't want to be *that guy* who tromps through code (and for whom it blows up if the Boundary objects change later.)

package Boundary; use strictures 2; use namespace::clean; use Data::Dumper; use Moo; use MooX::Types::MooseLike::Base qw/ HashRef Int Maybe /; with q{MooX::Singleton}; has q{limit} => ( is => q{rw}, isa => HashRef [ HashRef [ Maybe [Int] ] ], traits => [ q{Hash}, ], default => sub { {} }, );

I then tried a 'before' call to try to allow me to hand it a HoH and set the values there, or pass in a HoH with an undefined value to hopefully return the value:

before q{limit} => sub { my $self = shift; if ( scalar @_ ) { my $val = shift; if ( ref $val eq q{HASH} ) { foreach my $k ( keys %{$val} ) { if ( defined $val->{$k}->{$l} ) { $self->limit->{$k}{$l} = $val->{$k}->{$l}; } else { my $result = $self->limit->{$k}{$l}; return $result; } } } } }; __PACKAGE__->meta->make_immutable; 1; package main; my $wall = Boundary->instance(); $wall->limit( { min => { x => 0, y => 0, }, max => { x => 639, y => 478, }, }, ); # ... say $wall->limit( { min => { x => undef, }, }, ); # Expected: 0 # Actual: HASH(0x801a440b8) say $wall->limit( { max => { x => undef, }, }, ); # Expected: 639 # Actual: HASH(0x801a44160)

What I see with this is that I appear to be replacing the singleton when I try to use the undef setting to retrieve the value.

What is the proper way to do what I am trying to accomplish?

Thank you for your time and attention, and any guidance/assistance you may provide. Have a great day, and stay safe!

sscce.pl:

use strict; use warnings; use 5.014; use Moo; use strictures 2; use namespace::clean; package Boundary; use Data::Dumper; use Moo; use MooX::Types::MooseLike::Base qw/ HashRef Int Maybe /; with q{MooX::Singleton}; has 'limit' => ( is => q{rw}, isa => HashRef [ HashRef [ Maybe [Int] ] ], traits => [ q{Hash}, ], default => sub { {} }, ); before q{limit} => sub { my $self = shift; if ( scalar @_ == 1 ) { my $val = shift; if ( ref $val eq q{HASH} ) { foreach my $k ( keys %{$val} ) { foreach my $l ( keys %{ $val->{$k} } ) { if ( defined $val->{$k}->{$l} ) { $self->limit->{$k}{$l} = $val->{$k}->{$l}; } else { my $result = $self->limit->{$k}{$l}; return $result; } } } } } }; __PACKAGE__->meta->make_immutable; 1; package main; # use Boundary; my $wall = Boundary->instance(); $wall->limit( { min => { x => 0, y => 0, }, max => { x => 320, y => 240, }, }, ); say $wall->limit->{min}{x}; say $wall->limit->{min}{y}; say $wall->limit->{max}{x}; say $wall->limit->{max}{y}; # $wall->limit->{max}{x} = 325; # $wall->limit->{max}{y} = 245; say __LINE__ . q{: }, $wall->limit( { max => { x => 325, y => 245, }, }, ); say __LINE__ . q{: }, $wall->limit( { max => { q{x} => undef, }, }, ); say __LINE__ . q{: }, $wall->limit( { max => { q{y} => undef, }, }, ); # $wall->limit->{max}{x} = 320; # $wall->limit->{max}{y} = 240; $wall->limit( { max => { x => 320, y => 240, }, }, ); __END__

Replies are listed 'Best First'.
Re: Question regarding proper handling of a Hash/HashRef structure in Moose-variants
by haj (Vicar) on Dec 22, 2021 at 08:22 UTC

    I avoid to make HashRef attributes writable (and even making them readable can have surprising effects).

    In your case, you seem to want to overwrite individual elements of limit by passing a hashref containing only the values you want to change. While the auto-generated attribute accessors of Moo* are very convenient for simple cases, I don't use them for complex attributes. I'd rather write a separate method like that:

    sub new_limit { my $self = shift; my ($aggregation, $dimension, $val) = @_; # e.g. 'min', 'x', '-42' $self->limit->{$aggregation}{$dimension} = $val; }

    That would, for example, allow to change the interna from aggregation-first to dimension-first without changing the external API.

    Even if you want to allow many values to be changed in one go, I recommend not to use the auto-generated Moo* accessor, but to write a separate method which contains pretty much the same code which you used in your before modifier.

      I'll second this. The semantics expected by a random perl programmer are that

      $object->noun($value)

      has an implied verb of "set" which replaces the conceptual value of that attribute. If you want to apply changes to an attribute, you should make a method with a different verb in it, like apply_limit. You can then decide whether the attribute should even be exposed or not.

      For some variations on the theme, consider HTTP::Headers header($name, $value). That one acts sort of like your code, setting one of multiple headers, but it is understood that there is not one single attribute named "header", there are many headers given by name of the first argument, and you are setting one of them to a new value. If you were implementing HTTP::Headers in Moose, you would not declare 'header' as an attribute.

Re: Question regarding proper handling of a Hash/HashRef structure in Moose-variants
by bliako (Monsignor) on Dec 23, 2021 at 08:30 UTC

    Call me thick but writing code for that framework (Moose or Moo?) is as impenetrable to me as a hundred metre brick wall. A useless comment I know. What I really mean to say is that any such framework must do a serious and honest reality check before release. That really couldn't have happened with this framework. And more than once I called Java bureaucratic!

    feel free to downvote.

    bw, bliako

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11139816]
Approved by marto
Front-paged by Corion
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others browsing the Monastery: (4)
As of 2024-03-28 22:37 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found