热门标签 | 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)))))

验证一下

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



推荐阅读
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 本文介绍了2015年九月八日的js学习总结及相关知识点,包括参考书《javaScript Dom编程的艺术》、js简史、Dom、DHTML、解释型程序设计和编译型程序设计等内容。同时还提到了最佳实践是将标签放到HTML文档的最后,并且对语句和注释的使用进行了说明。 ... [详细]
  • java程序设计试题_《Java语言程序设计》期末考试模拟试题——填空题和编程题...
    一、根据题意,填写出空格中的内容Java平台包括三个技术方向,其中J2ME代表____________、J2SE代表___________、J2EE代表 ... [详细]
  • WPF之Binding初探
      初学wpf,经常被Binding搞晕,以下记录写Binding的基础。首先,盗用张图。这图形象的说明了Binding的机理。对于Binding,意思是数据绑定,基本用法是:1、 ... [详细]
  • 浅解XXE与Portswigger Web Sec
    XXE与PortswiggerWebSec​相关链接:​博客园​安全脉搏​FreeBuf​XML的全称为XML外部实体注入,在学习的过程中发现有回显的XXE并不多,而 ... [详细]
  • 获取时间的函数js代码,js获取时区代码
    本文目录一览:1、js获取服务器时间(动态)2 ... [详细]
  • 作用域链迷惑性代码vara100;functiontest(){console.log(a);}functiontestFun(){vara200;test();}不假思索的想到出 ... [详细]
  • 详解 Python 的二元算术运算,为什么说减法只是语法糖?[Python常见问题]
    原题|UnravellingbinaryarithmeticoperationsinPython作者|BrettCannon译者|豌豆花下猫(“Python猫 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
  • 本文讨论了编写可保护的代码的重要性,包括提高代码的可读性、可调试性和直观性。同时介绍了优化代码的方法,如代码格式化、解释函数和提炼函数等。还提到了一些常见的坏代码味道,如不规范的命名、重复代码、过长的函数和参数列表等。最后,介绍了如何处理数据泥团和进行函数重构,以提高代码质量和可维护性。 ... [详细]
  • Java SE从入门到放弃(三)的逻辑运算符详解
    本文详细介绍了Java SE中的逻辑运算符,包括逻辑运算符的操作和运算结果,以及与运算符的不同之处。通过代码演示,展示了逻辑运算符的使用方法和注意事项。文章以Java SE从入门到放弃(三)为背景,对逻辑运算符进行了深入的解析。 ... [详细]
  • Python基础知识:注释、输出和input交互
    本文介绍了Python基础知识,包括注释的使用、输出函数print的用法以及input函数的交互功能。其中涉及到字符串和整数的类型转换等内容。 ... [详细]
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社区 版权所有