NAME

Switch::Right - Switch and smartmatch done right this time

VERSION

This document describes Switch::Right version 0.000003

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.

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 or default 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 and default blocks can only be executed inside a given or a for loop. Your code is attempting to execute a when or default 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 a for 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 a given (not inside a for loop).

If your postfix when modifier is inside a loop, convert it to a when block instead. Or else wrap a given ($_) { ... } around the postfix when.

Can't "continue" outside a "when" or "default"

Calling a continue to override the automatic "jump-out-of-the-surrounding-given" behaviour of when and default blocks only makes sense if you're actually inside a when or a default. However, your code is attempting to call continue somewhere else.

Move your continue inside a when or a default.

Can't "break" outside a "given"

Calling a break to explicitly "jump-out-of-the-surrounding-given" only makes sense when you're inside a given in the first place.

Move your break inside a given.

Or, if you're trying to escape from a when in a loop, change break to next or last.

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 or when, you will need to provide it with either a SMARTMATCH() method or a local variant of smartmatch() that handles that kind of object.

See "Overloading smartmatch() globally via the SMARTMATCH() method" and "Overloading smartmatch() locally via multisubs" for details of these two different approaches for supporting objects in switches.

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 surrounding do block, this error probably indicates that your given/when is not one that this module can rewrite for that purpose.

See "Limitation 1. You can't always use a given inside a do 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.