Motivation
We all hate-love rusts orphan rule.
Traits that are foreign to our crate, cannot be implemented for types that are foreign to our crate.
That makes perfect sense and protects the ecosystem from a lot of catastrophic sillyness, or "unsoundness" as some people insist on calling it.
However, application authors and other leaf node library authors often despise this rule. Perhaps the biggest inconvenience is that the powerful derive macros that exist for a variety of use cases, are not available on foreign types. Using a newtype pattern, authors can only implement foreign traits manually. But this can be needlessly tedious.
This is definetly not ideal in the crates.io ecosystem - a lot of "serde" feature flags would not be necessary anymore - though still convenient. Less popular libraries than serde are usually just unsupported and cannot easily be combined with libraries like serde can ve- even if the derive macros they define would work, and no additional logic is required. This is a very annoying inflexibility, and prevents the ecosystem from progressing on these matters or even competing with serde. (not that that is necessary.)
Attempts to remove the glorious orphan rule have often been discussed:
But i am not really a fan of these proposals, as it makes existing code so much harder to reason about, and would add very complicated new syntax to the type system.
My idea is pretty simple, and could significantly ease any orphan-rule pain.
Syntax Idea
Compiler support for newtype enum
/ newtype struct
!
Simply define: pub newtype struct crate_a::TypeA as MyNewType
Why would it help!?
- Copy any type and make it local to your crate!
- Freely define impls for (no longer) foreign traits!
- The compiler can use the TokenStream from the original type for any derives on the newtype!
- No danger of changing any behaviour or breaking expectations in upstream code, since it is a new type that can't be passed off as the original!
- "Inherit" all impls from crate_a, and any impls from crates that implement their own traits on it!
- (We should discuss if you can override existing impls by crates on the newtype)
- (We should discuss restrictions on accessing/deriving on private fields and upholding encapsulation)
- (We should discuss newtypes of newtypes)
In it's most simple Form, this newtype would be nearly equivalent to:
struct MyNewType(crate_a::TypeA)
impl Deref<Target=crate_a::TypeA> for MyNewType { /**/ }
/*
impls outputted by the redirected "derive()"
*/
// the generated code should mostly work because of the deref
// we might need to change the identifier of the struct/union/enum in the original typedefs token stream to "MyNewType", etc..
The behaviour we would observe (overriding with deref, "inheriting" of impls) is pretty close to what we want here. Derive redirection is almost all that we need, even if it would be worth it thinking these newtypes through to the end.
Okay, but hold on, let's look at an example to understand how this would work in practice, and the implications and edge cases of this solution.
Cross-Crate Example
=== CRATE A ===
//! Provides an interesting type
struct TypeA { /* ... */ }
=== CRATE B ===
//! Provides a derivable super useful trait.
//! Think of Serialize, Deserialize
//! But also less popular similar libraries like ts-rs, Tsify, serde-likes that don't enjoy ecosystem-wide support.
//! Or think of Clone or Debug, PartialEq, things that force you to fork and MR just because someone forgot them upstream.
trait SuperUsefulTrait {
}
// in crate_b_proc
#[proc_macro_derive(SuperUsefulTrait)]
pub fn derive_super_useful_trait(_item: TokenStream) -> TokenStream {
// powerful and useful derive implementation.
}
=== CRATE C ===
//! Third Crate: Defines it's own trait and implements it.
//! This is the tricky part once we get to my proposal.
trait BoringTrait {
}
impl BoringTrait for TypeA {
}
=== MY CRATE ===
// NEW SYNTAX: "newtype struct" that uses definition and impls from crate_a.
// crate_c's impl for crate_c::BoringTrait could also be available (design choice)
// blanket impls will probably already apply directly on the new type.
// can be cast to TypeA using From/Into or "as", depending on whats easier.
newtype struct crate_a::TypeA as TypeAA;
// nice try orphan rule, this struct is not a foreign type any longer!
//
// The reason that this could be awesome:
// derive macro recieves same TokenStream as if derived for crate_a::TypeA directly!
// derived impl is available only on newtype for users of our library.
#[derive(SuperUsefulTrait)]
newtype struct crate_a::TypeA as TypeAAA;
// allowed:
impl crate_b::SuperUsefulTrait for TypeAAA {
}
// disallowed, already implemented (or other design choice: takes prescedence over impl by crate_c):
impl crate_c::Boring for TypeAAA {
}
Conclusion
What do you think! I have no idea how rustc works and how hard this would be - and the syntax may not be the most thought-through final thing ever, but... I think this actually has a shot of easing the pain with the orphan-rule, while keeping it in place! The orphan rule is good! The workarounds are actually kind of the way to go, and should be way less of a hassle.
I am extremely interested what issues arise with this idea! Please keep in mind that i am not that experienced in language design- I value your opinion far more than my own if you stay kind in the replies!