Google AI Challenge and Haskell list performance



Published on February 8, 2010 under the tag A post about a typical issue with Haskell listsPublished on February 8, 2010 under the tag haskell

What is this about

This blogpost is about the Google AI Challenge, which allows Haskell as a possible language one can write a bot in. Unfortunately, the code they provide for Haskell is very inefficient. Because the contest requires you to do all your calculations in one second, this is quite a setback. This post is literate Haskell, so you should basically be able to paste it into a .lhs file and run it.

module Main where

First, we need some imports. You can safely ignore these, but unfortunately the compiler cannot.

import Control.Monad (replicateM, forever) (replicateM, forever) import qualified Data.Array.Unboxed as UA import Data.Array.Unboxed ((!)) ((!)) import Data.List (transpose) (transpose) import System.IO (hSetBuffering, BufferMode ( LineBuffering ), stdin, stdout) (hSetBuffering,), stdin, stdout)

We start by defining a Tile type. Tiles are our points on the 2D map, so we can easily represent them as tuples:

type Tile = ( Int , Int )

This allows to write, for example, the code to determine the adjacent tiles in a very clear and concise way.

adjacentTiles :: Tile -> [ Tile ] = [ (x, y - 1 ) adjacentTiles (x, y)[ (x, y + 1 , y) , (x, y) + 1 ) , (x, y - 1 , y) , (x, y) ]

But that is not what this article is about. The thing is that in the code they provided as a starter package, they stored the information about the map in a [[Spot]] type – a matrix of Spot s, where a Spot would be either a wall, an empty tile, the player…

The problem with this is that Haskell’s default lists are linked lists. Imagine we have a m * n map – so m tiles wide and n tiles high. In most other languages, you would expect you can access the elements in constant time – so O(1) . But because we’re using linked lists, this is not the case. Worst case scenario, you would access the bottom right element. This would take O(m + n) time – and that is not acceptable.

So I’ll abuse this blogpost to ramble about Haskell arrays.

data Board = Board { boardWidth :: Int , boardHeight :: Int , boardWalls :: UA.UArray ( Int , Int ) Bool , boardBotPosition :: ( Int , Int ) , boardEnemyPosition :: ( Int , Int ) }

We use an “Unboxed Immutable Array” to store or walls. A bit of context:

Unboxed : this is basically a counter-measure against Haskell’s laziness. While that can be very useful in certain situations, it is not here. By using an unboxed array, we force the contents to be fully evaluated. This means the array will not take up much more room than the corresponding array in C would.

: this is basically a counter-measure against Haskell’s laziness. While that can be very useful in certain situations, it is not here. By using an unboxed array, we force the contents to be fully evaluated. This means the array will not take up much more room than the corresponding array in C would. Immutable: because we read the Board again every time – that’s the way the engine works – we don’t have to change the board at all. This means we can simply write nice, purely functional code here.

The code to load this Board follows later – let’s first see how we lookup a certain Tile in the Board :

isWall :: Board -> ( Int , Int ) -> Bool = x < 0 || x >= boardWidth board isWall board (x, y)boardWidth board || y < 0 || y >= boardHeight board boardHeight board || boardWalls board ! (x, y) boardWalls board(x, y)

Yep, we now have a function that runs in O(1) and that is quite efficient (of course we have to do some basic boundary checks). The code to read the Board is a little more complicated (but also more interesting):

readBoard :: Int -> Int -> [ String ] -> Board lines = Board readBoard width height = width { boardWidthwidth = height , boardHeightheight = walls , boardWallswalls = find '1' , boardBotPositionfind = find '2' , boardEnemyPositionfind }

The width and height are passed to the function, along with a number of lines. The real code happens in the where block:

where = concat $ transpose $ map ( take width) lines stringtransposewidth) = UA.listArray (( 0 , 0 ), (width - 1 , height - 1 )) string matrixUA.listArray ((), (width, height)) string = UA.amap ( /= ' ' ) matrix wallsUA.amap () matrix = head [ i | i <- UA.indices matrix, matrix ! i == c ] find c[ iUA.indices matrix, matrixc ]

What might not be obvious to see, is why we use transpose here. transpose transposes a matrix – why would we want that?

The answer is that I find it easier (if you will, more “natural”) to use (x, y) notation instead of (row, column) notation. You can safely remove the transpose function and switch to (row, column) if you want, but I think the (x, y) notation is a little easier.

We also consider everything that is not a space character a wall – that includes the player and the enemy. I do this because this makes my code easier (and it makes sense in a way, you don’t want to steer through yourself or the enemy).

Then we use a small find function to locate the player and the enemy in the array. This find functions should run very fast, since looking up in arrays is, well, very fast .

We call this readBoard function more or less in the following way:

takeTurn :: IO () () = do takeTurn <- getLine line let [width, height] = map read $ words line [width, height]line lines <- replicateM height getLine replicateM height let board = readBoard width height lines boardreadBoard width height -- Select tile to go next. -- Print out new direction to stdout. putStrLn "1"

So we can make our main very short:

main :: IO () () = do hSetBuffering stdin LineBuffering mainhSetBuffering stdin LineBuffering hSetBuffering stdout forever takeTurn