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
58 changes: 56 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc.
# SYNOPSIS

```perl
use Result::Simple qw( ok err result_for );
use Result::Simple qw( ok err result_for chain pipeline);
use Types::Standard -types;

use kura Error => Dict[message => Str];
Expand Down Expand Up @@ -40,7 +40,6 @@ sub validate_req {
my $req = shift;
my $err;

# $req = validate_name($req); # => Throw error! It requires list context to handle error
($req, $err) = validate_name($req);
return err($err) if $err;

Expand All @@ -57,6 +56,24 @@ $err1 # => undef;
my ($req2, $err2) = validate_req({ name => 'root', age => 20 });
$req2 # => undef;
$err2 # => { message => 'Reserved name' };

# Following are the same as above but using `chain` and `pipeline` helper functions.

sub validate_req_with_chain {
my $req = shift;

my @r = ok($req);
@r = chain(validate_name => @r);
@r = chain(validate_age => @r);
return @r;
}

sub validate_req_with_pipeline {
my $req = shift;

state $code = pipeline qw( validate_name validate_age );
$code->(ok($req));
}
```

# DESCRIPTION
Expand Down Expand Up @@ -133,6 +150,43 @@ sub half ($n) {
sub double ($n) { ok($n * 2) }
```

### chain($function, $data, $err)

`chain` is a helper function for passing result type `(T, E)` to the next function.

If an error has already occurred (when `$err` is defined), the new function won't be called and the same error will be returned as is.
If there's no error, the given function will be applied to `$data`, and its result `(T, E)` will be returned.

This is mainly suitable for use cases where functions need to be applied serially, such as in validation processing.

Example:

```perl
my @r = ok($req);
@r = chain(validate_name => @r);
@r = chain(validate_age => @r);
return @r;
```

In this way, if a failure occurs along the way, the process stops at that point and the failure result is returned.

### pipeline(@functions)

`pipeline` is a helper function that generates a pipeline function that applies multiple functions in series.

It returns a new function that applies the given list of functions in order. This generated function takes an argument in the form of `(T, E)`,
and if an error occurs during the process, it immediately halts processing as a failure. If processing succeeds all the way through, it returns `ok($value)`.

Example:

```perl
state $code = pipeline qw( validate_name validate_age );
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)`.

### 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
92 changes: 90 additions & 2 deletions lib/Result/Simple.pm
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use Exporter::Shiny qw(
ok
err
result_for
chain
pipeline
unsafe_unwrap
unsafe_unwrap_err
);
Expand Down Expand Up @@ -140,6 +142,43 @@ sub wrap_code {
*{$fullname} = $wrapped;
}

# `chain` takes a function name and a result tuple (T, E) and returns a new result tuple (T, E).
sub chain {
my ($f, $value, $error) = @_;

if (CHECK_ENABLED) {
croak "`chain` must be called in list context" unless wantarray;
croak "`chain` arguments must be func and result like (func, T, E)" unless @_ == 3;
}

my $code = ref $f ? $f : do { my $t = caller(0); $t->can($f) or croak "Function `$f` not found in $t" };
return err($error) if $error;
return $code->($value);
}

# `pipeline` takes a list of function names and returns a new function.
sub pipeline {
my (@f) = @_;

my @codes = map { ref $_ ? $_ : do { my $t = caller(0); $t->can($_) or croak "Function `$_` not found in $t" } } @f;

sub {
my ($value, $error) = @_;

if (CHECK_ENABLED) {
croak "pipelined function must be called in list context" unless wantarray;
croak "pipelined function arguments must be result such as (T, E) " unless @_ == 2;
}

return err($error) if $error;
for my $code (@codes) {
($value, $error) = $code->($value);
return err($error) if $error;
}
return ok($value);
}
}

# `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 @@ -185,7 +224,7 @@ Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc.

=head1 SYNOPSIS

use Result::Simple qw( ok err result_for );
use Result::Simple qw( ok err result_for chain pipeline);
use Types::Standard -types;

use kura Error => Dict[message => Str];
Expand Down Expand Up @@ -219,7 +258,6 @@ Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc.
my $req = shift;
my $err;

# $req = validate_name($req); # => Throw error! It requires list context to handle error
($req, $err) = validate_name($req);
return err($err) if $err;

Expand All @@ -237,6 +275,23 @@ Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc.
$req2 # => undef;
$err2 # => { message => 'Reserved name' };

# Following are the same as above but using `chain` and `pipeline` helper functions.

sub validate_req_with_chain {
my $req = shift;

my @r = ok($req);
@r = chain(validate_name => @r);
@r = chain(validate_age => @r);
return @r;
}

sub validate_req_with_pipeline {
my $req = shift;

state $code = pipeline qw( validate_name validate_age );
$code->(ok($req));
}

=head1 DESCRIPTION

Expand Down Expand Up @@ -306,6 +361,39 @@ When a function never returns an error, you can set type E to C<undef>:

=back

=head3 chain($function, $data, $err)

C<chain> is a helper function for passing result type C<(T, E)> to the next function.

If an error has already occurred (when C<$err> is defined), the new function won't be called and the same error will be returned as is.
If there's no error, the given function will be applied to C<$data>, and its result C<(T, E)> will be returned.

This is mainly suitable for use cases where functions need to be applied serially, such as in validation processing.

Example:

my @r = ok($req);
@r = chain(validate_name => @r);
@r = chain(validate_age => @r);
return @r;

In this way, if a failure occurs along the way, the process stops at that point and the failure result is returned.

=head3 pipeline(@functions)

C<pipeline> is a helper function that generates a pipeline function that applies multiple functions in series.

It returns a new function that applies the given list of functions in order. This generated function takes an argument in the form of C<(T, E)>,
and if an error occurs during the process, it immediately halts processing as a failure. If processing succeeds all the way through, it returns C<ok($value)>.

Example:

state $code = pipeline qw( validate_name validate_age );
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 C<(T, E)>.

=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
50 changes: 49 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 );
use Result::Simple qw( ok err result_for unsafe_unwrap unsafe_unwrap_err chain pipeline );

subtest 'Test `ok` and `err` functions' => sub {
subtest '`ok` and `err` functions just return values' => sub {
Expand Down Expand Up @@ -166,5 +166,53 @@ subtest 'Test `unsafe_unwrap_err` function' => sub {
};
};

subtest 'Test `chain` function' => sub {
sub chain_test {
my $v = shift;
return err('No more') if $v == 1;
return ok($v / 2);
}

my ($v1, $e1) = chain(chain_test => ok(8));
is $v1, 4;
is $e1, undef;

my ($v2, $e2) = chain(chain_test => ok(1));
is $v2, undef;
is $e2, 'No more';

my ($v3, $e3) = chain(chain_test => err('foo'));
is $v3, undef;
is $e3, 'foo';

like dies { my $v = chain(chain_test => 1, 2) }, qr/`chain` must be called in list context/;
like dies { my ($v, $e) = chain(chain_test => 1) }, qr/`chain` arguments must be func and result/;
like dies { my ($v, $e) = chain(unknown => 1, 2) }, qr/Function `unknown` not found in main/;
};

subtest 'Test `pipeline` function' => sub {
sub pipeline_test {
my $v = shift;
return err('No more') if $v == 1;
return ok($v / 2);
}

my $code = pipeline qw( pipeline_test pipeline_test );
my ($v1, $e1) = $code->(ok(8));
is $v1, 2;
is $e1, undef;

my ($v2, $e2) = $code->(ok(2));
is $v2, undef;
is $e2, 'No more';

my ($v3, $e3) = $code->(ok(1));
is $v3, undef;
is $e3, 'No more';

like dies { my $v = $code->(1, 2) }, qr/pipelined function must be called in list context/;
like dies { my ($v, $e) = $code->(1) }, qr/pipelined function arguments must be result/;
like dies { my $c = pipeline qw( unknown ) }, qr/Function `unknown` not found in main/;
};

done_testing;
22 changes: 20 additions & 2 deletions t/synopsis.t
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use Test2::V0 qw(is done_testing);
use Test2::Require::Module 'Type::Tiny' => '2.000000';
use Test2::Require::Module 'kura';
use feature qw( state );

use Result::Simple qw( ok err result_for );
use Result::Simple qw( ok err result_for chain pipeline);
use Types::Standard -types;

use kura Error => Dict[message => Str];
Expand Down Expand Up @@ -36,7 +37,6 @@ sub validate_req {
my $req = shift;
my $err;

# $req = validate_name($req); # => Throw error! It requires list context to handle error
($req, $err) = validate_name($req);
return err($err) if $err;

Expand All @@ -54,4 +54,22 @@ my ($req2, $err2) = validate_req({ name => 'root', age => 20 });
is $req2, undef;
is $err2, { message => 'Reserved name' };

# Following are the same as above but using `chain` and `pipeline` functions.

sub validate_req_with_chain {
my $req = shift;

my @r = ok($req);
@r = chain(validate_name => @r);
@r = chain(validate_age => @r);
return @r;
}

sub validate_req_with_pipeline {
my $req = shift;

state $code = pipeline qw( validate_name validate_age );
$code->(ok($req));
}

done_testing