Errable is an Option with inverted Try-semantics.

Overview

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)
}
You might also like...
Application microframework with command-line option parsing, configuration, error handling, logging, and shell interactions
Application microframework with command-line option parsing, configuration, error handling, logging, and shell interactions

Abscissa is a microframework for building Rust applications (either CLI tools or network/web services), aiming to provide a large number of features w

nothing::[Probably] is a better [Option].
nothing::[Probably] is a better [Option].

nothing::[Probably] is a better [Option].

Option and Either types with variants known at compile time.

Const Either Some types to allow deciding at compile time if an option contains a value or which variant from the either type is active. This might be

Provide CRUD CLI for Moco Activities with Jira Cloud Sync Option for faster time tracking.

Moco CLI Provide CRUD CLI for Moco Activities with Jira Cloud Sync Option for faster time tracking. Available commands Login Jira Must be called befor

An open source, high performance limit order book for the Seaport smart contracts. Implemented in Rust using ethers-rs, this offers a turnkey option for digital asset marketplaces.

Quay Quay is an open source, high performance backend for the Seaport smart contracts. The project is implemented in Rust, using Postgres as a storage

A scrapper that abstracts the IQ Option API calls into a Rust package.

IQ Option API - In Rust An abstraction of IQ Options API calls in a Rust library. License Licensed under either of MIT license (LICENSE-MIT or https:/

OptionCell: OnceCell but derivable from Option

OptionCell: OnceCell but derivable from Option This library provides an equivalent of OnceCell, but it guarantees layout compatibility with OptionT,

Natural language detection library for Rust. Try demo online: https://www.greyblake.com/whatlang/
Natural language detection library for Rust. Try demo online: https://www.greyblake.com/whatlang/

Whatlang Natural language detection for Rust with focus on simplicity and performance. Content Features Get started Documentation Supported languages

A template for a Rust-powered static-page Try Online interface

rust-tio-template A template for a Rust-powered static-page Try Online interface What is included This is an example setup that enables all of the fol

Small project that try to bend to world.

Small project that try to bend to world. May not be fully working, but should provide an improvable starting point. How to The only important file is

Try to learn Rust in a week. The goal is to finish a quiz at the end of the week.

RustInAWeek Try to learn Rust in a week. The goal is to finish the quiz at the end of the week. Quiz link https://dtolnay.github.io/rust-quiz/1 Book l

Just a little game I made in a day to try out the WASM-4 fantasy console.

Dodgeball This is just a little game I made in a day to try out the WASM-4 fantasy console. Play it here. The palette is SODA-CAP by Cappuchi. License

Expand your possibilities with the Try ? Operator

Expand your possibilities with the Try ? Operator Have you ever found yourself writing a function which may return early based on some condition? fn m

Try to find the correct word with only first letter and unknown letter count

Try to find the correct word with only first letter and unknown letter count

A rust program to try and detect some types of Hardware Keyloggers.
A rust program to try and detect some types of Hardware Keyloggers.

Hardware Keylogger Detection Warning: Certain Types of Hardware keyloggers can not be detected by this program, Passive Hardware Keyloggers are imposs

try to find the correct word with only first letter and unknown letter count.

MOTUS Current dictionaries are provided in french and can contain some words not included in the official Motus dictionary. Additionally, dictionaries

mn is my project to learn rust, I will try to build a markdown editor

med is a markdown editor This is my project to learn rust programming language, I will try to build a powerful markdown editor. Highlight: Support Vim

My try at Advent of Code 2022 in Rust.

Advent of Code 2022 My solutions to Advent of Code 2022, written in Rust. I love seeing the variety of solution other people come up with, so I decide

Rust crate for Ok-wrapping and try blocks

tryvial A small crate for Ok-wrapping and try blocks. This is compatible with Result, Option, and any type implementing the unstable std::ops::Try tra

Comments
  • Question: Why not a type alias?

    Question: Why not a type alias?

    Hello! I was reading your post and I was wondering: why not use a type alias instead? It will still communicate your intention better than the Result<(), E> and you will not be creating a new incompatible type for your errors.

    I mean, something like this:

    type Fallible<E> = std::result::Result<(), E>;
    fn fail<E>(e: E) -> Fallible<E> {
        Err(e)
    }
    fn success<E>() -> Fallible<E> {
        Ok(())
    }
    

    Link to playgroundwith tests.

    opened by rodrigorc 1
  • Thoughts on the blog post and Fallible in general

    Thoughts on the blog post and Fallible in general

    But what's with the Result<(), Error>? Obviously the function can fail, so the Error makes sense, but why is the Rust standard library using a Result for a function which clearly produces no output value? The description for Result even says that:

    It is an enum with the variants, Ok(T), representing success and containing a value, and Err(E) (...)

    Yet our result contains no value!

    That's exactly why () exists, to represent "no value". Some types exist not because they contain useful information, but because they are the information themselves. Take the ! type for instance. It's even less useful in practice than (). ! is a type with no possible values. Not only does it not contain any information, but you can't even instantiate and return one at all. The fact that it's impossible to do so is actually why ! is useful at all, () means "nothing", while ! means "impossible". Result<u16, !> conveys that this function will always succeed, even when a Result is required (e.g. trait impls, more on that later). Similarly, Result<(), E> conveys that the function might succeed or fail, and that it returns nothing on success. Just because a type is present doesn't mean that type needs to (or even should) carry information at runtime.

    To put it succinctly, () and ! convey information to the programmer during development, rather than conveying information to the function calling them at runtime.

    A better definition might use Option instead...

    While we do agree that it isn't a good solution, my reasoning differs. For me, it's not the semantics of the code itself, but the fact that Option conveys different meaning than Result. Result is clearly tied to success and failure, whereas Option is not. Returning a Result clearly signals "This might work, it might not", and having that readability is so important to me that oftentimes I'll even use Result<(), ()> instead of just bool. Looking at a function signature and seeing that it returns Result is an instant clue to me that this function fails. Seeing Option<Error> wouldn't be as clear to me. The try syntax is just a bonus compared to the readability problems.

    It fills the gap left by Option and Result by providing a type that signifies either the successful completion of an operation or an error.

    This might sound a bit harsh, but there really isn't a gap there. Result<(), E> does that already.

    If I were to be a little provocative, I would perhaps ask why Option exists, when its use cases could just as easily be covered by Result<T, ()>.

    Same thing as the Option<Error> bit, just reversed. Option tells the programmer "this function might return something, but it might not", whereas Result tells the programmer "this function might succeed, and it might fail". Opening your fridge and seeing no food in there isn't an error, your fridge isn't broken or anything, and you didn't fail to look for food, it's just empty. Result doesn't make sense, even if semantically it carries the same value.

    but I would argue that clearly communicating intent when designing the signatures of your functions is an absolute readability win.

    How is Fallible<E> clearer than Result<(), E>? The latter is just as clear if not clearer than the former, they both convey "this might succeed, and return nothing, or fail, and return E".

    95% of use cases can be covered by Result and Option

    What's the 5% here? I can't think of anything where Fallible<E> would work but Result<(), E> wouldn't.

    It's been said that perfection is achieved when there's nothing left to take away, and that feckin' () needs to go!

    I'd say the exact opposite. This crate adds a whole new type to represent something that can already be represented by another type, and it splits "might succeed, might fail" into two types that can't intermingle instead of having two different variants of the same type. To me, that seems like taking something simple and making it more complex.


    One other thing that's also worth noting, even if your crate saw 100% adoption, Result<(), E> wouldn't be entirely eliminated.

    trait Flammable<T> {
        fn try_burn(&mut self) -> Result<T, Error>;
    }
    
    // wood turns into charcoal when burnt
    impl Flammable<Charcoal> for Wood {
        fn try_burn(&mut self) -> Result<Charcoal, Error> {
            if self.wet {
                return Err(Error::Wet);
            }
            Ok(Charcoal::new())
        }
    }
    
    // paper burns cleanly and leaves nothing behind
    impl Flammable<()> for Paper {
        // even though we return no value here, we can't use Fallible
        // because the trait requires Result. since Wood still requires
        // a value in the successful case, we can't change the trait to
        // use Fallible either.
        fn try_burn(&mut self) -> Result<(), Error> {
            if self.wet {
                return Err(Error::Wet);
            }
            Ok(())
        }
    }
    

    There's no way to replace Result<(), E> with Fallible<E> without making two traits for things that burn cleanly and things that burn and produce a result.


    To sum up my thoughts, this crate seems a bit misguided. I think it fills a niche that I'm not sure exists. Splitting Result into 2 types seems like a hefty price to pay for the small benefit of not seeing or typing () some of the time. It's worse for readability, usability, and maintainability, with no real benefit. I don't really think Result<(), E> is unclear, it conveys exactly what it means, so there's not really much for this crate to fix.

    opened by typecasto 0
  • To me this is less clear than Ok(())

    To me this is less clear than Ok(())

    Normally I would not comment on something that I don't understand why people are doing, but in reply to "Feedback and comments are extremely welcome" I thought you might want to hear an opposite opinion.

    To me this doesn't simplify things, on the contrary it adds unnecessary cognitive overhead of learning yet another type. In case of Ok(()) at least you can clearly see that you return nothing wrapped in Result. In case of Success on the other hand you explicitly return SOMETHING from a function that "clearly produces no output value" and this is much more counterintuitive.

    I think if one seeks to seemingly not return the unit () type - Fehler is the way to go. This is the only way of not typing any return in both the signature and the body and still have the "?" shortcut that I'm aware of:

    #[throws(OtherError)]
    fn do_thing() {
        might_fail()?;
    }
    
    opened by andywwright 1
Releases(v0.1.3)
Owner
Mathias Pius
Mathias Pius
nothing::[Probably] is a better [Option].

nothing::[Probably] is a better [Option].

Btwiuse Arch 0 Sep 22, 2022
Provide CRUD CLI for Moco Activities with Jira Cloud Sync Option for faster time tracking.

Moco CLI Provide CRUD CLI for Moco Activities with Jira Cloud Sync Option for faster time tracking. Available commands Login Jira Must be called befor

Emanuel Vollmer 7 Nov 18, 2022
Expand your possibilities with the Try ? Operator

Expand your possibilities with the Try ? Operator Have you ever found yourself writing a function which may return early based on some condition? fn m

EC 1 Feb 1, 2022
try to find the correct word with only first letter and unknown letter count.

MOTUS Current dictionaries are provided in french and can contain some words not included in the official Motus dictionary. Additionally, dictionaries

Alexandre 6 Apr 11, 2022
mn is my project to learn rust, I will try to build a markdown editor

med is a markdown editor This is my project to learn rust programming language, I will try to build a powerful markdown editor. Highlight: Support Vim

Skoo Wu 7 Dec 29, 2022
My try at Advent of Code 2022 in Rust.

Advent of Code 2022 My solutions to Advent of Code 2022, written in Rust. I love seeing the variety of solution other people come up with, so I decide

Luke Taylor 6 Dec 15, 2022
Rust crate for Ok-wrapping and try blocks

tryvial A small crate for Ok-wrapping and try blocks. This is compatible with Result, Option, and any type implementing the unstable std::ops::Try tra

null 11 Jan 25, 2023
Neovim Configuration Manager (Swap/Backup/Try Configurations Easily)

ncm-rs Neovim Configuration Manager (Swap/Backup/Try Configurations Easily) I created this package because I wanted to try out Lazyvim (which is why i

instance.id 4 Mar 5, 2023
Need a powerful and simple library to work with arithmetic progressions in Rust? You should definitively try out ariprog!

Ariprog I had a test (03/2024) on arithmetic progressions, so I decided to create a library to study math. Taking advantage of the fact that I was stu

Kauê Fraga Rodrigues 4 Mar 19, 2024
Provides two APIs for easily cancelling futures, with the option to fallback to a timeout cancellation

tokio-context Provides two different methods for cancelling futures with a provided handle for cancelling all related futures, with a fallback timeout

Peter Farr 18 Dec 27, 2022