Lecture 4 — 2020-01-30

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 8.6.5: http://www.haskell.org/ghc/  :? for help
Prelude> :k []
[] :: * -> *
Prelude> :t [1,2]
[1,2] :: Num a => [a]

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 type classes

We discused types that characterize behavior, using 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)

  each = map -- overload with a known function

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 could write functions 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 our alternative implementation: union trees. The syntax here is a touch fancier than what we did in class, where we use record notation to name the two branches under a Union.

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, check out 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

Finally, you might run into a tricky spot. You might write something like:

s :: (Listlike f, Num a) => f a
s = cons 5 (cons 6 nil)

And the definition will be accepted just fine. But if you simply write cons 5 (cons 6 nil) in GHCi, you’ll get an error message:

> cons 5 (cons 6 nil)

<interactive>:5:1: error:
    • Ambiguous type variable ‘f0’ arising from a use of ‘print’
      prevents the constraint ‘(Show (f0 Integer))’ from being solved.
      Probable fix: use a type annotation to specify what ‘f0’ should be.
      These potential instances exist:
        instance [safe] Show a => Show (UnionTree a)
          -- Defined at Lec04.lhs:148:16
        instance Show a => Show (Maybe a) -- Defined in ‘GHC.Show’
        instance (Show a, Show b) => Show (a, b) -- Defined in ‘GHC.Show’
        ...plus 14 others
        ...plus one instance involving out-of-scope types
        (use -fprint-potential-instances to see them all)
    • In a stmt of an interactive GHCi command: print it

What does this mean? GHCi is basically complaining that we know that the term you wrote was of type f0 Integer, but it doesn’t know how to find Show (f0 a).

Here Integer was selected as the parameter type because it’s the default when Haskell can’t figure out a numeric type. That’s not the core problem.

The core issue is that f0 could be one of two things: ([]) or UnionTree. How should Haskell decide? The solution is to write a type ascription, as in:

> > cons 5 (cons 6 nil) :: UnionTree Int
Union {left = Singleton 5, right = Union {left = Singleton 6, right = Empty}}