Haskell - Разбор c с состоянием - PullRequest
1 голос
/ 10 января 2020

У меня есть файл, в котором состояние игры сохраняется в формате String. Эта строка состоит из списка ходов, разделенных ,. Из этого списка ходов я должен восстановить состояние игры. Таким образом, концептуально, для каждого разбираемого мною хода я хочу соответствующим образом изменить игровое состояние и передать это игровое состояние для разбора следующего хода. Концептуально это может быть эквивалентно наличию пустого списка в начале и для каждого хода, составляющего проанализированный ход в этом списке. В конце у вас должен быть список всех проанализированных ходов.

Я сделал приведенный ниже пример кода в качестве упрощенной версии для разбора букв alfabeti c и pu sh в списке. Основная концепция, которую я хочу изучить, заключается в том, как получить начальное состояние, передать его для каждого цикла синтаксического анализа и вернуть конечное состояние, используя parse c. someState изначально пустой список.

parseExample :: State -> Parser [Char]
parseExample someState = do spaces 
                            c <- char 
                            c : someState
                            return someState

Ответы [ 2 ]

3 голосов
/ 10 января 2020

Самый простой способ включить «состояние» в синтаксический анализатор - это вообще не делать этого. Допустим, у нас есть доска ti c -ta c:

data Piece = X | O | N deriving (Show)
type Board = [[Piece]]

Для анализа списка ходов:

X11,O00,X01

в доску [[O,X,N],[N,X,N],[N,N,N]], представляющую игровое состояние:

 O | X |
---+---+---
   | X |
---+---+---
   |   |

мы можем отделить парсер, который просто генерирует список ходов:

data Move = Move Piece Int Int
moves :: Parser [Move]
moves = sepBy move (char ',')
  where move = Move <$> piece <*> num <*> num
        piece = X <$ char 'X' <|> O <$ char 'O'
        num = read . (:[]) <$> digit

от функций, восстанавливающих игровое состояние:

board0 :: Board
board0 = [[N,N,N],[N,N,N],[N,N,N]]

game :: [Move] -> Board
game = foldl' turn board0

turn :: Board -> Move -> Board
turn brd (Move p r c) = brd & ix r . ix c .~ p

и затем соедините их вместе в функции loadGame:

loadGame :: String -> Board
loadGame str =
  case parse moves "" str of
    Left err -> error $ "parse error: " ++ show err
    Right mvs -> game mvs

Это должно быть вашим go -решением для решения проблемы такого рода: сначала проанализируйте простую промежуточную форму без состояния, а затем обработайте эту промежуточную форму в «вычислении с учетом состояния».

Если вы действительно хотите создать состояние во время анализа, есть несколько способов сделать это. В данном конкретном случае, учитывая приведенное выше определение turn, мы можем выполнить синтаксический анализ непосредственно в Board, включив в синтаксический анализатор сложение из функции game:

moves1 :: Parser Board
moves1 = foldl' turn board0 <$> sepBy move (char ',')
  where move = Move <$> piece <*> num <*> num
        piece = X <$ char 'X' <|> O <$ char 'O'
        num = read . (:[]) <$> digit

, но это выиграло ' Слишком хорошо обобщать, если у вас есть несколько анализаторов, которые должны работать в одном базовом состоянии.

Чтобы на самом деле обработать состояние через набор анализаторов, вы можете использовать функцию "пользовательского состояния" в Parse c , Определите синтаксический анализатор с пользовательским состоянием Board:

type Parser' = Parsec String Board

, а затем синтаксический анализатор для одного хода, который изменяет пользовательское состояние:

move' :: Parser' ()
move' = do
  m <- Move <$> piece <*> num <*> num
  modifyState (flip turn m)
  where piece = X <$ char 'X' <|> O <$ char 'O'
        num = read . (:[]) <$> digit

Обратите внимание, что move' тип возвращаемого значения (), потому что его действие реализовано как побочный эффект для пользовательского состояния.

Теперь, просто анализ списка ходов:

moves' :: Parser' ()
moves' = sepBy move' (char ',')

сгенерирует окончательное состояние игры:

loadGame' :: String -> Board
loadGame' str =
  case runParser (moves' >> getState) [[N,N,N],[N,N,N],[N,N,N]] "" str of
    Left err -> error $ "parse error: " ++ show err
    Right brd -> brd

Здесь loadGame' запускает анализатор состояния пользователя с помощью moves', а затем использует вызов getState для извлечения окончательной доски.

Почти эквивалентное решение, поскольку ParsecT является монадным преобразователем, состоит в создании ParsecT ... (State Board) монадного трансформаторного стека со стандартным слоем State. Например:

type Parser'' = ParsecT String () (Control.Monad.State.State Board)

move'' :: Parser'' ()
move'' = do
  m <- Move <$> piece <*> num <*> num
  modify (flip turn m)
  where piece = X <$ char 'X' <|> O <$ char 'O'
        num = read . (:[]) <$> digit

moves'' :: Parser'' ()
moves'' = void $ sepBy move'' (char ',')

loadGame'' :: String -> Board
loadGame'' str =
  case runState (runParserT moves'' () "" str) board0 of
    (Left err, _)   -> error $ "parse error: " ++ show err
    (Right (), brd) -> brd

Однако оба эти подхода к построению состояния при разборе являются странными и нестандартными. Парсер, написанный в этой форме, будет сложнее понять и изменить, чем стандартный подход. Кроме того, предполагаемое использование для пользовательского состояния - поддерживать состояние, необходимое для синтаксического анализатора, чтобы решить, как выполнить фактический анализ. Например, если вы разбираете язык с приоритетом оператора Dynami c, вы можете сохранить текущий набор приоритетов операторов в качестве состояний, поэтому при разборе строки infixr 8 ** вы можете изменить состояние для правильного анализа последующих выражения. Использование пользовательского состояния для фактического построения результата анализа не предназначено для использования.

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

import Control.Lens
import Control.Monad
import Control.Monad.State
import Data.Foldable
import Text.Parsec
import Text.Parsec.Char
import Text.Parsec.String

data Piece = X | O | N deriving (Show)
type Board = [[Piece]]

data Move = Move Piece Int Int

-- *Standard parsing approach

moves :: Parser [Move]
moves = sepBy move (char ',')
  where move = Move <$> piece <*> num <*> num
        piece = X <$ char 'X' <|> O <$ char 'O'
        num = read . (:[]) <$> digit

board0 :: Board
board0 = [[N,N,N],[N,N,N],[N,N,N]]

game :: [Move] -> Board
game = foldl' turn board0

turn :: Board -> Move -> Board
turn brd (Move p r c) = brd & ix r . ix c .~ p

loadGame :: String -> Board
loadGame str =
  case parse moves "" str of
    Left err -> error $ "parse error: " ++ show err
    Right mvs -> game mvs

-- *Incoporate fold into parser

moves1 :: Parser Board
moves1 = foldl' turn board0 <$> sepBy move (char ',')
  where move = Move <$> piece <*> num <*> num
        piece = X <$ char 'X' <|> O <$ char 'O'
        num = read . (:[]) <$> digit

-- *Non-standard effectful parser

type Parser' = Parsec String Board

move' :: Parser' ()
move' = do
  m <- Move <$> piece <*> num <*> num
  modifyState (flip turn m)
  where piece = X <$ char 'X' <|> O <$ char 'O'
        num = read . (:[]) <$> digit

moves' :: Parser' ()
moves' = void $ sepBy move' (char ',')

loadGame' :: String -> Board
loadGame' str =
  case runParser (moves' >> getState) board0 "" str of
    Left err -> error $ "parse error: " ++ show err
    Right brd -> brd

-- *Monad transformer stack

type Parser'' = ParsecT String () (Control.Monad.State.State Board)

move'' :: Parser'' ()
move'' = do
  m <- Move <$> piece <*> num <*> num
  modify (flip turn m)
  where piece = X <$ char 'X' <|> O <$ char 'O'
        num = read . (:[]) <$> digit

moves'' :: Parser'' ()
moves'' = void $ sepBy move'' (char ',')

loadGame'' :: String -> Board
loadGame'' str =
  case runState (runParserT moves'' () "" str) board0 of
    (Left err, _)   -> error $ "parse error: " ++ show err
    (Right (), brd) -> brd

-- *Tests

main = do
  print $ loadGame   "X11,O00,X01"
  print $ loadGame'  "X11,O00,X01"
  print $ loadGame'' "X11,O00,X01"
0 голосов
/ 10 января 2020

Вы, вероятно, хотите использовать foldl (если я правильно понимаю ваш вопрос). Таким образом, вы получите такую ​​функцию, как:

module Main where

import Data.Text
import Data.String

main :: IO ()
main =
  putStrLn (show $ parseGameState "a, b, c")

data State = State deriving (Show)

parseGameState :: String -> [State]
parseGameState stateString = parsedState where
  parsedState = Prelude.foldl mkNewStateFromPreviousAndMove [] moves where
    moves = splitOn (fromString ",") (fromString stateString)
    mkNewStateFromPreviousAndMove oldStates move = oldStates ++ [newState previousState move] where
      previousState = Prelude.last oldStates
      newState previousState move = State

Что это делает:

Принимая строку перемещения CSV в качестве входных данных.

Затем эта строка разбивается на список строк перемещения.

Затем мы начинаем с пустого списка и складываем строки перемещения в этот список, применяя mkNewStateFromPreviousAndMove к каждому элементу в Перемещает список и последний элемент списка, который складывается из сгиба.

Обратите внимание, вам необходимо добавить следующие файлы deps в ваш файл package.yaml (если используется стек):

  • text

Этот пункт используется для разделения строк.

...