この質問には2つの部分があります。
- モナドに依存しないチェスプレイフレームワークとインクリメンタルモナド固有の入力を組み合わせる方法
- 実行時にモナド固有の部分を指定する方法
前者の問題は、フリーモナド変換子の特殊なケースであるジェネレーターを使用して解決します。
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
ですFunctor
)。yield
これは、を使用してベースモナドを呼び出すことにより、(ベースモナドでは多型である)モナド固有の呼び出しと混合できることを意味しlift
ます。
この例のために、いくつかの偽のチェスの動きを定義します。
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
簡単でした!を使用して、一度に1ムーブずつ結果をアンラップでき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
モナドをベースモナドとして使用して同じことを行うことができます。
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
さて、ここで物事がトリッキーになります。実際には2人のプレーヤーが必要であり、両方のベースモナドを個別に指定する必要があります。IO
これを行うには、モナドのアクションとして各プレーヤーから1つの動きを取得し、後で使用するためにプレーヤーの残りの動きリストを保存する方法が必要です。
step
:: GeneratorT ChessMove m r
-> IO (Either r (ChessMove, GeneratorT ChessMove m r))
このEither r
部分は、プレーヤーが移動を使い果たした場合(つまり、モナドの終わりに達した場合)です。この場合、r
はブロックの戻り値です。
この関数は各モナドに固有であるため、classitm
と入力できます。
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イディオムの一般的なツアーを提供したいと思いました。その場でテストするための賢明なゲームロジックを思い付くことができなかったため、最後のいくつかのコードブロックを除くすべてをタイプチェックしました。
これらの例では不十分な場合は、無料のモナドと無料のモナド変換子について詳しく知ることができます。