0% found this document useful (0 votes)
17 views60 pages

Fun With Type Funs

The document discusses type classes and instances in Haskell, focusing on the implementation of numeric operations and data structures that depend on key types. It introduces concepts such as associated types, memoization, and the representation of collections, emphasizing the flexibility of type signatures and constraints. Additionally, it covers the design of processes and their dual protocols in a type-safe manner.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
17 views60 pages

Fun With Type Funs

The document discusses type classes and instances in Haskell, focusing on the implementation of numeric operations and data structures that depend on key types. It introduces concepts such as associated types, memoization, and the representation of collections, emphasizing the flexibility of type signatures and constraints. Additionally, it covers the design of processes and their dual protocols in a type-safe manner.
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 60

Simon Peyton Jones (Microsoft Research)

Chung-Chieh Shan (Rutgers University)


Oleg Kiselyov (Fleet Numerical Meteorology and
Oceanography Center)
Class decl gives type
signature of each
method

class Num a where


(+), (*) :: a -> a -> a
negate :: a -> a

Instance decl gives a square :: Num a => a -> a


“witness” for each
square x = x*x
method, matching the
signature
instance Num Int where
(+) = plusInt
(*) = mulInt
negate = negInt
plusInt :: Int -> Int -> Int
mulInt :: Int -> Int -> Int
negInt :: Int -> Int test = square 4 + 5 :: Int
plusInt :: Int -> Int -> Int
plusFloat :: Float -> Float -> Float
intToFloat :: Int -> Float

class GNum a b where


(+) :: a -> b -> ???

instance GNum Int Int where


(+) x y = plusInt x y

instance GNum Int Float where


(+) x y = plusFloat (intToFloat x) y

test1 = (4::Int) + (5::Int)


test2 = (4::Int) + (5::Float)

Allowing more good


programs
class GNum a b where
(+) :: a -> b -> ???

 Result type of (+) is a function of the


argument types SumTy is an
associated type of
class GNum a b where class GNum
type SumTy a b :: *
(+) :: a -> b -> SumTy a b

 Each method gets a type signature


 Each associated type gets a kind signature
class GNum a b where
type SumTy a b :: *
(+) :: a -> b -> SumTy a b
 Each instance declaration gives a “witness”
for SumTy, matching the kind signature
instance GNum Int Int where
type SumTy Int Int = Int
(+) x y = plusInt x y

instance GNum Int Float where


type SumTy Int Float = Float
(+) x y = plusFloat (intToFloat x) y
class GNum a b where
type SumTy a b :: *
instance GNum Int Int where
type SumTy Int Int = Int :: *
instance GNum Int Float where
type SumTy Int Float = Float

 SumTy is a type-level function


 The type checker simply rewrites
 SumTy Int Int --> Int
 SumTy Int Float --> Float
whenever it can

 But (SumTy t1 t2) is still a perfectly good type,


even if it can’t be rewritten. For example:
data T a b = MkT a b (SumTy a b)
 Simply omit instances for incompatible types
newtype Dollars = MkD Int

instance GNum Dollars Dollars where


type SumTy Dollars Dollars = Dollars
(+) (MkD d1) (MkD d2) = MkD (d1+d2)

-- No instance GNum Dollars Int

test = (MkD 3) + (4::Int) -- REJECTED!


 Consider a finite map, mapping keys to values
 Goal: the data representation of the map
depends on the type of the key
 Boolean key: store two values (for F,T resp)
 Int key: use a balanced tree
 Pair key (x,y): map x to a finite map from y to
value; ie use a trie!

 Cannot do this in Haskell...a good program


that the type checker rejects
data Maybe a = Nothing | Just a

Map is indexed by k,
class Key k where but parametric in its
data Map k :: * -> * second argument
empty :: Map k v
lookup :: k -> Map k v -> Maybe v
...insert, union, etc....
data Maybe a = Nothing | Just a

Optional value
class Key k where
for False
data Map k :: * -> *
empty :: Map k v
lookup :: k -> Map k v -> Maybe v
...insert, union, etc.... Optional value
for True
instance Key Bool where
data Map Bool v = MB (Maybe v) (Maybe v)
empty = MB Nothing Nothing
lookup True (MB _ mt) = mt
lookup False (MB mf _) = mf
data Maybe a = Nothing | Just a

class Key k where


Two-level
data Map k :: * -> *
map
empty :: Map k v
lookup :: k -> Map k v -> Maybe v Two-level
...insert, union, etc.... lookup

instance (Key a, Key b) => Key (a,b) where


data Map (a,b) v = MP (Map a (Map b v))
empty = MP empty
lookup (ka,kb) (MP m) = case lookup ka m of
Nothing -> Nothing
Just m2 -> lookup kb m2
 Goal: the data representation of the map
depends on the type of the key
 Boolean key: SUM
data Map Bool v = MB (Maybe v) (Maybe v)
 Pair key (x,y): PRODUCT
data Map (a,b) v = MP (Map a (Map b v))

 What about List key [x]:


SUM of PRODUCT + RECURSION?
instance (Key a) => Key [a] where
data Map [a] v = ML (Maybe v) (Map (a,[a]) v)
empty = ML Nothing empty
lookup [] (ML m0 _) = m0
lookup (h:t) (ML _ m1) = lookup (h,t) m1

 Note the cool recursion: these Maps are


potentially infinite!
 Can use this to build a trie for (say) Int
toBits :: Int -> [Bit]
 Easy to accommodate types with non-generic
maps: just make a type-specific instance
instance Key Int where
data Map Int elt = IM (Data.IntMap.Map elt)
empty = IM Data.IntMap.empty
lookup k (IM m) = Data.IntMap.lookup m k

module Data.IntMap where


data Map elt = …
empty :: Map elt
lookup :: Map elt -> Int -> Maybe elt
…etc…
 One way: when you evaluate (f x) to give val,
add x -> val to f’s memo table, by side effect.
 A nicer way: build a (lazy) table for all possible
values of x

class Memo k where


data Table k :: * -> *
toTable :: (k->r) -> Table k r
fromTable :: Table k r -> (k->r)

memo :: Memo k => (k->r) -> k -> r


memo f = fromTable (toTable f)
class Memo k where
data Table k :: * -> *
toTable :: (k->r) -> Table k r
fromTable :: Table k r -> (k->r)

instance Memo Bool where


data Table Bool w = TBool w w
toTable f = TBool (f True) (f False)
fromTable (TBool x y) b = if b then x else y

 Table contains (lazily) pre-calculated results


for both True and False
instance (Memo a) => Memo [a] where
data Table [a] w
= TList w (Table a (Table [a] w))

Value for (f []) Values for (f (x:xs))


instance (Memo a) => Memo [a] where
data Table [a] w = TList w (Table a (Table [a] w))
toTable f = TList (f [])
(toTable (\x ->
toTable (\xs -> f (x:xs))))
fromTable (TList t _) [] = t
fromTable (TList _ t) (x:xs) = fromTable
(fromTable t x) xs

 As with Map, the memo table is infinite (second use


of laziness)
class Memo k where
data Table k :: * -> *
toTable :: (k->r) -> Table k r
fromTable :: Table k r -> (k->r)
instance Memo Int where
data Table Int w = TInt (Table [Bool] w)

toTable f = TInt (toTable (\bs ->


f (bitsToInt bs)))

fromTable (TInt t) n = fromTable t (intToBits n)

class Memo k where


data Table k :: * -> *
toTable :: (k->r) -> Table k r
fromTable :: Table k r -> (k->r)
fib :: Int -> Int
fib = fromTable (toTable fib')
where
fib' :: Int -> Int
fib' 0 = 1
fib' 1 = 1
fib' n = fib (n-1) + fib (n-2)

 Recursive calls are to the memo’d function


[:Double:] Arrays of pointers to boxed
numbers are Much Too Slow
[:(a,b):] Arrays of pointers to pairs are
Much Too Slow

...
Idea!
Representation of an
array depends on the
element type
class Elem a where
data [:a:]
index :: [:a:] -> Int -> a

instance Elem Double where


data [:Double:] = AD ByteArray
index (AD ba) i = ...

instance (Elem a, Elem b) => Elem (a,b) where


data [:(a,b):] = AP [:a:] [:b:]
index (AP a b) i = (index a i, index b i)

AP
We do not want this for [: [:Float:] :]
• Concatenate sub-arrays into one big, flat array
• Operate in parallel on the big array
• Segment vector keeps track of where the sub-arrays
are

...etc

• Lots of tricksy book-keeping!


• Possible to do by hand (and done in
practice), but very hard to get right
• Blelloch showed it could be done
systematically
Flat data

instance Elem a => Elem [:a:] where


data [:[:a:]:] = AN [:Int:] [:a:]

concatP :: [:[:a:]:] -> [:a:] Shape


concatP (AN shape data) = data

segmentP :: [:[:a:]:] -> [:b:] -> [:[:b:]:]


segmentP (AN shape _) data = AN shape data

concatP, segmentP are constant time


And are important in practice
class Collection c where
insert :: a -> c a -> c a
Does not
work!
instance Collection [] where
insert x [] = [x] We need
insert x (y:ys) Eq!
| x==y = y : ys
| otherwise = y : insert x ys
class Collection c where
insert :: Eq a => a -> c a -> c a
This
instance Collection [] where works
insert x [] = [x]
insert x (y:ys)
| x==y = y : ys
| otherwise = y : insert x ys

instance Collection BalancedTree where


insert = …needs (>)…

BUT THIS
DOESN’T
 We want the constraint to vary with the
collection c! An associated
type of the
class Collection c where class
type X c a :: Constraint
insert :: X c a => a -> c a -> c a

instance Collection [] where


type X [] a = Eq a For lists, use
insert x [] = [x] Eq
insert x (y:ys)
| x==y = y : ys
| otherwise = y : insert x ys
 We want the constraint to vary with the
collection c!

class Collection c where


type X c a :: Constraint
insert :: X c a => a -> c a -> c a

instance Collection BalancedTree where


type X BalancedTree a = (Ord a, Hashable a)
insert = …(>)…hash…

For balanced
trees use
(Ord,Hash)
 Lovely because, it is simply a combination of
 Associated types (existing feature)
 Having Constraint as a kind

 No changes at all to the intermediate


language!
 ::= * |  -> 
| k.  | k
| Constraint
Client Server

 addServer :: In Int (In Int (Out Int End))


addClient :: Out Int (Out Int (In Int End))
 Type of the process expresses its protocol
 Client and server should have dual protocols:
run addServer addClient -- OK!
run addServer addServer -- BAD!
Client Server

 addServer :: In Int (In Int (Out Int End))


addClient :: Out Int (Out Int (In Int End))

data In v p = In (v -> p)
data Out v p = Out v p
data End = End

NB punning
data In v p = In (v -> p)
data Out v p = Out v p
data End = End

addServer :: In Int (In Int (Out Int End))


addServer = In (\x -> In (\y ->
Out (x + y) End))

 Nothing fancy here


 addClient is similar
run :: ??? -> ??? -> End

A process A co-process

class Process p where


type Co p
run :: p -> Co p -> End

 Same deal as before: Co is a type-level


function that transforms a process type into
its dual
class Process p where data In v p = In (v -> p)
type Co p data Out v p = Out v p
run :: p -> Co p -> End data End = End

instance Process p => Process (In v p) where


type Co (In v p) = Out v (Co p)
run (In vp) (Out v p) = run (vp v) p

instance Process p => Process (Out v p) where


type Co (Out v p) = In v (Co p)
run (Out v p) (In vp) = run p (vp v)

Just the obvious thing really


 C: sprintf( “Hello%s.”, name )
 Format descriptor is a string; absolutely no
guarantee the number or types of the other
parameters match the string.
 Haskell: (sprintf “Hello%s.” name)??
 No way to make the type of (sprintf f) depend on
the value of f
 But we can make the type of (sprintf f) depend on
the type of f!
sprintf :: F f -> SPrintf f
sprintf (Lit "Day") :: String
-- Like printf("Day")

sprintf (Lit "Day " `Cmp` int) :: Int -> String


-- Like printf("Day %n")

sprintf (Lit "Day " `Cmp` int `Cmp` Lit "Monnth" `Cmp` string)
:: Int -> String -> String
-- Like printf("Day %n Month %s")
data F f where
Lit :: String -> F L
Val :: Parser val -> Printer val -> F (V val)
Cmp :: F f1 -> F f2 -> F (f1 `C` f2)

data L
data V a
data C a b

type Parser a = String -> [(a,String)]


type Printer a = a -> String

int :: F (Val Int)


int = Val (..parser for Int..) (..printer for Int..)
data F f where
Lit :: String -> F L
Val :: Parser val -> Printer val -> F (V val)
Cmp :: F f1 -> F f2 -> F (f1 `C` f2)

int :: F (Val Int)


int = Val (…parser for Int..) (..printer for Int)

f_ld = Lit "day" :: F L


f_lds = Lit "day" `Cmp` Lit "s" :: F (L `C` L)
f_dn = Lit "day " `Cmp` int :: F (L `C` V Int)
f_nds = int `Cmp` Lit " day" `Cmp` Lit "s" :: F (V Int `C` L `C` L)
data F :: Fmt -> * where
Lit :: String -> F L
Val :: Parser val -> Printer val -> F (V val)
Cmp :: F f1 -> F f2 -> F (C f1 f2)
But can’t (quite)
data Fmt = L | V * | C Fmt Fmt write this yet

type Parser a = String -> [(a,String)]


type Printer a = a -> String

F L -- Well kinded
F (L `C` L) -- Well kinded
F Int -- Ill kinded
F (Int `C` L) -- Ill kinded
 Now we can write the type of sprintf:

sprintf :: F f -> SPrintf f

The type-level counterpart


to sprintf

SPrintf L = String
SPrintf (L `C` L) = String
SPrintf (L `C` V Int) = Int -> String
SPrintf (V Int `C` L `C` L) = Int -> String
SPrintf (V Int `C` L `C` V Int) = Int -> Int -> String

No type classes here: we are just doing


type-level computation
“Type family”
declares a
type function
type SPrintf f = TPrinter f String without
involving a
type family TPrinter f x type class
type instance TPrinter L x = x
type instance TPrinter (V val) x = val -> x
type instance TPrinter (C f1 f2) x
= TPrinter f1 (TPrinter f2 x)

 The `C` constructor suggests a (type-level)


accumulating parameter
sprintf :: F f -> SPrintf f
sprintf (f1 `Cmp` f2) = ???

-- sprintf f1 :: Int -> Bool -> String (say)


-- sprintf f2 :: Int -> String
-- These don’t compose!
 Use an accumulating parameter (a
continuation), just as we did at the type level

sprintf :: F f -> SPrintf f


sprintf f = print f (\s -> s)

print :: F f -> (String -> a) -> TPrinter f a


print (Lit s) k = k s
print (Val _ show) k = \v -> k (show v)
print (f1 `Cmp` f2) k = print f1 (\s1 ->
print f2 (\s2 ->
k (s1++s2)))
sscanf :: F f -> SScanf f

Result type
Same format computed by a
descriptor different type
function (of
course)
class Coll c where
type Elem c
insert :: c -> Elem c -> c

instance Coll BitSet where


type Elem BitSet = Char
insert = ...

instance Coll [a] where


type Elem [a] = a
insert = ...

 What is the type of union?


union :: Coll c => c -> c -> c
 But we could sensibly union any two collections
whose elements were the same type
eg c1 :: BitSet, c2 :: [Char]
 But we could sensibly union any two
collections whose elements were the same
type
eg c1 :: BitSet, c2 :: [Char]
 Elem is not injective

BitSet [Char]
Elem

Char
An equality predicate

union :: (Coll c1, Coll c2, Elem c1 ~ Elem c2)


=> c1 -> c2 -> c2
union c1 c2 = foldl insert c2 (elems c1)

insert :: Coll c => c -> Elem c -> c


elems :: Coll c => c -> [Elem c]
 Machine address computation
add :: Pointer n -> Offset m -> Pointer (GCD n m)
 Tracking state using Hoare triples
acquire :: (Get n p ~ Unlocked)
=> Lock n -> M p (Set n p Locked) ()

Lock-state before Lock-state after

 Type level computation tracks some abstraction of value-


level computation; type checker assures that they “line
up”.
 Need strings, lists, sets, bags at type level
 Type families let you do type-level
computation
 Data families allow the data representation
to vary, depending on the type index
 They fit together very naturally with type
classes. How else could you write
f :: F a -> Int
f x = ??? -- Don’t know what F a is!
 Wildly popular in practice
 Types have made a huge contribution
Theorem provers
to this ideal
Powerful, but
• Substantial manual  More sophisticated type systems
assistance required threaten both Happy Properties:
• PhD absolutely essential
(100s of daily users) 1. Automation is harder
2. The types are more complicated
(MSc required)
 Some complications (2) are exactly
Today’s due to ad-hoc restrictions to ensure
experiment full automation
 At some point it may be best to say
Type systems
“enough fooling around: just use Coq”.
Weak, but
• Automatically checked But we aren’t there yet
• No PhD required  Haskell is a great place to play this
(1000,000s of daily users) game
data F f where
Lit :: String -> F L
Val :: Parser val -> Printer val -> F (V val)
Cmp :: F f1 -> F f2 -> F (C f1 f2)

sprintf f = print f (\s -> s)

print :: F f -> (String -> a) -> TPrinter f a


print (Lit s) k = k s
...

In this RHS we know that f~L


data F f where
Lit :: String -> F L
Val :: Parser val -> Printer val -> F (V val)
Cmp :: F f1 -> F f2 -> F (C f1 f2)

sprintf f = print f (\s -> s)

print :: F f -> (String -> a) -> TPrinter f a


print (Lit s) k = k s
...

In this RHS we know that f~L

data F f where
Lit :: (f ~ L) => String -> F f
Val :: (f ~ V val) => … -> F f
Cmp :: (f ~ C f1 f2) => F f1 -> F f2 -> F f
class C a b | a->b, b->a where...

If I have evidence for (C a b), then I


have evidence that F1 a ~ b,
and F2 b ~ a

class (F1 a ~ b, F2 b ~ a)
=> C a b where
type F1 a
type F2 b
...

You might also like