Question

So if I have two data types which are mostly the same, I can write them like this:

data A t = A1 | A2 | A3 | A4 (B t)
data B t = B1 | B2 | B3 | B4 t

type AX = A X
type AY = A Y

Now it's easy to write functions over AX and AY, or 'A t' if they don't care. But then how do I write a conversion?

convert :: AX -> AY
convert (A4 (B4 x)) = A4 (B4 (xToY x))
convert ax = ay -- type error

So now I need to write all the other cases out by hand, even though none of them depend on the type parameter. What's worse, while I can match a constructor without depending on the arguments with 'A {}', that's not possible if I need those arguments to reconstruct the data.

Is there a nicer way to do this? I feel like GADTs should be able to express this but it's hard to see how to eliminate the type variable from the terms that don't depend on it. I think I'd have to have separate types for A1 A2 etc. and then I'd lose the closed-ness and case checking... besides I don't want to write Show, Eq, and Typeable by hand! The only other way I can think of is to rearrange the whole structure to isolate the changing part i.e.

data A t = Independent | B4 t
data Independent = A1 | A2 | ...

But this makes every other use of the data awkward for the benefit of one conversion function.

Of course, yet another option is to forget about type safety and include 'B4 (Either X Y)' and put in some nice runtime errors for when it has the wrong value.

Maybe there's a better way to approach "almost the same" data types?

Update: so I worked around by writing a pseudo-fmap:

convert :: (a -> b) -> A a -> A b

Which at least allows me to separate the conversion part and the packing-unpacking boilerplate. I guess a sufficiently advanced bit of TH could probably generate one of those for me. I'm still curious about other approaches though. I feel like this way is still not precise, because it allows the hole to be filled with anything while what I really mean is exactly one of two things.

Was it helpful?

Solution

It seems like basically what you want is for A and B to be Functors. Fortunately, you can allow Haskell to derive the obvious Functor instances for these types, using -XDeriveFunctor. Then, use this code:

data A t = A1 | A2 | A3 | A4 (B t) deriving Functor
data B t = B1 | B2 | B3 | B4 t deriving Functor

...

convert :: AX -> AY
convert a = fmap xToY a

Here's a sample GHCi session after adding some dummy definitions for X, Y and xToY, as well as having everything derive Show:

*Main> convert $ A4 B1
A4 B1
*Main> convert $ A4 (B4 X)
A4 (B4 Y)
*Main> convert $ A2
A2

Update

If you really want to only allow A and B to be "filled" with things of type X or Y, you could constrain them like so:

class XorY t where

instance XorY X
instance XorY Y

data XorY t => A t = A1 | A2 | A3 | A4 (B t)
data XorY t => B t = B1 | B2 | B3 | B4 t

Of course this will preclude the possibility of making A and B Functors, so the trick outlined above won't work any more.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top