NAME
Switch::Right - Switch and smartmatch done right this time
VERSION
This document describes Switch::Right version 0.000005
SYNOPSIS
use Switch::Right;
given ($value) {
when (undef) { say 'an undefined value' }
when (1) { say 'the number 1' }
when ('two') { say 'the string "two"' }
when (\@array) { say 'an identical arrayref' }
when (\%hash) { say 'an identical hashref (keys AND values)' }
when (qr/pat/) { say 'a non-reference that matched the pattern' }
when (\&foo) { say 'foo($value) returned true' }
when ( /pat/) { say 'a non-ref that matched the pattern' }
when (!/pat/) { say 'a non-ref that didn't match the pattern' }
when (0 <= $_ <= 9) { say 'a singe digit' }
when (defined && length > 0) { say 'a non-empty string' }
when (-r || -w) { say 'an accessible file' }
when (exists $hash{$_}) { say 'a key of the hash' }
when ( any => [keys %hash]) { say 'a key of the hash' }
when ( any => [0..9]) { say 'a single-digit number' }
when ( all => [qr/foo/, qr/bar/]) { say 'matched /foo/ and /bar/' }
when (none => \@prohibited) { say 'not a prohibited value' }
default { say 'none of the above'; }
}
DESCRIPTION
After 17 years as a core feature of Perl, 'given'
/'when'
and smartmatching are going away.
And for some very good reasons! Smartmatching is just too damn clever for most people (including its inventor!) to be able to easily remember all the rules about what matched what.
And the weird extra-special special-case behaviour of some (but not all) boolean expressions in a when
only makes things even worse.
Hindsight is supposedly 20/20 and — in hindsight — smartmatching should have been a lot simpler, a lot more explicit, and a lot easier to remember/predict.
That's what this module attempts to accomplish: to redesign the smartmatching and the given
/when
mechanisms so that they're easier to use and easier to understand.
It implements a version of smartmatching with only six rules to remember, eliminates all of that magical auto-distributivity of when
expressions, and provides clearer and more explicit ways to specify all those complicated and hard-to-remember special-case "match any of..." and "match all of..." behaviours.
You could think of it as the 'switch'
feature from an alternate timeline, Or you can think of it as a second chance: switch re-imagined...and done right this time.
Above all, this module is supposed to be useful. Once you're using Perl v5.42 you won't be able to write the old standard given
/when
blocks any more, but you'll still be able to write these new given
/when
blocks, with simpler semantics and clearer syntax.
INTERFACE
given
/when
redesigned
This module aims to greatly simplify the way that given
/when
switches operate. Mostly by greatly simplifying the smartmatching behaviours that a switch employs to select which when
to execute within its given
.
The given
/when
/default
syntax itself has not been significantly changed, with only one major addition: explicit junctives, which replace most of the previous complex magic of smartmatching.
The given
block still takes a single argument, which is evaluated in scalar context, and then aliased to a localized $_
in the scope of the given
's block. The argument to the given
is still simplified at compile-time. If the argument is an array, an array slice, a hash, or a kv-slice, it is still automatically converted to a reference (i.e. rather than to a count).
A when
block still takes a single argument, which is still compile-time folded and auto-enreferenced. The argument is still evaluated in scalar context, and then smartmatched against $_
(but now using the new simplified smartmatch semantics). If the when
's argument successfully smartmatches then its block is executed. At the end of that block, control then immediately jumps out of the surrounding given
or for
.
A default
block still takes a no argument and unconditionally executes its block, after which control then immediately jumps out of the surrounding given
or for
.
A call to the break
function immediately jumps out of the surrounding given
block.
A call to the continue
function immediately jumps out of the surrounding when
block and moves on to the following statement within the current given
or for
.
Differences from the old built-in given
/when
Junctive given
/when
arguments
Apart from the major changes in how given
/when
smartmatches, the most significant difference in this new approach is that some arguments passed to a given
or to a when
can now be optionally qualified with a junctive indicator: any
, all
, or none
.
For example:
for (readline()) {
chomp;
when (any => ["quit", "exit"]) { exit }
when (all => ["", @history == 0]) { warn "Can't repeat last input" }
when (all => ["", @history != 0]) { $input = $history[-1]; continue }
when (none => ["", qr/$old_format|$new_format/]) { warn "Unknown format" }
default { push @history, $input // $_ }
}
given (any => @history) {
when (undef) { die "Internal error: undefined input" }
when (-1) { die "Old-style -1 terminator detected" }
when ($_ > 99) { warn "Big values ($_) may be slow" }
}
These work more-or-less as you'd expect. The junctive keyword must always be followed by an array or an array reference. A when (any => @LIST)
or when (any => ['ref', 'to', 'array'])
succeeds if any of the elements of the array smartmatch against $_
. Likewise a when (all => @LIST)
or when (all => $ARRAYREF)
only succeeds if all the array elements smartmatch against $_
, and a when (none => @LIST)
or when (none => \@LIST)
only succeeds if none of the array elements smartmatch.
A given (any => @LIST)
or given (any => \@LIST)
modifies the subsequent smartmatching behaviour of the entire given
block, so that every when
within that block will succeed if any of the given
's array values matches the when
expression. Likewise, given (all => @LIST)
, and given (none => @LIST)
cause each nested when
to match only if that when
's expression smartmatches all/none of the given
's list of values.
You can also use the junctive forms in any postfix when
statement modifier (so long as they're in a given
):
for (readline()) {
chomp;
given ($_) {
exit when any => ["quit", "exit"];
warn "Can't repeat last input" when all => ["", @history == 0];
$input = $history[-1], continue when all => ["", @history != 0];
warn "Unknown format" when none => ["", qr/$old_format|$new_format/];
push @history, $input // $_;
}
}
This mechanism is designed to replace (and extend) the previous complex "vector" matching behaviours that the built-in given
/when
used to provide. And which very few people could ever remember how to use. Instead of a large number of (somewhat inconsistent) rules for smartmatching against a list of alternatives, you now just indicate explicitly that any match is sufficient, or all matches are necessary, or that none of them are permitted.
Note that the syntax for these junctive arguments to given
and when
is currently hard-coded and evaluated at compile-time. You must literally write any =>
, all =>
, and none =>
with the arrow syntax (not a comma), and no quoting on the all
/any
/none
, in order to correctly specify them.
So you can't, for example, use equivalent forms like 'any',
or qw<all> =>
or $NONE =>
. Note that this restriction might be relaxed in future releases of the module.
Non-distributive given
/when
arguments
Another change in the behaviour of a when
block is that, if the argument is a boolean expression involving and
, or or
, &&
, or ||
, that expression no longer has its smartmatching behaviour (sometimes) distributed across the components of the expression.
That is, whereas the former built-in when
preprocessed such boolean expressions using a complex set of rules:
when (/^\d+$/ && $_ < 75 ) # means: if ($_ =~ /^\d+$/ && $_ < 75 )
when ([qw(foo bar)] && /baz/ ) # means: if ($_ ~~ [qw(foo bar)] && $_ ~~ /baz/ )
when ([qw(foo bar)] || /^baz/) # means: if ($_ ~~ [qw(foo bar)] || $_ ~~ /^baz/)
when (/^baz/ || [qw(foo bar)]) # means: if ($_ =~ /^baz/ || [qw(foo bar)] )
when ("foo" or "bar" ) # means: if ($_ ~~ "foo" )
...this module always treats the contents of the parens as an expression to be compile-time simplified and then passed directly to smartmatch()
. No magical recomposition of the boolean expression is ever attempted. Hence, under this module those same when
expressions now mean:
when (/^\d+$/ && $_ < 75 ) # means: if (smartmatch($_, /^\d+$/ && $_ < 75) )
when ([qw(foo bar)] && /baz/ ) # means: if (smartmatch($_, [qw(foo bar)]) )
when ([qw(foo bar)] || /^baz/) # means: if (smartmatch($_, [qw(foo bar)]) )
when (/^baz/ || [qw(foo bar)]) # means: if (smartmatch($_, /^baz/ || [qw(foo bar)]))
when ("foo" or "bar" ) # means: if (smartmatch($_, "foo") )
Only the first of those will actually do what was probably intended. Whenever you would previously have used a "magic boolean expression" in a when
, you almost certainly will now want to use an explicit junctive:
when (all => [ /^\d+$/, $_ < 75 ] ) # means: when $_ smartmatches both
when (all => [ /foo|bar/, /baz/ ] ) # means: when $_ smartmatches both
when (any => [ qw(foo bar), /^baz/ ] ) # means: when $_ smartmatches either
when (any => [ /^baz/, qw(foo bar) ] ) # means: when $_ smartmatches either
when (any => [ "foo", "bar" ] ) # means: when $_ smartmatches either
If you previously needed to use very complex distributed expressions in a when
, it's likely that the new semantics will no longer support that directly. In such cases, you can always factor the test out into a subroutine. For example, instead of:
when (\&prime || 0 and length == 1 || q/2/) { say "found: $_" }
...which won't work under this module (and which didn't actually work as most people might have expected under the former built-in feature!), you could write:
sub is_special ($n) {
is_prime($n) || $n==0 and length($n) == 1 || $n =~ qr/2/;
}
# and later...
when (\&is_special) { say "found $_" }
The version of given
/when
provided by this module is otherwise identical in design to the former built-in constructs, though entirely different in its implementation. That difference leads to several additional limitations on its usage. See "LIMITATIONS" for more details of these restrictions.
Smartmatching re-imagined
The heart of this module is a near-complete change in the way smartmatching works. Rather than the 23 rules of the former built-in ~~
operator, this module provides a two-argument smartmatch()
subroutine with a single meta-rule (that the right-hand argument always determines the kind of matching used) and only six core rules for what kind of matching that right-hand argument selects:
- 1. A boolean: match by returning that arg (ignoring the left arg)
- 2. A ref or
undef
: match if left arg has the same type/contents - 3. A subroutine ref: match by calling the sub, passing the left arg
- 4. A
qr
regex: match a non-reference left arg by pattern matching - 5. A numeric value: match using numeric equality
- 6. Any other value: match using string equality
Or, as a table:
Any true always true (ignores left arg)
Any false always false (ignores left arg)
undef undef always true
CODE CODE same reference
Any CODE result of calling sub, passing left arg
REGEXP REGEXP same reference or identical contents
NonRef REGEXP stringify left arg and pattern-match
ARRAY ARRAY recursively smartmatch each pair of elements
HASH HASH same keys and all corresponding values smartmatch
OtherRef OtherRef referential equality (LEFT == RIGHT)
Numlike Number numeric equality (LEFT == RIGHT)
NonRef NonRef string equality (LEFT eq RIGHT)
Other Other always false
Note that the type of the right-hand argument is determined as follows:
true builtin::is_bool( $RIGHT ) && $RIGHT
false builtin::is_bool( $RIGHT ) && !$RIGHT
undef !defined( $RIGHT )
Ref builtin::reftype( $RIGHT )
NonRef !builtin::reftype( $RIGHT )
Number builtin::created_as_number( $RIGHT )
String builtin::created_as_string( $RIGHT )
Differences from the ~~
operator
The smartmatching behaviours described in the preceding section are obviously very different from the former ~~
operator. In particular...
The arguments being smartmatched are no longer auto-enreferenced
The former ~~
operator allowed you to pass two container variables as arguments, and have them automatically converted to references (rather than flattening them or converting them to element counts in the operator's scalar context).
@A1 ~~ @A2 # same as: \@ARRAY ~~ \@ARRAY
%H1 ~~ %H2 # same as: \%HASH ~~ \%HASH
However, the smartmatch()
subroutine is just an ordinary Perl subroutine, so it can't perform the same magical auto-conversion. Instead, it does what every other plain subroutine does: it evaluates its argument list in list context, which causes any array argument to flatten to a list of the array's values, and any hash argument to flatten to a key/value list of the hash's entries:
smartmatch(@A1, @A2) # same as: smartmatch($A1[0], $A1[1],...,$A2[0], $A2[1],...)
smartmatch(%H1, %H2) # same as: smartmatch(k=>$H1{k}, l=>$H1{l},...,x=>$H2{x}, y=>$H2{y},...)
This will usually result in a run-time error indicating that there is no suitable variant of smartmatch()
that can handle 17 arguments (or however many args your two containers happened to flatten down to).
To smartmatch two container variables, pass a reference to each of them instead:
smartmatch(\@A1, \@A2)
smartmatch(\%H1, \%H2)
Arrays no longer act like disjunctions
The ~~
operator treated most (but not all) array and arrayref operands as a disjunction of their values:
%HASH ~~ @ARRAY any array elements exist as hash keys
/REGEXP/ ~~ @ARRAY any array elements pattern match regex
undef ~~ @ARRAY any array element is undefined
$VALUE ~~ @ARRAY any array element smartmatches value
@ARRAY ~~ %HASH any array elements exist as hash keys
@ARRAY ~~ /REGEXP/ any array elements pattern match regex
The new smartmatch()
subroutine doesn't treat an array or arrayref as a list of alternatives. In fact, none of the following equivalent formulations matches at all:
smartmatch( \%HASH, \@ARRAY )
smartmatch( qr/REGEXP/, \@ARRAY )
smartmatch( undef, \@ARRAY )
smartmatch( $VALUE, \@ARRAY )
smartmatch( \@ARRAY, \%HASH )
smartmatch( \@ARRAY, qr/REGEXP/ )
See "How to get the missing ~~
behaviours back" for creating equivalents to these matches under the new mechanism.
Hashes no longer act like a set of their own keys
In a similar way, the ~~
operator mostly treated hash and hashref arguments as a set containing the hash's keys:
@ARRAY ~~ %HASH set of keys contains any of the array elements
/REGEXP/ ~~ %HASH set of keys has an element that pattern-matches regex
undef ~~ %HASH set of keys contains undef (always false)
$VALUE ~~ %HASH set of keys contains the value
%HASH ~~ @ARRAY set of keys contains any of the array elements
%HASH ~~ /REGEXP/ set of keys contains an element that pattern-matches regex
Once again, the corresponding calls to smartmatch()
never match for any of these combinations of arguments. And, once again, see "How to get the missing ~~
behaviours back" for creating new equivalents to these matches.
Arrays and hashes no longer occasionally act like conjunctions
One of the complications of the former ~~
operator was that array and hash arguments to ~~
didn't always match any of...; in two particular cases they matched all of... instead:
@ARRAY ~~ \&SUB sub always returns true when called separately on each element
%HASH ~~ \&SUB sub always returns true when called separately on each key
The corresponding smartmatch()
calls don't do that:
smartmatch( \@ARRAY, \&SUB ) sub returns true when called once on entire arrayref
smartmatch( \%HASH, \&SUB ) sub returns true when called once on entire hashref
References match in far fewer ways
As the preceding sections imply, whereas ~~
defined complex and vaguely inconsistent matching behaviours for numerous combinations of two reference arguments, the smartmatch()
subroutine provided by this module fails to match when passed most combinations of two references.
Under this module, two references only smartmatch if:
The two references are identical: they are of the same type (e.g. two globrefs, two IOrefs, two regexrefs, two arrayrefs, two subrefs, two scalarrefs, etc.) and their addresses are the same; or
The two references are of the same container type (e.g. two arrayrefs or two hashrefs) and their contents recursively smartmatch; or
The two references are both regexrefs and those two regexes contain identical patterns; or
The right-hand reference is a subref, which returns true when passed the left-hand reference.
Boolean arguments are handled differently
Since Perl v5.36, the language has offered distinguished true and false values that can be detected at runtime using the is_bool()
builtin. All boolean operators and some built-in functions (such as exists()
and defined()
) return these special values, which can then be identified as originating from a boolean expression.
The former ~~
operator had no special behaviour when passed such values, but smartmatch()
does. When a distinguished boolean value is passed as the right-hand argument, smartmatch()
simply returns that value as the result of the match. Hence:
smartmatch( $anything, $count == 1 ) # returns true if $count is 1
smartmatch( $anything, exists %seen{$key} ) # returns true if key is present
smartmatch( $anything, !$expected) # returns true is $expected is false
This behaviour might seem odd, unintuitive, or even wrong, but it makes much more sense when smartmatch()
is used automatically to evaluate a when
expression:
given ($value) {
when ($_ == 1) { say "value is 1" }
when (exists $seen->{$_}) { say "value has been seen" }
when (!$_) { say "value is false" }
}
The former built-in when
block had eight special cases where a when
behaved in this way (i.e. more like an if
).
The module has only one special case: wherever the when
expression (or the right-hand argument of an explicit call to smartmatch()
) evaluates to a distinguished boolean. This means that you can use any expression that produces builtin::true
or builtin::false
in a when
and have it act as expected:
given ($value) {
when (eof) { say "input is exhausted" }
when (m/\s/) { say "value has a space" }
when (defined) { say "value is defined" }
when (-r -w -x) { say "value has file permissions" }
when (-1 < $_ < 1) { say "value is in range" }
when (looks_like_number $_) { say "value is defined" }
when (my_bool_test_func($_)) { say "value passed the test" }
}
Note, however, that there are many expressions which Perl can treat as boolean, but which do not actually produce the distinguished boolean values that enable this special smartmatching behaviour.
For example, the following when
blocks don't work as intended:
given ($value) {
when (ref) { say "value is a reference" } # WRONG
when (tell) { say "not at start of input" } # WRONG
when ($_ % 2) { say "value is odd" } # WRONG
when (length) { say "value is not empty string" } # WRONG
when (keys %$_) { say "hashref is not empty " } # WRONG
when (blessed $_) { say "value is an object" } # WRONG
when (my_int_test_func($_)) { say "value passed the test" } # WRONG
}
To ensure that a when
treats its argument as a direct boolean test, you need to make that argument is explicitly boolean:
given ($value) {
when (ref ne "") { say "value is a reference" }
when (tell != 0) { say "not at start of input" }
when ($_ % 2 == 1) { say "value is odd" }
when (length > 0) { say "value is not empty string" }
when (keys %$_ > 0) { say "hashref is not empty " }
}
...or — if the argument is a subroutine call — you can specify the argument directly as a subref:
given ($value) {
when (\&blessed) { say "value is an object" }
when (\&my_int_test_func) { say "value passed the test" }
}
There are two other special cases to be aware of:
given ($value) {
when (true) { say "the value was true" } # WRONG
when (false) { say "the value was false" } # WRONG
}
A when (true)
doesn't test whether $_
is true
. Instead, it is always executed (like an if (true)
). Likewise, a when (false)
doesn't test against false
; it is simply never executed (like an if (false)
).
If you want to test for general truth or falsehood in a when
, you need:
given ($value) {
when (!! $_) { say "the value was true" }
when ( ! $_) { say "the value was false" }
}
And, if you want to test for distinguished truth or falsehood in a when
, you need:
given ($value) {
when (is_bool($_) && $_) { say "the value was distinguished true" }
when (is_bool($_) && !$_) { say "the value was distinguished false" }
}
Numeric matching is stricter
The former ~~
operator attempted to match numerically whenever its right-hand argument was an actual number, or whenever its left-hand argument was an actual number and its right-hand argument was a string that looks like a number.
In contrast, smartmatch()
only uses numeric comparison if its right-hand argument is an actual number. If its right-hand argument is a number-like string, it uses string comparison. If you need to ensure numeric comparison when the right-hand argument is a number-like string, add 0 to it:
my $input_num = readline; # for example: "42.000";
smartmatch( 42, $input_num ) # compares using "eq" --> fails to match
smartmatch( 42, 0+$input_num ) # compares using "==" --> matches
String and pattern matching no longer stringifies references
When passed an unblessed reference as its left-hand argument, the ~~
operator would convert the reference to a string before attempting to pattern- or string-match it.
The smartmatch()
subroutine does not do this; it simply fails to match. If you want to smartmatch against a left-hand reference in those ways, you must explicitly stringify it first:
smartmatch( $SOMEREF, qr/CODE|GLOB/ ) # always fails
smartmatch( "$SOMEREF", qr/CODE|GLOB/ ) # matches if left arg is a subref or globref
Junctive smartmatching
As the preceding sections imply, the new matching behaviour of smartmatch()
removes all of the complex implicit recursive any of... and all of... special cases when matching arrays and hashes.
This makes the approach much easier to understand and remember. But also much less convenient, because those complex implicit recursive any of... and all of... behaviours were amongst the most useful features of the ~~
operator. In particular, it was extremely handy to be able to use ~~
to test for list membership and key-set membership, and also to be able to test values against multiple distinct criteria in a single expression.
So this module reintroduces most of those abilities, but in a simpler and much easier-to-remember way: by adding one or two extra arguments to smartmatch()
. Those arguments tell the subroutine to recursively smartmatch all of a list of values, or any of a list of values, or none of a list of values. Unsurprisingly, those extra arguments are the strings "any"
, "all"
, and "none"
. For example:
smartmatch( $N, any => [2,3,5,7] )
...is exactly the same as:
smartmatch($N, 2) || smartmatch($N, 3) || smartmatch($N, 5) || smartmatch($N, 7)
Likewise:
smartmatch( $N, all => [qr/\d/, \&is_prime] )
...is identical to:
smartmatch($N, qr/\d/) && smartmatch($N, \&is_prime)
And:
smartmatch( $N, none => [qr/\d/, \&is_prime] )
...is identical to:
!smartmatch($N, qr/\d/) && !smartmatch($N, \&is_prime)
The "any"
, "all"
, or "none"
can be placed before the first argument as well:
smartmatch( any => \@numbers, \&is_prime ) # At least one number is prime
smartmatch( all => \@numbers, qr/7/ ) # Every number has a 7 in it
smartmatch( none => \@numbers, 42 ) # The number list doesn't include 42
And, of course, you can also place an "any"
, "all"
, or "none"
on in front of both arguments:
smartmatch( any => \@numbers, all => [qr/\d/, \&is_prime] )
smartmatch( all => \@numbers, none => [qr/\d/, \&is_prime] )
smartmatch( none => \@numbers, any => \@previous_numbers )
As the preceding examples illustrate, if smartmatch()
is called with either one or two of these "junctive" modifiers, then the argument immediately after the "any"
, "all"
, or "none"
must be an array reference containing the list of values to be tested against the other argument.
The individual tests in such junctive smartmatches still use the core six rules; they simply distribute the tests over the list(s) of values and then apply ||
, &&
, or !...&&
between the results, short-circuiting as soon as a guaranteed true or false result is detected.
How to get the missing ~~
behaviours back
The availability of junctive versions of smartmatch()
makes it straightforward to use that subroutine to produce most of the disjunctive and conjunctive comparisons that the former ~~
operator provided.
Here is a table of just those old ~~
behaviours that differ from the behaviour of smartmatch()
, and the new (mostly junctive) syntaxes needed to get smartmatch()
to match in the old ways.
Note that all other forms of $LEFT ~~ $RIGHT
could simply be converted directly to smartmatch($LEFT, $RIGHT)
and would work identically.
undef ~~ @ARRAY ---> smartmatch( undef, any => $ARRAY )
undef ~~ %HASH ---> smartmatch( undef, any => [keys %$HASH] )
%HASH ~~ @ARRAY ---> smartmatch( any => [keys %HASH], any => \@ARRAY )
/REGEXP/ ~~ @ARRAY ---> smartmatch( any => \@ARRAY, qr/REGEXP/ )
$VALUE ~~ @ARRAY ---> smartmatch( $VALUE, any => \@ARRAY )
%HASH1 ~~ %HASH2 ---> smartmatch( [sort keys %HASH1], [sort keys %HASH2] )
@ARRAY ~~ %HASH ---> smartmatch( any => \@ARRAY, any => [keys %HASH] )
/REGEXP/ ~~ %HASH ---> smartmatch( any => [keys %HASH], qr/REGEXP/ )
$VALUE ~~ %HASH ---> smartmatch( $VALUE, any => [keys %HASH] )
@ARRAY ~~ \&SUB ---> smartmatch( all => \@ARRAY, \&SUB )
%HASH ~~ \&SUB ---> smartmatch( all => [keys %HASH], \&SUB )
@ARRAY ~~ /REGEXP/ ---> smartmatch( any => \@ARRAY, qr/REGEXP/ )
%HASH ~~ /REGEXP/ ---> smartmatch( any => [keys %HASH], qr/REGEXP/ )
$REF ~~ /REGEXP/ ---> smartmatch( "$REF", qr/REGEXP/ )
$NUM ~~ $NUMLIKE ---> smartmatch( $NUM, 0 + $NUMLIKE )
$REF ~~ $STRING ---> smartmatch( "$REF", $STRING )
These new formulations have the obvious disadvantage of being considerably more verbose (i.e. harder to write), but they also have the obvious advantage of being considerably more verbose (i.e. easier to read, more self-documenting, less likely to accidentally be used incorrectly, no need to remember all 23 rules of ~~
matching).
Many of the changes in usage stem from the fact that hashes are now treated as full hashes, rather than as mere key-sets. The most common situation this alters is the use of smartmatching to ensure that a set of named arguments contains all the required keys (and no others):
my %REQUIRED_ARGS = ( name => 1, age => 1, addr => 1, status => 1 );
sub register (%named_args) {
croak "Incorrect named args in call to register()"
unless %named_args ~~ %REQUIRED_ARGS; # All the keys match
...
}
This particular usage is still possible under the new smartmatch rules, even though two hashes must now match keys and values. It's possible because the corresponding values are smartmatched, and we can now use the "always matches" behaviour of a true
on the right-hand side to create a %REQUIRED_ARGS
where the values always match, no matter what:
# Values of "true" will always smartmatch any other value...
my %REQUIRED_ARGS = ( name => true, age => true, addr => true, status => true );
sub register (%named_args) {
croak "Incorrect named args in call to register()"
unless smartmatch(\%named_args, \%REQUIRED_ARGS);
...
}
Better still, we could use the values of %REQUIRED_ARGS
to test the values of %named_args
in various useful ways:
my %REQUIRED_ARGS = (
name => qr/\S/, # Name can't be empty
age => sub ($a) { $a > 18 }, # Must be an adult
addr => true, # Address can be anything
status => ['member', 'guest'], # Only two statuses allowed
);
sub register (%named_args) {
croak "Incorrect named args in call to register()"
unless smartmatch(\%named_args, \%REQUIRED_ARGS);
...
}
We could also use smartmatch()
to test that %named_args
contains only valid keys, but without requiring that it contain every valid key:
my @PERMITTED_ARGS = qw( name age addr status location shoesize );
sub update_details (%named_args) {
croak "Unknown named arg in call to update_details()"
unless smartmatch(all=> [keys %named_args], any => \@PERMITTED_ARGS);
...
}
Overall, the goal of this reformulation of smartmatching is to continue to provide all of the capacities of the format ~~
operator, but without imposing all of that former operator's often-inscrutable complexity.
Overloading smartmatch()
globally via the SMARTMATCH()
method
Yet another way that this module's version of smartmatching differs from the former built-in mechanism is that the smartmatch()
function can no longer be extended to handle new types of objects by overloading their classes' ~~
operators. Because, of course, with the demise of the built-in mechanism, there is no longer any ~~
operator to overload.
So, by default, smartmatch()
does not accept any object as its right-hand argument, and immediately throws an exception if you attempt to pass one.
However, this module allows you to change that default behaviour...by defining a special SMARTMATCH()
method in your class. Subsequently, when an object of the class is passed to smartmatch()
as its right-hand argument (and only when it's the right-hand argument), then smartmatch()
matches by attempting to call the SMARTMATCH()
method of the right-hand object, passing its own left-hand argument to that method call.
In other words, smartmatch($left_arg, $right_obj)
simply returns the result of $right_obj->SMARTMATCH( $left_arg )
.
For example, suppose you wanted to be able to smartmatch against an ID::Validator
object. In particular, suppose that, when an ID::Validator
object is passed as the right-hand argument of smartmatch()
you need it to call the object's validate()
method. You could extend the behaviour of smartmatch()
in that way simply by defining a suitable SMARTMATCH()
method in the ID::Validator
class:
class ID::Validator {
...
method SMARTMATCH ($left_arg) {
return $self->validate( $left_arg );
}
}
# Now ID::Validator objects can be passed as the right-hand arg of smartmatch()
# (which also means they can be used as the target expression of a when block)...
state $VALID_ID = ID::Validator->new();
given ($id) {
when ($VALID_ID) { say 'valid ID' } # Same as: if ($VALID_ID->validate($_)) {...}
default { die 'invalid ID' }
}
In a similar way, you could allow any Type::Tiny
instance to be used as the right-hand argument of smartmatch()
(and therefore as the match target of a when
), by injecting a SMARTMATCH()
method into the base class of the framework's many type objects:
sub Type::Tiny::SMARTMATCH ($type_obj, $test_value) {
return $type_obj->check($test_value);
}
# and thereafter...
use Types::Standard ':all';
given ($data) {
when (Int) { $count += $data }
when (ArrayRef) { $count += sum @{data} }
when (HashRef) { $count += sum keys %{$data} }
when (FileHandle) { $count += $data->input_line_number }
when (Object) { $count += $data->get_count }
}
Note that, when an object is passed as the left-hand argument to smartmatch()
, that object's SMARTMATCH()
method is never invoked. The smartmatch may still, however, invoke other methods on the left-hand object (e.g. its q{0+}
or q{""}
operator overloadings to convert it for an ==
or eq
match when the right-hand argument is a number or string).
Overloading smartmatch()
locally via multisubs
Because the smartmatch()
subroutine provided by this module is actually a multisub, implemented via the Multi::Dispatch module, smartmatch()
can also easily be extended locally (i.e. within a given package) to allow it to match between additional types of arguments.
This kind of overloading is much more flexible that SMARTMATCH()
overloading because you can add or modify matching behaviours for any combination of the multisub's two arguments, rather than just adding behaviours when a particular class of object is the right-hand argument.
Suppose that, as in the preceding section, you wanted to be able to smartmatch against an ID::Validator
object. But suppose you don't control that class's code, and you're not comfortable violating its encapsulation by offhandedly injecting an unsanctioned SMARTMATCH()
method into the class (like we did in the Type::Tiny
example in the previous section).
To avoid that, you could extend the behaviour of smartmatch()
locally in your own code, by defining a suitable additional package-scoped variant in the current scope:
use Multi::Dispatch; # ...so we can define a new smartmatch() variant here
# Define new smartmatching behaviour on ID::Validator objects...
multi smartmatch ($value, ID::Validator $obj) {
return $obj->validate( $value );
}
# Now ID::Validator objects can be passed as the right-hand arg of smartmatch()
# (which also means they can be used as the target expression of a when block)...
state $VALID_ID = ID::Validator->new();
given ($id) {
when ($VALID_ID) { say 'valid ID' } # Same as: if ($VALID_ID->validate($_)) {...}
default { die 'invalid ID' }
}
Likewise, you could permit smartmatching against Type::Tiny objects, without messing about with the framework's internals, with:
multi smartmatch ($value, Type::Tiny $type) {
return $type->check( $value );
}
More generally, if you wanted to allow any object to be passed as the right-hand argument to smartmatch()
, provided that object has a stringification or numerification overloading, you could write:
use Multi::Dispatch;
use Types::Standard ':all';
# Allow smartmatch() to accept RHS objects that can convert to numbers...
multi smartmatch (Num $left, Overload['0+'] $right) {
return next::variant($left, 0+$right);
}
# Allow smartmatch() to accept RHS objects that can convert to strings...
multi smartmatch (Str $left, Overload[q{""}] $right) {
return next::variant($left, "$right");
}
You can also change the existing behaviours of smartmatch()
by providing local variants for one or more specific cases that the multisub already handles:
use Multi::Dispatch;
# Change how smartmatch() compares a hash and an array
# The standard behaviour is to always fail to match (because a hash isn't an array).
# But here we change it so that it uses the weird old C<~~> behaviour,
# which was to match if any hash key matched any value in the array...
multi smartmatch (HASH $href, ARRAY $aref) {
return smartmatch(any => [keys %{$href}], any => $aref);
}
For further details on the numerous features and capabilities of the multi
keyword, see the Multi::Dispatch module.
LIMITATIONS
The re-implementation of given
/when
provided by this module aims to provide all the capabilities of the former built-in given
/when
construct (albeit, via a vastly simplified set of rules for smartmatching, combined with the three junctive extensions)
However, it currently fails to meet that goal in several ways:
Limitation 1. You can't always use a given
inside a do
block
The former built-in switch feature allowed you to place a given
inside a do
block and then use a series of when
blocks to select the result of the do
. Like so:
my $result = do {
given ($some_value) {
when (undef) { 'undef' }
when (any => [0..9]) { 'digit' }
break when /skip/;
when ('quit') { 'exit' }
default { 'huh?' }
}
};
The module currently only supports a limited subset of that capability. For example, the above code will still compile and execute, but the value assigned to $result
will always be undefined, regardless of the contents of $some_value
.
This is because it seems to be impossible to fully emulate the implicit flow control at the end of a when
block (i.e. automatically jumping out of the surrounding given
after the last statement in a when
) by using other standard Perl constructs. Likewise, to emulate the explicit control flow provided by continue
and break
, the code has to be translated by adding at least one extra statement after the block of each given
and when
.
So it does not seem possible to rewrite an arbitrary given
/when
such that the last statement in a when
is also the last executed statement in its given
, and hence is the last executed statement in the surrounding do
block.
However, the module is able to correctly rewrite at least some (perhaps most) given
/when
combinations so that they work correctly within a do
block. Specifically, as long as a given
's block does not contain an explicit continue
or break
or goto
, or a postfix when
statement modifier, then the module optimizes its rewriting of the entire given
, converting it into a form that can be placed inside a do
block and still successfully produce values. Hence, although the previous example did not work, if the break when...
statement it contains were removed:
my $result = do {
given ($some_value) {
when (undef) { 'undef' }
when (any => [0..9]) { 'digit' }
# <-- POSTFIX when REMOVED
when ('quit') { 'exit' }
default { 'huh?' }
}
};
...or even if the postfix when
were converted to the equivalent when
block:
my $result = do {
given ($some_value) {
when (undef) { 'undef' }
when (any => [0..9]) { 'digit' }
when (/skip/) { undef } # <-- CONVERTED FROM POSTFIX
when ('quit') { 'exit' }
default { 'huh?' }
}
};
...then the code would work as expected and $result
would receive an appropriate value.
In general, if you have written a do
block with a nested given
/when
that is not going to work under this module, you will usually receive a series of compile-time "Useless use of a constant in void context" warnings, one for each when
block.
See the file t/given_when.t for examples of this construction that do work, and the file t/given_when_noncompatible.t for examples the will not work (currently, or probably ever).
Limitation 2. You can't ever use a when
modifier outside a given
The former built-in mechanism allowed a postfix when
modifier to be used within a for
loop, like so:
for (@data) {
say when $TARGET_VALUE
}
This module does not allow when
to be used as a statement modifier anywhere except inside a given
block. The above code would therefore have to be rewritten to either:
for (@data) {
given ($) {
say when $TARGET_VALUE;
}
}
Or, because the module does allow full when
blocks in a for
loop, you could also rewrite it to:
for (@data) {
when ($TARGET_VALUE) { say }
}
Limitation 3. Scoping anomalies with when
modifiers
The behaviour of obscure usages such as:
my $x = 0;
given (my $x = 1) {
my $x = 2, continue when 1;
say $x;
}
...differs between the former built-in given
/when
and this module's reinvention of it. Under the built-in feature, $x
would contain undef
at the say $x
line; under the module, $x
will contain 2.
As neither result seems to make much sense, or be particularly useful, it is unlikely that this incompatibility will be much of a issue for most users.
DIAGNOSTICS
The module may produce the following exceptions or warnings...
Incomprehensible "when"
Incomprehensible "default"
-
You specified a
when
ordefault
keyword, but the code following it did not conform to the correct syntax for those blocks. The error message will attempt to indicate where the problem was, but that indication may not be accurate.Check the syntax of your block.
Can't "when" outside a topicalizer
Can't "default" outside a topicalizer
-
when
anddefault
blocks can only be executed inside agiven
or afor
loop. Your code is attempting to execute awhen
ordefault
somewhere else. That never worked with the old built-in syntax, and it doesn't work with this module either.Move your block inside a
given
or afor
loop. Can't specify postfix "when" modifier outside a "given"
-
It is a limitation of this module that you can only use the
EXPR when EXPR
syntax inside agiven
(not inside afor
loop).If your postfix
when
modifier is inside a loop, convert it to awhen
block instead. Or else wrap agiven ($_) { ... }
around the postfixwhen
. Can't "continue" outside a "when" or "default"
-
Calling a
continue
to override the automatic "jump-out-of-the-surrounding-given
" behaviour ofwhen
anddefault
blocks only makes sense if you're actually inside awhen
or adefault
. However, your code is attempting to callcontinue
somewhere else.Move your
continue
inside awhen
or adefault
. Can't "break" outside a "given"
-
Calling a
break
to explicitly "jump-out-of-the-surrounding-given
" only makes sense when you're inside agiven
in the first place.Move your
break
inside agiven
.Or, if you're trying to escape from a
when
in a loop, changebreak
tonext
orlast
. Smart matching an object breaks encapsulation
-
The new smartmatching behaviour provided by this module does not, by default, support the smartmatching of objects in most cases.
If you want to use an object in a
given
orwhen
, you will need to provide it with either aSMARTMATCH()
method or a local variant ofsmartmatch()
that handles that kind of object.See "Overloading
smartmatch()
globally via theSMARTMATCH()
method" and "Overloadingsmartmatch()
locally via multisubs" for details of these two different approaches for supporting objects in switches. "when (true) {...}" better written as "default {...}"
-
Smartmatching a distinguished
true
value always matches, regardless of the other value. So awhen (true) {...}
will always execute, just like adefault
does. See "Boolean arguments are handled differently" for details.If you really did want this behaviour, you can silence this warning by changing the
when
to adefault
, or else changing thewhen
value to anything other than a rawtrue
. For example:when ( +true ) {...} when ( true() ) {...} when ( (true) ) {...} when ( !false ) {...}
If you were intending to smartmatch when the other value is true,
when (true)
will never accomplish that. Instead, use something like:when ( !!$_ ) {...}
Useless use of "when (false)"
-
Smartmatching a distinguished
false
value never matches, so awhen (false) {...}
will never execute, regardless of the other value you're matching against. So it's pointless writing it. See "Boolean arguments are handled differently" for details.If you really do want to disable the entire
when
, you can silence this warning by changing the value to anything other than a rawfalse
. For example:when ( +false ) {...} when ( false() ) {...} when ( (false) ) {...} when ( !true ) {...}
If you were intending to smartmatch a false boolean value,
when (false)
will never achieve that. Instead, use:when ( !$_ ) {...}
Useless use of a constant in void context
-
Apart from the many other unrelated reasons your code may produced this error, if the constant it is uselessly using is inside a
when
block that is supposed to feed a surroundingdo
block, this error probably indicates that yourgiven
/when
is not one that this module can rewrite for that purpose.See "Limitation 1. You can't always use a
given
inside ado
block" for details of this issue, and how to work around it.
CONFIGURATION AND ENVIRONMENT
Switch::Right requires no configuration files or environment variables.
DEPENDENCIES
This module requires the B::Deparse, Keyword::Simple, Multi::Dispatch, Object::Pad, PPR, Test2::V0, and Type::Tiny modules.
The module only works under Perl v5.36 and later.
INCOMPATIBILITIES
This module uses the Perl keyword mechanism to (re)extend the Perl syntax to include new versions of the given
/when
/default
blocks. Hence it is likely to be incompatible with other modules that add other keywords to the language.
BUGS AND LIMITATIONS
No bugs have been reported.
Please report any bugs or feature requests to bug-switch-right@rt.cpan.org
, or through the web interface at http://rt.cpan.org.
SEE ALSO
The Switch::Back module provides a (nearly) fully backwards-compatible alternative to this module, which restores (almost) all of the syntax, behaviour, and idiosyncrasies of former switch and smartmatch features.
AUTHOR
Damian Conway <DCONWAY@CPAN.org>
LICENCE AND COPYRIGHT
Copyright (c) 2024, Damian Conway <DCONWAY@CPAN.org>
. All rights reserved.
This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic.
DISCLAIMER OF WARRANTY
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.