Lecture 4 — 2018-01-25

Kinds and type classes

This lecture is written in literate Haskell; you can download the raw source.

module Lec04 where

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
  foldRight f b l =
    case openCons l of
      Nothing -> b -- list is empty
      Just (h,t) -> f h (foldRight f b t)      

  foldLeft :: (b -> a -> b) -> b -> f a -> b
  foldLeft f b l =
    case openCons l of
      Nothing -> b
      Just (h,t) -> foldLeft f (f b h) t

  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) !!!!

  foldRight f b u =
    case openCons u of
      Nothing -> b
      Just (h,t) -> f h (foldRight f b t)

  foldLeft f b u =
    case openCons u of
      Nothing -> b
      Just (h,t) -> foldLeft f (f b h) t

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