Haskell Intro to the Typeclassopedia

Handré Stolp

January 13, 2014

Introduction

The module declaration and imports

> {-# LANGUAGE FlexibleContexts #-}
> module Slides where
> 
> import Control.Applicative
> import Control.Monad
> import Data.Monoid
> import Data.Maybe
> import Data.Char
> import Debug.Trace
> import System.Random

Some Hints to follow the code

($) :: (a -> b) -> a -> b
f $ x =  f x

(.)    :: (b -> c) -> (a -> b) -> a -> c
(.) f g = \x -> f (g x)

Typeclassopdedia Diagram

Functor

Applicative

Monoid

class Monoid a where
  mempty  :: a
  mappend :: a -> a -> a
 
  mconcat :: [a] -> a
  mconcat = foldr mappend mempty
mempty `mappend` x = x
x `mappend` mempty = x
(x `mappend` y) `mappend` z = x `mappend` (y `mappend` z)

Coord Type

> newtype Coord a = Coord {unCoord :: (a,a)} deriving (Eq, Ord)
> 
> instance Show a => Show (Coord a) where show = show . unCoord

Coord Functor

> instance Functor Coord  where
>     fmap f (Coord (x,y)) = Coord (f x, f y)

Coord Applicative

> instance Applicative Coord where
>     pure a = Coord (a,a)
>     Coord (g, h) <*> Coord (x, y) = Coord (g x, h y)

Coord Monoid

> instance Monoid a => Monoid (Coord a) where
>    mempty = Coord (mempty, mempty)
>    Coord (lx, ly) `mappend` Coord (rx, ry) = Coord (lx <> rx, ly <> ry)

Coord Operators

> (|+|) :: Num a => Coord a -> Coord a -> Coord a
> infixl 6 |+|
> a |+| b = (+) <$> a <*> b
> 
> (|-|) :: Num a => Coord a -> Coord a -> Coord a
> infixl 6 |-|
> a |-| b = (-) <$> a <*> b

Extents Type

> newtype Extents a = Extents {coordFromExtents :: Coord a} deriving (Eq, Ord)
> 
> extentsFromCoord :: Num a => Coord a -> Extents a
> extentsFromCoord c = Extents . fmap abs $ c
> 
> instance Show a => Show (Extents a) where show = show . coordFromExtents

Bounds Type

> data Bounds a = Bounds { boundsCentre :: Coord a
>                        , boundsExtent :: Extents a
>                        }  deriving (Show, Eq, Ord)
> 
> class Divisor a where divideBy' :: a -> a -> a
> instance Divisor Double where divideBy' = (/)
> instance Divisor Float where divideBy' = (/)
> instance Divisor Int where divideBy' = div
> instance Divisor Integer where divideBy' = div
> instance (Divisor a, Num a, Eq a) => Monoid (Bounds a) where
>     -- A zero extents bounds is considered empty
>     mempty = Bounds (Coord (0,0)) (extentsFromCoord . Coord $ (0,0))
>     -- Appending empty to anything does not change it
>     Bounds _ (Extents (Coord (0,0))) `mappend` r = r
>     l `mappend` Bounds _ (Extents (Coord (0,0))) = l
>     -- Appending two non empties
>     l `mappend` r = Bounds c $ extentsFromCoord e
>         where
>             -- centre is the average of the two centres
>             c = (`divideBy'`2) <$> boundsCentre l |+| boundsCentre r
>             -- extents is the sum of the two extents
>             e = (coordFromExtents . boundsExtent $ l) |+| (coordFromExtents . boundsExtent $ r)

Convenience Integer Typedefs

> type CoordI = Coord Int
> type ExtentsI = Extents Int
> type BoundsI = Bounds Int
> 
> coordI :: Int -> Int -> Coord Int
> coordI x y = Coord (x,y)

Fill Type

> data Fill c a = Fill  { queryFill  :: Coord c -> a
>                       , fillBounds :: Bounds c
>                       , moveFill   :: Coord c -> Fill c a
>                       }

Fill Functor and Monoid

> instance Functor (Fill c) where
>     fmap g Fill  { queryFill = q
>                  , fillBounds = b
>                  , moveFill = m
>                  } = Fill (fmap g q)            -- map g over q to get new query
>                           b 
>                           ((fmap . fmap) g m)   -- lift g twice before applying to m to the new move function
> instance (Monoid a, Monoid (Bounds c)) => Monoid (Fill c a) where
>     mempty = Fill (const mempty) mempty (const mempty)
>     a `mappend` b = Fill (queryFill a <> queryFill b)       -- concat the result of the query
>                          (fillBounds a <> fillBounds b)     -- sum the bounds
>                          (moveFill a <> moveFill b)         -- concat the results of the move

Filling Primitives

> fillCircle :: (Real c, Divisor c, Monoid a) => a -> c -> Coord c -> Fill c a
> fillCircle val radius pos = Fill qry bnds mv
>     where
>     -- When the coordinate is within the radius distance from the centre produce
>     qry crd | coordLength (crd |-| pos) <= realToFrac radius  = val
>             | otherwise                                       = mempty
>     -- The bounds is a square centred around the position
>     bnds = Bounds pos (Extents . Coord $ (radius, radius))
>     -- Moving it construct circle with new centre
>     mv pos' = fillCircle val radius (pos |+| pos') 
> 
> fillRectangle :: (Real c, Divisor c, Monoid a) => a -> c -> c -> Coord c -> Fill c a
> fillRectangle val w h pos = Fill qry bnds mv
>     where
>     -- When the coordinate is within bounds of the rectangle produce
>     qry crd | let (x, y) = unCoord $ abs <$> (crd |-| pos) 
>                   in x <= halfW && y <= halfH       = val
>             | otherwise                             = mempty
>     halfW = w `divideBy'` 2
>     halfH = h `divideBy'` 2
>     -- the rectangle is its bounds
>     bnds = Bounds pos (Extents . Coord $ (halfW, halfH))
>     -- Moving it constructs a new rectangle centred on the new position
>     mv pos' = fillRectangle val w h (pos |+| pos') 

Drawing ASCII

> class ProduceChar a where produceChar :: a -> Char -- map some value to a Char
> 
> instance ProduceChar Char where 
>     produceChar = id    -- always produces itself (hence id)
> 
> instance ProduceChar a => ProduceChar (Maybe a) where
>     produceChar Nothing   = ' '             -- when nothing produce space
>     produceChar (Just a) = produceChar a    -- when something produce the related Char
> 
> instance ProduceChar a => ProduceChar (Last a) where 
>     produceChar = produceChar . getLast
> 
> lastChar :: Char -> Last Char
> lastChar = Last . Just
> drawFillMatrix :: ProduceChar a => Int -> Int -> Fill Int a -> IO ()
> drawFillMatrix cs ls fl = putStrLn $ ios cs2 ls
>     where
>         cs2      = cs * 2
>         -- we map produceChar over the result of the query
>         flToChar = queryFill . fmap produceChar $ fl    
>         ios 0 0  = []
>         -- end of each line we add a new line
>         ios 0 l  = '\n' : ios cs2 (l-1)
>         -- we iterate over the coordinates in our matrix
>         ios c l  = (flToChar $ Coord (cs - c `div` 2, ls - l)) : ios (c-1) l

Example Picture

> myPicture :: IO ()
> myPicture = drawFillMatrix 40 40 (border <> moveFill image (coordI 5 5))
>     where
>         border =  fillRectangle (lastChar '+') 1 1 (coordI 0 0)
>                <> fillRectangle (lastChar '+') 1 1 (coordI 40 40)
>                <> fillRectangle (lastChar '+') 1 1 (coordI 40 0)
>                <> fillRectangle (lastChar '+') 1 1 (coordI 0 40)
> 
>         image =  fillCircle    (lastChar 'X') 11  (coordI 15 15)
>               <> fillCircle    (lastChar '#') 7   (coordI 15 15)
>               <> fillRectangle (lastChar '$') 6 6 (coordI 15 15)
>               <> fillCircle    (lastChar ' ') 2   (coordI 15 15)
+                                                                              +
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                       XX                                       
                               XXXXXXXXXXXXXXXXXX                               
                           XXXXXXXXXXXXXXXXXXXXXXXXXX                           
                         XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX                         
                       XXXXXXXXXXXXXXXX##XXXXXXXXXXXXXXXX                       
                     XXXXXXXXXXXX##############XXXXXXXXXXXX                     
                     XXXXXXXXXX##################XXXXXXXXXX                     
                   XXXXXXXXXX######################XXXXXXXXXX                   
                   XXXXXXXX######$$$$$$$$$$$$$$######XXXXXXXX                   
                   XXXXXXXX######$$$$$$  $$$$$$######XXXXXXXX                   
                   XXXXXXXX######$$$$      $$$$######XXXXXXXX                   
                 XXXXXXXX########$$          $$########XXXXXXXX                 
                   XXXXXXXX######$$$$      $$$$######XXXXXXXX                   
                   XXXXXXXX######$$$$$$  $$$$$$######XXXXXXXX                   
                   XXXXXXXX######$$$$$$$$$$$$$$######XXXXXXXX                   
                   XXXXXXXXXX######################XXXXXXXXXX                   
                     XXXXXXXXXX##################XXXXXXXXXX                     
                     XXXXXXXXXXXX##############XXXXXXXXXXXX                     
                       XXXXXXXXXXXXXXXX##XXXXXXXXXXXXXXXX                       
                         XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX                         
                           XXXXXXXXXXXXXXXXXXXXXXXXXX                           
                               XXXXXXXXXXXXXXXXXX                               
                                       XX                                       
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
+                                                                              +

Battleship

+                                                                              +
 XX                                                                             
                                                                                
                                                                                
                                                                                
         XX                                                                     
                                                                                
                                                                                
               XX                                                               
                                       XX                                       
                 CC                                                             
               CCCCCCCCCCCC                                                     
             CCCCCCCCCCCCCC                                                     
               CCCCCCCCCCCC                                                     
                 CC                                                             
                                                                                
                             CC                                                 
                           CCCCCCCCCCCC                                         
                         CCCCCCCCCCCCCC                                         
                           CCCCCCCCCCCC                                         
               XX            CC        XX                  XX                   
                                                                                
                                                                                
                                                           XX                   
                                                         DD                     
                                                       DDDDDD                   
                                                     DDDDDDDDDD                 
                                                       DDDDDD                   
                                                         DD    CC               
                                                         DD  CCCCCCCCCCCC       
                                                         DDCCCCCCCCCCCCCC       
                                                       DDDDDDCCCCCCCCCCCC       
                                                     DDDDDDDDDDCC               
                                                       DDDDDD                   
                                                         DD        XX           
                                                                       CC       
                                                                     CCCCCCCCCCC
                                                                   CCCCCCCCCCCCC
                                                                     CCCCCCCCCCC
                                                                       CC       
+                                                                              +
Guess r c / Cheat 'c' / New Game 'n' / Quit 'q'

Defining ships

> -- Type of ship
> data ShipType = Cruiser | Destroyer
> 
> -- what character is produced by the ship type
> instance ProduceChar ShipType where
>     produceChar Cruiser = 'C'
>     produceChar Destroyer = 'D'
> 
> -- How is the ship oriented on the board
> data ShipOrientation = ShipVertical | ShipHorizontal deriving (Show, Eq, Ord, Bounded)
> -- Make a cruiser ship given an orientation and a centre
> cruiser :: ShipOrientation -> CoordI -> Fill Int (Last ShipType)
> cruiser o pos = case o of
>     ShipVertical    -> fillRectangle t 2 3 pos <> fillCircle t 2 (pos |-| coordI 0 3)
>     ShipHorizontal  -> fillRectangle t 3 2 pos <> fillCircle t 2 (pos |-| coordI 3 0)
>     where
>         t = Last . Just $ Cruiser
> 
> -- Make a destroyer ship given an orientation and a centre
> destroyer :: ShipOrientation -> CoordI -> Fill Int (Last ShipType)
> destroyer o pos = case o of
>     ShipVertical    -> fillRectangle t 1 2 pos 
>                     <> fillCircle t 2 (pos |-| coordI 0 3) 
>                     <> fillCircle t 2 (pos |+| coordI 0 3)
> 
>     ShipHorizontal  -> fillRectangle t 2 1 pos 
>                     <> fillCircle t 2 (pos |-| coordI 3 0) 
>                     <> fillCircle t 2 (pos |+| coordI 3 0)
>     where
>         t = Last . Just $ Destroyer

Laying out the board

> layoutBoard :: Int -> Int -> [Fill Int (Last ShipType)] -> [Fill Int (Last ShipType)]
> layoutBoard _ _ [] = []
> layoutBoard w h ships = ships'
>     where
>         ships' = foldl findPlace [] shipsInBnds
>         shipsInBnds = map toBnds ships
> 
>         toBnds s = let 
>                     bnds  = fillBounds s 
>                     Coord (cx, cy) = boundsCentre bnds
>                     Coord (ex, ey) = coordFromExtents . boundsExtent $ bnds
>                     dx = if cx < ex then ex - cx else (if cx + ex > w then w - cx - ex  else 0)
>                     dy = if cy < ey then ey - cy else (if cy + ey > h then h - cy - ey  else 0)
>                     in moveFill s (coordI dx dy)
> 
>         findPlace [] n = [n]
>         findPlace ps n = ps <> [offset (mconcat ps) n (coordI 0 0) 0]
> 
>         offset chk n o m = 
>                          let 
>                          n' = if m >= 2 * w * h then error "fails" else moveFill n o 
>                          bnds = fillBounds n'
>                          Coord (cx, cy) = boundsCentre bnds
>                          Coord (ex, ey) = coordFromExtents . boundsExtent $ bnds
>                          cs = [coordI x y | x <- [(cx - ex) .. (cx + ex)], y <- [(cy - ey) .. (cy + ey)]]
>                          in if isOk chk n' cs 
>                             then n'
>                             else offset chk n (incOff o) (m+1)
> 
>         isOk chk n cs = let 
>                         bnds  = fillBounds n
>                         Coord (cx, cy) = boundsCentre bnds
>                         Coord (ex, ey) = coordFromExtents . boundsExtent $ bnds
>                         xOk = cx >= ex && cx + ex <= w
>                         yOk = cy >= ey && cy + ey <= h
>                         in xOk && yOk && (not . getAny . mconcat . map (Any . col chk n) $ cs)
> 
>         col chk n c = let 
>                          a = getLast . queryFill chk $ c
>                          b = getLast . queryFill n $ c
>                          in case (a,b) of
>                             (Just _, Just _) -> True
>                             _                -> False
> 
>         incOff (Coord (x,y)) | x >= w && y >= h = Coord (0, 0)
>                              | x >= w           = Coord (0, y + 1)
>                              | otherwise        = Coord (x + 1, y)

Making a random board

> randomBoard :: StdGen -> (StdGen, [Fill Int (Last ShipType)])
> randomBoard gen = 
>     let ship = ZipList . map ([destroyer, cruiser] !!) . randomRs (0, 1) $ gen
>         orient = ZipList . map ([ShipVertical,ShipHorizontal] !!) . randomRs (0, 1) $ gen
>         cxs = ZipList . randomRs (0, 40) $ gen
>         cys = ZipList . randomRs (0, 40) $ gen
>     in ( mkStdGen . fst . random $ gen
>        , layoutBoard 40 40 . take 5 . getZipList $ ship <*> orient <*> (coordI <$> cxs <*> cys)
>        )

Managing the game

> data Game = Game { ships     :: [Fill Int (Last ShipType)] -- the alive ships
>                  , board     :: Fill Int (Last Char)       -- the board showing choices and dead ships
>                  }
> 
> -- Helper that maps a ship to characters
> shipToBrd :: Fill Int (Last ShipType) -> Fill Int (Last Char)
> shipToBrd s = Last . (fmap produceChar) . getLast <$> s
> 
> -- Helper action that draws the game board for us
> drawBrd :: Fill Int (Last Char) -> IO ()
> drawBrd = drawFillMatrix 40 40  
> 
> -- Action starting a new game
> playNewGame :: StdGen -> IO ()
> playNewGame gen = let 
>                   -- we generate a random board
>                   (gen', ships) = randomBoard gen
>                   -- draw the border '+' characters using the normal Applicative instance for list
>                   -- to get all the corner combinations
>                   border = mconcat $ map (fillRectangle (lastChar '+') 1 1) (coordI <$> [0,40] <*> [0,40]) 
>                   -- and then we start the game
>                   in playGame gen' (Game ships border)
> -- when we chose to cheat we show the board with all the ships on it and then continue playing
> cheatGame :: StdGen -> Game -> IO ()
> cheatGame gen g = drawBrd (board g <> (mconcat . map shipToBrd . ships $ g)) >> playGame gen g
> 
> -- when we win the game we can choice to play a new one
> wonGame :: StdGen -> IO () 
> wonGame gen = do
>     putStrLn "You won the game. Play another ? 'y'/'n'"
>     t <- getLine
>     case filter (not . isSpace) t of
>         'y' : _ -> playNewGame gen
>         'Y' : _ -> playNewGame gen
>         'n' : _ -> return ()
>         'N' : _ -> return ()
>         _       -> wonGame gen
> takeShot :: String -> StdGen -> Game -> IO ()
> takeShot t gen g = let 
>     r : c : _ = map (read) (words t)
>     -- The board is updated
>     b = board g 
>         -- With an 'X' showing where we guessed
>         <> fillRectangle (lastChar 'X') 1 1 (coordI r c) 
>         -- And the display of all the ships which were hit
>         <> (mconcat . map shipToBrd $ hit)
>     -- Hit ships are those which produce at the coordinate
>     produces = isJust . getLast . (\q -> q (coordI r c)) . queryFill
>     hit = filter produces (ships g)
>     -- Missed ships are those which do not produce at the coordinate
>     miss = filter (not . produces) (ships g)
>     -- The new game state is all the missed ships and the updated board
>     in playGame gen (Game miss b)
> playGame :: StdGen -> Game -> IO ()
> playGame gen g = if win g then wonGame gen else do 
>     drawBrd . board $ g    
>     putStrLn "Guess r c / Cheat 'c' / New Game 'n' / Quit 'q'"
>     t <- getLine
>     case filter (not . isSpace) t of
>         'c' : _ -> cheatGame gen g
>         'C' : _ -> cheatGame gen g
>         'n' : _ -> playNewGame gen
>         'N' : _ -> playNewGame gen
>         'Q' : _ -> return ()
>         'q' : _ -> return ()
>         _       -> takeShot t gen g
>     where 
>        win (Game [] _) = True
>        win _ = False

Main function entry point

> main :: IO ()
> main = do
>     putStrLn "An exmaple picture"
>     myPicture
>     _ <- putStrLn "enter any text to continue " >> getLine
>     gen0 <- getStdGen
>     playNewGame gen0
>     return ()