在运行时选择monad

bha*_*ama 11 monads haskell functional-programming

我正在尝试在Haskell中编写一个双人游戏,比如跳棋.我设想有类型GameState,Moveresult :: GameState -> Move -> GameState定义游戏规则的功能.我想拥有人工和自动播放器,我想我会通过一个类型类来做到这一点:

class Player p m | p -> m where
  selectMove :: p -> GameState -> m Move
Run Code Online (Sandbox Code Playgroud)

其中的想法是m可以是基本AI玩家的身份,人类的IO,动态的AI状态,等等.问题是如何从这些到整个游戏循环.我想我可以定义类似的东西:

Player p1 m1, Player p2 m2 => moveList :: p1 -> p2 -> GameState -> m1 m2 [Move]
Run Code Online (Sandbox Code Playgroud)

一个monadic函数,它接收玩家和初始状态,并返回懒惰的动作列表.但最重要的是,我想要一个基于文本的界面,比如说,允许首先从一系列可能性中选择每个玩家,然后让游戏进行播放.所以我需要:

playGame :: IO () 
Run Code Online (Sandbox Code Playgroud)

我看不出如何以通用方式定义给定moveList的playGame.或者我的总体方法不对?

编辑:进一步思考它,我甚至没有看到如何定义上面的moveList.例如,如果玩家1是人类,那么IO和玩家2是有状态的AI,那么State,玩家1的第一步就是类型IO Move.然后玩家2必须采取结果状态IO GameState并产生类型的移动State IO Move,玩家1的下一步移动将是类型IO State IO Move?这看起来不对.

Gab*_*lez 11

这个问题分为两部分:

  • 如何将monad独立的国际象棋游戏框架与增量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, ())
Run Code Online (Sandbox Code Playgroud)

GeneratorT a是monad变压器(因为FreeT f是monad变压器,免费,何时f是a Functor).这意味着我们可以yield通过使用lift调用基本monad 来混合(在基本monad中是多态的)和monad特定的调用.

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

data ChessMove = EnPassant | Check | CheckMate deriving (Read, Show)
Run Code Online (Sandbox Code Playgroud)

现在,我将定义一个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
Run Code Online (Sandbox Code Playgroud)

那很简单!我们可以使用一次移动一次移动结果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'
Run Code Online (Sandbox Code Playgroud)

我们来测试一下:

>>> runIOPlayer ioPlayer
Enter a move:
EnPassant
Player entered:
EnPassant
Enter a move:
Check
Player entered:
Check
...
Run Code Online (Sandbox Code Playgroud)

我们可以使用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
Run Code Online (Sandbox Code Playgroud)

注意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'
Run Code Online (Sandbox Code Playgroud)

我们来测试一下:

>>> purePlayerToList purePlayer
[Check, CheckMate]
Run Code Online (Sandbox Code Playgroud)

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

main = do
    putStrLn "Pick a monad!"
    whichMonad <- getLine
    case whichMonad of
        "IO"     -> runIOPlayer ioPlayer
        "Pure"   -> purePlayerToIO purePlayer
        "Purer!" -> print $ purePlayerToList purePlayer
Run Code Online (Sandbox Code Playgroud)

现在,这里的事情变得棘手.你实际上想要两个玩家,你想要为它们两个独立指定基础monad.要做到这一点,你需要一种方法从每个玩家中检索一个移动作为IOmonad中的动作,并保存玩家移动列表的其余部分以供日后使用:

step
 :: GeneratorT ChessMove m r
 -> IO (Either r (ChessMove, GeneratorT ChessMove m r))
Run Code Online (Sandbox Code Playgroud)

Either r部分用于玩家用完动作(即到达其monad的末尾),在这种情况下,r该块是块的返回值.

这个函数特定于每个monad m,所以我们可以输入class:

class Step m where
    step :: GeneratorT ChessMove m r
         -> IO (Either r (ChessMove, GeneratorT ChessMove m r))
Run Code Online (Sandbox Code Playgroud)

让我们定义一些实例:

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')
Run Code Online (Sandbox Code Playgroud)

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

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'
Run Code Online (Sandbox Code Playgroud)

我们的main功能只选择使用哪些玩家:

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

我希望有所帮助.这可能有点过于杀戮(你可能会使用比生成器更简单的东西),但我想在你设计你的游戏时提供一些很酷的Haskell成语.我对除了最后几个代码块之外的所有代码块进行了类型检查,因为我无法想出一个合理的游戏逻辑来动态测试.

如果这些例子不够,你可以了解更多关于免费monad免费monad变换器的信息.


Dan*_*ner 6

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

  1. 跳过定义新类型的类.
  2. 编程到现有类型类定义的接口.

对于第一部分,我的意思是你应该考虑创建一个类似的数据类型

data Player m = Player { selectMove :: m Move }
-- or even
type Player m = m Move
Run Code Online (Sandbox Code Playgroud)

第二部分意味着使用类MonadIO和类MonadState保持您的Player值多态,并在组合所有玩家后仅在结尾选择合适的monad实例.例如,你可能有

computerPlayer :: MonadReader GameState m => Player m
randomPlayer :: MonadRandom m => Player m
humanPlayer :: (MonadIO m, MonadReader GameState m) => Player m
Run Code Online (Sandbox Code Playgroud)

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

祝好运!