Remix.run Logo
logophobia 3 days ago

What is wrong with the following solution?

Any trait implementations where the type and trait are not local are:

* Private, and cannot be exported from a crate

* The local trait implementation always overrides any external implementation

That would solve part of the problem right? Only crate libraries that want to offer trait implementations for external traits/types are not possible, but that might be a good thing.

The solution proposed by the author with implicits is quite complex, I can see why it wasn't chosen.

haileys 3 days ago | parent | next [-]

The problem is that, when you're implementing a foreign trait for a foreign type, you usually want that impl to then be visible to a foreign crate. Not just the local crate.

If it were good enough to only have that impl be visible to the local crate, then you could side step this whole problem by defining a local trait, which you can then impl for any type you like.

So maybe we relax the rules a bit such that only the local crate, and crates that it calls, can see the impl. But then what if a third, unrelated crate, depends on that same foreign crate your local crate depends on? We'd need to keep some sort of stack tracking which impls are visible at any given time to make sure that such foreign crate can only see our impl when that foreign crate is used from our local crate. Hmmm... this is starting to look a lot like dynamic scoping.

atq2119 3 days ago | parent [-]

How about explicitly declaring orphan trait implementations as public (or not) and explicitly importing them (or not). Trait implementations are resolved at the point where a concrete type becomes generic, and the set of available traits can depend on that context.

This isn't exactly trivial, but it avoids coherence problems.

pornel 3 days ago | parent | prev | next [-]

This doesn't solve the hashtable problem:

    Crate A implements Hash(vA) for T
    Crate B implements Hash(vB) for T
    Crate C has a global HashSet<T>
    Crates A and B can both put their T instance in the C::HashSet. They can do it in their private code. Their Hash overrides any external implementation. The trait is used, but not exported.
    C::HashSet now has an inconsistent state. Boom!
withoutboats3 3 days ago | parent | prev | next [-]

Implementations are not exported or public at all: they are used in functions and those functions are exported. For correctness, you want those implementations to be resolved consistently (this is what coherence is). This post gives the example of unioning two sets: you need to know that they're ordered the same way for your algorithm to work.

So the problem isn't that the implementation is public, it's that its used somewhere by a function which is public (or called, transitively, by a public function). For a library, code which is not being used by a public function is dead code, so any impl that is actually used is inherently public.

You might say, okay, well can binaries define orphan impls? The problem here is that we like backward compatibility: when a new impl is added to your dependency, possibly in a point release, it could conflict with your orphan and break you. You could allow users, probably with some ceremony, to opt into orphan impls in binaries, with the caveat that they are accepting that updating any of their dependencies could cause a compilation failure. But that's it: if you allow this in libraries, downstream users could start seeing unsolvable, unpredictable compilation failures as point releases of their dependencies introduce conflicts with orphan impls in other dependencies.

lesuorac 3 days ago | parent | next [-]

It would still be consistent; everything with my crate resolves `impl Foo for Bar` to what I define, everything with other crate resolves `impl Foo for Bar` to what they defined, and any other crate would have a compilation error because those crates didn't `impl Foo for Bar`.

If I for some reason exported a method like `fn call_bar(foo: Foo) -> Bar` then I think it would use my `impl Foo for Bar` since the source code for the trait impl was within my crate. What happens if instead I export like `fn call_bar<F: Bar>(foo: F) -> Bar)` is probably a bit more up to debate as to whose trait impl should be used; probably whichever crate where F being Foo is originally known.

I think they did say binaries can define ophan impls; and the only way somebody should be able to break your code is by changing the trait definition or deleting the implementing type. Otherwise your implementation would override the changed implementation. This seems fine because even if I locally define `Foo` which lets me to `Foo impl Bar`; if you then delete Bar then my code breaks anyways.

pornel 3 days ago | parent | prev [-]

How about downgrading duplicate implementation in the binary to a warning?

SQL has CREATE TABLE IF NOT EXISTS. Rust could have `impl Trait if not already implemented`.

hdevalence 3 days ago | parent [-]

This is a bad solution because now method resolution is suddenly unpredictable and can change out from under you based on changes to remote crates

pornel 3 days ago | parent [-]

Of course it can change, that's what removal of coherence does.

It seems to me to be a logical impossibility to allow orphan implementations, and allow crate updates, and not have trait implementations changing at the same time. It's a pick-two situation.

withoutboats3 3 days ago | parent [-]

Your conclusion is correct. I'm very happy with the two that Rust picked and tired of people pretending that there will be a magical pick three option if we just keep talking about it.

pornel 2 days ago | parent | next [-]

I also think Rust has picked the right default, but I wouldn't mind having an opt in to the other pair of trade-offs. There are traits like `ToSql` that would be mostly harmless. Serde has tricks for customizing `Serialize` on foreign types, and this could be smoother with language support. Not every trait is equivalent to Hash.

Ygg2 3 days ago | parent | prev [-]

The problem is people want to write glue code that adds foreign traits to types they don't own.

For example they need to implement diesel trait on a type from crate they don't own (e.g. matrix)

Is it possible to square that circle? Perhaps not through traits, but something else?

withoutboats3 3 days ago | parent [-]

Better newtypes are the answer.

Consider Java for example. In Java, interfaces are even more restrictive than traits: only the package which defines the class can implement them for that class, not even the package which defines the interface. But this is fine, because if you want to implement an interface for a foreign class, you create a new class which inherits from it, and it can be used like an instance of the foreign class except it also implements this interface.

In Rust, to the extent this is possible with the new type pattern it’s a lot of cruft. Making this more ergonomic would ease the burden of the orphan rule without giving up on the benefits the orphan rule provides.

yccs27 3 days ago | parent | prev [-]

I think Rust assumes that trait implementations are the same across the whole program. This avoids problems e.g. with inlining code or passing data structures between crates. I don't believe this is absolutely necessary though.