Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Named impls #2251

Open
SoniEx2 opened this issue Dec 19, 2017 · 18 comments
Open

Named impls #2251

SoniEx2 opened this issue Dec 19, 2017 · 18 comments
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@SoniEx2
Copy link

SoniEx2 commented Dec 19, 2017

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
Copy link
Member

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
Copy link
Author

SoniEx2 commented Dec 19, 2017

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
Copy link
Member

@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
Copy link
Member

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
Copy link
Author

SoniEx2 commented Dec 19, 2017

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
Copy link
Contributor

Centril commented Dec 20, 2017

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. label Dec 20, 2017
@est31
Copy link
Member

est31 commented Dec 20, 2017

This seems some kind of anonymous newtypes like proposal. I think its best solved via delegation of implementation instead: #1406

@SoniEx2
Copy link
Author

SoniEx2 commented Dec 20, 2017

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
Copy link
Author

SoniEx2 commented Dec 21, 2017

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

@porky11
Copy link

porky11 commented Dec 21, 2017

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
Copy link
Author

SoniEx2 commented Dec 21, 2017

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

@durka
Copy link
Contributor

durka commented Dec 22, 2017

@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
Copy link
Author

SoniEx2 commented Dec 30, 2017

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
Copy link
Member

pnkfelix commented Jan 8, 2018

@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
Copy link
Author

SoniEx2 commented Jan 8, 2018

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
Copy link

comex commented Jan 9, 2018

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

  • Does u32 + MyHash coerce to u32? If you have fn x(foo: u32) { foo.hash() }, presumably it'll use the default hash impl, but can you call it and pass a value of type u32 + MyHash? If not, how do you convert?

  • Is u32 actually the same type as u32 + DefaultHash? Or is u32 not a real type anymore, but interpreted as a type with an omitted part, like Foo<_>, which the compiler has to fill in?

  • If it is a real type, does it coerce to u32 + MyHash?

  • How does this interact with specialization? Does a specialized impl Foo for u32 get passed over if the type is actually u32 + MyHash? Or if it matches, how does that work?

  • Can you declare a specialized impl Foo for (u32 + MyHash)?

@SoniEx2
Copy link
Author

SoniEx2 commented Jan 9, 2018

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
Copy link
Member

Dupe of #493 I think.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests

8 participants