Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Using Types to Write Your Code for You

Atomic Object
November 17, 2012

Using Types to Write Your Code for You

In languages with type inference (Haskell, Scala, F#), the type system can be an invaluable tool that not only can prevent silly errors but can actually make algorithm development easier. In fact, if you’re careful with how you construct your types, you can practically make the types write the code for you!

In this talk I’ll show how moving some information about the structures in your code into the type system can convert some of the hard work of algorithm development into something more akin to assembling puzzle pieces, and I’ll give some pointers on how to do that. I’ll show how you can construct your data types so that meaningless, “impossible”, or other error conditions cannot even be represented in your structures and the awesome effects that can have on your code. I’ll walk through a couple examples to illustrate the techniques.

My examples will be in Haskell but the ideas and techniques should apply to any language with Hindley-Milner type inference, especially those with constructs similar to Haskells’ type classes.

Atomic Object

November 17, 2012
Tweet

More Decks by Atomic Object

Other Decks in Technology

Transcript

  1. Using Types to Write Your Code for You 1DevDay Detroit

    November 17, 2012 Job Vranish, Atomic Object
  2. How? • Find properties of your program that you want

    the compiler to check for you. • Encode desired properties of your program into your types. • Shake your types until the algorithms fall out
  3. Example: Simple Vector and Matrix Operations • Here are the

    operations we are going to implement: - Vector addition - Vector (pairwise) multiplication - Vector dot product - Matrix transpose - Matrix multiplication • Let’s run through a simple naive implementation to get a feel for what properties we might want to encode.
  4. Example: Simple Vector and Matrix Operations Here are the types

    for a naive implementation (with lists): addVec :: (Num a) => [a] -> [a] -> [a] mulVec :: (Num a) => [a] -> [a] -> [a] dot :: (Num a) => [a] -> [a] -> a transpose :: [[a]] -> [[a]] mulMat :: (Num a) => [[a]] -> [[a]] -> [[a]] Ok, let’s try implementing these quick... (in these slides I use blue to indicate when code is a Type)
  5. Example: Simple Vector and Matrix Operations Ok, this should be

    easy... addVec :: (Num a) => [a] -> [a] -> [a] addVec [] _ = [] addVec _ [] = [] addVec (x:xs) (y:ys) = (x + y) : addVec xs ys What are the potential error conditions?
  6. Example: Simple Vector and Matrix Operations Still not too hard...

    dot :: (Num a) => [a] -> [a] -> a dot a b = sum (mulVec a b)
  7. Example: Simple Vector and Matrix Operations What about transpose? transpose

    :: [[a]] -> [[a]] transpose xss = ? What are the possible error conditions here? Oh my. I can see all sorts of potential error conditions here. Transpose really only works with rectangular matrices. Also the implementation is a bit more tricky. Especially with linked lists.
  8. Example: Simple Vector and Matrix Operations What about matrix multiplication?:

    mulMat :: (Num a) => [[a]] -> [[a]] -> [[a]] mulMat xss = ? What are the possible error conditions here? Wait. How does matrix multiplication work again?
  9. A quick refresher in matrix multiplication (thanks wikipedia): Wow, this

    is complicated. Lots of potential error conditions. Matrix Multiplication
  10. •Notice the constraints. The back arrows point to rows/columns that

    need to be the same length in order for matrix multiplication to make sense. •Even if you got this working, how would you handle error conditions? Can you even list them? •What is a common theme among our error conditions? Length! •Most of our potential errors happen when the lengths of our lists don’t match. Matrix Multiplication
  11. Encode Properties in Types • What property do we want

    to encode in the type? • Length! • How the heck do we do that? • Lets start by just trying to encode numbers into types • I’ll start with some C++ which should hopefully look a little more familiar
  12. Encode Properties in Types • The dumb way: - You

    could make a separate type for each number class Zero {}; class One {}; class Two {}; ... - And similarly we could make a type for each length of vector. - But please don’t do that.
  13. Encode Properties in Types • Using C++ templates we can

    do better: class Zero {}; template<typename T> class Succ {}; • Surprisingly this is basically all we need 0 is encoded as: Zero 1 is encoded as: Succ<Zero> 2 is encoded as: Succ<Succ<Zero> > ...
  14. Encode Properties in Types • Now you can ‘tag’ other

    types with your number types. So if you had a type like this: template<typename Length> class FixedIntArray { ... stuff ... }; • Then you can do something like this: FixedIntArray<Succ<Zero> > array_of_two; FixedIntArray<Succ<Succ<Zero> > > array_of_three; ... • In C++ there are much better ways to do this. Use for pedagogy only :)
  15. Example: Simple Vector and Matrix Operations • Is a list

    a good data type for a vector? • It’s not too bad. • For a matrix? • Not really. • Lets try making a data structure that naturally encodes its length in the type.
  16. Example: Simple Vector and Matrix Operations Lets make a better

    one. Here’s what the built-in list type looks like: data [a] = a : [a] | [] Here’s what it looks like without the sugar: data List a = Cons a (List a) | Nil We’re going to pull more of that type up into the type system.
  17. Example: Simple Vector and Matrix Operations Maybe we want something

    like this? data List len a = Cons a (List (len - 1) a) | Nil <-- This doesn’t work There are some awesome new Haskell features that can work almost exactly like this, but we’re going to do it the old-fashioned way.
  18. Example: Simple Vector and Matrix Operations How about this?: data

    Cons f a = Cons a (f a) And for the empty list: data Nil a = Nil . Notice this looks like the regular list, except it’s in the type system! ‘f’ is the type of the rest of the list. The “tail” Nil doesn’t need an ‘f’. There’s no more list
  19. Example: Simple Vector and Matrix Operations Now length is naturally

    encoded in the type: [] :: [a] Nil :: Nil a [3] :: [Integer] Cons 3 Nil :: Cons Nil Integer [3, 6, 2] :: [Integer] Cons 3 (Cons 6 (Cons 2 Nil)) :: Cons (Cons (Cons Nil)) Integer It looks like a regular list, except it’s a type!
  20. Example: Simple Vector and Matrix Operations But wait! We don’t

    want to define new functions for every size of list. How do we get around that? With type classes! class FixedList f instance FixedList f => FixedList (Cons f) instance FixedList Nil Note: These are not like OO classes, more like interfaces
  21. Example: Simple Vector and Matrix Operations What do the types

    of operations look like now? addVec :: (Num a) => [a] -> [a] -> [a] dot :: (Num a) => [a] -> [a] -> a transpose :: [[a]] -> [[a]] mulMat :: (Num a) => [[a]] -> [[a]] -> [[a]] After: addVec :: (Num a, FixedList f) => f a -> f a -> f a dot :: (Num a, FixedList f) => f a -> f a -> a transpose :: (Num a, FixedList f, FixedList g) => ?
  22. Example: Simple Vector and Matrix Operations What do the types

    of operations look like now? addVec :: (Num a) => [a] -> [a] -> [a] dot :: (Num a) => [a] -> [a] -> a transpose :: [[a]] -> [[a]] mulMat :: (Num a) => [[a]] -> [[a]] -> [[a]] After: addVec :: (Num a, FixedList f) => f a -> f a -> f a dot :: (Num a, FixedList f) => f a -> f a -> a transpose :: (Num a, FixedList f, FixedList g) => f (g a) -> g (f a) In this case a matrix is a fixedlist of fixedlists.
  23. Example: Simple Vector and Matrix Operations What do the types

    of operations look like now? Before: mulMat :: (Num a) => [[a]] -> [[a]] -> [[a]] After: mulMat :: (?) => ? What typeclass constraints do we want? How many different vector types are we going to need?
  24. Example: Simple Vector and Matrix Operations What do the types

    of operations look like now? Before: mulMat :: (Num a) => [[a]] -> [[a]] -> [[a]] After: mulMat :: (Num a, FixedList f, FixedList g , FixedList h) => ?
  25. Example: Simple Vector and Matrix Operations What do the types

    of operations look like now? Before: mulMat :: (Num a) => [[a]] -> [[a]] -> [[a]] After: mulMat :: (Num a, FixedList f, FixedList g , FixedList h) => f (g a) -> ? What are the second and third parameters?
  26. Example: Simple Vector and Matrix Operations What do the types

    of operations look like now? Before: mulMat :: (Num a) => [[a]] -> [[a]] -> [[a]] After: mulMat :: (Num a, FixedList f, FixedList g , FixedList h) => f (g a) -> g (h a) -> f (h a) Ooh this is nice! We’ve constrained the type to exactly what we want. There are very few ways this can fail.
  27. Whole classes of errors now can’t exist. • The error

    conditions are gone! • How many implementations could satisfy the original types? • What about these new types? • It’s now much harder to make a sensible algorithm that passes typecheck, but still doesn’t work
  28. Shake your types until the algorithms fall out! • There

    are a couple remarkable things about these types: 1. These types are very restrictive. 2. These types are very expressive. • The types both forbid many bad implementations and give us information about what an implementation will need. • If our types are chosen well they can point us right at the algorithm we want.
  29. An (informal) algorithm to find algorithms • Given a type

    that we want a value for: say (a -> F b -> G b) • First, see if we already have something that can satisfy this type directly (the easy case) Hoogle is helpful for this. • If that doesn’t work, look for something that has a matching return type, or try searching for a slightly more general type. Say we find : (Q c -> G c) • Now we can use that G c (which unifies with G b) if we can just find something that can give us a Q b! • However we can use the a and F b ! • Recurse! but include a value of a and F b in list of possibilities that we can choose from.
  30. Example: Simple Vector and Matrix Operations Ok, now lets try

    implementing this stuff again. Our goal: addVec :: (Num a, FixedList f) => f a -> f a -> f a Let us summon the power of Hoogle! (haskell.org/hoogle)
  31. Example: Simple Vector and Matrix Operations Our goal: addVec ::

    (Num a, FixedList f) => f a -> f a -> f a Lets start by punching (Num a) => f a -> f a -> f a into Hoogle and see what comes up.
  32. Example: Simple Vector and Matrix Operations Our goal: addVec ::

    (Num a, FixedList f) => f a -> f a -> f a mplus :: MonadPlus m => m a -> m a -> m a <|> :: Alternative f => f a -> f a -> f a The problem with these types is that they are fully polymorphic in a. mplus and <|> cannot know anything about a at all. addVec Needs at least some information about a.
  33. Example: Simple Vector and Matrix Operations Our goal: addVec ::

    (Num a, FixedList f) => f a -> f a -> f a Lets try relaxing our search a little. Hoogle: f a -> f b -> f c If a = b = c this is the same as: f a -> f a -> f a so it might be close enough
  34. Example: Simple Vector and Matrix Operations Our goal: addVec ::

    (Num a, FixedList f) => f a -> f a -> f a liftA2 :: (Applicative f) => (a -> b -> c) -> f a -> f b -> f c This might work! liftA2 Returns the type we want if we just pass it something of type: a -> a -> a And we need to carry that (Num a) forward so our new goal is: (Num a) => a -> a -> a Hoogle?
  35. Example: Simple Vector and Matrix Operations So liftA2 (+) has

    the type we want: addVec :: (Num a, FixedList f) => f a -> f a -> f a liftA2 (+) :: (Num a, Applicative f) => f a -> f a -> f a But what’s this Applicative typeclass? Is that something we want? What does liftA2 even do? Hoogle can link you right to the documentation, and the sources! ... But they are a little brain melty. Let just try it.
  36. Example: Simple Vector and Matrix Operations Our goal: Applicative instances

    for our types data Cons f a = Cons a (f a) data Nil a = Nil Let’s start with pure: instance Applicative Nil where pure a :: a -> Nil a pure a = ? instance (Applicative f) => Applicative (Cons f) where pure a :: (Applicative f) => a -> Cons f a pure a = ?
  37. Example: Simple Vector and Matrix Operations Our goal: Applicative instances

    for our types data Cons f a = Cons a (f a) data Nil a = Nil Lets start with pure: instance Applicative Nil where pure a :: a -> Nil a pure a = Nil instance (Applicative f) => Applicative (Cons f) where pure a :: (Applicative f) => a -> Cons f a pure a = Cons a (pure a) That wasn’t too bad...
  38. Example: Simple Vector and Matrix Operations Our goal: Applicative instances

    for our types data Cons f a = Cons a (f a) data Nil a = Nil Ok, now let’s try <*>: instance Applicative Nil where (<*>) :: Nil (a -> b) -> Nil a -> Nil b f <*> a = ? instance (Applicative f) => Applicative (Cons f) where (<*>) :: Cons f (a -> b) -> Cons f a -> Cons f b Cons f fs <*> Cons a as = ?
  39. Example: Simple Vector and Matrix Operations Our goal: Applicative instances

    for our types data Cons f a = Cons a (f a) data Nil a = Nil Ok, now let’s try <*>: instance Applicative Nil where (<*>) :: Nil (a -> b) -> Nil a -> Nil b f <*> a = Nil instance (Applicative f) => Applicative (Cons f) where (<*>) :: Cons f (a -> b) -> Cons f a -> Cons f b Cons f fs <*> Cons a as = Cons (f a) (fs <*> as) That wasn’t too bad either. Does it do what we want?
  40. Example: Simple Vector and Matrix Operations *Main> let addVec a

    b = liftA2 (+) a b *Main> let a = Cons 1 (Cons 4 (Cons 11 Nil)) *Main> addVec a a Cons 2 (Cons 8 (Cons 22 Nil)) *Main> ... Holy shit!
  41. Example: Simple Vector and Matrix Operations What about dot? dot

    :: (Num a, FixedList f) => f a -> f a -> a dot a b = ? Wouldn’t it be nice if we could do: dot a b = sum (liftA2 (*) a b) Oh, but we can! Hoogle: “sum”!
  42. Example: Simple Vector and Matrix Operations Wow. This is exactly

    what we want: sum :: (Foldable t, Num a) => t a -> a Except, what is Foldable? Foldable is actually easier to implement than Applicative. In fact! Foldable (along with Functor and Traversable) can be derived by Haskell automatically! Let’s add those now.
  43. Example: Simple Vector and Matrix Operations data Cons f a

    = Cons a (f a) deriving (Show, Eq, Ord, Functor, Foldable, Traversable) data Nil a = Nil deriving (Show, Eq, Ord, Functor, Foldable, Traversable) Does it work?
  44. Example: Simple Vector and Matrix Operations *Main> let a =

    Cons 1 (Cons 4 (Cons 11 Nil)) *Main> let dot a b = sum (liftA2 (*) a b) *Main> dot a a 138 ... OMG!
  45. Example: Simple Vector and Matrix Operations Ok, so what about

    a harder one? transpose :: (Num a, FixedList f, FixedList g) => f (g a) -> g (f a) Guess what we do next? Thats right! Hoogle: f (g a) -> g (f a)
  46. Example: Simple Vector and Matrix Operations sequenceA matches the type

    we want. It’s fully polymorphic in a but that’s expected in this case. You don’t need to know anything about the elements in a matrix in order to transpose it. And it looks like we already have all our instances. Let’s see what it does...
  47. Example: Simple Vector and Matrix Operations *Main> let a =

    Cons 2 (Cons 9 Nil) *Main> let b = Cons 6 (Cons 31 Nil) *Main> let m = Cons a (Cons b Nil) *Main> m Cons (Cons 2 (Cons 9 Nil)) (Cons (Cons 6 (Cons 31 Nil)) Nil) *Main> sequenceA m Cons (Cons 2 (Cons 6 Nil)) (Cons (Cons 9 (Cons 31 Nil)) Nil) ... Holy Shit! How the @$*&% did that work? Magic :D
  48. Example: Simple Vector and Matrix Operations Matrix multiplication can’t possibly

    be as easy: mulMat :: (Num a, FixedList f, FixedList g , FixedList h) => f (g a) -> g (h a) -> f (h a) Sadly Hoogling: f (g a) -> g (h a) -> f (h a) Doesn’t yield anything useful. What about just: f (h a) ?... still nothing. Hmmm how about (Applicative f) => f (h a) ? Remember if we can’t find the type you want directly, look for the return type instead.
  49. Example: Simple Vector and Matrix Operations Hey this traverse function

    returns what we want. Can we come up with the parameters it needs? traverse :: (Traversable t, Applicative f) => (c -> f a) -> h c -> f (h a) We can make a f (g a) -> g (h a) -> f (h a) from traverse, if we can come up with a c -> f a and a h c from a f (g a) and a g (h a) Can we do that? You could think of it as: f (g a) -> g (h a) -> (c -> f a, h c)
  50. Example: Simple Vector and Matrix Operations Goal: (c -> f

    a) h c Given: x :: f (g a) y :: g (h a) Does anything unify? If we could just swap the order in our second given...
  51. Example: Simple Vector and Matrix Operations SequenceA! (unified) Goals are

    now: (g a -> f a) (sequenceA y) :: h (g a) satisfied Given: x :: f (g a) y :: g (h a) Does anything unify? Hmm we can break open that function...
  52. Example: Simple Vector and Matrix Operations Goals are now: f

    a Given: x :: f (g a) y :: g (h a) z :: g a Does anything unify? We need an f and x is the only place we can get it. z is probably important too. Is there something that could convert that g a to an a ?
  53. Example: Simple Vector and Matrix Operations Maybe dot? dot ::

    (Num a, FixedList g) => g a -> g a -> a That’d really be more convenient as: ? :: f (g a) -> f (g a) -> f a Wait, doesn’t liftA2 do that? *Main> :t liftA2 liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c *Main> :t liftA2 dot liftA2 dot :: (Num c, Applicative t, Applicative f, Foldable t) => f (t c) -> f (t c) -> f c
  54. Example: Simple Vector and Matrix Operations Goals are now: liftA2

    dot x p :: f a satisfied p :: f (g a) Given: x :: f (g a) y :: g (h a) z :: g a Looks like we need to get an f (g a) from a g a Hmmm Hoogle gives nothing for g a -> f (g a) What about d -> f d ? (the same thing, just more abstract)
  55. Example: Simple Vector and Matrix Operations Pure! pure :: (Applicative

    f) => a -> f a If we substitute everything back in, we get: mulMat :: (Num a, FixedList f, FixedList g , FixedList h) => f (g a) -> g (h a) -> f (h a) mulMat a b = traverse (liftA2 dot a . pure) (sequenceA b) Which has the type we want. But does it work?
  56. Example: Simple Vector and Matrix Operations *Main> let mulMat a

    b = traverse (liftA2 dot a . pure) (sequenceA b) *Main> let v1 = Cons 2 $ Cons 1 $ Cons 7 $ Nil *Main> let v2 = Cons 1 $ Cons 4 $ Cons 6 $ Nil *Main> let v3 = Cons 4 $ Cons 9 $ Nil *Main> let v4 = Cons 8 $ Cons 1 $ Nil *Main> let v5 = Cons 5 $ Cons 3 $ Nil *Main> let m1 = Cons v1 $ Cons v2 $ Nil *Main> let m2 = Cons v3 $ Cons v4 $ Cons v5 $ Nil *Main> mulMat m1 m2 Cons (Cons 51 (Cons 40 Nil)) (Cons (Cons 66 (Cons 31 Nil)) Nil) *Main> Q.E.D.
  57. Example: Simple Vector and Matrix Operations One last thing. If

    you strip off the type of mulMat and have Haskell infer it for you, it gives you this: mulMat :: (Num a, Applicative f, Applicative g, Applicative h, Traversable h, Traversable g) => f (g a) -> g (h a) -> f (h a) It turns out our operations will work for any datatype that has a suitable instance of Applicative.
  58. Types have advantages • Can encode important properties in types

    • Wrong code is harder to write • Correct code is easier to write • More time looking for symmetry (this is worth it) • Less time looking for algorithms • Hoogle is awesome