Implement generic trait with multiple trait bounds

I'm not sure if what I'm trying to do is possible.
I want to implement a method on the types i8, i16, i32, i64, and i28.
I thought I could do this by relying on the Eq and Ord traits.
I can get the code below to work if it is not generic and just targets one of the integer types.
Maybe I'm close to a solution. Can you see where I'm going wrong?

use std::cmp::{Eq, Ord};

trait Days<T: Eq + Ord> {
    fn days_from_now(self) -> String;
}

impl<T: Eq + Ord> Days<T> for T {
    fn days_from_now(self) -> String {
        let s = match self {
            -1 => "yesterday",
            0 => "today",
            1 => "tomorrow",
            _ => if self > 0 { "future" } else { "past" }
        };
        s.to_string()
    }
}    

fn main() {
    let days: i32 = -1;
    println!("{}", days.days_from_now()); // yesterday
    println!("{}", 0.days_from_now()); // today
    println!("{}", 1.days_from_now()); // tomorrow
    println!("{}", 2.days_from_now()); // future
    println!("{}", (-2).days_from_now()); // past
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:11:13
   |
8  | impl<T: Eq> Days<T> for T {
   |      - this type parameter
9  |     fn days_from_now(self) -> String {
10 |         let s = match self {
   |                       ---- this expression has type `T`
11 |             -1 => "yesterday",
   |             ^^ expected type parameter `T`, found integer
   |
   = note: expected type parameter `T`
                        found type `{integer}`

error[E0308]: mismatched types
  --> src/main.rs:12:13
   |
8  | impl<T: Eq> Days<T> for T {
   |      - this type parameter
9  |     fn days_from_now(self) -> String {
10 |         let s = match self {
   |                       ---- this expression has type `T`
11 |             -1 => "yesterday",
12 |             0 => "today",
   |             ^ expected type parameter `T`, found integer
   |
   = note: expected type parameter `T`
                        found type `{integer}`

error[E0308]: mismatched types
  --> src/main.rs:13:13
   |
8  | impl<T: Eq> Days<T> for T {
   |      - this type parameter
9  |     fn days_from_now(self) -> String {
10 |         let s = match self {
   |                       ---- this expression has type `T`
...
13 |             1 => "tomorrow",
   |             ^ expected type parameter `T`, found integer
   |
   = note: expected type parameter `T`
                        found type `{integer}`

error[E0369]: binary operation `>` cannot be applied to type `T`
  --> src/main.rs:14:26
   |
14 |             _ => if self > 0 { "future" } else { "past" }
   |                     ---- ^ - {integer}
   |                     |
   |                     T
   |
help: consider further restricting this bound
   |
8  | impl<T: Eq + std::cmp::PartialOrd> Days<T> for T {
   |            ^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to 4 previous errors

Some errors have detailed explanations: E0308, E0369.
For more information about an error, try `rustc --explain E0308`.
error: could not compile `playground`

To learn more, run the command again with --verbose.

1 Like

First, I don't think Days needs a generic parameter. You probably want just trait Days { ... } instead.

Second, this line (modified to remove the generic parameter as I suggested)

impl<T: Eq + Ord> Days for T

means "I am implementing Days for every type T that already implements Eq and Ord". But this is not what you mean: you just want to implement Days for i8, i16, i32, i64, and i128. You don't want to implement it for, say, Vec<u8>, which does implement Eq and Ord.

I think what you want is not a generic impl but a macro_rules! macro, which will let you write something like

implement_days! {i8}
implement_days! {i16}
// etc.

Here's an attempt at that:

macro_rules! implement_days {
    ($t:ty) => {
        impl Days for $t {
            fn days_from_now(self) -> String {
                let s = match self {
                    -1 => "yesterday",
                    0 => "today",
                    1 => "tomorrow",
                    _ => if self > 0 { "future" } else { "past" }
                };
                s.to_string()
            }
        }
    };
}

(This is almost copy-pasted from something in one of my own projects. It's a good technique to have in your toolbox.)

3 Likes

Sometimes num-traits are what you want in order to work with numerical types generically. But often what you really want is a macro approach as @cole-miller has suggested. Given your example, I feel your use case is in the macro camp.

2 Likes

I'm fine with using a macro do this. But really I'm just trying to learn what is possible with generics. Is it fair to say using a macro is the only way to implement a function or method that works with all integer types and only with integer types?

Rust does not have "all integer types and only integer types" as a primitive concept. For a blanket impl like this you can either pick your own closed set of types ("i8, i16, ... , i128, and that's it") and write a macro, or you can find an (inherently open) trait, like num_traits::PrimInt or num_traits::Signed, that encompasses what you have in mind.

3 Likes

The practical answer to the question you're almost certainly asking is "yes, use a macro". Something like num-traits can do it, but isn't the most ergonomic, and if you don't really need to be generic it's probably not worth it. And if you wanted to close off the implementation options too, you could define your own sealed trait that builds off of num-traits and only implement it for the types you want... but why bother? Just macro it up for ease of writing and probably faster compiling.

You may be wishing for something like "if I could confine this generic function to integers, Rust will let me do integer-like things (such as compare against 1, 0, and -1; assume all the standard operators; etc)." There is no such functionality in Rust (not for integers, and more generally not for some arbitrary set of types and their operations/abilities). You only get what the trait bounds specify.

2 Likes

Note that there is no trait for "can be represented by an integer literal" -- the compiler doesn't support plugging non-primitive types into that behavior in the way it supports using + with non-primitive types via std::ops::Add.

1 Like

Is there a way to iterate over a collection of types similar to this which does not work?

const types: [type; 5] = [i8, i16, i32, i64, i128];
for t in types {
    implement_days! {t}
}

You can put a repetition in the macro definition (untested):

macro_rules! implement_days {
    ($($t:ty),*) => { $(
        impl Days for $t {
            fn days_from_now(self) -> String {
                let s = match self {
                    -1 => "yesterday",
                    0 => "today",
                    1 => "tomorrow",
                    _ => if self > 0 { "future" } else { "past" }
                };
                s.to_string()
            }
        }
    )* };
}

implement_days! { i8, i16, i32, i64, i128 }
2 Likes

for is strictly a run-time construct, you can't do metaprogramming with it (or with any language mechanism other than macros -- that includes const arrays).

No, that certainly isn't true. Here's a demonstration proving my claim. However, it is likely that doing so is completely missing the point of generics.

You can certainly create a private trait that you only implement for integer types. Your immediate problem of trying to match a universally quantified type with an integer literal can be overcome by providing explicit conversion to/from primitive types in the same trait.

Yet, the essential value of generics is that they allow reasoning about any set of types by means of describing their properties, rather than expecting a bunch of concrete types. For example, unlike in other languages, in Rust, you shouldn't write a print() function overloaded for every possible printable type in Rust. Instead, you should write it so that it works with any type T that implements T: Display.

4 Likes

That works with anything that implements Eq, Ord, and From<i8>, not only with integer types</pedant>.

Yes, and that's exactly my point. OP probably shouldn't try to artificially constrain trait impls to primitive types, neither do I want to provide such an example, it's completely intentional.

1 Like