我试图使自己对megaparsec更加熟悉,并且我遇到了一些存在问题。标题中的“嵌套数据”是指我试图解析类型的事实,而类型又可能包含其他类型。如果有人可以解释为什么这种行为不符合我的预期,请不要犹豫告诉我。
我正在尝试解析类似于在Haskell中找到的类型。类型或者是基本类型Int
,Bool
,Float
或类型变量a
(任何字小写)。我们还可以从类型构造函数(大写单词)(例如Maybe
和类型参数(任何其他类型))构造代数数据类型。例子是Maybe a
和Either (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时,如果有人看到任何误解或奇怪/意外的事情,请告诉我。我已经阅读了本教程的大部分内容,以使我了解到这一点。
请告知我为提高问题质量所做的任何修改!
您的代码根本不处理优先级,因此,它使用循环的左递归。
在代码中举一个左递归的例子,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
我们必须根据绑定强度对运算符进行分组。对于每种强度,我们都需要有一个单独的解析函数来解析该强度的所有运算符。在这种情况下,我们有pFun
,pApp
并且pClosed
按强度递增顺序排列。pExpr
只是一个处理顶级表达式的包装程序,它负责处理前导空格并匹配输入的末尾。
编写运算符解析器时,首先要确定的是封闭表达式组。封闭表达式由左右两侧的关键字或符号分隔。从概念上讲,这是“无限”的绑定强度,因为此类表达式之前和之后的文本完全不会更改其解析。
关键字和变量显然是封闭的,因为它们由单个标记组成。我们还有另外三种封闭的情况:单位类型,元组和带括号的表达式。由于所有这些开始用的(
,我因素这一点。在那之后,我们将一个或多个类型分隔开,,
并且必须分支已解析类型的数量。
优先级解析的规则是,在解析给定强度的运算符表达式时,当读取运算符之间的表达式时,我们总是调用下一个更强大的表达式解析器。
,
是最弱的运算符,因此我们将函数称为第二个最弱的运算符pFun
。
pFun
依次调用pApp
,它读取ADT应用程序,或者回退到pClosed
。在其中,pFun
您还可以看到正确的关联性的处理,这是我们foldr1 TFun
用来组合表达式的方式。在左关联的中缀运算符中,我们改为使用foldl1
。
请注意,解析器函数也始终会解析所有更强的表达式。因此pFun
,pApp
在没有时使用->
(因为sepBy1
接受没有分隔符的情况),而pApp
在pClosed
没有ADT应用程序时使用。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句