When compiling and linking a Rust project, the crates and their dependencies form a (semi)lattice that forms a partial order. For every trait implementation where the trait and the type originate from different crates, the actual implementation resides in the crate that is being lower of the two in the order, so that it’d be able to depend on the other. This is nice, since it forces a nice “single responsibility” for the implementation.
If the crates don’t have an ordered relation, they can’t have an
impl for a (type, trait) -pair. And the orphan rules make sure that no other crate does, because that would allow conflicting implementations.
However, how about resolving the inflexibility with the restrictions on the orphan
impls with the following kind of a system?
The crates that use an
impl of an (type, trait) -pair, are not allowed to declare an implementation, which would be similar to the current situation. They could, however, declare that they require an implementation, some implementation, to exist.
rustc would, then require there to be a single, global implementation when building an executable binary. (Note that this is analogous to the system that was discussed with memory allocators, and the crates that provide and crates that require them.)
An implementation must be be linked as a crate that would declare nothing else than that implementation. This is to discourage bundling implementations with libraries and disentangle de-facto dependencies between specific libraries and trait implementations.
This means in practice, that the final binary, the “bottom crate”, would decide the implementation to use, and the 3rd party library crates should have no say in the matter.
Cargo would support this in the following ways:
- It could list published implementations to help complying with the requirements, suggesting to link one.
- The trait and type authors could declare (possibly in relation with the cargo workspaces feature) which implementation is an “officially endorsed” one, and Cargo would automatically use that, and perhaps warn if the “official” one isn’t being used.
The second point brings us another problem: because the crates the trait and the type originate from, don’t have any well-defined order in this case, there could be conflicting opinion between the two crate authors about which
impl should be the officially endorsed one. This could be resolved either by having a global preference rule (e.g. type author’s opinion over trait author’s, or the other way around), or making an “endorsement” valid only when both crates agree on the
impl (or only single one of them endorses any
I think this kind of system has the plus sides of allowing flexibility in implementations while disallowing multiple conflicting implementations in a single build. I think that the fact that the libraries have no say over which implementations to use, also should be enough to prevent ecosystem splits over
impls – the library authors are encouraged to be generic and unassuming of the impls because they can’t bundle them, and because the authors of the original type and trait can have their say about the
impl. What do you think? Comments? (I think that at least @sgrif has expressed his desire to overcome some of the limitations on orphan