[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米,因為忽略了空氣阻力,所以這個值略微偏大。