1.1编程元素
一门高效的编程语言,通过它不仅能够让计算机执行任务,它还提供一个框架,让我们能够描述计算过程。所以,当我们介绍一门语言时,我们将重点关注这门语言将简单计算过程组合成复杂计算过程的方式。任何一门编程语言通过以下三种方式来实现这一需求:
原子表达式,一门语言最基本的表达元素。
组合方式,组合最基本的表达元素
抽象方式,将组合元素命名和复用
当编程时,我们处理两种类型的元素:计算过程和计算数据(稍后我们将发现,它们的区别其实并不明显,下面我们用简称“过程”来表示“计算过程”,“数据”来表示“计算数据”)。通俗的来讲,数据就是我们要计算的“东西”,过程是关于计算规则的描述。任何编程语言都含有原数据,原过程,以及将它们进行组合和抽象的方式。本章我们将通过一些简单的数值类型数据来介绍过程的构建。在后续章节中,我们将看到这些过程一样能够操作组合数据。
1.1.1 表达式
与诸如scheme这样的解释器进行一些简单的交互是开始编程之旅的好办法。想象一下,你坐在电脑前,输入一个表达式,然后解释器将它的计算结果打印出来。
先输入一个数值类型的表达式,如果你用lisp语言,当输入一个数字:486。解释器将打印486。数值类型表达式和最基本的过程表达式(如:+或*)组成组合表达式时表示的是过程如何操作数字,例如:
(+ 137 349) = 486
(- 1000 334) = 666
(* 5 99) = 495
(/ 10 5) = 2
(+ 2.7 10) = 12.7
像上面这样,将一系列表达式放在一组括号中用来表示一个过程实例,叫做组合式(由多个表达式组成,以 (+ 137 349)为例,“+”是一个过程表达式,137数值表达式,349也是)。最左边的元素叫做操作符,其他元素叫做操作数。表达式的值等于以操作数作为参数执行操作符。
将操作符放在最左边的形式成为前缀式,因为这种形式跟我们经常用的数学的表示形式不一样,看着不太习惯。但是,前缀式也有它的优势。首先,它能够方便的输入多个参数,向下面这样:
(+ 21 35 12 7) =75
(* 25 4 12) =1200
由于操作符在最左边,所有的组合元素都在一对括号内,所以它可读性好。
其次,它可以嵌套,一个组合式的元素也是组合式:
(+ (* 3 5) (- 10 6))=19
从理论上来说嵌套的深度是不受限制的,lisp的解释器可以处理任何复杂的表达式。下面这个相对简单的表达式对于我们来说还不够清晰
(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6))
但是解释器能够很快的给出答案57。如果把它写成如下形式可读性会好很多
(+ (* 3
(+ (* 2 4)
(+ 3 5)))
(+ (- 10 7)
6))
在这种形式中操作符纵向对齐。缩进清楚的展示出表达式的结构。
无论表达式多复杂,解释器的处理都是基于这样一个流程:从终端读取表达式,计算表达式,打印结果,这种处理模式称为(read-eval-print loop)。可以发现并不需要明确的打印指令,解释器都会打印结果。
1.1.2 命名与环境
编程语言另一个重要的方面是它给计算对象命名的方式。我们将用名称标识数值,这个数值就是一个对象。在scheme中,我们用define来给计算对象命名。输入
(define size 2)
将解释器与名称size数值2关联。一旦将名称size与数值2关联,我们可以通过名称来引用数值2:
(* 5 size) = 10
如下是更多的关于define的例子:
(define pi 3.14159)
(define radius 10)
(* pi (* radius radius))
314.159
(define circumference (* 2 pi radius))
circumference
62.8318
define是最基本和简单的抽象方式,它让我们能够通过简单的名字引用组合操作,如上例中的circumference的使用方式。总的来说,计算对象可以有非常复杂的结构,如果每次使用的时候都要记住其中的细节就太麻烦了。事实上,复杂的程序都是由构建过程,执行过程,计算对象组成。解释器之所以能够方便的构造按序执行的程序,是因为随着程序越来越大,可以创建更多的名称-对象的关联。由于这个特性,lisp程序可以由大量相对简单的过程组成。这让程序能够增量式开发,并且方便测试。
为了让数值与符号关联,并且在稍后能够使用他们,解释器必去维护一段内存来保存名称-对象的关联。这段内存称为环境(更准确的来说是全局环境,稍后我们将看到一个计算过程可能引用不同的环境)
1.1.3 组合式求值
本章主要介绍过程性思维,解释器通过以下步骤进行组合式求值:
1.计算组合式的子表达式的值
2.将其它子表达式(操作数)作为参数对最左边的子表达式(操作符)进行求值
以上简单的规则体现出一些重点。首先,为了求出组合表达式的值,需先求出子表达式的值。所以,求值的规则就是递归:在这个过程中,需要引用规则自身。
可以看出递归多么简洁的表达除这种深层次嵌套的组合式,另一方面,也被视为一个复杂过程。例如计算
(* (+ 2 (* 4 6))
(+ 3 5 7))
需要将这个计算规则应用与四种不同的组合式。我们可以将这个过程表示成一个树的结构,如图1.1。每一个表达式被表示成一个节点,每个节点由它的操作符和操作数的分支组成。叶子节点要么是数值要么是操作符。将计算过程表示成树的形式,操作数的值从叶子节点一层一层向上计算出来,在更高的级别进行组合。我们可以看到,递归是一种非常有高效的处理这种分层的树状对象的方法。事实上,这种自底向上的求值规则是求树和(tree accumulation 我也不知道怎么翻译-_-||)的一种方法。
Figure 1.1:
Tree representation, showing the value of each subcombination.
其次,重复执行第一步将我们带到元表达式(也就是说它的元素都不是组合表达式,例如数字,内置的操作符等等),元表达式定义如下:
1.数值名称的值就是它的名字本身,如:486的值就是486本身
2.内置操作符的值就是机器指令序列对应的操作。
3.其它名称的值在环境中可以找到。
我们可以发现,规则2是规则3的特例,例如:+或*都在全局环境中,它们关联的机器指令序列即使它们的“值”。环境非常重要,它决定了符号在表达式中的值。在交互式语言中,如lisp,单纯的求表达式的值而不提供任何环境信息是没有意义的,例如:(+ x 1),环境可以提供符号x的值(甚至符号+的值)。在第三章,在程序执行的过程中进行求值时,环境将扮演重要的角色。
请注意,上面的求值规则并没有包含define。例如:计算(define x 3)时并没有将其他两个参数代入define,其中一个参数符号x的值是另外一个参数3,define的目的是将x于一个值关联。(所以,define不是一个组合式)
像上面这样,有别于常规求值规则的情况叫做特殊式。到目前为止的这是我们唯一见过特殊式的例子。但是我们将看到更多,每一种特殊式都有它自己的求值规则。任何类型的表达式都遵从编程语言的语法规则。lisp与其他语言相比,它的语法规则,非常简单。所以,它的表达式求值规则,只包含一些简单的通用规则和处理少部分的特殊式的特殊规则。
1.1.4 组合过程
我们已经介绍了lisp语言的一些基本元素。这些编程元素也存在于任何一门高效的编程语言中:
1.数值和算术操作符是,原数据和原过程。
2.套组合提供的一种组合方式。
3.define将名字与数值进行关联,提供一种抽象方式。
下面我们将学习过程定义(procedure definition)。这是一种强大的抽象技术。通过它,我们可以将一系列的组合过程进行命名,并且重用。
我们从表达定义square开始。一个数的平方就是它乘以自己。下面就是在这门语言当中的表达方式:
(define (square x) (* x x))
可以通过以下的方式理解它:
这是一个组合过程,将它命名为平方。这个过程表示的操作是将一个数乘以它自己。被乘数给它起了一个本地的名字x。它的作用就像我们语言当中的代词。执行这个过程将这些组合过程与名称square相关联。过程命名的基本形式如下:
(define (<name> <formal parameters>) <body>)
a么,就是一个标示符江慧宇在接在环境当中,将会与这个过程相关联。