Optional Traits

I'm relatively certain that the answer here is "You can't do that", but on the off-chance I thought I'd ask :slight_smile:

Is there any way that I can have a function that takes arbitrary types, and if that type implements some trait then it acts slightly differently?

Actual code. This is what I'm wanting to do:

impl<O, I, D> From<Resource<I, D>> for Respondable<O>
where
    O: Serialize,
    Resource<I, D>: Into<O>,
{
    fn from(resource: Resource<I, D>) -> Self {
        let mut result = Self::new(resource.into());

        // If resource implements the ResourceCacheControl trait, then use that to add a Cache-Control header to the result.

        result
    }
}

I tried creating a standard implementation impl<I, D> ResourceCacheControl for Resource<I, D>, figuring that I could then create an implementation for the more specific impl ResourceCacheControl for Resource<UserId, UserData>, but that didn't work - instead failing with "conflicting implementations of trait". (I'm not surprised that happened, but was worth a try :slight_smile: )

I know I can just write two versions of the method - one with a generic bound that binds to + ResourceCacheControl and one that binds to + !ResourceCacheControl - but that doesn't scale as I add more such traits. (I want at least to have ones for ETag and Status Code as well, so that's already 9 combinations!)

The only other way I can think of is to just give the trait a default implementation and then require an empty impl ResourceCacheControl for Resource<UserId, UserData> {} for every resource type. Which works, but isn't really ideal.

Is there some better way that this can be achieved?

Cheers

I'm pretty sure you can do it with the unstable specialization feature, but not in stable Rust.

Could you give more details on the traits ResourceCashControl and Resource? What kinds of methods are there? Maybe you could combine them into a single trait with some default method implemenations on the caching-related methods.

@steffahn Sure thing:

Resource is my general purpose data that is returned by any service layer within my application (Not an HTTP service, but a business service):

pub struct Identity<I> {
    pub id: I,
    pub version: Uuid,
    pub created: DateTime<Utc>,
    pub updated: DateTime<Utc>,
}
pub struct Resource<I, D> {
    pub identity: Identity<I>,
    pub data: D,
}

My Respondable<T> type is something that can be used as an Actix response, and knows how to correctly marshal itself, including payload and headers.

What I'm doing here is a general setup for converting Resource into Respondable, such that all you need to write the interesting bits. You need to convert the Resource into the correct payload. You might want to have additional overrides for cache control, etag, etc but generally you don't - they are special cases.

So what I was hoping for was some way that I could implement traits like ResourceCacheControl only where needed, and if it's not implemented then default behaviour is used instead.

I've currently got the route of having a single ResourceResponse trait with default implementations, and then the absolute minimum you need to write is:

impl From<Resource<UserId, UserData>> for UserModel {
    ....
}
impl ResourceResponse for Resource<UserId, UserData> {}

Which works, but it's a shame I can't get rid of that one last line in cases where it's really not needed.

Ah, wait, I got it all mixed up in my head. For some reason my brain thought that Resource is a trait. Sorry if my previous comment was confusing. I’m reading the rest of your response now…

Alright. I feel like I’ve already come across “empty” trait impls like your

myself that I wasn’t able to get rid of without features like specialization.

Anyways, I would like to understand your setting better. Is the ResourceResponse trait an alternative to the From<Resource<I, D>> for Respondable<O> implementation? Does your current approach not have any ResourceCacheControl either but that trait is supposed to make some forms of what’s currently custom ResourceResponse implementations easier?

1 Like

My original desire was to have traits roughly like this:

pub trait ResourceCacheControl {
    fn cache_control(&self) -> Vec<CacheDirective>;
}

With ones for CacheControl, ETag, and StatusCode - and maybe more in the future.

And then if a particular Resource<> implemented this trait then the Cache-Control headers would be added to the response. That didn't work.

My current approach is to have a single trait like this:

pub trait ResourceResponse {
    fn status_code(&self) -> StatusCode {
        StatusCode::OK
    }

    fn etag(&self) -> Option<String> {
        None
    }

    fn cache_control(&self) -> Option<Vec<CacheDirective>> {
        None
    }
}

So each Resource<> must implement this, but in order to do so it doesn't actually need to do anything more than the empty block.

I'm fairly certain from what both yourself and Alice have said that this is the best I'm going to get without Specialization, and it does work. It's just a shame to need that one empty block instead of being able to avoid it entirely.

1 Like

Just out of curiosity, was this conditional application going to have to use a if/then clause? I ask because in terms of conveying the intent, the workflow, despite the redundant nature of sometimes having a dummy call, at least you are dealing with a unified, single, non-branched process.

Another thought, how about passing-in the function you want to use depending on the type? Where you don’t want anything to happen pass-in the identity function. That might be another way to get the different behaviors without the narly if/then branching.

One last thought, the above involve traits to unify your types, I suppose you eliminated an enum for the same scaling issue? Depending on the how the trait solution plays out, it might be a case of “six, half dozen”?