3
votes

Here's an error struct:

#[derive(Debug)]
pub struct Error {
    msg: &'static str,
  //source: Option<Box<dyn std::error::Error>>,        // old
    source: Option<Box<dyn std::error::Error + Send>>, // new
}

impl Error {
    fn new_caused<E>(msg: &'static str, err: E) -> Self
    where
        E: 'static + std::error::Error + Send,
    {
        Self {
            msg: msg,
            source: Some(Box::from(err)),
        }
    }
}

impl std::fmt::Display for Error {
    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(fmt, "{}", self.msg) // HACK
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source.as_ref().map(|err| err.as_ref())
    }
}

fn main() {
    let err = "this will fail".parse::<i32>().unwrap_err();
    let err = Error::new_caused("some msg", err);
}

I decided to make it Sendable so I changed source: Option<Box<dyn std::error::Error>> to source: Option<Box<dyn std::error::Error + Send>> and strange things happened.

Magic #1

new_caused refused to compile any more:

error[E0277]: the trait bound `std::boxed::Box<dyn std::error::Error + std::marker::Send>: std::convert::From<E>` is not satisfied
  --> src/main.rs:14:26
   |
14 |             source: Some(Box::from(err)),
   |                          ^^^^^^^^^^^^^^ the trait `std::convert::From<E>` is not implemented for `std::boxed::Box<dyn std::error::Error + std::marker::Send>`
   |
   = note: required by `std::convert::From::from`

Changing Box::from to Box::new helped, even though their signatures seem identical and Box::from's implementation just calls Box::new.

Magic #2

source also became incorrect:

error[E0308]: mismatched types
  --> src/main.rs:27:9
   |
27 |         self.source.as_ref().map(|err| err.as_ref())
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected trait `std::error::Error`, found trait `std::error::Error + std::marker::Send`
   |
   = note: expected enum `std::option::Option<&(dyn std::error::Error + 'static)>`
              found enum `std::option::Option<&dyn std::error::Error + std::marker::Send>`

Why isn't the unused Send trait ignored like other ones?

Replacing that combinator logic with its manual version worked fine:

fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
    match &self.source {
        Some(source) => Some(source.as_ref()),
        None => None
    }
}

Summary

What are the explanations for this "magic" and what are better methods of dealing with it?

1
@Shepmaster that's it, I believe: play.rust-lang.org/…. - passing_through

1 Answers

4
votes

For magic #1, it is because the standard library has these implementations:

impl<'a, E: Error + 'a> From<E> for Box<dyn Error + 'a>
impl<'a, E: Error + Send + Sync + 'a> From<E> for Box<dyn Error + Send + Sync + 'a>

There is no implementation for E: Error + Send without the Sync.

Easy solution #1: add Sync whereever there is a Send or use Box::new.

Magic #2 is more complicated: you have an std::option::Option<&dyn std::error::Error + Sync> where you need an Option<&dyn std::error::Error>. You know that a &(dyn std::error::Error + Send) is convertible to a &dyn std::error::Error so you expect that the Option<_> will be too, but those conversions are not transitive1, so it fails.

The difference between the map and the match is in the order of type deduction:

In the map case, the type of the closure is deduced as taking a Box<dyn std::error::Error + Sync>. Since it returns err.as_ref(), of type &dyn std::error::Error + Sync, that is the type that the closure returns. Then Option::map returns an Option<_> with the same type of the closure return type so you get a final Option<&dyn std::error::Error + Sync> and an error.

In the match code, when you write Some(source) => Some(source.as_ref()), source is deduced as type Box<dyn std::error::Error + Sync>, but the right side is deduced from the returned type Option<&dyn std::error::Error>, so the argument to Some is cast to that type: source.as_ref() is converted to the right type and it compiles.

I think the easiest way to write this example is to add a cast as _ inside the map, to instruct the compiler to deduce the type of the closure from the usage instead of from the inner code:

self.source.as_ref().map(|err| err.as_ref() as _)

If the code is more complex, as _ may not be feasible. Then a match is perfectly adequate.

Playground with the fixed code.


1: I think I read about making these conversions automatic (covariant on auto traits?) but I cannot find it anywhere...