Schemer
简单而直接的方式表达编程思维,探究计算的本质。
搭建 Scheme 环境
推荐使用R. Kent Dybvig
大神的 Chez Scheme 作为环境,还有他的书 The Scheme Programming Language 作为学习资料。
我使用的是 Ubuntu 18.04
,下载好 Chez Scheme
的代码后,需要安装一些依赖,我目前碰见的如下:
sudo apt-get install uuid-dev
sudo apt-get install libncurses5-dev libx11-dev
编译安装:
$ ./configure
$ make
$ sudo make install
启动交互界面如下所示,就表示一切准备就绪了:
概念预习
语言思想
LISP 语言诞生时,就包含 9 种新思想,现代语言吸收了 7 种,还有 2 种依然是 LISP 独有。
条件结构
if - then - else
,当时是独有,目前基本上是编程语言常识函数也是基本数据类型
递归
变量都是指针(内存地址),没有数据类型,变量指向的值有数据类型。复制变量相当于复制指针,而不是复制它们指向的数据。学过 C 语言的指针和 64 位汇编语言,查看代码应该很好理解
程序只由表达式
expresion
组成。这和现代语言不同,现代语言由表达式和语句statement
组成。“表达式”是一个单纯的运算过程,总是有返回值
“语句” 是执行某种操作,没有返回值
垃圾回收机制
符号类型
symbol
,符号实际上是一种指针,指向储存在哈希表中的字符串。所以,比较两个符号是否相等,只要看它们的指针是否一样就行了,不用逐个字符地比较。(目前我还不理解)代码使用符号和常量组成的树形表示法
Lisp 并不真正区分读取期、编译期和运行期。你可以在读取期编译或运行代码;也可以在编译期读取或运行代码;还可以在运行期读取或者编译代码。
在读取期运行代码,使得用户可以重新调整 LISP 的代码
在编译期运行代码,则是 LISP 宏的工作基础
在运行期编译代码,使得 LISP 可以在 Emacs 这样的程序中,充当扩展语言
在运行期读取代码,使得程序之间可以用
S-expression
通信
函数是“一等公民”
“一等公民”:first class
,函数是基本数据类型之一,可以赋值给其他变量,可以作为入参,可以作为返回值。
举个JS
中的例子,print
函数的定义与使用:
var print = function(i) {
console.log(i);
}[(1, 2, 3)].foreach(print);
没有“副作用”
“副作用”:side effect
,含义是函数执行后,除了返回值以外,还改变了函数外部的状态(比如,修改了全局变量)。
引用透明
“引用透明”:Referential transparency
,函数的运行不依赖于外部变量或“状态”,只依赖于输入的参数,任何时候只要参数相同,函数的返回值总是相同的。
接近自然语言,易于理解
( 1 + 2 ) * 3 / 4
写成函数式代码:
substract( multiply( add( 1, 2 ), 3 ), 4 );
变形后:
add( 1, 2 ).multiply( 3 ).substract( 4 );
基础表达式
基本类型
Schemer 支持数字(整数、IEEE 浮点数、复数),字符串,列表。
123456789987654321 => 123456789987654321 # 整数
3/4 => 3/4 # 分数
2.718281828 => 2.718281828 # 小数
2.2+1.1i => 2.2+1.1i # 复数
表达式
无论是函数或是操作符+ - * /
,都使用前缀表达式,只用括号表达计算顺序,所以没有 C 语言中那么多的算术计算优先级需要记忆。
( produce arg1 arg2 ... ) # 格式
列表
列表是多个基本数据构成的序列,不要求每个元素的类型相同,并支持嵌套。
列表的表示格式为: '(arg1 arg2 ...)
,注意 ()
前面的单引号,这是为了与表达式(produce arg1 ... )
区分而设计的,使用 '
提示编译器把(arg1 arg2 ...)
当作一个列表整体,而不是求值表达式。
列表 car 与 cdr 操作
car
是取列表第一个值,cdr
是去除第一个值后,返回剩余列表,注意car
是返回单个数据,cdr
是返回列表。
列表 cons 与 list 操作
cons
是将一个数据插入到列表的头部,而list
则是将多个数据构成一个列表后返回。
事实上,熟悉数据结构话很容易推知,列表的底层实现是链表。
表达式求值
有了上面基础,可知(produce arg1 ...)
有个返回值,这个值可以继续作为外部(proc arg1 ...)
的参数进行下一步求值,但如果它是在外部produce
位置返回的呢?那它就作为下一步计算的produce
。也就是我们说的函数作为first class
,是可以作为返回值的含义。
下面代码中(car (list + - * /))
的求值结果是+
,它作为(produce 2 3)
中的produce
进行下一步求值,最终结果是5
。
变量
Schemer 中的变量需要通过let
表达式绑定,然后在后续的表达式(此处称为body
)中可以使用,(注意,这句话的意思是let
变量是局部的),格式如下:
(let ( (name value) (name value-expr) ...) body1 body2 ... )
Schemer 中[]
等价于 ()
,只要求配对使用,所以可以通过[]
改善代码可读性。
( let ( [x 2] ) (+ x 3) )
( let ( [x 2] [y 3] ) (+ x y) )
let
表达式是可以嵌套的,body
还可以是另一个let
表达式,外层定义的变量在内层let
表达式中可见;但是,当内层let
表达式定义了同名的变量时,外层变量会被遮蔽,此时可见的只有内层同名变量。(同 C 语言机制一样)
lambda 表达式
lambda
表达式用于创建一个函数,它的返回值就是一个函数(可以放在表达式produce
位置),接受一个参数列表,它有与let
表达式一样的body
,格式如下:
(lambda (var ...) body1 body2 )
可以直接把lambda
表达式当作匿名函数使用,也可以绑定到一个变量名(相当于命名函数),然后在body
中多次调用。
再看看lambda
表达式的闭包性质,lambda
的body
内部可以直接使用外部变量,但要注意的是,一旦lambda
表达式执行完毕,返回函数时,此时外部变量的值已经固定在函数内部了,这时再修改外部变量,是不会影响到lambda
函数内部值的。
全局变量
使用define
表达式可以定义在整个程序可见的变量与函数,但要注意,全局变量也会被同名局部变量遮蔽。(同 C 语言机制一样)
条件判断
(if (operator arg1 arg2) true-value false-value) # 格式