Opt-in to overlapping impls and explicit impl selection

there should be a way for a trait to opt-in to non-specializable overlapping impls and require explicit impl selection at use. additionally, a set of "preferred" impls can be provided, so that a currently non-overlapping trait can bemade overlapping. this would be especially useful for Pattern, to make it accept Fn(&char) in addition to Fn(char).

for example, you could have an overlapping trait:

overlapping trait Pattern<'a> {
  ...
}

and then preferred impls (which act just like existing impls):

// syntax chosen to allow transforming a trait into an overlapping trait backwards-compatibly
// aka we just use existing syntax for non-overlapping impls
impl<'a, 'b> Pattern<'a> for &'b str {
  ...
}

...

and then the new overlapping impls:

// these don't participate in normal impl resolution
overlapping impl<'a, F> Pattern<'a> for F where F: FnMut(&char) -> bool {
  ...
}

when using a function, unless impl selection syntax is used, only the preferred impls are visible. (this can be broadened later, but it would introduce backwards-compatibility caveats similar to the IntoIterator/Deref interaction we had with arrays.) in other words:

// these all check the preferred impls
"foo".find("bar");
"foo".find('a');
"foo".find(['a', 'b']);
// this also checks the preferred impls, so it infers FnMut(char) -> bool
"foo".find(|c| c == 'a');

that is to say all existing code would remain working.

additionally, you'd be able to write this:

"foo".find<_: (impl<'a, F> Pattern<'a> for F where F: FnMut(&char) -> bool)>(char::is_ascii_lowercase);

which is very verbose, but makes a good starting point. (as we said, this can be broadened later, but it would introduce backwards-compatibility caveats similar to the IntoIterator/Deref interaction we had with arrays.)

As with nearly every one of your posts, can you please provide some more detail?

13 Likes

You mean something like this?

unsafe? { #![allow(overlapping_impls_that_can_cause_unsoundness_but_i_dont_care)] }

impl<T> Deref for T {
    type Output = str;

    fn deref(&self) -> &Self::Output { "foo" }
}

I created a trait mask topic a couple days ago which overlaps with this concept.

Do you have a specific plan for how someone would designate and invoke an alternative implementation without a wrapper class?

kinda hard to provide more detail when you don't know what the syntax for it would look like.

the basic idea is:

  1. the trait has to opt-in to it
  2. impls of the trait can be overlapping
  3. you choose which trait impl to use at use location, probably through using the impl line, without the impl block obviously.

more like:

overlapping trait Foo {
}

impl<T> Foo for T where T: Bar {
}

impl<T> Foo for T where T: Baz {
}

impl Foo for MyThing {
}

fn foo<T: Foo>(t: T) {
}

foo<_: (impl<T> Foo for T where T: Bar)>(MyThing); // inference
foo<MyThing: (impl<T> Foo for T where T: Bar)>(MyThing); // explicit
foo<_: (impl Foo for MyThing)>(MyThing); // oh yes

this can currently be emulated with newtypes as follows:

#[repr(transparent)]
pub struct FooForBar<T>(T);

impl<T> Foo for FooForBar<T> where T: Bar {
}

impl<T> FooForBar<T> {
  pub fn from(t: T) -> FooForBar<T> {
    FooForBar(t)
  }
  pub fn from_ref(t: &T) -> &FooForBar<T> {
    unsafe { ... }
  }
  pub fn from_mut(t: &mut T) -> &mut FooForBar<T> {
    unsafe { ... }
  }
}

but obviously the newtype-based approach requires the use of unsafe {} for the ref/mut projections (and doesn't play the nicest with Pin unless you also do from_pin and whatnot).

also, one obvious restriction is that you wouldn't be able to create non-overlapping impls bounded by overlapping traits, so the following would be an error:

trait Qux {
}
impl<T: Foo> Qux for T { // error: Foo is overlapping
}
impl<T: (impl<T> Foo for T where T: Bar)> Qux for T { // this is allowed tho
}

What's the usecase for this? If you already have to be explicit about which impl to use I don't see how it helps to have the impls be attached to the same trait. You can already create ambiguous method calls by having multiple traits in scope define the same method. How is this different?

1 Like

Rust doesn't (currently) support newtype projections, and we don't know of any proposals for it either. This isn't newtype projections but it does cover at least one use-case of newtype projections.

There are other ways to do this in addition to newtypes.

For example, you can pass a strategy type parameter (probably a ZST or phantom) that defaults to the usual impl, but a different strategy type can have a different implementation.

1 Like

That's not really straightforward tho. We can think of Pattern as being somewhat annoying, for example. Granted, you can't make Pattern overlapping because overlapping requires explicitly passing in the impl, and that would be breaking, but in new APIs it would allow impl'ing for various Fn types.

Back in 2017 you proposed this same feature twice. Both times you were criticized for not being detailed enough. This time your post is even shorter than those were.

It's not a bad idea, and I have no objection to revisiting the same topic when more than 3 years have passed, but if you can't improve your writing then this will end up the same way it did in 2017.

12 Likes

General newtype ref casts are not safe, because the newtype could have additional safety requirements.

At a library level, newtype ref casts are provided by the ref-cast crate. It currently only provides &T -> &U and &mut T -> &mut U casts, but could easily be extended (or a similar derive macro written) to provide casts for other receivers.

At a language level, ref casts are provided by project safe transmute.

Related:

Edit: Although I don't fully understand the proposal.. so might not be related.

Named impls is still a good idea, but it's also different from this. Because named impls have names, it would make sense/be possible to have them on a trait that isn't marked overlapping. (Altho this would probably be an issue for the existing Any trait.) They do more or less solve the same problems tho.

Given the syntax we used above (foo<_: (impl Foo for Bar)>()), it would also be possible to use a similar syntax for named impls: foo<_: ImplName>(). So we guess we are revisiting it over 3 years later, now with syntax? :‌p

(Altho we also think named impls is intended to interact weirdly with method resolution, based on which named impls you import in a given context? E.g. if you import the named impls from wrapping_ops::*, all your integers are wrapping, but if you import the named impls from panicking_ops::* then they panic instead? Which is completely different from this proposal, where they would be excluded from method resolution.)

Anyway: you keep saying we need to say more about the idea of overlapping impls but we keep coming up empty on what to say. What else do you want to hear? What's missing? What's so obvious to us that isn't so obvious to you? What if we don't have the answers you're looking for?

The main use case for this proposal is actually overloading on Fn traits, in places similar to Pattern. (but not Pattern itself, because doing it to Pattern would be breaking.)

What kind of additional safety requirements can you actually put on that?

Besides, we never said it shouldn't be opt-in. There's no reason we can't mark specific newtypes as being freely castable other than it not being implemented.

Can you please provide a clear use case where this is the only feasible option, including why the newtype pattern would be detrimental?

1 Like

Not really, because this is equivalent to, effectively, a subset of newtypes, just with different tradeoffs (mostly, being a subset).

As I said, please provide a use case where the newtype pattern would be detrimental. If you can't do so much as provide a single use case, why should anyone take this seriously?

1 Like

It's meant to be uh, hm...

"open" newtypes that can be freely casted would probably be easier to implement in the language, but eh it really does not hurt to suggest alternatives.

it's not about whether it's detrimental. it's about what you can do with it. yes you can do it with newtypes, you can have a CharMatcher(Fn(char) -> bool) and a CharRefMatcher(Fn(&char) -> bool), or you can have opt-in overlapping impls. it's two ways of doing more-or-less the same thing.

Updated OP with new details.

This is where you should start with future posts. I still disagree with the premise, and OP could still use some more details as to benefits and drawbacks, but it's much better than what you had before.

2 Likes