Haskell解析器为AST数据类型,赋值

时间:2012-10-03 15:49:26

标签: parsing haskell abstract-syntax-tree

我一直在搜索互联网几天,试图回答我的问题,我终于承认失败了。
我得到了一个语法:

Dig ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Int ::= Dig | Dig Int
Var ::= a | b | ... z | A | B | C | ... | Z
Expr ::= Int | - Expr | + Expr Expr | * Expr Expr | Var | let Var = Expr in Expr

我被告知使用这种语法解析,评估和打印表达式 运算符* + -具有正常含义的位置 具体任务是编写函数parse :: String -> AST

以字符串作为输入,并在输入格式正确时返回抽象语法树(我可以认为是这样)。

我被告知我可能需要一个合适的数据类型,并且该数据类型可能需要从其他一些类派生。

按照示例输出
data AST = Leaf Int | Sum AST AST | Min AST | ...

此外,我应该考虑写一个函数
tokens::String -> [String]
将输入字符串拆分为令牌列表
解析应该用  ast::[String] -> (AST,[String])
其中输入是一个令牌列表,它输出一个AST,并解析子表达式我应该简单地递归使用ast函数。

我还应该创建一个printExpr方法来打印结果 printE: AST -> String
printE(parse "* 5 5")会产生"5*5""(5*5)" 以及评估表达式的功能
evali :: AST -> Int

我只想指出我可能从哪里开始的正确方向。我一般都对Haskell和FP知之甚少,并试图解决这个任务,我用Java做了一些字符串处理功能,这让我意识到我已经偏离轨道了。
所以在正确的方向上有一个小指针,也许是对AST应该是什么样子的解释 连续第三天仍然没有正在运行的代码,我非常感谢任何帮助我找到解决方案的尝试! 提前谢谢!
  修改

我可能不清楚: 我想知道我应该如何阅读并将输入字符串标记为制作AST。

3 个答案:

答案 0 :(得分:20)

将标记解析为抽象语法树

好的,我们来看看你的语法

Dig ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Int ::= Dig | Dig Int
Var ::= a | b | ... z | A | B | C | ... | Z
Expr ::= Int | - Expr | + Expr Expr | * Expr Expr | Var | let Var = Expr in Expr

这是一个很好的简单语法,因为你可以从第一个标记告诉它将是什么样的表现。 (如果有更复杂的内容,例如+介于数字之间,或-用于减法 以及否定,你需要成功列表技巧,解释如下 Functional Parsers。)

我们有一些示例原始输入:

rawinput = "- 6 + 45 let x = - 5 in * x x"

我从语法中理解的是"(- 6 (+ 45 (let x=-5 in (* x x))))", 我假设你把它标记为

tokenised_input' = ["-","6","+","4","5","let","x","=","-","5","in","*","x","x"]

符合语法,但你很可能已经

tokenised_input = ["-","6","+","45","let","x","=","-","5","in","*","x","x"]

更适合您的样本AST。我认为在你的语法之后命名你的AST是一个好习惯, 所以我要继续替换

data AST = Leaf Int | Sum AST AST | Min AST | ...

data Expr = E_Int Int | E_Neg Expr | E_Sum Expr Expr | E_Prod Expr Expr | E_Var Char 
                      | E_Let {letvar::Char,letequal:: Expr,letin::Expr}
 deriving Show

我已经命名了E_Let的位,以使它更清楚地代表它们。

编写解析函数

您可以使用isDigit添加import Data.Char (isDigit)来帮助:

expr :: [String] -> (Expr,[String])
expr [] = error "unexpected end of input"
expr (s:ss) | all isDigit s = (E_Int (read s),ss)
             | s == "-" = let (e,ss') = expr ss in (E_Neg e,ss') 
             | s == "+" = (E_Sum e e',ss'') where
                          (e,ss') = expr ss
                          (e',ss'') = expr ss'
            -- more cases

糟糕!太多让条款掩盖了意义, 我们将为E_Prod编写相同的代码,对E_Let编写更差的代码。 让我们解决这个问题吧!

处理这个问题的标准方法是编写一些组合器; 而不是通过我们的定义令人厌倦地线索输入[String],定义方法 搞乱解析器的输出(map)并结合使用 多个解析器合为一个(升力)。

清理代码1:map

首先我们应该定义pmap,我们自己的map函数等价物,这样我们才能pmap E_Neg (expr1 ss) 而不是let (e,ss') = expr1 ss in (E_Neg e,ss')

pmap :: (a -> b) -> ([String] -> (a,[String])) -> ([String] -> (b,[String]))
非诺,我甚至都看不懂!我们需要一个类型同义词:

type Parser a = [String] -> (a,[String])

pmap :: (a -> b) -> Parser a -> Parser b
pmap f p = \ss -> let (a,ss') = p ss 
                  in (f a,ss') 

但如果我这样做会更好

data Parser a = Par [String] -> (a,[String])

所以我可以做到

instance Functor Parser where
  fmap f (Par p) = Par (pmap f p)

我会留下那个让你知道你是否喜欢。

清理代码2:组合两个解析器

当我们有两个解析器运行时,我们还需要处理这种情况, 我们希望使用函数组合他们的结果。这称为将函数提升为解析器。

liftP2 :: (a -> b -> c) -> Parser a -> Parser b -> Parser c
liftP2 f p1 p2 = \ss0 -> let
              (a,ss1) = p1 ss0
              (b,ss2) = p2 ss1
              in (f a b,ss2)

或者甚至可能是三个解析器:

liftP3 :: (a -> b -> c -> d) -> Parser a -> Parser b -> Parser c -> Parser d

我会让你想到如何做到这一点。 在let语句中,您需要liftP5来解析let语句的各个部分, 提升忽略"=""in"的函数。你可以做

equals_ :: Parser ()
equals_ [] = error "equals_: expected = but got end of input"
equals_ ("=":ss) = ((),ss)
equals_ (s:ss) = error $ "equals_: expected = but got "++s

还有几个人可以帮忙解决这个问题。

实际上,pmap也可以被称为liftP1,但map就是这类事物的传统名称。

用漂亮的组合器重写

现在我们已准备好清理expr

expr :: [String] -> (Expr,[String])
expr [] = error "unexpected end of input"
expr (s:ss) | all isDigit s = (E_Int (read s),ss)
            | s == "-" = pmap   E_Neg expr ss
            | s == "+" = liftP2 E_Sum expr expr ss
            -- more cases

这一切都很好。真的,没关系。但liftP5会有点长,感觉很乱。

进一步清理 - 超好的应用方式

Applicative Functors是要走的路。 记得我建议重构为

data Parser a = Par [String] -> (a,[String])

所以你可以把它变成Functor的一个实例?也许你不想, 因为所有你获得的是一个新名称fmap,用于完美工作pmap和。{ 你必须处理所有那些使你的代码混乱的Par构造函数。 也许这会让你重新考虑;我们可以import Control.Applicative, 然后使用data声明,我们可以 定义<*>,这意味着then并使用<$>代替pmap*>含义 <*>-but-forget-the-result-of-the-left-hand-side所以你要写

expr (s:ss) | s == "let" = E_Let <$> var *> equals_ <*> expr <*> in_ *> expr

这看起来很像你的语法定义,因此编写第一次有效的代码很容易。 这就是我喜欢编写Parsers的方法。事实上,这就是我喜欢写很多东西的方式。 您只需要定义fmap<*>pure,所有这些都是简单的,而不是长期重复liftP3liftP4等。

阅读有关Applicative Functors的内容。他们很棒。

使Parser适用的提示:pure不会更改列表。 <*>liftP2类似,但该函数不是来自外部,而是来自p1的输出。

答案 1 :(得分:4)

为了从Haskell本身开始,我推荐Learn You a Haskell for Great Good!,这是一本写得很好,很有趣的指南。 Real World Haskell是另一个经常被推荐的起点。

编辑:解析的更基本的介绍是Functional Parsers。我想要如何用PHilip Wadler的成功列表替换失败。可悲的是,它似乎无法在线获取。

要开始在Haskell中解析,我认为你应该先阅读monadic parsing in Haskell,然后再阅读this wikibook example,然后再阅读parsec guide

你的语法

Dig ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
Int ::= Dig | Dig Int
Var ::= a | b | ... z | A | B | C | ... | Z
Expr ::= Int | - Expr | + Expr Expr | * Expr Expr | Var | let Var = Expr in Expr 

建议了一些抽象数据类型:

data Dig = Dig_0 | Dig_1 | Dig_2 | Dig_3 | Dig_4 | Dig_5 | Dig_6 | Dig_7 | Dig_8 | Dig_9
data Integ = I_Dig Dig | I_DigInt Dig Integ
data Var = Var_a | Var_b | ... Var_z | Var_A | Var_B | Var_C | ... | Var_Z
data Expr = Expr_I Integ 
          | Expr_Neg Expr
          | Expr_Plus Expr Expr
          | Expr_Times Expr Expr Var
          | Expr_Var Var
          | Expr_let Var Expr Expr 

这本质上是一个递归定义的语法树,不需要再制作另一个。 对于笨重的Dig_Integ_内容感到抱歉 - 他们必须以大写字母开头。

(就我个人而言,我希望立即将Integ转换为Int,这样就可以完成newtype Integ = Integ Int,并且可能已经完成了newtype Var = Var Char dig但这可能不适合你。)

完成基本操作后,varneg_plus_in_expr等等。使用Applicative界面进行构建,例如Expr的解析器expr = Expr_I <$> integ <|> Expr_Neg <$> neg_ *> expr <|> Expr_Plus <$> plus_ *> expr <*> expr <|> Expr_Times <$> times_ *> expr <*> expr <|> Expr_Var <$> var <|> Expr_let <$> let_ *> var <*> equals_ *> expr <*> in_ *> expr 就像

{{1}}

几乎所有的时间,你的Haskell代码都很干净,与你给出的语法非常相似。

答案 2 :(得分:1)

好的,所以看起来你正在尝试构建很多很多东西,但你并不确定它到底在哪里。我建议让AST的定义正确,然后尝试实施evali将是一个良好的开端。

你列出的语法很有趣......你似乎想输入* 5 5,但输出5*5,这是一个奇怪的选择。这真的应该是一元减号,而不是二元吗?同样地,* Expr Expr Var看起来可能是您打算输入* Expr Expr | Var ...

无论如何,对你的意思做出一些假设,你的AST看起来会像这样:

data AST = Leaf Int | Sum AST AST | Minus AST | Var String | Let String AST AST

现在,让我们尝试printE。它需要一个AST并给我们一个字符串。根据上面的定义,AST必须是五种可能的东西之一。你只需要找出每个人要打印的内容!

 printE :: AST -> String
 printE (Leaf  x  ) = show x
 printE (Sum   x y) = printE x ++ " + " ++ printE y
 printE (Minus x  ) = "-" ++ printE x
 ...

showInt变为String++将两个字符串连接在一起。我会让你解决剩下的功能。 (棘手的是,如果你想要它打印括号以正确显示子表达式的顺序......因为你的语法没有提到括号,我猜不是。)

现在,evali怎么样?嗯,这将是一个类似的交易。如果AST是Leaf x,则xInt,您只需返回该值。如果您有Minus x,那么x不是整数,它是AST,因此您需要将其转换为evali的整数。该函数看起来像

evali :: AST -> Int
evali (Leaf  x  ) = x
evali (Sum   x y) = (evali x) + (evali y)
evali (Minus x  ) = 0 - (evali x)
...

到目前为止效果很好。可是等等!看起来您应该能够使用Let来定义新变量,并稍后使用Var引用它们。那么,在这种情况下,您需要在某处存储这些变量。这将使功能变得更加复杂。

我的建议是使用Data.Map来存储变量名称列表及其对应的值。您需要将变量映射添加到类型签名。你可以这样做:

evali :: AST -> Int
evali ast = evaluate Data.Map.empty ast

evaluate :: Map String Int -> AST -> Int
evaluate m ast =
  case ast of
    ...same as before...
    Let var ast1 ast2 -> evaluate (Data.Map.insert var (evaluate m ast1)) ast2
    Var var           -> m ! var

所以evali现在只需使用空变量映射调用evaluate。当evaluate看到Let时,它会将变量添加到地图中。当它看到Var时,它会在地图中查找名称。

首先将解析一个字符串转换为AST,这又是一个完整的其他答案......