在 Haskell 中重复将函数应用于游戏板

Repeatedly applying function to game board in Haskell

我用 Haskell 创建了一个国际象棋游戏,似乎一切正常。但是,我正在尝试定义程序的主要功能,以便每次移动时(将两个位置和一个棋盘作为参数)将结果棋盘保存在某个地方,以便它可以用作参数为下一步行动。代码看起来像这样。

makeMove :: Position -> Position -> Board -> Board
makeMove pos1 pos2 board = ...

我知道 do 符号并且对 Haskell 中的 IO 有基本的了解,但我仍然不确定如何继续。

我假设您希望您的游戏相对动态并响应输入,因此是 IO 问题。

我会给出一些关于命令式命令和解释为函数的 IO 的背景理论,然后在 Haskell 中查看这个,最后从这个角度谈谈你的案例。

命令式命令的一些背景知识

如果这是您所知道的,抱歉,但无论如何它可能会有所帮助,或者可能对其他人有所帮助。

在Haskell中,我们显然没有变量的直接突变。但是我们可以考虑 'functions on states' 的一个(密切)相关的想法 - 在命令式范例中,命令将被视为可变变量,可以被视为 'state transformer':一个函数,给定一个状态(程序,世界,等等)输出另一个。

一个例子:

假设我们有一个由单个整数变量 a 组成的状态。使用符号 x := y 表示 'assign the value of expression y to the variable x'。 (在许多现代命令式语言中,这写成 x = y,但为了消除等式关系 = 的歧义,我们可以使用稍微不同的符号。)然后命令(称之为 C

a := 0

可以看作是修改变量a的东西。但是如果我们对'states'的类型有一个抽象的概念,我们可以把C的'meaning'看作是从statesstates的一个函数。这有时写成〚C〛。 所以 〚C〛: states -> states,并且对于任何状态 s,〚C〛s = <the state where a = 0>。还有更复杂的状态转换器,作用于更复杂的状态种类,但原理并不比这更复杂!

从旧状态转换器创建新状态转换器的一个重要方法是用熟悉的分号表示。因此,如果我们有状态转换器 C1C2,我们可以编写一个新的状态转换器,其中 'does C1 and then C2' 为 C1;C2。这在许多命令式编程语言中都很熟悉。其实这'concatenation'条命令作为状态转换器的意义是

〚C1;C2〛: states -> states
〚C1;C2〛s = 〚C2〛(〚C1〛s)

即命令的组成。所以在某种意义上,在 Haskell-like notation

(;) : (states -> states) -> (states -> states) -> states -> states
c1 ; c2 = c2 . c1

(;) 是组成状态转换器的运算符。

Haskell的方法

现在,Haskell 有一些巧妙的方法可以将这些概念直接引入语言中。 Haskell 多少把这些合二为一了。 IO () 个实体表示纯状态修改操作,它没有作为表达式的意义,IO a 个实体(其中 a 不是 ())表示(潜在的)状态修改作为表达式(如 'return type')的含义是 a.

类型的操作

现在,由于 IO () 就像一个命令,我们需要类似 (;) 的东西,实际上,在 Haskell 中,我们有 (>>)(>>=) ('bind operators') 就像它一样。我们有 (>>) :: IO a -> IO b -> IO b(>>=) :: IO a -> (a -> IO b) -> IO b。对于命令 (IO ()) 或命令表达式 (IO a),(>>) 运算符会简单地忽略 return(如果有的话),并为您提供执行以下操作的操作两个命令顺序。 (>>=) 另一方面是为了我们是否关心表达式的结果。第二个参数是一个函数,当应用于命令表达式的结果时,给出另一个 command/command-expression 即 'next step'.

现在,由于 Haskell 没有 'mutable variables',IORef a 类型的变量代表一个可变的引用变量,指向 a 类型的变量。如果 ioA 是一个 IORef a 类型的实体,我们可以做 readIORef ioA 其中 return 是一个 IO a,表达式是读取变量的结果。如果 x :: a 我们可以执行 writeIORef ioA x,其中 return 是一个 IO (),该命令是将值 x 写入变量的结果。要创建一个新的 IORef a,值为 x,我们使用 newIORef x,它给出一个 IO (IORef a),其中 IORef a 最初包含值 x

Haskell 也有你提到的 do 符号,这是上面的一个很好的语法糖。简单地说,

do a; b            =        a >> b
do v <- e; c       =        e >>= \v -> c

你的情况

如果我们有一些 IO 实体 getAMove :: IO (Position, Position)(它可能是一些用户输入的简单解析器,或者任何适合您的情况),我们可以定义

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

这也可以用do表示法写成:

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

然后,每当您需要根据对 getAMove 的调用更新 IORef Board 的命令时,您可以使用此 moveIO.

现在,如果您使用以下签名创建适当的函数,则可以设计一个简单的主 IO 循环:

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

您可以使用递归来对状态建模,如下所示:

main :: IO ()
main = do
   let initialBoard = ...
   gameLoop initialBoard

gameLoop :: Board -> IO ()
gameLoop board | gameOver board = putStrLn "Game over."
               | otherwise = do
   print board
   move <- askUserToMove
   let newBoard = applyMove move board
   gameLoop newBoard

这里board是"changed"通过计算一个新的并递归调用游戏循环。