We're going to round out the application with some IO so that we can track the mouse through the maze. We'll print the state of the world (maze plus mouse) for each move that the mouse makes and we'll pause for a key press between each move.
In the process, the following concepts will be introduced:
- IO in Haskell
- Getting a package from Hackage using cabal
- Using a library function from the installed package
We're going to print the maze on the console. We can use the original character maze (CharMaze) for this, rather than converting the Material maze (Maze) back to characters. However, we need to create a function called markLocation that will replace one character in the maze with the position of the mouse (representing the mouse as a different character... a dot).
The markLocation function needs to take a coordinate for the location of the mouse, and a CharMaze. It will then return a CharMaze (with the location of the mouse indicated). The type is therefore:
markLocation :: (Int,Int) -> CharMaze -> CharMaze
In order to deconstruct and reconstruct the maze with the mouse location replaced with a dot, the markLocation function will need to do the following:
- Separate the rows into those before the mouse location row, the mouse row, and those after the mouse location row
- For the row containing the mouse, separate the row positions into those before the mouse, the character with the mouse, and the positions after the mouse
- We can then build a new mouse row, with the character position of the mouse replaced with a dot
- We can then rebuild the maze, with the rows before, the new mouse row, and the rows after
Here's a definition for this transformation:
markLocation (x,y) map =
(beforeRows, theRow : afterRows) = splitAt y map
(beforeItems, _ : afterItems) = splitAt x theRow
newRow = beforeItems ++ '.' : afterItems
beforeRows ++ newRow : afterRows
We are making use of the splitAt standard list function to divide a list into two components. It so happens that the second list after the split always includes the item at the split position, so it's easy to split this individual item off the front of this second list using the list cons operator (:). In the case of the rows, we bind the name theRow to this single row at the mouse (split) position. Similarly, when we deconstruct the row 'items' we bind names for beforeItems and afterItems, but this time there's no point assigning a name to the character at the mouse location as we're about to replace it with a dot.
To create a new row, we do everything in reverse, constructing the new row out of the beforeItems appended to the list made from the dot character cons'ed with the afterItems.
Finally, we similarly recompose the rows, with the beforeRows appended to the list of rows made up of the newRow cons'ed with the afterRows.
We're now ready to make the function that will plot a sequence of maze states, one for each mouse location in a given Path. This involves doing IO for the first time in this project.
Haskell's IO is extremely clever, but you won't have seen anything quite like it in any other language. Because function purity is sacrosanct in Haskell, we are not allowed to do anything in a function that directly causes or uses side-effects. IO is a huge elephant in the room when it comes to being side-effect free, because side-effects are the whole point of IO! The clever thing about Haskell is it does IO by not performing the IO directly in regular functions, but allowing you to create and compose together IO operation values that are then 'performed' in the right sequence at the right time. The right time is when the main function in your program has to perform the top-level IO command in order to get anything done. At this point, all the IO commands are 'performed', along with the regular functions that were glued into them to do normal things like adding up numbers.
The biggest thing to remember about using Haskell's IO features is that if you're defining a function that returns an IO operation value then you're in this special world. You'll be using some special ways to compose together IO operations to make bigger IO constructs. It's all quite cute when you get used to it, but there are some more special functions to learn, and some special syntax called "do notation" that makes composition of IO sequences more convenient. In fact, when you're using do notation, you're doing something very similar to using an imperative language embedded in a functional language.
With some scene-setting out the way, let's start by thinking about the type of the function (we'll call it plotPath) that we want to create:
plotPath :: CharMaze -> Path -> IO ()
The arguments here are straightforward. plotPath will take an original CharMaze and a Path to plot. The original CharMaze will be modified with markLocation at each step in the given Path, then printed to the screen. We'll then wait for a character to be entered at the keyboard with the getChar function.
The return type of plotPath is one of these IO operations. In fact it's the IO operation that has no residual value, which is represented by the unit type that we mentioned previously when discussing tuples. Output IO tends to leave no 'residue' (the values are 'gone' to the output device), whereas input is all about obtaining some value that is the residual of the IO. The type of getChar for instance is:
... indicating that a character is the result of performing this IO operation when that finally happens.
Here's the definition for plotPath:
plotPath charMaze path = forM_ path printMazeAndWait
printMazeAndWait point = do
forM_ (markLocation point charMaze) putStrLn
The first line of the definition "forM_ path printMazeAndWait" works just like a 'foreach' in imperative languages, however it builds a sequence of IO operations, each operating on one element from the list of values provided. In this case, we're saying "for each location in path, do the printMazeAndWait operation". Again, it's important to remember that these functions are only composing together IO operations that will eventually be performed later by something underlying the main function in our program.
In the 'where' part of the definition we define this output IO function in turn. Clearly, from the foregoing, this function takes a point (a location) as its argument. We then use Haskell's "do notation" to glue together a sequence of IO operations...
- First, for each row in the transformed maze (using markLocation) we print the row to the console (stdout) with a newline after each row.
- Then we wait and obtain the next key press from the console (stdin)
The result of this function is the IO () value representing all these IO operations combined and in order.
We need one more utility function to be ready to finally put this through its paces. This is a function that brings together generating a single path for a CharMaze and then showing it on the screen:
plotFirstPath charMaze = plotPath charMaze $ head $
mousePathsFromStart (materialMaze charMaze) 1
Now, we can finally complete the main function that is the entry point of the program and the top-level IO operation that gets performed.
main = do
putStrLn "Press space to move mouse one step"
hSetBuffering stdin NoBuffering
hSetEcho stdin False
putStrLn "\\o/ The mouse lived happily ever after!"
Again, we use the "do" syntax to sequence some IO operations that represent the root of the program that will execute when you run the binary in the console. There are a couple of special IO commands here for technical reasons:
- "hSetBuffering stdin NoBuffering" just makes sure that the input IO system doesn't wait for a newline before making characters available to the program (otherwise getChar will not behave as expected to pause the program between maze drawings).
- "hSetEcho stdin False" also turns off the echoing of typed characters to the keyboard. We want getChar to do its thing 'silently'.
If you compile and run the program, you should see mazes scrolling up the console when you press keys, until the program completes.
To go the final mile, we'll make a change to clear the console between maze drawing for a better effect.
To do this, we'll engage the services of one of the very many additional packages from the hackage repository. This one is called "ansi-terminal" and it provides a "clearScreen" function that will clear any ANSI terminal screen. This should certainly work on any Unix and Linux system. YMMV on Windows!
To hackage repository has a browser allowing users to see packages by category and to search for packages. All the available packages are obtainable using the standard package manager tool for Haskell called "cabal". Cabal is installed automatically when you install the Haskell Platform, which is the standard way of obtaining Haskell these days. Cabal takes care of downloading, building and archiving packages in standard locations. As most modern Haskell IDEs use cabal packages for projects, building projects (e.g. in EclipseFP or Leksah) will automatically invoke cabal to build the package artifacts (applications or libraries).
Both EclipseFP and Leksah have a way to declare external package dependencies for your project.
In EclipseFP, double-clicking on the "<name>.cabal" node near the top of the hierarchy in the Project Explorer will open the cabal properties editor. Clicking on the "Executables" tab will allow package dependencies to be added to executables included in the project cabal package.
In Leksah, the cabal settings are available from the Package -> Edit menu and the "Dependencies" tab provides a similar editor.
Whichever IDE you are using, you will need to add "ansi-terminal" as a package dependency. Then, this package will need to be fetched and installed. Both EclipseFP and Leksah have means to install dependencies from within the IDE. However, this can also be done from the command line with the cabal tool:
> cabal install ansi-terminal
When you execute this command in the shell, you should see messages indicating that the package is being fetched, built and installed successfully. Cabal has the ability to transitively fetch and install dependencies, so several packages might be obtained and built whenever you request a specific package for inclusion in a project.
Having installed ansi-terminal and added it to our project dependencies there are only a couple of things left to do to use it...
- Include an import statement for the modules you need from the package. In this case we need to add: import System.Console.ANSI
- Add the line "clearScreen" beneath "getChar" in the printMazeAndWait local function of plotPath.
When the application is rebuilt and run, you should see the effect of the clearScreen between key presses and maze drawing (assuming you are running the application in a shell in a ANSI terminal window).
So that just about wraps it up for the Amazing Mouse exercise.
Haskell can initially be a steep learning curve, especially when cross-training from imperative/procedural and OO languages. However, as is always the case, immersion and experimentation are the fastest paths to enlightenment.
I've tried to strike a balance in this example of explaining enough to avoid big mysteries, but invariably there will be gaps if you are completely new to the language. That being the case, I would highly recommend the book "Learn you a Haskell for Great Good". Once past the first half-dozen chapters, then this example should make a lot more sense!
There are plenty of ways that this example, as presented, could be improved for efficiency and style, let alone adding a few more bells and whistles. I may subsequently post a few suggestions in case anyone lacks imagination (!) but wants to tinker some more.