Reduce repetitive generic parameters and struct name in impl syntax

Proposal

I find impl StructName syntax quite repetitive, especially when the struct has generics with bound.

struct MyStruct<A, B: Foo, C: Bar, D: Baz<A, B, C>> { /* ... */ }

impl<A, B: Foo, C: Bar, D: Baz<A, B, C>> MyStruct<A, B, C, D> {
  pub fn method(/* ... */) { /* ... */ }
}

impl<A, B: Foo, C: Bar, D: Baz<A, B, C>> Trait for MyStruct<A, B, C, D> { /* ... */ }

I propose a cleaner syntax:

struct MyStruct<A, B: Foo, C: Bar, D: Baz<A, B, C>> {
  /* fields */

  impl Self {
    pub fn method(/* ... */) { /* ... */ }
  }

  impl MyTrait for Self { /* ... */ }
}

or even shorter:

struct MyStruct<A, B: Foo, C: Bar, D: Baz<A, B, C>> {
  /* fields */

  pub fn method(/* ... */) { /* ... */ }
}

// this variant lefts out trait impls
impl<A, B: Foo, C: Bar, D: Baz<A, B, C>> Trait for MyStruct<A, B, C, D> { /* ... */ }

I quite like @CAD97's suggestion as well:

for <A, B: Foo, C: Bar, D: Baz<A, B, C>> {
  struct MyStruct { /* ... */ }

  impl MyStruct {
    pub fn method(/* ... */) { /* ... */ }
  }

  impl Trait for MyStruct { /* ... */ }
}

Motivating Example

This is a real code in an application that I am working on:

use super::super::{data::TagMapIndex, sizes::sidebar::*, style, utils::Callable};
use super::IndentedButton;
use iced::*;
use std::collections::BTreeMap;

#[derive(Debug, Default, Clone)]
pub struct Controls(pub BTreeMap<TagMapIndex, button::State>);

pub struct TagList<'a, Theme, GetContent, GetMessage, GetActivated> {
    pub controls: &'a mut Controls,
    pub button_prefix: &'a str,
    pub get_content: GetContent,
    pub get_message: GetMessage,
    pub get_activated: GetActivated,
    pub theme: Theme,
}

impl<'a, Theme, Message, GetContent, GetMessage, GetActivated>  TagList<'a, Theme, GetContent, GetMessage, GetActivated>
where
    Message: Clone + 'a,
    Theme: style::Theme + Copy,
    GetContent: Callable<Input = TagMapIndex, Output = Element<'a, Message>> + Clone,
    GetMessage: Callable<Input = TagMapIndex, Output = Message> + Clone,
    GetActivated: Callable<Input = TagMapIndex, Output = bool> + Clone,
{
    fn into_element(self) -> Element<'a, Message> {
        let TagList {
            controls,
            button_prefix,
            get_activated,
            get_message,
            get_content,
            theme,
        } = self;

        let mut button_list = Column::new();

        for (index, state) in controls.0.iter_mut() {
            let index = *index;
            let activated = get_activated.clone().call(index);
            let button: Button<'a, Message> = IndentedButton {
                prefix: if activated { button_prefix } else { "" },
                content: get_content.clone().call(index),
                state,
            }
            .into_button()
            .width(Length::Units(SIDEBAR_LENGTH))
            .on_press(get_message.clone().call(index))
            .style(style::BinaryStateButton {
                style: theme.style(),
                activated,
            });
            button_list = button_list.push(button);
        }

        button_list.into()
    }
}

#[derive(Debug, Copy, Clone)]
struct GetStyle<GetActivated, Theme> {
    get_activated: GetActivated,
    theme: Theme,
}
impl<'a, GetActivated, Theme> Callable for GetStyle<GetActivated, Theme>
where
    GetActivated: Callable<Input = TagMapIndex, Output = bool>,
    Theme: style::Theme,
{
    type Input = TagMapIndex;
    type Output = style::BinaryStateButton;
    fn call(self, x: Self::Input) -> Self::Output {
        style::BinaryStateButton {
            activated: self.get_activated.call(x),
            style: self.theme.style(),
        }
    }
}

Originally, it was a function create_tag_list(theme: impl style::Theme + Copy, ...), the problem is:

  • I must call create_tag_list with the right order of arguments, but there are too many arguments making the code ambiguous.
  • Named function parameter would solve the aforementioned problem, however, it does not assign meaning to a set of named parameters the same way a struct does. I also want to reuse the same set of named parameters in different functions, a struct is better for that.

So I chose to create a struct. Other problems arise:

  • I still must ensure correct order of generic parameters when writing impl.
  • Using trait with associate types would solve aforementioned problem, but:
    • It would require even more boilerplate when I use the struct in another impl.
    • I still have to specify bound trait.
    • Type inference would stop working. Constructing the struct would require explicit type parameters that impls the utility trait.
  • Speaking of bound traits, when they are not satisfied, resulting error messages are numerous and confusing (not unlike that of a C++ template).
    • Specifying trait bound in the struct declaration would have resulted in a less confusing error messages in the right place. The only reason I did not do that is because a) I dislike boilerplate more than I dislike confusing error messages; and b) It would only work if the trait bounds in the struct are exactly the same as the trait bounds in the impl.
    • Thankfully, I did not so eagerly attempt to use trait with associate types, none can imagine what the error messages would be.
  • And of course, repetitive generic parameters in the struct and in the impl.
3 Likes

There is already the implied bounds RFC, which would fix the repetition of the trait bounds

7 Likes

What about repetitive generic parameters themselves?

I personally don't think reducing that duplication is worth the additional language complexity

8 Likes

Complex in implementation? Maybe, I'm not sure. But not in design.

On the contrary, C is dead simple in both design and implementation, yet working with it is hard.

You prefer having to write boilerplate?

I don't really mind writing a couple of letters twice. Compile times have a far bigger impact on my productivity, and fixing those doesn't require any language changes.

3 Likes

When I have a struct with a lot of generic parameters, I usually simplify the impl blocks by making the struct have ONE generic parameter that has a bunch of associated types instead. That makes it easier to refactor later – although it isn't 100% ergonomic in rust yet because of things like the derive bounds issue

1 Like

Those "couple of letters" not only reduce productivity, but also reduce readability. I don't want more than 50% of my code being boilerplate.

1 Like

Having the impl in a separate block helps a lot with clarity. It ensures the definition of a type only deals with the actual data contents of that type, and there aren't "surprise fields" (or variants) intermingled with methods. It would actually be detrimental to the language to allow that.

4 Likes

The language could still require that all fields are before any methods of an "implicit impl block".

However, more pertinent is the fact that typical API conventions are to not bound types in the struct definition at all, and only add the bounds for impl blocks. Having an implicit impl block would both be less useful when following that convention and encourage putting unneeded bounds on the struct parameters, in order to make the implicit impl block more useful.

1 Like

where keyword exists. If you don't want to have bound traits on struct parameters, you can use where after impl Self (impl Self where ...).

I also do not think only putting bound on impl is always a good thing. The error message is confusing ("cannot find a trait impl..." being shown on method calls as opposed to "[generic argument] does not satisfy [bound trait]" being shown on construction).

It is better that user is stopped prematurely from constructing a struct without useful methods than to show confusing error messages when the struct is put to use. The downside of this approach is that it requires convoluted boilerplate on behalf of library author (that is, until this very proposal is implemented).

1 Like

I believe this is a rustfmt problem. One that is easily solved.

Everyone seems to be so obsessed with and picking on that one word. First off I don't use rustfmt and I don't want to have to. Second, there are further problems other than the order of fields and methods. Methods and fields should be separated, because they have radically different meanings.

The fields and variants are inherent to the type and they define the type, furthermore they are not modifiable after the fact, whereas methods can be attached to types arbitrarily, even by 3rd-party code. Putting them in the definition suggests the opposite.

Plus, putting the methods inside the type definition introduces a new level of indentation for basically no good reason, and that is not fixable by rustfmt (unless one is willing to accept the horrible special case of "do not indent impl blocks when inside a type definition", which I'm definitely not.)

2 Likes

This is false. You cannot define a new method for a third-party crate. You must at least own the struct (for trait-less methods). You can also create your own trait, but that is beside the point.

Furthermore, field access and method call are both using the dot syntax. By your logic, they must be the same.

I proposed two variants of the syntax, one with impl Self and extra indentation, and one without.

You are also not forced to use this syntax.

1 Like

Java folks don't mind methods and fields in the same {..}

To make it even closer to Java and maintain parity between trait-less and trait methods after this we'll ask for impl Trait on the same block. E.g. we'll want the same method to function both as a trait and an inherent one; possibly as trait method for several traits at once - so long as there is no conflict. :slight_smile:

1 Like

Exactly — and I don't want Rust to approach Java the least bit in this regard. Rust types are not Java classes where behavior is inherently bundled with the type. Rust types are much cleaner, and changing this would make the language worse.

Extensions traits are exactly what I was referring to, so this is not "false".

That's a perpetually-parrotted non-argument about complications to the language, for the following reasons:

  1. I still have to know the new syntax if I want to read code that does use it.
  2. Or if I want to write procedural macros that operate over types and their methods.
  3. Such optional features create an ecosystem split based on whether they are used or not – this is one reason why other redundant, pure synactic sugar is (also) bad.
3 Likes

No, that is false. You can define a new trait that contains one or more methods and then implement that trait for the 3rd party type and now that type has those new methods.

2 Likes

This proposal only concern with trait-less method, so it is beside the point.

This syntax is simple and familiar (already present in C++, C#, Java, etc.), so I would expect any person to pick it up immediately.

I have never written a procedure macro before, so I can't comment on this. Does procedure macro receive the syntax-tree as it is written, or does the AST parser desugars things before feeding it to the macro?

How? Mere syntactic sugar does not create new API.

What might be a plus for everyone is if instead of adding syntactical shortcuts, the ability for simple macros to handle generics and where clauses were to be improved. Doing this currently is a bit painful at times, and that could be used to reduce any number of current or future "boilerplate" cases.

1 Like

While it's true that providing two different syntaxes with essentially the same functionality is generally a bad idea, and that impl Self inside of the struct block would likely mislead newcomers into expecting Rust structs to behave far more like Java classes than they really do, none of that is terribly important at the moment because we're completely missing a motivation.

The original motivation claimed for this was reducing duplication between the struct and impl blocks. As pointed out near the beginning, there's already an accepted "implied bounds" proposal which would remove the vast majority of that duplication. So we're back to needing some kind of motivating example of redundancy that's bad enough to justify an alternative syntax. Until then everything else is academic.

The fact that a struct/impl can theoretically accumulate several generic parameters is not a motivation by itself, because a) afaik that's extremely rare in practice, b) <A,B,C,D> is just not that long, c) since impls can be arbitrarily far away from the struct and have a different set of generic params from the struct it's unclear how you could avoid the need to redeclare them even in principle. If any of that is not true, we really need concrete motivating examples to show it.

10 Likes