2010-11-06 74 views
8

我对Haskell相对较新,主要编程背景来自OO语言。我正在尝试用解析器编写一个解释器来编写简单的编程语言。到目前为止,我的翻译处于一个我感到满意的状态,但我正在与解析器稍微挣扎。在Haskell中解析一个简单的解释器

下面是一段代码,我有问题

data IntExp 
= IVar Var 
| ICon Int 
| Add IntExp IntExp 
deriving (Read, Show) 

whitespace = many1 (char ' ') 

parseICon :: Parser IntExp 
parseICon = 
    do x <- many (digit) 
    return (ICon (read x :: Int)) 

parseIVar :: Parser IntExp 
parseIVar = 
    do x <- many (letter) 
    prime <- string "'" <|> string "" 
    return (IVar (x ++ prime)) 

parseIntExp :: Parser IntExp 
parseIntExp = 
    do x <- try(parseICon)<|>try(parseIVar)<|>parseAdd 
    return x 

parseAdd :: Parser IntExp 
parseAdd = 
    do x <- parseIntExp 
    whitespace 
    string "+" 
    whitespace 
    y <- parseIntExp 
    return (Add x y) 

runP :: Show a => Parser a -> String -> IO() 
runP p input 
    = case parse p "" input of 
     Left err -> 
     do putStr "parse error at " 
      print err 
     Right x -> print x 

语言稍微复杂一些,但这足以说明我的问题。

所以在IntExp类型中,ICon是一个常量,而IVar是一个变量,但现在出现问题。例如,这成功运行

runP parseAdd “5 + 5”

其给出(添加(图标5)(图标5)),这是预期的结果。高德使用时出现问题,而不是图标,例如

runP parseAdd“N + M”

这将导致程序出错了说有一个意外的“N”,其中数字预期。这导致我相信parseIntExp没有按照我的意图工作。我的意图是,它会尝试解析一个ICon,如果失败了,那么试着解析一个IVar等等。

因此,我认为问题存在于parseIntExp中,或者我缺少parseIVar和parseICon中的某些内容。

我希望我已经提供了足够的有关我的问题的信息,我已经清楚了。

感谢您给我的任何帮助!

回答

13

你的问题实际上是在parseICon

parseICon = 
    do x <- many (digit) 
    return (ICon (read x :: Int)) 

many组合子匹配零个或多个出现,所以它通过匹配零位,那么当read失败可能接替死去的“M”。


虽然我在这,因为你是新来的Haskell,这里的一些不请自来的建议:

  • 不要使用虚假的括号。 many (digit)应该是many digit。这里的括号只是分组的东西,它们不是功能应用所必需的。

  • 您不需要做ICon (read x :: Int)。数据构造函数ICon只能使用Int,所以编译器可以自己计算出你的意思。

  • 你不需要围绕parseIntExp前两个选项try,因为它代表 - 有没有输入,将导致任何一个失败之前消耗一些输入。他们会立即失败(不需要try),或者他们在匹配单个字符后会成功。

  • 在解析之前先标记第一个标记通常是个好主意。与语法同时处理空白是令人头疼的事情。

  • Haskell通常使用($)运算符来避免括号。它只是函数应用程序,但优先级很低,因此many1 (char ' ')可以写为many1 $ char ' '

同样,在做这样的事情是多余的和不必要的:

parseICon :: Parser IntExp 
parseICon = 
    do x <- many digit 
    return (ICon (read x)) 

当你正在做的是将一个普通函数解析器的结果,你可以只使用fmap

parseICon :: Parser IntExp 
parseICon = fmap (ICon . read) (many digit) 

他们是完全一样的东西。如果您导入Control.Applicative模块,您可以使事情看起来更好,该模块可为您提供fmap的运营商版本,称为(<$>),另一个运营商(<*>)可让您对多个参数的功能执行相同的操作。还有运营商(<*)(*>)分别丢弃右值或左值,在这种情况下,您可以在丢弃结果(例如空格等)时解析某些内容。

这是你的代码的轻微修改后的版本与某些应用上述建议和其他一些次要的风格调整:

whitespace = many1 $ char ' ' 

parseICon :: Parser IntExp 
parseICon = ICon . read <$> many1 digit 

parseIVar :: Parser IntExp 
parseIVar = IVar <$> parseVarName 

parseVarName :: Parser String 
parseVarName = (++) <$> many1 letter <*> parsePrime 

parsePrime :: Parser String 
parsePrime = option "" $ string "'" 

parseIntExp :: Parser IntExp 
parseIntExp = parseICon <|> parseIVar <|> parseAdd 

parsePlusWithSpaces :: Parser() 
parsePlusWithSpaces = whitespace *> string "+" *> whitespace *> pure() 

parseAdd :: Parser IntExp 
parseAdd = Add <$> parseIntExp <* parsePlusWithSpaces <*> parseIntExp 
+3

camccann的回答非常好。一些更进一步的技巧...“Lexing”和空白处理通常通过Parsec.Token和Parsec.Language模块完成。这些词法分析器的风格是相当习惯的 - 如果你从http://legacy.cs.uu.nl/daan/parsec.html得到Parsec源代码,有一些简单的例子,例如Henk的代码,你可以从中复制代码。令牌模块还为您提供了更好的数字解析器,因此您可以避免使用许多数字然后读取。此外,适用于Parsec以获取(<$>)&(<*>)符号的实例仅适用于版本3.0和更高版本。 – 2010-11-06 09:04:51

+0

非常感谢您的回答和建议。看起来它会解决我的问题,我应该能够改善我的编码风格。干杯! – Josh 2010-11-06 10:59:14

+0

在'parseICon'示例中,我更喜欢'ICon。阅读=“多位数”选择,因为它更清晰。 – fuz 2010-11-07 11:43:25

1

我也是新来的Haskell,只是想知道:

将parseIntExp曾经使它parseAdd?

似乎ICon或IVar在达到'parseAdd'之前总是会被解析。

例如runP parseIntExp “3 + M”

会尝试parseICon,并获得成功,给

(图标3),而不是(添加(图示3)(伊瓦尔·M))

很抱歉,如果我是这里很愚蠢,我只是不确定。

+0

是的,你是对的。我可能应该在我的回答中提及,对于这种简单的情况,最简单的方法可能是使用类似'sepBy'组合器的东西。另外,欢迎来到Haskell和堆栈溢出! – 2010-11-08 03:32:14