diff --git a/README.md b/README.md index 2a47839..c19712f 100644 --- a/README.md +++ b/README.md @@ -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]; @@ -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; @@ -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 @@ -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 and returns a T when the result is an Ok, otherwise it throws exception. diff --git a/lib/Result/Simple.pm b/lib/Result/Simple.pm index 8e6efaa..cea8b41 100644 --- a/lib/Result/Simple.pm +++ b/lib/Result/Simple.pm @@ -8,6 +8,8 @@ use Exporter::Shiny qw( ok err result_for + chain + pipeline unsafe_unwrap unsafe_unwrap_err ); @@ -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 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 { @@ -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]; @@ -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; @@ -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 @@ -306,6 +361,39 @@ When a function never returns an error, you can set type E to C: =back +=head3 chain($function, $data, $err) + +C 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 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. + +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 takes a Result and returns a T when the result is an Ok, otherwise it throws exception. diff --git a/t/Result-Simple.t b/t/Result-Simple.t index 894f75c..7527581 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 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 { @@ -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; diff --git a/t/synopsis.t b/t/synopsis.t index 980a994..752110c 100644 --- a/t/synopsis.t +++ b/t/synopsis.t @@ -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]; @@ -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; @@ -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