使用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, digitChar, 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时,如果有人看到任何误解或奇怪/意外的事情,请告诉我我已经阅读了教程的大部分内容,以使我了解到这一点。

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

安德拉斯·科瓦奇斯(AndrásKovács)

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

在代码中举一个左递归的例子pFunctionType将调用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, digitChar, char '_']
    pSpace
    return $ Ident (a:rest)

让我们在这里停止。

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

继续:

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

我们必须根据绑定强度对运算符进行分组。对于每种强度,我们都需要有一个单独的解析函数来解析该强度的所有运算符。在这种情况下,我们有pFunpApp并且pClosed按强度递增顺序排列。pExpr只是一个处理顶级表达式的包装程序,它负责处理前导空格并匹配输入的末尾。

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

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

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

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

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

请注意,解析器函数也始终会解析所有更强的表达式。因此pFunpApp在没有时使用->(因为sepBy1接受没有分隔符的情况),而pApppClosed没有ADT应用程序时使用。

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

来自分类Dev

解析嵌套数组数据

来自分类Dev

嵌套数组未返回正确的数据

来自分类Dev

使用JQuery解析JSON嵌套数组

来自分类Dev

使用Swift解析嵌套JSON数据的正确方法是什么?

来自分类Dev

嵌套数组解析

来自分类Dev

如何使用SparkR取消嵌套数据?

来自分类Dev

使用 Solr 索引嵌套数据

来自分类Dev

如何用megaparsec正确解析缩进的块?

来自分类Dev

在graphql中嵌套数据的正确方法是什么?

来自分类Dev

获取嵌套数据并使用嵌套DTO深入DTO

来自分类Dev

使用EF Code First,如何包括嵌套的嵌套数据

来自分类Dev

绑定嵌套数据

来自分类Dev

如何使用javascript正确遍历嵌套数组。

来自分类Dev

如何使用嵌套数组将JSON解析为对象

来自分类Dev

如何在Android上使用Json解析嵌套数组

来自分类Dev

使用VBA和JSON解析嵌套数组

来自分类Dev

使用对象映射器解析字典的嵌套数组

来自分类Dev

使用可解码(嵌套数组)解析不同的 JSON

来自分类Dev

如何使用 JavaScript 解析嵌套数组中的 JSON 值

来自分类Dev

AngularJS在嵌套数组中插入数据并使用特定的嵌套数组对象进行排序

来自分类Dev

嵌套数组的GSON解析

来自分类Dev

嵌套数组的GSON解析

来自分类Dev

如何从嵌套数组中提取JSON数据?使用Android

来自分类Dev

使用Go访问嵌套数组和对象中的数据

来自分类Dev

使用Golang将嵌套数据插入BigQuery

来自分类Dev

如何使用嵌套数据angularjs和rails?

来自分类Dev

Firebase使用“引用”嵌套数据:true而不是数组

来自分类Dev

Firebase和使用Swift读取嵌套数据

来自分类Dev

使用SASS的嵌套数据切换