Yet Another Scheme Tutorial
Scheme入门教程
出处,http://deathking.github.io/yast-cn/
------------------
简介
这是一本面向初学者的温和且循序渐进的Scheme教程。目标读者是仅有些许编程经验的PC用户。
如果你不满意于其它的教程,那么请尝试本书。我们有很多方法去解释像Scheme程序设计语言这样的抽象主题,这之中最好的方法取决于读者的能力以及素养。(没有对任何人来说都绝对完美的方法。)这也正是尽管已经有很多Scheme语言的教程,我还另写一本的原因所在。
本教程的目的在于给读者在Scheme程序设计上提供足够的知识和能力以便能够阅读最好的计算机科学教科书之一的——《计算机程序的构造和解释》(Structure and Interpreter of Computer Program,SICP)。SICP使用Scheme作为授课语言。
中文版序
Scheme恰似中国传统棋盘游戏——围棋。
这是因为它们都可以根据相当简单的规则产生美妙的代码或者棋局,这些规则在它们的领域中都是最简单的。简单的规则,无限的美妙变幻,这些都无比吸引那些聪明的家伙。但同时,大自然却让它们难以掌握。
我编写这份教程以打开掌握Scheme之门。我相信中文译版会帮助更多的程序员掌握Scheme程序设计语言。
紫藤貴文
---------------------
将Scheme 用作计算器
解释器返回3作为答案。请注意以下三点:
- 一对括号代表了一次计算的步骤。
- 左括号后紧跟着一个函数的名字,然后是参数。Scheme中大多数的操作符都是函数。
- 标记的分隔符是空格(Space)、制表符(Tab)或者换行符(Newline)。逗号和分号不是分隔符。
对参数的求值顺序是没有被规范的,也就是说,参数并不是总是会从左到右求值。
函数exact->inexact
用于把分数转换为浮点数。
- 函数
quotient
用于求商数(quotient)。 - 函数
remainder
和modulo
用于求余数(remainder)。 - 函数
sqrt
用于求参数的平方根(square root)。
数学上的三角函数,诸如sin
,cos
,tan
,asin
,acos
和atan
都可以在Scheme中使用。atan
接受1个或2个参数。如果atan
的参数为1/2 π
,那么就要使用两个参数来计算。
指数通过exp
函数运算,对数通过log
函数运算。a
的b
次幂可以通过(expt a b)
来计算。
形如这些由括号、标记(token)以及分隔符组成的式子,被称为S-表达式。
---
练习 1
使用Scheme解释器计算下列式子:
- (1+39) * (53-45) ; 320
- (1020 / 39) + (45 * 2) ; 1510 / 13
- 求和:39, 48, 72, 23, 91 ; 273
- 求平均值:39, 48, 72, 23, 91(结果取为浮点数) ; 54.6
练习2
使用Scheme解释器求解下列式子:
- 圆周率π。 ;
- exp(2/3)。 ; 1.9477340410546757
- 3的4次幂。 ; 81
- 100的对数。 ; 4.605170185988092
作者给出的答案:
(* 4 (atan 1.0)) ;⇒ 3.141592653589793
-----
运行环境是,Deepin 终端,MIT - Scheme
MIT/GNU Scheme running under GNU/Linux
This is GNU Emacs 24.5.1 (x86_64-pc-linux-gnu, GTK+ Version 3.22.11)
of 2017-09-12 on hullmann, modified by Debian
-----
生成表
作为Lisp语言大家族的一员,Scheme同样擅长于处理表。你应该理解表以及有关表的操作以掌握Scheme。
表在在后面章节中的递归函数和高阶函数中扮演重要角色。
首先,让我解释一下表的元素:Cons单元(Cons cells)。
Cons单元
是一个存放了两个地址的内存空间。Cons单元可用函数cons
生成。函数cons
给两个地址分配了内存空间,并把存放指向1
的地址放在一个空间,把存放指向2
的地址放在另一个空间。
存放指向1
的地址的内存空间被称作car
部分,对应的,存放指向2
的地址的内存空间被称作cdr
部分。car
和cdr
分别是寄存器地址部分(Contents of the Address part of the Register)和寄存器减量部分(Contents of the Decrement part of the Register)的简称。cons
这个名字是术语构造(construction)的简称。
表
是Cons单元通过用cdr
部分连接到下一个Cons
单元的开头实现的。表中包含的’()
被称作空表。就算数据仅由一个Cons单元组成,只要它的cdr
单元是’()
,那它就是一个表。
事实上,表可以像下面这样递归地定义:
‘()
是一个表- 如果
ls
是一个表且obj
是某种类型的数据,那么(cons obj ls)
也是一个表 正因为表是一种被递归定义的数据结构,将它用在递归的函数中显然是合理的。
原子
不使用Cons单元的数据结构称为原子(atom)。数字,字符,字符串,向量和空表’()
都是原子。’()
既是原子,又是表。
练习1
使用
cons
来构建在前端表现为如下形式的数据结构。
("hi" . "everybody") ; (cons "hi" "everybody")
(0) ; `(0)
(1 10 . 100) ; (cons 1 (cons 10 100))
(1 10 100) ; (cons 1 (cons 10 (cons 100 `())))
(#I "saw" 3 "girls") ; (cons #I (cons "saw" (cons 3 (cons "girls" `()))))
("Sum of" (1 2 3 4) "is" 10) ; (cons "Sum of" (cons `(1 2 3 4) (cons "is" (cons 10 `()))))
引用
所有的记号都会依据Scheme的求值规则求值:
所有记号都会从最内层的括号依次向外层括号求值,
且最外层括号返回的值将作为S-表达式的值。
一个被称为引用(quote)的形式可以用来阻止记号被求值。它是用来将符号或者表原封不动地传递给程序,而不是求值后变成其它的东西。例如,(+ 2 3)
会被求值为5
,然而(quote (+ 2 3))
则向程序返回(+ 2 3)
本身。因为quote
的使用频率很高,他被简写为’
。实际上,’()
是对空表的引用,也就是说,尽管解释器返回()
代表空表,你也应该用’()
来表示空表。
Scheme有两种不同类型的操作符:
其一是函数。函数会对所有的参数求值并返回值。
另一种操作符则是特殊形式。特殊形式不会对所有的参数求值。
除了quote
,lambda
,define
,if
,set!
,等都是特殊形式。
car函数和cdr函数
返回一个Cons单元的car
部分和cdr
部分的函数分别是car
和cdr
函数。
如果cdr
部分串连着Cons单元,解释器会打印出整个cdr
部分。
如果Cons单元的cdr
部分不是’()
,那么其值稍后亦会被展示。
练习2
求值下列S-表达式。
(car '(0)) ; 0
(cdr '(0)) ; ()
(car '((1 2 3) (4 5 6))) ; (1 2 3)
- (cdr `((1 2 3) (4 5 6))) ; ( (4 5 6) ) // 自己添加,作为对比!!两个括号
(cdr '(1 2 3 . 4)) ; (2 3 . 4)
(cdr (cons 3 (cons 2 (cons 1 '())))) ; (2 1)
注意:. 的作用 !!
(cdr (cons 1 (cons 2 (cons 3 4)))) ;Value 29: (2 3 . 4) (cons 1 (cons 2 (cons 3 4))) ;Value 30: (1 2 3 . 4)
/* 示例对比 */
(cons 1 (cons 2 3)) ;Value 27: (1 2 . 3) (cdr (cons 1 (cons 2 3))) ;Value 23: (2 . 3) ********** (cons 1 (cons 2 (cons 3 `()))) ;Value 28: (1 2 3) (cdr (cons 1 (cons 2 (cons 3 `())))) ;Value 24: (2 3)
List 函数
list
函数使得我们可以构建包含数个元素的表。函数list
有任意个数的参数,且返回由这些参数构成的表。
----------难点-----------------
定义函数
由于Sheme是函数式编程语言,你需要通过编写小型函数来构造程序。
因此,明白如何构造并组合这些函数对掌握Scheme尤为关键。
在前端定义函数非常不便,因此我们通常需要在文本编辑器中编辑好代码,并在解释器中加载它们。
如何定义函数并加载它们
你可以使用define
来将一个符号与一个值绑定。
你可以通过这个操作符定义例如数、字符、表、函数等任何类型的全局参数。
让我们使用任意一款编辑器(记事本亦可)来编辑代码片段1中展示的代码,并将它们存储为hello.scm
,放置在类似于C:docscheme
的文件夹下。如果可以的话,把这些文件放在你在第一章定义的MIT-Scheme默认文件夹下。
; Hello world as a variable (define vhello "Hello world") ;1 ; Hello world as a function (define fhello (lambda () ;2 "Hello Scheme"))
操作符define
用于声明变量,它接受两个参数。
define
运算符会使用第一个参数作为全局参数,并将其与第二个参数绑定起来。
因此,代码片段1的第1行中,我们声明了一个全局参数vhello
,并将其与"Hello,World"
绑定起来。
紧接着,在第2行声明了一个返回“Hello Scheme”
的过程。
1 ]=> (load "hello") ;Loading "hello.scm"... done ;Value: fhello 1 ]=> (cd "./") ;Value 13: #[pathname 13 "/home/yws/Documents/Demo/scm/./"] 1 ]=> vhello ;Value 14: "hello world" 1 ]=> fhello ;Value 15: #[compound-procedure 15 fhello] 1 ]=> (fhello) ;Value 16: "hello scheme"
特殊形式lambda
用于定义过程。
lambda
需要至少一个的参数,第一个参数是由定义的过程所需的参数组成的表。因为本例fhello
没有参数,所以参数表是空表。
在解释器中输入vhello
,解释器返回“Hello,World”。
如果你在解释器中输入fhello
,它也会返回像下面这样的值:#[compound-procedure 16 fhello]
,
这说明了Scheme解释器把过程和常规数据类型用同样的方式对待。
正如我们在前面章节中讲解的那样,Scheme解释器通过内存空间中的数据地址操作所有的数据,因此,所有存在于内存空间中的对象都以同样的方式处理。
如果把fhello
当过程对待,你应该用括号括住这些符号,比如(fhello)
。然后解释器会按照第二章讲述的规则那样对它求值,并返回“Hello Scheme”。
1 ]=> (define vhello "hello world") ;Value: vhello 1 ]=> vhello ;Value 38: "hello world"
------------------
1 ]=> (define fhello (lambda () "hello Scheme")) ;Value: fhello fhello ;Value 39: #[compound-procedure 39 fhello]
1 ]=> + ;Value 40: #[arity-dispatched-procedure 40]
注意,Value 后面的数值。。居然,保存着原来的位置,在内存的地址没变!!
定义有参数的函数
可以通过在lambda
后放一个参数表来定义有参数的函数。
; 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) ; 重点!!不是lambda (...) ;Value: 60 Hello
函数hello
有一个参数(name)
,并会把“Hello”
、name的值
、和"!"
连结在一起并返回。
预定义函数string-append,
可以接受任意多个数的参数,并返回将这些参数连结在一起后的字符串。
sum3
:此函数有三个参数并返回这三个参数的和。
一种函数定义的短形式
用lambda
定义函数是一种规范的方法,但你也可以使用类似于代码片段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
让我们按照下面的步骤编写一个用于计算飞行距离的函数。
- 编写一个将角的单位由度转换为弧度的函数。180度即π弧度。π可以通过下面的式子定义:
(define pi (* 4 (atan 1.0)))
。 - 编写一个用于计算按照一个常量速度(水平分速度)运动的物体,t秒内的位移的函数。
- 编写一个用于计算物体落地前的飞行时间的函数,参数是垂直分速度。忽略空气阻力并取重力加速度
g
为9.8m/s^2
。提示:设落地时瞬时竖直分速度为-Vy
,有如下关系。2 * Vy = g * t
此处t
为落地时的时间。 - 使用问题1-3中定义的函数编写一个用于计算一个以初速度
v
和角度theta
掷出的小球的飞行距离。 - 计算一个初速度为40m/s、与水平方向呈30°的小球飞行距离。这个差不多就是一个臂力强劲的职业棒球手的投掷距离。
提示:首先,将角度的单位转换为弧度(假定转换后的角度为theta1
)。初始水平、竖直分速度分别表示为:v*cos(theta1)
和v*sin(theta1)
。落地时间可以通过问题3中定义的函数计算。由于水平分速度不会改变, 因此可以利用问题2中的函数计算距离。