soenkehahn / generics-eot

A library for generic programming that aims to be easy to understand
https://generics-eot.readthedocs.io/
BSD 3-Clause "New" or "Revised" License
26 stars 6 forks source link

Removing `Void` and `()` terminators from final result #25

Closed masaeedu closed 4 years ago

masaeedu commented 4 years ago

Hello. I found this library because I needed something very similar to it, but unfortunately my use case prevented me from being able to use a type with extra ()s and Voids. So for example for the type:

data [a] = [] | a : [a]

I need:

Either () (a, [a])

instead of:

Either (Either (a, ([a], ())) Void)

I tried seeing if the former behavior could be implemented, and while it's not fun, I was able to build it on top of generics-sop. You can see the results here.

Here's what using it looks like:

λ> :t fwd gsop [1, 2, 3]
fwd gsop [1, 2, 3] :: Num a => () + (a × [a])
λ> fwd gsop [1, 2, 3]
Right (1,[2,3])
λ> bwd gsop (Right $ (1, [2, 3])) :: [Int]
[1,2,3]
λ> bwd gsop (Left ()) :: [Int]
[]

Would you be interested in a change to this library that makes it behave in a similar fashion? I understand that this is a big breaking change and maybe your use case actually intentionally prefers the end markers, but I thought I'd check anyway.

soenkehahn commented 4 years ago

The end markers are actually necessary for being able to properly iterate through the structure of the ADTs. Consider this example:

{-# LANGUAGE DeriveGeneric #-}

import Generics.Eot

data Foo = Foo Int Int
  deriving (Generic)

data Bar = Bar (Int, Int)
  deriving (Generic)

In the current implementation, i.e. with end markers you get:

Eot Foo ~ Either (Int, (Int, ())) Void
Eot Bar ~ Either ((Int, Int), ()) Void

With these types generic code can distinguish between tuples in fields in tuples stemming from Eot. And for some generic code that is essential. Imagine for example a serialization library that wants to preserve the tree structure of the data as it exists in the haskell types.

If I understand you right, what you're proposing is to have Eot behave like this:

Eot Foo ~ (Int, Int)
Eot Bar ~ (Int, Int)

Which then wouldn't allow to treat Foo and Bar differently any more. That's why these end markers exist.

I believe you can construct a similar example for the Void end marker.

I think what you want can be implemented on top of generics-eot. Although if you already have a working implementation on top of generics-sop there's no good reason to switch.

masaeedu commented 4 years ago

@soenkehahn That's a good point, I hadn't considered that use case. You did understand my proposal correctly; for my use case I need to eliminate the distinction between those two representations so the user doesn't need to worry about what the exact data type is, so long as they can build up something that contains the equivalent data.

You're right again about being able to build what I want on top of generics-eot, in fact the ENormalize class in the link I provided does exactly that. It takes an either-of-tuples representation with end markers that's identical to what you get from eot and then provides an isomorphism to a representation lacking the end markers. My suggestion was to roll this into eot, but now I see this doesn't quite satisfy all the use cases the library supports.