这个问题有两个部分:
- 如何将独立于 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
是一个单子变压器,免费的,什么时候f
是 a Functor
)。这意味着我们可以混合yield
(在基本 monad 中是多态的),通过使用lift
来调用基本 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
部分是为了防止玩家用尽所有动作(即到达他们的单子的末尾),在这种情况下,这r
是块的返回值。
这个函数是特定于每个 monadm
的,所以我们可以输入 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 习语进行总体介绍,你可以在设计游戏时从中取样。我对除了最后几个代码块之外的所有代码块进行了类型检查,因为我无法提出合理的游戏逻辑来进行动态测试。
如果这些示例还不够,您可以了解有关免费 monad和免费 monad 转换器的更多信息。