diff --git a/README.md b/README.md index fa1e0cb..b8bbea2 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,51 @@ if ($errors) { } ``` +### flatten(@results) + +`flatten` takes a list of array references that contain Result tuples and flattens them into a single list of Result tuples. + +For example, it converts `([T1,E1], [T2,E2], [T3,E3])` to `(T1,E1, T2,E2, T3,E3)`. + +This is useful when you have multiple arrays of Results that you need to combine or process together. + +Example: + +```perl +my @result1 = ok(1); +my @result2 = ok(2); +my @result3 = ok(3); + +my @all_results = flatten([\@result1], [\@result2], [\@result3]); +# @all_results is now (1,undef, 2,undef, 3,undef) + +# You can use it with combine: +my ($values, $error) = combine(flatten([\@result1], [\@result2], [\@result3])); +# $values is [1, 2, 3], $error is undef +``` + +### match($on\_success, $on\_failure) + +`match` provides a way to handle both success and failure cases of a Result in a functional style, similar to pattern matching in other languages. + +It takes two callbacks: +\- `$on_success`: a function that receives the success value +\- `$on_failure`: a function that receives the error value + +`match` returns a new function that will call the appropriate callback depending on whether the Result passed to it represents success or failure. + +Example: + +```perl +my $handler = match( + sub { my $value = shift; "Success: The value is $value" }, + sub { my $error = shift; "Error: $error occurred" } +); + +$handler->(ok(42)); # => Success: The value is 42 +$handler->(err("Invalid input")); # => Error: Invalid input occurred +``` + ### unsafe\_unwrap($data, $err) `unsafe_unwrap` takes a Result and returns a T when the result is an Ok, otherwise it throws exception. @@ -325,6 +370,93 @@ my ($v, $e) = ok(2); # => Critic: $e is declared but not used (Variables::Prohib print $v; ``` +## Using Result::Simple with Promises for asynchronous operations + +Result::Simple can be combined with Promise-based asynchronous operations to create clean, functional error handling in asynchronous code. Here's an example using Mojo::Promise: + +```perl +use Mojo::Promise; +use Mojo::UserAgent; +use Result::Simple qw(ok err combine flatten match); + +my $ua = Mojo::UserAgent->new; + +# Convert HTTP responses to Result tuples +sub fetch_result { + my $uri = shift; + $ua->get_p($uri)->then( + sub { + my $tx = shift; + my $res = $tx->result; + if ($res->is_success) { + return ok($res->json); # Success case with parsed JSON + } else { + return err({ # Error case with details + uri => $uri, + code => $res->code, + }); + } + } + )->catch( + sub { + my $err = shift; + return err($err); # Connection/network errors + } + ); +} + +# Fetch a specific todo item +sub fetch_todo { + my $id = shift; + my $uri = "https://jsonplaceholder.typicode.com/todos/${id}"; + fetch_result($uri); +} + +# Fetch multiple todos in parallel +Mojo::Promise->all( + fetch_todo(1), + fetch_todo(2), +)->then( + sub { + # Combine the results of multiple promises + my ($todos, $err) = combine(flatten(@_)); + + # Create a matcher to handle the combined result + state $handler = match( + sub { + my $todos = shift; + say "Successfully fetched all todos:"; + for my $todo (@$todos) { + say "- Todo #$todo->{id}: $todo->{title}"; + say " Completed: " . ($todo->{completed} ? "Yes" : "No"); + } + }, + sub { + my $error = shift; + say "Error fetching todos:"; + if (ref $error eq 'HASH' && exists $error->{code}) { + say "HTTP $error->{code} error for $error->{uri}"; + } else { + say "Connection error: $error"; + } + } + ); + + # Process the result + $handler->($todos, $err); + } +)->wait; +``` + +This pattern provides several benefits: + +- Clear separation between success and error cases +- Consistent error handling across both synchronous and asynchronous code +- Ability to combine multiple asynchronous operations and handle their results uniformly +- More expressive and maintainable code through functional composition + +The combination of `flatten`, `combine`, and `match` makes it easy to work with multiple promises while maintaining clean error handling. + ## Avoiding Ambiguity in Result Handling Forgetting to call `ok` or `err` function is a common mistake. Consider the following example: diff --git a/lib/Result/Simple.pm b/lib/Result/Simple.pm index d7c1dad..28f0403 100644 --- a/lib/Result/Simple.pm +++ b/lib/Result/Simple.pm @@ -12,6 +12,8 @@ use Exporter::Shiny qw( pipeline combine combine_with_all_errors + flatten + match unsafe_unwrap unsafe_unwrap_err ); @@ -189,7 +191,7 @@ sub pipeline { return ok($value); }; - my ($package, $file, $line) = caller(0); + my $package = caller(0); my $fullname = "$package\::__PIPELINED_FUNCTION__"; Sub::Util::set_subname($fullname, $pipelined); @@ -239,6 +241,40 @@ sub combine_with_all_errors { return ok(\@values); } +# `flatten` takes a list of Result like `([T1,E1], [T2,E2], [T3,E3])` and returns a new Result like ((T1,E1), (T2,E2), (T3,E3)). +sub flatten { + map { ref $_ && ref $_ eq 'ARRAY' ? @$_ : $_ } @_; +} + +# `match` takes two coderefs for on success and on failure, and returns a new function. +sub match { + my ($on_success, $on_failure) = @_; + + if (CHECK_ENABLED) { + croak "`match` arguments must be two coderefs for on success and on error" unless _is_callable($on_success) && _is_callable($on_failure); + } + + my $match = sub { + my ($value, $err) = @_; + + if (CHECK_ENABLED) { + croak "`match` function arguments must be result like (T, E)" unless @_ == 2; + } + + if ($err) { + return $on_failure->($err); + } else { + return $on_success->($value); + } + }; + + my $package = caller(0); + my $fullname = "$package\::__MATCHER_FUNCTION__"; + Sub::Util::set_subname($fullname, $match); + + return $match; +} + # `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 { @@ -273,6 +309,12 @@ sub _ddf { Data::Dumper::Dumper($v); } +# Check if the argument is a callable. +sub _is_callable { + my $code = shift; + (Scalar::Util::reftype($code)||'') eq 'CODE' +} + 1; __END__ @@ -514,6 +556,47 @@ Example: # Process valid form data } +=head3 flatten(@results) + +C takes a list of array references that contain Result tuples and flattens them into a single list of Result tuples. + +For example, it converts C<([T1,E1], [T2,E2], [T3,E3])> to C<(T1,E1, T2,E2, T3,E3)>. + +This is useful when you have multiple arrays of Results that you need to combine or process together. + +Example: + + my @result1 = ok(1); + my @result2 = ok(2); + my @result3 = ok(3); + + my @all_results = flatten([\@result1], [\@result2], [\@result3]); + # @all_results is now (1,undef, 2,undef, 3,undef) + + # You can use it with combine: + my ($values, $error) = combine(flatten([\@result1], [\@result2], [\@result3])); + # $values is [1, 2, 3], $error is undef + +=head3 match($on_success, $on_failure) + +C provides a way to handle both success and failure cases of a Result in a functional style, similar to pattern matching in other languages. + +It takes two callbacks: +- C<$on_success>: a function that receives the success value +- C<$on_failure>: a function that receives the error value + +C returns a new function that will call the appropriate callback depending on whether the Result passed to it represents success or failure. + +Example: + + my $handler = match( + sub { my $value = shift; "Success: The value is $value" }, + sub { my $error = shift; "Error: $error occurred" } + ); + + $handler->(ok(42)); # => Success: The value is 42 + $handler->(err("Invalid input")); # => Error: Invalid input occurred + =head3 unsafe_unwrap($data, $err) C takes a Result and returns a T when the result is an Ok, otherwise it throws exception. @@ -578,6 +661,98 @@ L is useful to chec my ($v, $e) = ok(2); # => Critic: $e is declared but not used (Variables::ProhibitUnusedVarsStricter, Severity: 3) print $v; +=head2 Using Result::Simple with Promises for asynchronous operations + +Result::Simple can be combined with Promise-based asynchronous operations to create clean, functional error handling in asynchronous code. Here's an example using Mojo::Promise: + + use Mojo::Promise; + use Mojo::UserAgent; + use Result::Simple qw(ok err combine flatten match); + + my $ua = Mojo::UserAgent->new; + + # Convert HTTP responses to Result tuples + sub fetch_result { + my $uri = shift; + $ua->get_p($uri)->then( + sub { + my $tx = shift; + my $res = $tx->result; + if ($res->is_success) { + return ok($res->json); # Success case with parsed JSON + } else { + return err({ # Error case with details + uri => $uri, + code => $res->code, + }); + } + } + )->catch( + sub { + my $err = shift; + return err($err); # Connection/network errors + } + ); + } + + # Fetch a specific todo item + sub fetch_todo { + my $id = shift; + my $uri = "https://jsonplaceholder.typicode.com/todos/${id}"; + fetch_result($uri); + } + + # Fetch multiple todos in parallel + Mojo::Promise->all( + fetch_todo(1), + fetch_todo(2), + )->then( + sub { + # Combine the results of multiple promises + my ($todos, $err) = combine(flatten(@_)); + + # Create a matcher to handle the combined result + state $handler = match( + sub { + my $todos = shift; + say "Successfully fetched all todos:"; + for my $todo (@$todos) { + say "- Todo #$todo->{id}: $todo->{title}"; + say " Completed: " . ($todo->{completed} ? "Yes" : "No"); + } + }, + sub { + my $error = shift; + say "Error fetching todos:"; + if (ref $error eq 'HASH' && exists $error->{code}) { + say "HTTP $error->{code} error for $error->{uri}"; + } else { + say "Connection error: $error"; + } + } + ); + + # Process the result + $handler->($todos, $err); + } + )->wait; + +This pattern provides several benefits: + +=over 4 + +=item Clear separation between success and error cases + +=item Consistent error handling across both synchronous and asynchronous code + +=item Ability to combine multiple asynchronous operations and handle their results uniformly + +=item More expressive and maintainable code through functional composition + +=back + +The combination of C, C, and C makes it easy to work with multiple promises while maintaining clean error handling. + =head2 Avoiding Ambiguity in Result Handling Forgetting to call C or C function is a common mistake. Consider the following example: diff --git a/t/Result-Simple.t b/t/Result-Simple.t index 118ed4e..ea4b471 100644 --- a/t/Result-Simple.t +++ b/t/Result-Simple.t @@ -14,7 +14,19 @@ BEGIN { # $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1; } -use Result::Simple qw( ok err result_for unsafe_unwrap unsafe_unwrap_err chain pipeline combine combine_with_all_errors ); +use Result::Simple qw( + ok + err + result_for + chain + pipeline + combine + combine_with_all_errors + flatten + match + unsafe_unwrap + unsafe_unwrap_err +); subtest 'Test `ok` and `err` functions' => sub { subtest '`ok` and `err` functions just return values' => sub { @@ -293,4 +305,46 @@ subtest 'Test `combine_with_all_errors` with pipeline' => sub { }; }; +subtest 'Test `flatten` function' => sub { + is [flatten(( [ok(1)], [ok(2)], [ok(3)] ))], [ok(1), ok(2), ok(3)]; + is [flatten(( [ok(1)], [err('foo')], [ok(3)] ))], [ok(1), err('foo'), ok(3)]; +}; + +subtest 'Test `match` function' => sub { + my $code = match( + sub { my $value = shift; return "Success: $value" }, + sub { my $error = shift; return "Error: $error" }, + ); + + is $code->(ok('foo')), 'Success: foo'; + is $code->(err('bar')), 'Error: bar'; + + like dies { my $v = $code->(1) }, qr/`match` function arguments must be result like \(T, E\)/; + + subtest 'stacktrace' => sub { + sub test_match_stacktrace { Carp::confess('on_success') } + + my $code = match( + \&test_match_stacktrace, + sub { }, + ); + + local $@; + eval { $code->(ok('foo')) }; + my $error = $@; + my @errors = split /\n/, $error; + + my $file = __FILE__; + my $line = __LINE__; + + like $errors[0], qr!on_success at $file line @{[$line - 13]}!, 'Throw an exception at `test_match_stacktrace` function'; + like $errors[1], qr!test_match_stacktrace\(["']foo["']\) called at .+/Result/Simple.pm!; + like $errors[2], qr!__MATCHER_FUNCTION__\(["']foo["'], undef\) called at $file line @{[$line - 5]}!; + + note $errors[0]; + note $errors[1]; + note $errors[2]; + }; +}; + done_testing;