Remix.run Logo
armchairhacker 3 days ago

I'd propose just getting rid of the orphan rule and keeping global coherence. If a crate has multiple dependencies with the different implementations on the same trait/type, let that crate select or define one implementation that will override the others, even internally.

Then I'd trust library authors to write orphan implementations sparingly, making sure they're either "obvious" (there's no other reasonable implementation for the same trait/type) or their internals aren't relied on, just the fact that the trait/type has a reasonable implementation (like defining `Serialize` and `Deserialize` but only relying on both being inverses, so a dependent crate could override them with a different `Serialize` and `Deserialize` implementation and the library would still work).

I'd claim the libraries that define bad orphan instances must be poorly written, and you should only depend on well-written libraries. If you want to depend on libraries A and B which both rely on conflicting orphan implementations, don't bother trying to patch one of them, instead re-write the libraries in a better way to keep your codebase ideal.

...I still want that kind of system, but I expect it would fail catastrophically in the real world, where developers aren't perfect, important projects depend on badly-written npm packages, and Hyrum's law is pervasive.

---

So instead I propose something more reasonable. Keep global coherence and:

- Get rid of the orphan rule for applications. An application has no dependents, so the entire issue "two dependencies differently implement the same trait on the same type" doesn't apply.

- Next, create an opt-in category of libraries, "glue" libraries, which can only define orphan implementations (if they absolutely need a unique type, e.g. an associated type of a type/trait implementation, it can be put in another crate that is a third dependency of the glue library). Glue libraries can only be depended on by applications (not libraries, including other glue libraries). This allows orphan code reuse but still prevents the vast majority of code (the code in libraries) from depending on orphan implementations.

Library authors who really want to depend on orphan instances can still do ugly workarounds, like asking application developers to copy/paste the library's code, or putting all the library's functions in traits, then implementing all the traits in a separate "glue" library. But I suspect these workarounds won't be an issue in practice, because they require effort and ugly code, and I believe trying to avoid effort and ugly code is what would cause people to write bad orphan instances in the first place. Also note that library authors today have ugly workarounds: they can copy/paste the foreign trait or type into their library, and ask developers to "patch" other libraries that depend on the foreign crate to depend on their library (which can be done in `Cargo.toml` today). But nobody does that.

Ideally, a library that really needs an orphan implementation would use a better workaround: create a new trait, that has the foreign trait a supertrait, and methods that implement functionality of the foreign type, then use the new trait everywhere you would use the foreign type. I suspect this solves the global coherence problem, because an application could depend on the glue library that implements your library's trait on the foreign type, but it could alternatively depend on a different glue library that implements the trait on a wrapper, and if there's another library that requires a conflicting implementation of the foreign type, its trait would be implemented on a different wrapper.

Archit3ch 2 days ago | parent [-]

> ...I still want that kind of system, but I expect it would fail catastrophically in the real world

Julia has its own version of the orphan rule called Type Piracy, which is merely discouraged instead of disallowed: https://viralinstruction.com/posts/latency/#a_brief_primer_o...

It's not a net gain, but rather a different tradeoff.