Search code examples
genericsrustassociated-types

A tricky Rust generics problem I'm scratching my head over


I have an application that extends over 3 crates: A crate that holds the abstract framework, another that holds one of a number of plugins selected as a cargo feature, and a third that contains a concrete implementation.

The problem is that the plugin determines the "Version" type throughout the application, and the implementation determines the Errors type throughout the application. To make the application plug-able across multiple plug-ins and across multiple implementations, I need the Errors type in the plugin to be generic, and I can't figure out how to do that.

In the minimal code below, I have hard coded the Plugin type Errors = MyThingErrors to show something that works. But I need the type of Errors here to be generic, not concrete. I've tried all sorts of combinations of generic parameters, but can't get it to compile.

So, is there a trick? Am I pushing Rust generics too far? Is this a Problem XY example, Perhaps I should follow a different approach?

Any suggestions gratefully received.

Here is the working example:

    use thiserror::Error;

// ----------------------------------------
// Abstract traits crate

trait Thing {
    type Errors;
    type Version;
    fn plugin(&self) -> &Box<dyn Plugin<Errors = Self::Errors, Version = Self::Version>>;
    fn foo(&self) -> Result<(), Self::Errors>;
}

trait Plugin {
    type Errors;
    type Version;
    fn bar(&self) -> Result<(), Self::Errors>;
}

// ----------------------------------------
// plugin crate

#[derive(Error, Debug, PartialEq)]
enum PluginErrors {
    #[error("First Plugin error")]
    Error1,
}
struct PluginVersion {}

struct MyPlugin {}
impl Plugin for MyPlugin {
    type Errors = MyThingErrors;
    type Version = PluginVersion;
    fn bar(&self) -> Result<(), Self::Errors> {
        Err(MyThingErrors::PluginError(PluginErrors::Error1))
    }
}

// ----------------------------------------
// concrete implementation crate

#[derive(Error, Debug, PartialEq)]
enum MyThingErrors {
    #[error("First MyThing error")]
    MTError1,
    #[error("Plugin Error: {0}")]
    PluginError(#[from] PluginErrors),
}

struct MyThing {
    p: Box<dyn Plugin<Errors = MyThingErrors, Version = <MyThing as Thing>::Version>>,
}
impl Thing for MyThing {
    type Version = PluginVersion;
    type Errors = MyThingErrors;
    fn plugin(&self) -> &Box<dyn Plugin<Version = Self::Version, Errors = Self::Errors>> {
        &self.p
    }
    fn foo(&self) -> Result<(), Self::Errors> {
        Err(MyThingErrors::MTError1)
    }
}

fn main() {
    let t = MyThing {
        p: Box::new(MyPlugin {}),
    };
    if let Err(e1) = t.foo() {
        assert_eq!(e1, MyThingErrors::MTError1);
    }
    if let Err(e2) = t.p.bar() {
        assert_eq!(e2, MyThingErrors::PluginError(PluginErrors::Error1));
    }
}

Solution

  • Another way to approach this is to provide a way to map error types on the plugins.

    I'd do this by adding a MappedPlugin type to the traits crate

    struct MappedPlugin<P,F> {
        p: P,
        f: F,
    }
    
    impl<P,F, E> Plugin for MappedPlugin<P,F>
    where
        P: Plugin,
        F: Fn(P::Errors) -> E,
    {
        type Errors = E;
        type Version = P::Version;
        fn bar(&self) -> Result<(), Self::Errors> {
            self.p.bar().map_err(&self.f)
        }
    }
    

    Then wrapping and creating the created plugin in the main crate:

    fn main() {
        let f = |e:PluginErrors| -> MyThingErrors { MyThingErrors::PluginError(e) };
        let t = MyThing {
            p: Box::new(MappedPlugin{ p:MyPlugin {}, f:f }),
        };
        if let Err(e1) = t.foo() {
            assert_eq!(e1, MyThingErrors::MTError1);
        }
        if let Err(e2) = t.p.bar() {
            assert_eq!(e2, MyThingErrors::PluginError(PluginErrors::Error1));
        }
    }
    

    You could add a simple function to do that wrapping for you so that this becomes MyPlugin{}.map_err(|e| MyThingErrors::PluginError).

    The main crate still needs to know about the error types in the plugin crates.

    A full working version can be seen here.