Beefy Boxes and Bandwidth Generously Provided by pair Networks
"be consistent"
 
PerlMonks  

Converting Hashes to Objects

by haukex (Archbishop)
on May 17, 2020 at 13:13 UTC ( [id://11116851]=perlmeditation: print w/replies, xml ) Need Help??

Inspiration whacked me in the head yesterday, and I realized that it was pretty easy to take an arbitrary hash reference and turn it into an object, so that instead of writing $hash->{key} I can just write $hash->key, saving two characters and giving me protection from typos. Enter Util::H2O:

use Util::H2O; my $hash = h2o { foo => "bar", x => "y" }, qw/ more keys /; print $hash->foo, "\n"; # prints "bar" $hash->x("z"); # setter

Deeply nested structures can be converted too...

my $struct = { hello => { perl => { world => "yay" } } }; h2o -recurse, $struct; print $struct->hello->perl->world, "\n"; # prints "yay"

The function blesses its hashref argument into a new package, and returns that hashref as well. The hashrefs otherwise remain the same as before, so you can still use $hash->{key} and so on if you like. The newly created package is cleaned up by default when the object is destroyed (configurable). These objects are of course a bit less performant than a regular hashref, but this wasn't intended for high-performance applications.

Update: As of the freshly-released v0.06, the h2o function also locks the hash's keyset by default, to prevent typos when using the hash like a regular hashref. This can be disabled with the -lock=>0 option. /Update

I quickly realized I could do more with this, like add methods to these objects:

my $obj = h2o -meth, { one => 123, two => 456, sum => sub { my $self = shift; return $self->one + $self->two; } }; print $obj->sum, "\n"; # prints "579"

And I could even make it easy to create classes; the -new option generates a constructor:

h2o -class=>'Point', -new, -meth, { angle => sub { my $self = shift; atan2($self->y, $self->x) }, }, qw/ x y /; my $one = Point->new(x=>1, y=>2); my $two = Point->new(x=>3, y=>4); $two->y(5); printf "%.4f\n", $two->angle; # prints 1.0304

Yes, I realize that might be taking it a little bit too far, but it was easy to implement - plus, I figured this could possibly be useful for whipping up mock objects in tests. And yes, I'm aware similar tools exist ;-)

Replies are listed 'Best First'.
Re: Converting Hashes to Objects
by tobyink (Canon) on May 18, 2020 at 07:55 UTC

    I wrote Object::Adhoc recently which is pretty similar.

    my $obj = object { foo => 1, bar => 2 };

    Main differences are that mine doesn't work recursively, and mine also generates has_foo methods.

    If you need nested objects, my solution would be to just be explicit:

    my $connection = object { client => object { address => '10.0.0.1', port => 20001 }, server => object { address => '10.0.0.2', port => 25 }, }; say $connection->server->address;

    As far as adding things like constructors and methods, I consider that beyond the scope of Object::Adhoc; use a proper class builder for that. Object::Adhoc is just for those cases where you want a sub to return something dict/struct-like.

    object { ... } is conceptually a quote-like operator.

      Nice, thanks! I've added references to the docs.

Re: Converting Hashes to Objects
by Corion (Patriarch) on May 17, 2020 at 17:01 UTC

    If you're not tied to creating objects, I found locking hashes quite good to prevent typos:

    use Hash::Util 'lock_keys'; my $hash = h2o { foo => "bar", x => "y" }, qw/ more keys /; lock_keys $hash; print $hash->{ foo }, "\n"; # prints "bar" $hash->{ x } = "z"; # setter $hash->{batman} = 'secret'; # dies

      Definitely, and I actually use that a fair amount, and Hash::Util's functions were part of the inspiration for this. The advantage of this module is that it saves you two characters ;-) (they're a little slower to type on a German keyboard too...) One of the disadvantages is that method calls don't interpolate into strings.

      (Hmmm, maybe I could to add a -lock option to additionally lock the hashref's keys, so the hash can be used safely in both ways... Update: Done!)

      I found locking hashes quite good to prevent typos

      Thank you very much for the inspiration, I just released v0.06 that locks the hash's keyset by default :-)

Re: Converting Hashes to Objects
by Eily (Monsignor) on May 19, 2020 at 11:33 UTC

    -meth
    Any code references present in the hash will become methods.
    This makes it sound like you can add methods after creating the hash. (Or, if keys are locked, that you can turn an existing key into a method), but from reading the code it looks like only subref present at creation are called directly.

    My original question was about whether the -meth option made it impossible to have a subref as a value, the answer would be it's impossible to have both methods and subref values at creation. Maybe the methods should be removed from the hashs' keys though, otherwise if you iterate over the hash with each it would be impossible to tell methods and subref values appart. Also if you have a method $hash->meth(), this mean you'd be able to set $hash->{meth} (to either a plain value or a subref) without any impact on the method call.

      Thank you for the feedback, that's an excellent point! I'll clarify the documentation. And I agree it does make sense to remove the hash entries that were turned into methods like you suggested. I'll have to think a bit more about whether it makes sense to also keep the method name in the "allowed keys" of the hash.

      Update 2020-05-23: I thought about this some more, and I've decided to delete the hash entry and remove the key from the "allowed keys", unless that key is also specified in @allowed_keys. This allows the user to keep the typo prevention by default, but also optionally keep the key in the hash, for example in case they want to write a custom getter/setter. I just released v0.08 with this change, thank you!

Re: Converting Hashes to Objects
by 1nickt (Canon) on May 17, 2020 at 14:31 UTC

    Hi Hauke, does this differ from Hash::AsObject (which is usually in my stack)?


    The way forward always starts with a minimal test.
      does this differ from Hash::AsObject

      Yes, since that uses AUTOLOAD, which means it doesn't provide full typo prevention:

      use Hash::AsObject; my $h = Hash::AsObject->new( foo => "bar" ); print $h->fOo, "\n"; # warning about undef use Util::H2O; my $o = h2o { foo => "bar" }; print $o->fOo, "\n"; # dies

      Plus Util::H2O has the extra goodies like custom methods that I wrote about.

        Interesting, thanks! I noticed after your reply that it's not possible to use Hash::AsObject with L in one-liners (which I hadn't tried, I think); I imagine because of dueling AUTOLOAD trickery.

        $ perl -ML -wE 'my $h = Hash::AsObject->new( foo => "bar" ); say $h->f +oo' Can't locate foo.pm in @INC (you may need to install the foo module)
        😞

        I am usually under strictures(2) to catch typos, so it's not usually a feature I need in packages:

        $ perl -MHash::AsObject -wE 'my $h = Hash::AsObject->new( foo => "bar" + ); say $h->f0o; say 42' Use of uninitialized value in say at -e line 1. 42
        $ perl -MHash::AsObject -Mstrictures=2,1 -wE 'my $h = Hash::AsObject-> +new( foo => "bar" ); say $h->f0o; say 42' Use of uninitialized value in say at -e line 1.

        Thanks!


        The way forward always starts with a minimal test.
Re: Converting Hashes to Objects
by bliako (Monsignor) on May 18, 2020 at 10:30 UTC

    yet another useful module (yaum!)

    I often miss Java's standard object methods toString, equals, compareTo, perhaps serialise. But if you do implement these you are building a Class builder I guess. A (more) standardised class builder.

    Another thought is to provide a *safe and proper* way to convert from a string representation of a perl data structure(pds) to pds via an object: fromString or deserialise. BUT AGAIN, that's moving away from keeping it all simple and compact and risk offering something that's already out there. The plus point is that you are setting a standard. Which may be against Perl spirit (edit: i mean my suggestions)?

      Thanks :-)

      I often miss Java's standard object methods toString, equals, compareTo, perhaps serialise.

      The first three would usually be done with overloading, I guess, and serialization depends on the format (like Storable or one of the many text-based formats). I wrote a bit about the complexity of equals and hashCode in this node.

      Another thought is to provide a *safe and proper* way to convert from a string representation of a perl data structure(pds) to pds

      I tried to do something like that with Config::Perl (see Undumping Perl).

        I was thinking that perhaps it would be easier to auto-implement these methods if you have the object as a hash, which is your input in this case.

        My point was that it is an advantageous start you have there when you are given the object as a hash. So, some things, otherwise tedious, could be auto-implemented. For example, equals() a hash or equals() an object are equivalent but the hash can be cleaner and simpler. Sure objects are hashes eventually but you never know what trickery you will find in there. Same with dumping an object versus dumping a hash.

Re: Converting Hashes to Objects
by perlfan (Vicar) on May 19, 2020 at 16:18 UTC
    Wow, this module makes a really compelling case for actually being useful - and the most Perlish approach I've seen thus far. The module is a nanoparticle and has almost no dependencies. I am glad to see Moo didn't rot your brain - however, it may have damaged it just enough to create something really cool like this. I will definitely keep this module in my back pocket.

      Thanks, and brain damage is certainly a possibility, I did name one of the options -meth after all ;-P

Re: Converting Hashes to Objects
by jo37 (Deacon) on May 17, 2020 at 13:46 UTC

    I like the idea, but dislike the name. H2O is water and nothing else :-)
    Maybe Ohash or Shash (smart hash)?

    Should have keys, values and each, too.

    EDIT: At a second thought, this would prevent "keys", "values" and "each" as keys :-(
    Not so good.

    Greetings,
    -jo

    $gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$
      I like the idea, but dislike the name. H2O is water and nothing else :-)

      I like the name, at the moment I don't think I'll change it ;-)

      The objects are still hashrefs, you can still do keys %$hash etc.

        Maybe something for the perldoc: keys must be valid Perl identifiers if accessed via this module.

        Greetings,
        -jo

        $gryYup$d0ylprbpriprrYpkJl2xyl~rzg??P~5lp2hyl0p$
      I like the idea, but dislike the name. H2O is water
      Yes, but if you're just taking a hash and autogenerating accessors for the hash elements, I'd say that's a pretty watered-down object.
Re: Converting Hashes to Objects
by Aldebaran (Curate) on May 20, 2020 at 06:42 UTC
    Inspiration whacked me in the head yesterday, and I realized that it was pretty easy to take an arbitrary hash reference and turn it into an object, so that instead of writing $hash->{key} I can just write $hash->key, saving two characters and giving me protection from typos. Enter Util::H2O:

    I'm glad that you took the time and effort to create this software tool, as it seems to have gotten me unstuck on a line of code and with sitting on the fence in general. I know that meditations are typically where a person doesn't come to ask questions, but I think a meditation gets bonus points when it solves a problem for someone else, that being me today. I like to conserve vertical space for the collective scrollfingers of the monastery so present output, source, and questions in readmore tags:

    Again, thx, haukex, for helping me off a dime.

      my %vars  = %$rvars;

      A note on this: When you do this, you're creating a (shallow) copy of the hash, and if $rvars happens to be a Util::H2O object, you'd lose its methods. If that's not what you want, you'll have to work with the hash reference directly. In newer versions of Perl (>=5.22), there is an experimental feature that allows aliasing:

      use experimental 'refaliasing'; my $rvars = { foo=>"bar" }; \my %vars = $rvars; # alias print $vars{foo}, "\n"; # prints "bar" $vars{abc} = "xyz"; # modifies $hashref's contents

      But since it's experimental, it's probably better to stick with $rvars->{foo} instead.

      Q1) How is a "setter" to be implemented?

      In the context of Util::H2O, getters/setters are created for every key that exists in the hash or is given as an "additional key" at the time of the h2o call. So for your $hash->x("z") to work, there'd need to be a key x in the hash or you need to specify it to the h2o function; you don't need to write your own sub x.

      Q2) Do I still have to pass this around as clumsily as I am with these hash references now.

      Q3) Is there now a better way to do this?

      TIMTOWTDI, here's how I might have written it without the module:

      use Path::Tiny; use Time::Piece; my $ref_var = { abs => path(__FILE__)->absolute, cwd => Path::Tiny->cwd, }; init_vars($ref_var); print $ref_var->{save_file}, "\n"; sub init_vars { my $rvars = shift; # ... $rvars->{save_file} = path( $rvars->{cwd}, "games", localtime->strftime("%d-%m-%Y-%H-%M-%S.txt") )->touchpath; # ... }

      And with the module, one can write:

      use Path::Tiny; use Time::Piece; use Util::H2O; my $ref_var = h2o { abs => path(__FILE__)->absolute, cwd => Path::Tiny->cwd, }, qw/ save_file ... /; init_vars($ref_var); print $ref_var->save_file, "\n"; sub init_vars { my $rvars = shift; # ... $rvars->save_file( path( $rvars->cwd, "games", localtime->strftime("%d-%m-%Y-%H-%M-%S.txt") )->touchpath ); # ... }
Re: Converting Hashes to Objects
by djerius (Beadle) on May 20, 2020 at 13:22 UTC
    Looks like I'll have to update my list of similar modules in my contribution to this space (Hash::Wrap) (shameless self plug here).

      Thank you! I'll update my list of references as well :-)

      Update: Done!

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others drinking their drinks and smoking their pipes about the Monastery: (4)
As of 2024-04-19 04:14 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found