热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

lispscheme果壳_(Scheme(Lisp))解析表达式,就这么简单

什么是表达式?计算机如何表达和计算一个算术表达式?搞懂了这个问题,我们也就弄清楚了如何去解析一个算术表达式。甚至再多学一点,

什么是表达式?

计算机如何表达和计算一个算术表达式?

搞懂了这个问题,我们也就弄清楚了如何去解析一个算术表达式。甚至再多学一点,就可以写一个可以进行简单编程的计算器。

一个表达式就是一个顺序表list,是一个由操作数(Operand, 算数)和操作符(Operator, 算符)按某种顺序组合起来的序列。

表示一个算术表达式的方法通常有三种,前缀表达式,也称为波兰表达式、中缀表达式,也就是我们人类常用的这种,后缀表达式,也称为逆波兰表达式。前缀表达式,顾名思义,就是操作符置于其对应的操作数的前面的表达式,而后缀表达式则是操作符置于对应的操作数之后的表达式,中缀则不用说,学过小学数学的人都知道。

解析表达式,就是要将它的的各项和各个运算符逐个读入内存,然后构造某种数据结构(通常是表达式树)将它们关联和表达出来。

表达式树的特点是:叶节点上存放操作数,而内部节点存放操作符,一个操作符的子树是它的一个子表达式(求值之后便是它的操作数)。构建好表达式树之后,通过不同的遍历算法即可生成不同的表达式。对表达式树执行前序遍历,生成的是前缀表达式(操作符置于操作数之前);对表达式树执行中序遍历,生成的是中缀表达式;对表达式树执行后序遍历,生成的是后缀表达式;

下面是一些表达式树

和对应的中缀表达式

1 + 2

2 * (3 + (3 + 1))

(((1 / 2) * 3) - 4) + 5

等价的前缀表达式

+ 1 2

* 2 + 3 + 3 1

+ - * / 1 2 3 4 5

以及等价的后缀表达式

1 2 +

2 3 3 1 + + *

1 2 / 3 * 4 - 5 +

看上去前缀表达式和后缀表达式比较复杂,而中缀表达式则较为简单。但事实上,对前缀和后缀两种表达式的求值比对中缀表达式的求值简单得多。这是因为前置和后置的操作符保证了操作符与操作数结合的唯一性。总之,前缀表达式和后缀表达式是没有歧义的表达式,而中缀表达式则是一种有歧义的表达式,所以需要借助括号和算符结合的优先级来消除歧义。正所谓抓住了树根,你就抓住了一切;

抓住了叶子,你就得一片叶子。

在现代计算机中,中缀表达式通常用于编写人类可读的源代码,例如大多数的编程语言的表达式都是中缀表达式。不过,也有使用前缀表达式进行编程的语言,比如汇编语言。为什么是是这样呢?因为前缀表达式就像是一条条的命令,这样的命令适合机器一步步的执行,而中缀表达式更适合人类的书写和阅读习惯。

表达式的求值

求值(evaluate),是常见于编译原理和编程语言相关领域的一个术语,表示计算并返回一个表达式或一段程序的结果。

如果已经有了该表达式的表达式树,我们可以直接对表达式树进行求值。由于树本身的自相似结构,以及树的遍历通常要用到递归或栈,使用递归函数对表达式树进行求值是最简单的方式。该递归函数的特点是对每一个节点,判断它是操作数还是操作符。如果是操作数,直接返回;如果是操作符,那么就递归地计算它的各个子项(操作数),再用该操作符把各个操作数计算出来并返回。

当然,直接使用一个栈对表达式进行求值亦可,就像借助栈来对树进行非递归的遍历一样。下面讲述如何对前缀、中缀和后缀三种的四则运算表达式进行求值。在之后的表达式树的生成一节中,将会讲解如何解析一个表达式生成它的表达式树。

前缀和后缀表达式的求值算法比较简单,中缀则较为困难。首先来看下后缀表达式的求值过程,

;;; 判断 elem 是否在 elems 中

(define(in elem elems)

(if(atom? elems)

(eqv?elems elem)

(if(null?elems)

#f

(or(in elem (carelems))

(in elem (cdrelems))))))

(defineeval-suffix

(lambda(expr)

(defineoperator? (lambda(elem) (in elem '(+ - * /))))

(letloop ([expr expr][stack '()])

(if(null?expr)

(carstack)

(let([elem (carexpr)]

[expr (cdrexpr)])

(if(operator? elem)

(let([op (evalelem)]

[first-operand (cadrstack)]

[second-operand (carstack)]

[stack (cddrstack)])

(loop expr (cons(op first-operand second-operand) stack)))

(loop expr (conselem stack))))))))

(printf "~a\n" (eval-suffix '(1 2 +))) ; 3

(printf "~a\n" (eval-suffix '(2 3 3 1 + + *))) ; 14

(printf "~a\n" (eval-suffix '(1 2 / 3 * 4 - 5 +))) ; 5/2

看起来很简单,总结为一条规则就是:从左往右顺序遍历每个操作数和操作符,遇见操作数压栈,遇到操作符弾栈执行对应计算,并将结果压入栈中。最后,返回栈顶的值。

前缀表达式的求值算法与此类似,只不过遍历操作符和操作数的时候要从右往左地进行,从栈中取操作数的顺序也与后缀表达式的相反。因此,将对后缀表达式进行求值的函数稍微改一下,就可以用来对前缀表达式进行求值了。

;;; 判断 elem 是否在 elems 中

(define(in elem elems)

(if(atom? elems)

(eqv?elems elem)

(if(null?elems)

#f

(or(in elem (carelems))

(in elem (cdrelems))))))

(defineoperator? (lambda(elem) (in elem '(+ - * /))))

(define(gen-eval get-first-operand get-second-operand)

(lambda(expr)

(letloop ([expr expr][stack '()])

(if(null?expr)

(carstack)

(let([elem (carexpr)]

[expr (cdrexpr)])

(if(operator? elem)

(let([op (evalelem)]

[first-operand (get-first-operand stack)]

[second-operand (get-second-operand stack)]

[stack (cddrstack)])

(loop expr (cons(op first-operand second-operand) stack)))

(loop expr (conselem stack))))))))

(defineeval-suffix (gen-eval cadr car))

(defineeval-prefix (lambda(expr) ((gen-eval car cadr) (reverseexpr))))写到这里,放张图片,向用SublimeText的同学推荐一下我为SublimeText开发的括号高亮插件:RainbowBrackets,和Scheme语言的语法高亮、代码补全插件SublimeScheme. 功能不尽完善,还望同学多多提意见。

中缀表达式的求值

(defineeval-infix

(lambda(expr)

(defineeval-help

(lambda(handled-value next-expr)

(if(null?next-expr)

handled-value

(let([op (carnext-expr)])

(caseop

[(+-) (eval(listop handled-value (eval-infix (cdrnext-expr))))]

[(*/) (eval-help (eval(listop handled-value (eval-infix (cadrnext-expr))))

(cddrnext-expr))])))))

(if(atom? expr)

(evalexpr)

(eval-help (eval-infix (carexpr)) (cdrexpr)))))

在中缀表达式的求值过程中,基于处理算符优先级和对自相似性的考虑,用到了递归函数。

S表达式

讲完了前缀中缀后缀三种表达式,下面,我们来讲一种更简单也更通用的表达式,它叫S表达式。

其实S表达式也是一种前缀表达式,只不过,它的每一个表达式都被用括弧括起来,而这括弧巧妙地隔离了优先级、参数个数(它的参数个数不限)等表达式自带的刺,并提供了自相似性,甚至提供了作用域,使得尽管S表达式可以异常复杂,但对S表达式的求值却极其简单。而它的操作符也因此可以更平凡因而更有表达力,既可以是各种简单的算符,也可以是一个复杂的函数。它的操作数可以是一个简单的原子类型(数值或符号),也可以是另一个嵌套的表达式。通过层层嵌套,使得s表达式本身就具有一种强有力的表示数据的能力。可以说,一个S表达式就是一个表达式树数据结构的字符化表示。

下面是一个简单的S表达式

(+1 (*2 (/3 4)))

它也是lisp编程语言和它的各种方言的一个程序片段。

lisp是一门强大的函数式编程语言,但相较于C,C++,Javascript,Python等语言,lisp语言并不怎么流行。这种处境在很大程度上是由充斥于lisp程序里的层层嵌套的括号导致的。对大部分人来说,层层嵌套看上去很怪异,就像某种魔咒,令他们害怕而悄悄远离。我想,这跟一个在别人看来比较怪异的同学在班上的待遇没有区别。确实,

要比

简洁不少。可以说,lisp的括号是屏障,也是墙壁,屏蔽掉了污染,也挡住了很多人。

关于另外一些原因,我会说,我个人觉得成员运算符『.』和下标运算符『[]』是两个很重要的操作符,它们表意明确而且可以使程序变得简洁,而lisp的语法系统天然导致了在lisp语言中加入这两个运算符绝无可能。此外,可移植的Unix操作系统的出现使另外一门同样优秀的语言——C语言——占尽了先机,而此后流行的编程语言在某种程度上都有着C语言的影子。但尽管如此,依然有很多人喜欢lisp,因为它足够强大、足够优雅(尽管怪异),因为他们都喜欢孤独。

前面讲到,如果一个表达式的表达式树已经给出,那么直接对表达式树进行求值是最简单的求值方式。既然S表达式是表达式树的字符化表示,那么下面就我们就来看看,如何将前缀、中缀和后缀这三种表达式转化为可以存储为文本并且lisp语言可以直接进行计算的表达式树——S表达式。

表达式树的生成

表达式树的生成,其过程是模拟表达式的求值过程。

前缀和后缀

(define(parse-expr get-first-operand get-second-operand)

(lambda(expr)

(letloop ([expr expr][stack '()])

(if(null?expr)

(carstack)

(let([elem (carexpr)]

[expr (cdrexpr)])

(if(operator? elem)

(let([op elem]

[first-operand (get-first-operand stack)]

[second-operand (get-second-operand stack)]

[stack (cddrstack)])

(loop expr (cons(listop first-operand second-operand) stack)))

(loop expr (conselem stack))))))))

(defineparse-suffix (parse-expr cadr car))

(defineparse-prefix (lambda(expr) ((parse-expr car cadr) (reverseexpr))))

验证一下结果的正确性

(defineassert-prefix

(lambda(expr)

(let([s-expr (parse-prefix expr)]

[value (eval-prefix expr)])

(let([s-value (evals-expr)])

(printf "prefix expr: ~a = ~a\n" expr value)

(printf " S expr: ~a = ~a\n" s-expr s-value)

(assert (=s-value value))))))

(defineassert-suffix

(lambda(expr)

(let([s-expr (parse-suffix expr)]

[value (eval-suffix expr)])

(let([s-value (evals-expr)])

(printf "suffix expr: ~a = ~a\n" expr value)

(printf " S expr: ~a = ~a\n" s-expr s-value)

(assert (=s-value value))))))

中缀

(defineparse-infix

(lambda(expr)

(definecompose

(lambda(handled-expr next-expr)

(if(null?next-expr)

handled-expr

(let([op (carnext-expr)])

(caseop

[(+-) (listop handled-expr (parse-infix (cdrnext-expr)))]

[(*/) (compose (listop handled-expr (parse-infix (cadrnext-expr)))

(cddrnext-expr))])))))

(if(atom? expr)

expr

(compose (parse-infix (carexpr)) (cdrexpr)))))

可以看到,表达式树的生成过程与求值过程具有相同的递归结构和执行流。

再来看一个对支持幂运算的中缀表达式的解析

(definepaser-infix

(lambda(expr)

(definesplit-high-precedence-item

(lambda(expr ops)

(let([first (paser-infix (carexpr))]

[next-expr (cdrexpr)])

(if(null?next-expr)

(valuesfirst next-expr)

(let([op (carnext-expr)])

(if(in op ops)

(let-values (([next-item next-expr]

(split-high-precedence-item (cdrnext-expr) ops)))

(values(listop first next-item) next-expr))

(valuesfirst next-expr)))))))

(definecompose

(lambda(handled-expr next-expr)

(if(null?next-expr)

handled-expr

(let([op (carnext-expr)])

(caseop

[(+-) (listop handled-expr (paser-infix (cdrnext-expr)))]

[(*/ ^)

(let-values (([next-item next-expr]

(split-high-precedence-item (cdrnext-expr) '^)))

(compose (listop handled-expr next-item) next-expr))])))))

(if(atom? expr)

expr

(compose (paser-infix (carexpr)) (cdrexpr)))))

验证一下

既然可以借助栈直接对表达式进行求值,那么将表达式转化为表达式树的意义何在呢?这当然是非常有意义的事情,将表达式转换为表达式树之后,表达式树的可操作性和可计算性为表达式的优化(比如消除冗余的计算)提供了可能,这对于编译优化是非常重要的。



推荐阅读
author-avatar
等号拖轮_496
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有