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

Time:12-17

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));
    }
}

CodePudding user response:

In the end, I went with the Box<dyn Error> version. The codes a bit simpler not having to have an Errors associated type, which isn't a bad thing. The requirement to have generic errors is met, but its not an ideal solution, because the implementation crate needs to know the error types in order to unwrap and process the errors.

Here's the resulting minimal code, for anyone that's interested

use std::error::Error;
use thiserror::Error;

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

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

trait Plugin {
    type Version;
    fn bar(&self) -> Result<(), Box<dyn Error>>;
}

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

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

struct MyPlugin {}
impl Plugin for MyPlugin {
    type Version = PluginVersion;
    fn bar(&self) -> Result<(), Box<dyn Error>> {
        Err(Box::new(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<Version = <MyThing as Thing>::Version>>,
}
impl Thing for MyThing {
    type Version = PluginVersion;
    fn plugin(&self) -> &Box<dyn Plugin<Version = Self::Version>> {
        &self.p
    }
    fn foo(&self) -> Result<(), Box<dyn Error>> {
        Err(Box::new(MyThingErrors::MTError1))
    }
}

fn main() {
    let t = MyThing {
        p: Box::new(MyPlugin {}),
    };

    if let Err(e1) = t.foo() {
        if let Some(err) = e1.downcast_ref::<MyThingErrors>() {
            assert_eq!(err.to_string(), "First MyThing error");
        }
    }
    if let Err(e2) = t.p.bar() {
        if let Some(err) = e2.downcast_ref::<PluginErrors>() {
            assert_eq!(err.to_string(), "First Plugin error");
        }
    }
}

CodePudding user response:

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.

  • Related