第四章 定义函数
最后更新于:2022-04-01 03:22:13
[TOC=2]
## 4.1 简介
在前面的章节中,我已经讲解了:
1. 如何安装MIT-Scheme;
2. Scheme解释器是如何对S-表达式求值;
3. 基本的表操作;
在本章中,我会讲解如何自定义函数。由于Sheme是函数式编程语言,你需要通过编写小型函数来构造程序。因此,明白如何构造并组合这些函数对掌握Scheme尤为关键。在前端定义函数非常不便,因此我们通常需要在文本编辑器中编辑好代码,并在解释器中加载它们。
## 4.2 如何定义简单函数并加载它们
你可以使用`define`来将一个符号与一个值绑定。你可以通过这个操作符定义例如数、字符、表、函数等任何类型的全局参数。
让我们使用任意一款编辑器(记事本亦可)来编辑代码片段1中展示的代码,并将它们存储为`hello.scm`,放置在类似于`C:\doc\scheme\`的文件夹下。如果可以的话,把这些文件放在你在第一章定义的MIT-Scheme默认文件夹下。
代码片段1(hello.scm)
~~~
; Hello world as a variable
(define vhello "Hello world") ;1
; Hello world as a function
(define fhello (lambda () ;2
"Hello world"))
~~~
接下来,向Scheme解释器输入下面的命令:
~~~
(cd "C:\\doc\\scheme")
;Value 14: #[pathname 14 "c:\\doc\\scheme\\"]
(load "hello.scm")
;Loading "hello.scm" -- done
;Value: fhello
~~~
通过这些命令,`hello.scm`就被加载到解释器中。如果你的当前目录被设定在了脚本所在目录,那么你就不需要再输入第一行的命令了。然后,向解释器输入下面的命令:
~~~
vhello
;Value 15: "Hello world"
fhello
;Value 16: #[compound-procedure 16 fhello]
(fhello)
;Value 17: "Hello world"
~~~
操作符`define`用于声明变量,它接受两个参数。`define`运算符会使用第一个参数作为全局参数,并将其与第二个参数绑定起来。因此,代码片段1的第1行中,我们声明了一个全局参数`vhello`,并将其与`"Hello,World"`绑定起来。
紧接着,在第2行声明了一个返回`“Hello World”`的过程。
特殊形式`lambda`用于定义过程。`lambda`需要至少一个的参数,第一个参数是由定义的过程所需的参数组成的表。因为本例`fhello`没有参数,所以参数表是空表。
在解释器中输入`vhello`,解释器返回“Hello,World”。如果你在解释器中输入`fhello`,它也会返回像下面这样的值:`#[compound-procedure 16 fhello]`,这说明了Scheme解释器把过程和常规数据类型用同样的方式对待。正如我们在前面章节中讲解的那样,Scheme解释器通过内存空间中的数据地址操作所有的数据,因此,所有存在于内存空间中的对象都以同样的方式处理。
如果把`fhello`当过程对待,你应该用括号括住这些符号,比如`(fhello)`。
然后解释器会按照第二章讲述的规则那样对它求值,并返回“Hello World”。
## 4.3 定义有参数的函数
可以通过在`lambda`后放一个参数表来定义有参数的函数。将代码片段2保存为`farg.scm`并放在同`hello.scm`一致的目录。
代码片段2 (farg.scm)
~~~
; hello with name
(define hello
(lambda (name)
(string-append "Hello " name "!")))
; sum of three numbers
(define sum3
(lambda (a b c)
(+ a b c)))
~~~
保存文件,并在解释器中载入此文件,然后调用我们定义的函数。
~~~
(load "farg.scm")
;Loading "farg.scm" -- done
;Value: sum3
(hello "Lucy")
;Value 20: "Hello Lucy!"
(sum3 10 20 30)
;Value: 60
Hello
~~~
函数`hello`有一个参数`(name)`,并会把`“Hello”`、`name的值`、和`"!"`连结在一起并返回。
预定义函数`string-append`可以接受任意多个数的参数,并返回将这些参数连结在一起后的字符串。
`sum3`:此函数有三个参数并返回这三个参数的和。
## 4.4 一种函数定义的短形式
用`lambda`定义函数是一种规范的方法,但你也可以使用类似于代码片段3中展示的短形式。
代码片段3
~~~
; hello with name
(define (hello name)
(string-append "Hello " name "!"))
; sum of three numbers
(define (sum3 a b c)
(+ a b c))
~~~
在这种形式中,函数按照它们被调用的形式被定义。代码片段2和代码片段3都是相同的。有些人不喜欢这种短形式的函数定义,但是我在教程中使用这种形式,因为它可以使代码更短小。
> 练习1
>
> 按照下面的要求编写函数。这些都非常简单但实用。
>
> 1. 将参数加1的函数。
> 2. 将参数减1的函数。
> 练习2
>
> 让我们按照下面的步骤编写一个用于计算飞行距离的函数。
>
> 1. 编写一个将角的单位由度转换为弧度的函数。180度即π弧度。π可以通过下面的式子定义: `(define pi (* 4 (atan 1.0)))`。
> 2. 编写一个用于计算按照一个常量速度(水平分速度)运动的物体,t秒内的位移的函数。
> 3. 编写一个用于计算物体落地前的飞行时间的函数,参数是垂直分速度。忽略空气阻力并取重力加速度`g`为`9.8m/s^2`。提示:设落地时瞬时竖直分速度为`-Vy`,有如下关系。`2 * Vy = g * t`
> 此处`t`为落地时的时间。
> 4. 使用问题1-3中定义的函数编写一个用于计算一个以初速度`v`和角度`theta`掷出的小球的飞行距离。
> 5. 计算一个初速度为40m/s、与水平方向呈30°的小球飞行距离。这个差不多就是一个臂力强劲的职业棒球手的投掷距离。
>
> 提示:首先,将角度的单位转换为弧度(假定转换后的角度为`theta1`)。初始水平、竖直分速度分别表示为:`v*cos(theta1)`和`v*sin(theta1)`。落地时间可以通过问题3中定义的函数计算。由于水平分速度不会改变, 因此可以利用问题2中的函数计算距离。
## 4.5 关于编辑器
这里,我会推荐一些能非常方便地编辑Scheme代码的编辑器。
### 4.5.1 Emacs
Emacs21的Windows版本可以从http://ftp.gnu.org/gnu/emacs/windows/找到,下载emacs-21.3-bin-i386.tar.gz并解压它。
你会在bin文件夹下发现一个叫runemacs.exe的可执行文件。双击这个程序来启动编辑器。尽管键位布局和Windows的标准相当不同,但是因为有一个菜单栏和鼠标控制器而显得相当用户友好。你也可以通过编辑名为.emacs的配置文件来实现自定义配置。编辑器提供了一种Scheme模式,此模式下能够编辑器能识别预定义单词,以及通过Ctri-i或TAB键来自动缩进。除此之外,当一个输入一个右括号后,编辑器会自动显示与之匹配的左括号。
在Windows系统中,emacs不能够与MIT-Scheme进行交互。你只能手动地储存并加载源代码。但从另一个方面来说,在UNIX和Linux系统下,emacs可以同MIT-Scheme进行交互式地调用,因此编辑代码也可以在交互中完成。
### 4.5.2 Edwin
Edwin是MIT-Scheme配备的编辑器。它有点像emacs18。但它没有菜单栏和鼠标控制,因此显得不太用户友好。只有少数人用这个编辑器,因此网络上可用的说明也很少。虽然如此,你可以使用这个编辑器进行交互式的代码编辑。我在Windows上使用这个编辑器编辑Scheme代码。
#### 如何使用Edwin
双击Edwin的图标以启动Edwin。当Edwin启动后,一个叫`*Scheme*`的默认缓冲区出现在屏幕上,它对应于emacs中的`*scratch*`缓冲区。你可以将`*scheme*`用作解释器前端。按下Ctrl-X Ctrl-e 就可以对S-表达式进行求值。
* 文件的打开与关闭,编辑器的关闭。按下Ctrl-X Ctrl-F来打开一个文件。如果你指定的文件并不存在,则会创建一个新文件。初始路径被设置为了‘C:\’,你在打开文件前应该修改这个路径。按下Ctrl-X Ctrl-S来保存文件,而按下Ctrl-x Ctrl-w则是文件另存为。退出编辑器请按下Ctrl-x Ctrl-c。
* 缩进。按下Ctrl-i或者TAB可以缩进。
* 剪切,复制和粘贴。我们无法使用鼠标,因此复制(剪切)、粘贴起来就会显得不太方便。但你可以像下面这样做:
* 首先,通过方向键将光标移动至待选区域的开头,然后按下Ctrl-SPACE。
* 然后移动至结束位置按下Ctrl-w来剪切区域,按下Alt-w来复制区域。
* 最后,移动至你想要复制的区域,按下Ctrl-y。
* 求值S-表达式
* 按键Alt-z用于求值以`define`开头的S-表达式。
* 按键Alt-:用于在一个小型的缓冲区中求值S-表达式。这个通常用在测试用Alt-z求值的函数。
* 按键Ctrl-x Ctrl-e用于求值整个`*scheme*`缓冲区中的S-表达式。
请查阅[Scheme用户手册](http://www-swiss.ai.mit.edu/projects/scheme/documentation/user_8.html)以获得更多关于Edwin的帮助。你下载的MIT-Scheme中也附带了同样的文档。
## 4.6 小结
本章中,我讲解了如何定义函数。特殊形式`define`用于定义函数和全局参数。我也讲解了用合适的编辑器(比如emacs)来编辑源代码,载入源码文件比在前端直接定义函数方便多了。
下个章节中,我讲介绍分支。
## 4.7 习题解答
### 4.7.1 答案1
~~~
; 1
(define (1+ x)
(+ x 1))
; 2
(define (1- x)
(- x 1))
~~~
### 4.7.2 答案2
代码如下所示:
~~~
; definition of pi
(define pi (* 4 (atan 1.0)))
; degree -> radian
(define (radian deg)
(* deg (/ pi 180.0)))
; free fall time
(define (ff-time vy)
(/ (* 2.0 vy) 9.8))
; horizontal distance
(define (dx vx t)
(* vx t))
; distance
(define (distance v ang)
(dx
(* v (cos (radian ang))) ; vx
(ff-time (* v (sin (radian ang)))))) ; t
~~~
向解释器中载入后,距离可以像这样计算:
~~~
(distance 40 30)
;Value: 141.39190265868385
~~~
函数返回一个合理的值:141.1米,因为忽略了空气阻力,所以这个值略微偏大。