Generalized binder types

A fundamental issue with the current unsafe_binder feature is that it's unsafe, and therefore carries the syntactic weight associated with it.

However it isn't actually unsafe to use an unsafe binder as a marker type.

Another issue with unsafe binders is that it doesn't allow you to bind the lifetime parameters, so it isn't very useful in generic contexts.

Instead of continuing with unsafe binders in its current form, I think we should instead work towards generalized binder types.


What is a binder type?

A binder type is a type that contains unbound type or lifetime parameters. Think of it as a miniature version of GATs.

For example: for<'a> &'a i32.

These types have the same exact layout as their fully-bound counterpart. (With exception of type binders in the form for<T> [T; 8], since T has an undefined layout.)

(I am still on the fence about the usefulness of type binders. I think it would be useful for going from SOA to AOS without writing two separate structs. But it is complicated to implement and specify the behavior of. Lifetimes are more useful in the near-term.)

(This would also need to be unified with HRTBs on functions)

Why would I want a binder type?

Code that needs to launder a value for an unspecified lifetime typically has to rely upon 'static or marker GATs.

For a concrete example, the Yokeable trait (Yokeable in yoke - Rust) requires a static lifetime and a trait to allow erasure and binding/substitution of lifetime parameters. It also depends on a derive macro to safely implement Yokeable (covariant lifetime requirement). You also cannot borrow data outside the yoke because of the static lifetime requirement.

With generalized binders, someone using yoke would instead write Yoke<for<'a> Cow<'a, str>, Rc<[u8]>>.

(I still need to think more about variance in this situation, because an equivalent GAT would be invariant. Maybe introduce some marker traits to inspect/declare the variance of a lifetime (Covariant<'a>, Contravariant<'a>), then use for<'a> T::<'a>: Covariant<'a> in the where clause.)

Another place where it would be useful is gc-arena, kyju.org. It currently uses a Rootable<'gc> trait (similar to Yokeable) and a trait object "hack" (dyn for<'gc> Rootable<'gc, Root = MyType<'gc>>), and unfortunately still requires 'static (because of both the trait and trait object).

Generic binders

To declare a generic parameter that accepts a binder, prefix the generic parameter with for<...>. e.g. trait AcceptsABinder<for<'_> T>. Then to bind the generic parameters, use turbofish syntax: fn borrow<'a>(&'a self) -> T::<'a>.

This works in traits, structs, fns, etc.

For a concrete example (using type binders, which again I am still on the fence about, but illustrates better than lifetime binders):

struct Data<for<_> C = for<T> T> {
  ints: C::<i32>,
  floats: C::<f32>,
}

type DataRef<'a> = Data<for<T> &'a T>;
type DataSlice<'a> = Data<for<T> &'a [T]>;
type DataArray<const N: usize> = Data<for<T> [T; N]>;
type DataVec = Data<for<T> Vec<T>>;`

Binders as values

Generalized binders have the same layout as their non-binder counterpart. Therefore you can transmute between for<'a> Something<'a> and Something<'b> (where 'b is a defined lifetime). This is the main entrypoint for using a binder type as a value, and is exactly what unsafe_binders does today.

A crate like yoke would store the Yokeable type as a binder, and would transmute from for<'a> Y to Y::<'b>, assuming its safety preconditions are met (covariance).

1 Like

Note that fn foo<for<'_> T> and similar are already possible with gats, see higher_kinded_types - Rust

True, however there are several shortcomings with the hkt crate, like having to create a trait for combination of arities and trait bounds. In the Data example you can write the following:

fn ints_slice<for<T> C: AsRef<[T]>>(data: &Data<C>) -> &[i32] {
  data.ints.as_ref()
}

which otherwise requires reimplementing the hkt crate inside your own crate just to add the AsRef<[T]> bound.

This does not require binder types (equivalent to full HKTs), it can be done with binders in bounds, like currently, if we allow type binders.