Lecture 4 — 2020-01-30
Kinds and type classes
This lecture is written in literate Haskell; you can download the raw source.
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.
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
.
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:
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}}