Understanding what the SNAFU solution does

Hello friends,

After being bullied on stackoverflow, I will give it a try here. I published my first post and they make it impossible to comment on an answer, so instead of moving my answer to the comment section, the moderator deleted the post before Shepmaster got a chance to have a look at it. At the end of the road, their software is not able to solve basic user issues, leaving all the power to the almighty moderators that can do whatever they please to bully new users. My answer was long and elaborate and I was genuinely looking for a solution.
In this forum, I've had even people help me by private message, which is rare these days, as most programmers are looking for fame above anything else.
By the way the initiative of rust mentors proves that the rust team is really trying to change things up and provide real help to people without reclaiming money or public recognition.

Here is the original question's link: Link to the stackoverflow question

I can't comment on this answer but I would like Shepmaster to give a little more code and details on how he is solving the problem by using that AsErrorSource trait you mentioned.

I am not able to understand when your solution should be used, in particular the input values involved (Error types).

If you get a chance to read it Shepmaster, please delete my answer and just edit yours if possible. Thanks

Edit. Just found this implementation of Box for Error. The op is saying that Box doesn't implement Error, but from what I can find, it seems it does. Anyways, it seems that there lots of moving parts and to be honest I am completely lost at this point. I will be doing my own research and checking this question from time to time today, and I hope Shepmaster will be able to give a detailed explanation of what is going on. Basically, I would like to have a complete example. From the time we receive a Box to its conversion to &(dyn Error + 'static) and how your solution is able to avoid the overlap you talked about.

#[stable(feature = "box_error", since = "1.8.0")]
impl<T: Error> Error for Box<T> {
    #[allow(deprecated, deprecated_in_future)]
    fn description(&self) -> &str {
        Error::description(&**self)
    }

    #[allow(deprecated)]
    fn cause(&self) -> Option<&dyn Error> {
        Error::cause(&**self)
    }

    fn source(&self) -> Option<&(dyn Error + 'static)> {
        Error::source(&**self)
    }
}

Edit, I understand what he meant by the fact that trait implements overlap. What I didn't understand is the solution that he proposed in his SNAFU error crate, basically implementing that trait does, the gist of it if you prefer.

pub trait AsErrorSource {
    /// For maximum effectiveness, this needs to be called as a method
    /// to benefit from Rust's automatic dereferencing of method
    /// receivers.
    fn as_error_source(&self) -> &(dyn Error + 'static);
}

impl AsErrorSource for dyn Error + 'static {
    fn as_error_source(&self) -> &(dyn Error + 'static) {
        self
    }
}
1 Like

@shepmaster

2 Likes

Rust's type system contains a "magic" trait called Sized, defined as "types with a constant size known at compile time." You cannot return, accept as a parameter, or store in a variable an un-Sized type; you have to indirectly work with it through a pointer. For context, &dyn Error and Box<dyn Error> are both Sized, but dyn Error is un-Sized.

One of the really weird parts of Rust is that, because so many things require Sized inputs, it automatically adds Sized to your bounds. T: Error and T: Error + Sized mean exactly the same thing. To remove the implicit Sized bound, you need to write T: Error + ?Sized.

So, in order for Box<dyn Error> to implement Error, the impl clause would need to be written like this:

impl<T: Error + ?Sized> Error for Box {
3 Likes

@notriddle addresses a lot of the points, but I'll chime in too!


Right now, Rust has this blanket implementation:

impl<T: Error> Error for Box<T> {}

This generic type has an implicit Sized bound, so it could be equivalently written as:

impl<T: Error + Sized> Error for Box<T> {}

If you try to apply this implementation to Box<dyn Error>, then T would need to equal dyn Error. The compiler disallows that because dyn Error by itself is unsized.

The next step you'd take to fix this would be to loosen the bound to allow ?Sized types:

impl<T: Error + ?Sized> Error for Box<T> {}

However, if you do this, then you'll end up with conflicting implementations of the From trait's blanket implementation and the conversion of an concrete error to a Box<dyn Error>:

impl<T> From<T> for T {} // #1

impl<'a, E: Error + 'a> From<E> for Box<dyn Error + 'a> {} // #2

If we could have both of these implementations in play at the same time, then it would become ambiguous what should happen for this code:

let a: Box<dyn Error> = todo!();
let b: Box<dyn Error> = a.into();

Should it leave it as is (following impl 1) or box it again (following impl 2)? Chances are that most people would want 1, but both paths are valid. That's the long form answer as to why Box<dyn Error> cannot implement Error itself at this point in time.

In the future, specialization might allow for both of these to overlap and pick a specific case (probably 1, I'd guess).

For details on the SNAFU-specific solution, it might be easiest to see what the macro expands to (cleaned up a bit):

#[derive(Debug, Snafu)]
struct Demo {
    source: Box<dyn std::error::Error>,
}

becomes

impl Error for Demo {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        use snafu::AsErrorSource;
        match *self {
            Self { ref source, .. } => Some(source.as_error_source()),
        }
    }
}

This way, we don't need to rely on source actually implementing Error itself, but instead have a number of concrete implementations that don't overlap with the blanket implementation:

impl AsErrorSource for dyn Error + 'static
impl AsErrorSource for dyn Error + Send + 'static
impl AsErrorSource for dyn Error + Sync + 'static
impl AsErrorSource for dyn Error + Send + Sync + 'static
impl<T: Error + 'static> AsErrorSource for T

95% of the credit for this solution goes to @kennytm, who helped me immeasurably by figuring out that trick!

2 Likes

Thank you @shepmaster. Also thank you @notriddle for the box explanation.

Edit: Thank you @TomP. I didn't know that Shep was a member of this forum.