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

Hello fellow monks, I've been Perl'ing for a long time and maybe I just haven't consumed sufficient caffeine today, but... I am at a loss. I have two hashes:
%A = (a => 1, b => 2, c => 3); %Z = (z => 9, y => 8, x => 7);
and I want to populate an array with all the keys in either. It works great if I do it as two steps:
%combined = (%A, %Z); @all_keys = keys %combined;
No problem. But I want to do it in one step:
@all_keys = keys %{{ %A, %Z }};
that works fine but it's a little extra (and I know my code is soon going to inflicted on a junior dev, so...) and anyway it seems like I ought to be able to do something simpler. So where it blows up is when I try to do:
@all_keys = keys(%A, %Z);
I get

Experimental keys on scalar is now forbidden at - line 3.
Type of arg 1 to keys must be hash or array (not list) at - line 3, near "%Z)"
Execution of - aborted due to compilation errors.

There isn't a scalar in sight, the type of arg1 is definitely a hash, and I am trying to figure out what is going on. I would have expected this just to function as a hash overlay, with the results passed to keys(), wham, bam, eat some jam. OK, maybe keys() is doing something fancy to its args? Functions like pop() know that the first arg is a real array, so maybe keys() is treating this as two args and not doing the overlay (and I've never passed more than one arg to keys()---why would I?---but my various attempts to fool the compiler clarify that the overlay should happen don't work either:
@all_keys = keys((%A, %Z)); @all_keys = keys(%temp::var = (%A, %Z)); @all_keys = keys(my %temp = (%A, %Z)); @all_keys = keys(do { %{{%A, %Z}} });
...not that the last one is "cleaner" that the version above that works

All generate the same error as above, and in fact none but the first work even if I take out the overlay part:
@all_keys = keys(%temp::var = (%A)); @all_keys = keys(my %temp = (%A)); @all_keys = keys(do { %{{%A}} });
What is going on?

Replies are listed 'Best First'.
Re: Problem combining hashes
by Corion (Patriarch) on Mar 14, 2024 at 16:38 UTC

    This all comes back to the error message:

    Type of arg 1 to keys must be hash or array (not list) at - line 3, ne +ar "%Z)"

    keys() really wants a hash as its single argument. Not a list. And %A, %Z is a list. Admittedly, it is a list of pairs that can again be interpreted as a hash, but keys() wants to have that hash.

    Using +{ %A, %Z }->%* or %{{ %A, %Z }} constructs the list of pairs from the two hashes, wraps these in a hash reference and then dereferences that hash reference, giving the hash to keys(). This is why that one works.

    Your other approaches fail because keys() only takes a single argument, and that argument must be a hash. Everything you try to pass to it is a list, because the comma operator "," creates a list or the assignments go through an intermediate list (and don't return the assigned hash).

    If you are only interested in getting a unique list of keys, you could use List::Utils uniq, but that is not shorter than the two step approach, especially if you also later want to look at the combined values:

    my @all_keys = uniq( keys(%A), keys(%Z) );
      I didn't use uniq() because I need unique keys, not values, so I'd have to call keys() twice and then use uniq() on the results. I figured this was simpler, LOL. I guess I was just surprised---args are so flexible in Perl I figured that keys() would be able to operator on an overlaid pair. But I guess it's no different than push() requiring the first arg be an array (not a list, not an arrayref, but an actual array).
Re: Problem combining hashes
by Fletch (Bishop) on Mar 14, 2024 at 16:40 UTC

    One expression, no lingering temp vars. But you're really trying to oversimplify.

    my @all_keys = do { my %c = ( %A, %Z ); keys %c };

    The keys function wants a single argument that's a just literal hash or array variable of some form (e.g. either a literal %foo or a postderef $bar->%*). It used to at one point be valid to give a scalar to keys and it would (I believe; not 100%) automagically deref. My guess when you tried to give it multiple items keys( %A, %Z ) it's parsing out as list context and you're actually passing it a list of the two hashes' contents unrolled which is why it's triggering the experimental scalar error and the error that you've given it more than one argument.

    Edit: Also you might want to take into account the sizes of the hashes involved. It may be much less data copying to iterate over the keys of each candidate hash separately and create a hash from those then take keys of that instead so you're not copying all the values around as well. Which is what the aforementioned uniq is pretty much doing under the hood already (although that's going to build a temporary list of the all the keys to pass as args instead of just iterating over them).

    And another thing: If you do make the big überhash be aware that the value is going to correspond to the last key seen from the last hash containing it which you use (e.g. %c = ( %A, %B ) if both have a key "a" the value in $c{a} will have what is in $B{a}).

    The cake is a lie.
    The cake is a lie.
    The cake is a lie.

      I guess it's one expression but it has a block and a semicolon in it so... (lol)

      I think you are right. When I tried to do keys(%A, %Z) it failed its arg magic and it was as if I had called keys("a", 1, "b", 2, "c", 3, "z", 9, "y", 8, "x", 7). It looked at its "one argument" ("a") and threw the "no scalars" error. I wouldn't call it a "bug" but it's a pretty misleading/confusing error message.

      I'm not worried about the sizes. In general, an excellent piece of advice, but in this case there will be literally twos of keys in each hash. I just wanted the union quick and easy.

      To your last point, sure, I use hash overlays on purpose all the time. In this case I only care about the keys, though.

Re: Problem combining hashes
by LanX (Saint) on Mar 14, 2024 at 22:26 UTC
    %A = (a => 1, b => 2, c => 3); %Z = (z => 9, y => 8, x => 7); @all_keys = (keys %A , keys %Z); print "@all_keys"; __END__ a b c x z y

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    see Wikisyntax for the Monastery

      This duplicates common keys.

      Interestingly, with the postdereference, we can use a hash slice in an assigment:

      my %A = (a => 1, b => 2, D => 3); my %Z = (z => 9, y => 8, D => 7); (\ my %all_keys)->@{keys %A, keys %Z} = (); say for keys %all_keys;

      The same construct using the "old" dereference doesn't work:

      @{ \ my %all_keys }{keys %A, keys %Z} = (); say for keys %all_keys; # Global symbol "%all_keys" requires explicit + package name (did you forget to declare "my %all_keys"?)
      because, alas, the my is scoped inside the dereference block.

      map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
        (\ my %all_keys)->@{keys %A, keys %Z} = ();
        What is the "\ my" doing? It seems to work without the "\".
        Yeah sorry I missed that requirement.

        The OP wasn't really a SSCCE tho. Duplicated keys would have shown the problem. :)

        Personally I would go for a clear 2 line approach, all this dereferencing doesn't make it more readable.

        Or to repeat my old mantra:

        • Perl would be better of with autobox-methods¹ {%A, %Z}->keys
        On a side note: I always have the feeling that a big deal of perl4's appeal got lost with my, because of conceptual problems.

        Hmm...People will misunderstand this.

        What I mean is that many things are elegantly solved if you don't need 'my'. A language design problem.

        Cheers Rolf
        (addicted to the Perl Programming Language :)
        see Wikisyntax for the Monastery

        ¹) compare autobox

        (\ my %all_keys)->@{keys %A, keys %Z} = ();

        LOL, I cannot condone that foolery.