Pre-RFC: Easier extension-traits

Summary

Extension traits are a common pattern to extend foreign types and with these changes I wish to make it easier to do so.

Motivation

As stated earlier extension traits are a common pattern to extend foreign types. With these changes it will be easier to create extension traits no longer requiring you to define an initial trait to create them. Creating an initial trait that you just implement immediately into a singular type is annoying to work with.

Explaination

impl trait<T: std::fmt::Display> Example for Option<T> {
  fn show(&self) {
    if let Some(x) = self { println!("{x}") }
  }
}

let opt = Some(15);
opt.show()

This code shows a proposed syntax for extension traits where you don't need to define an initial trait to begin with. This code can be compared to something like:

pub trait Example {
    fn show(&self);
}

impl<T: std::fmt::Display> Example for Option<T> {
  fn show(&self) {
    if let Some(x) = self { println!("{x}"); }
  }
}

let opt = Some(15);
opt.show()

This is more verbose and takes longer to write hence why this is a good change.

Alternative Syntax

An alternative syntax could be this:

pub trait<T: std::fmt::Display> Example for Option<T> {
    fn show(&self) {
        if let Some(x) = self { println!("{x}"); }
    }
}

This syntax could be confusing for people though because of it's very similar definition to a normal trait.

1 Like

How would one implement an extension trait with default implementations using this mechanism? Stated another way, what would Itertools look like here? I also wonder what happens if two pub trait impl blocks disagree on signatures. What should the error message(s) look like here? Different method sets sounds…easy, but if they disagree in trait bounds, that's going to be harder to explain I feel.

1 Like

I don't know honestly. We could brainstorm some syntax and see what sticks. I'm thinking of this currently:

impl trait<T> Example for Option<T> {
  fn show(&self)
  where T: std::fmt::Display
  {
    if let Some(x) = self { println!("{x}"); }
  }
  fn show(&self)
  where T: std::fmt::Debug
  {
    if let Some(x) = self { println!("{x:?}"); }
  }
}

Or

impl trait<T: std::fmt::Display> Example for Option<T> {
    fn show(&self) {
        if let Some(x) = self { println!("{x}"); }
    }
}
impl trait<T: std::fmt::Debug> Example for Option<T> {
  fn show(&self) {
    if let Some(x) = self { println!("{x:?}"); }
  }
}

There was a thread with a proposal for a uniform method call syntax about a month ago that is related. What if you could write x.(f)(y) to mean f(x, y)?

You can do something similar with macros already.

You normally import the extension trait to use the new methods, like use itertools::Itertools, how would that work with your proposal?

1 Like

I think it would be nice to have this in rust itself instead of having to use third-party libraries.

impl trait<T: std::fmt::Display> Example for Option<T> {
    fn show(&self) {
        if let Some(x) = self { println!("{x}"); }
    }
}

In this trait it has the name Example so if this was declared in a library named foo for example it would be use foo::Example.

What are some of the downsides to this approach?

  • If there are none, would this macro be a reasonable addition to std?
  • If there are some, are there smaller language changes that would give this macro (and others?) the necessary superpowers?

What are some of the downsides to this approach?

Not sure. It does support generics and constants, but not associated types (not sure why you would ever need that, other similar macros support them).

Too me some downsides of the macro approach is that it feels more hacky than an actual fix to the problem.

We could also go with this syntax to keep grepability:

impl<T: std::fmt::Display> pub trait Example for Option<T> {
  fn show(&self) {
    if let Some(x) = self { println!("{x}") }
  }
}

I feel like this wouldn't be very useful, because normally when I make an extension trait, I implement it for a whole bunch of different types, and this impl trait ... block's savings becomes much less important when you have 20+ impl blocks than if you're just writing one. I'd rather have one source of truth for what the signature of the trait is that I can read over to see just the trait definition without it getting mixed with an implementation.

Also, how does this work if you need multiple impl blocks (e.g. if I wanted to also implement your Example trait for Result<T: Display, E>)? Having multiple of these impl trait ... blocks would be defining the trait in multiple places, which feels wrong to me (and would allow those definitions to go out of sync with each other), while having one block be impl trait ... and the others just be impl ... feels weird to have one impl be different from all the others.

3 Likes

I don't think this is worth it. We should avoid these "one-item" syntactic optimizations like the plague, they're disaster factories when refactoring. This seems very nice, but as @JarredAllen was pointing out, these traits will often, at some point, be implemented for more types as your requirements evolve. Now, instead of just writing the impl block, you have to write the trait definition and rewrite your old impl declaration. The current syntax is basically syntactic amortization, and amortizing a few LOC for easy refactors is, in my mind, the right choice.

I should also point out that defining a trait also serves the purpose of giving a compiler-enforced implementation guideline, and prevents you from not declaring correctly your function definitions when implementing them for a type. I don't think that saving a few LOC on an edge case is worth losing these protections, either.

2 Likes

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