Any future plans for impl for multiple types?

greetings everyone

I realize that right now Rust has no support for "wholesale" impl trait for multiple types.

The current solutions in Rust 1.x - 1.79 are usually writing macros or using marker traits or other kinds of workounds (crates with such macros?).

I would like to know if there are any concrete plans to add this kind of functionality in future versions of Rust to the impl trait (simplistic example):

impl trait for type1, type2, type2, {
   fn some_func(&self) { 
           something; 
    } 
}

Ideally it would also support list of types inside generic bounds too.

thank you

I have zero idea what you are trying to say with that.

There can't be concrete plans to add something to the language that is impossible.

Every value in Rust has to have a type. There would be no single type to assign to self in your example.

(No, "just make Rust have union types" doesn't work, read up on it. The existence of generics basically prevents any sane implementation, and restricting unions to trait impls would be an ugly hack and special case that is definitely worse than generating a couple impls using macros.)

Those are not "workarounds", writing precisely one impl per type is the proper solution.

4 Likes

My apologies for lack of clarity in my question.

I was looking for ease of use feature to reduce the need for macros.

Here is an artificial example to illustrate what I was asking about.

trait HasLen {
    fn my_len(&self) -> usize;
}

impl<T> HasLen for [T] {
    fn my_len(&self) -> usize {
        self.len()
    }
}

impl<T> HasLen for Vec<T> {
    fn my_len(&self) -> usize {
        self.len()
    }
}

impl HasLen for String {
    fn my_len(&self) -> usize {
        self.len()
    }
}

impl HasLen for &str {
    fn my_len(&self) -> usize {
        self.len()
    }
}

Right now as I stated in my question this needs a macro or even 2 macros or 4 separate impls.

I was looking for a more straightforward impl such as:

impl<T> HasLen for Vec<T>, [T], &str, String {
    fn my_len(&self) -> usize {
        self.len()
    }
}

I did not ask for any Rust union types.

I was was looking for easier ability to write a single impl for multiple types (some concrete and some generic) which is currently done by a Rust macro.

Often the same impl is needed for (example) all integers from u8 .. u128 and/or i8 .. i128 which is typically achieved via a macro.

It's just an ease of use request to reduce the need for macros.
If this is not practically possible, I have no problems, I was just asking, politely, if this is something that could be done in future.

1 Like

Why should we reduce the need for macros? Macros are in the language because they are sometimes useful. If you need macros, use macros.

I am curious how you think it could work, mechanistically. It seems to me that your proposal is very shallow, as it does not consider even the very basic surface question of what type Self should have in this sort of multi-impl block.

3 Likes

I guess OP is talking about some kind of syntax sugar, which would essentially work the same as macros (i.e. copy-paste come code while filling the placeholders), but look more like the ordinary implementation.

4 Likes

Macros are a pain, often a habbit hole of complexity. As they are now, I only use them for the simplest most straightforward things as they are often orders of magnitude more complex to write and mantain than the actuall program I'm working on.

I understand why the OP would want that, but I agree with you that it doesn't seam feasable.

1 Like

The problem I see with this is that what you proposed is just a macro with different syntax.

In your example there's nothing at the type level guaranteeing that there's a len method on all those types. Thus the resolution of that method would have to work exactly the same as it works for macros.

14 Likes

I don't know if you're familiar with provided methods (judging by your register date, you probably should), but let me write it here for the others.

What you describe fits provided methods quite well. Such method is defined in the trait itself, not in impls. In case you want to redefine it for a partucular type, write it in the impl trait` block.

trait MyTrait {
    fn some_func(&self) {
        something;
    }
}

// copy&paste is reduced, and you can write a macro for these lines:
impl MyTrait for type1 {}
impl MyTrait for type2 {}
impl MyTrait for type3 {}

impl MyTrait for type4 {
    fn some_func(&self) {
        // different implementation here
        todo!();
    }
};

In this case, inside the trait, you can interact with self based on what's in the trait or trait bound, for exapmle trait MyTrait: OtherTrait will mean that self will have methods from OtherTrait:

trait OtherTrait {
    fn other_fn(&self) ... 
}

trait MyTrait: OtherTrait {
    fn some_func(&self) {
        self.other_fn(); // will compile
        ...
    }
}
4 Likes

This is, essentially, asking for C++-style template substitution, which I think most Rust programmers and nearly all Rust contributors would strongly prefer not to implement (and not to try to square with Rust's existing type system). It's not an unreasonable thing to want, but it's one that is very hard to square with the language as it exists today.

The trouble with your example is that while all of the methods you wish to call are named len, and all of them have the signature () -> usize, they do not share any properties that the type system Rust has is able to generalize over. There is not, for example, a type consisting of the union of types that contain a len() -> usize method (hence the responses about whether Rust can have union types or not), nor is there any way in Rust today to generalize over types by characteristics alone.

So far as I know, there is no future plan to allow this capability. If there is, nobody has written an rfc for it, or brought it up within recent memory on IRLO. You could be the first, but I would strongly encourage you to work with the language as-is for a while first, and to iterate on the design using other tools (macros, proc macros, or full-on code generation) first to nail down the semantics and the sharp edges before trying to propose a language change. Once something's in the language, it's effectively set in stone at this point - we're likely past the time when Rust can tolerate sweeping removals.

6 Likes

You skipped them most interesting part of the whole thing. How would your trait call len that's implemented for all these types, there? It's not callable without special trait and a lot of dance.

Are you really sure? Have that ever been investigated not among the guys who play with types but to use Rust in real-world tasks?

C++ templates are join to write, pain to use while Rust traits are the opposite. I have no idea how feasible is it to add them to the language, but I would love to see them as opt-in feature.

Have there been any attempts to see how much would it take? After reading that forum I have found few techniques which can simulate them with mix of macros, const experessions and accessor traits, but they are still PITA to write and use. Maybe even worse that C++ templates: at least when there's a bug in my C++ template I get a stack instantiation trace during compilation while when I try to use C++-template-simulation I'm just notified that none of 283 implementation of trait can not be used in this particular place which is harder to understand than what C++ emits.

And it's not as if crates that are doing these emulations are rare.

And why is that the problem? Why should everything be strongly typed at this point? C++ and, especially, Zig, show us that one can easily just use dynamic types for that work and in many cases (although not always) this makes for much easier to understand code.

1 Like

Thanks for the comments.

I find dealing with Rust macros, as a developer is (to me!) to be the most complex part of Rust. Using macros is a dream. Developing is not so.
Development and debugging of Rust macros is hard.
IMO - harder that ownership and borrowing (that is easy).
Harder than async.

However, macros are definitely a required part of Rust, no debate.

So I was looking for a simpler way, more accessible to beginners and intermediate Rust developers - and to help boost adoption as well as productivity and quality of life.

From what I read online, I am far from alone, be it declarative or more even complex procedural macros.

Even David Drysdale in his superb 2024 book Effective Rust says in Item 28, page 211 "Use Macros Judiciously".

Rust ownership and lifetimes in early Rust versions were much more convoluted to use and understand than in Rust 1.79.
Rust made them much more accessible today, a job well done.
The implicit coercion rules in Rust work exceptionally well too.
Another job well done.

Macros? Not so much, IMHO.

So essentially I was wondering if some of the current Rust macro common use-cases could be made simpler than resorting to macros. Or make the macros (much) easier to develop and debug?

I don't know how to solve this and intricacies of all edge cases, and if I did, I would just do an RFC and implement it! LOL 8^)

PS yes, I knew about default functions in traits, I was just trying to illustrate with the simplest example what I was trying to achieve, the problem I was looking to solve.

thanks for your comments, admins please feel free to close this topic off as "impossible", "naive", "poorly thought out" etc

Peace.

1 Like

In particular, it's asking for ad hoc name resolution, where the names are resolved as though instantiated through a macro.

Parts of template substitution I've expressed openness to considering before. For example, it might be ok to allow instantiation-time errors for things like fn foo<const N: usize>() -> [u8; N * 2]; -- after all, you already get instantiation-time errors with fn foo<const N: usize>() -> [u16; N]; on stable.

But anything like The Dreaded Two-Phase Name Lookup - The LLVM Project Blog I'd draw a hard line. I think it's important that macro-like name resolution be restricted to macros only. No adhoc extension points; only the principled ones that traits offer.

Macros aren't bogey-men. They should be avoided where a function works fine, but for something like this where you want token-expansion behaviour, no need to reduce the use of them.

4 Likes

What you're talking about is a huge enhancement, and may require a new language entirely.

If reducing complexity is the goal of your request, then adding something like C++ templates to Rust would be very counterproductive. Generics in Rust are complex enough! Templates are notoriously complex also, so having both would be a huge mistake IMO. The complexity of templates would impact both the Rust devs and the users of the language.

You may say: but how about a simple version of template-like programming? I don't think that's practical, but I am not a language designer and if you think it is practical then you are free to design and propose such a thing by creating an RFC. Creating an RFC is the only way to have the Rust designers take a significant proposal like this seriously. The rust internals forum is where RFCs and other detailed proposals are discussed.

2 Likes

Can someone explain why? My POV is biased, of course, for I know how to use C++ templates well, but AFAICS Rust's generics and C++ templates do the exact same work, just in different order.

And to reduce complexity of code written in the language you have to increase complexity of the language, this works the same way with all enhancement requests.

Worse: generics in Rust are much, much, MUCH more complicated than templates (complexity have to live somewhere thus generics, by design, move complexity from the instantiation moment to declaration moment… but is that the right decisions for all use-cases?).

But it's not clear why something that's more-or-less a subset of that complexity should be rejected.

Can you explain what's so complex about templates? Two-phase lookup? That's only needed in C++ because it's grammar is undecidable. Rust solved that particular issue with turbofish operator thus there are no need for that in Rust.

You kinda need to if you want to receive sane error messages. C++ template-derived error messages are notorious for being hard to read because they are long and include long sequence of instantiations, but compared to error messages from proc-macro of the form “that procmacro generated something thay I couldn't compile, but I wouldn't even tell you what and cargo expand doesn't work because there are error, you know” then are pinnacle of cleanleness. At least with template errors all the information is there if in large braindump, macros often lead to error messages that are plain out wrong or useless.

Rejecting templates on the basis of error messages that are hard to read looks strange to me when their replacement is worse.

I don't think that introducing yet another feature will help beginners learn Rust. If anything it would increase the amount of stuff they need to learn, which would likely slow them down.

But this doesn't mean "don't use them at all"! In fact I would argue that your use case is one of the better ones for macros, as it is not doing anything fancy: no custom syntax, no complex rewriting, you're just using it a very simple templating language and that's the best usecase for macros.


That said I do think that macros could be made simplier, the current macro_rules! syntax is kinda arcane and a bit boilerplaty for simple one-pattern macros. Macros 2.0 should help with that, but it's likely we won't see them soon.

7 Likes

Not many people will share that opinion, as you'll find. It looks to me like you should learn macros better, instead of asking for a substitute language feature. (Incidentally, this is a recurring pattern in the behavior and attitude of Rust beginners.)

Macros are a purely syntactic construct, and there isn't anything hard about syntax per se. It's the most uninteresting part of the entire language. I honestly cannot fathom how $(impl Trait for $type)* is any harder to grok than your proposed alternative.

Maybe you just need to get used to the matcher syntax more.

2 Likes

If all the types implement the same trait, then you can use blanket implementation, like this:

impl<T> MyTrait for T where T: OtherTrait + AnotherOne
{
   // implementation
}

However, that generally cannot be done if the required functions are not part of the same set of traits (because different traits can declare functions with the same name, but with completely different purposes).

You'll notice I never said "error messages" in my post. Indeed, the ones for [u16; usize::MAX] are actually worse than templates today, because they come with no context at all.

Complex error messages with lots of context aren't the problem. It's the error messages that don't happen at all because of the compilation model that are the problem.

2 Likes

How are they the problem? Because you are forcing someone like mythical “future you” to deal with the fallout if/when something would change?

I would say that in case of internal API that's a pretty good tradeoff: deal with cases that are happening right now, today, leave the rest for “future you”.

That's what happens with macros, anyway.

Exactly. And thus if you want that behaviour, a macro is fine.