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