[![Actions Status](https://github.com/kfly8/Result-Simple/actions/workflows/test.yml/badge.svg)](https://github.com/kfly8/Result-Simple/actions) [![Coverage Status](https://img.shields.io/coveralls/kfly8/Result-Simple/main.svg?style=flat)](https://coveralls.io/r/kfly8/Result-Simple?branch=main) [![MetaCPAN Release](https://badge.fury.io/pl/Result-Simple.svg)](https://metacpan.org/release/Result-Simple) # NAME Result::Simple - A dead simple perl-ish Result like F#, Rust, Go, etc. # SYNOPSIS ```perl use Result::Simple qw( ok err result_for chain pipeline); use Types::Standard -types; use kura Error => Dict[message => Str]; use kura Request => Dict[name => Str, age => Int]; result_for validate_name => Request, Error; sub validate_name { my $req = shift; my $name = $req->{name}; return err({ message => 'No name'}) unless defined $name; return err({ message => 'Empty name'}) unless length $name; return err({ message => 'Reserved name'}) if $name eq 'root'; return ok($req); } result_for validate_age => Request, Error; sub validate_age { my $req = shift; my $age = $req->{age}; return err({ message => 'No age'}) unless defined $age; return err({ message => 'Invalid age'}) unless $age =~ /\A\d+\z/; return err({ message => 'Too young age'}) if $age < 18; return ok($req); } result_for validate_req => Request, Error; sub validate_req { my $req = shift; my $err; ($req, $err) = validate_name($req); return err($err) if $err; ($req, $err) = validate_age($req); return err($err) if $err; return ok($req); } # my $req = validate_req({ name => 'taro', age => 42 }); # => Throw an exception, because `validate_req` requires calling in a list context to handle an error. my ($req1, $err1) = validate_req({ name => 'taro', age => 42 }); $req1 # => { name => 'taro', age => 42 }; $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 Result::Simple is a dead simple Perl-ish Result. Result represents a function's return value as success or failure, enabling safer error handling and more effective control flow management. This pattern is used in other languages such as F#, Rust, and Go. In Perl, this pattern is also useful, and this module provides a simple way to use it. This module does not wrap a return value in an object. Just return a tuple like `($data, undef)` or `(undef, $err)`. ## FUNCTIONS ### ok($value) Return a tuple of a given value and undef. When the function succeeds, it should return this. ```perl sub add($a, $b) { ok($a + $b); # => ($a + $b, undef) } ``` ### err($error) Return a tuple of undef and a given error. When the function fails, it should return this. ```perl sub div($a, $b) { return err('Division by zero') if $b == 0; # => (undef, 'Division by zero') ok($a / $b); } ``` Note that the error value must be a truthy value, otherwise it will throw an exception. ### result\_for $function\_name => $T, $E You can use the `result_for` to define a function that returns a success or failure and asserts the return value types. Here is an example: ```perl result_for half => Int, ErrorMessage; sub half ($n) { if ($n % 2) { return err('Odd number'); } else { return ok($n / 2); } } ``` - T (success type) When the function succeeds, then returns `($data, undef)`, and `$data` should satisfy this type. - E (error type) When the function fails, then returns `(undef, $err)`, and `$err` should satisfy this type. Additionally, type E must be truthy value to distinguish between success and failure. ```perl result_for foo => Int, Str; sub foo ($input) { } # => throw exception: Result E should not allow falsy values: ["0"] because Str allows "0" ``` When a function never returns an error, you can set type E to `undef`: ```perl result_for bar => Int, undef; 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 @result = ok($req); @result = chain(validate_name => @result); @result = chain(validate_age => @result); return @result; ``` 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. It should be used in tests or debugging code. ```perl sub div($a, $b) { return err('Division by zero') if $b == 0; return ok($a / $b); } unsafe_unwrap(div(4, 2)); # => 2 unsafe_unwrap(div(4, 0)); # => throw an exception: Error called in `unsafe_unwrap`: "Division by zero" ``` ### unsafe\_unwrap\_err($data, $err) `unsafe_unwrap_err` takes a Result and returns an E when the result is an Err, otherwise it throws exception. It should be used in tests or debugging code. ```perl sub div($a, $b) { return err('Division by zero') if $b == 0; return ok($a / $b); } unsafe_unwrap_err(div(4, 2)); # => throw an exception: No error called in `unsafe_unwrap_err`: 2 unsafe_unwrap_err(div(4, 0)); # => "Division by zero" ``` ## ENVIRONMENTS ### `$ENV{RESULT_SIMPLE_CHECK_ENABLED}` If the `ENV{RESULT_SIMPLE_CHECK_ENABLED}` environment is truthy before loading this module, it works as an assertion. Otherwise, if it is falsy, `result_for` attribute does nothing. The default is true. This option is useful for development and testing mode, and it recommended to set it to false for production. ```perl result_for foo => Int, undef; sub foo { ok("hello") } my ($data, $err) = foo(); # => throw exception when check enabled ``` # NOTE ## Type constraint requires `check` method Perl has many type constraint modules, but this module requires the type constraint module that provides `check` method. So you can use [Type::Tiny](https://metacpan.org/pod/Type%3A%3ATiny), [Moose](https://metacpan.org/pod/Moose), [Mouse](https://metacpan.org/pod/Mouse) or [Data::Checks](https://metacpan.org/pod/Data%3A%3AChecks) etc. ## Use different function name Sometimes, you may want to use a different name for `ok`, `err`, or some other functions of `Result::Simple`. For example, `Test2::V0` has `ok` functions, so it conflicts with `ok` function of `Result::Simple`. This module provides a way to set a different function name using the `-as` option. ```perl use Result::Simple ok => { -as => 'left' }, # `left` is equivalent to `ok` err => { -as => 'right' }; # `right` is equivalent to `err` ``` ## Avoiding Ambiguity in Result Handling Forgetting to call `ok` or `err` function is a common mistake. Consider the following example: ```perl result_for validate_name => Str, ErrorMessage; sub validate_name ($name) { return "Empty name" unless $name; # Oops! Forgot to call `err` function. return ok($name); } my ($name, $err) = validate_name(''); # => throw exception: Invalid result tuple (T, E) ``` In this case, the function throws an exception because the return value is not a valid result tuple `($data, undef)` or `(undef, $err)`. This is fortunate, as the mistake is detected immediately. The following case is not detected: ```perl result_for foo => Str, ErrorMessage; sub foo { return (undef, 'apple'); # No use of `ok` or `err` function. } my ($data, $err) = foo; # => $err is 'apple' ``` Here, the function returns a valid failure tuple `(undef, $err)`. However, it is unclear whether this was intentional or a mistake. The lack of `ok` or `err` makes the intent ambiguous. Conclusively, be sure to use `ok` or `err` functions to make it clear whether the success or failure is intentional. # LICENSE Copyright (C) kobaken. This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. # AUTHOR kobaken