Add helper functions to impl Trait block

Hi,

I'm wondering if there have been any RFCs or initiatives to support something like this in the language, if it even makes sense. I was working on something and, from a code organization point-of-view, I felt like it'd be best to be able to write an impl like this:

// Defined in different module/crate
enum Bar {
    A,
    B,
    C,
}

// A trait I defined
trait Foo {
    fn do_something(&self);
}

impl Foo for Bar {
    fn do_something(&self) {
        match self {
            Bar::A => {
                // Big chunk of code
                // or we could also do handle_a(self) if it was supported
            }
            Bar::B => {
                // Big chunk of code
                // or we could also do handle_b(self) if it was supported
            },
            Bar::C => {
                // Big chunk of code
                // or we could also do handle_c(self) if it was supported
            },
        }
    }

    // This doesn't work because these functions are not defined in the trait

    fn handle_a(&self) {
        // Big chunk of code
    }

    fn handle_b(&self) {
        // Big chunk of code
    }

    fn handle_c(&self) {
        // Big chunk of code
    }
}

What's wrong with this?

// Defined in different module/crate
enum Bar {
    A,
    B,
    C,
}

// A trait I defined
trait Foo {
    fn do_something(&self);
}

impl Foo for Bar {
    fn do_something(&self) {
        match self {
            Bar::A => {
                // Big chunk of code
                // or we could also do handle_a(self) if it was supported
            }
            Bar::B => {
                // Big chunk of code
                // or we could also do handle_b(self) if it was supported
            },
            Bar::C => {
                // Big chunk of code
                // or we could also do handle_c(self) if it was supported
            },
        }
    }
}


fn handle_a(this: &Bar) {
    // Big chunk of code
}

fn handle_b(this: &Bar) {
    // Big chunk of code
}

fn handle_c(this: &Bar) {
    // Big chunk of code
}

There's nothing wrong with that.

If I want to impl Foo for several types in the file that defines Foo, I could definitely do something like this:

trait Foo {
    fn do_something(&self);
}

impl Foo for Bar {
    fn do_something(&self) {
        match self {
            ...
        }
    }
}

fn handle_bar_a(&self) {
    ...
}

fn handle_bar_b(&self) {
    ...
}

fn handle_bar_c(&self) {
    ...
}

impl Foo for Qux {
    fn do_something(&self) {
        match self {
            ...
        }
    }
}

fn handle_qux_a(&self) {
    ...
}

fn handle_qux_b(&self) {
    ...
}

impl Foo for Baz {
    fn do_something(&self) {
        match self {
            ...
        }
    }
}

fn handle_baz_a(&self) {
    ...
}

fn handle_baz_b(&self) {
    ...
}

fn handle_baz_c(&self) {
    ...
}

fn handle_baz_d(&self) {
    ...
}

But it feels to me like it'd be cleaner if I was able to add the helper functions within their respective impls.

I actually ended up doing this:

impl Foo for Bar {
    fn do_something(&self) {
        match self {
            Bar::A => handle_a(self),
            Bar::B => handle_b(self),
            Bar::C => handle_c(self),
        }

        fn handle_a(&self) {
            ...
        }
        
        fn handle_b(&self) {
            ...
        }
        
        fn handle_c(&self) {
            ...
        }
    }
}

Which already works, but the indentation is kinda sucky.

You could also consider an inline module

trait Foo {
    fn do_something(&self);
}

mod bar_impl {
    impl super::Foo for Bar {
        fn do_something(&self) {
            match self {
                ...
            }
        }
    }

    fn handle_a(bar: &Bar) {
        ...
    }
    
    ...
}

mod impl_qux {
    impl super::Foo for Qux {
        fn do_something(&self) {
            match self {
                ...
            }
        }
    }

    fn handle_a(bar: &Qux) {
        ...
    }
    
    ...
}
2 Likes

I hadn't thought about that, it's not bad. Thanks :slight_smile:

I still think there's value in allowing impl Trait blocks to extend the impl of a type. All of these options seem like workarounds for something that feels like it should work to me (as a non-expert Rustacean).

1 Like

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[1][2] 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[3]. 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 :upside_down_face:

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

  1. Technically, all visible API changes have the potential to break code due to loose/convenience rules around name resolution and type inference. However, "fully elaborated" code which requires no nontrivial resolution/inference cannot be broken by "compatible" additions. Thus we consider this class of changes as "minor breaking" (implies a semver minor version bump). ↩ī¸Ž

  2. Manually authoring "fully elaborated" code is not fun — everything must be a fully qualified function call specifying all generic types — but a phase of compilation is to do this elaboration (roughly speaking, lowering HIR to THIR). It's considered an "interesting project" to teach rustc to take a crate and emit it in a "fully elaborated" source form, but it can't just lift THIR to source, as by that point stable things have been lossily lowered to unstable things, both by the compiler (e.g. async's coroutine transformation) and by library macros (e.g. macros using #[doc(hidden)]-private API). rust-analyzer's infrastructure might have a better infrastructure for attempting source elaboration, being focused on IDE usage instead of compilation. ↩ī¸Ž

  3. If the inherent Type::method doesn't exist, then that syntax also looks for <Type as _>::method. This is one of those convenience features which is necessary but contributes to all visible API changes being minor breakage. ↩ī¸Ž

5 Likes

Why not just use an impl Bar block? It's one } and one impl Bar { away from putting the functions inside the impl Foo for Bar.

impl Foo for Bar {
    fn do_something(&self) {
        match self {
            Bar::A => self.handle_a(),
            Bar::B => self.handle_b(),
            Bar::C => self.handle_c(),
        }
    }
}

impl Bar {
    fn handle_a(&self) {}
    fn handle_b(&self) {}
    fn handle_c(&self) {}
    // or if you still just want it as associated function,
    // not a method:
    //fn handle_a(this: &Self) {}
}
1 Like

Because Bar might come from a different crate where we can't just add stuff to the impl Bar block.

Okay I think I see the problem now. Thanks for the elaborate response :slight_smile:

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