Allow disabling orphan rules for applications

Orphan rules are very useful to ensure that libraries can be used in compatible manner. But it often breaks the end user experience.

For example: User uses some library with complex types. Now he wants to provide an openapi schema for those types. User adds apistos dependency and not requires to implement JsonSchema for all types that are getting returned from the handlers. The problem is that he cannot implement it for foreign types because trait is also not belonging to the current crate.

The only two existing options for user are either warpping every and single field in the object graph (Foo -> OpenApiCompatibleFoo, Bar -> OpenApiCompatibleBar) or forking the entire world and adding required implementations in there, condemning themselves for using the fork forever.

Another example is serde attributes: implementing serde for extrenal type is very much desired, but it's not doable. Most people are relying on making SerdeCompatible(Foo) and using it everywhere or having just a bunch of serde_with attributes (I currrently have 464 serde_as attributes in 20 files).


I think that the right solution here is to relax orphan rules. And by relaxing I mean disabling them entirely and allowing end applications to ignore them altogether (let's make this an option "I understand what I'm doing'). I agree to take the full responsibility and rewrite the code if any of the underlying implementations adds a trait impl and it breaks coherence, I understand that feature can make my code break due to minor changes in my dependencies. But the benefits vastly outweigh the costs, so I'd like to have this power.

I wonder if anyone share the sentiment.

3 Likes
1 Like

Yes, this discussion also goes in the same vein. Allowing this for all crates might be unwise, but but applications have no shortcomings in any way in my mind.

1 Like

Do note that adding impls against the orphan rules does have more technical aspects that need to be solved than simply “accept that my code can stop compiling if dependencies upgrade to new minor versions”.

Realistically, orphaned impls must be made available to the compiler already while a dependency (e.g. one that could write the same impl itself locally, or intermediate ones that depend on such dependency) itself is being checked or built.

This is because when those dependencies are checked, coherence can sometimes apply negative reasoning whose soundness relies on the orphan rules (like “these two impls can’t overlap because one has a trait bound that isn’t fulfilled in the potentially-overlapping case, and it can’t be fulfilled by dependents either due to orphan rules”). And when dependencies are built, code generation can rely on the precise knowledge of whether some concrete type implements some concrete trait (e.g. in specialization – which isn’t stable, but a few standard library features might already be using it – or notably the generation of dyn Trait vtables, where methods with additional trait bounds are only instantiated if those trait bounds are met.)

As a consequence, AFAICT besides the technical challenge of implementing some mechanisms for handling this in the first place, it’s probably hard to avoid a pass of “completely re-compile all (or at least a bunch of) dependencies” whenever you add an orphaned impl. This sounds expensive… so this sounds to me like perhaps those orphaned impls should perhaps better be declared in their own special way and not simply be normal impls in normal code compiled normally except there’s “relaxed orphan rules”.

In fact, such a mechanism might be more fittingly interpreted as just an alternative for – perhaps almost just sugar for – some sort of usage of the [patch] section?

4 Likes

It's maybe worth noting that Rust's code structure makes “condemning yourself to using the fork forever” a smaller deal in theory than it might be in another language — “all” you're doing is adding an entry to the [dependencies] table and adding a new, independent source file to the source tree. This patch can (in theory) be easily overlaid on any version of the source package automatically without any risk of conflicts (other than the obvious semantic one of them adding the impl themselves).

Actually setting up the patch to be simple to update and reapply is far from simple currently, but having a feature for it in Cargo would make it approachable. I propose to call this theoretical feature “impl adoption” (they're no longer orphans).

1 Like

That works, unless you want to publish to crates.io, which many open source command line tools want to do. So I don't see it as a useful option.

In particular this workaround shouldn't in any way reduce the urgency of the problem.

Crates.io might simply care about semver too much for it ever to be desirable to host binary crates that are set-up to break on minor updates of dependencies. In other words: wouldn’t perhaps the same arguments of why you can’t have [patch] dependencies on crates.io-hosted crates also apply to orphan impls in crates hosted on crates.io as well?

Just want to mention that, if there's a mechanism of include_crate, it dismisses this limitation in a lot of cases. Either include(merge) the trait crate or include the datatype crate into current crate, and now the corresponding piece is local, so implementing the trait on the datatype is no longer a problem.

Right, but if it's a nightmare in some other language and not an nighmire but just awful in rust - it doesn't make it very pleasant. Contrary the Rust reliance on crates can make it that one type has dependencies on 4 crates indirectly, and you end up forking them all to add instances for every type in the object tree.

Can you please elaborate? I'm not familiar with this mechanism.