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

Generic for enums <-> Coproducts #114

Open ExpHP opened 6 years ago

ExpHP commented 6 years ago

So, obviously, structs are to HLists as enums are to Coproducts. Frunk has both of these data types, so why doesn't it implement Generic for enums yet?

After thinking about this a bit, it occurred to me that there's a nontrivial technical challenge here, due to the fact that rust doesn't consider enum variants to be types. Basically, we'd have to do something like one of the following:

Centril commented 6 years ago

Multi field variants

rust doesn't consider enum variants to be types

I don't think this needs to be a problem, we could convert the following enum

enum Foo {
    Var1(A, B, C),
    Var2(D, E),
    Var2 {
        field: F,
    },
    Nil,
    Red
}

to:

impl Generic... {
    type Repr = Coprod!(
        Hlist![A, B, C],
        Hlist![D, E],
        Hlist[F],
        (),
        (),
    );
}

Recursive types

What worries me more are recursive types, c.f:

enum List<T> {
    Nil,
    Cons(Box<List<T>>),
}

We can translate this to: Coprod!( (), Box<List<T>> ) but then the type is still mentioned.

ExpHP commented 6 years ago

Something does feel off to me about the asymmetry between struct <-> HList and enum <-> Coproduct<HList> but I can't quite place my finger on it. Aside from that it certainly does sound more promising than the ideas I listed.

I don't see any issue with the fact that Coprod!( (), Box<List<T>> ) mentions List<T>. (though I might argue that it should perhaps be Coprod!((), HList![Box<List<T>>]) or Coprod!(HNil, HList![Box<List<T>>])

ExpHP commented 6 years ago

Ahh, just one thing: If the user wants to query what variant of the enum their Coproduct is, this will be difficult to do since the types of the variants cannot be easily written and may collide with each other (e.g. for unit variants).

(note: I do admit that the use cases for querying the variant of Generic output are limited, since it's really supposed to be for SYB purposes)

Reified indices could help here (which again is something I want to consider post-0.2.0)

Centril commented 6 years ago

I guess Coprod!(HNil, HList![Box<List<T>>]) would be more consistent / regular?

I don't see any issue with the fact that Coprod!( (), Box<List<T>> ) mentions List<T>.

Do you remember how Haskell does this (https://hackage.haskell.org/package/base-4.11.0.0/docs/GHC-Generics.html) ?

Reified indices could help here (which again is something I want to consider post-0.2.0)

I think this is somewhat what GHC does?

ExpHP commented 6 years ago

Oh, I think I see the issue now. For some SYB trait implementations, List<T> <-> Coprod!( (), Box<List<T>> ) would result in a trait impl with circular where bounds. How unfortunate =/

Centril commented 6 years ago

(There's also scrapmetal in the SYB field)

blog post: http://fitzgeraldnick.com/2017/08/03/scrapmetal.html

lloydmeta commented 6 years ago

It would definitely be nice to figure out a nice way to get enums working with Generic :)

I think I attempted to do this before, but ran into snags that you have already mentioned; e.g. enum-variants aren't types.

The other part of this is, I couldn't really come up with a good use-case for it myself. E.g. if one has an enum, what advantage would translating it to a Coproduct bring? I think the answer to this might depend on the implementation as well.

For instance, if we just take the types of each enum member as Repr, in the below scenario, it isn't possible, AFAICT, to use their respective Generics to do transformations between them safely because of the overlapping generic representations in the enum members (EDIT: specifically referring to writing a generalisable inject/uninject here.)? It seems like it might also be confusing to .fold over them; Poly (type-based) folding is out of the picture, but even passing an HList of closures might be a bit error prone if someone changes the ordering of the types.

enum Pets {
  Dog, 
  Horse,
  Cat(i32),  
  Snake(i32),
}

enum Colours {
    Red, 
   Yellow,
   Blue(i32), 
   Green(i32),
}

In addition; with a Repr = Coproduct![(), (), i32, i32], you can't use inject in a nice way because the compiler won't be able to infer an Index to inject into. Maybe this is where reified Indices can help?

If I'm not taking crazy pills, and these are indeed valid concerns, would it be OK to fail derivation when we resolve the same types for more than 1 enum member ?

ExpHP commented 6 years ago

RE: Variants with matching types: It may be difficult to work with the encoded form directly, but I believe there are still use cases for writing generic abstractions (SYB style). I feel Poly-based folding isn't necessarily out of the picture.

// horribly made-up example
pub enum Counts {
    None,
    Red(i32),
    Blue(i32),
    Both { red: i32, blue: i32 },
}

I can picture getting a total count out of the above with a Poly func that is implemented on HCons and HNil.

Moreover, a common pattern in my own code is stuff like the following:

pub enum CoordsKind<V> {
    Cart(V),
    Frac(V),
}

And I frequently have helper functions like the following, to help with the implementation of other things:

// once I have these three things, I seldom ever need to write a `match` expression again,
// except in cases where the behavior of Frac/Cart are legitimately different
impl<V> CoordsKind<V> {
    fn as_refs(&self) -> CoordsKind<&V> { ... }
    fn as_muts(&mut self) -> CoordsKind<&mut V> { ... }

    fn map<U, F>(self, f: F) -> CoordsKind<U>
    where F: FnOnce(V) -> U,
    { ... }

    fn fold<U, F>(self, f: F) -> U
    where F: FnOnce(V) -> U,
    { ... }
}

It often feels to me like there ought to be some way I could use Generic to accomplish some of this stuff. But I haven't really thought too hard about it yet to know how well it works out.

Maybe this is where reified Indices can help?

They could; #[derive(Generic)] could have an option to emit constants for the indices for each variant.

Mind; for the most part, reified indices doesn't add very many new capabilities, at least not strictly speaking. For the most part, they are simply a far-more-ergonomic alternative to manually specifying the Index param through UFCS. However, thanks to that, they make it reasonable to have public APIs where the user specifies the index, which in turn allows us to reasonably add methods to the API where the type of the element at a given index is given by an associated type rather than a type parameter. (and that could be considered to be the foundation for the new capabilities it adds)

lloydmeta commented 6 years ago

in turn allows us to reasonably add methods to the API where the type of the element at a given index is given by an associated type rather than a type parameter. (and that could be considered to be the foundation for the new capabilities it adds)

Ah interesting; I hadn't thought of that. Interesting food for thought :) Thanks.

Centril commented 6 years ago

Also, I wonder if it would be worthwhile to add something like the following to Generic:

    fn repr_map<Repr, Mapper>(self, mapper: Mapper) -> Self // Bikeshed on name
    where
        Self: Generic<Repr = Repr> + Sized,
        Mapper: FnOnce(Repr) -> Repr
    {
        Self::from(mapper(self.into()))
    }

This lets you apply a function to the Repr form of the type.

lloydmeta commented 6 years ago

Oh interesting indeed; that would make it easy to apply the same Mapper to different Generic impl'ing types :)

thomaseizinger commented 6 years ago

Just wanted to put this here. There is now an RFC for making enum variants actual types: https://github.com/rust-lang/rfcs/pull/2593

Diggsey commented 5 years ago

What about deriving LabelledGeneric? If you use the variant name to distinguish variants rather than the type, then doesn't this solve the problem with overlapping impls?

ExpHP commented 5 years ago

Probably, yes. There is a bit of ambiguity though: Enum variants have fields themselves, so LabelledGeneric on enums could be interpreted in a couple of ways:

  1. (Field option) Produces unlabelled variants with labelled fields.
  2. (Variant option) Produces labelled variants with unlabelled fields.
  3. (Double-down option) Produces labelled variants with labelled fields.

(of course, we can still also have Generic that produces unlabelled variants with unlabelled fields)

Doubling down doesn't sound like too bad of an idea, though it may annoy some users who get more than they bargained for. Eh.

I've been mostly fine without the feature, so I haven't put that much energy into thinking about it since making the issue. If anybody can get it to work, PRs are welcome!