【问题标题】:Repeatedly applying function to game board in Haskell在 Haskell 中将函数重复应用于游戏板
【发布时间】:2015-11-17 23:25:50
【问题描述】:

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

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

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

【问题讨论】:

  • foldr (uncurry makeMove) :: [ (Position, Position) ] -> Board -> Board 是一个函数,它接受一个移动列表和一个初始棋盘“做出”这些移动从初始状态开始,返回最终状态。本质上,它会运行你的游戏直到完成。不需要 IO。

标签: haskell state main chess


【解决方案1】:

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

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

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

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

在 Haskell 中,我们显然没有直接的变量变异。但是我们可以考虑一个(密切)相关的“状态函数”的概念——命令,在命令式范式中,被视为变异变量,可以被视为“状态转换器”:一个函数,给定一个状态(程序,世界等)输出另一个。

一个例子:

假设我们有一个由单个整数变量a 组成的状态。使用符号x := y 表示“将表达式y 的值赋给变量x”。 (在许多现代命令式语言中,这是写成x = y,但为了消除等式关系= 的歧义,我们可以使用稍微不同的符号。)然后是命令(称为C

a := 0

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

用熟悉的分号表示从旧状态转换器制作新状态转换器的重要方法。因此,如果我们有状态转换器C1C2,我们可以编写一个新的状态转换器,它“执行C1 然后C2”为C1;C2。这在许多命令式编程语言中都很熟悉。事实上,这种“串联”命令作为状态转换器的意义是

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

即命令的组成。所以在某种意义上,在类似 Haskell 的符号中

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

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

Haskell 的方法

现在,Haskell 有一些巧妙的方法可以将这些概念直接引入语言。 Haskell 没有为命令(本身没有类型的状态修饰符)和表达式(根据命令式上下文,也可能允许修改状态并产生值)提供不同的类型,而是在某种程度上结合了这些合而为一。 IO () 实体代表纯粹的状态修改动作,它没有作为表达式的意义,IO a 实体(其中 a 不是 ())表示(潜在的)状态修改动作,其意义为表达式(如'返回类型') 的类型为a

现在,由于IO () 就像一个命令,我们想要类似(;) 的东西,事实上,在Haskell 中,我们有(>>)(>>=)('绑定运算符'),它们的作用就像它一样。我们有(>>) :: IO a -> IO b -> IO b(>>=) :: IO a -> (a -> IO b) -> IO b。对于命令 (IO ()) 或命令表达式 (IO a),(>>) 运算符会简单地忽略返回(如果有返回),并为您提供按顺序执行两个命令的操作。另一方面,(>>=) 用于我们是否关心表达式的结果。第二个参数是一个函数,当应用于命令表达式的结果时,它会给出另一个命令/命令表达式,即“下一步”。

现在,由于 Haskell 没有“可变变量”,IORef a-type 变量表示可变引用变量,指向 a-type 变量。如果ioAIORef a 类型的实体,我们可以做readIORef ioA,它返回一个IO a,这是读取变量的结果表达式。如果x :: a 我们可以执行writeIORef ioA x,它返回一个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

【讨论】:

  • 这已经是一个很好的答案,但我认为谈论 state monad 可能也很有用
【解决方案2】:

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

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 通过计算一个新的并递归调用游戏循环来“改变”。

【讨论】:

    猜你喜欢
    • 2021-03-03
    • 2017-07-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-28
    • 1970-01-01
    相关资源
    最近更新 更多