Question about why generics are implemented asymmetrically in structs and methods


#1

Currently generics are implemented asymmetrically like this:

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

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

Instead of symmetrically like for example this:

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

impl<T> Point: T {
    fn x(&self) -> &T {
        &self.x
    }
}

fn<T> largest(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

Is there a practical reason they are implemented differently (and in particular why method syntax looks redundant), or is the reason for this historic?


#2

The key difference is that Point<T> in struct Point<T> introduces a type variable, while in impl<T> Point<T> it’s being used, in the same way that you need to write

fn consume<T>(_: Point<T>) {}

You put the <..> bit after the name of the item being introduced. impls all have the same “name”, i.e., the empty string, so you just write it after the keyword.

Another key reason for defining paramters before using them in an impl is that it allows the following:

struct K<T, U> { .. }

impl<U, T: Trait<U>> K<T, U> {}
impl<T, U> K<Vec<(T, U)>, ()> {}
impl<'a, 'b: 'a, T> K<&'a T, &'b T> {}

Now, if you ask me, it would be nice to be able to write impl Type<Params..> for the “obvious” desugar, but this has a nasty ambiguity problem!

struct K<T> { .. }

impl K<T> {} 
// desugar to
impl<T> K<T> {} //... right?

use foo::T;
impl K<T> {}
// does this still mean
impl<T> K<T> {}
// or
impl K<foo::T> {}

This sort of semantic change caused by use is unacceptable, imo.


#3

In that case it seems like it seems like it would be more clear to specify impl this way to mirror function definitions

impl<T> K: T {}

instead of

impl<T> K<T> {}

using two <T> to mean different things on the same line is confusing to look at

Note: I know any sort of change on this level wouldn’t make sense for anything less than a major revision since it would break basically everything


#4

Are you sure that’s what you mean? There’s no place in the language where type: type is allowed; only ident: type and type: trait.


#5

hmm. I guess vectors are of Vec<T>

But having two <T> that mean different things within 1 word of each other seems confusing, and that seems like a very common implementation case? What about using this for sugar for the common case of passing the same type through? Unless it’s actually not very common?

impl<T> K {} == impl<T> K<T> {}

Sorry if I’m asking silly questions, I’m still trying to get a handle on everything.


#6

It is very common to pass the same arguments into a type constructor; however, I think that this might result in unfortunate confusion. The following would fail to compile under your suggested desugaring:

impl<T> K {
    fn new() -> K { .. } // expected 1 type paramter for type ctor K
}

Unless you’re suggesting that all uses of the bare type constructor without type paramters inside the impl follow the same desugaring, which is a bad idea for when we decide to make type ctors first class citizens with the “higher kinded types” feature. For example,

// takes a type constructor with one concrete type
// argument, i.e. a fn(type) -> type. OfInt<i32> does
// not typeck, but OfInt<Vec> does!
struct OfInt<T<_>>(T<i32>);

impl<T> K {
    fn new_of_int() -> OfInt<K> {} // to determine that this should not be the
}                                  // concrete type K<T> but the type constructor
                                   // K, we need to look up the definition of OfInt, which delays
                                   // desugaring until a much later, post typeck pass

#7

I may not be following but it sounds like here you’re trying to allow a bare K without a parent type, but declare a generic T to be used further down in the impl?


#8

Can you ellaborate what this means? Rust does not have inheritance; the only “subtyping” (if you can even call them that) relationships that ever turn out to matter are &mut T: &T and &'a T: &'b T for 'b: 'a.

What I wrote is what a new user would expect of your desugaring; if you can write impl<T> K for impl<T> K<T>, a new user might expect you can write K for K<T> anywhere in the impl body, which would result in unfortunate problems when we get HKTs.


#9

when declaring a variable you use this syntax, where my_variable: Type means my_variable of type Type

let my_variable: Type = something;

when you declare a constant you use this syntax, where MY_CONSTANT: Type means MY_CONSTANT of type Type

const MY_CONSTANT: Type = something;

when you declare a function you use this syntax to hint to the compiler, where argument: Type means argument of type Type

fn my_function(argument: Type){}

This syntax is used repeatedly throughout Rust for several different kinds, but always when specifically declaring you want to use a given Type. I guess I don’t get why type (struct) definitions can’t also follow this where MyType: Type means MyType of type Type

struct MyType: Type

Unless I’m misunderstanding what this means? I’m extrapolating from the mental model of Vectors; Where if you declare Vec<String> you’re creating a list of type String

struct MyType<Type>

#10

if I declare

struct MyType<i32>{x: String,}

How would you describe what <i32> means here? I have a mental model of the object, I just don’t know the right words to use in this case


#11

Ok, I think you’ve got types and traits mixed up. Here’s a handy playground link that explains the difference!


#12

struct MyType<i32> declares MyType with a type paramter i32 (which does not refer to the builtin type; builtin names are not keywords in rust, unlike in most langauges). This declaration is further a compile error, since the paramter i32 is unused in the definition (Rust requires all type paramters be used; see PhantomData).


#13

Sorry I misspoke here, what I meant to say was I don’t understand why impl can’t follow this same format where

impl<T> MyType: T {}

instead of

impl<T> MyType<T> {}

For reference I’m looking at the Second Edition book here: https://doc.rust-lang.org/book/second-edition/ch10-01-syntax.html#in-method-definitions

We could, for example, implement methods only on Point<f32> instances rather than on Point<T> instances with any generic type. In Listing 10-10 we use the concrete type f32, meaning we don’t declare any types after impl.

impl Point<f32> { //snip

I don’t see why that format would make more sense than this

impl Point: f32 { //snip

#14

@drXor I really do appreciate your answers even though I am getting a bit lost. I’m going to read a bit further and see if your answers start falling into place.

Thank you for at least bringing a candle to my darkness even if my eyes haven’t adjusted yet


#15

Yeah, my recommendation is to read the entire book (especially the parts about types, traits, and generics) before trying to form a mental model. There’s a lot of subtle pitfalls if you come from an object-oriented setting (which I will emphasize that Rust is not, in any traditional sense).


#16

Yup; A language like Haskell solves this syntactic ambiguity by ensuring that all type variables start lower cased and that all concrete types start upper cased. Thus, we can, without ambiguity, say:

id :: a -> a
id x = x

which is equivalent to:

id :: forall a. a -> a
id x = x

#17

There’s a difference between MyType<T> and MyType: T.

The first means that MyType on its own is a type constructor, and that you must provide it a type argument to produce a specific concrete type, like MyType<i32>, Vec<String>, or Point<f32>. The <>s are analogous to the ()s of a function.

The second means that MyType on its own is a type, and it goes on to further specify that it has an implementation for the trait T. This use of : is analogous to, but not the same as the : in fn f(x: i32) or let y: i32—it’s “up a level” from some_value: SomeType to SomeType: SomeTrait. (In case it’s not clear, traits are not types.)

So this means that your example of impl Point<f32> { fn m(&self) { .. } } provides methods that can be called on values of type Point<f32>—for example, fn f(p: Point<f32>) { p.m() }, while impl Point: f32 { .. }, if it were valid syntax, would means something more like “implement the trait f32 for the single type Point,” or maybe “provide some methods for the type Point, assuming that it implements the trait f32.”

Another potential misunderstanding that might clear things up is your wording around “if you declare Vec<String> you’re creating a list of type String.” This statement doesn’t make sense—you’re not creating a list/vector “of type String,” you’re creating a list/vector that contains Strings. It’s “of type” Vec<String>.