Beefy Boxes and Bandwidth Generously Provided by pair Networks
Think about Loose Coupling
 
PerlMonks  

Is it safe to use join on a hash?

by pritesh_ugrankar (Monk)
on Sep 06, 2020 at 22:49 UTC ( [id://11121423]=perlquestion: print w/replies, xml ) Need Help??

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

Respected Monks,

The code given below is something I've written as a test.

use strict; use warnings; use diagnostics; my @nums = (17, 10, 20, 33, 30, 40, 10, 33, 40, 15, 16, 17); my %unordered = map { $_ => undef } @nums; my @un_nums = join " ", keys %unordered; print "@un_nums\n";

And it seems to work fine.

D:\perlscripts>perl test.pl 30 17 33 10 40 15 16 20

I'll be using this in a script to gather some information, therefore kindly let me know if 1) It's ok to use a join on a hash? 2) It's ok to collect the output of a join as an array context? Or is this a bad practice? Any side effects?

Replies are listed 'Best First'.
Re: Is it safe to use join on a hash?
by choroba (Cardinal) on Sep 06, 2020 at 22:53 UTC
    Using join on a hash would be kind of strange, but using it on the keys of a hash is quite common. Join has no side effects (it stringifies the elements, which might change the internal representation of the values if they are numbers, but from Perl perspective, they don't change; moreover, hash keys are always strings, anyway).

    To give predictable output, you might want to sort the keys before joining them:

    my $unique_nums = join ' ', sort keys %unordered;

    Update: See davido's post.

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
Re: Is it safe to use join on a hash?
by davido (Cardinal) on Sep 07, 2020 at 01:08 UTC

    Consider what you're doing. Perl is perfectly capable of using join on any list, including a list of hash keys. But that doesn't mean that your usage actually makes a whit of sense.

    my @nums = (17, 10, 20, 33, 30, 40, 10, 33, 40, 15, 16, 17);

    Here you have an array with the following elements: 17, 10, 20, 33, 30, 40, 10, 33, 40, 15, 16, 17. The number of elements in the array is 12.

    my %unordered = map { $_ => undef } @nums;

    Now you have a hash of key/value pairs where the keys are comprised of the elements that are in the @nums array. Except for one problem? 17 is repeated twice. 10 is repeated twice, 33 is repeated twice. 40 is repeated twice. Hash keys can only be unique. Duplicates replace previous instances. So now the keys are: 17, 10, 20, 33, 30, 40, 15, 16. So there are now 8 hash elements.

    my @un_nums = join " ", keys %unordered;
    What do you think @un_nums contains now? It contains a single element. $un_nums[0] contains a string: "17 10 20 33 30 40 15 16" (but with its numbers in unpredictable order). There is no $un_nums[1], or beyond. Only a single element. Of what use is that?

    print "@un_nums\n";

    Now this is funny: if @un_nums contained eight elements, or only one element that consists of a single string of eight numbers, the output here will be the same because by default the list separator $" is a single space by default (see perlop). Of course you're not invoking the list separator because your array contains only a single element. But what I'm saying is this:

    my @array1 = (1, 2, 3, 4, 5); my @array2 = ("1 2 3 4 5"); print "@array1\n"; print "@array2\n";

    These two will produce identical output.

    There's nothing wrong with using join on hash keys, nor is there anything wrong with using join to construct an element of an array. But it looks a lot like you're using it mistakenly in this case. Otherwise, why would you have assigned the string to @un_nums instead of $un_nums?

    So the effect of your code is to take a list of 12 elements, boil it down to a list of 8 unique elements in no particular order, and then convert those elements to a string, and store that string in the first (and only) element of an array. Then print all the elements of that array (of which there is only one; a string of 8 unique numbers in whatever order they ended up being placed in the string).


    Dave

      Hi David, Thanks. And I forgot to mention but I wanted to extract unique elements only. Still, your answer clears a lot of doubts.
      Thanks once again.

        The keys returned by keys are unique. The keys of a hash are always unique.


        Give a man a fish:  <%-{-{-{-<

Re: Is it safe to use join on a hash?
by BillKSmith (Monsignor) on Sep 07, 2020 at 02:07 UTC
    And it seems to work fine

    This suggests that you want an array of unique numbers. It would be better to use the "uniq" function of List::Util. This function preserves the original order. Note that I print colons (:) to separate array elements.

    use strict; use warnings; use List::Util qw(uniq); my @nums = (17, 10, 20, 33, 30, 40, 10, 33, 40, 15, 16, 17); my @un_nums = uniq(@nums); local $" = ":"; print "@un_nums\n";

    OUTPUT:

    17:10:20:33:30:40:15:16
    Bill
      It would be better to use the "uniq" function of List::Util

      Beware List::Util::uniq() with numbers.
      There's no problem with the integer values being thrown about in this thread, but uniq looks at the stringified values, and there are large numbers of floating point values (including many that represent large integer values) that are unequivalent, yet stringify to the same string:
      use warnings; use List::Util qw(uniq uniqnum); $x = sqrt 2.0; # Create $y with a less precise # approximation of sqrt 2 $y = "$x" + 0; #$y != $x print "unequivalent\n" if $x != $y; # But $x and $y both stringify # to 1.4142135623731, hence: $count = uniq($x, $y); print $count; __END__ # Outputs: unequivalent 1
      Using uniqnum instead of uniq yields a value of 2 for $count .
      If you want to weed out duplicate strings, use uniq or uniqstr .
      But if you want to safely weed out duplicate numeric values, use uniqnum ... and, even then, use only the uniqnum implementation that comes with List::Util-1.55 or later.
      (Actually, there might be other modules that now implement a uniqnum function correctly .... I haven't checked.)

      Cheers,
      Rob

      pritesh_ugrankar:   Note that List::Util::uniq() will return unique elements in the exact order of the input list (less duplicates, of course); this can be very useful in some cases. A duplicate removal approach like that shown here will return unique elements in random order.

      Also note that in older (and also in current) versions of Perl, uniq() can also be found in the List::MoreUtils module; it has not always lived in List::Util.


      Give a man a fish:  <%-{-{-{-<

Re: Is it safe to use join on a hash?
by GrandFather (Saint) on Sep 07, 2020 at 03:20 UTC

    You may like:

    use strict; use warnings; my @nums = (17, 10, 20, 33, 30, 40, 10, 33, 40, 15, 16, 17); my %values; ++$values{$_} for @nums; print join "\n", map{"$_: $values{$_}"} sort {$a <=> $b} keys %values;

    which prints an ordered list of the unique values and their counts:

    10: 2 15: 1 16: 1 17: 2 20: 1 30: 1 33: 2 40: 2
    Optimising for fewest key strokes only makes sense transmitting to Pluto or beyond
Re: Is it safe to use join on a hash?
by clueless newbie (Curate) on Sep 06, 2020 at 22:55 UTC

    You're using join on keys of a hash - which is a list.

Re: Is it safe to use join on a hash?
by perlfan (Vicar) on Sep 07, 2020 at 04:09 UTC
    In addition to relying on the return of keys or values, a hash is just a list. It can be seen an array where there exists an ordering between each tuple (key, value). BUT there is no ordering among tuples. The => is also known as a fat comma.

    This is why you can assign a hash, equivalently like:

    my %hash1 = ('keyA' => 'valA', 'keyB' => 'valB'); my %hash2 = ('keyA', 'valA', 'keyB', 'valB'); my %hash3 = ('keyA' => 'valA'=> 'keyB' => 'valB');

    And use it to capture arguments in a subroutine into a hash, like:

    sub mySub { my %args = @_ #... do stuff } # and call it equivalently like mySub ('keyA' => 'valA', 'keyB' => 'valB'); mySub ('keyA', 'valA', 'keyB', 'valB');

    This is also why you may see the error, "Odd number of elements in anonymous hash". It's because list assignment to a hash must have an even number of elements (i.e., key/value pairs).

    Finally, to get back to your question. It is not only safe to use join on the output of keys, but also on the hash itself:

    my $stringA = join ',', %hashA; #additionally, this is valid (to further demonstrate the point): my @arrayA = %hashA;

    It bears repeating that hash to array assignment, key/value ordering between pairs is necessarily retained, but ordering among tuples is not guaranteed.

    update: fixed spelling of comma!

Re: Is it safe to use join on a hash?
by pritesh_ugrankar (Monk) on Sep 07, 2020 at 17:44 UTC

    Respected Monks,

    Thank you for your patience and readiness to help and guide. I would love to upvote the answers, but I do not see the upvote option anymore for any of the answers. Is it temporarily disabled?

      I do not see the upvote option anymore for any of the answers. Is it temporarily disabled?

      Likely you've just used up all your votes for the day; you'll have to wait till tomorrow, the vote fairly will visit during the night.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11121423]
Approved by Athanasius
Front-paged by haukex
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: (4)
As of 2024-04-19 21:23 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found