使用megaparsec正确解析嵌套数据

问题描述

我试图使自己更熟悉megaparsec,并且遇到了一些存在问题。 标题中的“嵌套数据”是指我试图解析类型的事实,而该类型又可能包含其他类型。如果有人可以解释为什么这不符合我的预期,请随时告诉我。

我正在尝试解析类似于在Haskell中找到的类型。类型是基本类型IntBoolFloat或类型变量a(任何小写字母)。 我们还可以从类型构造函数(大写单词)(例如Maybe和类型参数(任何其他类型)构造代数数据类型。示例为Maybe aEither (Maybe Int) Bool。函数与右侧相关联,并由->构成,例如Maybe a -> Either Int (b -> c)。 N元元组是由,分隔并包含在()中的一系列类型,例如(Int,Bool,a)。可以将类型包装在括号中以提高其优先级(Maybe a)。还定义了单位类型()

我正在使用此ADT对此进行描述。

newtype Ident  = Ident String
newtype UIdent = UIdent String
data Type a
    = TLam a (Type a) (Type a)
    | TVar a Ident
    | TNil a
    | TAdt a UIdent [Type a]
    | TTup a [Type a]
    | TBool a
    | TInt a
    | TFloat a

我试图编写一个megaparsec解析器来解析此类类型,但是得到了意外的结果。我在下面附加了相关代码,之后我将尝试描述我的经验。

{-# LANGUAGE OverloadedStrings #-}
module Parser where

import AbsTinyCamiot

import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as Lexer
import Text.Megaparsec.Debug

import Control.Applicative hiding (many,some,Const)
import Control.Monad.Combinators.Expr
import Control.Monad.Identity
import Data.Void
import Data.Text (Text,unpack)

type Parser a = ParsecT Void Text Identity a

-- parse types

pBaseType :: Parser (Type ())
pBaseType = choice [
    TInt   () <$  label "parse int"           (pSymbol "Int"),TBool  () <$  label "parse bool"          (pSymbol "Bool"),TFloat () <$  label "parse float"         (pSymbol "Float"),TNil   () <$  label "parse void"          (pSymbol "()"),TVar   () <$> label "parse type variable" pIdent]

pAdt :: Parser (Type ())
pAdt = label "parse ADT" $ do
    con <- pUIdent
    variables <- many $ try $ many spaceChar >> pType
    return $ TAdt () con variables

pType :: Parser (Type ())
pType = label "parse a type" $ 
        makeExprParser 
        (choice [ try pFunctionType,try $ parens pType,try pTupleType,try pBaseType,try pAdt
                ]) 
        []--[[InfixR (TLam () <$ pSymbol "->")]]

pTupleType :: Parser (Type ())
pTupleType = label "parse a tuple type" $ do
    pSymbol "("
    fst  <- pType
    rest <- some (pSymbol "," >> pType)
    pSymbol ")"
    return $ TTup () (fst : rest)

pFunctionType :: Parser (Type ())
pFunctionType = label "parse a function type" $ do
    domain <- pType
    some spaceChar
    pSymbol "->"
    some spaceChar
    codomain <- pType
    return $ TLam () domain codomain

parens :: Parser a -> Parser a
parens p = label "parse a type wrapped in parentheses" $ do
    pSymbol "("
    a <- p
    pSymbol ")"
    return a

pUIdent :: Parser UIdent
pUIdent = label "parse a UIdent" $ do
    a <- upperChar
    rest <- many $ choice [letterChar,digitChar,char '_']
    return $ UIdent (a:rest)

pIdent :: Parser Ident
pIdent = label "parse an Ident" $ do
    a <- lowerChar
    rest <- many $ choice [letterChar,char '_']
    return $ Ident (a:rest)

pSymbol :: Text -> Parser Text
pSymbol = Lexer.symbol pSpace

pSpace :: Parser ()
pSpace = Lexer.space 
           (void spaceChar) 
           (Lexer.skipLineComment "--") 
           (Lexer.skipBlockComment "{-" "-}")

这可能会让人不知所措,所以让我解释一些关键点。我知道我在开括号上可以匹配很多不同的构造,所以我将这些解析器包装在try中,这样,如果它们失败了,我可以尝试下一个可能占用开括号的解析器。也许我过多使用了try?是否会严重影响性能以回溯?

我还试图通过定义一些术语和一个运算符表来制作一个表达式解析器。现在,您可以看到我已经注释掉了运算符(功能箭头)。 由于代码现在看起来很象,当我尝试解析函数类型时,我会无限循环。我认为这可能是由于以下事实:当我尝试解析函数类型(从pType调用)时,我立即尝试解析表示函数域的类型,该类型再次调用pType。我将如何正确执行此操作?

如果我决定改用运算符表,而不是对函数类型使用自定义解析器,则会使用错误的优先级来解析事物。例如,Maybe a -> b被解析为Maybe (a -> b),而我希望它被解析为(Maybe a) -> b有没有一种方法可以使用运算符表,并且使类型构造函数的绑定比功能箭头更紧密

最后,当我正在学习megaparsec时,如果有人看到任何误解或感到困惑/意外的事情,请告诉我。我已经阅读了this教程的大部分内容,以帮助我理解这一点。

请告知我为提高问题质量所做的任何修改!

解决方法

您的代码根本不处理优先级,因此,它使用循环的左递归。

在您的代码中举例说明left-recursionpFunctionType调用pType作为第一个动作,而调用pFunctionType作为第一个动作。显然这是一个循环。

关于优先级,我建议看一下“递归下降运算符解析”教程,通过Google的快速搜索可以发现其中有几个。不过,我可以在这里总结要点。我写一些代码。

{-# language OverloadedStrings #-}

import Control.Monad.Identity
import Data.Text (Text)
import Data.Void
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as Lexer

type Parser a = ParsecT Void Text Identity a

newtype Ident  = Ident String deriving Show
newtype UIdent = UIdent String deriving Show

data Type
    = TVar Ident
    | TFun Type Type       -- instead of "TLam"
    | TAdt UIdent [Type]
    | TTup [Type]
    | TUnit                -- instead of "TNil"
    | TBool
    | TInt
    | TFloat
    deriving Show

pSymbol :: Text -> Parser Text
pSymbol = Lexer.symbol pSpace

pChar :: Char -> Parser ()
pChar c = void (char c <* pSpace)

pSpace :: Parser ()
pSpace = Lexer.space
           (void spaceChar)
           (Lexer.skipLineComment "--")
           (Lexer.skipBlockComment "{-" "-}")

keywords :: [String]
keywords = ["Bool","Int","Float"]

pUIdent :: Parser UIdent
pUIdent = try $ do
    a <- upperChar
    rest <- many $ choice [letterChar,digitChar,char '_']
    pSpace
    let x = a:rest
    if elem x keywords
      then fail "expected an ADT name"
      else pure $ UIdent x

pIdent :: Parser Ident
pIdent = try $ do
    a <- lowerChar
    rest <- many $ choice [letterChar,char '_']
    pSpace
    return $ Ident (a:rest)

让我们在这里停止。

  • 我更改了Type中的构造函数的名称,以使其与在Haskell中的调用方式一致。我还删除了Type上的参数,以减少噪音,但您当然可以重新添加。
  • 请注意已更改的pUIdent和添加的keywords。通常,如果您想解析标识符,则必须将它们与关键字区别开。在这种情况下,Int可以解析为Int和大写标识符,因此我们必须指定Int不是标识符。 li>

继续:

pClosed :: Parser Type
pClosed =
      (TInt   <$  pSymbol "Int")
  <|> (TBool  <$  pSymbol "Bool")
  <|> (TFloat <$  pSymbol "Float")
  <|> (TVar   <$> pIdent)
  <|> (do pChar '('
          ts <- sepBy1 pFun (pChar ',') <* pChar ')'
          case ts of
            []  -> pure TUnit
            [t] -> pure t
            _   -> pure (TTup ts))

pApp :: Parser Type
pApp = (TAdt <$> pUIdent <*> many pClosed)
   <|> pClosed

pFun :: Parser Type
pFun = foldr1 TFun <$> sepBy1 pApp (pSymbol "->")

pExpr :: Parser Type
pExpr = pSpace *> pFun <* eof

我们必须根据绑定强度对运算符进行分组。对于每种强度,我们都需要有一个单独的解析函数来解析该强度的所有运算符。在这种情况下,我们具有pFunpApppClosed的粘合强度增强顺序。 pExpr只是一个包装器,用于处理顶级表达式,并处理前导空格并匹配输入的结尾。

在编写运算符解析器时,首先要确定的是封闭表达式组。封闭表达式由左右两侧的关键字或符号分隔。从概念上讲,这是“无限”的绑定强度,因为此类表达式之前和之后的文本根本不会更改其解析。

关键字和变量显然是封闭的,因为它们由单个标记组成。我们还有另外三种封闭的情况:单位类型,元组和带括号的表达式。由于所有这些都以(开头,因此我factor出来了。之后,我们将一个或多个类型用,分隔开来,我们必须分支已解析类型的数量。

优先级解析的规则是,在解析给定强度的运算符表达式时,在读取运算符之间的表达式时,我们总是调用 nextstrong 表达式解析器。

,是最弱的运算符,因此我们将函数称为第二弱的运算符pFun

pFun依次调用pApp,后者读取ADT应用程序,或回退到pClosed。在pFun中,您还可以看到权限关联性的处理,因为我们使用foldr1 TFun来组合表达式。在左关联的中缀运算符中,我们改用foldl1

请注意,解析器函数也始终会解析所有更强的表达式。因此,pFun在没有pApp时退回到->(因为sepBy1接受没有分隔符的情况),而pApp退回到{{1 }}没有ADT申请。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...