Restricting generics to just a few types

Suppose I have

struct SomeStruct<T> {
    inner: Vec<T>,
}

At least for the time being, I only need T to be one of i64 or String.

Getting the generic T to satisfy trait bounds can be quite a battle. For example, using rkyv, it has to satisfy all sorts of bounds from inside that crate.

To save myself some trouble, I'd like to just tell the compiler "hey, this is only a i64 or String, it can't be anything else", but I don't know of any way of doing that. Here are some things I have considered:

  • The standard advice seems to be to use an enum, but I think that's wrong here as then every element of the Vec would be an enum. This is not what I want, I want the Vec elements to be the same fixed type.
  • I could promote SomeStruct to a trait, and just write a few specific structs with i64 or String. This looks better in this example than it does in real life where I have at least two parameters that I need to do this for. The other problem is that this seems like a temporary solution, because implementing generics for that trait is going to run into some of the same issues.
  • I could write a macro to generate impl of the appropriate method for each case, but this seems pretty elaborate for something so simple.

I feel like there's a more standard way of doing this that I'm missing, any ideas?

No, there isn't anything like that in Rust. This is a common limitation of generics. The best you can do is trait bounds, and the type system will always assume there could be more trait implementations (even if the trait is "sealed" and there can't be).

You could also push that around the Vec, something like:

struct SomeStruct {
    inner: Either<Vec<i64>, Vec<String>>,
}

But either way with enums, anything that takes a SomeStruct has to be prepared to work with all variants, even if that's just to panic on the unexpected case.

1 Like

I'm not sure I would restrict the struct's parameters in the way you're proposing. It's more aligned with Rust's designed to restrict the impl blocks:

struct SomeStruct<T> {
    inner: Vec<T>,
}

impl SomeStruct<String> {
  // String-specific methods here …
}

impl SomeStruct<i64> {
  // i64-specific methods here …
}

This may come with the cost of some redundancy if the two impls have methods that are broadly similar. You can also combine this with an impl<T> SomeStruct<T> block to hold common behaviour, though you can't call into the impl SomeStruct<String> or impl SomeStruct<i64> methods from there because T is not actually constrained to be either String or i64, so this has its own limitations.

Personally, I would choose not to care that T is only ever going to be one of two types.

1 Like

I completely agree with that, but again, the trait bounds can just make life really miserable.

Anyway, considering the feedback here the solution I've settled on is to just write a bunch of macro_rules! to generate what I need for specific types, the macros unrolling to what @derspiny showed above.

Thanks all.

I found it was missing, too, which is why I made the crate trait_gen. In fact, I was wondering if it could be a good suggestion as a little language extension, and I made it to test the concept. But I don't think there's any interest for that.

It's indeed possible to use declarative macros, though I find it harder to write, read, and maintain. On the other hand, it's lighter on the compiler than a procedural macro. The standard library makes extensive use of it.

Small example to illustrate:

struct SomeStruct<T> {
   inner: Vec<T>,
}

#[trait_gen(T -> i64, String)]
impl SomeStruct<T> {
    fn to_str(&self, separator: &str) -> String {
        self.inner.iter()
            .map(|x| x.to_string())
            .collect::<Vec<_>>()
            .join(separator)
    }
}

#[test]
fn test() {
    let a = SomeStruct { inner: vec![10, 20, 30] };
    let b = SomeStruct { inner: vec!["a".to_string(), "b".to_string()] };
    assert_eq!(a.to_str(", "), "10, 20, 30");
    assert_eq!(b.to_str(":"), "a:b");
}

PS: The name comes from the macro being first destined to trait implementation, but it can be applied to other items to substitute types.

2 Likes

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.