From 7165fce9df51809bc9a239c06c3d39aa15e9c611 Mon Sep 17 00:00:00 2001 From: kobaken Date: Fri, 18 Apr 2025 15:13:18 +0900 Subject: [PATCH 1/7] [BREAKING CHANGES] Rename to ok,err. Remove :Result and so on 1. Rename `Ok`,`Err` to `ok`,`err` - naming to better align with Perl conventions. 2. Remove :Result attribute, Added `result_for` function - `use Result::Simple qw(ok err)` fails to import :Result attribute - It is hard to control attribute, so added just result_for function. 3. Use @EXPORT_OK to export `ok`, `err` and `result_for` --- lib/Result/Simple.pm | 110 +++++++++++++++++++++----------------- t/Result-Simple.t | 124 ++++++++++++++++++++++++------------------- t/check-disabled.t | 105 +++++++++++++++++++----------------- t/synopsis.t | 28 +++++----- 4 files changed, 203 insertions(+), 164 deletions(-) diff --git a/lib/Result/Simple.pm b/lib/Result/Simple.pm index 1350267..b42f836 100644 --- a/lib/Result/Simple.pm +++ b/lib/Result/Simple.pm @@ -6,66 +6,78 @@ our $VERSION = "0.03"; use Exporter 'import'; -our @EXPORT = qw( Ok Err _ATTR_CODE_Result ); +our @EXPORT_OK = qw( ok err result_for ); use Carp; -use Attribute::Handlers; use Scope::Upper (); use Sub::Util (); use Scalar::Util (); -# If this option is true, then check `Ok` and `Err` functions usage and check a return value type. +# If this option is true, then check `ok` and `err` functions usage and check a return value type. # However, it should be falsy for production code, because of performance and it is an assertion, not a validation. use constant CHECK_ENABLED => $ENV{RESULT_SIMPLE_CHECK_ENABLED} // 0; -# Err does not allow these values. +# err does not allow these values. use constant FALSY_VALUES => [0, '0', '', undef]; # When the function is successful, it should return this. -sub Ok { +sub ok { if (CHECK_ENABLED) { - croak "`Ok` must be called in list context" unless wantarray; - croak "`Ok` does not allow multiple arguments" if @_ > 1; - croak "`Ok` does not allow no arguments" if @_ == 0; + croak "`ok` must be called in list context" unless wantarray; + croak "`ok` does not allow multiple arguments" if @_ > 1; + croak "`ok` does not allow no arguments" if @_ == 0; } ($_[0], undef) } # When the function fails, it should return this. -sub Err { +sub err { if (CHECK_ENABLED) { - croak "`Err` must be called in list context" unless wantarray; - croak "`Err` does not allow multiple arguments." if @_ > 1; - croak "`Err` does not allow no arguments" if @_ == 0; - croak "`Err` does not allow a falsy value: @{[ _ddf($_[0]) ]}" unless $_[0]; + croak "`err` must be called in list context" unless wantarray; + croak "`err` does not allow multiple arguments." if @_ > 1; + croak "`err` does not allow no arguments" if @_ == 0; + croak "`err` does not allow a falsy value: @{[ _ddf($_[0]) ]}" unless $_[0]; } (undef, $_[0]) } -# This attribute is used to define a function that returns a success or failure. +# result_for foo => (T, E); +# This is used to define a function that returns a success or failure. # Example: `sub foo :Result(Int, Error) { ... }` -sub Result : ATTR(CODE) { - return unless CHECK_ENABLED; +sub result_for { + unless (CHECK_ENABLED) { + # This is a no-op if CHECK_ENABLED is false. + return; + } + + my ($function_name, $T, $E, %opts) = @_; + + my @caller = caller($opts{caller_level} || 0); + my $package = $opts{package} || $caller[0]; + my $filename = $caller[1]; + my $line = $caller[2]; - my ($package, $symbol, $referent, $attr, $data, $phase, $filename, $line) = @_; - my $name = *{$symbol}{NAME}; + my $code = $package->can($function_name); + + unless ($code) { + croak "result_for: function `$function_name` not found in package `$package` at $filename line $line\n"; + } - my ($T, $E) = @$data; unless (Scalar::Util::blessed($T) && $T->can('check')) { - croak "Result T requires `check` method, got: @{[ _ddf($T) ]} at $filename line $line\n"; + croak "result_for T requires `check` method, got: @{[ _ddf($T) ]} at $filename line $line\n"; } if (defined $E) { unless (Scalar::Util::blessed($E) && $E->can('check')) { - croak "Result E requires `check` method, got: @{[ _ddf($E) ]} at $filename line $line\n"; + croak "result_for E requires `check` method, got: @{[ _ddf($E) ]} at $filename line $line\n"; } if (my @f = grep { $E->check($_) } @{ FALSY_VALUES() }) { - croak "Result E should not allow falsy values: @{[ _ddf(\@f) ]} at $filename line $line\n"; + croak "result_for E should not allow falsy values: @{[ _ddf(\@f) ]} at $filename line $line\n"; } } - wrap_code($referent, $package, $name, $T, $E); + wrap_code($code, $package, $function_name, $T, $E); } # Wrap the original coderef with type check. @@ -77,7 +89,7 @@ sub wrap_code { my @result = &Scope::Upper::uplevel($code, @_, &Scope::Upper::CALLER(0)); unless (@result == 2) { - Carp::confess "Invalid result tuple (T, E) in `$name`. Do you forget to call `Ok` or `Err` function? Got: @{[ _ddf(\@result) ]}"; + Carp::confess "Invalid result tuple (T, E) in `$name`. Do you forget to call `ok` or `err` function? Got: @{[ _ddf(\@result) ]}"; } my ($data, $err) = @result; @@ -152,18 +164,18 @@ Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc. 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); + 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); + 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]) { @@ -176,8 +188,8 @@ Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc. 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 }); + return err(\@errors) if @errors; + return ok({ name => $name, age => $age }); } my ($user1, $err1) = new_user({ name => 'taro', age => 42 }); @@ -200,21 +212,21 @@ This module does not wrap a return value in an object. Just return a tuple like =head2 EXPORT FUNCTIONS -=head3 Ok +=head3 ok 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) + ok($a + $b); # => ($a + $b, undef) } -=head3 Err +=head3 err 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); + 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. @@ -227,9 +239,9 @@ You can use the C<:Result(T, E)> attribute to define a function that returns a s sub half :Result(Int, ErrorMessage) ($n) { if ($n % 2) { - return Err('Odd number'); + return err('Odd number'); } else { - return Ok($n / 2); + return ok($n / 2); } } @@ -249,7 +261,7 @@ Additionally, type E must be truthy value to distinguish between success and fai When a function never returns an error, you can set type E to C: - sub double :Result(Int, undef) ($n) { Ok($n * 2) } + sub double :Result(Int, undef) ($n) { ok($n * 2) } =back @@ -263,7 +275,7 @@ L, L, L or L etc. If the C environment is truthy before loading this module, it works as an assertion. Otherwise, if it is falsy, C<:Result(T, E)> attribute does nothing. The default is false. - sub invalid :Result(Int, undef) { Ok("hello") } + sub invalid :Result(Int, undef) { ok("hello") } my ($data, $err) = invalid(); # => throw exception when check enabled @@ -280,11 +292,11 @@ This option is useful for development and testing mode, and it recommended to se =head2 Avoiding Ambiguity in Result Handling -Forgetting to call C or C function is a common mistake. Consider the following example: +Forgetting to call C or C 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); + return "Empty name" unless $name; # Oops! Forgot to call `err` function. + return ok($name); } my ($name, $err) = validate_name(''); @@ -294,16 +306,16 @@ In this case, the function throws an exception because the return value is not a 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. + 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 C<(undef, $err)>. However, it is unclear whether this was intentional or a mistake. -The lack of C or C makes the intent ambiguous. +The lack of C or C makes the intent ambiguous. -Conclusively, be sure to use C or C functions to make it clear whether the success or failure is intentional. +Conclusively, be sure to use C or C functions to make it clear whether the success or failure is intentional. =head1 LICENSE diff --git a/t/Result-Simple.t b/t/Result-Simple.t index dcfd50c..c374bbf 100644 --- a/t/Result-Simple.t +++ b/t/Result-Simple.t @@ -4,7 +4,7 @@ Test the Result::Simple module with CHECK_ENABLED is truthy. =cut -use Test2::V0; +use Test2::V0 qw(subtest is like unlike dies done_testing); use lib "t/lib"; use TestType qw( Int NonEmptyStr ); @@ -13,52 +13,64 @@ BEGIN { $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1; } -use Result::Simple; +use Result::Simple qw( ok err result_for ); -subtest 'Test `Ok` and `Err` functions' => sub { - subtest '`Ok` and `Err` functions just return values' => sub { - my ($data, $err) = Ok('foo'); +subtest 'Test `ok` and `err` functions' => sub { + subtest '`ok` and `err` functions just return values' => sub { + my ($data, $err) = ok('foo'); is $data, 'foo'; is $err, undef; - ($data, $err) = Err('bar'); + ($data, $err) = err('bar'); is $data, undef; is $err, 'bar'; }; - subtest '`Ok` and `Err` must be called in list context' => sub { - like dies { my $data = Ok('foo') }, qr/`Ok` must be called in list context/; - like dies { my $err = Err('bar') }, qr/`Err` must be called in list context/; + subtest '`ok` and `err` must be called in list context' => sub { + like dies { my $data = ok('foo') }, qr/`ok` must be called in list context/; + like dies { my $err = err('bar') }, qr/`err` must be called in list context/; }; - subtest '`Ok` and `Err` does not allow multiple arguments' => sub { - like dies { my ($data, $err) = Ok('foo', 'bar') }, qr/`Ok` does not allow multiple arguments/; - like dies { my ($data, $err) = Err('bar', 'foo') }, qr/`Err` does not allow multiple arguments/; + subtest '`ok` and `err` does not allow multiple arguments' => sub { + like dies { my ($data, $err) = ok('foo', 'bar') }, qr/`ok` does not allow multiple arguments/; + like dies { my ($data, $err) = err('bar', 'foo') }, qr/`err` does not allow multiple arguments/; }; - subtest '`Ok` and `Err` does not allow no arguments' => sub { - like dies { my ($data, $err) = Ok() }, qr/`Ok` does not allow no arguments/; - like dies { my ($data, $err) = Err() }, qr/`Err` does not allow no arguments/; + subtest '`ok` and `err` does not allow no arguments' => sub { + like dies { my ($data, $err) = ok() }, qr/`ok` does not allow no arguments/; + like dies { my ($data, $err) = err() }, qr/`err` does not allow no arguments/; }; - subtest '`Err` does not allow falsy values' => sub { - like dies { my ($data, $err) = Err(0) }, qr/`Err` does not allow a falsy value: 0/; - like dies { my ($data, $err) = Err('0') }, qr/`Err` does not allow a falsy value: '0'/; - like dies { my ($data, $err) = Err('') }, qr/`Err` does not allow a falsy value: ''/; + subtest '`err` does not allow falsy values' => sub { + like dies { my ($data, $err) = err(0) }, qr/`err` does not allow a falsy value: 0/; + like dies { my ($data, $err) = err('0') }, qr/`err` does not allow a falsy value: '0'/; + like dies { my ($data, $err) = err('') }, qr/`err` does not allow a falsy value: ''/; }; }; -subtest 'Test :Result attribute' => sub { +subtest 'Test `result_for` function' => sub { # valid cases - sub valid :Result(Int, NonEmptyStr) { Ok(42) } - sub no_error :Result(Int, undef) { Ok(42) } + result_for valid => Int, NonEmptyStr; + sub valid { ok(42) } + + result_for no_error => Int, undef; + sub no_error { ok(42) } # invalid cases - sub invalid_ok_type :Result(Int, NonEmptyStr) { Ok('foo') } - sub invalid_err_type :Result(Int, NonEmptyStr) { Err(\1) } - sub a_few_result :Result(Int, NonEmptyStr) { 'foo' } - sub too_many_result :Result(Int, NonEmptyStr) { (1,2,3) } - sub never_return_error :Result(Int, undef) { Err('foo') } + result_for invalid_ok_type => Int, NonEmptyStr; + sub invalid_ok_type { ok('foo') } + + result_for invalid_err_type => Int, NonEmptyStr; + sub invalid_err_type { err(\1) } + + result_for a_few_result => Int, NonEmptyStr; + sub a_few_result { 'foo' } + + result_for too_many_result => Int, NonEmptyStr; + sub too_many_result { (1,2,3) } + + result_for never_return_error => Int, undef; + sub never_return_error { err('foo') } subtest 'When a return value satisfies the Result type (T, E), then return the value' => sub { my ($data, $err) = valid(); @@ -75,8 +87,8 @@ subtest 'Test :Result attribute' => sub { subtest 'When a return value does not satisfy the Result type (T, E), then throw a exception' => sub { like dies { my ($data, $err) = invalid_ok_type() }, qr!Invalid success result in `invalid_ok_type`: \['foo',undef\]!; like dies { my ($data, $err) = invalid_err_type() }, qr!Invalid failure result in `invalid_err_type`: \[undef,\\1\]!; - like dies { my ($data, $err) = a_few_result() }, qr!Invalid result tuple \(T, E\) in `a_few_result`. Do you forget to call `Ok` or `Err` function\? Got: \['foo'\]!; - like dies { my ($data, $err) = too_many_result() }, qr!Invalid result tuple \(T, E\) in `too_many_result`. Do you forget to call `Ok` or `Err` function\? Got: \[1,2,3\]!; + like dies { my ($data, $err) = a_few_result() }, qr!Invalid result tuple \(T, E\) in `a_few_result`. Do you forget to call `ok` or `err` function\? Got: \['foo'\]!; + like dies { my ($data, $err) = too_many_result() }, qr!Invalid result tuple \(T, E\) in `too_many_result`. Do you forget to call `ok` or `err` function\? Got: \[1,2,3\]!; like dies { my ($data, $err) = never_return_error() }, qr!Never return error in `never_return_error`: \[undef,'foo'\]!; }; @@ -84,43 +96,47 @@ subtest 'Test :Result attribute' => sub { like dies { my $result = valid() }, qr/Must handle error in `valid`/; }; - subtest 'Result(T, E) requires `check` method' => sub { - eval "sub invalid_type_T :Result('HELLO', NonEmptyStr) { Ok('HELLO') }"; - like $@, qr/Result T requires `check` method/; + subtest '(T, E) requires `check` method' => sub { + sub invalid_type_T { ok(42) }; + like dies { result_for invalid_type_T => 'Hello', NonEmptyStr }, qr!result_for T requires `check` method!; - eval "sub invalid_type_E :Result(Int, 'WORLD') { Err('WORLD') }"; - like $@, qr/Result E requires `check` method/; + sub invalid_type_E { err(42) }; + like dies { result_for invalid_type_E => Int, 'World' }, qr!result_for E requires `check` method!; }; subtest 'E should not allow falsy values' => sub { - eval "sub should_not_allow_falsy :Result(Int, Int) { }"; - like $@, qr/Result E should not allow falsy values: \[0,'0'\]/; + sub should_not_allow_falsy { err(0) }; + like dies { result_for should_not_allow_falsy => Int, Int }, qr/result_for E should not allow falsy values: \[0,'0'\]/; }; -}; -subtest 'Test the details of :Result attribute' => sub { - subtest 'Useful stacktrace' => sub { - sub test_stacktrace :Result(Int, NonEmptyStr) { Carp::confess('hello') } + subtest 'Test the details of `retsult_for` function' => sub { + subtest 'Useful stacktrace' => sub { - eval { my ($data, $err) = test_stacktrace() }; + result_for test_stacktrace => Int, NonEmptyStr; + sub test_stacktrace { Carp::confess('hello') } - my $file = __FILE__; - like $@, qr!hello at $file line!; - like $@, qr/main::test_stacktrace\(\) called at $file line /, 'stacktrace includes function name'; - unlike $@, qr/Result::Simple::/, 'stacktrace does not include Result::Simple by Scope::Upper'; - }; + eval { my ($data, $err) = test_stacktrace() }; + + my $file = __FILE__; + like $@, qr!hello at $file line!; + like $@, qr/main::test_stacktrace\(\) called at $file line /, 'stacktrace includes function name'; + unlike $@, qr/Result::Simple::/, 'stacktrace does not include Result::Simple by Scope::Upper'; + }; + + subtest 'Same subname and prototype as original' => sub { - subtest 'Same subname and prototype as original' => sub { - sub same (;$) :Result(Int, NonEmptyStr) { Ok(42) } + result_for same => Int, NonEmptyStr; + sub same (;$) { ok(42) } - my $code = \&same; + my $code = \&same; - require Sub::Util; - my $name = Sub::Util::subname($code); - is $name, 'main::same'; + require Sub::Util; + my $name = Sub::Util::subname($code); + is $name, 'main::same'; - my $proto = Sub::Util::prototype($code); - is $proto, ';$'; + my $proto = Sub::Util::prototype($code); + is $proto, ';$'; + }; }; }; diff --git a/t/check-disabled.t b/t/check-disabled.t index d1c4f65..bf8e870 100644 --- a/t/check-disabled.t +++ b/t/check-disabled.t @@ -5,46 +5,52 @@ These tests are same cases as Result-Simple.t, but CHECK_ENABLED is falsy. =cut -use Test2::V0; +use Test2::V0 qw(subtest is like unlike lives done_testing); +use Test2::V0 ok => { -as => 'test_ok' }; use lib "t/lib"; use TestType qw( Int NonEmptyStr ); BEGIN { - # Default is falsy - # $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 0; + $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 0; } -use Result::Simple; +use Result::Simple qw( ok err result_for ); -subtest 'Test `Ok` and `Err` functions' => sub { - subtest '`Ok` and `Err` functions just return values' => sub { - my ($data, $err) = Ok('foo'); +subtest 'Test `ok` and `err` functions' => sub { + subtest '`ok` and `err` functions just return values' => sub { + my ($data, $err) = ok('foo'); is $data, 'foo'; is $err, undef; - ($data, $err) = Err('bar'); + ($data, $err) = err('bar'); is $data, undef; is $err, 'bar'; }; - subtest '`Ok` and `Err` must be called in list context, but when CHECK_ENABLED is falsy, then do not throw exception' => sub { - ok lives { my $data = Ok('foo') }; - ok lives { my $err = Err('bar') }; + subtest '`ok` and `err` must be called in list context, but when CHECK_ENABLED is falsy, then do not throw exception' => sub { + test_ok lives { my $data = ok('foo') }; + test_ok lives { my $err = err('bar') }; }; - subtest '`Err` does not allow falsy values, but when CHECK_ENABLED is falsy, then do not throw exception' => sub { - ok lives { my ($data, $err) = Err() }; - ok lives { my ($data, $err) = Err(0) }; - ok lives { my ($data, $err) = Err('0') }; - ok lives { my ($data, $err) = Err('') }; + subtest '`err` does not allow falsy values, but when CHECK_ENABLED is falsy, then do not throw exception' => sub { + test_ok lives { my ($data, $err) = err() }; + test_ok lives { my ($data, $err) = err(0) }; + test_ok lives { my ($data, $err) = err('0') }; + test_ok lives { my ($data, $err) = err('') }; }; }; -subtest 'Test :Result attribute' => sub { - sub valid : Result(Int, NonEmptyStr) { Ok(42) } - sub invalid_ok_type :Result(Int, NonEmptyStr) { Ok('foo') } - sub invalid_err_type :Result(Int, NonEmptyStr) { Err(\1) } +subtest 'Test `result_for` function' => sub { + + result_for valid => Int, NonEmptyStr; + sub valid { ok(42) } + + result_for invalid_ok_type => Int, NonEmptyStr; + sub invalid_ok_type { ok('foo') } + + result_for invalid_err_type => Int, NonEmptyStr; + sub invalid_err_type { err(\1) } subtest 'When a return value satisfies the Result type (T, E), then return the value' => sub { my ($data, $err) = valid(); @@ -53,53 +59,56 @@ subtest 'Test :Result attribute' => sub { }; subtest 'When a return value does not satisfy the Result type (T, E), then throw a exception, but CHECK_ENABLED is falsy, then do not' => sub { - ok lives { my ($data, $err) = invalid_ok_type() }; - ok lives { my ($data, $err) = invalid_err_type() }; + test_ok lives { my ($data, $err) = invalid_ok_type() }; + test_ok lives { my ($data, $err) = invalid_err_type() }; }; subtest 'Must handle error, but CHECK_ENABLED is falsy, then do not throw exception' => sub { - ok lives { my $result = valid() }; + test_ok lives { my $result = valid() }; }; subtest 'Result(T, E) requires `check` method, but CHECK_ENABLED is falsy, then do not throw exception' => sub { - eval "sub invalid_type_T :Result('HELLO', NonEmptyStr) { Ok('HELLO') }"; - is $@, ''; + sub invalid_type_T { ok(42) }; + test_ok lives { result_for invalid_type_T => 'Hello', NonEmptyStr }; - eval "sub invalid_type_E :Result(Int, 'WORLD') { Err('WORLD') }"; - is $@, ''; + sub invalid_type_E { err(42) }; + test_ok lives { result_for invalid_type_E => Int, 'World' }; }; subtest 'E should not allow falsy values, but CHECK_ENABLED is falsy, then do not throw exception' => sub { - eval "sub should_not_allow_falsy :Result(Int, Int) { }"; - is $@, ''; + sub should_not_allow_falsy { err(0) }; + test_ok lives { result_for should_not_allow_falsy => Int, Int }; }; -}; -subtest 'Test the details of :Result attribute' => sub { - note 'When CHECK_ENABLED is falsy, then do not wrap the original function'; + subtest 'Test the details of `retsult_for` function' => sub { + # 'When CHECK_ENABLED is falsy, then do not wrap the original function'; - subtest 'Useful stacktrace' => sub { - sub test_stacktrace :Result(Int, NonEmptyStr) { Carp::confess('hello') } + subtest 'Useful stacktrace' => sub { + result_for test_stacktrace => Int, NonEmptyStr; + sub test_stacktrace { Carp::confess('hello') } - eval { my ($data, $err) = test_stacktrace() }; + eval { my ($data, $err) = test_stacktrace() }; - my $file = __FILE__; - like $@, qr!hello at $file line!; - like $@, qr/main::test_stacktrace\(\) called at $file line /, 'stacktrace includes function name'; - unlike $@, qr/Result::Simple::/, 'stacktrace does not include Result::Simple by Scope::Upper'; - }; + my $file = __FILE__; + like $@, qr!hello at $file line!; + like $@, qr/main::test_stacktrace\(\) called at $file line /, 'stacktrace includes function name'; + unlike $@, qr/Result::Simple::/, 'stacktrace does not include Result::Simple by Scope::Upper'; + }; + + subtest 'Same subname and prototype as original' => sub { - subtest 'Same subname and prototype as original' => sub { - sub same (;$) :Result(Int, NonEmptyStr) { Ok(42) } + result_for same => Int, NonEmptyStr; + sub same (;$) { ok(42) } - my $code = \&same; + my $code = \&same; - require Sub::Util; - my $name = Sub::Util::subname($code); - is $name, 'main::same'; + require Sub::Util; + my $name = Sub::Util::subname($code); + is $name, 'main::same'; - my $proto = Sub::Util::prototype($code); - is $proto, ';$'; + my $proto = Sub::Util::prototype($code); + is $proto, ';$'; + }; }; }; diff --git a/t/synopsis.t b/t/synopsis.t index 7e76a63..762184a 100644 --- a/t/synopsis.t +++ b/t/synopsis.t @@ -1,11 +1,11 @@ -use Test2::V0; +use Test2::V0 qw(is done_testing); use Test2::Require::Module 'Type::Tiny' => '2.000000'; use Test2::Require::Module 'kura'; # Enable type check. The default is false. BEGIN { $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1 } -use Result::Simple; +use Result::Simple qw( ok err result_for ); use Types::Common -types; use kura ErrorMessage => StrLength[3,]; @@ -15,21 +15,23 @@ 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); + 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); + 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]) { +result_for new_user => ValidUser, ArrayRef[ErrorMessage]; + +sub new_user { my $args = shift; my @errors; @@ -39,8 +41,8 @@ sub new_user :Result(ValidUser, ArrayRef[ErrorMessage]) { 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 }); + return err(\@errors) if @errors; + return ok({ name => $name, age => $age }); } my ($user1, $err1) = new_user({ name => 'taro', age => 42 }); From 7427b40841fb0149998c4de14ff7e7fa496ebdf3 Mon Sep 17 00:00:00 2001 From: kobaken Date: Fri, 18 Apr 2025 15:37:17 +0900 Subject: [PATCH 2/7] [BREAKING CHANGES] Change default value of RESULT_SIMPLE_CHECK_ENABLED --- lib/Result/Simple.pm | 29 +++++++++++------------------ t/Result-Simple.t | 3 ++- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/lib/Result/Simple.pm b/lib/Result/Simple.pm index b42f836..7aeae2e 100644 --- a/lib/Result/Simple.pm +++ b/lib/Result/Simple.pm @@ -14,8 +14,8 @@ use Sub::Util (); use Scalar::Util (); # If this option is true, then check `ok` and `err` functions usage and check a return value type. -# However, it should be falsy for production code, because of performance and it is an assertion, not a validation. -use constant CHECK_ENABLED => $ENV{RESULT_SIMPLE_CHECK_ENABLED} // 0; +# However, it should be falsy for production code, because of performance, and it is an assertion, not a validation. +use constant CHECK_ENABLED => $ENV{RESULT_SIMPLE_CHECK_ENABLED} // 1; # err does not allow these values. use constant FALSY_VALUES => [0, '0', '', undef]; @@ -150,11 +150,8 @@ Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc. =head1 SYNOPSIS - # Enable type check. The default is false. - BEGIN { $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1 } - use Test2::V0; - use Result::Simple; + use Result::Simple qw(ok err result_for); use Types::Common -types; use kura ErrorMessage => StrLength[3,]; @@ -178,7 +175,9 @@ Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc. return ok($age); } - sub new_user :Result(ValidUser, ArrayRef[ErrorMessage]) { + result_for new_user => ValidUser, ArrayRef[ErrorMessage]; + + sub new_user { my $args = shift; my @errors; @@ -273,20 +272,14 @@ L, L, L or L etc. =head3 C<$ENV{RESULT_SIMPLE_CHECK_ENABLED}> If the C environment is truthy before loading this module, it works as an assertion. -Otherwise, if it is falsy, C<:Result(T, E)> attribute does nothing. The default is false. +Otherwise, if it is falsy, C attribute does nothing. The default is true. +This option is useful for development and testing mode, and it recommended to set it to false for production. - sub invalid :Result(Int, undef) { ok("hello") } + result_for foo => Int, undef; + sub foo { ok("hello") } - my ($data, $err) = invalid(); + my ($data, $err) = foo(); # => 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. =head1 NOTE diff --git a/t/Result-Simple.t b/t/Result-Simple.t index c374bbf..229fed7 100644 --- a/t/Result-Simple.t +++ b/t/Result-Simple.t @@ -10,7 +10,8 @@ use lib "t/lib"; use TestType qw( Int NonEmptyStr ); BEGIN { - $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1; + # Enable type check. The default is true. + # $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1; } use Result::Simple qw( ok err result_for ); From a59d021cb59a3f456cd69d4237830594e7453ca4 Mon Sep 17 00:00:00 2001 From: kobaken Date: Fri, 18 Apr 2025 15:54:30 +0900 Subject: [PATCH 3/7] Fixed document about :Result attribute --- lib/Result/Simple.pm | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/Result/Simple.pm b/lib/Result/Simple.pm index 7aeae2e..6d0fb60 100644 --- a/lib/Result/Simple.pm +++ b/lib/Result/Simple.pm @@ -43,7 +43,18 @@ sub err { # result_for foo => (T, E); # This is used to define a function that returns a success or failure. -# Example: `sub foo :Result(Int, Error) { ... }` +# +# Example +# +# result_for div => Int, ErrorMessage; +# +# sub div { +# my ($a, $b) = @_; +# if ($b == 0) { +# return err('Division by zero'); +# } +# return ok($a / $b); +# } sub result_for { unless (CHECK_ENABLED) { # This is a no-op if CHECK_ENABLED is false. @@ -211,7 +222,7 @@ This module does not wrap a return value in an object. Just return a tuple like =head2 EXPORT FUNCTIONS -=head3 ok +=head3 ok($value) Return a tuple of a given value and undef. When the function succeeds, it should return this. @@ -219,7 +230,7 @@ Return a tuple of a given value and undef. When the function succeeds, it should ok($a + $b); # => ($a + $b, undef) } -=head3 err +=head3 err($error) Return a tuple of undef and a given error. When the function fails, it should return this. @@ -230,13 +241,13 @@ Return a tuple of undef and a given error. When the function fails, it should re Note that the error value must be a truthy value, otherwise it will throw an exception. -=head2 ATTRIBUTES +=head3 result_for $function_name => $T, $E -=head3 :Result(T, E) +You can use the C to define a function that returns a success or failure and asserts the return value types. Here is an example: -You can use the C<:Result(T, E)> attribute to define a function that returns a success or failure and asserts the return value types. Here is an example: + result_for half => Int, ErrorMessage; - sub half :Result(Int, ErrorMessage) ($n) { + sub half ($n) { if ($n % 2) { return err('Odd number'); } else { @@ -255,12 +266,15 @@ When the function succeeds, then returns C<($data, undef)>, and C<$data> should When the function fails, then returns C<(undef, $err)>, and C<$err> should satisfy this type. Additionally, type E must be truthy value to distinguish between success and failure. - sub foo :Result(Int, Str) ($input) { } + result_for foo => Int, Str; + + sub foo ($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 C: - sub double :Result(Int, undef) ($n) { ok($n * 2) } + result_for bar => Int, undef; + sub double ($n) { ok($n * 2) } =back @@ -287,7 +301,9 @@ This option is useful for development and testing mode, and it recommended to se Forgetting to call C or C function is a common mistake. Consider the following example: - sub validate_name :Result(Str, ErrorMessage) ($name) { + result_for validate_name => Str, ErrorMessage; + + sub validate_name ($name) { return "Empty name" unless $name; # Oops! Forgot to call `err` function. return ok($name); } @@ -298,7 +314,9 @@ Forgetting to call C or C function is a common mistake. Consider the fo In this case, the function throws an exception because the return value is not a valid result tuple C<($data, undef)> or C<(undef, $err)>. This is fortunate, as the mistake is detected immediately. The following case is not detected: - sub foo :Result(Str, ErrorMessage) { + result_for foo => Str, ErrorMessage; + + sub foo { return (undef, 'apple'); # No use of `ok` or `err` function. } From ed75aa6ab2541df20383e2b87cf17f8e94ef6e30 Mon Sep 17 00:00:00 2001 From: kobaken Date: Fri, 18 Apr 2025 16:07:50 +0900 Subject: [PATCH 4/7] Added unsafe_unwrap, unsafe_unwrap_err --- lib/Result/Simple.pm | 44 ++++++++++++++++++++++++++++++++++++++++---- t/Result-Simple.t | 24 +++++++++++++++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/lib/Result/Simple.pm b/lib/Result/Simple.pm index 6d0fb60..932b2aa 100644 --- a/lib/Result/Simple.pm +++ b/lib/Result/Simple.pm @@ -6,7 +6,13 @@ our $VERSION = "0.03"; use Exporter 'import'; -our @EXPORT_OK = qw( ok err result_for ); +our @EXPORT_OK = qw( + ok + err + result_for + unsafe_unwrap + unsafe_unwrap_err +); use Carp; use Scope::Upper (); @@ -63,10 +69,10 @@ sub result_for { my ($function_name, $T, $E, %opts) = @_; - my @caller = caller($opts{caller_level} || 0); - my $package = $opts{package} || $caller[0]; + my @caller = caller($opts{caller_level} || 0); + my $package = $opts{package} || $caller[0]; my $filename = $caller[1]; - my $line = $caller[2]; + my $line = $caller[2]; my $code = $package->can($function_name); @@ -136,6 +142,26 @@ sub wrap_code { *{$fullname} = $wrapped; } +# `unsafe_nwrap` takes a Result and returns a T when the result is an Ok, otherwise it throws exception. +# It should be used in tests or debugging code. +sub unsafe_unwrap { + my ($value, $err) = @_; + if ($err) { + croak "Error called in `unsafe_unwrap`: @{[ _ddf($err) ]}" + } + return $value; +} + +# `unsafe_unwrap_err` takes a Result and returns an E when the result is an Err, otherwise it throws exception. +# It should be used in tests or debugging code. +sub unsafe_unwrap_err { + my ($value, $err) = @_; + if (!$err) { + croak "No error called in `unsafe_unwrap_err`: @{[ _ddf($value) ]}" + } + return $err; +} + # Dump data for debugging. sub _ddf { my $v = shift; @@ -276,6 +302,16 @@ When a function never returns an error, you can set type E to C: result_for bar => Int, undef; sub double ($n) { ok($n * 2) } +=head3 unsafe_unwrap($data, $err) + +C takes a Result and returns a T when the result is an Ok, otherwise it throws exception. +It should be used in tests or debugging code. + +=head3 unsafe_unwrap_err($data, $err) + +C takes a Result and returns an E when the result is an Err, otherwise it throws exception. +It should be used in tests or debugging code. + =back Note that types require C method that returns true or false. So you can use your favorite type constraint module like diff --git a/t/Result-Simple.t b/t/Result-Simple.t index 229fed7..6f1e38a 100644 --- a/t/Result-Simple.t +++ b/t/Result-Simple.t @@ -14,7 +14,7 @@ BEGIN { # $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1; } -use Result::Simple qw( ok err result_for ); +use Result::Simple qw( ok err result_for unsafe_unwrap unsafe_unwrap_err ); subtest 'Test `ok` and `err` functions' => sub { subtest '`ok` and `err` functions just return values' => sub { @@ -141,4 +141,26 @@ subtest 'Test `result_for` function' => sub { }; }; +subtest 'Test `unsafe_unwrap` function' => sub { + subtest 'When ok() is called, then return the value' => sub { + my $got = unsafe_unwrap(ok(42)); + is $got, 42; + }; + + subtest 'When err() is called, then throw a exception' => sub { + like dies { my ($data, $err) = unsafe_unwrap(err('foo')) }, qr/Error called in `unsafe_unwrap`/; + }; +}; + +subtest 'Test `unsafe_unwrap_err` function' => sub { + subtest 'When ok() is called, then throw a exception' => sub { + like dies { my ($data, $err) = unsafe_unwrap_err(ok(42)) }, qr/No error called in `unsafe_unwrap_err`/; + }; + subtest 'When err() is called, then return the value' => sub { + my $got = unsafe_unwrap_err(err('foo')); + is $got, 'foo'; + }; +}; + + done_testing; From bed7f0f9fdf9cce8a70e294ad9c5a0a1ce660379 Mon Sep 17 00:00:00 2001 From: kobaken Date: Fri, 18 Apr 2025 16:17:22 +0900 Subject: [PATCH 5/7] Added alias features --- cpanfile | 1 + lib/Result/Simple.pm | 15 ++++++++++++--- t/alias.t | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 t/alias.t diff --git a/cpanfile b/cpanfile index f4be1be..bf23881 100644 --- a/cpanfile +++ b/cpanfile @@ -2,6 +2,7 @@ requires 'perl', '5.014004'; requires 'Scope::Upper'; requires 'Sub::Util'; +requires 'Exporter::Tiny'; on 'configure' => sub { requires 'Module::Build::Tiny', '0.035'; diff --git a/lib/Result/Simple.pm b/lib/Result/Simple.pm index 932b2aa..dae1925 100644 --- a/lib/Result/Simple.pm +++ b/lib/Result/Simple.pm @@ -4,9 +4,7 @@ use warnings; our $VERSION = "0.03"; -use Exporter 'import'; - -our @EXPORT_OK = qw( +use Exporter::Shiny qw( ok err result_for @@ -364,6 +362,17 @@ The lack of C or C makes the intent ambiguous. Conclusively, be sure to use C or C functions to make it clear whether the success or failure is intentional. +=head2 Use alias name + +You can use alias name for C and C functions like this: + + use Result::Simple + ok => { -as => 'left' }, + err => { -as => 'right' }; + + left('foo'); # => ('foo', undef) + right('bar'); # => (undef, 'bar') + =head1 LICENSE Copyright (C) kobaken. diff --git a/t/alias.t b/t/alias.t new file mode 100644 index 0000000..23b23ad --- /dev/null +++ b/t/alias.t @@ -0,0 +1,17 @@ +=pod + +Test using alias names for Result::Simple functions. + +=cut + +use Test2::V0; + +use Result::Simple + ok => { -as => 'left' }, + err => { -as => 'right' }, + ; + +is [ left('foo') ], ['foo', undef ], 'ok() is aliased to success()'; +is [ right('bar') ], [undef, 'bar'], 'err() is aliased to failure()'; + +done_testing; From 11ee9a786314807ef0afb7b3b9932aa982e4c01a Mon Sep 17 00:00:00 2001 From: kobaken Date: Fri, 18 Apr 2025 16:56:59 +0900 Subject: [PATCH 6/7] Update meta --- META.json | 1 + README.md | 121 +++++++++++++++++++++++++------------------ lib/Result/Simple.pm | 4 +- 3 files changed, 74 insertions(+), 52 deletions(-) diff --git a/META.json b/META.json index 58b26d7..5130b96 100644 --- a/META.json +++ b/META.json @@ -42,6 +42,7 @@ }, "runtime" : { "requires" : { + "Exporter::Tiny" : "0", "Scope::Upper" : "0", "Sub::Util" : "0", "perl" : "5.014004" diff --git a/README.md b/README.md index 730b0cb..1d595e3 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,8 @@ Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc. # SYNOPSIS ```perl -# Enable type check. The default is false. -BEGIN { $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1 } - use Test2::V0; -use Result::Simple; +use Result::Simple qw(ok err result_for); use Types::Common -types; use kura ErrorMessage => StrLength[3,]; @@ -20,21 +17,23 @@ 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); + 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); + 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]) { +result_for new_user => ValidUser, ArrayRef[ErrorMessage]; + +sub new_user { my $args = shift; my @errors; @@ -44,8 +43,8 @@ sub new_user :Result(ValidUser, ArrayRef[ErrorMessage]) { 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 }); + return err(\@errors) if @errors; + return ok({ name => $name, age => $age }); } my ($user1, $err1) = new_user({ name => 'taro', age => 42 }); @@ -69,41 +68,41 @@ This module does not wrap a return value in an object. Just return a tuple like ## EXPORT FUNCTIONS -### Ok +### ok($value) Return a tuple of a given value and undef. When the function succeeds, it should return this. ```perl sub add($a, $b) { - Ok($a + $b); # => ($a + $b, undef) + ok($a + $b); # => ($a + $b, undef) } ``` -### Err +### err($error) Return a tuple of undef and a given error. When the function fails, it should return this. ```perl sub div($a, $b) { - return Err('Division by zero') if $b == 0; # => (undef, 'Division by zero') - Ok($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. -## ATTRIBUTES - -### :Result(T, E) +### result\_for $function\_name => $T, $E -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: +You can use the `result_for` to define a function that returns a success or failure and asserts the return value types. Here is an example: ```perl -sub half :Result(Int, ErrorMessage) ($n) { +result_for half => Int, ErrorMessage; + +sub half ($n) { if ($n % 2) { - return Err('Odd number'); + return err('Odd number'); } else { - return Ok($n / 2); + return ok($n / 2); } } ``` @@ -118,16 +117,29 @@ sub half :Result(Int, ErrorMessage) ($n) { Additionally, type E must be truthy value to distinguish between success and failure. ```perl - sub foo :Result(Int, Str) ($input) { } + result_for foo => Int, Str; + + sub foo ($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`: ```perl - sub double :Result(Int, undef) ($n) { Ok($n * 2) } + result_for bar => Int, undef; + sub double ($n) { ok($n * 2) } ``` +### unsafe\_unwrap($data, $err) + +`unsafe_unwrap` takes a Result and returns a T when the result is an Ok, otherwise it throws exception. +It should be used in tests or debugging code. + +### unsafe\_unwrap\_err($data, $err) + +`unsafe_unwrap_err` takes a Result and returns an E when the result is an Err, otherwise it throws exception. +It should be used in tests or debugging code. + Note that types require `check` method that returns true or false. So you can use your favorite type constraint module like [Type::Tiny](https://metacpan.org/pod/Type%3A%3ATiny), [Moose](https://metacpan.org/pod/Moose), [Mouse](https://metacpan.org/pod/Mouse) or [Data::Checks](https://metacpan.org/pod/Data%3A%3AChecks) etc. @@ -136,35 +148,29 @@ Note that types require `check` method that returns true or false. So you can us ### `$ENV{RESULT_SIMPLE_CHECK_ENABLED}` 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. +Otherwise, if it is falsy, `result_for` attribute does nothing. The default is true. +This option is useful for development and testing mode, and it recommended to set it to false for production. ```perl -sub invalid :Result(Int, undef) { Ok("hello") } +result_for foo => Int, undef; +sub foo { ok("hello") } -my ($data, $err) = invalid(); +my ($data, $err) = foo(); # => throw exception when check enabled -# => no exception when check disabled -``` - -The following code is an example to enable it: - -```perl -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. - # NOTE ## Avoiding Ambiguity in Result Handling -Forgetting to call `Ok` or `Err` function is a common mistake. Consider the following example: +Forgetting to call `ok` or `err` function is a common mistake. Consider the following example: ```perl -sub validate_name :Result(Str, ErrorMessage) ($name) { - return "Empty name" unless $name; # Oops! Forgot to call `Err` function. - return Ok($name); +result_for validate_name => Str, ErrorMessage; + +sub validate_name ($name) { + return "Empty name" unless $name; # Oops! Forgot to call `err` function. + return ok($name); } my ($name, $err) = validate_name(''); @@ -175,8 +181,10 @@ In this case, the function throws an exception because the return value is not a This is fortunate, as the mistake is detected immediately. The following case is not detected: ```perl -sub foo :Result(Str, ErrorMessage) { - return (undef, 'apple'); # No use of `Ok` or `Err` function. +result_for foo => Str, ErrorMessage; + +sub foo { + return (undef, 'apple'); # No use of `ok` or `err` function. } my ($data, $err) = foo; @@ -184,9 +192,22 @@ my ($data, $err) = foo; ``` 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. +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. +Conclusively, be sure to use `ok` or `err` functions to make it clear whether the success or failure is intentional. + +## Use alias name + +You can use alias name for `ok` and `err` functions like this: + +```perl +use Result::Simple + ok => { -as => 'left' }, + err => { -as => 'right' }; + +left('foo'); # => ('foo', undef) +right('bar'); # => (undef, 'bar') +``` # LICENSE diff --git a/lib/Result/Simple.pm b/lib/Result/Simple.pm index dae1925..160d84b 100644 --- a/lib/Result/Simple.pm +++ b/lib/Result/Simple.pm @@ -300,6 +300,8 @@ When a function never returns an error, you can set type E to C: result_for bar => Int, undef; sub double ($n) { ok($n * 2) } +=back + =head3 unsafe_unwrap($data, $err) C takes a Result and returns a T when the result is an Ok, otherwise it throws exception. @@ -310,8 +312,6 @@ It should be used in tests or debugging code. C takes a Result and returns an E when the result is an Err, otherwise it throws exception. It should be used in tests or debugging code. -=back - Note that types require C method that returns true or false. So you can use your favorite type constraint module like L, L, L or L etc. From 2294be89b1963346eb0732b85923172a6cf63f22 Mon Sep 17 00:00:00 2001 From: kobaken Date: Fri, 18 Apr 2025 17:05:32 +0900 Subject: [PATCH 7/7] Added test case giving invalid function name to result_for --- t/Result-Simple.t | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/t/Result-Simple.t b/t/Result-Simple.t index 6f1e38a..894f75c 100644 --- a/t/Result-Simple.t +++ b/t/Result-Simple.t @@ -138,6 +138,10 @@ subtest 'Test `result_for` function' => sub { my $proto = Sub::Util::prototype($code); is $proto, ';$'; }; + + subtest 'When function is not found, then throw a exception' => sub { + like dies { result_for xxx => Int, NonEmptyStr } => qr/result_for: function `xxx` not found/; + }; }; };