Constructing associated type in trait impl with feature(specialization)

Hi,

I'm kind of re-inventing anyhow's Context here, more to learn how to work with traits and to experiment with feature(specialization).

Means I have my own error type that just wraps around any existing one and adds a string vector

#![feature(specialization)]

#[derive(Debug, Clone)]  
pub struct MyErrorWrapper<E>
where                    
    E: std::error::Error,
{
    inner: E,
    contexts: Vec<String>,
}

impl<E> std::fmt::Display for MyErrorWrapper<E>
where
    E: std::error::Error,
{
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        todo!();
    }
}

impl<E> std::error::Error for MyErrorWrapper<E> where E: std::error::Error {}

Now for any not-MyErrorWrapper, I can add a trait and implement it as

trait MyContext<T>                                                                                      
where                                                                                                   
    T: Sized,                                                                                           
{                                                                                                       
    type Contexted;                                                                                     
    fn context(self, _: &str) -> Result<T, Self::Contexted>;                                            
}                                                                                                       
                                                                                                        
impl<T, E> MyContext<T> for Result<T, E>                                                                
where                                                                                                   
    E: std::error::Error + Sized,                                                                       
    T: Sized,                                                                                           
{                                                                                                       
    type Contexted = MyErrorWrapper<E>;                                                                 
    fn context(self, s: &str) -> Result<T, MyErrorWrapper<E>> {
        self.map_err(|e| MyErrorWrapper::<E> {
            inner: e,
            contexts: vec![s.to_string()],                                                              
        })  
    }
}

for a Result<T, E>, that allows me to do some_result.context("hm, something wrong") and obtain a Result<T, MyErrorWrapper<E>>. However, calling .context again, I don't want a Result<T, MyErrorWrapper<MyErrorWrapper<E>>>. Thus I'm throwing a supertrait and specialization into the mix:

impl<T, E> MyContext<T> for Result<T, E>
where
    E: MyErrorTrait + Sized,
    T: Sized,
{
    type Contexted = E;
    fn context(self, s: &str) -> Result<T, Self::Contexted> {
        self.map_err(|e| e.push(s))
    }
}

trait MyErrorTrait: std::error::Error {
    fn push(self, s: &str) -> Self;
}       

impl<E> MyErrorTrait for MyErrorWrapper<E>
where
    E: std::error::Error + Clone,
{   
    fn push(self, s: &str) -> Self {
        let mut rv = self.clone();
        rv.contexts.push(s.to_string());
        rv
    }
}   

so far, so good, I only need to add some default to the former impl

impl<T, E> MyContext<T> for Result<T, E>                                                                
where                                                                                                   
    E: std::error::Error + Sized,                                                                       
    T: Sized,                                                                                           
{                                                                                                       
    default type Contexted = MyErrorWrapper<E>;                                                         
    default fn context(self, s: &str) -> Result<T, MyErrorWrapper<E>> {
        self.map_err(|e| MyErrorWrapper::<E> {
            inner: e, 
            contexts: vec![s.to_string()],
        })
    }
}

But here is where I start diving down compiler errors. I get

error[E0053]: method `context` has an incompatible type for trait                                                                                                                                                 
  --> src/main.rs:39:42                                                                                                                                                                                           
   |                                                                                                                                                                                                              
38 |     default type Contexted = MyErrorWrapper<E>;                                                                                                                                                              
   |     ------------------------------------------- associated type is `default` and may be overridden                                                                                                                                                                                                                      
39 |     default fn context(self, s: &str) -> Result<T, MyErrorWrapper<E>> {                                                                                                                                      
   |                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                                                                        
   |                                          |                                                                                                                                                                   
   |                                          expected associated type, found `MyErrorWrapper<E>`                                                                                                                 
   |                                          help: change the output type to match the trait: `Result<T, <Result<T, E> as MyContext<T>>::Contexted>`                                                             
   |                                                                                                                                                                                                              
note: type in trait                                                                                                                                                                                               
  --> src/main.rs:29:34                                                                                                                                                                                           
   |                                                                                                                                                                                                              
29 |     fn context(self, _: &str) -> Result<T, Self::Contexted>;                                                                                                                                                 
   |                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^                                                                                                                                                  
   = note: expected signature `fn(Result<_, _>, &_) -> Result<_, <Result<T, E> as MyContext<T>>::Contexted>`                                                                                                      
              found signature `fn(Result<_, _>, &_) -> Result<_, MyErrorWrapper<E>>`                                                                                                                              
                                                                                                                                                                                                                  

fine, I can't use MyErrorWrapper<E> in the return type, that's why I already added type Contexted as associated type, next try:

impl<T, E> MyContext<T> for Result<T, E>
where
    E: std::error::Error + Sized,
    T: Sized,
{
    default type Contexted = MyErrorWrapper<E>;
    default fn context(self, s: &str) -> Result<T, <Result<T, E> as MyContext<T>>::Contexted> {
        self.map_err(|e| <Result<T, E> as MyContext<T>>::Contexted {
            inner: e,
            contexts: vec![s.to_string()],
        })
    }
}

which gives me a different error (after adding feature(more_qualified_paths)):

error[E0071]: expected struct, variant or union type, found associated type                                                                                                                                       
  --> src/main.rs:48:26                                                                                                                                                                                           
   |                                                                                                                                                                                                              
48 |         self.map_err(|e| <Result<T, E> as MyContext<T>>::Contexted {                                                                                                                                         
   |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ not a struct                                                                                                                              

i.e. now I have a type and not a struct and honestly can't figure out how to construct it (using functions that return my type, I always end up in either of the situations, that I try to construct a Contexted or call a function that returns MyErrorWrapper<E> instead of Contexted).

Any hints? (ftr, yes i read case-studies/autoref-specialization/README.md at master · dtolnay/case-studies · GitHub , that's what gave me the idea) and ofc playground link.

Thanks in advance,
Paul

I think it's because MyErrorWrapper<E> is the default impl, but the associated type Self::Contexted is a generic type (I don't know much about specialization, but it seems the type checker thinks the associated type might have different impl than the default)

although I don't have the answer to your exact question, but I think for the current specialization feature, you cannot rely on the type checker to unify the two types Self::Contexted and MyErrorWrapper<E> in the default impl, you'll have to treat Contexted as a generic type and use trait bounds to interact with it.

in other words, you cannot construct a MyErrorWrapper directly, but have to somehow convert E to Self::Contexted generically, which means your trait MyContext must also include the E type somewhere (generic type parameter or associated type). for example, in the following snippet, I add From<E> to construct a wrapper, and MyErrorTrait to push a context message. this requires an impl<E> From<E> for MyErrorWrapper<E>.

trait MyContext<T, E> {
    type Contexted: From<E> + MyErrorTrait;
    fn context(self, s: &str) -> Result<T, Self::Contexted>;
}

this seems to have worked playground, at least you can call context() on a result.

if you try to use the returned result, you'll find out that the type inference cannot decide the concrete type of the Err variant, instead it's inferred to have some opaque type Result<T, <Result<_, _> as MyContext<_, _>>::Contexted>. this is good enough in practice, although you cannot access the error value as MyErrorWrapper directly, you can still access it through the trait MyErrorTrait.

2 Likes

Thanks.

any hint maybe how i would return that opaque type from a function? I'm thinking

fn some_fun() -> Result<i32, MyErrorWrapper<SomeError>> {
  let a = something_that_returns_a_result("a").context("first bit failed")?;
  let b = something_that_returns_a_result("b").context("second bit failed")?;
  a+b
}

my initial guess was to use a generic type (or the unstable type_alias_impl_trait feature) like this:

fn some_fun<WrappedError: MyErrorTrait>() -> Result<i32, WrappedError> {
    //...
}
// or
type WrappedError = impl MyErrorTrait;
fn some_fun() -> Result<i32, WrappedError> {
    //...
}

but it doesn't work , because the question mark operator deguar to an extra Into.

so I believe you must use the fully qualified type. you can use an type alias to reduce some repetition, but I don't know any other better way than this:

fn some_fun() -> Result<i32, <Result<i32, std::num::ParseIntError> as MyContext<i32, std::num::ParseIntError>>::Contexted> {
    //...
}
// or with a type alias
type MyResultWrapper<T, E> = Result<T, <Result<T, E> as MyContext<T, E>>::Contexted>;
fn some_fun() ->MyResultWrapper<i32, std::num::ParseIntError> {
    //...
}
1 Like

after some thinking, I think there's an alternative approach to specialize the MyContext trait. we can instead specialize a helper trait that converts an E: Error to MyErrorWrapper<E>, and the trait MyContext can have a blanket implementation for all Result<T, E> generically.

essentially, split the associated type Contexted and associated method context() into separate traits, something like

trait WrappableError: Sized {
    type Wrapped: MyErrorTrait + From<Self>;
}
trait MyContext<T, E: WrappableError> {
    fn context(self, s: &str) -> Result<T, E::Wrapped>;
}
// default impl
impl<E: std::error::Error> WrappableError for E {
    default type Wrapped = MyErrorWrapper<E>;
}
// specialized impl
impl<E: std::error::Error> WrappableError for MyErrorWrapper<E> {
    type Wrapped = Self;
}
// blanket impl
impl<T, E: WrappableError> MyContext<T, E> for Reslt<T, E> {
    fn context(self, s: &str) -> Result<T, E::Wrapped> {
        self.map_error(|inner| E::Wrapped::from(inner).push(s))
    }
}
// because we only specialized Err, the fully qualified type doesn't need to repeat the whole `Result`
fn some_fun() -> Result<i32, <std::num::ParseIntError as WrappableError>::Wrapped> {
    //...
}
// or with a type alias
type MyResultWrapper<T, E> = Result<T, <E as WrappableError>::Wrapped>;
fn some_fun() ->MyResultWrapper<i32, std::num::ParseIntError> {
    //...
}
1 Like