audunhalland / entrait

Loosely coupled Rust application design made easy
86 stars 3 forks source link

REAL dependency inversion: Support internal dispatch by using a pair of traits #8

Closed audunhalland closed 2 years ago

audunhalland commented 2 years ago

TL;DR: The crate that defines an interface should not need to be the crate that implements it. The way entrait is designed, using delegating blanket implementations, requires some more delegation "magic".

The entrait-for-trait feature #[entrait(delegate_by = Borrow)] only works for leaf dependencies.

Here is an idea for how to do something similar, but without exiting the Impl layer.

We have some API that we want to potentially implement (downstream) in different dynamic ways:

pub trait Facade {
    fn foo(&self) -> i32;
}

We want to implement this trait for Impl<T> so it can be used as a dependency. But that, by definition, is the only implementation. We want to have a delegation through dynamic dispatch to reach the final destination. Since that trait is already implemented for Impl<T>, we need another trait! We can use the entrait syntax to generate a trait from a trait 😬 Let's call the new trait Backend:

#[entrait(pub Backend)]
pub trait Facade {
    fn foo(&self) -> i32;
}

The generated trait is generic, and has two receivers:

pub trait Backend<T>: 'static {
    fn foo(&self, _impl: &entrait::Impl<T>) -> i32;
}

The generated delegation looks like this:

impl<T: 'static> Facade for Impl<T>
where
    T: core::borrow::Borrow<dyn Backend<T>>,
{
    fn foo(&self) -> i32 {
        self.borrow().foo(self)
    }
}

Now the application has to implement Borrow<dyn Backend<Self>>. This is the manual part.

To define an implementation of Backend<T>, we can write the following module:

pub struct MyImpl;

#[entrait_impl(some_crate::Backend for MyImpl)] <--- proposed syntax
#[entrait_impl(dyn some_crate::Backend for MyImpl)] <--- dyn version
mod my_impl {
    fn foo(deps: &impl Whatever) -> i32 {
         42
    }
}

The functions inside this module need to match the backend interface. I think the compile errors will be good enough. The implementation is of course auto-generated:

pub struct MyImpl;

impl<T> Backend<T> for MyImpl
where
    Impl<T>: Whatever,
{
    fn foo(&self, _impl: &Impl<T>) -> i32 {
        foo(_impl)
    }
}

The Borrow part of the application could either be a Box or some enum. Avoiding allocation could look like this:

impl Borrow<dyn facade::Backend<Self>> for App {
    fn borrow(&self) -> &dyn Backend<Self> {
        match &self.facade_impl {
            AppFacadeImpl::My(my_impl) => my_impl,
            AppFacadeImpl::Other(other_impl) => other_impl,
        }
    }
}
audunhalland commented 2 years ago

Note: This is also possible to do using static dispatch. ~It's much simpler~, and the Backend trait is just a verbatim copy of the Facade trait. The Facade implementation very simply delegates with a Self: Backend bound. The only constraint is that there can only be one implementation of Backend for Impl<T>.

Static:

#[entrait(pub Backend)]
pub trait Facade {}

Dynamic:

#[entrait(pub dyn Backend)]
pub trait Facade {}

Impl modules:

#[entrait_impl(Backend)]
#[entrait_impl(pub MyImpl: dyn Backend)]

Edit: This could be problematic because of coherence rules. I.e. we cannot implement Backend for Impl<T> in a crate that is downstream to both :(

So the implementing crate has to introduce a new type into the mix, just like in the dyn example. The application has to statically "link" this type in via implementing an associated type on some entrait-trait.

audunhalland commented 2 years ago

The only solution I found (so far) that allows static dispatch is this, which involves 3 traits:

use implementation::Impl;

pub trait Facade {
    fn foo(&self) -> i32;
}

// Implement in intermediate layer:
pub trait FacadeImpl<T> {
    fn foo(__self: &Impl<T>) -> i32;
}

// Implement in app layer ("main.rs")
pub trait FacadeSelectImpl<T> {
    type Impl: FacadeImpl<T>;
}

// Delegation - implemented here, in "core"
impl<T> Facade for Impl<T> where T: FacadeSelectImpl<T> {
    fn foo(&self) -> i32 {
        <T as FacadeSelectImpl<T>>::Impl::foo(self)
    }
}
audunhalland commented 2 years ago

Solution exploration in this repo: https://github.com/audunhalland/entrait-dependency-inversion. There is a way to do it using one trait, but use custom smart pointers.

audunhalland commented 2 years ago

Implemented in #10 .

The final syntax is

#[entrait(TraitImpl, delegate_by = DelegationTrait)]
trait Trait {
    fn foo(&self);
}

I was not able to get zero-cost futures working when delegating using an alternative implementation of the original trait, .e.g. for SomeImplRef<'s, T>(&'s Impl<T>). I wasn't able to make the resulting future implement Send.

The solution is to generate the TraitImpl, consisting of static methods:

trait TraitImpl<T> {
    fn foo(_impl: &Impl<T>);
}

The implementation block looks like:

#[entrait_impl]
mod my_impl {
    pub fn foo(deps: &impl Any) {
    }

    #[derive_impl(super::TraitImpl)]
    pub struct MyImpl;
}

We choose the implementation by writing:

impl DelegationTrait<Self> for App {
    type Target = my_impl::MyImpl;
}
audunhalland commented 2 years ago

Released in 0.4.4