State Monad для сохранения доски в игре

У меня есть модуль Game, который определяет метод play, подобный этому play :: Board -> Move - > Board.

Я хочу использовать State Monad в другом модуле с именем Playing, который импортирует модуль Game, чтобы я мог вызывать play оттуда в цикле, пока Board не достигнет определенного состояния.

Я хочу вызвать метод с игрой с Board, который я получаю от монады состояний, а затем обновить значение монады состояния с помощью Board, возвращенного play.

Итак, пока цикл продолжается, я хочу получать ходы для применения к методу play и моему текущему состоянию.

Но я совершенно запутался в том, как добиться этого таким образом, чтобы модуль Game не знал, что я использую Монаду состояний.

Я просмотрел довольно много руководств и примеров (например, this, this, this и т. д.), и мне кажется, что я понимаю, каким образом State Monad применяется там, но, видимо, нет достаточно хорошо, чтобы абстрагироваться от этой конкретной реализации.

playing :: IO ()
playing = do
            putStr $ "The board looks like:"
            board <- get
            putStr $ showBoard  board 
            putStr $ "Indicate a move:"
            move <- getLine
            if validMove move then do
                newBoard <- play board (getMove move)
                if gameEnded newBoard then do
                    putStr $ "You win!" --stop the execution
                else do
                    put newBoard
            else do
               putStr $ "Invalid move"

Я хочу, чтобы playing был в цикле, пока он не получит конкретное Board, которое означает, что игра закончена. И используйте Монаду состояния для отправки текущего Board в play и другие методы в модуле Game, такие как gameEnded :: Board -> Bool, showBoard :: Board -> String и getMove :: String -> Move.

Любая помощь приветствуется


person moondaisy    schedule 02.06.2017    source источник
comment
Я не понимаю, о чем вы спрашиваете. Есть ли у вас проблемы с написанием конкретной функции?   -  person melpomene    schedule 02.06.2017
comment
@melpomene Я отредактировал свой вопрос, включив в него что-то более конкретное, у меня проблемы с написанием playing   -  person moondaisy    schedule 02.06.2017
comment
@moondaisy Откуда ваши Move? Ваша итерация будет основана на этом источнике. Вы хотите, чтобы playing проверял состояние доски и возвращал индикацию того, окончена ли игра, или playing просто обновит состояние доски, и вызывающий абонент решит, применить ли другой ход или прекратить?   -  person ryachza    schedule 02.06.2017
comment
@ryachza Я мог получить ходы, прочитав Char с консоли, используя. Думаю, я мог бы написать вызывающую программу, которая использует IO запускает playing, получает ходы и показывает результат Board, пока не решит, что игра окончена, в этом случае playing может вернуть индикатор.   -  person moondaisy    schedule 02.06.2017
comment
@ryachza Я просто включил более подробный псевдокод того, что playing должен делать, но я не знаю, как обрабатывать часть State Monad и какой должна быть фирма playing   -  person moondaisy    schedule 02.06.2017


Ответы (1)


Если вы поменяете порядок аргументов на play, у вас будет функция типа:

Move -> Board -> Board

Что вы можете частично применить с Move, чтобы получить один из типов:

Board -> Board

Вы можете преобразовать это в действие на State, используя modify :: (s -> s) -> State s () для изменения доски:

playing :: Move -> State Board ()
playing move = modify (play move)

Одно из решений - преобразователи монад - звучит страшнее, чем есть на самом деле. Вы можете использовать StateT вместо IO, StateT для сохранения состояния игры и IO, чтобы запрашивать у пользователя ходы. Например:

import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.State (evalStateT, gets, modify)

-- Get a move from the user.
getMove :: IO Move
getMove = do
  line <- getLine
  -- (Your implementation of parsing moves here.)

-- The initial state of the board.
initialBoard :: Board
initialBoard = -- ...

-- Whether the board represents a completed game.
boardDone :: Board -> Bool
boardDone board = -- ...

-- Main game loop.
gameLoop :: IO ()
gameLoop = evalStateT loop initialBoard
  where
    loop = do
      move <- lift getMove
      modify (play move)
      done <- gets boardDone
      if done then pure () else loop

Вы используете lift для преобразования обычного IO действия в StateT Board IO действие, modify :: (Monad m) => (s -> s) -> StateT s m () для изменения состояния и gets :: (Monad m) => (s -> a) -> StateT s m a для чтения свойств текущего состояния. loop либо хвостом зовет себя продолжить игру, либо возвращается.

Используя структуру и имена в отредактированном вопросе:

playing :: IO ()
playing = evalStateT loop initialBoard
  where

    loop :: StateT Board IO ()
    loop = do
      printBoard
      move <- lift promptMove
      modify (play move)
      ended <- gets gameEnded
      if ended
        then lift $ putStrLn "You win!"
        else loop

    printBoard :: StateT Board IO ()
    printBoard = do
      lift $ putStrLn $ "The board looks like:"
      board <- get
      lift $ putStrLn $ showBoard board

    promptMove :: IO Move
    promptMove = do
      putStr "Indicate a move: "
      move <- getLine
      if validMove move
        then pure $ getMove move
        else do
          putStrLn "Invalid move."
          promptMove
person Jon Purdy    schedule 02.06.2017
comment
Спасибо, определенно работает. Я не понимаю, что делает pure? - person moondaisy; 03.06.2017
comment
@moondaisy: с pure :: Applicative f => a -> f a pure x - это действие, которое просто возвращает x. Он имеет то же значение, что и return :: Monad m => a -> m a, который существует по историческим причинам: Applicative раньше не был суперклассом Monad. Вы можете использовать тот, который вам больше нравится. Я предпочитаю использовать pure, потому что он имеет более общий тип: pure работает, работает как для Applicative, так и для Monad, но return работает только для Monad. - person Jon Purdy; 03.06.2017