Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, E> and returns a T when the result is an Ok, otherwise it throws exception.
Expand Down Expand Up @@ -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:
Expand Down
177 changes: 176 additions & 1 deletion lib/Result/Simple.pm
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use Exporter::Shiny qw(
pipeline
combine
combine_with_all_errors
flatten
match
unsafe_unwrap
unsafe_unwrap_err
);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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<T, E> 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 {
Expand Down Expand Up @@ -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__

Expand Down Expand Up @@ -514,6 +556,47 @@ Example:
# Process valid form data
}

=head3 flatten(@results)

C<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 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<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:
- C<$on_success>: a function that receives the success value
- C<$on_failure>: a function that receives the error value

C<match> 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<unsafe_unwrap> takes a Result<T, E> and returns a T when the result is an Ok, otherwise it throws exception.
Expand Down Expand Up @@ -578,6 +661,98 @@ L<Perl::Critic::Policy::Variables::ProhibitUnusedVarsStricter> 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<flatten>, C<combine>, and C<match> makes it easy to work with multiple promises while maintaining clean error handling.

=head2 Avoiding Ambiguity in Result Handling

Forgetting to call C<ok> or C<err> function is a common mistake. Consider the following example:
Expand Down
Loading