http://qs321.pair.com?node_id=459286

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

I am writing a subroutine that chooses a random key from a hash, but the choice is weighted depending on the value of the hash. The keys are a list of words from a text, the values are the frequency of occurence. (This is from the online book "Learning Perl the Hard Way" http://greenteapress.com/perl/)

anyway I don't understand the syntax error being flagged:

# sub weighted_hash # chooses and returns a random key from a hash; # the probability of choosing a specific key # is weighted by the value associated with that key sub weighted_hash{ my $hashref = shift; my %hash = $$hashref; my @weighted; foreach my $key (keys %hash){ my $weight = $hash{$key}; while ($weight >0){ push @weighted $key; $weight--; } } rand_elt @weighted; }
update: changed
push @weighted $hash{$key);
to
push @weighted $key;error still there

Replies are listed 'Best First'.
Re: "Scalar found where operator expected", references
by dave_the_m (Monsignor) on May 22, 2005 at 00:24 UTC
    In addition to the missing comma, the following line is nonsense:
    my %hash = $$hashref;
    You can either make a complete copy of the hash by doing
    my %hash = %$hashref;
    or more efficiently, just delete that line and always directly use the hashref, eg
    foreach my $key (keys %$hashref) { my $weight = $hashref->{$key}; ...

    Dave.

Re: "Scalar found where operator expected", references
by fglock (Vicar) on May 21, 2005 at 23:31 UTC

    You need a comma between the array and the scalar:

    push @weighted, $key;
Re: "Scalar found where operator expected", references
by sh1tn (Priest) on May 22, 2005 at 00:15 UTC
Re: "Scalar found where operator expected", references
by Roger (Parson) on May 22, 2005 at 09:21 UTC
    Other monks have already pointed out the bug and offered suggestions. I just want to show that there are more than one way to do it in Perl, depending on, personal taste, I'd say.

    # Implementation 1 - the mundane way sub weighted_hash { my $hashref = shift; my @weighted; for my $key (keys %$hashref) { for (1 .. $hashref->{$key}) { push @weighted, $key; } } rand_elt @weighted; } # Implementation 2 - the perl monk one-liner sub weighted_hash { my $hashref = shift; rand_elt map { my $key = $_; map { $key } 1 .. $hashref->{$key} } keys %$hashref; }


Re: "Scalar found where operator expected", references (perl6)
by mrborisguy (Hermit) on May 22, 2005 at 16:35 UTC

    Just thought I'd plug a few Perl6 ways of doing it, for future reference.

    my %hash = ( word => 1, second => 3, third => 7 ); ###### my @weighted = (); for %hash.keys -> $key { push @weighted, ( $key ) xx %hash{ $key }; } say @weighted.pick; ###### Basically the same thing: my @weighted = (); push @weighted, ( $_ ) xx %hash{ $_ } for %hash.keys; say @weighted.pick; ###### One-liner style. *Somewhat* based on Roger's above. %hash .keys .map:{ ( $_ ) xx %hash{ $_ } } .pick .say; ## (or actually on one line) %hash.keys.map:{ ( $_ ) xx %hash{ $_ } }.pick.say;

    (And if one of those Perl6 experts comes along, tell him or her that I'm still kinda new to p6, so there might even be better ways to do it! ;)

    -Bryan

Re: "Scalar found where operator expected", references
by TedPride (Priest) on May 22, 2005 at 16:45 UTC
    You need two passes, one to count the total number of words, the second to pick a word based on the rand value.
    use strict; use warnings; my (%h, %c); while (<DATA>) { chomp; @_ = split / /; $h{$_[0]} = $_[1]; } $c{weighted(\%h)}++ for 1..1000; print "$_ : $c{$_}\n" for sort {$c{$b} <=> $c{$a}} keys %c; sub weighted { my ($p, $c) = shift; $c += $_ for values %$p; $c = (int rand $c) + 1; for (keys %$p) { return $_ if ($c -= $p->{$_}) <= 0; } } __DATA__ gamma 7 beta 6 epsilon 3 alpha 2 delta 1