Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc.
# Enable type check. The default is false.
BEGIN { $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1 }
use Test2::V0;
use Result::Simple;
use Types::Common -types;
use kura ErrorMessage => StrLength[3,];
use kura ValidName => sub { my (undef, $e) = validate_name($_); !$e };
use kura ValidAge => sub { my (undef, $e) = validate_age($_); !$e };
use kura ValidUser => Dict[name => ValidName, age => ValidAge];
sub validate_name {
my $name = shift;
return Err('No name') unless defined $name;
return Err('Empty name') unless length $name;
return Err('Reserved name') if $name eq 'root';
return Ok($name);
}
sub validate_age {
my $age = shift;
return Err('No age') unless defined $age;
return Err('Invalid age') unless $age =~ /\A\d+\z/;
return Err('Too young age') if $age < 18;
return Ok($age);
}
sub new_user :Result(ValidUser, ArrayRef[ErrorMessage]) {
my $args = shift;
my @errors;
my ($name, $name_err) = validate_name($args->{name});
push @errors, $name_err if $name_err;
my ($age, $age_err) = validate_age($args->{age});
push @errors, $age_err if $age_err;
return Err(\@errors) if @errors;
return Ok({ name => $name, age => $age });
}
my ($user1, $err1) = new_user({ name => 'taro', age => 42 });
is $user1, { name => 'taro', age => 42 };
is $err1, undef;
my ($user2, $err2) = new_user({ name => 'root', age => 1 });
is $user2, undef;
is $err2, ['Reserved name', 'Too young age'];
Result::Simple is a dead simple Perl-ish Result.
Result represents a function's return value as success or failure, enabling safer error handling and more effective control flow management. This pattern is used in other languages such as F#, Rust, and Go.
In Perl, this pattern is also useful, and this module provides a simple way to use it.
This module does not wrap a return value in an object. Just return a tuple like ($data, undef)
or (undef, $err)
.
Return a tuple of a given value and undef. When the function succeeds, it should return this.
sub add($a, $b) {
Ok($a + $b); # => ($a + $b, undef)
}
Return a tuple of undef and a given error. When the function fails, it should return this.
sub div($a, $b) {
return Err('Division by zero') if $b == 0; # => (undef, 'Division by zero')
Ok($a / $b);
}
Note that the error value must be a truthy value, otherwise it will throw an exception.
You can use the :Result(T, E)
attribute to define a function that returns a success or failure and asserts the return value types. Here is an example:
sub half :Result(Int, ErrorMessage) ($n) {
if ($n % 2) {
return Err('Odd number');
} else {
return Ok($n / 2);
}
}
-
T (success type)
When the function succeeds, then returns
($data, undef)
, and$data
should satisfy this type. -
E (error type)
When the function fails, then returns
(undef, $err)
, and$err
should satisfy this type. Additionally, type E must be truthy value to distinguish between success and failure.sub foo :Result(Int, Str) ($input) { } # => throw exception: Result E should not allow falsy values: ["0"] because Str allows "0"
When a function never returns an error, you can set type E to
undef
:sub double :Result(Int, undef) ($n) { Ok($n * 2) }
Note that types require check
method that returns true or false. So you can use your favorite type constraint module like
Type::Tiny, Moose, Mouse or Data::Checks etc.
If the ENV{RESULT_SIMPLE_CHECK_ENABLED}
environment is truthy before loading this module, it works as an assertion.
Otherwise, if it is falsy, :Result(T, E)
attribute does nothing. The default is false.
sub invalid :Result(Int, undef) { Ok("hello") }
my ($data, $err) = invalid();
# => throw exception when check enabled
# => no exception when check disabled
The following code is an example to enable it:
BEGIN { $ENV{RESULT_SIMPLE_CHECK_ENABLED} = is_test ? 1 : 0 }
use Result::Simple;
This option is useful for development and testing mode, and it recommended to set it to false for production.
Forgetting to call Ok
or Err
function is a common mistake. Consider the following example:
sub validate_name :Result(Str, ErrorMessage) ($name) {
return "Empty name" unless $name; # Oops! Forgot to call `Err` function.
return Ok($name);
}
my ($name, $err) = validate_name('');
# => throw exception: Invalid result tuple (T, E)
In this case, the function throws an exception because the return value is not a valid result tuple ($data, undef)
or (undef, $err)
.
This is fortunate, as the mistake is detected immediately. The following case is not detected:
sub foo :Result(Str, ErrorMessage) {
return (undef, 'apple'); # No use of `Ok` or `Err` function.
}
my ($data, $err) = foo;
# => $err is 'apple'
Here, the function returns a valid failure tuple (undef, $err)
. However, it is unclear whether this was intentional or a mistake.
The lack of Ok
or Err
makes the intent ambiguous.
Conclusively, be sure to use Ok
or Err
functions to make it clear whether the success or failure is intentional.
Copyright (C) kobaken.
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
kobaken [email protected]