Why do we need to add the generic type two times after impl?

In this code

mod complex {
    pub struct Complex<Num> {
        re: Num,
        im: Num,
    }

    impl<Num> Complex<Num> {
        pub fn from_re_im(re: Num, im: Num) -> Self {
            Self { re, im }
        }

        pub fn re(&self) -> &Num {
            &self.re
        }

        pub fn im(&self) -> &Num {
            &self.im
        }
    }

    impl<Num> std::ops::Add for Complex<Num>
    where
        Num: std::ops::Add<Output = Num>,
    {
        type Output = Self;

        fn add(self, rhs: Self) -> Self::Output {
            Self {
                re: self.re + rhs.re,
                im: self.im + rhs.im,
            }
        }
    }
}

fn main() {
    use complex::Complex;
    let z1 = Complex::from_re_im(3.4, 5.8);
    let z2 = Complex::from_re_im(1.2, 3.7);
    let z3 = z1 + z2;
    println!("{} + {}i", z3.re(), z3.im());

    let z1 = Complex::from_re_im(3, 5);
    let z2 = Complex::from_re_im(1, 3);
    let z3 = z1 + z2;
    println!("{} + {}i", z3.re(), z3.im());
}

Why is the Num generic type added two times after impl here

impl<Num> Complex<Num>

You can add functions generically,

Impl<T> Complex<T> { ... }

Or specifically

Impl Complex<f32> { ... }

In a given situation, the compiler checks all impl's to find a method.
Note that you can have multiple impl's for a single type, which even can be distributed over many files.

2 Likes

You have to declare type variables before you use them, and impl<_> is where you declare them for implementations. One reason is that you can introduce type variables in a nested context,

impl<T> Foo<T> {
    fn bar<U>(_: U) {
    }
}

and if type variables could could be used without being declared, that could be written

impl Foo<T> {
    fn bar(_: U) {
    }
}

but then it is very easy to mistake that for, or accidentally code,

impl Foo<T> {
    fn bar(_: T) {
    }
}

where the type variables are no longer independent.


There was an accepted RFC to allow that for lifetimes, but eventually that part of the RFC got scrapped.

2 Likes

I still can't understand it. Why can't we just do it like that:

Impl Complex<T> { ... }

What is the difference between the 2 occurrences of the T ?

I asked ChatGPT about it and it gave me this:

When you see impl<Num> Complex<Num> in Rust code, it means that the type Complex is generic over a type parameter Num. The <Num> in impl<Num> and Complex<Num> means that Num is a type parameter that is used throughout the implementation of Complex.

The first Num refers to the generic type parameter of the impl block, which specifies that the implementation is generic over a type Num.

The second Num refers to the Num type parameter of the Complex struct. This specifies that the re and im fields of the Complex struct are of the same generic type Num.

Together, these two Nums specify that the Complex struct and its associated methods are generic over a type Num. This means that you can create a Complex instance using any type that implements the necessary operations, such as i32, f64, or even a custom type.

But i still can't comprehend it

To prevent adding struct T { ... } somewhere change the meaning of the code.

3 Likes

the form Complex<Num> consumes Num, it does not introduce the argument Num, it assumes Num is a valid generic argument. consider:

// the following is not a generic implementation, it is a implementation for the type `MyNumber`
struct MyNumber {}
impl Complext<MyNumber> {}

// the following, however, is a generic implementation
// typically, you add trait bounds to the generic parameter `MyNumber`
impl<MyNumber> Complex<MyNumber> {}
2 Likes

There's no syntactical difference between a type variable and a named type. If the type variable didn't need to be declared, then the compiler couldn't know whether you meant a concrete type or a type variable. (Or at the very least, it would be ambiguous and result in code that's easy to break.)

An analogue with regular functions would be:

fn foo() {
    let x = 1;
    bar();
}

fn bar() {
    println!("{}", x);
}

Would you argue here that the argument x should not be declared in bar?

1 Like

Let me see if I can try a different tack. The generics are often named the same by convention, but don't have to be. Consider:

struct Foo<T>{
    val: T
}

Impl <R> Foo<R> {
    fn bar(val: R) {}
}

This is functionally equivalent as when the names are the same, but this should make it clear that the two distinct generics serve different purposes.

T makes it so that the struct can store any type. R makes it so that functions declared in this impl block can reference values of any type, matching the generic contained in foo.

The reason this distinction is important is because, as others have mentioned in this thread, you don't have to use generics for the impl block. Imagine you have a trait you want to implement for a struct with a generic, but you need to have custom logic depending on the actual type contained in the variable.

trait Cost{
    fn pretty_cost(&self) -> String;
}
struct USA{
    cost: f32
}
struct Europe {
    cost: f32
}
struct Foo<T> {
    val:T
}

impl Cost for Foo<USA> {
    fn pretty_cost(&self) -> String {
        format!("${}", self.val.cost)
    }
}
impl Cost for Foo<Europe> {
    fn pretty_cost(&self) -> String {
        format!("€{}", self.val.cost)
    }
}

The alternative of using a generic in the impl gives less flexibility for creating custom behavior for types, but the up side is you only have to write one implementation.

Also this was written on my mobile phone, apologies if there are typos.

4 Likes

Thank you all for your help and support.

This also helped me Why does Rust require generic type declarations after the "impl" keyword? - Stack Overflow

Also a relevant link: impl<T> Drop for List<T> why need two T - language design - Rust Internals

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.