pre-RFC: delegation by import

So recently I posted this idea on Twitter:

The positive feedback makes me think it may be a good idea:

  • For structs only (at least for an initial impl)
  • use self.foo::bar 'imports' the bar method of self.foo by generating the following function: fn bar(&self, x: bool, other: Args) -> ReturnValue { self.foo.bar(x, other) }. The self borrowing mode will be that of the original method, as will be the rest of the signature.
  • Visibility works as usual; pub use to publicly delegate.
  • If the bar method already exists, it will of course be the same error as if the method had been written twice.
  • Self types in the original methods will of course keep being made explicit before delegating.
  • The private-in-public lint will probably need to be updated.

Would this be worth a try?

10 Likes

I'd see an improvement to the delegation story in Rust as a HUGE win. This seems like a reasonable approach that would make delegation much more declarative.

1 Like

Will this import attributes on the delegated method as well (e.g. #[doc] and #[inline] being two very relevant ones I can think of).

1 Like

The syntax seems a bit cryptic, though that alone is not a show-stopper to me. More discouraging is the fact that it doesn't seem to be easily extensible to enums (let's forget for a moment that the enum case is currently impossible to type-check anyway, because we still don't have enum impl Trait).

I guess one could do this:

use (match self {
    Foo(x, y) => x,
    Bar(x, y) => y,
})::{foo, bar};

but it looks quite ugly.

If we're reusing existing keywords for delegation, how about this instead?

for {a, b, c} use self.x;

for {foo, bar} use match self {
    Foo(x, y) => x,
    Bar(x, y) => y,    
};

It reads almost like English, and doesn't seem to conflict with any grammar production valid inside an impl block.

1 Like

I like this idea. It would be really good for newtype wrappers.

Would you include nested uses or just one level? use self.foo.bar.baz::foo?

How about in trait implementations? I could see some corner cases - for example:

struct Foo(Bar);
impl Iterator for Foo {
    type Item = <Bar as Iterator>::Item;  // Or use self.0::Item ?
    use self.0::next;  // what about `Iterator::count()` etc. - do they get the delegated type's implementation or the trait's provided version?
}
1 Like

I believe that this exact syntax has been suggested before, though I'm at this moment unable to find a reference to where it was.

Personally, with an answer to how documentation is handled (probably allow #[doc(inline)] and behave like other re-exports?) and a "Future Possibilities" for how it can be extended to enums (or why it shouldn't be?) this is probably the best option for syntax of a "minimal delegation" first step RFC.

1 Like

I think it should, at least for doc, inline, must_use and lint settings.

Edit: thinking a bit more about it, we should at least inline the delegator methods, and update to inline(always) if the delegatee has it.

If we also allow to import from zero-arg methods, it could be made to work with enums and even traits, e.g. if we have a method inner(&self) -> &Inner we could use self.inner.inner_method;.

I'd leave out more complex variants because they'd easily get unwieldy.

Sounds very useful but I don't see the connection with use. I think it has to be inside an impl block, so you have the right type parameters and bounds as context for the methods, too. Maybe it could use an attribute somehow?

Let's try a completely different syntax for fun.

struct Foo<T> {
    field1: T,
    field2: Bar,
}

impl<T> Foo<T> where T: Clone {
    /* New syntax */
    delegate fn frob = field2::frob;

    /* is equivalent to */
    fn frob(&mut self, x: i32) -> i32 {
        self.field2.frob(x)
    }
}

impl Bar {
    fn frob(&mut self, x: i32) -> i32 { unimplemented!() }
}
4 Likes

My recollection is that the last conversation about this was leaning towards

impl<T> Foo<T> where T: Clone {
    delegate frob to self.field2;
}

I think the reason for not using use was that one might potentially want to allow things like use MyEnum::*; inside an impl for its normal use, and thus it shouldn't get co-opted for delegation.

14 Likes

How about

use self.foo for fn bar;

to clarify, pub use self.x::{a, b c} means that I can take struct Foo and have instance foo and say foo.a(); foo.b(); foo.c(); and these will delegate to field foo.x.a(); foo.x.b(); foo.x.c()? Is there a particular reason why macros couldn't accomplish the same thing?

Because they don't have access to function signatures, and supplying signatures would defeat the purpose of delegation.

1 Like

The connection with use is that it can be thought of creating an alias, or 'importing' something at an alternative location. So just like in Java where you can reference both static (SomeClass::someMethod) and instance (someObject::someMethod or even this::someMethod) to be used in functional position, use could also be, if you forgive the pun, dual-use.

The difference would be to allow . as joiner after use, and it would require the left hand side to be self or an index, field or function (with no additional argument but self, &self, &mut self, Pin<self> or any other valid receiver) of it.

This way, the syntax is non-ambiguous (because for now . is not allowed), and I think we can craft good error messages in case someone writes use my_module.MyType by mistake.

3 Likes

I would suggest pull frob for self.field2 instead of delegate. The latter is quite a long keyword and rust is famous for its keywords shortcuts :smile:

To be clear, the thing being discussed here is instead of writing this:

struct Foo {
  bar: Bar,
}

impl Foo {
    fn do_stuff(&self) {
        self.bar.do_stuff();
    }
}

You would instead write:

struct Foo {
  bar: Bar,
}

impl Foo {
    use self.bar::do_stuff; // Replace with your favorite alternative syntax.
}

Right?

This seems like a super tiny paper cut. One hardly worth modifying Rust for. Even if the signature of do_stuff is more complicated I still don't think this is a big enough problem to add more syntax baggage to Rust, and this isn't something I'd want to blow the language strangeness budget on.

3 Likes

If normal use clauses are to ever be allowed inside impl blocks as @scottmcm suggested, this (and the OP's proposal) may conflict with the goal of having a backtracking-free parser for Rust. My own proposal to put for first doesn't have this drawback.

When you only delegate a single method with a known signature, it may seem not worth it, but when it's used with a whole trait or even multiple ones, it really starts paying off. This has been a somewhat oft-requested feature that would enable the sorts of patterns that in other OOP languages are usually implemented via inheritance.

3 Likes

Just as a counterexample, consider forwarding serde::de::Visitor. With this adjustment it's (without the 'de-dependent methods because lifetime complexity)

impl<'de> Visitor<'de> for MyType {
    use self.raw::{
        expecting,
        visit_bool,
        visit_i8,
        visit_i16,
        visit_i32,
        visit_i64,
        visit_u8,
        visit_u16,
        visit_u32,
        visit_u64,
        visit_f32,
        visit_f64,
        visit_char,
        visit_str,
        visit_string,
        visit_bytes,
        visit_byte_buf,
        visit_none,
        visit_some,
        visit_unit,
        visit_newtype_struct,
        visit_seq,
        visit_map,
        visit_enum,
    };
    serde_if_integer128!(use self.raw::{visit_u128, visit_i128});
}

I really don't want to write out the explicit version on my phone; it's a lot more boilerplate to read, even if your IDE can generate it for you.

For a more reasonable example, consider forwarding all the Iterator methods for your IntoIter wrapper type.

The real advantage comes from forwarding a large trait, but it adds clarity to the single case as well as the large one. I think it's a clear positive in some form at least.

The syntax of allowing . in a path (use self.member::{func}) doesn't require backtracking (or equivalently, infinite lookahead). It's even LL(3) in the simple case (use, self, .; the . means it's a delegation use). Even in worst case "you don't know what kind of use tree it is until after you parse the main tree", it doesn't require backtracking/infinite lookahead; you just delay the decision. Parse the use tree and then decide what kind of use treee it is. Backtracking is a problem when you need to reinterpret how you've parsed what you've already passed. If it parses the same on all paths, just delay the decision.

5 Likes

It seems obvious to extend this to work on a whole trait:

struct MyIter<'a, T>(std::slice::Iter<'a, T>);

impl<T> Iterator for MyIter<'_, T>
where
    T: Sync,
use self.0;

equivalent to:

struct MyIter<'a, T>(std::slice::Iter<'a, T>);

impl<'a, T> Iterator for MyIter<'a, T>
where
    T: Sync,
{
    type Item = &'a T;
    #[inline]
    fn next(&mut self) -> Option<&'a T> {
        self.0.next()
    }
    // delegates all the other Iterator members too...
}

If there were an API for a procedural macro to pull in the text of a given import path, then this could be implemented as a macro without adding any special builtin syntax.