Type synonym families are just type-level functions: they associate parameter types with result types. These come in three different varieties.
These work much like ordinary value-level Haskell functions: you specify some clauses, mapping certain types to others:
{-# LANGUAGE TypeFamilies #-}
type family Vanquisher a where
Vanquisher Rock = Paper
Vanquisher Paper = Scissors
Vanquisher Scissors = Rock
data Rock=Rock; data Paper=Paper; data Scissors=Scissors
These work more like typeclass instances: anybody can add more clauses in other modules.
type family DoubledSize w
type instance DoubledSize Word16 = Word32
type instance DoubledSize Word32 = Word64
-- Other instances might appear in other modules, but two instances cannot overlap
-- in a way that would produce different results.
An open type family can also be combined with an actual class. This is usually done when, like with associated data families, some class method needs additional helper objects, and these helper objects can be different for different instances but may possibly also shared. A good example is VectorSpace
class:
class VectorSpace v where
type Scalar v :: *
(*^) :: Scalar v -> v -> v
instance VectorSpace Double where
type Scalar Double = Double
μ *^ n = μ * n
instance VectorSpace (Double,Double) where
type Scalar (Double,Double) = Double
μ *^ (n,m) = (μ*n, μ*m)
instance VectorSpace (Complex Double) where
type Scalar (Complex Double) = Complex Double
μ *^ n = μ*n
Note how in the first two instances, the implementation of Scalar
is the same. This would not be possible with an associated data family: data families are injective, type-synonym families aren't.
While non-injectivity opens up some possibilities like the above, it also makes type inference more difficult. For instance, the following will not typecheck:
class Foo a where
type Bar a :: *
bar :: a -> Bar a
instance Foo Int where
type Bar Int = String
bar = show
instance Foo Double where
type Bar Double = Bool
bar = (>0)
main = putStrLn (bar 1)
In this case, the compiler can't know what instance to use, because the argument to bar
is itself just a polymorphic Num
literal. And the type function Bar
can't be resolved in “inverse direction”, precisely because it's not injective† and hence not invertible (there could be more than one type with Bar a = String
).
†With only these two instances, it is actually injective, but the compiler can't know somebody won't add more instances later on and thereby break the behaviour.