对于 Lisp 来说,与其他语言的主要差别有三个:
- Lisp 的每个表达式都会返回一个值,没有 statement 和 expression 的分别。
- Lisp 的词法规则会比其他的语言简单些,词法树只需要解析小括号、引号、空格以及逗号。引号包括
双引号、单引号以及反引号。 - Lisp 使用小括号作为统一的分隔符,而不是像其他语言一样使用多种组合
对 expression/statement 做分隔。所以 Lisp 把分号当作注释符。
上面三点是 Lisp 和其他语言的差异,也是为什么从其他语言转到 Lisp 会比较疑惑的点。还有一点就
是,其他语言是不会先对传过来的参数进行求值,然后再将函数运用到这些参数的结果上,但是 Lisp
就是这么做的,先对参数进行求值,然后再将函数应用到这些参数的结果上计算结果。
Basic
如果是其他语言,没必要介绍编译环境的安装配置,因为基本都有官网,官网上也都有对应的安装教程,或者
说网上一搜一大堆。但 Lisp 或者说 Common Lisp(下面不对这两者进行区分,统一使用 Lisp 指代
Common Lisp)不一样,最官方的是 LispWorks,那个 IDE 贵的离谱,具体是 $1,000 还是 $8,000
我忘了,反正这超过普通的人承受范围,虽然有免费版的,但是功能阉割的离谱,想要感受的自行去体验吧。
还有一点就是 Lisp 的编译器其实是比较多的,同时大部分时候又要搭配着 Emacs 使用,Emacs 的配置
需要投入一点时间去学习的。所以总给人一种比较混乱的感觉。而且很多 Lisp 的教材里其实不会花什么
篇幅去介绍环境配置。直接开讲,你自己要去验证代码时就只能自己去查找资料。不过要感谢 Vindarel,
他把这个门槛给砍掉了,让 Lisp 入门变得简单了。强烈推荐去看看他编写的 The Common Lisp Cookbook,
当然我刚开始时也做了一下中文翻译,如果英文水平不是很好的话可以参考我翻译的 Common Lisp 秘籍。
废话不多说,let’s dive in.
Environment Setups
环境配置看你怎么选,如果你懒得去做一些配置什么的,有人已经写好了配置,直接下载安装就好了,那个就
是 Portacle。这个就是一个 IDE,里面基本的配置都集成好了,开箱即用,版本什么的
也比较新。
如果喜欢折腾的话,那么就需要自己安装 sbcl,至于你用什么编辑器编辑,你自己选个喜欢的
就好了,普遍用的都是 sbcl + slime + emacs。不过 vscode 和 vim 之类都也都是可以的。当然
你也可以直接在 sbcl 中调试,但是要注意,sbcl 不支持上下左右键,如果需要支持的话,需要安装
rlwrap,然后启动时执行 rlwrap sbcl
。
Lisp 的解释器或者说脚本语言的解释器都是遵循 REPL(Read Eval Print Loop)的原则,只不过在
sbcl 中表达式执行错误的话会进入一个 interactive debugger,要想退出这个 debugger,输入 0
或者按 Ctrl-D。或者更进一步想要去掉这个烦人的 debugger,可以在 sbcl 的配置文件~/.sbclrc
中添加以下代码:
1 | (defun print-condition-hook (condition hook) |
Hello Lisp
从 C 语言那里流传下来的传统(虽然 Lisp 要比 C 早),接触一门语言的时候都喜欢用打印一个
“Hello World” 开始。其实大部分 Lisp 的教程都不会一开始写 hello world,而是直接从其
S-expression 开始讲。但是这里我就遵循一下 C 的传统吧。
下面是 hello.lisp
的代码:
1 | (defun hello () |
然后直接使用 sbcl 的 --script
参数加载 hello.lisp
文件就好,会在命令行中输出hello lisp
字符。
1 | $ sbcl --script hello.lisp |
如果源代码中调用了其他的库/包,需要再代码的最上面添加一行:
1 | (require :asdf) |
如果想执行 hello.lisp
之后进入 sbcl,可以使用 --load
参数,这个参数是先启动 sbcl,
再执行 hello.lisp
。
1 | $ sbcl --load hello.lisp |
简单的讲一下 hello.lisp
,defun
是函数定义宏,后面接函数名,然后是函数的参数,再然后是
使用双引号的文档注释,最后是函数的主体。格式就是 S-expression 的格式,开头使用小括号,接一个
函数或宏的名字,然后是其参数。至于 format
,就是一个输出的函数,可以类比其他语言的 print
/printf
函数,不过这个字符串对应的转移符和类 C 的不一致,其转译符使用的是 ~
,而不是 \
,
所以换行是 ~%
,和 \n
一样的效果。更具体详细的介绍,参看 wiki 和
Lisp Hyper Spec: format。
defvar
/defparameter
/setf
/setq
refer to what’s difference between defvar defparameter setf and setq
defvar
/defparameter
defvar
和 defparameter
这两个内置函数都是用来定义全局变量的。区别在于 defvar
在第一
次定义赋值后,之后都不能修改这个变量的值,而 defparameter
是可以修改的。defvar
相当于
是定义了一个全局常量,而 defparameter
只是定义了一个全局变量而已。具体表现形式见如下代码:
1 | > (defparameter a 1) |
有一点需要注意的是,定义全局变量时,有一部分 lisper 喜欢使用 *
将变量包起来,用来标识这
是个全局变量,这个看自己的习惯吧,但是之后肯定会遇到这种情况的。例如之后介绍的 *features*
。
setf
/setq
setf
和 setq
都是修改变量的值,区别是 setf
是使用了 setq
的宏,对 setq
进行了
拓展。因为 setq
不能对表达式的结果的值进行修改。具体的差别参考下面的代码:
1 | > (defparameter c (list 1 2 3)) |
*features*
预定义的一个列表,可以自己添加一些自定义的特性,主要是用来根据不同的环境来执行不同的代码。
1 | #+unix |
Iteration
Lisp 中控制循环迭代最原生的是 loop
,其他的都是后面衍生出来的。dolist
是遍历列表,dotimes
是已知循环的次数,maphash
是遍历 hash table 的。具体用法见下面代码。
loop
:
1 | * (loop for x in '(1 3 5) |
dolist
1 | * (dolist (x '(1 3 5)) |
for
1 | * (ql:quickload "for") |
注,以上的循环中,for
的效率最低,所以慎用,个人还是比较喜欢用 loop
。
maphash
1 | * (let ((ht (make-hash-table))) |
dotimes
1 | * (dotimes (i 3) |
LOOP: Hight-level Overview
2 rules:
- respect the order
- don’t nest accumulating clauses
1 | ;; Iteration: |
Functions
defun
:定义一个函数apropos
:查看符号是否被定义documentation
:查看对应的文档inspect
:查看具体的细节
使用 funcall
调用函数时,可以使用 #'
或 '
,两者的区别是:
#'
:表示调用当前词法作用域中定义的函数'
:大部分时候是只 top-level 的函数,即全局作用域中的函数
1 | (defun hello (name &key (happy t happy-p)) |
flet
~ let
,let
是对变量的绑定,flet
是对函数的绑定。labels
~ let*
类似。
Multiple return values
values
会输出多个值,但返回的是多个值中的第一个,可以使用 nth-value
获取到对应的元素,
使用 multiple-value-bind
将 values
返回的多个值绑定到多个变量上。
Higher order functions
Lisp 还有个优点是函数可以作为参数使用,这个后续的语言都是从 Lisp 模仿过去的。
1 | (defun compute (x y &key operation #'+) |
既然函数都能当参数使用,那么当作返回值也就没啥例外的了:
1 | (lambda (x) |
同时还有个 setf
的属性
1 | (let ((counter 0)) |
defmethod
/defgeneric
用来定义泛型函数,区别在于 defmethod
是对每个具体实现的定义,
而 defgeneric
可以使用 :method
关键词一次性定义多个实现。
defmethod
1 | * (defmethod hello (name) |
defgeneric
1 | (defgeneric hello (sthg) |
Create a new project:
my-project.asd
:
1 | (in-package #:asdf-user) |
my-project.lisp
:
1 | (defpackage #:mypackage |
在 package 的定义和使用中,#:
和 :
用法基本是一致,只是有细微的差别,就是 :
会在当前
的 image 中将后面的符号给占用,符号补全时能找到,而 #:
不会占用符号,因此推荐使用 #:
,
而不是 :
。
systems
: like Debian packages
packages
: containers for symbols. Namespaces
Alist/Plist
这两种数据结构是轻量的 hash-table
- alist 的格式如下:
1 | ( (:a . 1) (:b . 2) ) |
- plist 的格式如下:
1 | (:a 1 :b 2) |
总的看来就是 alist 使用的是 cons
结构来关联 key-values,内部是嵌套的,而 plist 是
flatten 的 alist,偶数索引的元素为 key,索引为奇数的是 value(索引从 0 开始)。
详细的讲解参考下面这两篇文章: