8

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

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。

4

3 回答 3

26

将标记解析为抽象语法树

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

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

这是一个很好的简单语法,因为你可以从第一个标记中看出它将是什么类型的 epression。(如果有一些更复杂的东西,比如+出现在数字之间,或者-用于减法和求反,您需要成功列表技巧,在 功能解析器中进行了解释。)

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

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

我已经命名了 an 的各个部分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

哎呀!太多的 let 子句模糊了意思,我们会E_ProdE_Let. 让我们解决这个问题!

处理这个问题的标准方法是编写一些组合器;与其在我们的定义中烦人地处理输入[String]s,不如定义一些方法来混淆解析器的输出(map)并将多个解析器组合成一个(lift)。

清理代码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]))

nonono,我什至看不懂!我们需要一个类型同义词:

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 语句的各个部分,提升忽略"="and的函数"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会有点长,感觉很乱。

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

应用函子是要走的路。记得我建议重构为

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

这看起来很像您的语法定义,因此很容易编写第一次工作的代码。这就是我喜欢编写解析器的方式。事实上,这就是我喜欢写很多东西的方式。您只需要定义fmap,<*>pure,所有简单的,并且不再重复liftP3liftP4

阅读有关应用函子的信息。他们真棒。

使 Parser 适用pure的提示:不更改列表。 <*>就像liftP2,但函数不是来自外部,它来自p1.

于 2012-10-04T02:27:34.430 回答
4

要开始使用 Haskell 本身,我建议向您学习 Haskell for Great Good!,一个写得很好的和有趣的指南。Real World Haskell是另一个经常被推荐的起点。

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

要开始在 Haskell 中进行解析,我认为您应该首先阅读Haskell 中的 monadic parsing,然后可能是这个 wikibook 示例,然后是parsec 指南

你的语法

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_-它们必须以大写字母开头。

(就我个人而言,我想立即将 s 转换为Integs Int,所以会这样做newtype Integ = Integ Int,并且可能会这样做,newtype Var = Var Char但这可能不适合你。)

一旦你完成了基本的 - digand var, and neg_,plus_in_我会使用 Applicative 接口来构建它们,所以例如你的expr解析器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 

所以几乎在所有时间里,你的 Haskell 代码都是干净的,并且与你得到的语法非常相似。

于 2012-10-03T16:23:04.527 回答
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
 ...

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

现在,怎么样evali?好吧,这将是一个类似的交易。如果 AST 是 a Leaf x,那么x是 a Int,你只需返回它。如果你有,比如说,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使用空变量映射调用。当evaluatesees时Let,它将变量添加到地图中。当它看到 时Var,它会在地图中查找名称。

至于首先将字符串解析为 AST,这又是一个完全不同的答案......

于 2012-10-03T16:28:22.957 回答