Confused about requiring trait implementation with type alias

I'm writing two traits; IDriven<T> which basically defines a "repository" and IDriving<T> which defines a "controller" (kind of), for an API.

IDriving::create(repository, entity) receives an argument that need to implement ADriving<T> for all entities in the application so it may freely call it's repositories when attempting to create an entity.

How can I constrain the type for repository so that it requires several implementations of IDriven<T> for diffrent types?

This is what I've got so far:

/* --- --- Entities --- --- */
pub struct A();

pub struct B();

/* --- --- Trait definitions --- --- */
pub trait TryIntoEntity<T> {
    fn try_into_entity(&self) -> Result<T, MyError>;
}

pub trait IDriving<T> {
    type Output: From<T>;
    type Error;

    fn create(
        repository: &impl IDriven<A>, // repository should require IDriven<T> for both A and B
        entity: impl TryIntoEntity<T>,
    ) -> Result<Self::Output, Self::Error>;
}

pub trait IDriven<T> {
    type Output: TryIntoEntity<T>;
    type Error;

    fn create(&self, entity: T) -> Result<T, Self::Error>;
}

/* --- --- API --- --- */
pub struct ApiA;

impl IDriving<A> for ApiA {
    type Output = ApiObjectA;
    type Error = MyError;

    fn create(
        repository: &impl IDriven<A>, // Should require IDriven<A> + IDriven<B>
        entity: impl TryIntoEntity<A>,
    ) -> Result<ApiObjectA, MyError> {
        let a_req = entity.try_into_entity()?;
        let a = SvcCreateA::execute(repository, a_req)?;
        Ok(a.into())
    }
}

You just specify both trait bounds. You need parentheses here because of the & though

fn create(
    repository: &(impl IDriven<A> + IDriven<B>),
    entity: impl TryIntoEntity<T>,
) -> Result<Self::Output, Self::Error>;

Though when you have multiple bounds like that it might be easier to use a normal type parameter especially if the number of bounds grows.

fn create<Repo>(
        repository: &Repo,
        entity: impl TryIntoEntity<T>,
    ) -> Result<Self::Output, Self::Error>
    where
        Repo: IDriven<A>,
        Repo: IDriven<B>;

I feel a bit in the dark with this, but found a way that seems to work and that compiles - does this solution really do what I want? Thankful for any further insight and comments.

pub trait IDriving<T> {
    type Output: From<T>;
    type Error;
    type Repository: IDriven<A> + IDriven<B>;

    fn create(
        repository: &Self::Repository,
        entity: impl TryIntoEntity<T>,
    ) -> Result<Self::Output, Self::Error>;
}

That means any implementer of IDriving<T> can only have a single repository type it accepts which must implement those traits, which is different from what you had before where any type could be passed as long as it implemented those traits. Either could work depending on how you intend to use it.

hmm, so in your proposal the constraint is on the "method level" meaning that I can implement the trait for any type, but that the function can't be called without satisfying the type constrain? The difference with my own proposal is that the constrain is "brought up one level" to the trait implementation?

For my usecase - I think every method on the IDriving trait will need access to the repository, and since all the domain-logic is kept out of this layer the api-controller need to be able to hand a reference to the entire repository to a service function (in the domain-logic layer) that may need to read, create or update different associated entities when a service is called. From that I think it feels reasonable to put the constrain on the associated Repository type, to guarantee the domain-logic level full access to each entity?

I'm thinking out loud here, not sure I'm making sense :slight_smile: thanks alot @semicoleon for your example and suggestions. Very interested in understanding the differences and how they translate to my need/application structure.

Correct, with your second version you choose the repository type when you implement IDriving. The way you originally had it meant you didn't need to know what specific repository type was passed to the method, just that it met your requirements.

Neither way is necessarily better, they just have different tradeoffs. It sounds like for your use case the associated type might make more sense if you can make it work.

1 Like

Hey Again :slight_smile:
I'm looking back at this and trying to implement your proposal with the where-clause, but I really dont understand how to make calls to the specific implementations on Repository. I.e.

impl Test {
    fn execute<Repository>(repository: &Repository, script: Script) -> Result<Strategy, DomainError>
    where
        Repository: IDriven<Tag>,
        Repository: IDriven<Script>,
    {
        // This works fine, i think because the argument `script` has type `Script`?
        let r = Repository::create(&repository, script);

        // This throws an error, see below
        let s = IDriven::<Tag>::get_by_id(&repository, 1);
        !unimplemented!()
    }
}

the trait bound &Repository: IDriven<entities::tag::Tag> is not satisfied the trait IDriven<entities::tag::Tag> is not implemented for &Repository

Didn't I specify IDriven<Tag> to be implemented on Repository in the where-clause?

That error message looks like you're passing &&Repository instead of &Repository.

If removing the extra reference doesn't work can you include the current trait definitions?