Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

derive macros for ToRef and ToMut? #224

Open
dspyz-matician opened this issue Oct 18, 2023 · 4 comments
Open

derive macros for ToRef and ToMut? #224

dspyz-matician opened this issue Oct 18, 2023 · 4 comments

Comments

@dspyz-matician
Copy link

dspyz-matician commented Oct 18, 2023

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
Copy link
Owner

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:

  • What is your use case for this? Can you provide some context and maybe a concrete example, just to make sure we're solving your problem?
  • Curious about your thoughts on whether something like this should be its own method (e.g. s_ref.into_generic_ref()) or the same method as the existing one (e.g. s_ref.into_generic())
  • Curious about your thoughts on whether it would be useful to have this implemented for LabelledGeneric as well as Generic

@dspyz-matician
Copy link
Author

dspyz-matician commented Oct 22, 2023

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

  • In answer to 1:

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.

  • In answer to what I think you're asking in 2: I'm guessing the compiler will be able to infer types without so much
    hand-holding at the call-site if the trait is defined as:

(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 {
  ...
}

  • In answer to 3, I'm not really using LabelledGeneric for anything besides getting field names at runtime.

Thanks so much!

@dspyz-matician
Copy link
Author

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

@lloydmeta
Copy link
Owner

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants