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

Hello monks!

Is it possible to use hash reference as a key in another hash reference?

I realize I am looking for functionality that a database could give me easily but I have been using Perl hashrefs for log analysis and would like to continue that way.

In the below, simplified example I am keeping stats of sales at a burger joint. The hash that I would like to use as a key contains the date and time data (we'll call it the "key hash"). In a current, working system I have a function (basically a join) that is used to convert the "key hash" into a string that becomes the key used to store the sales data. Later on I have to use another function to convert that string back into a hash -- this is fine for homogenous data but I would like to use abstraction to allow dynamic "hash keys" and this idea seemed simpler than having the data carry around it's own transform functions.

I believe that am misunderstanding something fundamental about hash references because the code below seems to work. It takes the "hash key" hash reference and inserts the "stats hash" but when I try to reference the "hash key" I only have a "string" which of course throws up errors. I've tried to re-reference the hash but can't seem to find a way to do it properly.

thanks for whatever help you can provide

use strict; use warnings; use Data::Dumper; my $key_hash_1 = { 'day' => 1, 'month' => 1, 'year' => 2000, 'hour' => + 4, 'minute' => 44}; my $key_hash_2 = { 'day' => 1, 'month' => 1, 'year' => 2000, 'hour' => + 4, 'minute' => 45}; my $sales_hash = { $key_hash_1 => {'burgers_sold' => 5, 'fries_sold' => 3, 'sodas_so +ld' => 11}, $key_hash_2 => {'burgers_sold' => 2, 'fries_sold' => 4, 'sodas +_sold' => 7} }; print Dumper($key_hash_1,$key_hash_2); print Dumper($sales_hash); while (my ($key_hash,$sales) = each (%$sales_hash)){ print "$key_hash => $sales\n"; while (my ($sales_item,$total_sold) = each(%$sales)){ print "\t$sales_item => $total_sold\n"; } while (my ($time_item,$time_value) = each(%${$key_hash})){ print "\t$time_item => $time_value\n"; } }
Results:
$VAR1 = { 'hour' => 4, 'minute' => 44, 'month' => 1, 'day' => 1, 'year' => 2000 }; $VAR2 = { 'hour' => 4, 'minute' => 45, 'month' => 1, 'day' => 1, 'year' => 2000 }; $VAR1 = { 'HASH(0x904d00)' => { 'fries_sold' => 4, 'sodas_sold' => 7, 'burgers_sold' => 2 }, 'HASH(0x815d48)' => { 'fries_sold' => 3, 'sodas_sold' => 11, 'burgers_sold' => 5 } }; HASH(0x904d00) => HASH(0x90c238) fries_sold => 4 sodas_sold => 7 burgers_sold => 2 Can't use string ("HASH(0x904d00)") as a SCALAR ref while "strict refs +" in use

Replies are listed 'Best First'.
Re: Use a hashref as a key in another hashref?
by 2teez (Vicar) on Jun 05, 2014 at 04:13 UTC

    hi mwb613,
    ..Is it possible to use hash reference as a key in another hash reference?..

    Ordinarily, maybe not! (But that is a 'white' lie) ;) Until you consider module like Tie::RefHash, which allows you use reference as hash key.

    use strict; use warnings; use Data::Dumper; use Tie::RefHash; tie my %h, "Tie::RefHash"; my $key_hash_1 = { 'day' => 1, 'month' => 1, 'year' => 2000, 'hour' => 4, 'minute' => + 44 }; my $key_hash_2 = { 'day' => 1, 'month' => 1, 'year' => 2000, 'hour' => 4, 'minute' => + 45 }; %h = ( $key_hash_1 => { 'burgers_sold' => 5, 'fries_sold' => 3, 'sodas_sold' => 11 }, $key_hash_2 => { 'burgers_sold' => 2, 'fries_sold' => 4, 'sodas_so +ld' => 7 } ); print Dumper( $key_hash_1, $key_hash_2 ); print Dumper( \%h ); for ( keys %h ) { print join ' ' => %{$_}, ' => ', %{ $h{$_} }, $/; }
    Output: Secondly, I think where you are using chains of while loops, a for loop would have been better.

    If you tell me, I'll forget.
    If you show me, I'll remember.
    if you involve me, I'll understand.
    --- Author unknown to me

      There's also a Tie::RefHash::Weak on CPAN. (The core module Tie::RefHash treats keys as strong references.)

      use Moops; class Cow :rw { has name => (default => 'Ermintrude') }; say Cow->new->name
Re: Use a hashref as a key in another hashref?
by LanX (Saint) on Jun 05, 2014 at 03:32 UTC
    > Is it possible to use hash reference as a key in another hash reference?

    That's a classic! :)

    Hash-keys are always stringified and there is no "direct" way to dereference the stringification.

    BUT you are free to keep the original ref somewhere. Either as value in your data-structure or in a separate hash with $ref_of_str{"$key_hash"}=$key_hash. ¹

    Thats much safer BTW cause a string doesn't increment the reference count ... i.e. keeping the reference somewhere protects the data from destruction if ref-count reaches 0.

    (I hope it's clear now why dereferencing a string will hardly be ever possible)

    HTH! :)

    Cheers Rolf

    (addicted to the Perl Programming Language)

    update

    ¹) I rarely come into situations where I really need this trick. I'm sure your data-structure could be rewritten to avoid this, but tl;dr...

      The problem with this is that reference addresses are not guaranteed to be unique. If a hash is destroyed (e.g. has gone out of scope), then its address can be re-used when a new hash is created. On my machine at least, the following prints the same reference address twice:

      { my $x = { foo => 1 }; print $x, "\n"; } { my $y = { bar => 2 }; print $y, "\n"; }
      use Moops; class Cow :rw { has name => (default => 'Ermintrude') }; say Cow->new->name
        Sure that's why you have to keep the original unweakend ref for each key!

        The keystrings are guaranteed to be unique as long as the ref count is positive.

        And as soon as a hash entry is deleted the shadow entry should be deleted too.

        This could best be synchronized with a tied hash or a dedicated object.

        And that structure can be safely passed around then.(CPAN to rescue! ;)

        But as I already said this is rarely worth the trouble, avoiding refs as keys is the better design decision.

        Cheers Rolf

        (addicted to the Perl Programming Language)

      Thanks Rolf,

      Since it's a classic hopefully it at least brought a sense of nostalgia for you. ;)

      What you said regarding the key being stringified and references possibly being lost to garbage collection makes sense.

      My hope was to be able to traverse the hash and search for specific values in the "key hash" to determine whether I want the stats or not. Searching through a separate hash wherein I store the "keys" doesn't appeal to me much but I will test it as an option. Right now I'm reconsidering the idea of each "data set" having it's own key constructor and destructor subs that just ride around in the data structure with it. I could effectively have a sub that takes a search value and determines if it's part of the key similar a hash -- though I'm sure not as fast. For example

      #imagine the config was loaded from a YAML file my $string_search_function = $config->{'string_search_function'}; if(&{ $string_search_function }($key_string, $key_component_label, $va +lue_to_search_for)){ #do something interesting }

      Thanks again for your thoughts!

Re: Use a hashref as a key in another hashref?
by GrandFather (Saint) on Jun 05, 2014 at 08:45 UTC

    Why not use the time stamp as the key then you can order output in a sane fashion:

    use strict; use warnings; my @dateFields = qw(year month day hour minute); my @itemKeys = qw(burgers fries sodas); my %sales = ( makeEntry( 'day' => 1, 'month' => 1, 'year' => 2000, 'hour' => 4, 'minute' => 44, 'burgers_sold' => 5, 'fries_sold' => 3, 'sodas_sold' => 11 ), makeEntry( 'day' => 1, 'month' => 1, 'year' => 2000, 'hour' => 4, 'minute' => 45, 'burgers_sold' => 2, 'fries_sold' => 4, 'sodas_sold' => 7 ), ); for my $key (sort keys %sales) { print "$key\n"; printf " %-7s %s\n", $_, $sales{$key}{"${_}_sold"} for @itemKeys +; } sub makeEntry { my (%entry) = @_; return sprintf ("%04d/%02d/%02d %02d:%02d:00", @entry{@dateFields}), \%entry; }

    Prints:

    2000/01/01 04:44:00 burgers 5 fries 3 sodas 11 2000/01/01 04:45:00 burgers 2 fries 4 sodas 7
    Perl is the programming world's equivalent of English
Re: Use a hashref as a key in another hashref?
by GrandFather (Saint) on Jun 05, 2014 at 11:35 UTC

    For interest the same task using a database could look like the following:

    use strict; use warnings; use DBI; my @date = qw(year month day hour minute); my @items = qw(burgers fries sodas); my @fields = (@date, @items); my @entries = ( makeEntry( 'day' => 1, 'month' => 1, 'year' => 2000, 'hour' => 4, 'minute' => 44, 'burgers' => 5, 'fries' => 3, 'sodas' => 11 ), makeEntry( 'day' => 1, 'month' => 1, 'year' => 2000, 'hour' => 4, 'minute' => 45, 'burgers' => 2, 'fries' => 4, 'sodas' => 7 ), ); unlink 'delme.sqlite'; my $dbh = DBI->connect("dbi:SQLite:dbname=delme.sqlite","",""); my $sql = <<SQL; CREATE TABLE Sales ( year INTEGER, month INTEGER, day INTEGER, hour INTEGER, minute INTEGER, burgers INTEGER, fries INTEGER, sodas INTEGER ) SQL $dbh->do ($sql); my $columns = join ', ', @fields; my $places = join ', ', ('?') x @fields; my $sth = $dbh->prepare ("Insert INTO Sales ($columns) VALUES ($places +)"); $sth->execute(@$_) for @entries; $sth = $dbh->prepare("select * from Sales order by Year, Month, Day, H +our, Minute"); $sth->execute(); while (my $row = $sth->fetchrow_hashref()) { printf "%04d/%02d/%02d %02d:%02d:00\n", @{$row}{@date}; printf " %-7s %s\n", $_, $row->{"${_}"} for @items; } sub makeEntry { my %row = @_; return [@row{@fields}]; }

    Prints:

    2000/01/01 04:44:00 burgers 5 fries 3 sodas 11 2000/01/01 04:45:00 burgers 2 fries 4 sodas 7

    But now that you have the data in a database you have much better options for extracting interesting information. For example, you can now order the output by, say, soda sales with a simple change to the select (select * from Sales order by fries desc, Year, Month, Day, Hour, Minute) or look at sales during a particular time each day (select * from Sales Where minute = 45).

    Perl is the programming world's equivalent of English
      Or 'Relational' like this perhaps
      #!perl use strict; use warnings; use DBI; #create db my $dbfile = 'database.sqlite'; unlink($dbfile); my $dbh = DBI->connect('dbi:SQLite:dbname='.$dbfile , undef , undef , {RaiseError =>1, AutoCommit =>0}) or die $DBI::errstr; $dbh->do('CREATE TABLE STATS (ID integer, DTIME time, PRIMARY KEY (ID))'); $dbh->do('CREATE TABLE SALES (ID integer, PRODUCT, VOLUME integer, PRIMARY KEY (ID,PRODUCT))'); $dbh->do('CREATE TABLE PRODUCT (PRODUCT, SECTOR, PRIMARY KEY (PRODUCT))'); $dbh->commit; # load data my $sth = $dbh->prepare('INSERT INTO STATS VALUES (?,?)'); while (<DATA>){ chomp; last if $_ eq 'PRODUCT'; my ($id,@f) = split ','; my $dt = sprintf "%04d-%02d-%02d %02d:%02d:00",@f[2,1,0,3,4]; $sth->execute($id,$dt) if ($id); print "STATS INSERT $id,$dt\n"; } $dbh->commit; $sth = $dbh->prepare('INSERT INTO PRODUCT VALUES (?,?)'); while (<DATA>){ chomp; last if $_ eq 'SALES'; my (@f) = split ','; $sth->execute(@f) if (@f==2 ); print "PRODUCT INSERT @f\n"; } $dbh->commit; $sth = $dbh->prepare('INSERT INTO SALES VALUES (?,?,?)'); while (<DATA>){ chomp; my (@f) = split ','; $sth->execute(@f) if (@f==3); print "SALES INSERT @f\n"; } $dbh->commit; # report Sales in Sector By Year print "\n"; my $sql = "SELECT strftime('%Y',DTIME) as year,SECTOR,SUM(VOLUME) AS t +otal FROM SALES AS sa LEFT JOIN STATS AS st ON sa.ID = st.ID LEFT JOIN PRODUCT AS pr ON sa.PRODUCT = pr.PRODUCT GROUP BY year,SECTOR ORDER BY total"; my $ar = $dbh->selectall_arrayref($sql); print "REPORT\n"; printf "%4s %-10s %6s\n",qw(Year Sector Volume); printf "---- ---------- ------\n"; for (@$ar){ printf "%4d %-10s %6d\n",@$_; } __DATA__ 1,1,1,2000,4,44 2,1,1,2000,4,45 PRODUCT burgers, food fries, food sodas, drink SALES 1,burgers,5 1,fries,3 1,sodas,11 2,burgers,2 2,fries,4 2,sodas,7

      Produces

      REPORT Year Sector Volume ---- ---------- ------ 2000 food 14 2000 drink 18
      poj
Re: Use a hashref as a key in another hashref?
by locked_user sundialsvc4 (Abbot) on Jun 05, 2014 at 13:35 UTC

    If you want to insist on continuing to use in-memory data structures to, in effect, represent database tables, then you will need to follow the same general design principles of third-normal form.   For instance, a timestamp is an attribute of the data which, even though it may be thought of as “unique,” is not suitable for use as a key because it contains data.   Instead, you need to assign each and every record an abstract foreign-key value ... in this confined case, an incrementing integer would do, since apparently the data is not stored ... and reference each record by the use of these keys.   Where one record “refers to” another, a column in that table hashref in that object should contain the corresponding record’s key.   (Circular storage references are thereby avoided.)

    I recommend, further, that each type of record be declared as an object.   Specifically so that “the data can ‘carry around its own transform functions,’ ” or, anything else that it needs to carry.   A blessed-reference costs no more than an ordinary one.

    A single hashref could contain a dictionary for all objects indiscriminately, and/or you could have a separate dictionary by type ... since the keys are in any case unique throughout the system, either or both alternatives would do as it suits you.

    Having said all of that, however, I do suggest that this is an excellent application for an SQLite database disk file or in-memory file.

Re: Use a hashref as a key in another hashref?
by perlfan (Parson) on Jun 05, 2014 at 13:01 UTC
    No, but you can create a consistent serialization of the hashref you wish to use as a key - or some sort of consistent checksum "hash" of it.