Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiply all values in a %hash and return a %hash with the same structure

Tags:

raku

rakudo

I have some JSON stored in a database column that looks like this:

pokeapi=# SELECT height FROM pokeapi_pokedex WHERE species = 'Ninetales';
-[ RECORD 1 ]------------------------------------------
height | {"default": {"feet": "6'07\"", "meters": 2.0}}

As part of a 'generation' algorithm I'm working on I'd like to take this value into a %hash, multiply it by (0.9..1.1).rand (to allow for a 'natural 10% variance in the height), and then create a new %hash in the same structure. My select-height method looks like this:

method select-height(:$species, :$form = 'default') {
    my %heights = $.data-source.get-height(:$species, :$form);

    my %height = %heights * (0.9..1.1).rand;

    say %height;
}

Which actually calls my get-height routine to get the 'average' heights (in both metric and imperial) for that species.

method get-height (:$species, :$form) {
    my $query = dbh.prepare(qq:to/STATEMENT/);
           SELECT height FROM pokeapi_pokedex WHERE species = ?;
        STATEMENT

    $query.execute($species);

    my %height = from-json($query.row);
    my %heights = self.values-or-defaults(%height, $form);

    return %heights;
}

However I'm given the following error on execution (I assume because I'm trying to multiple the hash as a whole rather than the individual elements of the hash):

$ perl6 -I lib/ examples/height-weight.p6
{feet => 6'07", meters => 2}
Odd number of elements found where hash initializer expected:
Only saw: 1.8693857987465123e0
  in method select-height at /home/kane/Projects/kawaii/p6-pokeapi/lib/Pokeapi/Pokemon/Generator.pm6 (Pokeapi::Pokemon::Generator) line 22
  in block <unit> at examples/height-weight.p6 line 7

Is there an easier (and working) way of doing this without duplicating my code for each element? :)

like image 823
kawaii Avatar asked Apr 18 '19 16:04

kawaii


2 Answers

Firstly, there is an issue with logic of your code. Initially, you are getting a hash of values, "feet": "6'07\"", "meters": 2.0 parsed out of json, with meters being a number and feet being a string. Next, you are trying to multiply it on a random value... And while it will work for a number, it won't for a string. Perl 6 allomorphs allow you to do that, actually: say "5" * 3 will return 15, but X"Y' pattern is complex enough for Perl 6 to not naturally understand it.

So you likely need to convert it before processing, and to convert it back afterwards.

The second thing is exact line that leads to the error you are observing.

Consider this:

my %a = a => 5;
%a = %a * 10 => 5; # %a becomes a hash with a single value of 10 => 5
# It happens because when a Hash is used in math ops, its size is used as a value
# Thus, if you have a single value, it'll become 1 * 10, thus 10
# And for %a = a => 1, b => 2; %a * 5 will be evaluated to 10
%a = %a * 10; # error, the key is passed, but not a value

To work directly on hash values, you want to use map method and process every pair, for example: %a .= map({ .key => .value * (0.9..1.1).rand }).

Of course, it can be golfed or written in another manner, but the main issue is resolved this way.

like image 115
Takao Avatar answered Nov 01 '22 18:11

Takao


You've accepted @Takao's answer. That solution requires manually digging into %hash to get to leaf hashes/lists and then applying map.

Given that your question's title mentions "return ... same structure" and the body includes what looks like a nested structure, I think it's important there's an answer providing some idiomatic solutions for automatically descending into and duplicating a nested structure:

my %hash = :a{:b{:c,:d}}

say my %new-hash = %hash».&{ (0.9 .. 1.1) .rand }
# {a => {b => {c => 1.0476391741359872, d => 0.963626602773474}}}

# Update leaf values of original `%hash` in-place:
%hash».&{ $_ = (0.9 .. 1.1) .rand }

# Same effect:
%hash »*=» (0.9..1.1).rand;

# Same effect:
%hash.deepmap: { $_ = (0.9..1.1).rand }

Hyperops (eg ») iterate one or two data structures to get to their leaves and then apply the op being hypered:

say %hash».++ # in-place increment leaf values of `%hash` even if nested

.&{ ... } calls the closure in braces using method call syntax. Combining this with a hyperop one can write:

%hash».&{ $_ = (0.9 .. 1.1) .rand }

Another option is .deepmap:

%hash.deepmap: { $_ = (0.9..1.1).rand }

A key difference between hyperops and deepmap is that the compiler is allowed to iterate data structures and run hyperoperations in parallel in any order whereas deepmap iteration always occurs sequentially.

like image 5
raiph Avatar answered Nov 01 '22 18:11

raiph