Clojure解析数学表达式(优先级)
这两天大概写完了编译原理大作业。于是用一些博客来记下来里面用了哪些东西。其实里面根本没有用到什么高深的技巧,好像就是一路顺下来就写完了;唯一有些弯弯绕绕的地方就是解析数字的parser了。
这在之前也已经写过了,那是一个Lisp语言的解析数学表达式的东西,只是在Lisp中数学表达式的写法是自带语法树的,在对于普通的数学表达式的时候,由于有了优先级的限制,这个就显得很麻烦了。
数学表达式的文法
语法分析是在词法分析的基础上将单词的序列组合成表达式。里面会有定义好的一些文法的检查,类似于这样:
1 | F = num | FF |
说明F由两个F或一个num组成。那也就是说,F是一个由无穷多个num组成的文法。
如果用这样文法分析的做法来做的话,一个只有一个优先级符号(比如+
)的可求值的数学表达式可以做成下面这样的形式:
1 | numExpr = num symbol |
为了使表达式的求值有优先级,我们需要对上面的式子做一点改写:
1 | num0 = num1 symbol1 |
这个最重要的就是出现了一个求值的顺序,也即首先对优先级最低的做处理,然后处理两侧优先级更高的。
一些别的规则
比如说可能引入了括号,在括号内的东西需要优先计算,这在上面的文法表达式里面只要做稍微的改动就好了,也就是最后一句的处理:
1 | num3 = num | (num0) |
对于一个函数,他应该也是跟单纯数字是同一个级别的:
1 | num3 = num | func(num0) | (num0) |
另外我们需要明确的是,我们在这个函数中是有变量的,而且是个数不明的变量。因此这里我们应该返回的是一个函数,接受一定的参数并返回一定的结果。但是要返回一个函数的话其实还是很麻烦的,因为我们没有办法确定参数的数量和参数之间的关系。因此我们需要返回的其实是一个列表,列表里面包含了这个数字的解析式。因此对于一个表达式,我们不需要求它的结果,直接返回获得的解析树就好了。
Clojure实现
如果用C语言来做的话,可能很容易写出一系列的跟文法表达式一样的函数,但是我觉得那样实在是有些蠢。其实讲道理C语言也可以像这样写,而且很优雅,但是我看到的都是用各种一系列的switch-case
,至少并不好看。
因为上面的是几乎一样的结构,我想可以用一个数字来表示这个0123,然后写出num3的情况就好了。这个数字也就是对应运算符的优先级。
这两个函数,因为首先要给后面的东西留下解析的结果,也就是剩下留给它们解析的部分;然后是解析得到的解析式。解析数字和解析符号的函数返回值都是这个,在明确了这一点之后写起来就会好写一点了。
下面是解析数字函数,中间对于数字、函数、括号的处理部分略。
1 | (defn __parseNum |
解析符号的函数:
1 | (defn __parseOp |
最后再做一个针对这两个函数解析的接口:
1 | (defn parseNumExpr |
这就是一个解析数字的表达式的相应结构。
需要注意的是,由于这两个函数互相引用,所以在它们定义的前面应该首先说明一个__parseNum
或者__parseOp
才能避免报错。
整个的函数可以到这里看到。
另:只解析数字,不包含符号的版本可以在这里看到。
化简函数
对于这个函数的返回值的第二部分,可以看到,它是一个包含变量和数字的表达式。我们可以直接用它来计算,只要之后在环境中需要变量的值就可以,但是这样并不是最好的方案,因为有一些东西我们是可以提前算出来结果的,比如:
1 | '(+ (- "8" "4") (* "5" "v")) |
这个式子中(- 8 4)
是可以提前得到结果的,在求完后化简一下这样的表达式可以避免重复计算。
化简的函数如下:
1 | (defn simplify |
也就是这样的一个逻辑:
- 如果式子是列表,返回式子参数化简结果和式子的符号,如果式子参数都是数字,计算这个式子,返回数字。
- 如果式子是符号,返回符号本身
- 如果式子是数字,返回解析后的数字。