FunSpIns - Moving a Rectangle

19 Sep 2013 in software-development | haskell | fun-spin |

Functional space-invaders series

  1. A recap of Rob Ashton’s lessons - Das Intro
  2. Drawing a Rectangle
  3. Moving a Rectangle
  4. No attributes, No vectors, A tiny Workflow and more squares
  5. State, the World, the Loop
  6. The hero must move, the enemies must move smarter
  7. The hero shoots
  8. Collisions, the dead, and a (not so) grateful ending

Inspired by Rob Ashton’s series “Learn functional programming with me” Last time a square got drawn, this time moving it would be nice. Rob’s approach was based on a recursive function call, one of the basic tools in FP to essentially do loops.

Another way to consider the movement would be to look at it as a list of numbers, or coordinates, that dictate at which position the rectangle should be drawn. One weapon of choice in Haskell for sequences of things are list comprehensions.

Incidentally I learned about list comprehensions in Erlang, which look very similar to Haskell’s

Hence, from a given sequence I would create Rectangles that were ready to be drawn onto a screen:

[(Just (Rect x 25 50 50)) | x <- [1..450]]

and for each one of those I would want to draw it on the screen, hence I prepared myself this function:

fillDo canvas rect = do 
 FX.fillRect canvas Nothing (Pixel 0)
 FX.fillRect canvas rect (Pixel 0xFFFFFF)
 FX.flip canvas

The do-notation

all SDL operations happen in the IO monad, which can be seen nicely when we look at the type of the involved functions:

FX.fillRect :: Surface -> Maybe Rect -> Pixel -> IO Bool
FX.flip :: Surface -> IO ()

If we want to work with them, we will need to work in that context ourselves. Haskell has a number of functions to work with Monads, one of them being (»). Its type is

(>>) :: Monad m => m a -> m b -> m b

*) Within the same monad, given a one-in, one-out function, return the output of the function

It looks and sounds trivial, but actually allows to chain functions together that happen in the same context. i.e. something like

-- putStr :: String -> IO ()
putStr "Hello" >> 
putStr " "
--becomes
do
  putStr "Hello"
  putStr " "

List + fillDo = …

My initial attempt was to take my list of Rectangles and map the fillDo over it:

map (fillDo canvas) [(Just (Rect x 25 50 50)) | x <- [1..450]]

Note the use of currying - (fillDo canvas) returns a function that takes a rect as a parameter and returns IO ().

Alas, it wouldn’t compile. After some thinking and googling, the issue dawned on me: every function inside a do block must return into the same context with which we started off. Alas, the last line of code did not return an IO sth, but rather [IO sth], an array of things in the IO context. What I wanted was actually IO [sth].

Once you get the hang out of it, Haskell errors can be quite understandable. This one was…

Couldn't match expected type `IO a0' with actual type `[b0]'
In the return type of a call of `map'
In a stmt of a 'do' block:
...

Thankfully there is a version of map with the kind of signature that we need:

mapM :: Monad m => (a -> m b) -> [a] -> m [b]

*) Given a function taking a’s and returning b’s within m and an array of a’s, give me the resulting array of b’s, within the m context

Bingo, small change and here’s the full program:

module Main where

  import Graphics.UI.SDL as FX

  main :: IO ()
  main = do
    init
    canvas <- FX.getVideoSurface
    mapM (fillDo canvas) [Rect x 25 50 50 | x <- [1..450]]
    delay 1000
    FX.quit
    where
      fillDo canvas rect = do 
        FX.fillRect canvas Nothing (Pixel 0)
        FX.fillRect canvas (Just rect) (Pixel 0xFFFFFF)
        FX.flip canvas
      init = do
        FX.init [InitVideo]
        FX.setVideoMode 640 480 32 []

So far I do not want to go full steam ahead and implement the logic/render/repeat thing, since, to be honest, I don’t quite see where this is heading. Even so, there is some potential looming for separating logic from rendering as the production of the rectangle “path” can be looked at independent of IO.

PS

we saved ourselves the “white smear” stage by adding that canvas cleaning code in the fillDo (FX.fillRect canvas Nothing (Pixel 0))

Chronology

  |  
comments powered by Disqus