This could be alleviated with some very basic GAT helper trait (actually, a set of trait to cover arities):
trait TypeConstructor1 { type T<_0> : ?Sized; }
trait TypeConstructor2 { type T<_0, _1> : ?Sized; }
// etc.
// and some ergonomic type aliases
type Feed1<T_, A0 > = <T_ as TypeConstructor1>::T<A0 >;
type Feed2<T_, A0, A1> = <T_ as TypeConstructor2>::T<A0, A1>;
// ...
and then have something like
#[derive(TypeConstructor)]
struct Arc<T>...
doing the expected thing for some type Arc_
.
From there, any downstream library could write their own type aliases:
trait RProvider : TypeConstructor1 {
type R<T> : Deref<Target = T> + Clone;
}
impl<RC_> RProvider for RC_
where
RC_ : TypeConstructor1,
for<T> // <- no `for<T>` yet...
Feed1<RC_, T> : Deref<Target = T> + Clone
,
{
type R<T> = Feed1<RC_, T>;
}
If we had:
- those
for<T>
higher order bounds (that is, HRTB but with types rather than just lifetimes), - potentially
trait aliases
for these versions with added bounds to show better intent (but does not seem paramount), - the
#[derive(TypeConstructor)]
automagically applied to all type definitions (with a way to handle any arity), - and the sugar to be able to:
- write
Rc
rather thanRc_
, - write
Thing<T>
rather thanFeed1<Thing, T>
,
- write
then we'd have HKTs. When you stare at that list, besides the type-generic HRTB (for<T> ...
), everything is mostly sugar and convenience (but for the impossibility to forget to derive(TypeConstructor)
). This last point does indeed hurt in practice because of coherence: should a user library try to provide this, there should be only one such canonical library all the crates ought to turn to, based on cooperation and stuff, à la serde.
So I am mostly agreeing with you , but I just wanted to mention that regarding coherence, there could be a decoupling between some universal serde-like crate providing the TypeConstructor
definitions, and then freedom for downstream crates to provide their own specialized aliases