ここで起こっていることには 2 つの部分があります。1 つ目は、複数の異なるタイプのモナドを組み合わせて同時に実行する方法です — 指摘されているように、これはモナド トランスフォーマーを使用して実行できます — 2 つ目は、各プレイヤー タイプが必要なモナドだけにアクセスできるようにすることです。この後者の問題に対する答えは型クラスです。
まず、モナド変換子について調べてみましょう。モナド変換子は、追加の「内部」モナドを持つモナドのようなものです。この内部モナドが Identity モナド (基本的に何もしない) である場合、動作は通常のモナドと同じです。このため、モナドは通常、トランスフォーマーとして実装され、通常のモナドをエクスポートするために Identity にラップされます。モナドの変換バージョンは通常、型の末尾に T を追加するため、状態モナド変換子は StateT と呼ばれます。型の唯一の違いは、内部モナドState s a
vsの追加ですMonad m => StateT s m a
。たとえば、状態として整数のリストが添付された IO モナドは type を持つことができますStateT [Int] IO
。
変圧器を適切に使用するには、さらに 2 つのポイントが必要です。まず、内部モナドに影響を与えるには、lift
関数を使用します (既存のモナドトランスフォーマーが定義します)。Lift を呼び出すたびに、トランスフォーマーのスタックを 1 つ下に移動します。liftIO
IO モナドがスタックの一番下にあるときの特別なショートカットです。(そして、あなたが期待するように IO トランスフォーマーがないので、それは他のどこにもあり得ません。) したがって、状態部分から int リストの先頭をポップし、IO 部分を使用してそれを出力する関数を作成できます。
popAndPrint :: StateT [Int] IO Int
popAndPrint = do
(x:xs) <- get
liftIO $ print x
put xs
return x
2 番目のポイントは、実行中の関数の変換バージョン (スタック内のモナド変換子ごとに 1 つ) が必要だということです。したがって、この場合、GHCi で効果を実証するには、
> runStateT popAndPrint [1,2,3]
1
(1,[2,3])
これを Error モナドでラップすると、呼び出しrunErrorT $ runStateT popAndPrint [1,2,3]
などが必要になります。
これはモナドトランスフォーマーの簡単な紹介です。オンラインには他にもたくさんあります。
しかし、これは話の半分にすぎません。理想的には、さまざまなプレイヤー タイプが使用できるモナドを分離したいからです。トランスフォーマーのアプローチはすべてを提供するように見えますが、すべてのプレーヤーに IO へのアクセスを必要としているという理由だけで提供したくはありません。では、どのように進めるのですか?
プレイヤーのタイプが異なれば、Transformer スタックの異なる部分にアクセスする必要があります。そのため、プレーヤーごとに、そのプレーヤーが必要とするものだけを公開する型クラスを作成します。それぞれが別のファイルに入る可能性があります。例えば:
-- IOPlayer.hs
class IOPlayerMonad a where
getMove :: IO Move
doSomethingWithIOPLayer :: IOPlayerMonad m => m ()
doSomethingWithIOPLayer = ...
-- StatePlayer.hs
class StatePlayerMonad s a where
get :: Monad m => StateT s m s
put :: Monad m => s -> StateT s m ()
doSomethingWithStatePlayer :: StatePlayerMonad s m => m ()
doSomethingWithStatePlayer = ...
-- main.hs
instance IOPlayerMonad (StateT [Int] IO) where
getMove = liftIO getMoveIO
instance StatePlayerMonad s (StateT [Int] IO) where
get' = get
put' = put
これにより、アプリのどの部分が全体的な状態から何にアクセスできるかを制御でき、この制御はすべて 1 つのファイルで行われます。個々のパーツは、メイン状態の特定の実装とはまったく別のインターフェイスとロジックを定義します。
PS、上部にこれらが必要になる場合があります。
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
import Control.Monad.Trans.State
import Control.Monad.IO.Class
import Control.Monad
-
アップデート
この方法ですべてのプレイヤーに共通のインターフェースを維持できるかどうかについては、いくつかの混乱がありました。私はあなたができると主張します。Haskell はオブジェクト指向ではないため、自分たちでディスパッチ プラミングを少し行う必要がありますが、結果は同様に強力であり、詳細をより適切に制御でき、完全なカプセル化を実現できます。これをよりよく示すために、完全に機能するおもちゃの例を含めました。
ここでは、Play
クラスが多数の異なるプレーヤー タイプに単一のインターフェースを提供し、それぞれが独自のファイルにロジックを持ち、トランスフォーマー スタック上の特定のインターフェースのみを参照していることがわかります。このインターフェイスは Play モジュールで制御され、ゲーム ロジックはこのインターフェイスのみを使用する必要があります。
新しいプレーヤーを追加するには、新しいファイルを作成し、必要なインターフェイスを設計し、これを AppMonad に追加し、Player タイプの新しいタグで接続する必要があります。
すべてのプレーヤーは AppMonadClass クラスを介してボードにアクセスできることに注意してください。このクラスは、必要な共通インターフェイス要素を含めるように拡張できます。
-- Common.hs --
data Board = Board
data Move = Move
data Player = IOPlayer | StackPlayer Int
class Monad m => AppMonadClass m where
board :: m Board
class Monad m => Play m where
play :: Player -> m Move
-- IOPlayer.hs --
import Common
class AppMonadClass m => IOPLayerMonad m where
doIO :: IO a -> m a
play1 :: IOPLayerMonad m => m Move
play1 = do
b <- board
move <- doIO (return Move)
return move
-- StackPlayer.hs --
import Common
class AppMonadClass m => StackPlayerMonad s m | m -> s where
pop :: Monad m => m s
peak :: Monad m => m s
push :: Monad m => s -> m ()
play2 :: (StackPlayerMonad Int m) => Int -> m Move
play2 x = do
b <- board
x <- peak
push x
return Move
-- Play.hs --
import Common
import IOPLayer
import StackPlayer
type AppMonad = StateT [Int] (StateT Board IO)
instance AppMonadClass AppMonad where
board = return Board
instance StackPlayerMonad Int AppMonad where
pop = do (x:xs) <- get; put xs; return x;
peak = do (x:xs) <- get; return x;
push x = do (xs) <- get; put (x:xs);
instance IOPLayerMonad AppMonad where
doIO = liftIO
instance Play AppMonad where
play IOPlayer = play1
play (StackPlayer x) = play2 x
-- GameLogic.hs
import Play
updateBoard :: Move -> Board -> Board
updateBoard _ = id
players :: [Player]
players = [IOPlayer, StackPlayer 4]
oneTurn :: Player -> AppMonad ()
oneTurn p = do
move <- play p
oldBoard <- lift get
newBoard <- return $ updateBoard move oldBoard
lift $ put newBoard
liftIO $ print newBoard
oneRound :: AppMonad [()]
oneRound = forM players $ (\player -> oneTurn player)
loop :: AppMonad ()
loop = forever oneRound
main = evalStateT (evalStateT loop [1,2,3]) Board