Failure is an option in Perl 6

Failure is an option in Perl 6

In the eighth article in this series comparing Perl 5 to Perl 6, learn about their differences in creating and handling exceptions.

hands programming
Image by : 
WOCinTech Chat. Modified by Opensource.com. CC BY-SA 4.0
x

Get the newsletter

Join the 85,000 open source advocates who receive our giveaway alerts and article roundups.

This is the eighth in a series of articles about migrating code from Perl 5 to Perl 6. This article looks at the differences in creating and handling exceptions between Perl 5 and Perl 6.

The first part of this article describes working with exceptions in Perl 6, and the second part explains how you can create your own exceptions and how failure is an option in Perl 6.

Exception-handling phasers

In Perl 5, you can use eval to catch exceptions in a piece of code. In Perl 6, this functionality is covered by try:

# Perl 5
eval {
    die "Goodbye cruel world";
};
say $@;           # Goodbye cruel world at ...
say "Alive again!"

# Perl 6
try {
    die "Goodbye cruel world";
}
say $!;           # Goodbye cruel world␤  in block ...
say "Alive again!"

In Perl 5, you can also use the return value of eval in an expression:

# Perl 5
my $foo = eval { ... };  # undef if exception was thrown

This works the same way in Perl 6 for try:

# Perl 6
my $foo = try { 42 / $something };   # Nil if $something is 0

and it doesn't even have to be a block:

# Perl 6
my $foo = try 42 / $something;       # Nil if $something is 0

In Perl 5, if you need finer control over what to do when an exception occurs, you can use special signal handlers $SIG{__DIE__} and $SIG{__WARN__}.

In Perl 6, these are replaced by two exception-handling phasers, which due to their scoping behaviour must always be specified with curly braces. These exception-handling phasers (in the following table) are applicable only to the surrounding block, and you can have only one of each type in a block.

Name Description
CATCH Run when an exception is thrown
CONTROL Run for any (other) control exception

Catching exceptions

The $SIG{__DIE__} pseudo-signal handler in Perl 5 is no longer recommended. There are several competing CPAN modules that provide try/catch mechanisms (such as: Try::Tiny and Syntax::Keyword::Try). Even though these modules differ completely in implementation, they provide very similar syntax with only very minor semantic differences, so they're a good way to compare Perl 6 and Perl 5 features.

In Perl 5, you can catch an exception only in conjunction with a try block:

# Perl 5
use Try::Tiny;               # or Syntax::Keyword::Try
try {
    die "foo";
}
catch {
    warn "caught error: $_"; # $@ when using Syntax::Keyword::Try
}

Perl 6 doesn't require a try block. The code inside a CATCH phaser will be called whenever an exception is thrown in the immediately surrounding lexical scope:

# Perl 6
{                        # surrounding scope, added for clarity
    CATCH {
        say "aw, died";
        .resume;         # $_, AKA the topic, contains the exception
    }
    die "goodbye cruel world";
    say "alive again";
}
# aw, died
# alive again

Again, you do not need a try statement to catch exceptions in Perl 6. You can use a try block on its own, if you want, but it's just a convenient way to disregard any exceptions thrown inside that block.

Also, note that $_ will be set to the Exception object inside the CATCH block. In this example, execution will continue with the statement after the one that caused the Exception to be thrown. This is achieved by calling the resume method on the Exception object. If the exception is not resumed, it will be thrown again and possibly caught by an outer CATCH block (if there is one). And if there are no outer CATCH blocks, the exception will result in program termination.

The when statement makes it easy to check for a specific exception:

# Perl 6
{
    CATCH {
        when X::NYI {       # Not Yet Implemented exception thrown
            say "aw, too early in history";
            .resume;
        }
        default {
            say "WAT?";
            .rethrow;       # throw the exception again
        }
    }
    X::NYI.new(feature => "Frobnicator").throw;  # caught, resumed
    now / 0;                                     # caught, rethrown
    say "back to the future";
}
# aw, too early in history
# WAT?
# Attempt to divide 1234.5678 by zero using /

In this example, only X::NYI exceptions will resume; all the others will be thrown to any outer CATCH block and will probably result in program termination. And we'll never go back to the future.

Catching warnings

If you do not want any warnings to emanate when a piece of code executes, you can use the no warnings pragma in Perl 5:

# Perl 5
use warnings;     # need to enable warnings explicitely
{
    no warnings;
    my $foo;
    say $foo;     # no visible warning
}
my $bar;
print $bar;
# Use of uninitialized value $bar in print...

In Perl 6, you can use a quietly block:

# Perl 6
                  # warnings are enabled by default
quietly {
    my $foo;
    say $foo;     # no visible warning
}
my $bar;
print $bar;
# Use of uninitialized value of type Any in string context...

The quietly block will catch any warnings that emanate from that block and disregard them.

If you want finer control on which warnings you want to see, you can select the warning categories you want enabled or disabled with use warnings or no warnings, respectively, in Perl 5. For example:

# Perl 5
use warnings;
{
    no warnings 'uninitialized';
    my $bar;
    print $bar;    # no visible warning
}

If you want to have finer control in Perl 6, you will need a CONTROL phaser.

CONTROL

The CONTROL phaser is very much like the CATCH phaser, but it handles a special type of exception called the "control exception." A control exception is thrown whenever a warning is generated in Perl 6, which you can catch with the CONTROL phaser. This example will not show warnings for using uninitialized values in expressions:

# Perl 6
{
    CONTROL {
        when CX::Warn {  # Control eXception type for Warnings
            note .message
              unless .message.starts-with('Use of uninitialized value');
        }
    }
    my $bar;
    print $bar;          # no visible warning
}

There are currently no warning categories defined in Perl 6, but they are being discussed for future development. In the meantime, you will have to check for the actual message of the control exception CX::Warn type, as shown above.

The control exception mechanism is used for quite a lot of other functionality in addition to warnings. The following statements (in alphabetical order) also create control exceptions:

Control exceptions generated by these statements will also show up in any CONTROL phaser. Luckily, if you don't do anything with the given control exception, it will be rethrown when the CONTROL phaser is finished and ensure its intended action is performed.

Failure is an option

In Perl 5, you need to prepare for a possible exception by using eval or some version of try when using a CPAN module. In Perl 6, you can do the same with try (as seen before).

But Perl 6 also has another option: Failure, which is a special class for wrapping an Exception. Whenever a Failure object is used in an unanticipated way, it will throw the Exception it is wrapping. Here is a simple example:

# Perl 6
my $handle = open "non-existing file";
say "we tried to open the file";
say $handle.get;  # unanticipated use of $handle, throws exception
say "this will never be shown";
# we tried to open the handle
# Failed to open file non-existing file: No such file or directory

The open function in Perl 6 returns an IO::Handle if it successfully opens the requested file. If it fails, it returns a Failure. This, however, is not what throws the exception—if we actually try to use the Failure in an unanticipated way, then the Exception will be thrown.

There are only two ways of preventing the Exception inside a Failure to be thrown (i.e., anticipating a potential failure):

  • Call the .defined method on the Failure
  • Call the .Bool method on the Failure

In either case, these methods will return False (even though technically the Failure object is instantiated). Apart from that, they will also mark the Failure as "handled," meaning that if the Failure is later used in an unanticipated way, it will not throw the Exception but simply return False.

Calling .defined or .Bool on most other instantiated objects will always return True. This gives you an easy way to find out if something you expected to return a "real" instantiated object returned something you can really use.

However, it does seem like a lot of work. Fortunately, you don't have to explicitly call these methods (unless you really want to). Let's rephrase the above code to more gently handle not being able to open the file:

# Perl 6
my $handle = open "non-existing file";
say "tried to open the file";
if $handle {                    # "if" calls .Bool, True on an IO::Handle
    say "we opened the file";
    .say for $handle.lines;     # read/show all lines one by one
}
else {                          # opening the file failed
    say "could not open file";
}
say "but still in business";
# tried to open the file
# could not open file
# but still in business

Throwing exceptions

As in Perl 5, the simplest way to create an exception and throw it is to use the die function. In Perl 6, this is a shortcut to creating an X::AdHoc Exception and throwing it:

# Perl 5
sub alas {
    die "Goodbye cruel world";
    say "this will not be shown";
}
alas;
# Goodbye cruel world at ...

# Perl 6
sub alas {
    die "Goodbye cruel world";
    say "this will not be shown";
}
# Goodbye cruel world
#   in sub alas at ...
#   in ...

There are some subtle differences between die in Perl 5 and Perl 6, but semantically they are the same: they immediately stop an execution.

Returning with a failure

Perl 6 has added the fail function. This will immediately return from the surrounding subroutine/method with the given Exception: if a string is supplied to the `fail` function (rather than an `Exception` object), then an X::AdHoc exception will be created.

Suppose you have a subroutine taking one parameter, a value that is checked for truthiness:

# Perl 6
sub maybe-alas($expected) {
    fail "Not what was expected" unless $expected;
    return 42;
}
my $a = maybe-alas(666);
my $b = maybe-alas("");
say "values gathered";
say $a;                   # ok
say $b;                   # will throw, because it has a Failure
say "still in business";  # will not be shown
# values gathered
# 42
# Not what was expected
#   in sub maybe-alas at ...

Note that you do not have to provide try or CATCH: the Failure will be returned from the subroutine/method in question as if all is normal. Only if the Failure is used in an unanticipated way will the Exception embedded in it be thrown. An alternative way of handling this would be:

# Perl 6
sub maybe-alas($expected) {
    fail "Not what was expected" unless $expected;
    return 42;
}
my $a = maybe-alas(666);
my $b = maybe-alas("");
say "values gathered";
say $a ?? "got $a for a" !! "no valid value returned for a";
say $b ?? "got $b for b" !! "no valid value returned for b";
say "still in business";
# values gathered
# got 42 for a
# no valid value returned for b
# still in business

Note that the ternary operator ?? !! calls .Bool on the condition, so it effectively disarms the Failure that was returned by fail.

You can think of fail as syntactic sugar for returning a Failure object:

# Perl 6
fail "Not what was expected";

# Perl 6
return Failure.new("Not what was expected");  # semantically the same

Creating your own exceptions

Perl 6 makes it very easy to create your own (typed) Exception classes. You just need to inherit from the Exception class and provide a message method. It is customary to make custom classes in the X:: namespace. For example:

# Perl 6
class X::Frobnication::Failed is Exception {
    has $.reason;  # public attribute
    method message() {
        "Frobnication failed because of $.reason"
    }
}

You can then use that exception in your code in any die or fail statement:

# Perl 6
die X::Frobnication::Failed.new( reason => "too much interference" );

# Perl 6
fail X::Frobnication::Failed.new( reason => "too much interference" );

which you can check for inside a CATCH block and introspect if necessary:

# Perl 6
CATCH {
    when X::Frobnicate::Failed {
        if .reason eq 'too much interference' {
            .resume     # too much interference is ok
        }
    }
}                       # all others will re-throw

You are completely free in how you set up your Exception classes; the only thing the class needs to provide a method called 'message' that should return a string. How that string is created is entirely up to you, as long as the method returns a string. If you prefer working with error codes, you can:

# Perl 6
my @texts =
  "unknown error",
  "too much interference",
;
my constant TOO_MUCH_INTERFERENCE = 1;
class X::Frobnication::Failed is Exception {
    has Int $.reason = 0;
    method message() {
        "Frobnication failed because of @texts[$.reason]"
    }
}

As you can see, this quickly becomes more elaborate, so your mileage may vary.

Summary

Catching exceptions and warnings are handled by phasers in Perl 6, not by eval or signal handlers, as in Perl 5. Exceptions are first-class objects in Perl 6.

Perl 6 also introduces the concept of a Failure object, which embeds an Exception object. If the Failure object is used in an unanticipated way, the embedded Exception will be thrown.

You can easily check for a Failure with if, ?? !! (which checks for truthiness by calling the .Bool method) and with (which checks for definedness by calling the .defined method).

You can also create Exception classes very easily by inheriting from the Exception class and providing a message method.

Topics

About the author

Elizabeth Mattijsen
Elizabeth Mattijsen - Elizabeth Mattijsen has been programming for a living since 1978 in various (mostly now defunct) programming languages before she started programming in Perl 4. In 1994 she started the first commercial web site development company in the Netherlands, using Perl 5 as the main programming language. From 2003 onwards, she was involved in the rapid growth of an online Hotel Reservation service. In 2012 she got more directly involved in the development of Rakudo Perl 6. From 2015 she has also...