/p6-result

Encapsulate the result of a computation

Primary LanguagePerl 6Artistic License 2.0Artistic-2.0

Build Status

Result

Result - Encapsulate the result of a computation.

SYNOPSIS

Result is a simple module which provides some tools for returning values from a function and signaling to the caller if the function succeeded or failed. Results are an explicitly returned encapsulation and therefore have to be used to access the return of a function, in contrast to a perl6 Failure which is invisible unless there is a problem.

The synopsis below demonstrates handling a Result and just unwrapping it and accepting the exception if it's an Result::Err.

# examples/synopsis2.p6

use Result;

# An example function for attempting a conversion of a value to an Int.
# This function can return two outcomes.
# An Ok outcome with either an Int or something which accepts .Int or otherwise an Err.
sub to-int(Any $val --> Result::Any) {
    return Ok $val if $val ~~ Int;
    try return Ok $val.Int;
    Err "Unable to convert value of type { $val.WHAT.perl } to Int."
}

# To get the result of the conversion you have to access it via the returned Result object.
my @test-values = "Not an Int", 7, "42", Any, pi;
for @test-values -> $val {
    given to-int($val) {
        when .is-ok { say "{ $val.WHAT.perl } $val converted to Int { .value }" }
        when .is-err { say "Well that wasn't an Int: { .error }" }
    }
}

# If you want an exception on error but the value otherwise you can use .ok
say 'The numeral 3 is an Int' ~ to-int('3').ok('Unable to convert value to Int');

try {
    CATCH { 
        default { say .gist }
    }
    say 'My asciimote is an Int: ' ~ to-int('<3').ok('No, even asciimotes are not an Int!');
}

# You can also use a with block via the .err-to-undef adapter method.
for @test-values -> $val {
    with to-int($val).err-to-undef {
        say "{ $val.WHAT.perl } $val converted to Int { .value }"
    }
    else {
        say "Well '{ $val.gist }' wasn't an Int"
    }

}

# Lastly if in doubt, you can wrap any code with a result block to wrap any exceptions or failures to results.
# Oh and results will also be returned as is so don't worry if you mix things up!
for <die fail smile other> {
    my $val = result {
        when 'die'   { die 'boom' }
        when 'fail'  { fail 'bang' }
        when 'smile' { Ok '' }
        default      { '' }
    }

    say "$_ => { $val.WHAT.gist } with value: { $val.map-err( { Ok .error } ).value }";
}

DESCRIPTION

Result was originally inspired by Rust's Result enum, but Perl 6 is a rather different languages and as such while the core concept remains, the implementation and features are distinct. The Result module provides an error management framework similar to Perl6's Failures, but with stricter semantics.

The error handling pattern provided by the Result module attempts to make as clear as possible to the consumer of a function, that the function can return an error and therefore the consumer must take responsibility for handling an error case. Therefore all values returned from a function, for which success is not certain, are of the Result::Any type, either an OK or an Err. Hence to obtain the value returned by the function you can choose to dispatch the error yourself with the following methods:

  • .is-ok

  • .is-err

  • .map-ok

  • .map-err

Or if you just want to fail on error, call the .ok(Str) method. The .ok(Str) method simply returns the value if it is called on a Result::Ok object. However if it is called on a Result::Err object the error will be thrown.

Mapping Results

There are two methods provided in the Result::Any interface for chaining computations together depending on the result.

  • .map-err(&code --> Result::Any)

  • .map-ok(&code --> Result::Any)

These routines call the provided Callable if their is-* identity is True and otherwise will simply skip the Callable and return the result object as is. The first argument to the Callable will be the result object which is being mapped. Be sure to return a result object else you will end up throwing an exception.

Interfacing with core Perl 6 error handling

The identity methods, .is-* work well with the pattern but the with pattern depends on the definedness of the topic. An instance of Result::Any is defined (although an Err is Falsy and a Ok is Trueish), so to use a result object in a with block, use the .err-to-undef method (See the synopsis for an example).

If you want to adapt existing perl6 code to return an appropriate Result::Any value, use the result sub (See the synopsis for an example and below for more detail).

See Also

The perl6 Failure constructs provide a slightly different approach for solving the core problem of returning error state from functions or methods. Be sure to consider if they might be a better fit for your needs. For more on Failures, see: https://docs.perl6.org/language/control#fail.

Changes

head

0.2.5

  • Fixed issue #2 duplicate call of callable block in result routine.

  • Minor tweaks of the documentation.

head

0.2.4

Contributed by JJ

  • Type constraint on Result::Ok.value attributes relaxed.

  • Internal refactoring of .is-ok and .is-err from methods to attributes.

  • Cleaned up documentation

head

0.2.3

  • Added result sub which wraps any exceptions, failures into results for a given Callable. Result:Err and Result::Ok objects are passed on without any alteration.

  • Added .map-err and .map-ok methods to the Result::Any role allowing for type dependent chaining of handlers for result objects.

0.2.2

  • Added .err-to-undef to facilitate use of Result values in with blocks.

0.2.0

Major braking changes! Either specify a version tag or you can try the migration script: migrate-0.1.0-to-0.2.0.p6.

  • Result role renamed Result::Any - use this instead in signatures.

  • All result objects and helpers are exported with use Result;. Multiple imports are no longer required.

  • Result::Err is no longer a Failure object. This fixes throwing of exceptions when requesting an error message from a Result::Err object which happens in newer version of Rakudo.

  • Result::OK renamed to Result::Ok. The naming is now more consistent and feels better.

  • Factory routine Error renamed Err. More consistent naming.

  • Factory routine OK renamed Ok. More consistent naming. Lowercase was considered but it clashes to easily with the Test module so leading case naming was maintained for factory routines.

  • Type constraining of Result::Ok payloads removed as I have not encountered a useful application of this feature while using the Result module.

0.1.0

Initial release of Result module. Result:Err objects are Failures and do the Result role. Experimental type constraining of Result::OK object payloads.

AUTHOR

Sam Gillespie samgwise@gmail.com

COPYRIGHT AND LICENSE

Copyright 2019 Sam Gillespie

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.

sub Ok

sub Ok(
    $value
) returns Result::Ok

Creates a Result::OK containing the given value. To extract the payload, after checking with *.is-ok or * ~~ Result::OK, you can read the .value attribute.

sub Err

sub Err(
    Str $error
) returns Result::Err

Creates a Result::Err with the given message. The message is readable from the .error attribute.

sub result

sub result(
    &code
) returns Result::Any

Wraps the returned value of a Callable in a Result::OK and returns exceptions as a Result::Err. Returned failures are also transformed into Result::Err objects. If a Result::Any value is returned from the Callable it will be transperently passed along.