Overloaded type families
Overloading
Overloading is to give the same name to different implementations.
In Haskell, type classes enable overloading of terms. For example, the two
functions map
and (.)
are generalized by the concept of “functors”:
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
instance Functor [] where
fmap :: (a -> b) -> ([a] -> [b])
fmap = map
instance Functor ((->) r) where
fmap :: (a -> b) -> ((r -> a) -> (r -> b))
fmap = (.)
What about type families?
We can also define a type-level fmap
simply as:
type family FMap (m :: a -> b) (x :: f a) :: f b
(Enable the extensions TypeFamilies
and also
TypeInType
.)
“Type class instances of Functor
” become simply “type family instances of
FMap
”. For lists:
-- FMap :: (a -> b) -> ([a] -> [b])
type instance FMap m '[] = '[]
type instance FMap m (v ': vs) = m v ': FMap m vs
And FMap
for type constructors (r -> a
) can be defined using
Data.Functor.Compose
.
Compare this to the instance Functor ((->) r)
above:
-- FMap :: (a -> b) -> ((r -> a) -> (r -> b))
type instance FMap m n = Compose m n
Whereas at the term level, functions can be defined by pattern-matching
on their arguments, at the type level, type families can be defined by
pattern-matching not only on their arguments but also their kinds.
Notice that in the last type family instance above, we actually don’t
inspect m
and n
, but the type family will check whether the kind
of n
is an arrow r -> a
in order to use that instance.
We can similarly write a lazy instance of FMap
for Proxy
:
-- FMap :: (a -> b) -> Proxy a -> Proxy b
type instance FMap m p = 'Proxy
The resulting mechanism resembles type classes, but the ergonomics are not
quite the same. One difference is that there is no type class constraint on
FMap
, because constraints are not a thing at that level. Whereas wrongly
applying fmap
may cause a “missing instance” error, incorrect usage of FMap
just gives us a stuck term, and we may somehow have to look for our mistake in
a huge type-level expression dumped by the compiler. That gap might be filled by
recent work on “constrained type families”.
Nevertheless, it’s quite cool that overloading at the type level Just Works™.
Here, this is a simple type family to keep it simple, but it applies to first-class families too. Thanks to isovector for pointing out this feature to me!