NAME
results - why throw exceptions when you can return them?
SYNOPSIS
use results;
sub to_uppercase {
my $str = shift;
return err( "Cannot uppercase a reference." ) if ref $str;
return err( "Cannot uppercase undef." ) if not defined $str;
return ok( uc $str );
}
my $got = to_uppercase( "hello world" )->unwrap();
DESCRIPTION
This module is a Perl implementation of Rust's standard error handling
mechanism. Rust doesn't have a `try`/`catch`/`throw` mechanism for
throwing errors. Instead, functions can be declared as returning a
"Result" which may be an "Ok" result or an "Err" result. Callers of these
functions will get a compile-time error if they do not inspect the result
and potentially deal with the error. (There is syntactic sugar for
propagating the error further up the call stack.)
Recent versions of Perl provide `try`/`catch`/`throw` (though `throw` is
spelled "die"), and in older versions the same thing can be roughly
accomplished using `eval` or CPAN modules, making Rust's error handling
seem fairly foreign. For this reason I do not recommend using the a
mixture of `try`/`catch`/`throw` error handling and Result-based error
handling in the same codebase. Pick one or the other.
Result-based error handling can provide some pretty succinct idioms, so I
do think it is worthy of consideration.
RETURNING RESULTS
Introduction
If you decide that your function should return a Result object, your
function should *always* return a Result object.
Do not return Results for errors but bare values for success. The
following example is bad because the caller cannot rely on the result of
`to_uppercase` *always* being a Result object.
use results;
sub to_uppercase {
my $str = shift;
return err( "Cannot uppercase a reference." ) if ref $str;
return err( "Cannot uppercase undef." ) if not defined $str;
return uc $str; # BAD
}
Instead:
use results;
sub to_uppercase {
my $str = shift;
return err( "Cannot uppercase a reference." ) if ref $str;
return err( "Cannot uppercase undef." ) if not defined $str;
return ok( uc $str ); # FIXED
}
`die` can still be used in code that uses result-based error handling, but
it should only be used for errors that are thought to be unrecoverable.
Don't expect your caller to `catch` exceptions.
Functions
The results module provides three functions used to return results. These
functions should nearly always be prefixed with Perl's `return` keyword.
`ok()`
The `ok()` function returns a successful result. It can be called without
arguments to represent success without any particular value to return.
return ok(); # success
You may also include a value:
sub your_function () {
...;
return ok( $output );
}
Or multiple values:
sub your_other_function () {
...;
return ok( $count, \@output );
}
The caller can then retrieve those values using:
my $output = your_function()->unwrap();
my ( $count, $output_ref ) = your_other_function()->unwrap();
If a list of return values was provided, then calling `unwrap()` in scalar
context will return the last item on the list only.
`ok_list()`
This function acts identically to `ok()` except that calling `unwrap()` in
scalar context will die.
Your caller should never need to check at runtime whether it got an `ok()`
result or an `ok_list()` result. For any given function, you should settle
on just one of them. `ok()` is usually the best choice as it can still be
used in list context.
`ok_list()` is not exported by default, but can be requested:
use results qw( :default ok_list );
`err()`
The `err()` function returns an error, or unsuccessful result.
It can be called without arguments to represent a general sense of doom,
but this is usually a bad idea:
return err(); # failed
It is generally better to give a reason why your function failed:
return err( "This feature isn't implemented" );
Or even better, an exception object:
return err( MyApp::Error::NotImplemented->new );
The results::exceptions module provides a very convenient way to create a
large number of lightweight exception classes suitable for that.
Like `ok()` this can take a list:
return err( MyApp::Error::Net->new, 0 .. 99 );
This would be unusual though, and is not generally recommended.
Exception Objects
It is often easier for your caller to deal with exception objects rather
than string error messages. This module comes with results::exceptions to
make creating these a little easier. The example in the "SYNOPSIS" section
could be written as:
use results;
use results::exceptions qw( UnexpectedRef UnexpectedUndef );
sub to_uppercase {
my $str = shift;
return UnexpectedRef->err if ref $str;
return UnexpectedUndef->err if not defined $str;
return ok( uc $str );
}
HANDLING RESULTS
Introduction
If you call a function which returns a Result, you are *required* to
handle the result in some way. If a Result goes out of scope or otherwise
gets destroyed before being handled, this is considered a programming
error. Currently this will only result in a warning being printed, as Perl
demotes exceptions thrown in destructors to warnings.
Results are blessed objects and should be handled by calling methods on
them.
Function
`is_result( $val )`
Returns true if $val is a Result object. You should rarely need to use
`is_result()` because a function which returns Results should never return
anything that isn't a Result.
`is_result()` is not exported by default, but can be requested:
use results qw( :default is_result );
Methods
The full set of methods available on Results is documented in
Result::Trait, but a few important ones are described here. These methods
are `is_err()`, `is_ok()`, `unwrap()`, `unwrap_err()`, `expect()`, and
`match()`.
`$result->is_err()`
Returns true if and only if the Result is an error.
`$result->is_ok()`
Returns true if and only if the Result is a success.
`$result->unwrap()`
Called on a successful Result, returns the result.
May be called in scalar or list context, and may return a list if `ok()`
was given a list.
If called on an unsuccessful result (error), will promote the error to a
fatal error. (That is, calls `die`.)
my $upper_name = to_uppercase( $name );
if ( $upper_name->is_ok() ) {
say "HELLO ", $upper_name->unwrap();
}
else {
warn "An error occurred!";
}
If `unwrap` is called, the Result is considered to be handled.
`$result->unwrap_err()`
Called on a unsuccessful Result, returns the error.
May be called in scalar or list context, and may return a list if `err()`
was given a list. A list of multiple values rarely makes sense though.
If called on a successful result (error), will result in a fatal error.
(That is, calls `die`.)
my $upper_name = to_uppercase( $name );
if ( $upper_name->is_ok() ) {
say "HELLO ", $upper_name->unwrap();
}
else {
warn "An error occurred: " . $upper_name->unwrap_err();
}
If `unwrap_err` is called, the Result is considered to be handled.
`$result->expect( $msg )`
Similar to `unwrap`, but if called on an unsuccessful Result, dies with
the given error message.
If `expect` is called, the Result is considered to be handled.
`$result->match( %dispatch_table )`
This provides an easy way to deal with different kinds of Results at the
same time.
$result->match(
err_Unauthorized => sub { ... },
err_FileNotFound => sub { ... },
err => sub { ... }, # all other errors
ok => sub { ... },
);
Other methods
See Result::Trait for other ways to concisely handle Results.
DIFFERENCES WITH RUST
Rust is strongly typed and can check many things at compile time which
this implementation cannot. These must all be done through self-discipline
in Perl. This includes:
* Ensuring that functions which return a Result cannot return a
non-Result.
* Ensuring that the recipient of a Result handles that Result.
* Ensuring that the type of the value inside the Result is expected by
the recipient. (Result::Trait includes a handful of methods for
run-time enforcement of type constraints though.)
Methods related to Rust's borrowing, copying, and cloning are not
implemented in Result::Trait as they do not make a lot of sense.
EXPORTS
This module exports four functions:
* `err`
* `ok`
* `ok_list`
* `is_result`
By default, only the first two are exported, but you can list the
functions you want like this:
use results qw( err ok ok_list is_result );
Or just:
use results -all;
You can import no functions using:
use results ();
And then just refer to them by their full name like `results::ok()`.
You can rename functions:
use results (
ok => { -as => 'Okay' },
err => { -as => 'Error' },
);
Renaming imports may be useful if you find the default names conflict with
other modules you're using. In particular, Test::More and other Perl
testing modules export a function called `ok`.
Lexical exports
If you have Perl 5.37.2 or above, or install Lexical::Sub on older
versions of Perl, you can import this module lexically using:
use results -lexical;
# or
use results -lexical, -all;
# or
use results -lexical, (
ok => { -as => 'Okay' },
err => { -as => 'Error' },
);
results::exceptions also supports lexical exports:
use results::exceptions -lexical, qw(
UnexpectedRef
UnexpectedUndef
);
BUGS
Please report any bugs to <https://github.com/tobyink/p5-results/issues>.
SEE ALSO
Result::Trait, <https://doc.rust-lang.org/std/result/>.
AUTHOR
Toby Inkster <tobyink@cpan.org>.
COPYRIGHT AND LICENCE
This software is copyright (c) 2022 by Toby Inkster.
This is free software; you can redistribute it and/or modify it under the
same terms as the Perl 5 programming language system itself.
DISCLAIMER OF WARRANTIES
THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.