This specifically shouldn't work because of code evolution, forwards/backwards compatibility, and the fact that fns in impl Trait don't need to be marked as pub.
Namely, it's considered a backwards compatible change for a library to add new methods to a trait, so long as the methods have a default implementation. If the default is provided, all downstream impl blocks for the trait will continue to compile as intended, using the default body for the new method.
If an impl block has a helper method with the same name, then adding defaulted methods becomes a major breaking change. If you're lucky, name collisions will cause an error about mismatched signature. If you're unlucky, you'll have a trait method implemented without acknowledgement of intent for the semantics matching.
I think there's a minor misunderstanding about traits underlying this want. Type::method
and <Type as Trait>::method
are distinct function items. In a language with "duck typed" polymorphism like Python or C++, there's an implicit expectation that the caller won't make silly mistakes and will only provide types where the used function/method names have the intended semantics. Rust takes a stricter approach: you must declare that a set of impl
items conforms to a trait
in order to use it polymorphically. This allows the compiler to catch silly mistakes (such as not agreeing on what this.fmt(f)
means) up front, but it requires only designating impl items as conforming to a trait (i.e. placing them within an impl Trait for Type
block) if they actually match the impl items declared by the trait.
There might be other ways of improving structure of the discussed code shape. Two previously discussed potential features are "ad hoc method extensions", "nested impl blocks", or even "enum variants as types", e.g.
// ad hoc method extensions
fn handle_a(self: &Bar) { /* ... */ }
fn handle_b(self: &Bar) { /* ... */ }
impl Foo for Bar {
fn do_something(&self) {
match self {
Bar::A => self.handle_a(),
Bar::B => self.handle_b(),
}
}
}
// nested impl blocks
impl Bar {
fn handle_a(&self) { /* ... */ }
fn handle_b(&self) { /* ... */ }
impl Trait {
fn do_something(&self) {
match self {
Bar::A => self.handle_a(),
Bar::B => self.handle_b(),
}
}
}
}
// enum variants as types
impl Bar::A {
fn handle_a(&self) { /* ... */ }
}
// e.g. via "pattern restricted types"
impl (Bar is Bar::B) {
fn handle_b(&self) { /* ... */ }
}
impl Foo for Bar {
fn do_something(&self) {
match self {
// rebind to avoid flow typing requirement
this @ Bar::A => this.handle_a(),
this @ Bar::B => this.handle_b(),
}
}
}
These are all very draft ideas and have major unresolved questions for behavior specifics, but they're generally agreed as being plausible within Rust's system.
In fact, you can emulate each of them already.
I have ultra vague plans of eventually publishing a futuresight
package which is a collection of macros/techniques for this kind of syntax sugar with reasonably loose contribution requirements. A community library for emulating potential future features; Python's from __future__ import
; standback but for unstable features. But it's super low priority and I've got other projects in flight that want to get finished at some point
Example "desugars" for each feature (simplified; real desugar would need to handle a lot more, thus the complexity):
// adhoc extension methods
#[futuresight]
$vis fn $name(self: &$Self) -> $Return $body
$vis trait $name {
fn $name(&self) -> $Return;
}
impl $name for $SelfTy {
fn $name(&self) -> $Return $body
}
$vis fn $name(__self: &$Self) -> $Return {
<$Self as $name>::$name(__self)
}
// nested impl blocks
#[futuresight]
impl $Self {
$($item_impl)*
$(impl $Trait $trait_impl)*
}
impl $Self { $($item_impl)* }
$(impl $Trait for $Self $trait_impl)*
// enum variant types
// too complex for macro desugar
// use "newtype" variants instead
struct BarA;
struct BarB;
enum Bar {
A(BarA),
B(BarB),
}
impl Trait for Bar {
fn do_something(&self) {
match self {
Bar::A(this) => this.do_something(),
Bar::B(this) => this.do_something(),
}
}
}
// this has no size penalty with unit structs
// and in fact BarA is ZST; (Bar is Bar::A) isn't
// but you do forfeit simple E::A syntax
// or use a struct with associated consts
// and forfeit downstream match exhaustiveness
// plus non-ZST variants forfeit some repr niche potential
// pattern restricted types are a very complicated impl
// and there are many subtle type system implications
// mostly around inference, subtyping, and coercions
// but they look promising and exciting if they work out