在运行时选择一个monad

我正在尝试在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上面。 例如,如果玩家1是人类,所以IO和玩家2是有状态的AI,所以在状态中,玩家1的第一步移动将具有IO Move类型。 那么玩家2将不得不采取类型IO GameState的结果状态并产生一个State IO Move类型的State IO Move ,而玩家1的下一步移动将是IO State IO Move ? 这看起来不正确。


这个问题有两个部分:

  • 如何将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, ())
    

    GeneratorT a是一个monad变换器(因为FreeT f是一个monad变换器,当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
    ...
    

    我们可以使用Identity monad作为基本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。 要做到这一点,你需要一种方法来从每个玩家身上检索一个动作,作为IO monad中的一个动作,并保存该玩家的其余动作列表供以后使用:

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

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

    这个函数是针对每个monad m ,所以我们可以输入class:

    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习语,你可以在设计游戏时进行抽样。 除了最后几个代码块之外,我对所有代码都进行了类型检查,因为我无法想出一个明智的游戏逻辑来进行即时测试。

    如果这些例子不足,你可以了解更多关于免费单子和免费单子变压器的信息。


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

  • 跳过定义一个新的类型类。
  • 编程到由现有类型类定义的接口。
  • 对于第一部分,我的意思是你应该考虑创建一个数据类型

    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
    

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

    祝你好运!

    链接地址: http://www.djcxy.com/p/58755.html

    上一篇: choosing a monad at runtime

    下一篇: How do you structure a stateful module in Haskell?