Самый простой способ включить «состояние» в синтаксический анализатор - это вообще не делать этого. Допустим, у нас есть доска 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"