Idea: allow traits to define un-overridable default methods


#1

The Iterator trait has a bunch of useful default methods. Implementors of the trait can choose to override these default methods to the point where they don’t do what they claim to do. I would rather be sure that with an implementor of Iterator I can always call zip(), and it will do exactly what I expect it to do.

Defining zip() as a function that takes the trait object as an argument gives this assurance, but you loose the convenience of being able to call zip() on the actual Iterator object.

These un overridable trait methods could also maybe be allowed to use impl Trait without having to worry about “traits returning traits” problems, since they would really just be concrete functions with the added shortcut of being able to be called with by iterator object.


#2

You have some assurance about Iterator::zip because any overridden implementation still has to return the correct std::iter::Zip type. But if you don’t trust implementors to do the right thing, then how do you know they’re not already doing something funny in next()?


#3

There was some debate about this on the RFCs for specialization, and afaik there was a fairly broad consensus that the efficiency gains from overriding/specializing methods are much more valuable in practice than the kinds of bugs that could be ruled out by forbidding overriding/specialization, mostly because the inability to “override” or specialize a certain method doesn’t have all that much to do with correctness in practice. You can still write an incorrect impl in pretty much any impl X for Y block even when there’s no default impls involved. There may even be cases where specialization is the only way to achieve correctness because there is no single default implementation that’s correct for all types.

To me, an un-overrideable method would only make sense in cases where there’s no way anyone could ever invent a type that would benefit from a specialized implementation. But I can’t imagine how any method would ever satisfy that. Consider that in C++, many containers have a size() method and an is_empty() method, and the default implementation of is_empty() is the utterly trivial size() == 0. But there are some containers (such as linked lists) where size() is an O(n) operation and but is_empty() can be done in O(1) just by checking if some pointer is null. It’s hard to imagine a more trivial method than is_empty(), yet even it benefits dramatically from specialization.


#4

I’m expecting next() to possibly be funny since that is definitely user defined. But I’ve never found a reason to override the iterator helper methods, and I’ve yet to see it in the wild. So i was thinking, why allow it?

But after reading by lxrecs post, I can see how there can be specialization benefits to many of the iterator helper methods. If I ever really want the generic zip, I can call the generic function myself. But usually, I would want implementors to possibly implement a more specifalized/better version of zip, even at the risk of them messing it up. Makes sense!


#5

I haven’t seen anyone override Iterator::zip in particular. Actually it would be hard to do, if not impossible, because Zip is an opaque type with no public constructor at all. I think a third-party implementation of zip could only diverge, not return at all. Same for all the other Iterator methods that return opaque types – this is probably the best way to make something un-overridable.

I’ve definitely seen other Iterator methods overridden though, and a prime one to consider is fold. I went through myself to make sure most of the std::iter types implement a custom fold. For example, Chain::fold can do a much better job of folding all of its first part, then all of its second part, rather than the default fold calling next() item-by-item. For other wrapper types, while they may not have a benefit like that themselves, they can forward to their inner type’s fold in case it’s something optimized like Chain.


#6

Discussion of non-overridable methods in conjunction with overlapping marker traits: https://github.com/rust-lang/rust/issues/29864#issuecomment-368478686


#7

That is usually done when you are writing an iterator for a custom data structure. It’s done very commonly, and it is almost always necessary for the most efficient implementation. For example, size_hint can give rise to an enormous speed boost in collect. You would certainly not want to make it un-overridable.

If you want an un-overridable function, write a free fn without a trait.