I'm relatively new to Haskell, and I'm trying to understand one of the definitions of HList.
data instance HList '[] = HNil
newtype instance HList (x ': xs) = HCons1 (x, HList xs)
pattern HCons x xs = HCons1 (x, xs)
I have a couple specific questions:
What is the
'[]
and(x ': xs)
syntax I'm seeing? It almost looks like it's pattern matching on variadic type parameters, but I have never seen this syntax before, nor am I familiar with variadic type parameters in Haskell. I would guess this is part of GHC's Type Families, but I don't see anything about this on the linked page, and it's rather hard to search syntax in Google.Is there any point in using a
newtype
declaration with a tuple (instead of adata
declaration with two fields) besides avoiding boxing ofHCons1
?
'[]
and(x ': xs)
are syntax for type-level lists in the sense that theDataKinds
language extension allows promoting types to kinds and constructors to types; i.e. ifk
is some kind, then'[k]
is also a kind, and'[]
is a type of kind'[k]
, and ift :: k
andts :: '[k]
, thent ': ts :: '[k]
. Everything gets shifted by one.So in
HList (x ': xs)
,x
andxs
match two types:x
matches a "normal" type of kind*
(e.g.Int
) andxs
matches another type-level list of kind'[*]
. The right-hand side defines a (newtype
) datatype that has a constructorHCons1
with a parameter of type(x, HList xs)
.As an example, we can have
Or, using the pattern synonym:
I don't have a good answer to your second question regarding why it's represented as a newtype with a tuple.
First, you are missing part of the definition: the
data family
declaration itself.This is called a
data family
(available under theTypeFamilies
extension).This is a bidirectional pattern (available under the
PatternSynonyms
extension).When you see
'
marks in front of constructors, it is to denote their promoted type-level counterparts. As a syntactic convenience, promoted lists and tuples also just need the extra tick (and we still get to write'[]
for the empty type-level list and':
for the type level cons. All of this is available through theDataKinds
extension.Yes, it is to make sure that
HList
has a representational role, which means you can coerce betweenHList
s1. This is a bit too involved to explain in just an answer, but here is an example of where things don't go as we want when we haveinstead of the
newtype instance
(and no pattern). Consider the followingnewtype
s which are representationally equivalent toInt
,Bool
, and()
respectivelyRecall we can use
coerce
to wrap or unwrap these types automatically. Well, we'd like to be able to do the same thing, but for a wholeHList
:This works with the
newtype instance
variant, but not thedata instance
one because of the roles. (More on that here.)1 technically, there is no role for a
data family
as a whole: the roles can be different for eachinstance
/newtype
- here we only really need theHCons
case to be representational, since that's the one that gets coerced. Check out this Trac ticket.