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
foldl
andfoldr
(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 c
The 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 c
When 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 result
The 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 = cmpTVL
newtype
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; like
data`, 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 -> LT
Reversing 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 b
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.
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 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
Equality 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 maxBound
Haskell could do this in its standard library, but doesn’t… why not? Why might this be a bad idea?