Search code examples
design-patternsrustidioms

What design pattern does the SNAFU library use to extend external types?


I've been playing around with the interesting SNAFU library.

A slightly modified and stand-alone example from the SNAFU page is as below:

use snafu::{ResultExt, Snafu};
use std::{fs, io, path::PathBuf};

#[derive(Debug, Snafu)]
enum Error {
    #[snafu(display("Unable to read configuration from {}: {}", path.display(), source))]
    ReadConfiguration { source: io::Error, path: PathBuf },
}

type Result<T, E = Error> = std::result::Result<T, E>;

fn process_data() -> Result<()> {
    let path = "config.toml";
    let read_result: std::result::Result<String, io::Error> = fs::read_to_string(path);    
    let _configuration = read_result.context(ReadConfiguration { path })?;
    Ok(())
}

fn main() {
    let foo = process_data();

    match foo {
        Err(e) => println!("Hello {}", e),
        _ => println!("success")
    }
}

The change I've made is to make the type on the Result from fs::read_to_string(path) explicit in process_data().

Given this, I can't understand how read_result has the context method available to it, as the std::result::Result docs don't make any reference to context (and the compiler similarly complains if you strip out the SNAFU stuff and try to access context).

There is a pattern being used here that is not obvious to me. My naive understanding is that external types cannot be extended because of the orphan rules, but something is happening here that looks much like such an extension.

I'm also confused by the type Result... line. I'm aware of type aliasing, but not using the syntax in which the left hand side has a generic assigned. Clearly, this is an important part of the design pattern.

My request is for clarification as to what pattern is being used here and how it works. It seems to get at some pretty interesting aspects of Rust. Further reading would be valued!


Solution

  • ResultExt is an extension trait, which is why it uses the somewhat common suffix Ext.

    Reduced, the implementation contains the definition of the trait and a small handful of implementations of the trait for specific types (or just one):

    pub trait ResultExt<T, E>: Sized {
        fn context<C, E2>(self, context: C) -> Result<T, E2>
        where
            C: IntoError<E2, Source = E>,
            E2: std::error::Error + ErrorCompat;
    }
    
    impl<T, E> ResultExt<T, E> for std::result::Result<T, E> {
        fn context<C, E2>(self, context: C) -> Result<T, E2>
        where
            C: IntoError<E2, Source = E>,
            E2: std::error::Error + ErrorCompat,
        {
            self.map_err(|error| context.into_error(error))
        }
    }
    

    By importing ResultExt, you bring the trait and its methods into scope. The library has implemented them for the Result type, so you are able to make use of them as well.

    See also:

    I'm aware of type aliasing, but not using the syntax in which the left hand side has a generic assigned. Clearly, this is an important part of the design pattern.

    It's not important for the ability to use .context(), it's just a practice I encourage. Most custom Result aliases shadow the name Result (e.g. std::io::Result), which means if you need to use a different error type, you need to use ugly full paths or another alias (e.g. type StdResult<T, E> = std::result::Result<T, E>).

    By creating the local alias Result with a default generic, the user can type Result<T> instead of the more verbose Result<T, MyError>, but can still use Result<T, SomeOtherError> when needed. Sometimes, I'll even go further and define a default type for the success type. This is most common in unit tests:

    mod test {
        type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
    
        fn setup() -> Result<String> {
            Ok(String::new())
        }
    
        fn special() -> Result<String, std::io::Error> {
            std::fs::read_to_string("/etc/hosts")
        }
    
        #[test]
        fn x() -> Result {
            Ok(())
        }
    }