簡體   English   中英

Haskell - 帶狀態的 Parsec

[英]Haskell - Parsec with state

我有一個文件,其中以String格式保存游戲狀態。 該字符串由招式,通過分隔列表, 從這個動作列表中,我必須重建游戲狀態。 因此,從概念上講,對於我解析的每一步,我想適當地修改游戲狀態並將這個游戲狀態傳遞給下一步的解析。 從概念上講,這可能等同於在開始時有一個空列表,並且對於每個移動都將解析的移動到該列表。 最后你應該有一個包含所有解析動作的列表。

我將下面的代碼示例作為一個簡化版本來解析 alfabetic 字母並將它們推送到列表中。 我想學習的核心概念是如何擁有一個初始狀態,為每個解析周期傳遞它並使用 parsec 返回最終狀態。 someState最初是空列表。

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

將“狀態”合並到解析器中的最簡單方法是根本不這樣做。 假設我們有一個井字棋盤:

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

這應該是此類問題的首選解決方案:首先解析為簡單的無狀態中間形式,然后在“有狀態”計算中處理該中間形式。

如果你真的想在解析過程中建立狀態,有幾種方法可以做到。 在這種特殊情況下,鑒於上述turn的定義,我們可以通過將game函數中的折疊合並到解析器中來直接解析為Board

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

但是如果您有多個解析器需要對單個底層狀態進行操作,這將不會很好地概括。

要通過一組解析器實際線程化一個狀態,您可以使用 Parsec 的“用戶狀態”功能。 定義一個具有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是一個 monad 轉換器,是創建一個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

然而,這兩種在解析時建立狀態的方法都是奇怪的和非標准的。 以這種形式編寫的解析器將比標准方法更難理解和修改。 此外,用戶狀態的預期用途是維護解析器決定如何執行實際解析所必需的狀態。 例如,如果您正在解析具有動態運算符優先級的語言,您可能希望將當前的運算符優先級集保持為狀態,因此在解析中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"

您可能想使用 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 文件中(如果使用堆棧):

  • 文本

這個 dep 用於拆分字符串。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM