【问题标题】:choosing a monad at runtime在运行时选择一个 monad
【发布时间】:2012-10-14 11:37:26
【问题描述】:

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

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]

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

playGame :: IO () 

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

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

【问题讨论】:

    标签: haskell functional-programming monads


    【解决方案1】:

    这个问题分为两部分:

    • 如何将独立于 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 转换器,免费的,而 f 是一个 Functor)。这意味着我们可以通过使用 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]
    

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

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

    现在,事情变得棘手了。你实际上想要两个玩家,并且你想独立地为他们指定基本单子。为此,您需要一种方法来检索每个玩家的一个动作作为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 习语进行总体介绍,您可以在设计游戏时从中采样。我对除了最后几个代码块之外的所有代码块进行了类型检查,因为我无法提出合理的游戏逻辑来进行动态测试。

    如果这些示例还不够,您可以了解有关 free monadsfree monad transformers 的更多信息。

    【讨论】:

    • 谢谢!这似乎是一个非常好的通用解决方案。一个问题:您将如何为 State monad 实施 step?
    • 在我想“我敢打赌这是加布里埃尔的答案”之前,我写了四行:)
    • @bhaskarama 你没有!您实际上会写一些类型的东西:StateT s (Free ChessMove) r,使用runStateT 将其转换为Free ChessMove r,然后将其作为“纯”播放器传递。并非所有扩展播放器的 monad 都属于基本 monad。
    • 真的吗?但是玩家的状态如何在动作中传递呢?
    • @bhaskarama 抱歉,我的回复有误。我忘记了你的有状态玩家仍然需要与另一个玩家互动才能决定他们的行动。这种交互只能在IO monad 中完成。改变两个玩家两个交互的 monad 的唯一方法是改变每个 step 函数目标的基本 monad。
    【解决方案2】:

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

    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
    

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

    祝你好运!

    【讨论】:

    • 谢谢。考虑到这些玩家,我仍然看不到如何编写如上所述的整个游戏循环。即,我将如何根据用户输入确定每个玩家的类型,然后进行游戏?
    • @bhaskarama 在实现了几个多态播放器之后,您选择一个单一的单态类型并将它们全部专门用于该类型。然后游戏循环也使用该单态类型。
    • 好的,如果这是一个基本问题,很抱歉,但是这种专业化如何运作?例如,如果在 IO、Identity 中存在玩家,以及在单子状态缓存中缓存移动结果的一个有状态玩家,以及随着时间调整其游戏风格的另一个有状态玩家,比如 State Style。在这种情况下,单一的单态类型是什么?如何将玩家专门用于该类型?
    • @bhaskarama 正如我在回答中所说:与其使用IO MoveIdentity MoveState Style Move,不如使用MonadIO m =&gt; m MoveMonad m =&gt; m MoveMonadState Style m =&gt; m Move;那么实现所有这些接口的一种单态类型是(例如)StateT Style IO Move
    • 好的,有道理。与 Gabriel 的解决方案相比,我想这个限制是您必须提前知道要使用的单子;例如,如果我现在想使用不同类型的状态编写播放器,我必须更改主循环。但它肯定更简单。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-03-15
    • 2015-08-23
    • 2017-11-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多