How to pass generic to generic in a declaration, beautifully?

Hi! I'm trying to make a data structure that stores an array of items that implement, for example, std::fmt::Display after calling .into() on them. In rust code, I would describe it like so:

struct Smth<T: Into<impl std::fmt::Display>, const N: usize>(pub [T; N]);

However, this is invalid syntax since impl isn't allowed in such cases. So, the other idea is to write:

struct Smth<T: Into<D>, D: std::fmt::Display, const N: usize>(pub [T; N]);

But this gives the following error:

error[E0392]: parameter `D` is never used
 --> src/lib.rs:1:25
  |
1 | struct Smth<T: Into<D>, D: std::fmt::Display, const N: usize>(pub [T; N]);
  |                         ^ unused parameter
  |
  = help: consider removing `D`, referring to it in a field, or using a marker such as `PhantomData`
  = help: if you intended `D` to be a const parameter, use `const D: usize` instead

For more information about this error, try `rustc --explain E0392`.

Following the compiler's recommendation, I can make it work by storing std::marker::PhantomData inside struct:

struct Smth<T: Into<D>, D: std::fmt::Display, const N: usize>(pub [T; N], pub std::marker::PhantomData<D>);

However, this approach requires me to provide PhantomData when constructing the struct. But I wanted to make it sort of DSL, to be used like so:

Smth([
    smth_else,
    Text("Hello, World!"),
    Smth([
        Text("etc")
    ])
])

And recommended approach is going to ruin this aesthetic.

I'll go with dyn std::fmt::Display anyway, because of other reasons, but I still wonder if there is a more beautiful way to solve this issue.

If you could specify a type T: Into<D>, D: Display, then many types T will have have multiple possible types D and no way to specify which D you want. E.g. u8 implements Into for u16, u32, u64, etc as well as any number of types from libraries, and there is no way for the caller of your function to know which type D is actually wanted. So that is not legal.

3 Likes

Depending on the use case, something like T: Into<Box<dyn Display>> might work, at the cost of heap-allocating each item.

Incidentally IIRC this is what format!() and friends do behind the screens, so if using that would be fast enough for the use case, then most likely this would be too.

I'm pretty sure that format_args!() doesn't do any heap allocations, though it might coerce things to &dyn Display. Whatever it's doing, its result (fmt::Arguments<'a>) has an embedded lifetime that prevents you from returning the value from a function.

1 Like

The thing is, I can do that. I just have to refer to D inside a struct.

And if it can't infer which type I'm talking about, I can specify it like so:

Smth::<_, String, 1>(["Hello, World"], std::marker::PhantomData);

(const arguments cannot yet be inferred with _ )

link to playground

Yeah, that's what I'll go with. Just wondering whether there is a way to go without PhantomData.

A couple of points:

  1. As long as you keep D a type argument to Smth<T, D, N>, the answer to that one is no.

  2. However, you might be able to get away with struct Smth<T: Into<Box<dyn Display>>, const N: usize>(pub [T; N]);

That said, at this point in time putting type parameter bounds on the type itself is considered unidiomatic (this may change in the future).

So why not write

struct Smth<T, const N: usize>(pub [T; N]);

impl<T: Into<Box<dyn Display>>, const N: usize> Smth<T, N> {
    // any functionality you need for the version of `Smth` where the array elements are `Display`able
}

instead?

1 Like

Thanks, I just wasn't entirely sure since this limitation seemed strange.

Why unidiomatic, tho? I would expect to get the error when constructing the struct with an unsupported type rather than when calling a method that wasn't designed to handle it. I guess that not specifying type bounds can make sense on a struct with private fields. Because they will be specified anyway in the impl blocks where constructors are. But this can lead to some constructor storing a thing of one type while methods expect another, can't it? If they're in different impl blocks ofc.

Because it's an unnecessary constraint that causes unforeseen burden in downstream code.

There is no reason to actively prevent the construction of a wrapper type just because the inner value doesn't satisfy some trait bounds. If you do that, other 3rd-party code using your type will get a surprise compiler error if they try to merely create a value. That is undesirable because you can't envision every possible way one might want to (legitimately) use your type in the future.

Put bounds only on the methods/impl blocks where you actually need some functionality provided by the trait(s) you require in the interface.

4 Likes

Another reason is that the bounds can run into precisely what you're describing, where it can be easier when the bounds are on the functions.

Compare

struct Foo<T, D : Into<T>> { ... } // Needs Phantom

impl<T, D: Into<Y>> Foo<T, D> {
    fn frobnicate(self, d: D) { ... }
}

with

struct Foo<T> { ... } // No Phantom

impl<T> Foo<T> {
    fn frobnicate<D: Into<To>>(self, d: D) { ... }
}

In this case the user may well never need to write down a type bound, because they just provide the right d of the right type. It's less stringent than of the type were hardcoded to only frobnicate work one type, but if you really not storing anything of type D why impose that restriction?

4 Likes

It sounds like you are really after an elastic tuple struct or cons list type container which can contain any possible Sized objects of various types which can perform a common trait. Defining a struct as you have implies that all of the contained objects are of just one singular type T (one known size).

Whereas you go on to try and call creation of it with multiple types [T1, T2, T3 (holds nested T2 again)]:

What it sounds like you are after would need to be implemented something like this: playground

2 Likes