Lecture 4.0 — 2016-09-08

Our first interpreter; type classes

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

After looking at the language likes and dislikes, we saw some examples from the homework and did some refactoring as a class.

Then I introduced us to our first interpreter. We started on the board, defining a grammar like so:

p, q in Prop ::= T | ⊥ | p ∧ q | p ∨ q | ¬ p | p ⇒ q

p1 = T ∧ ⊥
p2 = ⊥ ∨ ⊥

We talked about what the propositions p1 and p2 mean… nothing, yet! We had to define an interpretation function, where we interpret elements of Prop as booleans:

2 = { true, false} with standard operations &&, ||, and !

interp : Prop -> 2
interp(T) = true
interp(⊥) = false
interp(p ∧ q) = interp(p) && interp(q)
interp(p ∨ q) = interp(p) || interp(q)
interp(¬ p) = !interp(p)
interp(p ⇒ q)  = !interp(p) || interp(q)

Then we did the same in Haskell:

data Prop =
    T
  | F
  | And Prop Prop
  | Or Prop Prop
  | Not Prop
  | Implies Prop Prop

p1 :: Prop
p1 = T `And` F

interp :: Prop -> Bool
interp T = True
interp F = False
interp (And p q) = interp p && interp q
interp (Or p q) = interp p || interp q
interp (Not p) = not $ interp p
interp (Implies p q) = not (interp p) || interp q

We wrote a “pretty printer” for Prop which prints out appropriately parenthesized concrete syntax. It was a little aggressive, though, printing way too many parentheses:

parens :: String -> String
parens s = "(" ++ s ++ ")"

instance Show Prop where
  show T = "T"
  show F = "_|_"
  show (And p q) =
    parens (show p) ++ " /\\ " ++ parens (show q)
  show (Or p q) =
    parens (show p) ++ " \\/ " ++ parens (show q)
  show (Not p) = "~" ++ parens (show p)
  show (Implies p q) =
    parens (show p) ++ " => " ++ parens (show q)

Finally, we talked about how we can manipulte the abstract syntax of Prop in its own right; for example, we can translate away any use of Implies.

compile :: Prop -> Prop
compile (Implies p q) = Not (compile p) `Or` compile q
compile (And p q) = And (compile p) (compile q)
compile (Or p q) = Or (compile p) (compile q)
compile (Not p) = Not $ compile p
compile T = T
compile F = F

Kinds; Listlike as a typeclass

Kinds

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

  tl [] = Nothing
  tl (_:xs) = Just xs

  isNil = null
  foldRight = foldr
  foldLeft = foldl
  -- just take each and append as the usual

We also defined a union-tree as an alternate list representation.

data UnionTree a =
    Empty
  | Singleton a
  | Union { left :: (UnionTree a), right :: (UnionTree a) }
  deriving Show

instance Listlike UnionTree where
  nil = Empty

  cons x Empty = Singleton x
  cons x xs = Union (Singleton x) xs

  openCons Empty = Nothing
  openCons (Singleton a) = Just (a,Empty)
  openCons (Union l r)   =
    case openCons l of
      Nothing -> openCons r
      Just (x,l') -> Just (x,Union l' r)

  isNil Empty = True
  isNil (Singleton _) = False
  isNil (Union l r) = isNil l && isNil r

  foldRight f v l =
    case openCons l of
      Nothing -> v
      Just (x,xs) -> f x (foldRight f v xs)

  foldLeft f v l =
    case openCons l of
      Nothing -> v
      Just (x,xs) -> foldLeft f (f v x) xs

  each f Empty = Empty
  each f (Singleton a) = Singleton $ f a
  each f (Union l r) = Union (each f l) (each f r)

  append = Union

Note that we overload append to use a much more efficient implementation—O(1) compared to O(n)!

asList :: Listlike f => f a -> [a]
asList = foldRight (:) []

concat :: Listlike f => f (f a) -> f a
concat = foldRight append nil