Ast*_*rno 3 parsing haskell parsec
我有一个文件,其中以String
格式保存游戏状态。该字符串由一系列移动组成,以,
. 从这个动作列表中,我必须重建游戏状态。因此,从概念上讲,对于我解析的每一步,我想适当地修改游戏状态并将这个游戏状态传递给下一步的解析。从概念上讲,这可能等同于在开始时有一个空列表,并且对于每个移动都将解析的移动到该列表。最后你应该有一个包含所有解析动作的列表。
我将下面的代码示例作为一个简化版本来解析 alfabetic 字母并将它们推送到列表中。我想学习的核心概念是如何拥有一个初始状态,为每个解析周期传递它并使用 parsec 返回最终状态。someState
最初是空列表。
parseExample :: State -> Parser [Char]
parseExample someState = do spaces
c <- char
c : someState
return someState
Run Code Online (Sandbox Code Playgroud)
将“状态”合并到解析器中的最简单方法就是根本不这样做。假设我们有一个井字棋盘:
data Piece = X | O | N deriving (Show)
type Board = [[Piece]]
Run Code Online (Sandbox Code Playgroud)
解析动作列表:
X11,O00,X01
Run Code Online (Sandbox Code Playgroud)
[[O,X,N],[N,X,N],[N,N,N]]
进入代表游戏状态的棋盘:
O | X |
---+---+---
| X |
---+---+---
| |
Run Code Online (Sandbox Code Playgroud)
我们可以分离解析器,它只生成一个移动列表:
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
Run Code Online (Sandbox Code Playgroud)
来自重新生成游戏状态的函数:
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
Run Code Online (Sandbox Code Playgroud)
然后在一个loadGame
函数中将它们连接在一起:
loadGame :: String -> Board
loadGame str =
case parse moves "" str of
Left err -> error $ "parse error: " ++ show err
Right mvs -> game mvs
Run Code Online (Sandbox Code Playgroud)
这应该是解决此类问题的首选解决方案:首先解析为简单的无状态中间形式,然后在“有状态”计算中处理该中间形式。
如果您确实想在解析期间建立状态,有多种方法可以实现。在这种特殊情况下,考虑到上面的定义turn
,我们可以Board
通过将函数中的折叠合并game
到解析器中来直接解析为 a :
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
Run Code Online (Sandbox Code Playgroud)
但如果您有多个需要在单个底层状态上操作的解析器,那么这并不能很好地概括。
要实际通过一组解析器线程化状态,您可以使用 Parsec 的“用户状态”功能。定义一个带有用户状态的解析器Board
:
type Parser' = Parsec String Board
Run Code Online (Sandbox Code Playgroud)
然后是修改用户状态的单个移动的解析器:
move' :: Parser' ()
move' = do
m <- Move <$> piece <*> num <*> num
modifyState (flip turn m)
where piece = X <$ char 'X' <|> O <$ char 'O'
num = read . (:[]) <$> digit
Run Code Online (Sandbox Code Playgroud)
请注意,move'
的返回类型是()
因为它的操作是作为用户状态的副作用实现的。
现在,简单地解析一系列动作:
moves' :: Parser' ()
moves' = sepBy move' (char ',')
Run Code Online (Sandbox Code Playgroud)
将生成最终的游戏状态:
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
Run Code Online (Sandbox Code Playgroud)
在这里,loadGame'
在用户状态上运行解析器moves'
,然后使用getState
调用来获取最终的板。
由于是 monad 变压器,因此几乎等效的解决方案ParsecT
是创建ParsecT ... (State Board)
具有标准层的 monad 变压器堆栈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
Run Code Online (Sandbox Code Playgroud)
然而,这两种在解析时建立状态的方法都很奇怪且不标准。以这种形式编写的解析器比标准方法更难理解和修改。此外,用户状态的预期用途是维护解析器决定如何执行实际解析所需的状态。例如,如果您正在解析具有动态运算符优先级的语言,您可能希望将当前的运算符优先级集保留为状态,因此当您解析行时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"
Run Code Online (Sandbox Code Playgroud)