Basic usage of generic traits

Hello everyone!

Rust newcomer trying to get up to speed in regards to traits. Apologize in advance for the (most probably) very dumb question :slight_smile:

Consider this toy example:

use num::{pow};

trait HasPower {
    type Output;
    fn power(&self, exp: usize) -> Self::Output;
}

struct NumWithPower<T> {
    num: T
}

impl<T> HasPower for NumWithPower<T> {
    type Output = T;
    fn power(&self, exp : usize) -> Self::Output {
        pow(self.num, exp) // error
    }
}

fn main() {
    let a = NumWithPower{num:5};
    let b = NumWithPower{num:5i64};
    let c = NumWithPower{num:5f64};
    println!("{}", a.power(5));
    println!("{}", b.power(5));
    println!("{}", c.power(5));
}

The compiler is kindly hinting to me that the T does not implement the Clone and One traits:

error[E0277]: the trait bound `T: Clone` is not satisfied
error[E0277]: the trait bound `T: One` is not satisfied

Coming from C++, I would expect this error to come up at the time of using the specific power function with types that do not satisfy those traits (e.g. a String), while here it's erroring out at the definition stage, regardless of the use of the function. In fact, if I explicitly restricts the boundaries of T, the code compiles fine:

impl<T: One + Clone + Copy> HasPower for NumWithPower<T> {
    type Output = T;
    fn power(&self, exp : usize) -> Self::Output {
        pow(self.num, exp)
    }
}

Wouldn't the usage of the power call be enough to determine the boundaries of the T in this case? Am I missing some Rust gotcha?

Thanks!

You're missing that Rust has explicitly rejected the C++ template generics. Rust generics are fully typed at definition time, so that post-monomorphization inference and errors are (mostly) impossible. To the first approximation, you must explicitly specif generic bounds for any property or methods that you need, and you can't use anything which is not specified at definition time.

This means that if you call a generic method foo, then you must use at least as strong where-bounds on your function as on foo itself (or call it with concrete types).

4 Likes

That's what I thought. Glad to have it confirmed.

So, would something like this be ideomatic Rust?:

use num::{pow, One, Zero};

// Trick to implement trait aliases without experimental
trait CopyableNumber: One + Zero + Clone + Copy {}
impl<T: One + Zero + Clone + Copy> CopyableNumber for T {}

trait HasPower {
    type Output;
    fn power(&self, exp: usize) -> Self::Output;
}

struct NumWithPower<T> {
    num: T
}

impl<T: CopyableNumber> HasPower for NumWithPower<T> {
    type Output = T;
    fn power(&self, exp : usize) -> Self::Output {
        pow(self.num, exp) // errpr
    }
}

fn main() {
    let a = NumWithPower{num:5};
    let b = NumWithPower{num:5i64};
    let c = NumWithPower{num:5f64};
    println!("{}", a.power(5));
    println!("{}", b.power(5));
    println!("{}", c.power(5));
}

The CopyableNumber trick looks very ugly, but I guess it does the job until trait aliases are merged to master?

I'd probably impl HasPower directly on T instead of going through NumWithPower.

In general, this is how the story with numerics goes in Rust unfortunately. Trait aliases will hopefully lessen this burden :slight_smile:

But also, CopyableNumber doesn't seem to be used anywhere else, so why not simplify it so that power is part of CopyableNumber (and maybe rename it)?

2 Likes

I'm not sure I get what you mean, can you show me an example?

Yeah, hopefully so

Yeah, that's also an option. I'm just making this toy examples to understand the trait system better

Sure!

impl<T: CopyableNumber> HasPower for T {
    type Output = T;
    fn power(&self, exp: usize) -> Self::Output {
        pow(self, exp)
    }
}

(This can't possibly compile because CopyableNumber says nothing about Mul, but once that's been added, all's well).

Great! Ended up with this, which covers this basic usage:

use num::{pow, One, Zero};

// Trick to implement trait aliases without experimental
trait CopyableNumber: One + Zero + Clone + Copy {}
impl<T: One + Zero + Clone + Copy> CopyableNumber for T {}

trait HasPower {
    type Output;
    fn power(&self, exp: usize) -> Self::Output;
}

// Impl for generic number
impl<T: CopyableNumber> HasPower for T {
    type Output = T;
    fn power(&self, exp: usize) -> Self::Output {
        pow(*self, exp)
    }
}

struct MyNum<T: CopyableNumber> {
    num: T
}

// Impl for custom struct
impl<T: CopyableNumber> HasPower for MyNum<T> {
    type Output = T;
    fn power(&self, exp : usize) -> Self::Output {
        pow(self.num, exp) // errpr
    }
} 

fn main() {
    // Generic numbers
    let a = 5;
    let b = 5i64;
    let c = 5f64;4;
    println!("{}", a.power(5));
    println!("{}", b.power(5));
    println!("{}", c.power(5));

    // Custom struct
    let a = MyNum{num:5};
    let b = MyNum{num:5i64};
    let c = MyNum{num:5f64};
    println!("{}", a.power(5));
    println!("{}", b.power(5));
    println!("{}", c.power(5));
}