It often leads to problems, when I want to implement a trait of one crate for a type of another crate. I’m thinking about ways how this problem could be solved in a not too complex way. Besides of const genercis, this seems like a pretty important feature to me.
The problem occurs, when I have at least two independant crates and want to define traits, that contain only types and traits, which are not defined in the current crate.
Here some examples with fictional crates:
use renderer::Draw; // a trait for drawing something
use shapes::Circle; // some shapes and common operations on them (intersection, volume, etc.)
impl Draw for Circle {…}
In this case it would probably be useful to have a wrapper type. But redefining every needed function for a wrapper type seems complicated in most cases.
use physics::Vector as VectorP; // a vector optimized for physics
use graphics::Vector as VectorG; // a vector optimized for graphics
impl From<VectorP> for VectorG {…}
impl From<VectorG> for VectorP {…}
Here a wrapper type would not be useful. Instead, it would be possible to use a new vector type and define From for all vector types or an exisiting type where both already can be converted into.
Instead it may be useful to use a common vector type, where multiple crates depend on, but that may also be a problematic solution, if one lib just uses vectors, one implements vectors generically as special case of matrices and one implements vectors generically as special case of multivectors.
It would be possible to change one of the crates yourself, for example by adding optional dependencies of the other crate, but if they really are independend, that also seems not like a good solution.
There were some approaches to solve this problem, but they allowed multiple implementations of the same trait, which seems too confusing and useless for most use cases.
So here are my some ideas, I had in mind, which aim to solve these problem. If one of my approaches seems interesting, I’ll write an RFC for it.
Approaches
Local impls
The most trivial solution to most problems is to allow local impls. This means, that an implementation is only used in the current crate but not exported to other crates.
In order to avoid breakage, local impls are also allowed, if there already exists an implementation. If one of the used crates implements it later, it won’t break anything. A local impl could look like this:
#[local_impl]
impl Draw for Circle {…}
// or using keywords
crate impl Draw for Circle {…}
// or using keywords
crate impl Draw for Circle {…}
priv impl Draw for Circle {…}
If the impl already exists in some extern crate, it will be warned by default.
This won’t solve more complicated use cases, where the new implementations are reexported.
Therefore I have some other ideas.
Extender crates
This will allow to extend foreign crates. This is similar to adding optional dependencies to a crate, but will be possible inside a new crate.
Everything in the new crate, called extender crate, acts as if it is defined in the extended crate and so can define traits on types, even if none of them is in the new crate. A crate can define at most one crate to be extended.
If crate uses multiple extender crates, only one is allowed per crate. Else multiple extender crates may define the same traits on the same types. Instead one is allowed to extend an extender crate further.
The rules for cyclic dependencies also have to be different for extender crates.
For example when crate X
extends A
and depends on B
, and crate Y
extends B
and depends on A, they may both implement the same additional traits. So when some other crate depends on X
and Y
it may cause multiple implementations of the same traits on the same objects.
Therefore in order to compute cyclic dependencies. Since an extender crate is seen as if all were defined in the crate itself, every crate, that depends on a crate, implicitely depends on the extender crate.
So X
depends on B
and implicitely depends on Y
, which depends on A
and implicitely depends on X
, so this is a cyclic crate definition.
Optional additions
It may be a good addition to not allow implementations of combinations of traits and types, that could be implemented inside the extended crate. Else it may be possible to This will also allow multiple extender crates for the same crate, as long as they have different dependencies.
But in may be pretty common to have multiple dependencies, and if one of them is not used by the extended crate, it won’t be possible to use both extender crates, even if the implemented traits are the same.
Glue crates
In order to fix this, it would be useful to be able to specify, which other crates are used, to implement the new traits. So it doesn’t extend a single crate anymore, but a set of crates, and now has some restrictions. This won’t be called extender crate, but glue crate. Every glue crate specifies some sets of crates. Each of these sets has exactly two elements. Both crates in this set are then glued together. If a set has more than two elements it will be decomposed into all subsets, which have exactly two elements. A glue crate also won’t need a special check for cyclic dependencies. Instead two crates are not compatible, if one of its glue sets is equal.
Crate X, which glues A and B may implement the following:
use A::Trait;
use B::Type;
impl Trait for Type {…}
It may also implement more complicated traits, as long one type or trait is in crate A and one is in crate B:
use A::Type as TypeA;
use B::Type as TypeB;
impl From<TypeA> for Vec<TypeB> {…}
Even this should be possible:
use A::Trait;
use B::Type;
impl<T> Trait<Type> for T {…}
The rules for using of new types and traits inside a glue crate is as in other crates, so there are no problems. But it may be useful to forbid adding new types and traits in glue crates (at least public ones) so it is just there for gluing things together and will not export anything.
Adding dependencies later
A problem may occur, when adding dependencies between both crates later. For example if the crate for physics vectors implements a converter from and into graphics vectors. If it’s an optional dependency, then it’s not a problem to use the matching extender or glue crates, when the dependency isn’t used. Other extender or glue crates will break, when they glue it and one of the new dependencies.
This could be fixed by explicitely adding, which crates will be used for trait impl, to the configuration of the crate. This way new crates are not implictely allowed to use std.
So if I define a vector math library, I probably would add std
, so I can implement my traits for the types in std
and implement the traits of std
for my vector types. I also may implement num-traits
.
But some user of my crate may want to implement the alga
traits for my vector math, which is possible, since I didn’t explicitely add it to my crate. I may even use alga later in my crate without breaking anything, but am not allowed to implement any traits (except using local_impl
, if this also exists)
It’s not required to depend on std, when I do this:
struct Vector<T>…;
impl<T,U> From<Vector<T>> for Vector<U> where U: From<T> {…}
// this is currently a problem anyway, because the default definition (From<T> for T) exists and cannot be specialized
The reason is, that at least two types of my own crate participate.
If only one trait takes part on the implementation, it’s also allowed without explicitely adding a crate:
trait Test {…}
impl<T> Test for T {…}
It will still be allowed, not to explicitely add the impls of each crate, but then adding glue crates may cause problems, when adding new dependencies. By default just all dependencies are allowed for implementing new traits on the new types.
New types
Another approach, based on the wrapper type stategy, is a way to copy types and all things implemented on them. This could look like this:
struct Vector = physics::Vector;
The new type Vector
defined like this, may work as if it was defined using type
, but it’s a new type as defined with a default structs.
All public fields and function impls will be derived for this type.
In order to derive the existing trait implementations, this has to be done explicitely, and may be complicated.
This may also be implementable as a macro.
If too many types would have to be redefined this way, this won’t be a good approach.
instead they may even be implemented implicitely when using, like this:
use impl physics::Vector;
This will create a new type, that implements all public fields and functions.
It could also implement the traits by default, that are already implemented in physics
.
When using other crates, that implement something on physics::Vector
, it won’t be implemented by default.
In order to use a more specialized version of a struct, the crate, which uses Vector and implements new structs for it, will have to reexport it.
If the vector is not reexported, a special crate can be used for that:
// crate reexport_physics_with_additional_traits;
extern crate physics;
extern crate additional_physics_traits;
pub use physics::Vector;
This is everythign the crate has to do. Now i have to use it in another crate:
use impl reexport_physics_with_additional_traits::Vector;
// this will create a new vector type, which implements all traits accessible inside the crate, it is used from
Now it’s seems already usable, even if a bit complicated with the need of additional crates in some cases.
Conclusion
I hope, some of these ideas sound interesing enough, so I can write an RFC for one of my approach.
At least local_impl
should be trivial, but it’s also not that useful. It should be good enough to solve this problem for simple crates.
The extender and glue crate approach seems pretty complicated, but may solve the problem in a clean way.
The ability to declare new types seems not to suit well for this problem, but may at least have some other use cases.
What do you think is worth to get an own RFC in the RFCs section? Are there already other good approaches, I didn’t mention? Do you have other ideas in mind?