7

Part of the specification says "Some names are special, e.g. Hughie, Dewey, Louis, and Donald. Other names may be added over the lifetime of the project at arbitrary times. Whenever you input one of those names, play quack.wav."

I could write ...

while (<>) {
    if ($_ =~ /Hughie|Dewey|Louis/) {
        quack() ;
    }
    elsif ($_ =~ /Donald/ {
        quack() ;
        you_re_fired_apprentice() ; # Easter egg don't tell QA
    }
}

... but though untaxing to implement, it looks WTF-y: Where's the binary search? What if there were a sudden stupendous increase in the number of duck names? It would not scale at all!

I could create empty files using those names in a temporary directory, and then use the "file exists" API, but that seems roundabout, and I would have to be sure they were deleted at the end.

Surely there is a better way?

Thomas L Holaday
  • 13,614
  • 6
  • 40
  • 51
  • 4
    **‘In’?** Did you say *‘in’?* Whenever you catch yourself using words like *in, once, first, unique, union, intersection, set, record, struct, union,* or indeed several other unrelated words as well, you should be having an instant, knee-jerk, Pavlovian response of a Eureka lightbulb going off in your head shouting **HASH!! I SHOULD BE USING A HASH!! OF COURSE!!!** – tchrist Feb 01 '11 at 04:16
  • 2
    Never trust anybody who writes `$_ =~ /whatever/`. Ever. – tchrist Feb 01 '11 at 04:20
  • @tchrist: you've never encountered the always-specify-$_-even-when-its-the-default crowd? how about the `my @foo = ();` (just in case) crowd? – ysth Feb 01 '11 at 05:32

6 Answers6

7

You could write that, but you should write this:

my %ducks = map {$_ => 1} qw(Hughie Dewey Louis);

while (<>) {
    if ($ducks{$_}) {
        quack() ;
    }
    elsif ($_ eq 'Donald') {
        quack() ;
        you_re_fired_apprentice() ; # Easter egg don't tell QA
    }
}

Creating the hash takes a little bit of time, but not more than O(n). Lookup with a hash is O(1) though, so it is much more efficient than sequential search (via grep or a regex with alternation) assuming you will be checking for more than one or two items.

By the way, the regex that you have will match the words anywhere in the search string. You need to add start and end anchors if you want an exact match.

Eric Strom
  • 39,821
  • 2
  • 80
  • 152
7

Alternatively, you could use smart matching

my @ducks = qw(Hughie Dewey Louis);
my $name = 'Dewey';

say 'smart match' if $name ~~ @ducks;

This is what is used by switch statements, so you could write

given ($name) {
    when (@ducks) {
        quack();
    }
    when ('Donald') {
        quack();
        you_re_fired_apprentice(); # Easter egg don't tell QA
    }
}
oylenshpeegul
  • 3,404
  • 1
  • 18
  • 18
  • Yay! I was afeared [sic] nobody was going to mention `~~`. Still, you’re still back to O(N). BTW, oughtn’t that be `you're_fired_apprentice()`? ☺ – tchrist Feb 01 '11 at 04:13
  • Yeah, if nothing else, it seems like the most concise answer. Pavlovian responses aside, 'in' can usually be spelled '~~' too. – oylenshpeegul Feb 01 '11 at 11:35
  • Search engines are reluctant to report results for the ~~ operator. – Thomas L Holaday Feb 01 '11 at 13:29
  • +1 for the concise answer. smart matching is now a new tool in my Perl Utility Belt, thanks. – Day Feb 16 '11 at 19:03
4

As mentioned, hashes are the way to go for this. This is sort of what OOP looked like before OOP.

use strict;
use warnings;

my %duck_action = (
  Donald => sub {quack(); you_re_fired_apprentice()},
  Hughie => sub {quack()},
  Dewie  => sub {quack()},
  Louis  => sub {quack()},
);

for my $duck (qw( Hughie Dewie Donald Louis Porkie )) {
    print "$duck: ";
    my $action = $duck_action{$duck} || &no_such_duck;
    $action->();
}

sub quack {
    print "Quack!\n";
}

sub you_re_fired_apprentice {
    print "You're fired!\n";
}

sub no_such_duck {
    print "No such duck!\n";
}
Bill Ruppert
  • 8,956
  • 7
  • 27
  • 44
3

You can use a Perl Hash. See also How can I represent sets in Perl? and Representing Sets in Perl.

Using hashes to implement a set is not exactly pretty, but it should be fast.

Community
  • 1
  • 1
thkala
  • 84,049
  • 23
  • 157
  • 201
  • 5
    Using hashes to **implement** (not ‘simulate’) a set data type is both perfectly natural and reasonably clean in its expressivity. – tchrist Feb 01 '11 at 04:10
  • @tchrist: the "not exactly pretty" was referring to the necessity for dummy values, as compared to e.g. the sets in Python where no dummy values are visible to the user. – thkala Feb 01 '11 at 09:16
2

To find a string in a list, you could also use any in List::MoreUtils

use List::MoreUtils qw(any);

my @ducks = qw(Hughie Dewey Louis);
my $name = 'Dewey';

say 'any' if any {$name eq $_} @ducks;
oylenshpeegul
  • 3,404
  • 1
  • 18
  • 18
1

If you're tied to using an array rather than a hash, you can use perl's grep function to search the array for a string.

@specialnames = qw(Hughie Dewey Louis);
while (my $value = <>) {
    if (grep {$value eq $_}, @specialnames) {
        quack() ;
    }
    elsif ($_ =~ /Donald/ {
        quack() ;
        you_re_fired_apprentice() ; # Easter egg don't tell QA
    }
}

This does scale a lot worse than a hash, and might even scale worse than copying the array into a hash and then doing hash lookups.

Wooble
  • 87,717
  • 12
  • 108
  • 131
  • I had a recent (last 3 weeks) answer on SO that analyzed in detail what conditions prompt using which list searching techniques. – DVK Feb 01 '11 at 02:10
  • 1
    @DVK this one --> http://stackoverflow.com/questions/4678195/binary-search-in-an-array-in-perl/4679652#4679652 ? – Thomas L Holaday Feb 01 '11 at 13:44