Lecture 12 — 2017-10-05

Do notation; QuickCheck

This lecture is written in literate Haskell; you can download the raw source.

do notation

Haskell’s do notation is a concise way of writing monadic (and therefore applicative or functorial) computations. Compare contrast:

                                 do
action1 >>= \res1 ->   VS      res1 <- action1
action2 >>                     action2
let x = g res1 in              let x = g res 1
action3 >>= \res3 ->           res3 <- action3
action4 (f g res3)             action4 (f g res3)

More concretely, here are two bits of code for swapping array cells, one in bind notation and the other in do notation.

swap arr i1 i2 =
  readArray arr i1 >>= \v1 ->
  readArray arr i2 >>= \v2 ->
  writeArray arr i1 v2 >>
  writeArray arr i2 v1

swap arr i1 i2 = do
  v1 <- readArray arr i1
  v2 <- readArray arr i2
  writeArray arr i1 v2
  writeArray arr i2 v1

QuickCheck

import Test.QuickCheck
import System.Random

We spent the last bit class talking about QuickCheck. First, we saw the Gen monad for generating random values. The sample :: Show a => Gen a -> IO () and sample' :: Gen a -> IO [a] functions were useful here.

evenBelowTen :: Gen Int
evenBelowTen = elements [0,2,4,6,8]

fiveNums :: Gen [Int]
fiveNums = do
  n1 <- evenBelowTen
  n2 <- evenBelowTen
  n3 <- evenBelowTen
  n4 <- evenBelowTen
  n5 <- evenBelowTen
  return [n1,n2,n3,n4,n5]

Next, we wrote some properties. for example, we can check that addition is commutative by taking the following property “for all n1 and n2”:

sums_ok :: Int -> Int -> Bool
sums_ok n1 n2 = n1 + n2 == n2 + n1

and running quickCheck sums_ok.

Next we played with generating binary trees:

data BST a = Empty | Node (BST a) a (BST a) deriving (Eq, Show)

bt :: Arbitrary a => Gen (BST a)
bt = oneof [return Empty,
            Node <$> bt <*> arbitrary <*> bt]

The Arbitrary type class is the piece of magic that lets quickCheck work, by giving it a default generator of random values.

instance Arbitrary a =>  Arbitrary (BST a) where
  arbitrary = bt

We can generate arbitrary binary search trees:

isBST :: Ord a => BST a -> Bool
isBST t = isBST' Nothing Nothing t
 where isBST' lower upper Empty = True
       isBST' lower upper (Node l x r) =
           maybeBounded lower upper x &&
           isBST' lower (Just x) l &&
           isBST' (Just x) upper r
       maybeBounded Nothing Nothing x = True
       maybeBounded Nothing (Just upper) x = x < upper
       maybeBounded (Just lower) Nothing x = lower < x
       maybeBounded (Just lower) (Just upper) x = lower < x && x < upper

boundedBST :: (Arbitrary a, Ord a, Random a, Enum a) => a -> a -> Gen (BST a)
boundedBST lo hi | lo >= hi = return Empty
boundedBST lo hi =
  oneof [return Empty,
         do
           v <- choose (lo,hi)
           l <- boundedBST lo (pred v)
           r <- boundedBST (succ v) hi
           return $ Node l v r]

bsts :: (Arbitrary a, Bounded a, Ord a, Random a, Enum a) => Gen (BST a)
bsts = boundedBST minBound maxBound

We can test it out:

all_ok = forAll (bsts :: Gen (BST Int)) isBST

And then we con go further, testing some real BST functions:

insert x Empty = Node Empty x Empty
insert x (Node l y r) | x < y  = Node (insert x l) y r
                      | x == y = Node l y r
                      | x > y  = Node l y (insert x r)

delete x Empty = Empty
delete x (Node l y r)
  | x < y  = Node (delete x l) y r
  | x > y  = Node l y (delete x r)
  | x == y = case (l,r) of
               (Empty, _) -> r
               (l, Empty) -> l
               _          -> let (min, r') = deleteMin r in
                             Node l min r'
  where deleteMin Empty = error "nope"
        deleteMin (Node Empty x r) = (x,r)
        deleteMin (Node l x r) =
          let (min, l') = deleteMin l in
          (min, Node l' x r)

find _ Empty = Nothing
find v (Node l x r)
  | v < x  = v `find` l
  | v == x = Just x
  | v > x  = v `find` r

Here’s a different way of genering BSTs, as proposed by Evan in class: just use the existing functions!

bsts' :: (Arbitrary a, Ord a) => Gen (BST a)
bsts' = listOf arbitrary >>= return . foldr insert Empty

There are pros and cons to the above. The biggest negative is that if we’re not confident in our insert function, then who knows what we’ll get out. But in general, this is a great way of generating complex data structures.

here are some other BST properties we can check.

insertFind :: Int -> BST Int -> Bool
insertFind x t = (find x $ insert x t) /= Nothing

insertValid :: Int -> Property
insertValid x = forAll bt $ \t -> isBST t ==> isBST $ insert x t

insertValid' :: Int -> Property
insertValid' x = forAll bsts $ \t -> isBST $ insert x t

insertValid'' :: Int -> Property
insertValid'' x = forAll bsts' $ \t -> isBST $ insert x t