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.
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.
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)?
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.
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.