计算器 abacus 技术文档之二----初步设计

﹥>﹥吖頭↗ 提交于 2020-03-01 08:30:56

=======================================

计算器 abacus 的下载地址:http://www.oschina.net/code/snippet_736932_13725

      如果你有关于 abacus 的问题或者建议,请发邮件至 zhoucosin@163.com。谢谢。

=======================================  

本节介绍一些问题以及如何设计计算器以解决这些问题。

程序的目标:

  • 支持四则混合运算 ok.
  • 支持数学函数,如三角函数、指对函数、组合数等 ok.
  • 支持符号常量,如圆周率、自然对数的底数等  ok.
  • 支持变量运算(并非符号计算) doing...
  • 支持表达式函数(即含有变量的表达式作为函数) doing...
  • 支持有控制流程的函数 wait for doing.
     首先确定程序的使用方式,目前只打算以命令行的方式运行程序,暂不考虑界面的问题,程序启动后,用户逐条输入表达式以计算其值,每计算完一个表达式并显示之后,程序将等待用户输入下一个表达式,直到用户输入"quit" 退出程序。

      表达式在本质上就是一个由运算符、运算数、标点符号这些表达式元素组成的序列,所以问题的关键在于解释这些序列的数学意义。

      首先需要从字符串形式的表达式中提取各个表达式元素(运算符、运算数、标点符号:主要是括号和逗号),并将这些表达式元素依次保存到一个线性的数据结构中去,这称为词法分析。在词法分析之前,应该进行一些预处理,比如检查括号是否匹配。而在词法分析之后也应有一些后处理,比如对标识符作进一步处理,词法分析器只能识别出标识符,它并不知道一个标识符到底是一个函数的名字还是一个符号常量,需要做相应的更换。

      经过词法分析之后,我们就得到了一个由表达式元素所组成的序列了,而表达式元素都是有相应的数学意义的,比如运算符有优先级,需要的运算数个数,以及最重要的计算函数指针这些属性,而运算数最主要的属性就是它所对应的数值了。比如表达式 1-2*(sin(pi/3))^2 在经过词法分析以后将被分割成如下的表达式元素序列:

                                            1  -  2  *  (  sin  (  pi  /  3  )  )  ^  2

     其中的各个部件都已经有了数学意义(这需要相应的表达式部件类的实现),比如运算符 * 具有属性: 字符串体:“*”,优先级:2, 所需运算数个数:2, 计算函数指针:(一个完成乘法运算的函数)。而最后的运算数2具有属性:字符串体:“2”,数值:2.

      这里有一个非常重要的设计,就是把数学函数当成运算符处理。这样做的理由是,函数运算符跟普通的四则混合运算符都能对给定的若干个实数,计算出一个结果来,这说明它们在本质上是一致的,你可以把加法 2+3 写成函数调用的形式 +(2,3),也可以把函数调用 pow(2,3) 按照二元运算符的惯例放在两个运算数之间: 2 pow 3,想必人们对于 2^3 这样的指数写法习以为常,那么这个 2 pow 3 不也是一脉相承的么?而对于一元函数也是一样,一元运算符负号放在运算符之前,那么 sin(3) 也可以写成 sin 3 的形式。总之,把函数运算符跟普通运算符统一处理,这是 abacus 在设计上的一大特色。

      接下来就要调整表达式的结构,传统的数学表达式其实是很不规范的,历史上加减乘除这些二元运算符最早发明,很自然的就把它们放在两个运算符之间了,但同样的形式对于非二元运算符就不适用了。又如,作为一元运算符的负号是放在运算数之前的,而阶乘符号却是放在运算数之后的,而函数运算符却采用了另外一种写法: opt(a1,a2,.....)。由此可见传统的数学表达式虽然对人来说是易于理解的,但却不利于计算机进行处理,在此我们需要用一种统一的观点来看待所有的运算符,即运算符的本质是一个映射,能对给定的若个数(输入)产生一个确定的结果(输出),有了这一点认识,我们就希望用一种统一的形式来刻画表达式。

      我们采用“运算符前置”的形式来规范数学表达式的结构,它把任何一个表达式放在一对小括号内部,而小括号内的第一个对象就是运算符,剩余的对象均是参与运算的运算数,例如 2+3 的规范形式为 (+ 2 3),而 sin(pi) 的规范式为 (sin pi),并且表达式可以任意嵌套,比如 5*(2-3) 的规范形式为 (* 5 (- 2 3)),作为一个更复杂的例子,一元二次方程x^2-3x+2=0的求根公式(正根)将表示为 (/ (+ (- (-3)) (sqrt (- ((^ (- 3) 2)) (* (* 4 1) 2)))) (* 2 1)) 。虽然这样的表达式对于人来讲是一种痛苦,但由于其格式的规范性,计算机是乐于处理这样的结构的。

      由传统形式向规范式转换的过程我们就称之为语法分析,如果有语法错误,在这个过程中将会被发现。在语法分析之前也应该会有一些预处理,例如在词法分析阶段并不能区分负号与减号,这需要在语法分析之前进行一点“有语法倾向”的检查纠正,然后根据括号的层次提升运算符的优先级。

     表达式在经过语法分析之后,就已经被转换为规范式了,这个时候进行计算就相当简单了,只要采用递归的方式,在表达式中查找主运算符(即优先级最低的运算符),然后检查剩余的各个运算对象,如果某个运算对象本身也是一个表达式,即含有子表达式,则先计算子表达式的值,最后计算整个表达式的值。 1-2*(sin(pi/3))^2 的计算过程如下:

转换为规范式:  (- 1 (* 2 (^ (sin (/ pi 3)) 2)))

计算过程:

      Step 1:     (- 1 (* 2 (^ (sin (/ pi 3)) 2)))

      Step2:     (- 1 (* 2 (^ (sin 1.047197551) 2)))

      Step3:     (- 1 (* 2 (^ 0.8660256282 2)))

      Step4:     (- 1 (* 2 0.7500010327))

      Step5:     (- 1 1.500002065)

      Step6:     -0.500002065

这样就完成了计算过程。在此处可以看到这个“规范式”带来的一个最大的好处:把语法分析跟递归计算分离开来了,如果不转换成这种规范式而使用传统的数学表达式,那么由于表达式的各种形式(分一元普通运算符,二元普通运算符,函数运算符.....),就只能在语法分析的过程中根据相应的运算符选择合适的形式来进行计算了。而现在引入了这种规范式,在计算过程中根本不用去关心这个运算符是什么类型的运算符。这说明,运算符前置表达式的引进,是 abacus 的另一个非常好的设计。

      至此大体设计基本上就完成了。这里有一个用脑图描述的计算器 abacus 的实现,一看就懂的:

http://www.mindomo.com/mindmap/abacus-5b6804e0f3fa466f96bc198c9b5d63d3

zhcosin

2012-10-29

     

   

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!