pre-RFC: delegation by import

I've written a fair share of procedural macros and I'm not sure I'd want that much power.

That would be use self.0::*, and you'd still need the curly braces for your impl block, but otherwise, yes, it would import all member items.

That's a perfect use case for a one-off macro, and indeed, Serde itself has biolerplate reduction macros like forward_to_deserialize_any! (it's not Visitor, it's Deserializer, but it's the same problem).

It could even be done in full generality with a declarative macro (although I haven't yet tried it), because it's all about substituting identifiers and generating very uniform code.

(Incidentally, I don't think Iterator is a good counterexample, because most of its methods have default implementations, so it's easy to get a correct and performant iterator by just implementing e.g next(), size_hint(), and maybe a couple other methods specifically optimizable to the concrete iterator type.)

1 Like

Wouldn't that basically be the same as Deref?

The function arguments are the hard part, and why there doesn't already exist a general forwarding macro that's much better than just forwarding by hand. forward_to_deserialize_any only works because all of the Deserializer methods have the same signature.

Consider the case of

struct IntoIter {
    raw: iter::Map<vec::IntoIter<Foo>, fn(Foo) -> Bar>,
}

iter::Map provides specific implementations for next, size_hint, try_fold, and fold. Which would you rather read?

impl Iterator for IntoIter {
    type Item = Bar;
    pub use self.raw::{next, size_hint, try_fold, fold};
}
impl Iterator for IntoIter {
    type Item = Bar;

    fn next(&mut self) -> Option<Bar> {
        self.raw.next()
    }

    fn size_hint(&mut self) -> (usize, Option<usize>) {
        self.raw.size_hint()
    }

    fn try_fold<B, F, R>(&mut self, init: B, f: F) -> R
    where
        F: FnMut(B, Bar) -> R,
        R: Try<Ok = B>,
    {
        self.raw.try_fold(init, f)
    }

    fn fold<B, F>(&mut self, init: B, f: F) -> B
    where
        F: FnMut(B, Bar) -> B,
    {
        self.raw.fold(init, f)
    }
}

The latter adds no clarity to the former.

(Unrelated side note: is iter::Map specified to preserve side effects? Because if not, it could provide specialized impls for e.g. nth. And in the case it does add new specialized impls of Iterator methods, the delegation approach likely already forwards the methods (pub use (self.raw as Iterator)::* or whatever) whereas the manual delegation would either have to include a lot more boilerplate proactively or release a new version to hook up to the standard library improvements.)

1 Like

Yes, as I said, I haven't tried it yet, but I'd actually prefer to extend macros (if possible) so that this would be possible in its full generality. Delegation is likely not the only valid use case for matching function definitions. Furthermore, extending an already-existing feature is a smaller effort in terms of design, learnability, and implementation than cooking up a completely new one.

I'd challenge this assertion. Not having the signatures in scope requires jumping to the definition of the function being delegated from. Having to jump around in order to see types is one of the most frequent annoyances I encounter when reading Rust code. (Nevertheless, it is definitely not my main argument.)

It is a good question whether it is specified or ought to preserve side effects, but for now I couldn't find any specialized impls. (Incidentally, I can't decide off the top of my head whether I'd rather have a constant-time Map<[T]>::nth() or one that still dutifully calls the function on each slice item. I'd probably lean towards the latter, but the former is tempting.)

1 Like

It is, and this is why it doesn't override nth. Although I don't remember where the source for this is (it certainly isn't in the docs)

1 Like

Even leaving aside the issues of macros asking rustc "what does the type at this path look like" and the hidden dependency loops that can introduce, at worst this completely removes privacy (as the macro can get the definition) and at best it gives procedural macros scarily powerful unscoped compile-time reflection prowess. I'd definitely like to see the proc-macro interface get smarter (my pet APIs are "do these two paths refer to the same symbol" and "lex this file into a tokenstream with proper spans"), but adding a full-on reflection API is a much larger addition than you seem to imply that it would be.

"Remote derives" are something that seems to be wanted decently commonly. But I think that use case is served perfectly fine by defining a new type from the exposed public API, especially if things can be somewhat simply forwarded

In the case of Iterator especially, what you care about is "ThisIter implements Iterator<Item=ThisItem>" and "Iterator has these methods". The implementation block isn't going to tell you all the Iterator methods anyway (due to the number of default-provided methods with no better implementation), so you have to jump to the Iterator definition to see the full list of methods anyway.

I can understand this argument for inherent impls, but for trait impls at least the source of truth for impl headers is the trait itself, not the implementation block. Would you be more accepting of delegation if it only work for trait method implementations rather than generally on all impl blocks? (I think the restriction would seem arbitrary, but it is true that the header is already defined for the trait impl but needs defining for an inherent impl.)

So, I'm not entirely sure what you're envisioning, but the issue with newtype wrappers for derives is that there might not be any upstream implementation to forward to. One example would be deriving serde::Serialize for a 3rd party struct that has no knowledge of serde. Right now it just doesn't work.

What I'd really like (and you might have been thinking something along this route) is that the derive for a newtype could automatically apply the derive to whatever it wraps, but have that inner derive only exposed/exist for code specific to to the newtype derive.

There's been an interesting discussion on runtime reflection that runs into the same issues. I think a basic approach would be to allow structs to by default only expose pub fields, and perhaps opt-in to exposing non-pub fields. And I think if you limited it to newtypes, it would greatly reduces the "unscoped"-ness of the reflection abilities, or at least make the dependencies pretty visible.

I'm not entirely sure in this case that implementing remote macros would be easier than adding simple delegation. Remote macros seems like a much larger and ill-defined problem space than function delegation by type/field.

Ah, ok. I didn't realize you could use * to delegating everything (even though I knew that worked for importing names outside impl blocks).

I had left off the curly braces specifically to distinguish delegating an entire trait (including type and const members) from delegating certain functions or whatever else use could be used for. Since use is not otherwise allowed before the curly braces, it would be unambiguous to parse.

No, it would not, because Deref allows using all members and traits of the target type (with caveats), whereas delegating a trait delegates only that trait.

Regarding more power for procedural macros, I'm holding out for specialization. In conjunction with procedural macros one can achieve some surprising things.

The whole "delegate a trait" business also makes me think we may want to allow trait delegation use <self.0 as Iterator<Item=Foo>>::* (so the trait bound actually restricts the import).

This proposal has the advantage that items can be easily renamed, e.g.

use self.0::{foo as bar};

I think this should be disallowed, because it makes the code harder to understand (to which type/trait do these methods belong?)

What about methods?

use self.get_inner().0::{foo, bar};

fn get_inner(&self) -> &Baz {
    match self {
        Foo(x, y) => x,
        Bar(x, y) => y,
    }
}

I was not talking about remote derive macros.

Could you clarify a little bit then? Rereading that paragraph a couple of times, I see now that's not what you meant, but I'm still not sure I follow what you were enough to articulate it. My new impression (still possibly wrong) is that you'd like to extend the macro system to be able to see remote method/function definitions instead of having specialized delegation syntax?

If a macro is able to see the full signature of a trait, and that is (somewhat) logically extended to types as well, you effectively have everything you need for a remote derive.

What I'm saying about the "remote derive workaround" is that given some struct

struct Foo { /* ... */ }
impl Foo {
    fn spam(&self) -> Spam;
    fn eggs(&self) -> Eggs;
    fn new(spam: Spam, eggs: Eggs) -> Foo;
}

Even though you know "nothing" about the struct, you can do

#[derive(Serialize, Deserialize)]
struct FooHelper {
    spam: Spam,
    eggs: Eggs,
}

fn serialize_foo(foo: &Foo) {
    FooHelper { spam: foo.spam(), eggs: foo.eggs() }
        .serialize()
}

fn deserialize_foo() -> Foo {
    let helper = FooHelper::deserialize();
    Foo::new(helper.spam, helper.eggs)
}

Obviously the real code is more complicated to handle less-copy code, but the idea is this. This is more truthful than any derivation of a shim from a remote definition as well, as it explicitly only relies on public API.

Worst case scenario, you can read the dependency's source files in the macro :stuck_out_tongue:

To the trait that is currently being implemented. The point is that x and y may be different types, both of which implement a common trait. This currently cannot type-check, which is why I mentioned enum impl Trait, some forms of which would allow it to work seamlessly.

I don't think this syntax would make much sense for inherent methods (where signatures may diverge), although I guess in some cases assigning some meaning to it would be possible.

If you allow method calls and field accesses to the left of ::, why not allow match, or in fact, arbitrary expressions, other than ‘it would be difficult to parse’?

and also @samsieber:

Just to clear up any misunderstanding, I explicitly do not want a macro to be able to harvest type information from an arbitrary context. As I've expressed previously, I'd very much like to see function signatures in the place they are used.

Instead, I was proposing to extend regular declarative macros just enough (syntactically) that a macro like forward_to_deserialize_any can be implemented generally, without baking in the signatures. Such a macro could be called in an impl block in order to generate forwarding code. This only requires macros to be able to capture entire function signatures or perhaps even more conveniently, only select parts of them.

I did not propose, and I would not like to see, the addition of type information to macros, nor do I want first-class support for calling derive macros on remote types.

Maybe take the macro discussion to a separate thread? It’s itself another proposal so...

Please re-read my suggestion above. I never said anything about arbitrary expressions in paths! I'd only allow resolve to accept more paths – those of fields or methods with only self-arguments of self.