在运行时选择一个单体



我正在尝试用Haskell编写一个双人游戏,例如跳棋。 我设想有类型GameStateMove,以及定义游戏规则的函数result :: GameState -> Move -> GameState。 我想同时拥有人类和自动化玩家,我想我会通过一个类型类来做到这一点:

class Player p m | p -> m where
selectMove :: p -> GameState -> m Move

其中的想法是,m可以是基本AI玩家的身份,人类的IO,跨移动保持状态的AI的状态等。 问题是如何从这些进入整个游戏循环。 我想我可以定义这样的东西:

Player p1 m1, Player p2 m2 => moveList :: p1 -> p2 -> GameState -> m1 m2 [Move]

一个 monadic 函数,它接收玩家和初始状态,并返回移动的惰性列表。 但除此之外,假设我想要一个基于文本的界面,比如说,允许首先从可能性列表中选择每个玩家,然后让游戏开始。 所以我需要:

playGame :: IO () 

我看不出如何以通用方式定义给定的 playGame 给定的 moveList。 还是我的整体方法不正确?

编辑:进一步考虑,我什至不知道如何定义上面的 moveList。 例如,如果玩家 1 是人类,那么 IO,而玩家 2 是有状态的 AI,那么 State 则玩家 1 的第一步将具有类型IO Move。 然后玩家 2 必须采用IO GameState类型的结果状态并产生类型State IO Move的移动,而玩家 1 的下一步将是IO State IO Move类型? 这看起来不对。

这个问题有两个部分:

  • 如何将独立于 monad 的国际象棋框架与增量 monad 特定的输入混合在一起
  • 如何在运行时指定特定于 monad 的部分

您可以使用发电机解决前一个问题,这是自由单元变压器的特例:

import Control.Monad.Trans.Free -- from the "free" package
type GeneratorT a m r = FreeT ((,) a) m r
-- or: type Generator a = FreeT ((,) a)
yield :: (Monad m) => a -> GeneratorT a m ()
yield a = liftF (a, ())

GeneratorT a是单变压器(因为FreeT f是单体变压器,免费,当fFunctor时)。 这意味着我们可以通过使用lift调用基础 monad 来混合yield(在基础 monad 中是多态的)与特定于 monad 的调用。

我将为此示例定义一些假国际象棋移动:

data ChessMove = EnPassant | Check | CheckMate deriving (Read, Show)

现在,我将定义一个基于IO的国际象棋移动生成器:

import Control.Monad
import Control.Monad.Trans.Class
ioPlayer :: GeneratorT ChessMove IO r
ioPlayer = forever $ do
lift $ putStrLn "Enter a move:"
move <- lift readLn
yield move

这很容易! 我们可以使用runFreeT一次解包一个动作的结果,这只会要求玩家在绑定结果时输入一个动作:

runIOPlayer :: GeneratorT ChessMove IO r -> IO r
runIOPlayer p = do
x <- runFreeT p -- This is when it requests input from the player
case x of
Pure r -> return r
Free (move, p') -> do
putStrLn "Player entered:"
print move
runIOPlayer p'

让我们测试一下:

>>> runIOPlayer ioPlayer
Enter a move:
EnPassant
Player entered:
EnPassant
Enter a move:
Check
Player entered:
Check
...

我们可以使用Identitymonad 作为基本 monad 来做同样的事情:

import Data.Functor.Identity
type Free f r = FreeT f Identity r
runFree :: (Functor f) => Free f r -> FreeF f r (Free f r)
runFree = runIdentity . runFreeT

注意transformers-free包已经定义了这些(免责声明:我写了它,Edward 合并了它的功能,合并到free包中。 我只保留它用于教学目的,您应该使用free如果可能的话)。

有了这些,我们可以定义纯国际象棋移动生成器:

type Generator a r = Free ((,) a) r
-- or type Generator a = Free ((,) a)
purePlayer :: Generator ChessMove ()
purePlayer = do
yield Check
yield CheckMate
purePlayerToList :: Generator ChessMove r -> [ChessMove]
purePlayerToList p = case (runFree p) of
Pure _ -> []
Free (move, p') -> move:purePlayerToList p'
purePlayerToIO :: Generator ChessMove r -> IO r
purePlayerToIO p = case (runFree p) of
Pure r -> return r
Free (move, p') -> do
putStrLn "Player entered: "
print move
purePlayerToIO p'

让我们测试一下:

>>> purePlayerToList purePlayer
[Check, CheckMate]

现在,回答你的下一个问题,即如何在运行时选择基 monad。 这很简单:

main = do
putStrLn "Pick a monad!"
whichMonad <- getLine
case whichMonad of
"IO"     -> runIOPlayer ioPlayer
"Pure"   -> purePlayerToIO purePlayer
"Purer!" -> print $ purePlayerToList purePlayer

现在,事情变得棘手了。 您实际上想要两个玩家,并且您希望为这两个玩家独立指定基本 monad。 为此,您需要一种方法来检索每个玩家的一个动作作为IOmonad 中的动作,并保存玩家移动列表的其余部分以供以后使用:

step
:: GeneratorT ChessMove m r
-> IO (Either r (ChessMove, GeneratorT ChessMove m r))

Either r部分是以防玩家用完移动(即到达他们的monad的末尾),在这种情况下,r是块的返回值。

这个函数特定于每个 monadm,所以我们可以键入类它:

class Step m where
step :: GeneratorT ChessMove m r
-> IO (Either r (ChessMove, GeneratorT ChessMove m r))

让我们定义一些实例:

instance Step IO where
step p = do
x <- runFreeT p
case x of
Pure r -> return $ Left r
Free (move, p') -> return $ Right (move, p')
instance Step Identity where
step p = case (runFree p) of
Pure r -> return $ Left r
Free (move, p') -> return $ Right (move, p')

现在,我们可以将游戏循环编写为如下所示:

gameLoop
:: (Step m1, Step m2)
=> GeneratorT ChessMove m1 a
-> GeneratorT ChessMove m2 b
-> IO ()
gameLoop p1 p2 = do
e1 <- step p1
e2 <- step p2
case (e1, e2) of
(Left r1, _) -> <handle running out of moves>
(_, Left r2) -> <handle running out of moves>
(Right (move1, p2'), Right (move2, p2')) -> do
<do something with move1 and move2>
gameLoop p1' p2'

我们的main函数只是选择要使用的播放器:

main = do
p1 <- getStrLn
p2 <- getStrLn
case (p1, p2) of
("IO", "Pure") -> gameLoop ioPlayer purePlayer
("IO", "IO"  ) -> gameLoop ioPlayer ioPlayer
...

我希望这有所帮助。 这可能有点过分了(你可能会使用比生成器更简单的东西),但我想介绍一下很酷的Haskell习语,你可以在设计游戏时从中抽取。 除了最后几个代码块之外,我对所有代码块进行了类型检查,因为我无法想出一个合理的游戏逻辑来即时测试。

您可以了解有关免费 monads 和免费 monad 变压器的更多信息,如果这些示例还不够。

我的建议有两个主要部分:

  1. 跳过定义新类型类。
  2. 对现有类型类定义的接口进行编程。

对于第一部分,我的意思是您应该考虑创建类似

data Player m = Player { selectMove :: m Move }
-- or even
type Player m = m Move

第二部分的意思是使用MonadIOMonadState等类来保持Player值的多态性,并在组合所有玩家后仅在最后选择合适的 monad 实例。例如,您可能有

computerPlayer :: MonadReader GameState m => Player m
randomPlayer :: MonadRandom m => Player m
humanPlayer :: (MonadIO m, MonadReader GameState m) => Player m

也许你会发现还有其他你想要的球员。无论如何,这样做的重点是,一旦你创建了所有这些玩家,如果它们是如上所述的typeclass多态性,你可以选择一个特定的monad来实现所有必需的类,你就完成了。例如,对于这三个,您可以选择ReaderT GameState IO

祝你好运!

最新更新