GitXplorerGitXplorer
M

fallible-option

public
26 stars
0 forks
1 issues

Commits

List of commits on branch main.
Verified
fbe42ba1fd5a58c251bf8b8ea2b4fa6dec3b82fb

Add example showing automatic conversion from Result<T, E> to Fallible<E>

MMathiasPius committed 2 years ago
Verified
a1661001917dc841305813783acd865ed828f9ea

fmt

MMathiasPius committed 2 years ago
Verified
09acf557ed4db710282099a36d950a2aae9fea24

Generalize error-producing fn

MMathiasPius committed 2 years ago
Verified
9c11bd50ca5e13ab2b39949a64f95a5d613c8991

Add Result test

MMathiasPius committed 2 years ago
Verified
1b87214db26ec4f3961c6c57a5c174d1a5e9a722

Implement error casting for Results too

MMathiasPius committed 2 years ago
Verified
8f14ec15ce84ce20ff5a1ac445506299320a99f7

Implemented error conversion for FromResidual<Fallible<U>> to Fallible<E> where E: From<U>

MMathiasPius committed 2 years ago

README

The README file for this repository.

Fallible Latest Version Docs

Fallible is an Option with inverted Try-semantics.

What this means is that using the ? operator on a Fallible<E> will exit early if an error E is contained within, or instead act as a no-op, if the value is Success.

This is in contrast to Option where using ? on a None-value will exit early.

Fallible fills the gap left by the Result and Option types:

Potential Success Potential Failure
Result<T , E>
Option<T> Fallible<E>

Example

This code illustrates how Fallible can be used to write succint validation code which exits early in case of failure.

use fallible_option::Fallible::{self, Fail, Success};

// Validates the input number `n`, returning a `Fail`
// if the input number is zero, or `Success` otherwise.
fn fails_if_number_is_zero(n: u32) -> Fallible<&'static str> {
    if n == 0 {
        Fail("number is zero")
    } else {
        Success
    }
};

// Check many numbers, returning early if a tested
// number is equal to zero.
fn check_many_numbers() -> Fallible<&'static str> {
    fails_if_number_is_zero(1)?;
    fails_if_number_is_zero(3)?;
    fails_if_number_is_zero(0)?; // <--- Will cause early exit

    // Following lines are never reached
    fails_if_number_is_zero(10)?;
    
    Success
}

assert_eq!(check_many_numbers(), Fallible::Fail("number is zero"));

Motivation

Fallible fills the gap left by Option and Result and clearly conveys intent and potential outcomes of a function.

A function which returns Fallible has only two potential outcomes, it can fail with an error E, or it can succeed.

Why not Result?

Because Result implies output. Take std::fs::rename for instance:

If I told you that the return type of rename was a Result<T, E>, what would you guess T and E to be?

You might rightly assume that E was std::io::Error, but what about T? It could reasonably return any number of things:

  • The canonical path of the destination of the renamed file.
  • The size of the moved file.
  • The size of the file (if any) replaced by the renamed file.
  • Or perhaps even a handle to the overwritten file.

Of course none of these are true, as the T value of rename is the unit value (). rename never produces any output, it can only signal errors. So why not signal that clearly to the user?

I would argue that using a type which signals the potential for failure, but no output upon success would more clearly express the intent and potential outcomes when using this function.

Why not Option?

Potential failure could be expressed using an Option<E>, but as stated above, the Try-semantics of Option makes it unergonomic to work with:

type Error = &'static str;

fn fails_if_number_is_zero(n: u32) -> Option<Error> {
    if n == 0 {
        Some("number is zero")
    } else {
        None
    }
};

fn check_many_numbers() -> Option<Error> {
    // We have to explicitly check, since using `?` here would result in an early exit,
    // if the call returned None, which is the opposite of what we intend.
    if let Some(err) = fails_if_number_is_zero(1) {
        return Some(err)
    }

    // .. Repeating the above three lines for each check is tedious compared to
    // just using the `?` operator, as in the example.

    None
}

Conversion from Result

Switching from using Result to Fallible is very simple, as illustrated with this before/after example:

fn validate_number(x: u32) -> Result<(), &'static str> {
    match x {
        0 ..= 9 => Err("number is too small"),
        10..=30 => Ok(()),
        31..    => Err("number is too large")
    }
}

Using Fallible:

fn validate_number(x: u32) -> Fallible<&'static str> {
    match x {
        0 ..= 9 => Fail("number is too small"),
        10..=30 => Success,
        31..    => Fail("number is too large")
    }
}

Compatibility

Fallible contains utility functions for mapping to and from [Result] and [Option], as well as [FromResidual] implementations for automatically performing these conversions when used with the ? operator.

fn fails_if_true(should_fail: bool) -> Fallible<&'static str> {
    if should_fail {
        Fail("Darn it!")
    } else {
        Success
    }
}

fn try_producing_value() -> Result<u32, &'static str> {
    fails_if_true(false)?;
    fails_if_true(true)?;

    Ok(10)
}