9

Haskell を学ぼうとする Haskell でQuartoゲームを実装しています。昨年受講したコースの演習として、Python でゲームを既に実装しました。そのアイデアは、ランダム プレイヤー、初心者プレイヤー、ミニマックス プレイヤーの 3 人の異なる「AI」プレイヤーと一緒にゲームを実装することでした。ピース ロジックとボード ロジックは非常に簡単に実装できますが、プレイヤーを実装する必要がある段階に来ており、ゲーム ロジックが何も知る必要がないようにプレイヤーを最適に設計する方法を考えています。特定のプレーヤーについてですが、異なるモナドを使用できるようにします。

問題は、各プレイヤーが異なるモナドを必要とし、ランダムプレイヤーが State モナドまたは RandomState モナドのいずれかで動作する必要があることです。初心者のプレイヤーもおそらく何らかの形の状態を必要とし、ミニマックスのプレイヤーは状態または純粋のいずれかを使用できます (これにより、実装がはるかに遅くなり、実装が少し難しくなりますが、実行できます) さらに、「人間」が必要です。 " 人間から入力を得るために IO モナドで作業する必要があるプレーヤー。簡単な解決策の 1 つは、すべてを IO モナドに入れることですが、個々の設計がやや難しくなり、各プレーヤーの設計が必要以上に処理しなければならなくなると感じています。

私の最初の考えは次のようなものです:

class QuartoPlayer where
    place :: (Monad m) => QuartoPiece -> QuartoBoard -> m (Int, Int)
    nextPiece :: (Monad m) => QuartoBoard -> [QuartoPiece] -> m QuartoPiece

私は試していないので、これが機能するかどうかはわかりませんが、正しい方向に向かっており、設計が Haskell で理にかなっている場合は、入力をお願いします。

4

1 に答える 1

9

ここで起こっていることには 2 つの部分があります。1 つ目は、複数の異なるタイプのモナドを組み合わせて同時に実行する方法です — 指摘されているように、これはモナド トランスフォーマーを使用して実行できます — 2 つ目は、各プレイヤー タイプが必要なモナドだけにアクセスできるようにすることです。この後者の問題に対する答えは型クラスです。

まず、モナド変換子について調べてみましょう。モナド変換子は、追加の「内部」モナドを持つモナドのようなものです。この内部モナドが Identity モナド (基本的に何もしない) である場合、動作は通常のモナドと同じです。このため、モナドは通常、トランスフォーマーとして実装され、通常のモナドをエクスポートするために Identity にラップされます。モナドの変換バージョンは通常、型の末尾に T を追加するため、状態モナド変換子は StateT と呼ばれます。型の唯一の違いは、内部モナドState s avsの追加ですMonad m => StateT s m a。たとえば、状態として整数のリストが添付された IO モナドは type を持つことができますStateT [Int] IO

変圧器を適切に使用するには、さらに 2 つのポイントが必要です。まず、内部モナドに影響を与えるには、lift関数を使用します (既存のモナドトランスフォーマーが定義します)。Lift を呼び出すたびに、トランスフォーマーのスタックを 1 つ下に移動します。liftIOIO モナドがスタックの一番下にあるときの特別なショートカットです。(そして、あなたが期待するように 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
于 2013-09-18T11:02:57.200 に答える