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
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,67 @@ my ($req, $err) = $code->($input);
This allows you to describe multiple processes concisely as a single flow.
Each function in the pipeline needs to return `(T, E)`.

### combine(@results)

`combine` takes a list of Result like `((T1,E1), (T2,E2), (T3,E3))` and returns a new Result like `([T1,T2,T3], E)`.

If all Result values are successful, it returns a new Result with all success values collected into an array reference. If any Result has an error, the function short-circuits and returns the first error encountered.

This is useful when you need to collect the results of multiple operations that all need to succeed, similar to how `Promise.all` works in JavaScript. For example, when fetching data from multiple sources or validating multiple aspects of input data.

Example:

```perl
sub fetch_user { ... } # Returns Result<User, Error>
sub fetch_orders { ... } # Returns Result<Order[], Error>
sub fetch_settings { ... } # Returns Result<Settings, Error>

my ($data, $err) = combine(
fetch_user($user_id),
fetch_orders($user_id),
fetch_settings($user_id)
);

if ($err) {
# Handle error
} else {
my ($user, $orders, $settings) = @$data;
# Process all successful results
}
```

### combine\_with\_all\_errors(@results)

`combine_with_all_errors` takes a list of Result like `((T1,E1), (T2,E2), (T3,E3))` and returns a new Result.

Unlike `combine` which stops at the first error, this function collects all errors from the input Results. If all Results are successful, it returns `([T1,T2,T3], undef)`. If any Results have errors, it returns `(undef, [E1,E2,E3])` with an array reference containing all encountered errors.

This is particularly useful for validation scenarios where you want to report all validation errors at once rather than one at a time. For example, when validating a form, you might want to show the user all fields that have errors rather than making them fix one error at a time.

Example:

```perl
sub validate_name { ... } # Returns Result<Name, Error>
sub validate_email { ... } # Returns Result<Email, Error>
sub validate_age { ... } # Returns Result<Age, Error>

my ($data, $errors) = combine_with_all_errors(
validate_name($form->{name}),
validate_email($form->{email}),
validate_age($form->{age})
);

if ($errors) {
# Show all validation errors to the user
for my $error (@$errors) {
print "Error: $error->{message}\n";
}
} else {
my ($name, $email, $age) = @$data;
# Process valid form data
}
```

### 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
102 changes: 102 additions & 0 deletions lib/Result/Simple.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use Exporter::Shiny qw(
result_for
chain
pipeline
combine
combine_with_all_errors
unsafe_unwrap
unsafe_unwrap_err
);
Expand Down Expand Up @@ -194,6 +196,49 @@ sub pipeline {
return $pipelined;
}

# `combine` takes a list of Result like `((T1,E1), (T2,E2), (T3,E3))` and returns a new Result like `([T1,T2,T3], E)`.
sub combine {
my @results = @_;

if (CHECK_ENABLED) {
croak "`combine` must be called in list context" unless wantarray;
croak "`combine` arguments must be Result list" unless @_ % 2 == 0;
}

my @values;
for (my $i = 0; $i < @results; $i += 2) {
my ($value, $error) = @results[$i, $i + 1];
if ($error) {
return err($error);
}
push @values, $value;
}
return ok(\@values);
}

# `combine_with_all_errors` takes a list of Result like `((T1,E1), (T2,E2), (T3,E3))` and returns a new Result like `([T1,T2,T3], [E1,E2,E3])`.
sub combine_with_all_errors {
my @results = @_;

if (CHECK_ENABLED) {
croak "`combine_with_all_errors` must be called in list context" unless wantarray;
croak "`combine_with_all_errors` arguments must be Result list" unless @_ % 2 == 0;
}

my @values;
my @errors;
for (my $i = 0; $i < @results; $i += 2) {
my ($value, $err) = @results[$i, $i + 1];
if ($err) {
push @errors, $err;
} else {
push @values, $value;
}
}
return err(\@errors) if @errors;
return ok(\@values);
}

# `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 @@ -412,6 +457,63 @@ Example:
This allows you to describe multiple processes concisely as a single flow.
Each function in the pipeline needs to return C<(T, E)>.

=head3 combine(@results)

C<combine> takes a list of Result like C<((T1,E1), (T2,E2), (T3,E3))> and returns a new Result like C<([T1,T2,T3], E)>.

If all Result values are successful, it returns a new Result with all success values collected into an array reference. If any Result has an error, the function short-circuits and returns the first error encountered.

This is useful when you need to collect the results of multiple operations that all need to succeed, similar to how C<Promise.all> works in JavaScript. For example, when fetching data from multiple sources or validating multiple aspects of input data.

Example:

sub fetch_user { ... } # Returns Result<User, Error>
sub fetch_orders { ... } # Returns Result<Order[], Error>
sub fetch_settings { ... } # Returns Result<Settings, Error>

my ($data, $err) = combine(
fetch_user($user_id),
fetch_orders($user_id),
fetch_settings($user_id)
);

if ($err) {
# Handle error
} else {
my ($user, $orders, $settings) = @$data;
# Process all successful results
}

=head3 combine_with_all_errors(@results)

C<combine_with_all_errors> takes a list of Result like C<((T1,E1), (T2,E2), (T3,E3))> and returns a new Result.

Unlike C<combine> which stops at the first error, this function collects all errors from the input Results. If all Results are successful, it returns C<([T1,T2,T3], undef)>. If any Results have errors, it returns C<(undef, [E1,E2,E3])> with an array reference containing all encountered errors.

This is particularly useful for validation scenarios where you want to report all validation errors at once rather than one at a time. For example, when validating a form, you might want to show the user all fields that have errors rather than making them fix one error at a time.

Example:

sub validate_name { ... } # Returns Result<Name, Error>
sub validate_email { ... } # Returns Result<Email, Error>
sub validate_age { ... } # Returns Result<Age, Error>

my ($data, $errors) = combine_with_all_errors(
validate_name($form->{name}),
validate_email($form->{email}),
validate_age($form->{age})
);

if ($errors) {
# Show all validation errors to the user
for my $error (@$errors) {
print "Error: $error->{message}\n";
}
} else {
my ($name, $email, $age) = @$data;
# Process valid form data
}

=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
33 changes: 32 additions & 1 deletion t/Result-Simple.t
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ BEGIN {
# $ENV{RESULT_SIMPLE_CHECK_ENABLED} = 1;
}

use Result::Simple qw( ok err result_for unsafe_unwrap unsafe_unwrap_err chain pipeline );
use Result::Simple qw( ok err result_for unsafe_unwrap unsafe_unwrap_err chain pipeline combine combine_with_all_errors );

subtest 'Test `ok` and `err` functions' => sub {
subtest '`ok` and `err` functions just return values' => sub {
Expand Down Expand Up @@ -262,4 +262,35 @@ subtest 'Test `pipeline` function' => sub {
}
};

subtest 'Test `combine`` function' => sub {
subtest 'All ok()' => sub {
my ($v, $e) = combine( ok(3), ok(4), ok(5) );
is $v, [3, 4, 5];
is $e, undef;
};

subtest 'When err are included, then return the first error' => sub {
my ($v, $e) = combine( ok(3), err('foo'), ok(5), err('bar') );
is $v, undef;
is $e, 'foo';
};

like dies { my $v = combine( ok(3), 4, ok(5) ) }, qr/`combine` must be called in list context/;
like dies { my ($v, $e) = combine( ok(3), 4 ) }, qr/`combine` arguments must be Result list/;
};

subtest 'Test `combine_with_all_errors` with pipeline' => sub {
subtest 'All ok()' => sub {
my ($v, $e) = combine_with_all_errors( ok(3), ok(4), ok(5) );
is $v, [3, 4, 5];
is $e, undef;
};

subtest 'When err are included, then return all errors' => sub {
my ($v, $e) = combine_with_all_errors( ok(3), err('foo'), ok(5), err('bar') );
is $v, undef;
is $e, ['foo', 'bar'];
};
};

done_testing;