Conditional default trait method impls

There's been an issue for a long time with code such as the following:

fn require_sized<T: Sized>(_: &T) {}

trait Foo {
    fn foo(&self) {
        require_sized(self);
    }
}

struct Bar;
impl Foo for Bar {}

fn main() {
    let x: &dyn Foo = &Bar;
    x.foo();
}

Specifically, there's no way to provide the default implementation of Foo::foo without also breaking the object safety of Foo by adding Self: Sized onto the trait or method.

This is an artificial limitation, because although the dyn Foo type does implement the Foo trait, it does not use the default implementation of Foo::foo - a trait object's method implementation always defers to the corresponding method on the underlying type (where Self: Sized holds).

The problem with just relaxing this limitation is that there are other unsized types where the default trait implementation would not hold.

I came up with the following idea to solve this:

trait Foo {
    fn foo(&self) if Self: Sized {
        require_sized(self);
    }
}

The idea behind if Self: Sized is that when a user implement the Foo trait, a default implementation will only be provided if the bound Self: Sized holds. If the bound does not hold, then foo is just a normal method that the user must implement themselves.

As a result, the default implementation is free to rely on these additional bounds.

3 Likes

See also.

I believe specialization gives this to you:

trait Foo {
    fn foo(&self);
}

default impl<T: Sized> Foo for T {
    fn foo(&self) {
        require_sized(self);
    }
}

Though maybe that could be spelled

trait Foo {
    fn foo(&self) default where Self: Sized {
        require_sized(self);
    }
}

That's slightly different because it actually implements the trait for all those types, which you might not want. Also, if the trait has more than one item then this is not possible.

I'd love a way to have more conditionals on when a default implementation is available. Imagine size_hint() defaulting to (len(), Some(len())) if you implement ExactSizeIterator, for example, or Iterator::next() defaulting to try_for_each(ControlFlow::Break).break_value() if you've overridden try_fold.

(Or even, for compilation time improvements, only implementing fold in terms of try_fold if you're provided an implementation for the latter.)

2 Likes

It doesn't implement the trait for all those types.

RFC

The specialization design in this RFC also allows for default impls , which can provide specialized defaults without actually providing a full trait implementation

[...]

This default impl does not mean that Add is implemented for all Clone data, but just that when you do impl Add and Self: Clone , you can leave off add_assign

And you can have as many items in there as you want (or don't). More examples.

My mistake, it seems that does solve the same problem :slight_smile: - I was getting confused between the use of default on an impl item and default on the trait implementation itself.

1 Like

I agree the "phrasing" is confusing.

I wonder if it's possible to split this part of the RFC off ahead of time, e.g. if you could only have one blanket default impl per item (so there was no actual specialization involved).

2 Likes

It does, but IMO having this kind of feature connected to specialization is a big mistake. Even with the current implementation on nightly, the fact that such a default impl (big misnomer btw.) creates issues with coherence/overlap (I could try to produce a code example if you like) clearly demonstrates that connecting conditionally available default implementations of methods with specialization is a bad idea. (But of course the biggest problem is that - technically - default method implementations aren't actual specialization, they don't come with any soundness issues, if implemented correctly, and the feature is unnecessary held back if it has to wait for specialization to solve all of its soundness issues before it can be stabilized.)

I think that the thing @scottmcm proposes is the right way forward in the long run: considering other forms of conditional default implementations that make sense. We should try to make this a separate thing. In my opinion we'd want to have e.g. things like choosing different default implementations based on which methods were provided manually. We could support different (minimal) sets of methods that need to be implemented for a trait. To name a stupid/simple example, someone implementing PartialOrd could choose to implement partial_cmp or lt or le or gt or ge. The remaining functions could be implemented in terms of the manually provided one, whichever it is. The ExactSizeIterator example that @scottmcm also gives suggests things like the ability to implicitly/by default implement PartialEq's methods in term of partial_cmp, if the type also implements PartialOrd; but considering entire trait hierarchies like that seems even more complex - maybe we shouldn't try to do too much at once?

I also think that a minimal viable product for default method implementations with additional trait bound, similar (at least in terms of capability, perhaps also in terms of syntax) to GHC Haskell's DefaultSignatures feature, can be a good way to get something useful eventually stabilized without needing to get specialization stabilized and also without needing to finish and stabilize a complete general solution/framework of conditional default method implementations.

It's already part of the RFC, and whatever default impl capabilities arise will have to interact with specialization in one way or another. Or more accurately, coherence will have to contend with both.

Maybe I'm missing your point or some proposal.

(Open) RFC 628.

I believe this also came up before, when a std trait was modified to deprecate a previously required method and add a new method that did things more correctly. I was actually looking for it earlier today for unrelated reasons, but failed to find it, if anyone happens to know what I'm talking about. Anyway, if my memory serves: either method could be implemented in terms of the other, but if both default bodies were included in such a way, you would get infinite recursion unless you overrode one of them. That was deemed too foot-gunny, and instead only one of the default methods is in terms of the other. Ideally, it would have instead been "you have to implement method a or (deprecated) method b."

However, this seems a lot more complicated to me than the ability to have a bound on default methods / partial implementations (i.e. the OP). It's a whole new thing that will need to be first approved by RFC. Splitting off partial impl will require settling some questions (bike-shed and probably the big one is "can the methods within be final'), but still seems more easily obtainable to me.

Can you flesh this out a bit and/or sketch it in Rust syntax? Is it any different from splitting off partial impl from the specialization RFC?

I found it: RFC 2930: ReadBuf which was updated to not provide a default body for read after this comment and the following conversation. (Not yet implemented much less stabilized.)

A previous sortof-related thread: Brainstorming: allowing implementing just `try_fold`, and getting `next` free

This makes me think of a potential way here of like a "bundle" kind of thing, where you implement that to get an implementation of a different trait (or maybe even a set of traits).

This would be sortof like the thing that people seem to want to do often of "anything that implements this trait gets a free implementation of Iterator" that doesn't work because of the blanket impl. But maybe by making that part of the bundle/mixing/whatever it would be ok because the rule would be more specific and the compiler would check it more specifically.

(Very undeveloped idea, as I'm sure you can tell.)

1 Like

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