Smart Matching and given-when

The Smart Match Operator

The smart match operator, ~~, looks at both of its operands and decides on its own how it should compare them. If the operands look like numbers, it does a numeric comparison. If they look like strings, it does a string comparison. If one of the operands is a regular expression, it does a pattern match. It can also do some complex tasks that would otherwise take a lot of code, so it keeps you from doing too much typing.

examples:

use 5.010;
# these 2 are the same
print "I found Fred in the name!\n" if $name =~ /Fred/;
say "I found Fred in the name!" if $name ~~ /Fred/;

The smart match operator starts to show its power with more complex operations. Suppose you wanted to print a message if one of the keys in the hash %names matches Fred. You can’t use exists because it only checks for the exact key. You could do it with a foreach that tests each key with the regular expression operator, skipping those that don’t match. When you find one that does match, you can change the value of $flag and skip the rest of the iterations with last:

my $flag = 0;
foreach my $key ( keys %names ) {
    next unless $key =~ /Fred/; 
    $flag = $key;
    last;
}
print "I found a key matching 'Fred'. It was $flag\n" if $flag;

Whew! That was a lot of work just to explain it, but it works in any version of Perl 5. With the smart match operator, you just need the hash on the lefthand side and the regular expression operator on the righthand side:

use 5.010;
say "I found a key matching 'Fred'" if %names ~~ /Fred/;

The smart match operator knows what to do because it sees a hash and a regular expression. With those two operands, the smart match operator knows to look through the keys in %names and apply the regular expression to each one. If it finds one that matches, it already knows to stop and return true. It’s not the same sort of match as the scalar and regular expression. It’s smart; it does what’s right for the situation. It’s just that the operator is the same, even though the operation isn’t.

If you want to compare two arrays (limiting them to the same size just to make things simpler), you could go through the indices of one of the arrays and compare the corresponding elements in each of the arrays. Each time the corresponding elements are the same, you increment the $equal counter. After the loop, if $equal is the same as the number of elements in @names1, then the arrays must be the same:

my $equal = 0;
foreach my $index ( 0 .. $#names1 ) {
    last unless $names1[$index] eq $names2[$index]; 
    $equal++;
}
print "The arrays have the same elements!\n" if $equal == @names1;

With smart match operator, the code can be quite simple:

use 5.010;
say "The arrays have the same elements!" if @names1 ~~ @names2;

Okay, one more example. Suppose you call a function and want to check that its return value is one of a set of possible or expected values. Going back to the max() subroutine, you know that max() should return one of the values you passed it. You could check that by comparing the return value of max to its argument list using the same techniques as the previous hard ways:

use List::Util qw[max min];
my @nums = qw( 1 2 3 27 42);
my $result = max(@nums);

my $flag = 0;
foreach my $num ( @nums ) {
    next unless $result == $num; 
    $flag = 1;
    last;
}

print "The result is one of the input values\n" if $flag;

With smart match operator, the code would be:

use 5.010;
my @nums = qw( 1 2 3 27 42 ); 
my $result = max( @nums );
say "The result [$result] is one of the input values (@nums)" if @nums ~~ $result;

You can also write that smart match with the operands in the other order and get the same answer. The smart match operator doesn’t care which operands are on which side. The smart match operator ~~ is no longer commutative. See https://metacpan.org/pod/release/JESSE/perl-5.12.2/pod/perl5120delta.pod#Smart-match-changes for more details.

use 5.010;
my @nums = qw( 1 2 3 27 42 ); 
my $result = max( @nums );
say "The result [$result] is one of the input values (@nums)" if $result ~~ @nums;

The smart match operator is commutative, which you may remember from high school algebra as the fancy way to say that the order of the operands doesn’t matter.

Smart Match Precedence

Below is a table showing what smart match operator can do and their precedence:

Smart match operations for pairs of operands

ExampleType of match
%a ~~ %bHash keys identical
%a ~~ @bAt least one key in %a is in @b
%a ~~ /Fred/At least one key matches pattern
%a ~~ 'Fred'Hash key exists $a{Fred}
@a ~~ @bArrays are the same
@a ~~ /Fred/At least one element matches pattern
@a ~~ 123At least one element is 123, numerically
@a ~~ 'Fred'At least one element is 'Fred', stringwise
$name ~~ undef$name is not defined
$name ~~ /Fred/Pattern match
123 ~~ '123.0'Numeric equality with “numish” string
'Fred' ~~ 'Fred'String equality
123 ~~ 456Numeric equality

When you use the smart match operator, Perl goes to the top of the chart and starts looking for a type of match that corresponds to its two operands. It then does the first type of match it finds. The order of the operands doesn’t matter.

The given Statement

The given-when control structure allows you to run a block of code when the argument to given satisfies a condition. It’s Perl’s equivalent to C’s switch statement, but as with most things Perly, it’s a bit more fancy, so it gets a fancier name.

Here’s a bit of code that takes the first argument from the command line, $ARGV[0], and goes through the when conditions to see if it can find Fred. Each when block reports a different way that it found Fred, starting with the least restrictive to the most:

use 5.010;
given( $ARGV[0] ) {
    when( /fred/i ) { say 'Name has fred in it' } 
    when( /^Fred/ ) { say 'Name starts with Fred' } 
    when( 'Fred' ) { say 'Name is Fred' }
    default { say "I don't see a Fred" }
}

The given aliases its argument to $_,* and each of the when conditions tries an implicit smart match against $_. You could rewrite the previous example with explicit smart matching to see exactly what’s happening:

use 5.010;
given( $ARGV[0] ) {
    when( $_ ~~ /fred/i ) { say 'Name has fred in it' }
    when( $_ ~~ /^Fred/ ) { say 'Name starts with Fred' }
    when( $_ ~~ 'Fred' )  { say 'Name is Fred' }
    default { say "I don't see a Fred" }
}

Note*: In Perl parlance, given is a topicalizer because it makes its argument the topic, the fancy new name for $_ in Perl 6.

Unless you say otherwise, there is an implicit break at the end of each when block, and that tells Perl to stop the given-when construct and move on with the rest of the program. The previous example really has breaks in it, although you don’t have to type them yourself:

use 5.010;
given( $ARGV[0] ) {
    when( $_ ~~ /fred/i ) { say 'Name has fred in it'; break; }
    when( $_ ~~ /^Fred/ ) { say 'Name starts with Fred'; break; }
    when( $_ ~~ 'Fred' ) { say 'Name is Fred'; break; }
    default { say "I don't see a Fred"; break; }
}

If you use continue at the end of a when instead, Perl tries the succeeding when statements too, repeating the process it started before. That’s something that if-elsif-else can’t do. When another when satisfies its condition, Perl executes its block (again, implicitly breaking at the end of the block unless you say otherwise). Putting a continue at the end of each when block means Perl will try every condition:

given( $ARGV[0] ) {
    when( $_ ~~ /fred/i ) { say 'Name has fred in it'; continue; }
    when( $_ ~~ /^Fred/ ) { say 'Name starts with Fred'; continue; }
    when( $_ ~~ 'Fred' ) { say 'Name is Fred'; }
    default { say "I don't see a Fred"; }
}

!!!Note!!!: when testing with "Fredx", you will get the first 2 when and the default executed. See this post for details: http://grokbase.com/t/perl/beginners/10bthx24xk/given-when-problem

Dumb Matching

Although the given-when can use smart matching, you can use the “dumb” comparisons that you already know. It’s not really dumb, it’s just the regular matching that you already know. When Perl sees an explicit comparison operator (of any type) or the binding operator, it does only what those operators do:

given( $ARGV[0] ) {
    when( $_ =~ /fred/i ) { say 'Name has fred in it'; continue; }
    when( $_ =~ /^Fred/ ) { say 'Name starts with Fred'; continue; }
    when( $_ =~ 'Fred' ) { say 'Name is Fred'; }
    default { say "I don't see a Fred"; }
}

You can even mix and match dumb and smart matching; the individual when expressions figure out their comparisons on their own:

use 5.010;

given( $ARGV[0] ) {
    when( /fred/i ) { #smart 
        say 'Name has fred in it'; continue } 
    when( $_ =~ /^Fred/ ) { #dumb
        say 'Name starts with Fred'; continue } 
    when( 'Fred' ) { #smart
        say 'Name is Fred' }
    default { say "I don't see a Fred" } 
}

Note that the dumb and smart match for a pattern match are indistinguishable since the regular expression operator already binds to $_ by default.

There are certain situations in which Perl will automatically use dumb matching. You can use the result of a subroutine inside the when, in which case, Perl uses the truth or falseness of the return value:

use 5.010;
given( $ARGV[0] ) {
    when( name_has_fred( $_ ) ) { #dumb
        say 'Name has fred in it'; 
        continue 
    } 
}

The subroutine call rule also applies to the Perl built-ins defined, exists, and eof too, since those are designed to return true or false. Negated expressions, including negated regular expressions, don’t use a smart match either. These cases are just like the control structure conditions you saw in previous chapters.

when with Many Items

Sometimes you’ll want to go through many items, but given takes only one thing at a time. In this case, you could wrap given in a foreach loop. If you wanted to go through @names, you could assign the current element to $name, then use that for given:

use 5.010;
foreach my $name ( @names ) { 
    given( $name ) {
        #...
    } 
}

To go through many elements, you don’t need the given. Let foreach put the current element in $_ on its own. If you want to use smart matching, the current element has to be in $_.

use 5.010;

foreach ( @names ) { # don't use a named variable!
    when( /fred/i ) { say 'Name has fred in it'; continue } 
    when( /^Fred/ ) { say 'Name starts with Fred'; continue }   
    when( 'Fred' ) { say 'Name is Fred'; }
    default { say "I don't see a Fred" }
}

If you are going to go through several names though, you probably want to see which name you’re working on. You can put other statements in the foreach block, such as a say statement:

use 5.010;

foreach ( @names ) { # don't use a named variable!
    say "\nProcessing $_";
    
    when( /fred/i ) { say 'Name has fred in it'; continue } 
    when( /^Fred/ ) { say 'Name starts with Fred'; continue }   
    when( 'Fred' ) { say 'Name is Fred'; }
    default { say "I don't see a Fred" }
}

You can even put extra statements between the whens, such as putting a debugging
statement right before the default (which you can also do with given):

use 5.010;

foreach ( @names ) { # don't use a named variable!
    say "\nProcessing $_";
    
    when( /fred/i ) { say 'Name has fred in it'; continue } 
    when( /^Fred/ ) { say 'Name starts with Fred'; continue }   
    when( 'Fred' ) { say 'Name is Fred'; }
    say "Moving on to default...";
    default { say "I don't see a Fred" }
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注