Limited Opt Out of Orphan Rule

The first sentence is correct, because every type can have at most 1 implementation for a trait. The second sentence is incorrect. Even if the orphan rules didn't exist, the compiler would still forbid conflicting trait impls, so there is no unsoundness. The orphan rules exist because, when conflicting trait impls exist in some transitive dependencies, this leads to a compiler error you can't easily fix.

1 Like

We have a big workspace and to improve compilation speed / caching we need to split types/imps into separate crates. But with those limitations we are forced to use free standing functions instead of trait implementations (I'm not talking about traits we define).

It's also really natural to have the same types in different crates but with traits implemented for the needs of those specific crates (in the context of workspaces or other "structures", where you have control over a big number of crates involved) - and yet they are just free standing functions :frowning: .

Opting out of orphan rule on bin and getting compiler/linker errors on conflicting implementations (like on conflicting implementations inside one crate) would solve that issue.

That's a really interesting proposition. Our explorations about the orphan rule thus far have assumed that it'd be an error to have conflicting implementations. The ability for a binary crate to override an implementation with its own is a fascinating idea. It'd have a lot of hazards, but it could be really useful.

Your phrasing it like this made me realize that an override mechanism could also maybe provide an escape hatch for some of the problems with relaxing the orphan rules: if there's a conflict somewhere in the full dependency graph, a binary crate could resolve that conflict by picking an impl. Of course this only works if there's no reason why each side of the conflict needs its own impl, and there would be other hazards, but it's something to think about anyway...

2 Likes

Right, exactly. There are other Cargo mechanisms for which we similarly want "you can have only one choice across the whole crate graph, but if your dependencies make conflicting choices you can arbitrate or make your own choice".

2 Likes

Note that such overrides would require deferring monomorphization for (even non-generic) code that uses the affected trait in all crates until the binary is compiled. Thus, this would be a thing which reaches from dependents to dependencies like features do, but one which cannot be fully resolved until compile time of the final binary.

4 Likes

MIR-only rlibs would let this Just Work.

This sort of "the binary is the final decider on all implementations" flexibility is why Pigweed's "facades" are designed how they are, though that's handled by the build system calling rustc.

Ah, you're right, I was thinking about two cases at once and got them mixed up. I think I was thinking about library use that avoids Semver issues.

In this case here: The library can currently unsafely assume that certain types do not implement certain traits where it owns at least one of type or trait. It's not extremely common, but happens occasionally near sealed traits. For that reason, allowing 3rd party implementations on (currently) existing traits can still cause unsoundness.

2 Likes

Just wanted to bump this thread with interesting food for thought: Traits are a Local Maxima · thunderseethe's devlog

Article argues that Traits without orphan rules run into problems that implicits are used to solve.

Although matheium argued it might not be strictly necessary.

To me this jumps too quickly to implicits, rather than looking at other options. The whole use of an instance, for example, bothers me because it never talks about the hashtable problem.

I'd have hoped it would at least have talked about using Heap<T, C> where C: Comparer<T> instead of Heap<T> + implicit, for example, since using different implicits for a Heap like that is always a risk. (This is also the same problem with using an implicit for an allocator, for example.)

1 Like

(NOT A CONTRIBUTION)

Not sure what you mean about the use of instance, this is just the standard term for impl outside of Rust. And it eventually talks about the hashtable problem in the section "Correctness," using the ord impl for two btreesets its trying to union as the example. It does not have a solution though, except falling back to named instances whenever an instance is part of your type's public behavior and you care if it is correct.

Rebranding incoherence as "local coherence," as this post does, does not make me like giving up coherence any more than I did before. Instance resolution involves tough trade offs but incoherent instance resolution has to be the worst option, not the "global maxima."

2 Likes

The linked article does talk about the hashtable problem here, and correctly identifies that each hashtable needs to carry its own hashing function (or rather, each btree needs to carry its own ordering function). For some reason the author wants to encode which instance the data structure uses at the type level (maybe to achieve static dispatch?) and seem to think that this can only be achieved with dependent types (the answer is: you can do this lifting with more verbosity), but it works just fine if you store a function pointer and use dynamic dispatch instead.

Of course that sucks and not only because of verbosity/noise - if you use the ordering function for two unrelated things, you either unify them and lose some of the flexibility you were after, or you store two function pointers (or add two type parameters).

But note that custom allocators have the same problem: if you have two separate kinds of allocations to do, you either parametrize your data structure on two different allocators, or you reuse the same allocator for both. What I mean is, a Vec<T, MyAllocator> looks a lot like BTreeSet<T, MyCompare>.

1 Like

That doesn't work for associated types or if the trait is not object safe for another reason.

Someone mentioned here or on some social site (I think it was Hacker News) that having better newtypes wrapper would practically solve Orphan Rule. Does anyone have any idea how that would work?

How would it solve stuff like changing display for rustix::io::Errno?

pub struct BetterDisplay(Errno);

impl Dislay for BetterDisplay {...}

fn main() {
   let err = Errno::ACCESS
   writeln!(stderr, "failed to {operation}: {err as BetterDisplay}")
   // or 
   let btr_err = err as BetterDisplay
   writeln!(stderr, "failed to {operation}: {btr_err}")
}

You would still need to determine which delegate/new_type to use.

2 Likes