【问题标题】:Haskell - Parsec with stateHaskell - 带状态的 Parsec
【发布时间】:2020-01-10 12:00:55
【问题描述】:

我有一个以String 格式保存游戏状态的文件。该字符串由一个移动列表组成,由, 分隔。从这个移动列表中,我必须重建游戏状态。因此,从概念上讲,对于我解析的每一步,我都想适当地修改游戏状态并将这个游戏状态传递给下一步的解析。从概念上讲,这可能等同于在开始时有一个空列表,并且每次移动都将解析移动到该列表。最后,您应该有一个包含所有已解析动作的列表。

我将下面的代码示例作为简化版本来解析字母字母并将它们推送到列表中。我想学习的核心概念是如何拥有一个初始状态,为每个解析周期传递它并使用解析返回最终状态。 someState 最初是空列表。

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

【问题讨论】:

    标签: parsing haskell parsec


    【解决方案1】:

    将“状态”合并到解析器中的最简单方法是根本不这样做。假设我们有一个井字棋盘:

    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 转换器,一个几乎等效的解决方案是创建一个带有标准State 层的ParsecT ... (State Board) monad 转换器堆栈。例如:

    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"
    

    【讨论】:

      【解决方案2】:

      您可能想使用 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 用于分割字符串。

      【讨论】:

      • 据我了解,您在此示例中没有使用 parsec 对吗?这将是要求之一,因为我的其他功能是使用这些解析器组合器实现的。
      • 嗯不,我不是,但你说:“我想学习的核心概念是如何拥有一个初始状态,在每个解析周期传递它并返回最终状态。”,我认为(而且我不是 haskell 专家)foldl 将是您想要的工具。您将希望在我发布的函数的最后一行使用 Parsec 以实际解析给定的移动字符串(如果您打算使用它?)
      • 嘿,刚刚做了一些研究,我想你可能想要sepBy。见:stackoverflow.com/questions/48048903/…hackage.haskell.org/package/parsec-3.1.14.0/docs/…
      猜你喜欢
      • 2017-07-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多