Util::H2O and More, during Ordinary Times

Background

During the 2022 Perl Advent, in particular the entry for December 06; Perl Advent fans were introduced to a little module called Util::H2O.

A lot has already been said about Util::H2O, and this author uses it a lot in client and production code because it helps produce very clean and maintainable HASH reference heavy code. So much so, that he created the Util::H2O::More module to encapsulate some common tasks and additional capabilities for working between pure Perl data structures and blessed objects that have real data accessors, in a natural and idiomatic way.

Support for Generic Perl Data Structures

h2o is perfect for dealing with data structures that are made up of strictly HASH references, but it is often the case that useful data structures contain a mix of HASH and ARRAY references. For example, when using databases or web API calls returning JSON, it is often a list of records that is returned. This was the case of the example call that was in the December 06 Perl Advent 2022 article.

Recall the original example,

use strict;
use warnings;
use JSON       qw//;
use HTTP::Tiny qw//;
use Util::H2O;    # only exports 'h2o'
my $http     = HTTP::Tiny->new;
my $response = h2o $http->get(q{https://jsonplaceholder.typicode.com/users});
if ( not $response->success ) {
    print STDERR qq{Cannot get list of online persons to watch!\n};
    printf STDERR qq{Web request responded with with HTTP status: %d\n}, $response->status;
    exit 1;
}
my $json_array_ref = JSON::decode_json( $response->content );

print qq{lat, lng, name, username\n};
foreach my $person (@$json_array_ref) {

    # objectify each HASH reference at a time
    h2o -recurse, $person;
    printf qq{%5.4f, %5.4f, %s, %s\n},
      $person->address->geo->lat,
      $person->address->geo->lng,
      $person->name, $person->username;
}

Which outputs:

lat, lng, name, username
-37.3159, 81.1496, Leanne Graham, Bret
-43.9509, -34.4618, Ervin Howell, Antonette
-68.6102, -47.0653, Clementine Bauch, Samantha
29.4572, -164.2990, Patricia Lebsack, Karianne
-31.8129, 62.5342, Chelsey Dietrich, Kamren
-71.4197, 71.7478, Mrs. Dennis Schulist, Leopoldo_Corkery
24.8918, 21.8984, Kurtis Weissnat, Elwyn.Skiles
-14.3990, -120.7677, Nicholas Runolfsdottir V, Maxime_Nienow
24.6463, -168.8889, Glenna Reichert, Delphine
-38.2386, 57.2232, Clementina DuBuque, Moriah.Stanton

Some New Keywords

New keywords have been introduced in Util::H2O::More, called d2o and it's undoer, o2d (like o2h is to h2o). Essentially, it traverses a Perl data structure, looking for pure HASH refs that may be potentially contained inside of ARRAY refs, and adds accessors. For ARRAYs, it adds some handy vmethods to make accessing the lists much cleaner (e.g., all).

Without much ado:

use strict;
use warnings;
use JSON            qw//;
use HTTP::Tiny      qw//;
use Util::H2O::More qw/h2o d2o/;
my $http           = HTTP::Tiny->new;
my $response       = h2o $http->get(q{https://jsonplaceholder.typicode.com/users});
if ( not $response->success ) {
    print STDERR qq{Cannot get list of online persons to watch!\n};
    printf STDERR qq{Web request responded with with HTTP status: %d\n}, $response->status;
    exit 1;
}

# objectify contents of $json_array_ref in one pass
my $json_array_ref = d2o JSON::decode_json( $response->content );

# $json is an ARRAY reference
foreach my $person ( $json_array_ref->all ) {
    printf qq{%5.4f, %5.4f, %s, %s\n},
      $person->address->geo->lat,
      $person->address->geo->lng,
      $person->name, $person->username;
}

Which outputs, like above:

lat, lng, name, username
-37.3159, 81.1496, Leanne Graham, Bret
-43.9509, -34.4618, Ervin Howell, Antonette
-68.6102, -47.0653, Clementine Bauch, Samantha
29.4572, -164.2990, Patricia Lebsack, Karianne
-31.8129, 62.5342, Chelsey Dietrich, Kamren
-71.4197, 71.7478, Mrs. Dennis Schulist, Leopoldo_Corkery
24.8918, 21.8984, Kurtis Weissnat, Elwyn.Skiles
-14.3990, -120.7677, Nicholas Runolfsdottir V, Maxime_Nienow
24.6463, -168.8889, Glenna Reichert, Delphine
-38.2386, 57.2232, Clementina DuBuque, Moriah.Stanton

In this example, the initial HASH reference returned by HTTP::Tiny is made into an object with accessors using h2o like the original code. However, rather than having to dereference the ARRAY reference $json_array_ref (returned after decoding by decode_json), d2o is employed to convert the data structure such that all HASH references have accessors as expected. And the ARRAY reference containing the list of HASH references has been blessed so that it has the virtual methods on ARRAYs briefly mentioned above.

This allows the code to collapse from:

foreach my $person (@$json_array_ref) {
    h2o -recurse, $person;
...

to, simply:

foreach my $person ( $json_array_ref->all ) {

thus avoiding the call to h2o since the d2o rooted out all the HASH references buried in $json_array_ref and applied h2o to them.

More importantly, this requires no a priori knowledge of the data structure.

Conclusion

It is critical to point out, that h2o does one thing and does it very well - which is a cherished and time tested UNIX ideal for standard tooling. But as a result of present'ing Util::H2O in 2022's Perl Advent, it was clear that for this common case, h2o by itself was insufficient to idiomatically improve the handling of Perl data structures that was resulting from the web API call and subsequent decode_json. The solution is to just add some support for data structure traversal (applying h2o along the way), which is all d2o does.

So d2o was then added to Util::H2O::More to make things a little nicer, and thus taking Perl another step closer to a situation that alls programmers to more cleanly work with complex data structures - by eliminating the glut of curley and square braces and the need to dereference data structures along the way. The addition of the virtual methods to the ARRAY containers, is just more sweetness.

Please checkout Util::H2O and Util::H2O::More; alone or combined, the options available to deal with ad hoc or in flight complex data structures in Perl in very clean and idiomatic ways without resorting to so called "POOP" (Perl Object Oriented Programming) is continuing to improve. Practical sources of these data structures include, DBI (e.g., DBD::mysql), JSON::decode_json, and Web::Scraper. Util::H2O::More even provides special methods for working with Getopt::Long and Config::Tiny, opt2h2o and ini2h2o, respectively.

PS: Sorry for the bad code highlighting, I have no idea how this thing works. :-)

2 Comments

I've just released Util::H2O v0.22, in which the new -arrays option allows h2o and o2h to recurse into arrays as well!

Leave a comment

About Oodler 577

user-pic Mayor of Falvortown