Generic generics

Is it possible to have generic generics, to go with generic associated types?

Something like struct Foo<T<'a>>, used as Foo<T<'a>=Bar<'a>>.

What does Foo<T<'a>=Bar<'a>> mean in this context?

As for struct Foo<T<'a>> does this mean that Foo forces T to be generic over a lifetime parameter? What is the benefit of that over just defining what traits T should implement (if any)?

Rust doesn't support Higher Kinded Types, you can read about a hack to emulate them here

2 Likes

It is setting the generic T<'a> to a Bar<'a>. Specifically, it needs to name the lifetime to use in the Bar<'a>.

The struct would carry with its type a type constructor. Ofc, you'd also need to have a PhantomData somewhere, like:

struct Foo<T<'a>> {
  _x: PhantomData<fn() -> T<'_>>
}

In particular you'd be able to do this:

impl<T<'a>> Something for Foo<T<'a>=T<'a>> {
  type ATC<'a> = T<'a>;
}

Here's something that cannot be done with GATs, but can be done with GGs: AnyA<'a>!

Example:

trait AnyA<'any_a> where Self: 'any_a {
  fn downcast_ref<T<'t_a>>(&self) -> &T<'any_a>;
}

struct Foo<'a>(&'a str);

fn bar<'bar_a>(x: &dyn AnyA<'bar_a>) -> Option<&'bar_a str> {
  x.downcast_ref::<T<'t_a>=Foo<'t_a>>().map(|foo| foo.0)
}

let s = String::from("hello world!");
let foo = Foo(&s);
let s_str = bar(&foo as &dyn AnyA<'_>);

You can model that (even on stable!):

trait OneLifetimeParam<'a> {
    type This: 'a;
}

struct Foo<'a>(&'a str);
struct FooFamily;

impl<'a> OneLifetimeParam<'a> for FooFamily {
    type This = Foo<'a>;
}

trait AnyA<'any_a> where Self: 'any_a {
  fn downcast_ref<F: for<'t_a> OneLifetimeParam<'t_a>>(&self) -> &<F as OneLifetimeParam<'any_a>>::This;
}

impl<'a> AnyA<'a> for Foo<'a> {}

fn bar<'bar_a>(x: &dyn AnyA<'bar_a>) -> Option<&'bar_a str> {
  x.downcast_ref::<F=FooFamily>().map(|foo| foo.0)
}

fn main() {
    let s = String::from("hello world!");
    let foo = Foo(&s);
    let s_str = bar(&foo as &dyn AnyA<'_>);
}

However it won't compile because you can't have generic functions on a trait object

So wait, how does Any have generic functions again?

Oh, right, TypeId.

Uh. Model this on stable? (This would require a bit more than just GGs, but...)

// ideally the commented "where" would be a thing, but if not we just mark the trait
// as unsafe and call it a day.
unsafe trait AnyA<'any_a> where Self: 'any_a /*where Self<'static>: 'static*/ {
  fn type_id(&self) -> TypeId;
}

// note that types such as &'static str are valid in T<'a> position, since 'a can just be ignored.
// so this would actually be implemented for *all* types with one or less lifetime parameters.
unsafe impl<'any_a, T<'a>> AnyA<'any_a> for T<'any_a> where T<'static>: 'static {
  fn type_id(&self) -> TypeId { TypeId::of::<T<'static>>() }
}

impl<'any_a> dyn AnyA<'any_a> {
  fn downcast_ref<T<'t_a>: AnyA<'t_a>>(&self) -> Option<&T<'any_a>> where T<'static>: 'static {
    if TypeId::of::<T<'static>>() == self.type_id() {
      // SAFETY: same as Any + because the lifetimes are literally encoded in the type system!
      unsafe { Some(&*(self as *const dyn AnyA<'t_a> as *const T<'t_a>)) } 
    } else {
      None
    }
  }
}

struct Foo<'a>(&'a str);

fn bar<'bar_a>(x: &dyn AnyA<'bar_a>) -> Option<&'bar_a str> {
  x.downcast_ref::<T<'t_a>=Foo<'t_a>>().map(|foo| foo.0)
}

let s = String::from("hello world!");
let foo = Foo(&s);
let s_str = bar(&foo as &dyn AnyA<'_>);

How would you handle types that contain more than one lifetime and are invariant over them?

What's invariant again?

Anyway, this handles zero or one lifetimes. To handle more you'd need AnyAB<'a, 'b>, AnyABC<'a, 'b, 'c>, or so on. For more lifetimes you'd have to somehow be able to have unsafe trait AnyAB<'a, 'b> where Self: 'a + 'b but if not, can always leave out the where and rely on the unsafe.

See variance. It basically means that if you have a Foo<'short, 'long> or Foo<'long, 'short> you can't convert them to a Foo<'short, 'short>, which would allow to use just one lifetime.

But you'll have to enforce this somehow, because otherwise you allow to swap the lifetimes of a type. Take for example T<'a> = Foo<'a, 'static> and T<'a> = Foo<'static, 'a>. They can both implement AnyA<'a> and the type id of T<'static> is the same between them, meaning you could transmute a Foo<'a, 'static> to a Foo<'static, 'a>.

1 Like

No, you can only create a TypeId for T<'static, 'static>, so there is no safety concerns there

Ah hm, so you'd need to relax TypeId::of to accept non-'static...

(Or otherwise disallow Foo<'a, 'static> from being a valid T<'a>... but Any does accept things like Cow<'static, str>, so it'd be inconsistent...)

No. Rust cannot support lifetime introspection. Higher-rank lifetimes on function pointers (you know, for <'a> fn (&'a)) allow you to use a function at a single memory address for whatever lifetime you want, so generating different code for different lifetimes isn’t an option. And any option that adds overhead for code that doesn’t use it is also a non-starter (so no hidden parameters). So it’s impossible to give types that differ only in lifetime different TypeIds.

It’s unsound to give them the same TypeId, because you’re supposed to be able to assume that types with the same TypeId can safely transmute between each other (that’s kind of what “being the same type” means), and it would allow dangling pointers if you did that with types that had different lifetimes.

3 Likes

Rust can support lifetime-to-TypeId. Monomorphization applies to lifetimes as well, and different monomorphizations of the same lifetimes would be allowed to convert to different TypeId.

This means there could be some edge-cases where it would be safe to downcast a lifetime but the type system wouldn't allow you to because the monomorphization doesn't agree with it, but nevertheless it would still be sound. (Yes, that one with function pointers is a good example.)

This isn't true. Lifetimes are erased far before monomorphization occur. If lifetimes where monomorphized, it would result in a massive hit to compile times and binary sizes.

9 Likes

It would also break C FFI. I can export a C function with lifetime parameters no problem, but exporting a C function with type parameters is a bit more of a problem:

// This will do what you mean
#[no_mangle]
extern "C" fn write_num_ref<'a>(c: &'a u32) {
    println!("{:?}", c);
}

//  This won't
#[no_mangle]
extern "C" fn write_num_generic<T: Into<u32>>(c: T) {
    println!("{:?}", c.into());
}

fn main() {
    // call write_num_ref with two different lifetimes
    write_num_ref(&32u32);
    let u = 32u32;
    write_num_ref(&u);
    // call write_num_generic with two different types
    write_num_generic(32u32);
    write_num_generic(32u16); //~ERROR
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=0702afe42c0659156b95289ac5bc2068

That's not actually what I'm worried about. My problem is with the fact that, right now, I can construct a single function pointer, and then, because of how subtyping works, use it as any lifetime. In retrospect, the FFI example is probably more compelling, because it's so explicit (and obviously unfixable, since it involves the C toolchain).

3 Likes

Ah hm :‌(

You could "tag" some lifetime parameters as monomorphizing (so that if you pass them to TypeId the whole thing needs to be monomorphized)... But yes, we see, and that makes us sad because we really wanted to see AnyA<'a> be a thing. Hm...

I don't even think it's possible to monomorphize lifetimes since there might be infinitely many lifetimes (e.g. in a recursive function) and the exact number may be known only at runtime.

2 Likes

What if you "cheated" with TypeId::of::<for<'a> fn(PhantomData<T<'a>>)>()? (thanks to @RustyYato for this one :‌p)

unsafe trait AnyA<'any_a> where Self: 'any_a {
  fn type_id(&self) -> TypeId;
}

// note that types such as &'static str are valid in T<'a> position, since 'a can just be ignored.
// so this would actually be implemented for *all* types with one or less lifetime parameters.
unsafe impl<'any_a, T<'t_a>> AnyA<'any_a> for T<'any_a> where T<'any_a>: 'any_a {
  fn type_id(&self) -> TypeId { TypeId::of::<for<'a> fn(PhantomData<T<'a>>)>() }
}

impl<'any_a> dyn AnyA<'any_a> {
  fn downcast_ref<T<'t_a>: AnyA<'t_a>>(&self) -> Option<&T<'any_a>> where T<'static>: 'static {
    if TypeId::of::<for<'a> fn(PhantomData<T<'a>>)>() == self.type_id() {
      // SAFETY: caveats of Any apply, but in addition:
      //
      // - the concrete lifetimes don't actually matter for the TypeId, as we carry them throughout the code
      // (the dyn AnyA<'a> requires a lifetime parameter). we don't need them to be monomorphized as
      // such.
      //
      // - there are no issues related to lifetime bindings (e.g. T<'a>=Foo<'a, 'static> isn't the same as
      // T<'a>=Foo<'static, 'a> isn't the same as T<'a>=Foo<'static, 'static>) because we encode
      // these with an for<'a> fn(...<'a>) type. this basically allows us to get different TypeIds for
      // different ways to bind the same lifetime into a given type constructor.
      unsafe { Some(&*(self as *const dyn AnyA<'t_a> as *const T<'t_a>)) } 
    } else {
      None
    }
  }
}

struct Foo<'a>(&'a str);

fn bar<'bar_a>(x: &dyn AnyA<'bar_a>) -> Option<&'bar_a str> {
  x.downcast_ref::<T<'t_a>=Foo<'t_a>>().map(|foo| foo.0)
}

let s = String::from("hello world!");
let foo = Foo(&s);
let s_str = bar(&foo as &dyn AnyA<'_>);