Beefy Boxes and Bandwidth Generously Provided by pair Networks
No such thing as a small change
 
PerlMonks  

Using 'keys' on a list

by lammey (Novice)
on Jun 29, 2021 at 12:14 UTC ( [id://11134433]=perlquestion: print w/replies, xml ) Need Help??

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

Suppose a function f() returns an even-sized list e.g.

sub f {a=>1,b=>2}

I'd like to interpret the list as a hash and list the keys with something like

say for keys f;

but this will produce an error - "Experimental keys on scalar is now forbidden" (I'm running perl 5.33.9). I've found what does work is

say for keys %{{f}};

but I find this kind of inelegant. I'm far far from a monk so bear with me, but I can't see why 'keys' doesn't provide list context.

Also, is there a way of creating a hash for consumption by 'keys' in the above situation which doesn't involve creating a hash reference from a list and then immediately dereferencing?

Replies are listed 'Best First'.
Re: Using 'keys' on a list
by choroba (Cardinal) on Jun 29, 2021 at 12:41 UTC
    keys doesn't operate on a list, it operates on a hash (or an array since 5.12).

    You can create a hash, populate it with the list, and get its keys:

    my %h = f(); say for keys %h;

    You can also just return every second element of the list:

    my $i; say for grep ++$i % 2, f();

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]

      Also, when the Perl RFC 0001 comes into a stable release, you can write

      for my ($key, $value) (f()) { say $key; }

      I guess that this will come in 5.36 as experimental feature and hopefully will leave the experimental stage with 5.38.

        And this would have its own iterator?
      Thanks for the response =).
      keys doesn't operate on a list
      Do you know why this might be? Maybe because strictly speaking, only hashes have keys, and lists don't? But then perl isn't usually that strict e.g. as you mentioned from 5.12 keys can operate on an array. I ask because I was hoping to be able to do this in one line. It's not a big deal but I'm kind of curious to see if it's possible.
        > Do you know why this might be?

        Lists don't have keys. For arrays, keys returns the indices of the elements, not every second element. So, given a list, should keys use the hash semantics or the array one?

        Also note that a "list" in fact doesn't exist in Perl. There's a list context, but there always has to be something in the context, and this something is never a list. It might be a hash, it might be an array, or a sequence of comma operators. But keys doesn't want to see the hash (or array) in a list context, it wants to see its underlying HV or AV (which also makes getting the keys much faster, Perl doesn't really iterate over alternating keys and values, skipping the latter).

        map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
Re: Using 'keys' on a list
by tybalt89 (Monsignor) on Jun 29, 2021 at 18:40 UTC

    At 5 characters

    my @keys = keys %{{f}};
    is much more elegant than the equivalent
    use List::Util qw( shuffle uniq pairkeys ); my @keys = shuffle uniq pairkeys f;
    Remember that hashes do uniq'ing and shuffling for free :)

    UPDATE: added a missed "keys" as LanX pointed out.

      >   my @keys = %{{f}};

      Did you mean:

        my @keys = keys %{{f}};

      ???

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

        sure did, updated.

        Apparently. But that's exactly what the OP was using, so it doesn't help.

        Seeking work! You can reach me at ikegami@adaelis.com

Re: Using 'keys' on a list (updated)
by haukex (Archbishop) on Jun 29, 2021 at 17:13 UTC

    You can use pairkeys from List::Util, i.e. say for pairkeys f; works fine. You'll need List::Util 1.29 for this function, which is part of the Perl core since 5.20, or you can get it from CPAN.

    Update: tybalt89 makes a good point that pairkeys may return duplicate "keys", unlike keys.

Re: Using 'keys' on a list
by LanX (Saint) on Jun 29, 2021 at 12:37 UTC
    > but I find this kind of inelegant.

    well, how would you do it in other "more elegant languages"?

    > I'm far far from a monk so bear with me, but I can't see why keys doesn't provide list context.

    keys HASH operates on the datatype hash not list, which means something starting with a % sigil here

    > "Experimental keys on scalar is now forbidden"

    that's another topic which wouldn't have helped you much, but you "probably" would have been able to write keys {f()}

    Questions are:

    • why do you return a list anyway? why not a hash-ref?

      sub f { {a=>1,b=>2} }

    • if it has to be a list and you want more elegance why not defining your elegant helper

      sub lkeys { keys %{{@_}} }

    HTH! :)

    edit

    my preferred way to gain elegance would be a private method, operating on a reference

    my $keys = sub { my $self =shift; keys %$self }; sub f { return { a=>1, b =>2 } } print for f->$keys;

    edit

    > Also, is there a way of creating a hash for consumption by keys in the above situation which doesn't involve creating a hash reference from a list and then immediately dereferencing?

    could you please elaborate? I don't understand the question ...

    update

    see also Re^10: Using 'keys' on a list (prototype & backwards compatibility) for a full explanation why your feature request can't possibly be implemented.

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

      Firstly, thanks for your response, much appreciated =).
      could you please elaborate? I don't understand the question ...

      I'm referring to this %{{f}} code which creates a transient hash (which keys can then operate on) from a hash reference. Perhaps I'm not using the jargon correctly, my apologies.

      I don't know of more elegant ways to do this in other languages to be honest. I was hoping that it were possible to create a transient hash more directly with something like  keys %(f) for example.

      Creating a helper routine which hides this away or adjusting the f() routine to return a hash reference would both work, but the former sacrifices the brevity I'm after and the latter may not always be feasible.
      keys HASH operates on the datatype hash not list, which means something starting with a % sigil here
      Do you know if there's a reason that it can't operate on a list?
        > Do you know if there's a reason that it can't operate on a list?

        Lists are the most basic containers in perl syntax but not a variable's data type.

        A list is not necessarily a hash, you can have odd numbers and dupplicate keys or irregular "keys"

        Keys would also be more vulnerable to syntax errors.

        Many other languages don't even know how to handle lists.

        But you're free to implement the workarounds I've shown you.

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

Re: Using 'keys' on a list
by ikegami (Patriarch) on Jun 29, 2021 at 20:03 UTC

    The issue is that keys requires a hash (or an array), but you are (attempting to) provide it a bunch of scalars.

    But while keys doesn't help, working with such data is exactly the purpose of the pair* functions in core module List::Util.

    say for pairkeys f;

    You could also use grep, but it's a bit inelegant.

    sub pairkeys { my $i = 0; grep { $i ^= 1 } @_ }

    Seeking work! You can reach me at ikegami@adaelis.com

Re: Using 'keys' on a list
by Marshall (Canon) on Jun 30, 2021 at 00:46 UTC
    Perhaps consider this: Updated code to be perhaps more helpful...
    I think an issue here that when you "say", you have to say what you want to "say"!
    use strict; use warnings; use Data::Dumper; use v5.10; sub f {a=>1,b=>2} my %result = f(); #converts a a hash passed as an arry #back into a hash print Dumper \%result; print "to print just the keys of the hash:\n"; say "$_" for keys %result; # this is much faster, execution wise... # passes back a referecne to a hash that has # been already been created. sub x { my %result; $result{a}=1; #more likely way to set the results $result{b}=2; return \%result; } my $hash_ref = x(); print "to print the keys of this hash\n"; print "$_\n" for keys (%$hash_ref); print Dumper $hash_ref; __END__ $VAR1 = { 'b' => 2, 'a' => 1 }; to print just the keys of the hash: b a to print the keys of this hash a b $VAR1 = { 'a' => 1, 'b' => 2 };
    PS: I did find that say for keys %{{f}}; did indeed work.
    I didn't expect that result, but it does "work".
    I find say "$_" for keys %{{f}}; to be easier to understand. However, Perl is gonna make a hash from the return value of f() whether or not you give it name. I prefer the above syntax.
      # this is much faster, execution wise...

      then your benchmark is very different from mine

        I don't think a benchmark makes much sense with this very small contrived example. Most of the hashes I work with have a lot more than 2 keys! In general passing back a single thing (ref to a hash) is gonna be much faster than passing back a list of key/value pairs so that the hash can be re-created by the caller. The bigger the hash is, the more apparent this speed difference is going to be. With 2 keys, there isn't a huge difference in a practical sense (impact on total application performance might not be anything of note). With say 80,000 keys, there is a lot of difference between the 2 methods of passing back an entire hash!

        Now if all you need are the keys instead of the full hash, yes, there won't be much of a difference at all. The sub could traverse the hash and make a list. Or the sub gives the caller the ref and the caller traverses the hash to make a list. The effect and performance will be about the same. Sometimes these very short snippets of code don't reflect what is going on the in overall application. The code as formulated is a rather odd idea, pass a list of key/value pairs to the sub, only to ask what the keys are? Why have to make a hash in the first place? I dunno.

Re: Using 'keys' on a list
by glycine (Sexton) on Jun 29, 2021 at 17:57 UTC
    >Also, is there a way of creating a hash for consumption by 'keys' in the above situation which doesn't involve creating a hash reference from a list and then immediately dereferencing?

    this is because hash hope first argument is a hash or array, so if offer a list the first argument is first element of list.
    so keys just can't work on 'a' (in your example)...
    if want keys work on return values of f(), the list still need stored into hash or array.

    my %hash = f(); my @keys = keys %hash;
    but you hope finish in one line but don't want %{{}}
    I can't offer any to avoid hash reference, but if the pupose just look tidy
    maybe let subroutine f() return reference?
    so we can use:
    my @keys = keys %{f()}; #or in postfix dereferencing my @keys = keys f()->%*;
    hope this can help.

    edit:I see you say you don't want :(

      so keys just can't work on 'a' (in your example)...

      no, list in scalar context returns the last value

        A "list in scalar context" doesn't exist.
        - Ron
Re: Using 'keys' on a list
by Marshall (Canon) on Jul 06, 2021 at 23:41 UTC
    Further down in this thread, I got trolled by an Anon Monk.

    Instead of complicated syntax, I would recommend something easier.
    Performance-wise it is always better to return fewer things from the sub() that more things.
    That should be obvious!

    The fastest way (performance-wise) and often the easiest way for a sub() to return a hash to the caller is via a reference to the hash.
    Don't worry at all about taking a couple of lines of code vs one line. Sometimes 3 lines are faster and easier to understand than a single line.

    Here are some benchmarks for your perusal.
    Number (3) below is a "go to" winner!

    use strict; use warnings; use Benchmark; use Data::Dumper; # Note: You can increment a string in Perl! WOW! # As long as you don't use the string in a numeric context! # $string++ is quite different than $string +=1 # # Below, this is used to make a bunch of unique hash keys # I don't think that the fact that they are sequential in # an alphabetic sense makes much difference in the generated # hash table because of the way that the Perl hash algorithm # works. sub return_hash { # create a large hash and return that entire hash as list my $string = "ABCDEFGHIJ"; my %hash = map{$string++ => 1}(1..100000); return %hash; } sub return_hash_ref { # create a large hash and return a ref to that hash my $string = "ABCDEFGHIJ"; my %hash = map{$string++ => 1}(1..100000); return \%hash; } sub return_just_keys { # create a large hash and return just the keys of that hash my $string = "ABCDEFGHIJ"; my %hash = map{$string++ => 1}(1..100000); return keys %hash; } timethese(1000, { '1)Keys of Hash via list' => 'my @keys = keys %{{return_hash()}}', '2)Keys of Local Hash copy' => 'my %hash2 = return_hash(); my @key +s = keys %hash2;', '3)Keys of local Hash Ref' => 'my $href = return_hash_ref(); my @k +eys = keys %$href;', '4)Just the returned keys' => 'my @keys = return_just_keys()', }); __END__ Benchmark: timing 1000 iterations of 1)Keys of Hash via list, 2)Keys of Local Hash copy, 3)Keys of local Hash Ref, 4)Just the returned keys... 1)Keys of Hash via list: 182 wallclock secs (179.20 usr + 1.97 sys = +181.17 CPU) @ 5.52/s (n=1000) 2)Keys of Local Hash copy: 184 wallclock secs (182.69 usr + 0.42 sys += 183.11 CPU) @ 5.46/s (n=1000) 3)Keys of local Hash Ref: 130 wallclock secs (129.56 usr + 0.72 sys = + 130.28 CPU) @ 7.68/s (n=1000) 4)Just the returned keys: 125 wallclock secs (125.20 usr + 0.52 sys = + 125.72 CPU) @ 7.95/s (n=1000)
    Added: When looking at benchmarks, don't worry so much about 182 vs 184. That could be an artifact of a particular run. Most of the CPU MIP's are being consumed by creating and re-creating the hash. Look at ~180 vs ~130. The relative performance difference between the 2 methods of passing the result back is actually much, more than that because generating the hash "takes a lot of effort".

      I'm surprised that returning a list of keys is faster than returning a hashref and then getting its keys. A dereference requires more operations, but should it be that much?

      A good chunk of the time is spent in building the hash, so I added a baseline sub to measure it. I also reduced the number of iterations so it would run in reasonable time on my machine. And some minor formatting of results.

      This is perl 5, version 28, subversion 0 (v5.28.0) built for MSWin32-x +64-multi-thread
      # Adapted from https://www.perlmonks.org/?node_id=11134740 use strict; use warnings; use Benchmark; use Data::Dumper; # Note: You can increment a string in Perl! WOW! # As long as you don't use the string in a numeric context! # $string++ is quite different than $string +=1 # # Below, this is used to make a bunch of unique hash keys # I don't think that the fact that they are sequential in # an alphabetic sense makes much difference in the generated # hash table because of the way that the Perl hash algorithm # works. sub baseline { # create a large hash but return nothing my $string = "ABCDEFGHIJ"; my %hash = map{$string++ => 1}(1..100000); return; } sub return_hash { # create a large hash and return that entire hash as list my $string = "ABCDEFGHIJ"; my %hash = map{$string++ => 1}(1..100000); return %hash; } sub return_hash_ref { # create a large hash and return a ref to that hash my $string = "ABCDEFGHIJ"; my %hash = map{$string++ => 1}(1..100000); return \%hash; } sub return_just_keys { # create a large hash and return just the keys of that hash my $string = "ABCDEFGHIJ"; my %hash = map{$string++ => 1}(1..100000); return keys %hash; } timethese(200, { '1)Keys of Hash via list ' => 'my @keys = keys %{{return_hash()}} +', '2)Keys of Local Hash copy' => 'my %hash2 = return_hash(); my @key +s = keys %hash2;', '3)Keys of local Hash Ref ' => 'my $href = return_hash_ref(); my @ +keys = keys %$href;', '4)Just the returned keys ' => 'my @keys = return_just_keys()', '5)Baseline ' => 'my $res = baseline()', }); __END__ Benchmark: timing 200 iterations of 1)Keys of Hash via list , 2)Keys +of Local Hash copy, 3)Keys of local Hash Ref , 4)Just the returned ke +ys , 5)Baseline ... 1)Keys of Hash via list : 50 wallclock secs (49.44 usr + 1.52 sys = +50.95 CPU) @ 3.93/s (n=200) 2)Keys of Local Hash copy: 50 wallclock secs (49.98 usr + 0.38 sys = +50.36 CPU) @ 3.97/s (n=200) 3)Keys of local Hash Ref : 35 wallclock secs (34.75 usr + 0.34 sys = +35.09 CPU) @ 5.70/s (n=200) 4)Just the returned keys : 33 wallclock secs (32.51 usr + 0.16 sys = +32.67 CPU) @ 6.12/s (n=200) 5)Baseline : 25 wallclock secs (24.86 usr + 0.20 sys = +25.06 CPU) @ 7.98/s (n=200)
        I liked your idea of adding a "baseline"!
        I also really don't know either why generating the list of keys in the sub appears to be faster then giving the caller the ref and having him do it? I would have expected that difference to be smaller. Hopefully some other Monk knows?

        However, the main point remains: If the caller needs the whole hash, give him a ref to a hash. This is much faster than passing the entire hash back as a list. Of course there are memory allocation issues with that because Perl will keep the memory for that hash allocated as long as there is reference to it.

        Added: Except as a part of an object method, I don't know why a sub() in general would create a hash, only to just pass back just the keys? Seems a bit weird, but I'd also like to know why this appears to be somewhat faster.

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others having a coffee break in the Monastery: (6)
As of 2024-11-01 19:29 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    chatterbot is...






    Results (4 votes). Check out past polls.