What/why are the differences between those three? Is a GADT (and regular data types) just a shorthand for a data family? Specifically what's the difference between:
data GADT a where
MkGADT :: Int -> GADT Int
data family FGADT a
data instance FGADT a where -- note not FGADT Int
MkFGADT :: Int -> FGADT Int
data family DF a
data instance DF Int where -- using GADT syntax, but not a GADT
MkDF :: Int -> DF Int
(Are those examples over-simplified, so I'm not seeing the subtleties of the differences?)
Data families are extensible, but GADTs are not. OTOH data family instances must not overlap. So I couldn't declare another instance/any other constructors for FGADT
; just like I can't declare any other constructors for GADT
. I can declare other instances for DF
.
With pattern matching on those constructors, the rhs of the equation does 'know' that the payload is Int
.
For class instances (I was surprised to find) I can write overlapping instances to consume GADTs:
instance C (GADT a) ...
instance {-# OVERLAPPING #-} C (GADT Int) ...
and similarly for (FGADT a), (FGADT Int)
. But not for (DF a)
: it must be for (DF Int)
-- that makes sense; there's no data instance DF a
, and if there were it would overlap.
ADDIT: to clarify @kabuhr's answer (thank you)
contrary to what I think you're claiming in part of your question, for a plain data family, matching on a constructor does not perform any inference
These types are tricky, so I expect I'd need explicit signatures to work with them. In that case the plain data family is easiest
inferDF (MkDF x) = x -- works without a signature
The inferred type inferDF :: DF Int -> Int
makes sense. Giving it a signature inferDF :: DF a -> a
doesn't make sense: there is no declaration for a data instance DF a ...
. Similarly with foodouble :: Foo Char a -> a
there is no data instance Foo Char a ...
.
GADTs are awkward, I already know. So neither of these work without an explicit signature
inferGADT (MkGADT x) = x
inferFGADT (MkFGADT x) = x
Mysterious "untouchable" message, as you say. What I meant in my "matching on those constructors" comment was: the compiler 'knows' on rhs of an equation that the payload is Int
(for all three constructors), so you'd better get any signatures consistent with that.
Then I'm thinking data GADT a where ...
is as if data instance GADT a where ...
. I can give a signature inferGADT :: GADT a -> a
or inferGADT :: GADT Int -> Int
(likewise for inferFGADT
). That makes sense: there is a data instance GADT a ...
or I can give a signature at a more specific type.
So in some ways data families are generalisations of GADTs. I also see as you say
So, in some ways, GADTs are generalizations of data families.
Hmm. (The reason behind the question is that GHC Haskell has got to the stage of feature bloat: there's too many similar-but-different extensions. I was trying to prune it down to a smaller number of underlying abstractions. Then @HTNW's approach of explaining in terms of yet further extensions is opposite to what would help a learner. IMO existentials in data types should be chucked out: use GADTs instead. PatternSynonyms should be explained in terms of data types and mapping functions between them, not the other way round. Oh, and there's some DataKinds stuff, which I skipped over on first reading.)
data family
is not much more than a way to group together several newtype/data declarations. In fact, you can almost think of a data family as an injective open type family (where eachdata instance
corresponds to a data decl and atype instance
to that decl). That analogy breaks down in two places (that I can think of): data family type constructors can be partially applied and data family type constructors have stronger typing rules (MyDataFamily a ~ g b
impliesMyDataFamily ~ g
anda ~ b
, whileMyInjectiveTyFam a ~ MyInjectiveTyFam b
impliesa ~ b
). – Offering