Associated types with an unknown number of type parameters

I have a question about traits. The below example parses some arbitrary data that is specified by how an associated type is defined in a trait implementation. You can run it here.

use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde_json::json;

pub trait MyTrait<T> {
    type MyType: DeserializeOwned;

    fn parse(&self, content: String) -> Self::MyType {
        serde_json::from_str(content.as_str()).unwrap()
    }
}
pub struct MyStruct {}

#[derive(Debug, Deserialize)]
pub struct MyResponse<T> {
    pub data: T,
}

#[derive(Debug, Deserialize)]
pub struct MyData {
    pub value: u64,
}

impl<T: DeserializeOwned> MyTrait<T> for MyStruct {
    type MyType = MyResponse<T>;
}

fn main() {
    let content = json!({"data": {"value": 42}}).to_string();
    let my = MyStruct {};
    let result: MyResponse<MyData> = my.parse(content);
    dbg!(result);
}

My question is in regards to MyTrait. It's quite possible that the value given to MyType can have any number of generic parameters required (i.e. MyResponse<A, B>). In the above case, it requires just a single parameter, however, what if it had three parameters? Would my only option be defining the trait like this?

pub trait MyTrait<X, Y, Z> {
    type MyType: DeserializeOwned;

    fn parse(&self, content: String) -> Self::MyType {
        serde_json::from_str(content.as_str()).unwrap()
    }
}

What if I had five type parameters? I guess I'm trying to figure out the correct way to pass type parameters to an associated type in a trait without having to know the exact number of parameters ahead of time.

In its full generality, this requires two features which are non-existent and unstable today: variadic generics and associated type parameters, respectively.

However, it looks like your associated type isn't even actually parameterized in the example above. So, depending on how you use the types and how you implement your trait, you may be able to defer handing all of the parameters to impl-time:

trait NotGeneric {
    type Assoc;
}

impl<T> NotGeneric for OneGeneric<T> {
    type Assoc = AssocOneGeneric<T>;
}

impl<X, Y> NotGeneric for TwoGenerics<X, Y> {
    type Assoc = AssocTwoGenerics<X, Y>;
}

Can you show what OneGeneric and TwoGenerics would look like? With the example I gave in mind, MyStruct doesn't take any type parameters and adding them wouldn't compile since they are never used in the body. Unless there's something I'm not understanding - I am still a novice with Rust.

If your types aren't generic, what do you need the generic arguments for? What is it you are trying to do at a higher level?

The example is fairly close to my actual implementation except that MyTrait is doing a lot more under the hood. Specifically, it's executing some HTTP requests and then parsing the result using serde.

The problem is that implementations of MyTrait may only know some partial amount of the response. In the given example, the implementation of MyStruct knows the response will contain some structure that looks like MyResponse but what MyResponse contains can only be known when the parse function is called.

This case may vary - for example the implementation may know the entire structure ahead of time and not need to rely on any generics. My goal was to allow the trait to be flexible enough to support these different use-cases without having to define a trait for each case.

Based on perusing this thread, it appears the solution in the context of the example is to use PhantomData.

use std::marker::PhantomData;

use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde_json::json;

pub trait MyTrait {
    type MyType: DeserializeOwned;

    fn parse(&self, content: String) -> Self::MyType {
        serde_json::from_str(content.as_str()).unwrap()
    }
}
pub struct MyStruct<T> {
    data: PhantomData<*const T>,
}

#[derive(Debug, Deserialize)]
pub struct MyResponse<T> {
    pub data: T,
}

#[derive(Debug, Deserialize)]
pub struct MyData {
    pub value: u64,
}

impl<T: DeserializeOwned> MyTrait for MyStruct<T> {
    type MyType = MyResponse<T>;
}

fn main() {
    let content = json!({"data": {"value": 42}}).to_string();
    let my = MyStruct { data: PhantomData };
    let result: MyResponse<MyData> = my.parse(content);
    dbg!(result);
}

This allows passing the generic to the struct without it actually being used in the body. It's quite the eyesore and I agree with the rhetoric in the thread that it would make more sense to me that the compiler would see T being "used" in the implementation of the associated type. Unfortunately, I'm not sure of a better way around this.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.