Lecture 17 — 2015-11-04
Type classes
This lecture is written in literate Haskell; you can download the raw source.
What’s happening? We’ll do more weeks of serious Haskell training, and then… cool stuff! Topics will definitely include automatic random testing and parser combinators. We’ll choose between a few others: concurrency, lenses/bidirectional programming, applications of PL in networks, or other topics (like static analysis). I’ll put out a poll when the time comes.
For the upcoming HW08, it will come in two parts. The first part is up now; I’ll post to Piazza when the second part is available. It will be due on 2015-11-15.
Type classes
Type classes are groups of functions associated with a type. Each type can have a single instance for each type class.
What are they for? In general, type classes characterize common interfaces and behaviors. For example:
- printing (
Show) - parsing (
Read) - equality (
Eq) - ordering (
Ord) - representability in memory/disk (
Storable)
Type classes get a lot of use in Haskell. More complex type classes include:
- enumerability (
Enum) - boundedness (
Bounded) - being a structure that has
foldlandfoldr(Foldable)
Type classes are very much like classes in object-oriented languages, but there are important differences. Java, C++, and Python use classes to group code and data together, but Haskell separates data definitions from class definitions. Traditional OO “objects” are instances of classes, but Haskell’s “objects” are just values, while “instances” are implementations of interfaces.
Running examples
We used three types as our running examples. TVL is a three-valued logic; List a is lists containing a; OneOfThree a b c is like Either a b but has a third option.
module Lec17 where
data TVL = Yes | No | Maybe
data List a =
Nil
| Cons a (List a)
data OneOfThree a b c =
L a
| M b
| R cThe Show type class
Haskell declares Show as follows:
class Show a where
show :: a -> String
Here are some instances that use it.
instance Show TVL where
show Yes = "Yes"
show No = "No"
show Maybe = "Maybe"
instance (Show a) => Show (List a) where
show Nil = "Nil"
show (Cons x xs) = show x ++ "," ++ show xs
instance (Show a, Show b, Show c) => Show (OneOfThree a b c) where
show (L a) = "L " ++ show a
show (M b) = "M " ++ show b
show (R c) = "R " ++ show cWhen we want to use a type class function, we have to add a constraint to our type, as below. Pay careful attention to the difference between => and ->.
decision :: Show a => a -> String
decision result = "The committee answer was: " ++ show resultThe Eq and Ord type classes
The Eq type class is how we establish equality for Haskell types; the Ord type class is used for ordering. They are (effectively) defined as follows:
class Eq a where
(==) : a -> a -> Bool
(/=) : a -> a -> Bool
a /= b = not (a == b)
data Ordering = LT | EQ | GT
class Eq a => Ord a where
compare :: a -> a -> Ordering
There are a few things to note here. First, note that Eq provides a default implementation for (/=), which just negates (==). We could have separately defined:
(/=) :: Eq a => a -> a -> Bool
a /= b = not (a == b)
The Haskell library designers included (/=) in the Eq type class with a default implementation, instead. Why? This way, someone can create an instance of Eq that defines a more efficient verison of (/=) if their data structure permits it.
The Ord type class has an Eq type class constraint. You can’t define an instance for Ord a unless there’s already an instance for Eq a.
As a first cut for TVL, we might write:
instance Eq TVL where
No == No = True
Maybe == Maybe = True
Yes == Yes = True
_ == _ = False
instance Ord TVL where
compare No No = EQ
compare Yes Yes = EQ
compare Maybe Maybe = EQ
compare No _ = LT
compare Maybe No = GT
compare _ Yes = LT
We can do better though—there’s some redundancy here. If we define a single function for comparison, we can use it for both instances.
cmpTVL :: TVL -> TVL -> Ordering
cmpTVL No No = EQ
cmpTVL Maybe Maybe = EQ
cmpTVL Yes Yes = EQ
cmpTVL No _ = LT
cmpTVL Maybe No = GT
cmpTVL _ Yes = LT
instance Eq TVL where
a == b = cmpTVL a b == EQ
instance Ord TVL where
compare = cmpTVLnewtype and its uses
We’ve seen the data keyword, which introduces data structures that will exist at runtime. We’ve also seen the type keyword, which introduces a type synonym, a convenient shorthand for the type checker. The newtype keyword has features of both. Like type, it's just a hint for the type checker, and will have no influence on the runtime; likedata`, it has constructors… but only a single one.
The most common way to use a newtype is as follows:
newtype Reversed a = Reversed { reversedValue :: a }Note that (a) it takes a parameter, a, (b) the constructor has the same name as the type, and (c) we’ve used record notation to define an accessor, reversedValue.
So we have a constructor Reversed :: a -> Reversed a, an accessor or deconstructor reversedValiue :: Reversed a -> a… why bother? Recall that each type class can only have a single implementation of a given type class. Reversed a and a are different type, so we can give them different implementations. For example, here’s an implementation that reverses the ordering:
instance Show a => Show (Reversed a) where
show (Reversed a) = "Reversed " ++ show a ++ ""
instance Eq a => Eq (Reversed a) where
(Reversed a) == (Reversed b) = a == b
instance Ord a => Ord (Reversed a) where
compare (Reversed a) (Reversed b) =
case compare a b of
LT -> GT
EQ -> EQ
GT -> LTReversing the ordering is useful: this way sorting functions don’t need parameters to indicate direction, but can just follow the ordering of the type class.
Here’s another variation, where we suppress printing of values. This way we can print values but controllably hide the parts that might be too big or awkward to print on our console.
newtype Hidden a = Hidden { unhide :: a }
instance Show (Hidden a) where
show _ = "Hidden _"
instance Eq a => Eq (Hidden a) where
(Hidden a) == (Hidden b) = a == b
instance Ord a => Ord (Hidden a) where
compare (Hidden a) (Hidden b) = compare a bKinds
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.
The type Mu we saw last week is even stranger:
newtype Mu f = Fold { unFold :: f (Mu f) }
Here, f has kind * -> *, and Mu f has kind *. So Mu itself has kind (* -> *) -> *. That is, Mu is a higher-kinded type: a type constructor that takes another type constructor.
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
Finally, we looked 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 aNote 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 xsWe 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 usualWe 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 = UnionNote 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 nilEquality for functions on finite domains
Finally, we defined a notion of equality for function on finite domains.
instance (Eq a, Bounded a, Enum a, Eq b) => Eq (a -> b) where
f1 == f2 =
let checkAt x = f1 x == f2 x in
and $ map checkAt $ enumFromTo minBound maxBoundHaskell could do this in its standard library, but doesn’t… why not? Why might this be a bad idea?