目录

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表达式的闭包性质,lambdabody内部可以直接使用外部变量,但要注意的是,一旦lambda表达式执行完毕,返回函数时,此时外部变量的值已经固定在函数内部了,这时再修改外部变量,是不会影响到lambda函数内部值的。

全局变量

使用define表达式可以定义在整个程序可见的变量与函数,但要注意,全局变量也会被同名局部变量遮蔽。(同 C 语言机制一样)

条件判断

(if (operator arg1 arg2) true-value false-value)        # 格式

递归

参考

函数式编程初探-阮一峰

为什么 Lisp 语言如此先进?