Why is <T> mandatory in impl<T>?

Hi, experts. This is the code

struct Point2D<T> {
    x: T,
    y: T,
}

impl Point2D<T> {             // fails: T not found in this scope
    fn x(&self) -> &T {
        &self.x
    }
}

T is obviously found in Point2D<T>. It works if written as impl<T> Point2D<T>. Why is that?

What should this code do?

struct T;
impl Point2D<T> {             // succeeds: T is found in this scope
    fn x(&self) -> &T {
        //...
    }
}

You're trying to impl Point2D for T, but the compiler can't tell if T is supposed to be a type or a type parameter. The impl<T> part declares T to be a type parameter. If you just write impl, you're implementing it for a specific type (which must be in scope.)

9 Likes

That makes sense. Thank you.

Generic "types" are not types. They are functions at the level of the type system. A declaration like Point2D<T> produces a type-level function with one input type, T, and an output type. When you apply it to, say, f64, it produces the concrete type struct { x: f64, y: f64 }.

Traits are implemented for types. Implementing a trait for a generic "type" can be considered syntactic sugar for creating many implementations for all concrete types produced by that type-level function. Of course, for the type-level function to be able to produce a concrete type, it needs another concrete type as its input to begin with. Just like a regular, value-level function needs an input value (argument) in order to produce an output (return) value.

Thus, when you impl Trait for Point2D<T>, you have to pass a concrete type to the Point2D type-level function. But if you want to implement the trait for all such cases, then of course you can't just pick one single type, and you can't possibly enumerate all types, either. (It would be majorly inconvenient at least, but also impossible when considering that downstream crates may create new Ts that you have never even dreamt of.) You basically want to express universal quantification instead: "for all concrete types T, let Point2D<T> have the following implementation". But notice that this in turn itself requires a free variable, T, to quantify over. Basically, the impl just passes through the requirement of a concrete type, much like value-level functions can call each other and require arguments to be passed through:

fn foo(x: u64) -> u64 {
    x * 2
}

fn bar(x: u64) -> u64 {
    foo(x) * 3
}

println!("{}", bar(4)); // prints 24

In the above example, I think we can agree you would be majorly upset if any and all occurrences of the letter x would somehow magically refer to the same function argument, and thus bar could call foo(x) without ever declaring that it itself takes an argument named x. Since a name like x is used all over the place, having it be "inferred" magically would be disastrous with regards to code legibility and structure. It would basically amount to having implicit global variables everywhere and uncontrollably.

The same would happen if the <T> free type variable weren't required in generic impls. It would basically be a global type variable. Given that <T> is also used all over the place in generic code, having it inferred implicitly to something "obvious" would be highly ambiguous and have a far-reaching negative impact on code quality.

4 Likes

This is something new to me. Thank you.

How can I create a function at the level of the type system that the "type" is not type but some value? For example:

fn get_n<T>(buffer: &[u8]) -> &[u8;T] {   // fails: &[u8;T] T is not a value
    std::convert::TryInto::try_into(buffer).expect("get_n fails")
}

You need to use const generics:

use std::convert::TryInto;

fn array_from_slice<T, const N: usize>(slice: &[u8]) -> &[u8; N] {
    slice.try_into().expect("oh noes")
}
1 Like

That works. Thanks.

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.