Open SoniEx2 opened 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?
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.
@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.
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...
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.
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
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);
btw: it's X as (Y for Z)
not (X as Y) for Z
(altho this can be argued)
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
eh maybe we should just go for impl C = A for B
tbh, way less confusing >.<
@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.
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.
@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.
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:
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.)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.)
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)
?
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:
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.T
is just T
.impl Foo for T + Hash
and have Foo
only work in places where Y
(assuming Y
is some sort of Hash
) is being used.Dupe of https://github.com/rust-lang/rfcs/issues/493 I think.
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 implpub impl X as Y for Z
for a public implThis 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 youuse
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 haveimpl Y for Z
andpub impl Y for Z
, except maybe in Rust 2.0.)