rust-lang / rfcs

RFCs for changes to Rust
https://rust-lang.github.io/rfcs/
Apache License 2.0
5.93k stars 1.57k forks source link

Named impls #2251

Open SoniEx2 opened 6 years ago

SoniEx2 commented 6 years ago

As of right now all impls are anonymous and public. I propose named impls and named public impls:

impl X as Y for Z for a private impl

pub impl X as Y for Z for a public impl

This is similar to how traits currently work: you need to import them to use them.

With a non-pub impl, you can use it within your module. With a pub impl, you can use it outside your module. In any case, you need to explicitly import it with use. You can also import them from an external crate, but it follows the same rules - only available where you use them.

Later we could also add !pub impl Y for Z if we want crate-wide, anonymous impls. (Sadly, since they're already public by default, we can't have impl Y for Z and pub impl Y for Z, except maybe in Rust 2.0.)

pnkfelix commented 6 years ago

Rust used to have named impls. (Ancient history; I'm having trouble even finding a proper record of when they were removed.)

My memory is that they were removed, at least in part, because one could express such functionality via traits: instead of naming an impl and exporting it, you would instead define a trait, impl it, and export the trait (and then clients who wanted that implementation code would import the trait).

You yourself even note that "this is similar to how traits currently work."

Can you elaborate on what the advantage(s) are of your proposal over the status quo of using traits to express this?

SoniEx2 commented 6 years ago

When crate A defines struct X, crate B defines trait Y, crate C defines impl Z as X for Y, you can use Z if you don't wanna make your own newtype struct and impl and so on.

They're contextual impls, just like traits are contextual.

pnkfelix commented 6 years ago

@SoniEx2 Can't one run into coherence issues then? I.e. you have crate C adding its own named impl (of trait) Y for (struct) X, but then crate D can add its own conflicting impl of that same trait for the same struct?

A typical example of where incoherence like that can cause a problem: Hashtables. Lets say the trait is Hash. If two different crates implement Hash in different ways for the same type, then passing a hashtable around between the two crates is going to cause them to use inconsistent hash-values when attempting to lookup entries for the same key.

pnkfelix commented 6 years ago

To be clear: I do not want to shoot down new ideas.

I am just trying to remember, and document, the reasons for why this feature was removed in the first place, which is what is pushing me to construct examples such as the aforementioned incoherent hashtable...

SoniEx2 commented 6 years ago

The named impl is part of the (generic) type.

HashMap<usize(X)> is different from HashMap<usize(Y)>.

It's similar to newtype structs in that aspect.

Centril commented 6 years ago

Relevant: https://internals.rust-lang.org/t/looking-for-rfc-coauthors-on-named-impls/6275

est31 commented 6 years ago

This seems some kind of anonymous newtypes like proposal. I think its best solved via delegation of implementation instead: https://github.com/rust-lang/rfcs/pull/1406

SoniEx2 commented 6 years ago

Keep in mind you'd be able to use named impls for the same type from multiple different crates. This means you don't have to juggle 3 (or more) types around.

use a::Base;
use b::Append;
use c::Prepend;

let x = Base::new();
x.append(0);
x.prepend(1);
SoniEx2 commented 6 years ago

btw: it's X as (Y for Z) not (X as Y) for Z (altho this can be argued)

porky11 commented 6 years ago

Will this allow something like this:

// crate A
trait A {…}

// crate B
struct B;

//crate C
use A::A;
use B::B;

// following not possible

trait C {…}

impl<T> A for T where T: C {…}

impl C for B  {…}

// the proposed syntax, does the same, but would work

impl A as C for B {…}

Did I understand it correctly? So I could use something of type B as A when I use C

SoniEx2 commented 6 years ago

eh maybe we should just go for impl C = A for B tbh, way less confusing >.<

durka commented 6 years ago

@pnkfelix isn't the idea with named impls that coherence issues all go away because you choose which impls to use, eliminating any ambiguity? So I guess a coherence error would come up when you try to use two conflicting impls, rather than when writing them.

SoniEx2 commented 6 years ago

I think there should be an explicit syntax for choosing named impls? It should be implicit by default, except when you use two conflicting impls, in which case you should use explicit ones.

pnkfelix commented 6 years ago

@durka I don't know how to answer your question, because @SoniEx2 is adding information to their presentation of named impls that does not match my mental model of them.

E.g. as soon as the choice of named impl for Hash shows up in one's HashMap type, then that to me looks like a newtype instance (as noted by @est31), not a named impl as I understand it.

Except that @SoniEx2 later claimed that one isn't juggling types.

So I'm just going to say "I don't understand this proposal" and not attempt to suggest further interpretations of it.

SoniEx2 commented 6 years ago

You know how you can have T: Hash + Eq?

Well I'm proposing something along the lines of let v: u32 + MyHash = 3;

But the + MyHash is implicit based on context, unless there's a type conflict (two different named impls for the same trait for the same type), in which case you need to use some sort of explicit syntax (I personally do not like type + named impl, but it's the closest thing I can think of to describe it (also, should you be able to do <T: trait + named impl>?)).

This means using the type in generic contexts carries additional information.

In other words:

  1. Code aware of that additional information must respect it. This means generics and things that use that additional information. (e.g. HashSet<T> where T: Hash + Eq uses traits Hash and Eq, so any named impls for Hash or Eq would be part of the type.)
  2. Code unaware of that additional information must act as if it doesn't exist.

These 2 simple rules make the following code work as expected:

mod a {
    use ::something::SomeHash;
    use ::something::SomeType;
    pub fn do_thing(x: &SomeType) {
        // may use x.hash() here.
    }
}
mod b {
    use ::something::SomeType;
    use ::a::do_thing;
    pub fn do_things() {
        do_thing(SomeType::new());
    }
}

While still avoiding incoherence. (I'm trying to come up with a case where these 2 rules lead to incoherence, but it seems astronomically difficult.)

comex commented 6 years ago

I think it's harder to spec out than you think - but perhaps not impossible.

SoniEx2 commented 6 years ago

I think it's easier to spec out than you think. The only confusion seems to be the fact that we're talking about u32 and Hash, while u32 already has a built-in/default Hash impl. Sorry about that.

Adding new (default) impls for an existing type would be a breaking change. (Then again, adding new types is currently also a breaking change, so I don't see why this would be an issue.)

I'll try to answer your questions anyway, reworded for less confusion:

  1. T + Y and T coerce between eachother, in any situation where their specificity is unnecessary. For example, if Y is SomeHash and you build a HashSet: HashSet::<T>::new(), then T+SomeHash is relevant for the HashSet, as it requires a Hash implementation, and thus their specificity matters - T+Y is a Hash, T is not.
  2. T is just T.
  3. See above.
  4. I don't understand the question.
  5. Yes. It wouldn't make any sense not to be able to. You should even be able to impl Foo for T + Hash and have Foo only work in places where Y (assuming Y is some sort of Hash) is being used.
ibraheemdev commented 3 years ago

Dupe of https://github.com/rust-lang/rfcs/issues/493 I think.