Search code examples
haskellstateprogram-entry-pointchess

Repeatedly applying function to game board in Haskell


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.


Solution

  • 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.

    Some background on imperative commands

    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.

    Haskell's approach

    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
    

    Your case

    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