Lecture 5 — 2017-09-12
Kinds and type classes
This lecture is written in literate Haskell; you can download the raw source.
Just as types classify terms, kinds classify types. Haskell has two kinds:
k ::= * | * -> *
First, *
, pronounced “star”, is the kind of complete types, which classify terms. Int
has kind *
, as does Bool
. The types [Int]
and Maybe Bool
have kind *
, too. The types []
(read “list”) or Maybe
have kind * -> *
: they’re type constructors. There are no terms with the type []
or Maybe
… terms only ever have types of kind *
.
But: if you give []
a type, then you will have a complete type, as in [Int]
.
Next, the type of functions (->)
has the kind * -> * -> *
. If you give (->)
two type parameters a
and b
, you will have a function type, a -> b
. If you give it just one parameter, you will have a type constructor (a ->)
of kind * -> *
. It is unfortunately somewhat confusing that ->
means two different things here: it’s both the function type and the ‘arrow’ kind. Rough stuff.
Just as :t
in GHCi will tell you the type of a term, :k
will tell you the type of a kind. For example:
GHCi, version 7.10.2: http://www.haskell.org/ghc/ :? for help
Prelude> :k []
[] :: * -> *
Prelude>
Why do we care about kinds? In the next few classes, we’ll be looking at non-*
kinded types in order to talk about groups of behavior. The next bit will be a first foray into types with interesting kinds.
Interface-like typeclasses
We didn’t get a chance to look at type classes that characterize behavior. The Listlike
type class characterizes type constructors of kind * -> *
that behave like lists.
class Listlike f where
nil :: f a
cons :: a -> f a -> f a
Note that f
must have kind * -> *
, because we apply it to the type parameter a
, which must have kind *
. Why? Because cons
has type a -> f a -> f a
, and (->)
has kind * -> * -> *
and is applied to a
.
openCons :: f a -> Maybe (a,f a)
hd :: f a -> Maybe a
hd l =
case openCons l of
Nothing -> Nothing
Just (x,_) -> Just x
tl :: f a -> Maybe (f a)
tl l =
case openCons l of
Nothing -> Nothing
Just (_,xs) -> Just xs
isNil :: f a -> Bool -- true iff openCons returns Nothing
isNil l =
case openCons l of
Nothing -> True
Just _ -> False
foldRight :: (a -> b -> b) -> b -> f a -> b
foldLeft :: (b -> a -> b) -> b -> f a -> b
each :: (a -> b) -> f a -> f b
each f = foldRight (cons . f) nil
append :: f a -> f a -> f a
append xs ys = foldRight cons ys xs
We can show that the list type constructor, []
, which has kind * -> *
, is an instance of the Listlike
class. On an intuitive level, this should be no surprise: lists are indeed listlike.
instance Listlike [] where
nil = []
cons = (:)
openCons [] = Nothing
openCons (x:xs) = Just (x,xs)
foldRight = foldr
foldLeft = foldl
-- just take each and append as the usual
We’ve defined here a minimal instance: for every undefined type signature in the class, we give a definition. If anything were defined circularly—say, openCons
in terms of isNil
, hd
, and tl
—we’d have to define at least enough to “break the loop”. Finally, it’s critical that we listen to the comments in the class—if we defined an isNil
that disagreed with openCons
, all kinds of crazy things would happen!
Once we defined our minimal instance, we wrote a function that only uses the Listlike
type class, without any knowledge of the underlying implementation f
.
myConcat :: Listlike f => f (f a) -> f a
myConcat = foldRight append nil
For example, the following wouldn’t type check:
myConcat' :: Listlike f => f (f a) -> f a
myConcat' = foldRight (++) []
Why not? Haskell sees that f
is some Listlike
type, but that could (in principle) be anything. And just because ([])
happens to be Listlike
, who’s to say that our f
is ([])
this time?
Here’s an alternative implementation: union trees.
data UnionTree a =
Empty -- []
| Singleton a -- [a]
| Union { left :: UnionTree a, right :: UnionTree a }
deriving Show
instance Listlike UnionTree where
-- nil :: f a
nil = Empty
-- cons :: a -> f a -> f a
cons x xs = Union (Singleton x) xs
-- openCons :: f a -> Maybe (a,f a)
openCons Empty = Nothing
openCons (Singleton x) = Just (x,Empty)
openCons (Union l r) =
case openCons l of
Nothing -> openCons r
Just (x,l') -> Just (x,Union l' r)
append = Union -- O(1) vs. O(n) !!!!
By defining append
inside the Listlike
class, we were able to override the definition for the UnionTree
instance with one that’s much more efficient. Neato! For an example of this in action, checkout the Foldable
type class.
ut1,ut2,ut3 :: UnionTree String
ut1 = Union (Singleton "hi") (Singleton "there")
ut2 = Union Empty Empty
ut3 = Union (Singleton "everybody") Empty
ut = myConcat (Union (Singleton ut1) (Union (Singleton ut2) (Singleton ut3)))
toList :: Listlike f => f a -> [a]
toList l = foldRight (:) [] l
fromList :: Listlike f => [a] -> f a
fromList l = foldr cons nil l