I have a GADT constructor representing the same idea, but I need two of them because the type is flexible depending on context. See this contrived example:
data A
data B
data C
data Thing a where
AFoo :: String -> Thing A
Bar :: Float -> Thing A
BFoo :: String -> Thing B
Baz :: Int -> Thing B
Bah :: Char -> Thing C
AFoo
and BFoo
represent the same underlying data concept: some data with a string. But in this context, it's more than that. Ideally AFoo
and BFoo
would be merged into Foo
, because they represent exactly the same thing, but I need one that types like Thing A
and one that types like Thing B
. As mentioned before, some contexts in which a Foo
can be used require a Thing A
and some require a Thing B
, so to satisfy the type system one constructor needs to exist for each.
I could of course write a function that "casts" to the desired type by switching the constructor if needed:
fooAsThingA :: Thing a -> Maybe (Thing A)
fooAsThingA t@(AFoo _) = Just t
fooAsThingA (BFoo s) = Just $ AFoo s
fooAsThingA _ = Nothing
(And similarly for fooAsThingB
)
But this is ugly, because it requires a Maybe
that I have to propagate because not all Thing B
s can become Thing A
s (in fact, only BFoo
can).
Ideally, I'd like to write:
data A
data B
data C
data Thing a where
Foo :: String -> Thing ??
Bar :: Float -> Thing A
Baz :: Int -> Thing B
Bah :: Char -> Thing C
But I'm unclear on what I'd put in place of the ??
. Presumably it's some way of representing the union of A
and B
(and perhaps this requires changing the types of the other constructors for this GADT, but that's fine).
Just to be clear, if the above were valid, I'd expect given the following functions:
processThingA :: Thing A -> String
processThingB :: Thing B -> Int
processThingC :: Thing C -> Float
Then, I'd be able to do all of the following:
processThingA $ Foo "Hello"
processThingB $ Foo "World"
processThingA $ Bar 3.14
processThingB $ Baz 42
processThingC $ Bah '\n'
tl;dr In the first snippet, is it possible to merge AFoo
and BFoo
into a Foo
that types as both Thing A
and Thing B
?
edit: Note: there are other EmptyDataDecls
than A
and B
that I use for Thing a
. I added C
as an example.
data Thing a where Foo :: String -> Thing a ...
? Or you could useDataKinds
to restrict it a bit:data ThingType = A | B; data Thing (a :: ThingType) where Foo :: String -> Thing a ...
. Do those fit with your use case? – David YoungDataKinds
. I do have other types (updated question to add aThing C
) which I don't want to part of this "union," so I don't think the first would work. But if I diddata AOrB = A | B; data C
would there be a way withDataKinds
to restrict only the type for theFoo
constructor? – Bailey ParkerFoo :: String -> AorB t -> Thing t
whereAorB t
is a suitable GADT, only populated byJustA :: AorB A
andJust B :: AorB B
. It's not terribly convenient, and has some memory overhead, but it could work well enough. You could even use a type class to keep that witness "implicit", in principle. The overhead is still there, but one does not have to pass it explicitly. – chiBar :: Float -> Thing JustA
? And forBah
, I'd keep thedata C
and have it beBah :: Char -> Thing C
? I'm not too bothered by the memory overhead, but I am a little irked that this would require pattern matching on an additional useless value. Can you expand on how this can be done with type classes? It's not immediately obvious to me. – Bailey ParkerAorB
value. How do I instantiate aFoo
?Foo "abc" JustA
can only be used inThing (AorB A)
contexts. It seems like I'd still need a "runtime cast"-like thing to changeFoo "abc" JustA
into aFoo "abc" JustB
to be able to pass it into a function expectingThing (AorB B)
(and this has the aforementioned problem of having too broad of a typeThing a -> Thing (AorB A)
which requires aMaybe
). Is there a way to avoid this? – Bailey Parker