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

Generic for enums <-> Coproducts #114

Open
ExpHP opened this issue Apr 7, 2018 · 14 comments
Open

Generic for enums <-> Coproducts #114

ExpHP opened this issue Apr 7, 2018 · 14 comments

Comments

@ExpHP
Copy link
Collaborator

ExpHP commented Apr 7, 2018

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:

  • we could generate our own structs for the enum variants
    • this raises questions about type parameters. (consider some type equivalent to Option<T>. Should the None type have type parameters? If so, does the user need to add phantom fields?)
  • we could restrict it to enums where all of the variants are newtype-like
  • other ideas welcome
@Centril
Copy link
Collaborator

Centril commented Apr 7, 2018

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
Copy link
Collaborator Author

ExpHP commented Apr 7, 2018

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
Copy link
Collaborator Author

ExpHP commented Apr 7, 2018

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

Centril commented Apr 7, 2018

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
Copy link
Collaborator Author

ExpHP commented Apr 7, 2018

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

Centril commented Apr 7, 2018

(There's also scrapmetal in the SYB field)

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

@lloydmeta
Copy link
Owner

lloydmeta commented Apr 8, 2018

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
Copy link
Collaborator Author

ExpHP commented Apr 8, 2018

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

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

Centril commented Apr 9, 2018

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

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

@thomaseizinger
Copy link

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

@Diggsey
Copy link
Contributor

Diggsey commented May 31, 2019

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
Copy link
Collaborator Author

ExpHP commented May 31, 2019

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!

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

5 participants