I have some rough ideas about how to improve crates ecosystem to enable Clean Architecture. I think this is best explained using an example. Let's say developer A creates fast_serialize crate that defines serialization traits. Developer B creates crate foo_types that contains bunch of basic types in domain foo.
Developer A doesn't know/care about foo_types.
Developer B doesn't know/care about fast_serialize.
Developer C wants to use both crates and integrate them. The current answer is for C to use a newtype.
The issue is newtypes are annoying to use because C has to write boilerplate to delegate the trait methods as well as perform conversions. Even worse, converting things like Vec<NewtypeOverT> to Vec<T> could cause reallocation (unless unsafe is involved).
Now let's imagine a slightly different scenario
There are multiple crates with interesting traits (serde, postgres-types, slog...)
A crate defining some base types may want to support them (so like developer B above except he cares about A). So the developer adds multiple optional crates as features. However they could accidentally interact so the proper test would be to test all possible combinations of features. Which is 2^feature_count tests. That may be a lot of tests.
I was thinking it'd be helpful to have some mechanism to allow binary crates to implement external traits for external types. A trait-defining crate could declare it won't implement the trait for any type outside of crates it already uses and vice versa. Having only one impl is then guaranteed and it's guaranteed to be in binary crate only.
There could also be impl crates that provide these impls but can only be used as direct dependency to binary crate, never dependency to library crate. This would be useful if developer C in the top-most scenario wants to make the impl reusable.
I know this is just some rough idea, would like to know what people think.
What if one of those libraries adds an overlapping implementation of the trait in the future? This feature would make adding any trait implementation a semver breaking change. The coherence rules exist among other reasons to allow adding trait implementations without causing a semver breaking change.
That is roughly #[fundamental]. It doesn't allow you to implement foreign traits for foreign types though. Only for example Box<LocalType> despite Box being a foreign type as Box is marked as #[fundamental].
One more thing I should note. Not being able to impl a trait later is not too bad. The crate author can just impl it in a separate crate that the consumers can import. Sure it can get a bit annoying sometimes but at least it's not impossible and the annoyance is not too bad.
Alternatively the crate author could write those traits in the original crate with some marker and then the consumer must opt into importing them. (Resembles #[macro_export]/#[macro_use])
How can there be two binaries that are compiled at the same time?
It is bad due to the hashtable problem (or whatever is called, I don't remember). Imagine crate A and B implemented Hash for Foo in two different ways, both with its own named impl. Then an HashSet<Foo> created in A would be incompatible (at runtime!) with crate B due to different rules for hashing the elements.
I just don't see these as being particularly likely to cause issues. Sure, there's pub(crate) and #[non_exhaustive] and some other stuff where it could hypothetically matter, but for the most part if you write the different features in different modules you have the vast majority of the protection that crates would offer.
So yes, you'd need to test with each feature on its own and with all the features enabled, but that's only one more test than if they were all separate crates.
That'll catch #[cfg(feature = "bar")] when you meant #[cfg(feature = "foo")], things that are unused without a particular feature, and even breakage from things like #[cfg(all(feature = "bar", not(feature = "foo")))].
Yes, it won't catch a sufficiently-complicated guard like #[cfg(all(feature = "foo", not(feature = "bar"), feature = "baz"))], but it's also just not that hard to avoid writing that in the first place.
And separate crates doesn't necessarily make them work together either, since the different crates could try to link different versions of native code that fail if put in the same binary.
Then maybe the proposal should be: allow libraries other than stdlib define new #[fundamental] itens (both types and traits). That way, if a library indeed adds a new impl, it will be a breaking change (and must be accompanied by a semver bump)
I do think this is a substantial scaling problem in the crate ecosystem.
To give a concrete example I've run into: suppose you want to store a git2::Oid in a database. It definitely doesn't make sense to make git2 add a feature flag to depend on the database crate just to provide that impl. That tends to be true of most types. So in practice, database crates need a pile of features to pull in dependencies just to impl traits for other crates' types.
A substantial part of software development is gluing things together in interesting ways not anticipated by the original developers. This limitation makes it hard to do that in the Rust ecosystem; you need to get one or the other original developer to add support for the combination.
@scottmcm those are some interesting points. The usefulness of separating things into crates does seem low.
@dlight maybe, I'd have to better understand what it does to say for sure. Sadly being unstable feature it's not well documented.
@josh I approached this so far by picking popular crates/crates I use. I think it's even reasonable to do for super-popular/important things like serde. But it sucks for the consumers of less popular crates.
So, I do agree that it would be nice to have a clean way around the ever-growing dependencies problem.
Interestingly, this is in a sense more flexibility than I'm used to. In C#, if I wanted Git2.Oid to have a [JsonConverter(typeof(OidJsonConverter))] attribute on it or to implement IXmlSerializable, then that's only possible by having Git2 do it. So the impl being able to be in either of the two crates, rather than only one option, is already an improvement.