I've created a chess game with Haskell and everything seems to be working. However, I'm trying to define the main function of the program so that each time a move is made (which takes two positions and a board as arguments) the resulting board is kept somewhere, so that it can then be used as an argument for the next move. The code looks something like this.
makeMove :: Position -> Position -> Board -> Board
makeMove pos1 pos2 board = ...
I'm aware of the do
notation and have a basic understanding of IO in Haskell, but I'm still unsure on how to proceed.
I'm assuming you want your game to be relatively dynamic and to respond to input, hence the IO question.
I'll give a bit of background theory on imperative style commands and IO interpreted as functions, then look at this in Haskell and finally talk about your case from this point of view.
If this is stuff you know, apologies, but it might help anyway, or it might help others.
In Haskell, we obviously have no direct mutation of variables. But we can consider a (closely) related idea of 'functions on states' - commands which would, in an imperative paradigm, be seen as mutating variables, can be seen as a 'state transformer': a function which, given one state (of the program, world, whatever) outputs another one.
An example:
Suppose we have a state consisting of a single integer variable a
. Use the notation x := y
meaning 'assign the value of expression y to the variable x'. (In many modern imperative languages this is written x = y
, but to disambiguate with the equality relation =
we can use a slightly different symbol.) Then the command (call it C
)
a := 0
can be seen as something which modifies the variable a. But if we have an abstract idea of a type of 'states
', we can see the 'meaning' of C
as a function from states
to states
. This is sometimes written 〚C〛
.
So 〚C〛: states -> states
, and for any state s, 〚C〛s = <the state where a = 0>
. There are much more complicated state transformers that act on much more complicated kinds of state, but the principle is not more complicated than this!
An important way to make new state transformers from old ones is notated by the familiar semicolon. So if we have state transformers C1
and C2
, we can write a new state transformer which 'does C1
and then C2
' as C1;C2
. This is familiar from many imperative programming languages. In fact, the meaning as a state transformer of this 'concatenation' of commands is
〚C1;C2〛: states -> states
〚C1;C2〛s = 〚C2〛(〚C1〛s)
i.e. the composition of the commands. So in a sense, in Haskell-like notation
(;) : (states -> states) -> (states -> states) -> states -> states
c1 ; c2 = c2 . c1
i.e. (;)
is an operator on state-transformers which composes them.
Now, Haskell has some neat ways of bringing these concepts directly into the language. Instead of having a distinct type for commands (state modifiers without a type, per se) and expressions (which, depending on the imperative context, may also be allowed to modify the state as well as resulting in a value), Haskell somewhat combines these into one. IO ()
entities represent pure state modifying actions which don't have a meaning as an expression, and IO a
entities (where a
is not ()
) represent (potential) state modifying actions whose meaning as an expression (like a 'return type') is of type a
.
Now, since IO ()
is like a command, we want something like (;)
, and indeed, in Haskell, we have the (>>)
and (>>=)
('bind operators') which act just like it. We have (>>) :: IO a -> IO b -> IO b
and (>>=) :: IO a -> (a -> IO b) -> IO b
. For a command (IO ()
) or command-expression (IO a
), the (>>)
operator simply ignores the return if there is one, and gives you the operation of doing the two commands in sequence. The (>>=)
on the other hand is for if we care about the result of the expression. The second argument is a function which, when applied to the result of the command-expression, gives another command/command-expression which is the 'next step'.
Now, since Haskell has no 'mutable variables', an IORef a
-type variable represents a mutable reference variable, to an a
-type variable. If ioA
is an IORef a
-type entity, we can do readIORef ioA
which returns an IO a
, the expression which is the result of reading the variable. If x :: a
we can do writeIORef ioA x
which returns an IO ()
, the command which is the result of writing the value x
to the variable. To create a new IORef a
, with value x
we use newIORef x
which gives an IO (IORef a)
where the IORef a
initially contains the value x
.
Haskell also has do
notation which you alluded to, which is a nice syntactic sugar for the above. Simply,
do a; b = a >> b
do v <- e; c = e >>= \v -> c
If we have some IO entity getAMove :: IO (Position, Position)
(which might be a simple parser on some user input, or whatever suits your case), we can define
moveIO :: IORef Board -> IO ()
moveIO board =
readIORef board >>= \currentState -> -- read current state of the board
getAMove >>= \(pos1, pos2) -> -- obtain move instructions
writeIORef board (makeMove pos1 pos2 currentState) -- update the board per makeMove
This can also be written using do
notation:
moveIO board = do
currentState <- readIORef board; -- read current state of the board
(pos1, pos2) <- getAMove; -- obtain move instructions
writeIORef board (makeMove pos1 pos2 currentState) -- update the board per makeMove
Then, whenever you need a command which updates an IORef Board
based on a call to getAMove
you can use this moveIO
.
Now, if you make appropriate functions with the following signatures, a simple main IO loop can be devised:
-- represents a test of the board as to whether the game should continue
checkForContinue :: Board -> Bool
checkForContinue state = ...
-- represents some kind of display action of the board.
-- could be a simple line by line print.
displayBoardState :: Board -> IO ()
displayBoardState state = ...
-- represents the starting state of the board.
startState :: Board
-- a simple main loop
mainLoop :: IORef Board -> IO ()
mainLoop board = do
currentState <- readIORef board;
displayState currentState;
if checkForContinue currentState then
do moveIO board; mainLoop board
else return ()
main :: IO ()
main = do
board <- newIORef startState;
mainLoop board