有兩個部分對這個問題:
- 如何搭配增量具體的單子輸入
- 單子獨立的下棋框架如何在運行時指定
具體的單子部分
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
這很容易!
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
...
我們可以做的,當你綁定的結果我們可以解開使用runFreeT
在同一時間,結果一招,這將只要求玩家輸入的舉動使用Identity
單子爲基數單子同樣的事情:
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
NoteThe 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
單子動作的一招,並保存玩家的舉動列表的其餘部分供以後:
step
:: GeneratorT ChessMove m r
-> IO (Either r (ChessMove, GeneratorT ChessMove m r))
的Either r
部分的情況下,玩家運行沒有動作(即到達他們的monad的末尾),在這種情況下r
是塊的返回值。
此功能是具體到每一個單子m
,所以我們可以鍵入類是:
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
函數只是選擇哪個playe rs使用:
main = do
p1 <- getStrLn
p2 <- getStrLn
case (p1, p2) of
("IO", "Pure") -> gameLoop ioPlayer purePlayer
("IO", "IO" ) -> gameLoop ioPlayer ioPlayer
...
我希望有所幫助。這可能有點過分(你可能會使用比發電機更簡單的東西),但我想給大家介紹一下酷炫的Haskell習語,你可以在設計遊戲時進行抽樣。除了最後幾個代碼塊之外,我對所有代碼都進行了類型檢查,因爲我無法想出一個明智的遊戲邏輯來進行即時測試。
如果這些示例不足,您可以瞭解有關free monads和free monad transformers的更多信息。
謝謝!這似乎是一個不錯的通用解決方案。一個問題:你將如何實施國家monad的步驟? – bhaskarama
我以前有四行,我想「我敢打賭,這是加百列的回答」:) –
@bhaskarama你不會!實際上你可以寫一些類型:'StateT s(Free ChessMove)r',使用'runStateT'將其轉換爲'Free ChessMove r',然後將其作爲「純粹」播放器傳遞。並非所有延伸玩家的monad都屬於基本monad。 –