Выбор монады во время выполнения

Я пытаюсь написать двух игроков в Haskell, например, шашки. Я предполагаю наличие типов GameState, Move и функцию result :: GameState -> Move -> GameState, которая определяет правила игры. Я хочу иметь как человеческих, так и автоматизированных игроков, и я решил, что сделаю это, имея класс:

class Player p m | p -> m where
  selectMove :: p -> GameState -> m Move

где идея заключалась бы в том, что m может быть Identity для базового игрока AI, IO для человека, состояния для ИИ, который поддерживает состояние через ходы и т.д. Вопрос заключается в том, как перейти от этого к общему игровому циклу. Я полагаю, что могу определить что-то вроде:

Player p1 m1, Player p2 m2 => moveList :: p1 -> p2 -> GameState -> m1 m2 [Move]

монадическая функция, которая принимает игроков и начальное состояние и возвращает ленивый список ходов. Но затем поверх этого пусть скажу, что мне нужен текстовый интерфейс, который, скажем, позволяет сначала выбрать каждого игрока из списка возможностей, а затем заставляет игру играть. Поэтому мне нужно:

playGame :: IO () 

Я не вижу, как определить playGame, заданный moveList, в общем виде. Или мой общий подход не прав?

EDIT: размышляя об этом, я даже не вижу, как определить moveList выше. Например, если игрок 1 был человеком, поэтому IO и игрок 2 были ИО с состоянием, поэтому State, первый ход игрока 1 имел бы тип IO Move. Затем игроку 2 нужно было бы получить состояние типа IO GameState и произвести перемещение типа State IO Move, а следующий игрок 1 следующего типа будет иметь тип IO State IO Move? Это не выглядит правильно.

Ответ 1

В этом вопросе есть две части:

  • Как смешивать независимую от монады шахматную структуру с инкрементным входом, специфичным для монады.
  • Как указать часть, специфичную для монады, во время выполнения

Вы решаете прежнюю проблему с помощью генератора, который является частным случаем свободного монадного трансформатора:

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

Это было легко! Мы можем развернуть результат одним движением за раз, используя 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 определяют их уже (Отказ от ответственности: я написал его, и Эдвард объединил его функциональность, был объединен в пакет 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 и сохранить оставшуюся часть списка перемещения плеера позже:

step
 :: GeneratorT ChessMove m r
 -> IO (Either r (ChessMove, GeneratorT ChessMove m r))

Часть Either r находится в случае, если у игрока заканчиваются ходы (т.е. доходит до конца их монады), и в этом случае r является возвращаемым значением блока.

Эта функция специфична для каждой монады m, поэтому мы можем ввести класс it:

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, которые вы можете попробовать при разработке своей игры. Я печатал все, кроме последних нескольких блоков кода, так как я не мог придумать разумную логику игры для проверки на лету.

Вы можете узнать больше о свободных монадах и свободных монадных трансформаторах если этих примеров недостаточно.

Ответ 2

Мой совет состоит из двух основных частей:

  • Пропустить определение нового типа.
  • Программа для интерфейсов, определенных существующими классами типов.

В первой части я имею в виду, что вам следует подумать о создании типа данных, например

data Player m = Player { selectMove :: m Move }
-- or even
type Player m = m Move

Вторая часть означает использование таких классов, как MonadIO и MonadState, чтобы сохранить ваши значения Player полиморфными и выбрать соответствующий экземпляр монады только в конце после объединения всех игроков. Например, у вас может быть

computerPlayer :: MonadReader GameState m => Player m
randomPlayer :: MonadRandom m => Player m
humanPlayer :: (MonadIO m, MonadReader GameState m) => Player m

Возможно, вы обнаружите, что есть и другие игроки, которых вы хотите. Во всяком случае, дело в том, что как только вы создали всех этих игроков, если они являются полиморфными типами, как указано выше, вы можете выбрать определенную монаду, которая реализует все необходимые классы, и вы закончили. Например, для этих трех вы можете выбрать ReaderT GameState IO.

Удачи!