lloydmeta / frunk

Funktional generic type-level programming in Rust: HList, Coproduct, Generic, LabelledGeneric, Validated, Monoid and friends.
https://beachape.com/frunk/
MIT License
1.29k stars 58 forks source link

derive macros for ToRef and ToMut? #224

Open dspyz-matician opened 1 year ago

dspyz-matician commented 1 year ago

If I have an &'a T where T implements Generic, it seems like I ought to be able to get a <T::Repr as ToRef<'a>>::Output from it, but there isn't any safe way to do that generically. It would be nice to have a derive macro which does this

lloydmeta commented 1 year ago

Just checking what you're asking for by using a concrete example

Given

struct S {
  a: u64,
  b: bool
}

let s = Struct { a: 5, b: false};
let s_ref = &s;

You want to be able to (something like) this:

let s_ref_repr: HList![ &u64, &bool ] = s_ref.into_generic_ref();

?

Some follow up questions:

dspyz-matician commented 1 year ago

Yes, that's exactly what I'm looking for

I have a large crate that was originally littered with structs like:

struct Fields {
  foo: Field<Foo>,
  bar: Field<Bar>,
  baz: Field<Baz>,
}

struct Page<'a> {
  foo: Accessor<'a, Foo>,
  bar: Accessor<'a, Bar>,
  baz: Accessor<'a, Baz>,
}

struct Mission {
  foo: Layer<Foo>,
  bar: Layer<Bar>,
  baz: Layer<Baz>,
}

I think in total there were about 8 structs like this and each one has the "same" 21 fields. About half have associated lifetimes and are constructed from references to others. Every time anyone wants to add a new field, they have to go through and update all of them and then they have to go through and update every place where each of the fields is addressed in turn doing the exact same thing.

I noticed I could reduce boilerplate with GATs and marker types:

trait Domain {
  type ElemType<T>;
}

struct General<D: Domain> {
  foo: D::ElemType<Foo>,
  bar: D::ElemType<Bar>,
  baz: D::ElemType<Baz>,
}

struct FieldsDomain;
impl Domain for FieldsDomain {
  type ElemType<T> = Field<T>;
}
type Fields = General<FieldsDomain>;

struct PageDomain<'a>;
impl Domain for PageDomain<'a> {
  type ElemType<T> = Accessor<'a, T>;
}
type Page<'a> = General<PageDomain<'a>>;

struct MissionDomain;
impl Domain for MissionDomain {
  type ElemType<T> = Layer<T>;
}
type Mission = General<MissionDomain>;

Then for the conversions between them that didn't involve references, I could use frunk (by deriving Generic on my General type) and expressing conversions with HList::map:

trait GenFunc<D: Domain, E: Domain> {
  fn call<T>(t: D::ElemType<T>) -> E::ElemType<T>;
}

struct MyPoly<F>(F)

impl<D: Domain, E: Domain, F: GenFunc<D, E>, T> Func<D::ElemType<T>> for MyPoly<F> {
  type Output = E::ElemType<T>;

  // This is a bit of a simplification, I actually wrote `MyFunc` with a `call` that takes `&mut self` as well and
  // re-implemented `MyHMappable` to use it so that I could smuggle in context from the caller. Also I'm
  // leaving out a constraint on T.
  fn call(input: D::ElemType<T>) -> E::ElemType<T> {
    GenFunc::<F>::call(t)
  }
}

impl<D: Domain> General<D> {
  fn hmap<F: GenFunc<D, E>, E: Domain>(self, f: F) -> General<E>
  where Self::Repr: HMappable<MyPoly<F>, Output=General<E>::Repr> // This constraint always holds, but the compiler doesn't know that until it sees the concreate instance
  {
    Generic::from(Generic::into(self).map(MyPoly(f)))
  }
}

But I still ended up with three instances of expressing all the fields in turn rather than a single source of truth. The first was the General struct itself, and the other two were the manual implementations of to_refs and to_muts. Auto-deriving this would allow me to reduce it to exactly one place since to_refs and to_muts could be expressed in terms of these functions together with HList::map.

(Preferrable):

trait RefToGeneric<'a> {
  type Repr;
  fn to_generic_refs(&'a self) -> Self::Repr;
}
impl RefToGeneric for Foo {
  ...
}

than if it's defined as:

(Less preferrable):

trait ToGenericRefs {
  type Repr;
  fn to_generic_refs(self) -> Self::Repr
}
impl<'a> ToGenericRefs for &'a Foo {
  ...
}

Thanks so much!

dspyz-matician commented 1 year ago

FYI, just opened https://github.com/davidspies/frunk_utils

lloydmeta commented 1 year ago

Thanks @dspyz-matician I think what you want makes sense and would make a good addition to the lib.

Regarding introducing a new into_generic_ref or re-using the existing into_generic method; I was mostly curious whether from an API/DX perspective, it would be preferred to have an explicit ref-handling way of going generic (ignoring for a second whether the compiler would actually allow us to use it nicely (w/o requiring type ascriptions), and whether we can even re-use the backing traits!). My thinking there is that it could be nice to try to re-use the existing one just to avoid increasing the API surface of frunk :)

BTW, if you're willing to give this a go, by all means, please go ahead!