NAME

Switch::Back - given/when for a post-given/when Perl

VERSION

This document describes Switch::Back version 0.000002

SYNOPSIS

use v5.42;          # given/when were removed in this version of Perl

use Switch::Back;   # But this module brings them back

given ($some_value) {

    when (1)   { say 1; }

    when ('a') { say 'a'; continue; }

    break when ['b'..'e'];

    default    { say "other: $_" }
}

DESCRIPTION

The given/when construct was added to Perl in v5.10, deprecated in Perl v5.38, and removed from the language in Perl v5.42.

Code that uses the construct must therefore be rewritten if it is to be used with any Perl after v5.40.

Or you can leave the given and when code in place and use this module to (partially) resurrect the former feature in more recent Perls.

The module exports three keywords: given, when, and default, and two subroutines: break and continue, which collectively provide most of the previous behaviour provided by use feature 'switch'.

The module doesn't resurrect the smartmatch operator (~~), but does export a smartmatch() subroutine that implements a nearly complete subset of the operator's former behaviour.

INTERFACE

The module has no options or configuration. You simply load the module:

use Switch::Back;

...and its exported keywords will then rewrite the rest of your source code (safely, using the extensible keyword mechanism, not a source filter) to translate any subsequent given, when, and default blocks into the equivalent post-Perl-v5.42 code. See "LIMITATIONS" for an overview of just how backwards compatible (or not) this approach is.

Loading the module also unconditionally exports two subroutines, break() and continue(), which provide the same explicit flow control as the former built-in break() and continue() functions.

The module also unconditionally exports a multiply dispatched subroutine named smartmatch(), which takes two arguments and smartmatches them using the same logic as the former ~~ operator. See "Smartmatching differences" for a summary of the differences between smartmatch() and the former built-in ~~ operator. See "Overloading smartmatch()" for an explanation of how to add new matching behaviours to smartmatch() (now that ~~ is no longer overloadable).

Smartmatching differences

The smartmatch() subroutine provided by this module implements almost all of the behaviours of the former ~~ operator, with the exceptions listed below.

Note, however, that despite these limitations on smartmatching, the given and when keywords implement almost the complete range of smartmatching behaviours of the former given and when constructs. Specifically these two keywords will still auto-enreference arrays, hashes, and slices that are passed to them, and when also implements the so-called "smartsmartmatching" behaviours on boolean expressions.

1. No auto-enreferencing of arguments

The smartmatch() subroutine is a regular Perl subroutine so, unlike the ~~ operator, it cannot auto-enreference an array or hash or slice that is passed to it. That is:

            %hash ~~ @array    # Works (autoconverted to; \%hash ~~ \@array)

smartmatch( %hash,   @array)   # Error (hash and array are flattened within arglist)

smartmatch(\%hash,  \@array)   # Works (smartmatch() always expects two args)

2. No overloading of ~~

Because overloading of the ~~ operator was removed in Perl 5.42, the smartmatch() subroutine always dies if its right argument is an object. The former ~~ would first attempt to call the object's overloaded ~~, only dying if no suitable overload was found. See "Overloading smartmatch()" for details on how to extend the behavior of smartmatch() so it can accept objects.

Overloading smartmatch()

Because the smartmatch() subroutine provided by this module is actually a multisub, implemented via the Multi::Dispatch module, it can easily be extended to match between additional types of arguments.

For example, if you want to be able to smartmatch against an ID::Validator object (by calling its validate() method), you would just write:

use Switch::Back;
use Multi::Dispatch;

# Define new smartmatching behaviour on ID::Validator objects...
multi smartmatch ($value, ID::Validator $obj) {
    return $obj->validate( $value );
}

# and thereafter...

state $VALID_ID = ID::Validator->new();

given ($id) {
    when ($VALID_ID) { say 'valid ID' }     # Same as: if ($VALID_ID->validate($_)) {...}
    default          { die 'invalid ID' }
}

More generally, if you wanted to allow any object to be passed as the right-hand argument to smartmatch(), provided the 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 behaviour of smartmatch() by providing a variant for specific cases that the multisub already handles:

use Multi::Dispatch;

# Change how smartmatch() compares a hash and an array
# (The standard behaviour is to match if ANY hash key is present in the array;
#  but here we change it so that ALL hash keys must be present)...

multi smartmatch (HASH $href, ARRAY $aref) {
    for my $key (keys %{$href}) {
        return false if !smartmatch($key, $aref);
    }
    return true;
}

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 be fully backwards compatible with the former built-in given/when construct, but 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 (currently, or probably ever) work.

2. Use of when modifiers 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 to:

for (@data) {
    when (@target_value) { say }
}

3. Scoping anomalies of 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 built-in given/when and this module's reimplementation. Under the built-in feature, $x contains undef at the say $x line; under the module, $x contains 2.

As neither result seems to make much sense, or be particularly useful, it is unlikely that this backwards incompatibility will ever be rectified.

DIAGNOSTICS

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 built-in syntax, and so 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). And, of course, you couldn't ever use it outside of both.

If your postfix when is inside a loop, convert it to a when block instead.

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

This module does not support the smartmatching of objects (because from Perl v5.42 onwards there is no way to overload ~~ for an object).

If you want to use an object in a given or when, you will need to provide a variant of smartmatch() that handles that kind of object. See "Overloading smartmatch()" for details of how to do that.

Use of uninitialized value in pattern match
Use of uninitialized value in smartmatch

You passed a value to given or when that included an undef, which was subsequently matched against a regex or against some other defined value. The former ~~ operator warned about this in some cases, so the smartmatch() subroutine does too.

To silence this warning, add:

no warnings 'uninitialized';

before the attempted smartmatch.

CONFIGURATION AND ENVIRONMENT

Switch::Back requires no configuration files or environment variables.

DEPENDENCIES

This module requires the Multi::Dispatch, PPR, Keyword::Simple, and Type::Tiny modules.

INCOMPATIBILITIES

This module uses the Perl keyword mechanism to (re)extend the Perl syntax to include given/when/default blocks. Hence it is likely to be incompatible with other modules that add other keywords to the language.

BUGS

No bugs have been reported.

Please report any bugs or feature requests to bug-switch-back@rt.cpan.org, or through the web interface at http://rt.cpan.org.

SEE ALSO

The Switch::Right module provides a kinder gentler approach to replacing the now defunct 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.