Reduce repetitive generic parameters and struct name in impl syntax

It is common when your code is generic-heavy. For example, in an algebra library.

It is only short in the example I provided. In practice, I prefer using meaningful names (such as <Key, Value, Left, Right>), short names are cryptic.

If that is the case, then you have no choice but to use the old syntax. I see no problem here.

impl must always provide enough generic params required but the struct, no more, no less. Or did I misinterpret "different set"?

For the first proposed variant (impl Self), one may use impl<AdditionalParams> Self to provide additional generic params, or just write additional generic params directly in the fn declarations.

For the second proposed variant (just fn), write additional generic params directly in the fn declarations.

1 Like

The above argumentation could be convincing. Would you post some worked, non-toy examples of your proposal that could convince the many readers of this forum of your proposal's advantages in real code? (Most of the disadvantages have already been pointed out by others.)

1 Like

I was thinking of things like this:

struct Foo<A,B> { ... }
impl<A> Foo<A,A> { ... }

which looks weird at first but iirc does come up in practice, especially with tuple types today, and specialization in the future.

While I agree descriptive names should be preferred in general, it sounds like the cases you're interested in all have the impl fairly close to the struct, so it may be reasonable to use single-letter names the second time:

struct Foo<Key: Hash + Eq + Whatever, Value, Left, Right> { ... }
impl<K,V,L,R> Foo<K,V,L,R> { ... }

Really, this just highlights why we need concrete examples to make any of this convincing.


We should consider other syntaxes too. For example:

struct Foo<Key: Hash + Eq + Whatever, Value, Left, Right> {
    // fields
}

// literal ellipsis here means copy `Foo`'s type params
impl<...> Foo<...> {
    // methods
}

would avoid many of the above complaints about impl Self inside struct. Maybe you could make a case for Foo<> or Foo<_> too, but atm I think Foo<...> is the best choice other than "do nothing".

3 Likes

I am working on an app where the needs for this syntax arises. It is not completed yet. Many structs have callable functions for field, and as such, FnType: Fn(...) -> ... is repeated many times.

We know that you're working on something that induced you to create this thread. We don't need to see your full app, but we do need to see some sample code, in large enough samples, that demonstrates where the current situation falls so short of what is desirable that this change is warranted.

We need to see this because the proposed change will add additional grammar complexity and learning requirements, as well as require modification of rustc and of a lot of support tooling (e.g., rls, rust analyzer, clippy, rustfmt, IntelliJ IDEA, etc).

7 Likes

Even more fundamentally, we simply can't know what changes (if any) would improve your situation when we know so little about what went wrong for you.

(and yes, it's very common for people to request changes that, on closer inspection, either wouldn't help them at all or have alternatives that are better for everyone)

4 Likes

Apart from everything else, what's stopping this from being

struct Foo<Key: Hash + Eq + Whatever, Value, Left, Right> { ... }
impl Foo<K,V,L,R> { ... }

?

1 Like

Because in general, the impl's type parameters are not always the same as the type's type parameters. This is most common with trait methods where you often need to write impl<A, B> MyType<A> for MyTrait<B> { ... }. But it can also happen with inherent methods like the impl<A> Foo<A,A> { ... } case I mentioned earlier.

We certainly could propose additional elision rules for the impl<A,B,C> Foo<A,B,C> cases, but I suspect that hits diminishing returns or subtle confusion about type parameter semantics very quickly. One reason I suggested impl<...> Foo<...> { ... } is because I think that's also the best we can do for that problem without creating any new problems.

We certainly could propose additional elision rules for the impl<A,B,C> Foo<A,B,C> cases

I don't think I'm proposing elision. I think I'm proposing that we deprecate impl<...> and fn name<...> and instead implicitly create type variables whenever an unbound identifier appears in a context where a type is required. Thus, your examples would become

impl MyType<A> for MyTrait<B>
impl Foo<A, A>

and here's some more complicated cases picked at random from the stdlib:

impl Debug for fn(A, B, C) -> Ret
impl From<(I: Into<IpAddr>, u16)> for SocketAddr
pub const fn identity(x: T) -> T
pub fn min(v1: T, v2: T) -> T where T: Ord
pub fn min_by(v1: T, v2: T, compare: F) -> T
    where F: FnOnce(&T, &T) -> Ordering
2 Likes

Oh duh, I completely forgot to point out the basic scoping issue.

type Key = u32;

// implements "for any type"
impl<T> Debug for MyType<T> { ... }

// explicitly declares a new type parameter,
// so this also means "for any type"
impl<Key> Debug for MyType<Key> { ... }

// currently uses the concrete type / u32 alias
impl Debug for MyType<Key> { ... }

Making the latter syntax interpret Key as a type parameter probably isn't feasible. Theoretically we could make it mean declare type parameter only if there isn't a concrete type by that name in scope, but then that means really weird and confusing things could happen whenever you change what names are in scope, so I'm fairly confident we shouldn't do that.

Plus, I believe completely deprecating type parameter declaration isn't even possible because of cases where it'd be ambiguous what scope you wanted it to apply to (the entire impl? just one fn?).

impl<...> doesn't create any of those problems (assuming we stick to special cases where the inference is trivial), and is only 5 characters longer no matter how messy the generics are.

7 Likes

Yes, that's what I had in mind. I guess I'm not much worried about weird things happening, in my experience people tend to pick type variable names that are safely nothing like any concrete types in scope. I wonder how hard it would be to canvas crates.io for potential clashes.

As for ambiguous scoping of implicit type variables, we could say that implicit type variables always have the narrowest possible scope, and keep the explicit notation around for when they need to have a broader scope, but I've never actually seen that come up, can you think of an example?

1 Like

Fully generalized impl Trait syntax could allow for

struct Foo<Key: Hash + Eq + Whatever, Value, Left, Right> { ... }

impl Foo<impl Hash + Eq + Whatever, impl Sized, impl Sized, impl Sized> { ... }

Though the original example cleverly avoids being able to use this by having one of the bounds depend on all of the other parameters (not something I've seen very often in real code).

2 Likes

Having the meaning of impl Type<Parameter> different based on what names are in scope is spooky action at a distance. It could end up changing behavior due to changes in another module due to glob imports. Even if you don't use glob imports, it can change meaning due to code elsewhere in the file.

And yes, while typically type parameter names are not anything like type names, because type parameter names tend to just be one-letter names, I honestly hope that falls out of fashion. It's much more useful to use a meaningful name as the type parameter, and as soon as you do, it's a possible reasonable type name.

3 Likes

An alternative is impl Type<_, _, _>, since the underscore is already used for anonymous lifetimes. The downside is that type parameters could only be replaced with _ if they aren't used anywhere in the impl block:

struct Foo<A, B>(A, B);

impl<A> Foo<A, _> {
    fn get_a(&self) -> &A { &self.0 }
}
1 Like

I have added "Motivating Example" section.

My intention was not to replace old syntax completely, but to reduce boilerplate for the most common case.

BTW, in your example, if you use the methods in impl<A> Foo<A,A> on Foo<A, B>, I can guarantee you that the error message would be very confusing. I would recommend creating a wrapper type (e.g. struct FooWrapper<T>(pub Foo<T, T>)) and add methods to that wrapper type, the error messages would be clearer this way.

I have no problem with this syntax (for now).

struct S<A,B>(A,B);

impl<T> S<T,T> {
    pub fn f(&self) {}
}

fn q<A,B>(s: S<A,B>) {
    s.f()
}
error[E0599]: no method named `f` found for struct `S<A, B>` in the current scope
 --> src/lib.rs:8:7
  |
1 | struct S<A,B>(A,B);
  | ------------------- method `f` not found for this
...
8 |     s.f()
  |       ^ method not found in `S<A, B>`

This is very improvable, though. For example, see the error for a slight alteration of this example:

  impl<T> S<T,T> {
-     pub fn f(&self) {}
+     pub fn f() {}
  }
error[E0599]: no method named `f` found for struct `S<A, B>` in the current scope
 --> src/lib.rs:8:7
  |
1 | struct S<A,B>(A,B);
  | ------------------- method `f` not found for this
...
8 |     s.f()
  |     --^
  |     | |
  |     | this is an associated function, not a method
  |     help: use associated function syntax instead: `S::<A, B>::f`
  |
  = note: found the following associated functions; to be used as methods, functions must have a `self` parameter
note: the candidate is defined in an impl for the type `S<T, T>`
 --> src/lib.rs:4:5
  |
4 |     pub fn f() {}
  |     ^^^^^^^^^^

If the problem is with errors, we can fix the errors rather than the language.

(Who wants to file the issue? I've no real idea how to word it well. Note that using S::<A,B>::f() doesn't get the nice hint about it being S::<T,T>::f() either.)

3 Likes

I don't want to get off-topic. But I am sure that this would happen very often. Filing an issue is necessary.

What would the ideal error message be? I'm guessing that you are thinking of adding some hints? I tell you this: Too much text is also confusing.

This only way to fix this is that the struct author should prevent consumer from constructing useless struct in the first place by adding trait bounds to the struct.

Let's look at this example:

// without trait bound
struct Vec2<X, Y>(pub X, pub Y);

impl<X, Y> Add for Vec2<X, Y>
where X: Add<Output=X>, Y: Add<Output=Y> { /* ... */ }

fn add<X, Y>(a: Vec2<X, Y>, b: Vec2<X, Y>) -> Vec2<X, Y> {
  a + b // error message would be "Add is not implemented" followed by dozen other text
}
// with trait bound
struct Vec2<X, Y>(pub X, pub Y)
where
  X: Add<Output=X> + Mul<Output=X> + Neg<Output=X>,
  Y: Add<Output=Y> + Mul<Output=Y> + Neg<Output=Y>;

impl<X, Y> Add for Vec2<X, Y>
where 
  X: Add<Output=X> + Mul<Output=X> + Neg<Output=X>,
  Y: Add<Output=Y> + Mul<Output=Y> + Neg<Output=Y>,
{ /* ... */ }

fn add<X, Y>(
  a: Vec2<X, Y>, // error message would appear here, tell us exactly the problem
  b: Vec2<X, Y>,
) -> Vec2<X, Y> {
  a + b // error message would not appear here
}

As you can see with the second code snippet: Although error messages are clearer, it requires a lot of boilerplate.

1 Like

Just two further notes:

As written, your proposal doesn't help for trait impls. It's certainly a simple extension to let it, but you need to spell that out if it's part of it.

Duplication between impl blocks for separate traits is a known issue and one that's desired to make simpler. What's been suggested previously is something along the lines of

for<X, Y>
where
  X: Add<Output=X> + Mul<Output=X> + Neg<Output=X>,
  Y: Add<Output=Y> + Mul<Output=Y> + Neg<Output=Y>,
{
    impl Add for Vec2<X, Y> { ... }
    impl Mul for Vec2<X, Y> { ... }
    impl Neg for Vec2<X, Y> { ... }
}

It's still an informal idea rather than a proposal, but one with a mild positive community opinion.

(IIRC, at last discussion, the consensus was to do implied bounds first, as they service a highly related pain point. Additionally, I believe implied bounds also gets you 90% of what benefit you're looking for, as the only repetition left is the necessary-for-root-level declaration of type parameters' names.)

2 Likes

The two snippets of code I wrote were only meant to demonstrate the pros and cons of trait bound in struct.

It is true that I didn't think of trait impls when I wrote this proposal. But seeing trait impls being even more verbose than trait-less methods, perhaps we should extend the topics of this discussion?

This is a neat syntax, but it still duplicates generic parameters from the struct. How about extending this syntax to include struct declaration as well (see below)?

for<X, Y>
where
  X: Add<Output=X> + Mul<Output=X> + Neg<Output=X>,
  Y: Add<Output=Y> + Mul<Output=Y> + Neg<Output=Y>,
{
    struct Vec2(pub X, pub Y);
    impl Add for Vec2 { ... }
    impl Mul for Vec2 { ... }
    impl Neg for Vec2 { ... }
}