A shorthand for implementing traits

A shorthand for implementing traits, that would turn code that looks like this:

trait SuperCoolTrait {
  fn myFunction(&self);
}

// Implementing the type itself
impl MyType {
  fn function() {
    unimplemented!()
  }
}

// Implementing trait for the type

// Duplicating the type's name name (bad for maintainability),
// Extra typing that could be avoided
impl SuperCoolTrait for MyType {
  fn myFunction(&self) {
    unimplemented!()
  }
}

into this:

trait SuperCoolTrait {
  // --snip--
}

// Maybe it should be closer to `impl MyType with SuperCoolTrait`
// for increased clarity over which traits the type implements at
// first glance.
impl MyType {
  fn function() {
    unimplemented!()
  }

  // Prefix with trait keyword?
  // trait SuperCoolTrait { /* --snip-- */ }
  // Feel like this would cause ambiguity though,
  // for humans AND the compiler
  SuperCoolTrait {
    fn my_function(&self) {
      unimplemented!()
    }
  }
  FooTrait {
    fn foo_function() {

    }
  }
}

This shorthand just looks much cleaner (in my personal opinion), duplicates names less (if you are renaming a struct, that is one less place(s?) to rename).

I want to know what you guys think of such a design and if you have anything to add to the conversation.

The biggest drawback I see to this shorthand is that it risks making it less clear which types implement which traits. Perhaps the impl should be more like impl MyType **with SuperCoolTrait** (no asterisks) to make it more obvious that this type implements the given trait.

The reason I decided to nest these inside of their trait names instead of have them be at the top level of the impl is to preserve clarity about which functions are part of which traits. This does not mean that you would have to do thing.SuperCoolTrait.my_function() to access a function in a trait.

Declaring function with the same name at the top-level of the impl should be disallowed (for obvious reasons)

I feel like all of these can drastically cut down on the amount of typing, but can also reduce the amount of places in which you have to rename a struct/enum if you have implemented several traits for it.

Let me know what you think, and why this is potentially a bad idea!

Since this thread came from URLO, for the sake of completeness, let me add a backlink to my post describing why I oppose this idea: link

3 Likes

One disadvantage of this shorthand is that it can't replace all uses of the existing impl Trait for Type syntax. For example, it doesn't work when the type and the impl are defined in different crates—unless we start allowing impl Type { ... } for foreign types with added restrictions.

Even if we could theoretically deprecate the original syntax, it would affect so much existing code that it is almost certainly too big a change in practice. Regardless, the old syntax would live on in code and docs written in the current edition of Rust. Every time we add two ways to do the same thing, it adds another bit of minor complexity for Rust learners.

(Yes, this is a general argument that applies to any new syntax sugar, so it should always be weighed against other factors.)

The reduction in typing seems minor (it saves one or two words per trait impl). Meanwhile, the number of lines is either the same or higher than the existing syntax, and the increased nesting uses more horizontal space as well.

Yes, it reduces the number of places affected by renaming the type, but most IDEs can rename a type automatically.

3 Likes

This has previously been discussed; I specifically recall this thread, though I also think @Soni has mentioned it more recently. (Side note: linking is for context purposes only, and not endorsement of any specific author or post in linked thread. (Note: this disclaimer is not trying to put down any author or post in the linked thread.))

While the amount of redundant typing (note: typing as in types in the language, not keyboard typing) reduction is limited in the simple case, more interesting bounds on generic types can lead to a lot of duplicated type information for multiple trait impls.

Additionally, impl Type { impl Trait { fn foo() {} } } is potentially useful as an inherent impl, where the Trait functions are available for Type even without bringing Trait into scope, equivalently to if extra forwarding inherent impls were provided. (Including precluding a separate inherent impl with the same name.)

Implied bounds for inherent trait bounds would reduce again the benefit of deduplicating the typing overhead, but there are still cases where the ideal situation of bound-free type definitions and bounded impls are useful and would reduce typing overhead there.

The main downside I can see, though, is that the deduplicating would encourage over-bounded trait impls, as it'd be easier to provide one superset bounded impl and impl all traits inside it, than implement the traits separately with proper bounds.

I still personally like the idea, but if and only if the functions are (all) also provided as inherent functions. This meaningfully separates it from the separate item case (which it desugars to).

2 Likes

tl;dr: It interacts badly with existing macros.

Personally, we'd much rather love to see a proposal to enable

#[impl_trait]
impl Foo {
  // contents are opaqueified so the proc attribute macro can do more things
}

which would make our crate better by removing a whole level of nesting, and hopefully work better with existing macros. Specifically, currently impl_trait::impl_trait! doesn't let you nest other similar macros, but the attribute variation would let you (altho success is dependent on various other factors).

impl_trait! {
  this_doesnt_work! {
    // mainly the problem is that we expect a bunch of attributes and an
    // impl block, but instead we're met with a macro.
  }
}

There is some value in bringing impl_trait!'s syntax to the compiler tho, in one particular place: docs. Currently trait impls are seen as "extrinsic" to the type. For example, for Cursor in std::io - Rust, only get_mut, get_ref, into_inner, new, position and set_position are considered "intrinsic" features of the Cursor, with the trait impls all being moved together to their own section of "extrinsic" features. Being able to put trait impls inside inherent impls (even if it requires using the full existing syntax) would enable you to say, "These trait impls are important features of this type, you are probably interested in using them almost every time you use this type.". This could even enable deprecating T::new() in favour of Default::default()!

2 Likes

Context Soni omitted (please try to provide more context rather than assuming people have the full context you do): they maintain this crate:

https://crates.io/crates/impl_trait

TL;DR: this adds impl_trait! which lets you do:

struct Foo;

trait Bar {}

impl_trait! {
  /// Yes, you can write docs here.
  impl Foo {
    /// You can write docs here too.
    fn as_dyn_bar(&self) -> &dyn Bar {
      self
    }
    /// You can even write docs here!
    impl trait Bar {
    }
  }
}
1 Like

.


we actually did provide context, altho you may have missed it. anyway we're too sleep deprived for this so gl.

Manually hidden reply

You referenced it without introducing what it was. Thus, a reader not already familiar with the impl_trait crate lacks context. Providing a link on "our crate" would at least allow said reader to discover said context, where currently there is no context for what impl_trait! even does.

In any case, this is wildly off topic, and I'll stop going off topic to try to help you improve your posts.

5 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.