rust-ioc icon indicating copy to clipboard operation
rust-ioc copied to clipboard

How to do automatic resolving?

Open zerkalica opened this issue 8 years ago • 4 comments

In rust-ioc we need to resolve any dependency semi-manually:

    fn resolve((x, y): Self::Dependency) -> Self {
        Z {
            x: x.into_inner(),
            y: y.into_inner(),
        }
    }

In C# ninject we can bind interface to class


public class WarriorModule : NinjectModule
{
    public override void Load() 
    {
        this.Bind<IWeapon>().To<Sword>();
    }
}

We can use concrete types and automatically inject classes without binding.

Something like this:

class Sword {}

class Samurai 
{
    readonly Sword weapon;
    public Samurai(Sword weapon) 
    {
        this.weapon = weapon;
    }

    public void Attack(string target) 
    {
        this.weapon.Hit(target);
    }
}

Do you think about same style?

  1. It's possible to do same in rust traits?
  2. If not, how to use reflection, may be create some language extensions for it?

zerkalica avatar Apr 12 '17 10:04 zerkalica

That's a good question! So currently resolution is provided by a trait that you implement, which needs a little manual work and doesn't support abstract dependencies, but does resolution at compile-time. That catches any cycles or 'missing' dependencies as early as possible.

From a resolution point of view, you could associate a factory method with a trait bound, and resolve that as a trait object. As for removing the manual implementation, the most idiomatic approach would probably be a custom derive macro, which is about as close as you get to reflection in Rust.

So it seems like all the key pieces are there, but I think there are a few things that could complicate it:

  • Rust is 'explicit' (ie leaks) the way data is stored, because data doesn't live on the heap by default. And to use trait objects you either need a borrowed reference or an owned Box (maybe you can store a trait object behind an Rc too... I haven't actually tried). The alternative is a generic. Either way means your types that you want to inject into need to know how dependencies are stored.
  • Rust doesn't have inheritance, so it's common for trait bounds to cover many possible types. Attempting to resolve a type by a trait bound could be a bit weird

This is just a random dump of my thoughts at this stage :) This repo is just an experiment, so I don't expect anyone to actually use this code.

What do you think?

KodrAus avatar Apr 12 '17 11:04 KodrAus

I think this is a very interesting experiment--thank you for posting not only the repo, but so much of your thinking. It's helpful to gain the benefits of your insight as I go down this same DI/IoC path.

U007D avatar Aug 31 '17 22:08 U007D

No worries @U007D! If you're looking at experimenting in this space too I'd be interested to see where you end up :)

Lately I've been structuring components in my Rust apps a bit like this:

// A component
#[auto_impl(Fn)] // a little utility to automatically impl `GetProductQuery` for all `Fn(GetProduct) -> Result<GetProductResult, QueryError>`
pub trait GetProductQuery {
    fn get_product(&self, query: GetProduct) -> Result<GetProductResult, QueryError>;
}

// Dependency injection
pub fn get_product_query<TStore>(store: TStore) -> impl GetProductQuery
    where TStore: ProductStore
{
    // The actual implementation
    move |query: GetProduct| {
        let ProductData { id, title, .. } = store.get(query.id)?.ok_or("not found")?.into_data();

        Ok(GetProductResult {
            id: id,
            title: title
        })
    }
}

// Dependency resolution
impl Resolver {
    pub fn get_product_query(&self) -> impl GetProductQuery {
        let store = self.product_store();

        get_product_query(store)
    }
}

I've got a little sample app I'm working on at the moment that uses this approach. I plan to throw it up on GitHub this month. The essential bits are:

  • Rust's impl Trait feature lets you hide the implementation of GetProductQuery, so it can't be given an explicit name. You have to treat it generically throughout your codebase
  • Rust's closures automatically generate a structure for you that captures the state you pass in, in this case the TStore dependency. This saves you from having to write a structure to implement GetProductQuery with a bunch of generic types on it
  • Resolver is a concrete type that knows how to construct components without you having to know what their dependencies are. It's a bit service-locator-y, but gets the job done

So you end up with something that separates the concerns of dependency injection, resolution and storage without a lot of magic.

I think there's definitely room for some kind of full-fledged IoC container for Rust that gives you even more flexibility to manage these things with a mix of compile-time and run-time work. Containers make figuring out how to compose your app together much more obvious.

KodrAus avatar Sep 01 '17 02:09 KodrAus

Wow, that is elegant! Thank you for sharing your thinking! My colleague @bgbahoue and I have been thinking about DI off and on for a few months now. He's created a remarkably capable first pass at emulating C#'s AutoFac. You can see it at https://github.com/humanenginuity/shaku.

We also plan to open source this; meanwhile we're wrestling with how many attributes to get rid of; how much pain are we signing up for by heavily depending on procedural macros (unstable), the various limitations of Trait objects (eg. methods returning Self), generics and the inability to generate types at runtime, and so on.

I like how your concept minimizes magic, and subsequently the drama. Looking forward to seeing the sample app!

U007D avatar Sep 03 '17 17:09 U007D