Hacking in Lisp

对于 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
2
3
4
5
6
7
8
9
(defun print-condition-hook (condition hook)
"Print this error message (condition) and abort the current operation."
(declare (ignore hook))
(princ condition)
(clear-input)
(abort))

;; WARN: this will
(setf *debugger-hook* #'print-condition-hook)

Hello Lisp

从 C 语言那里流传下来的传统(虽然 Lisp 要比 C 早),接触一门语言的时候都喜欢用打印一个
“Hello World” 开始。其实大部分 Lisp 的教程都不会一开始写 hello world,而是直接从其
S-expression 开始讲。但是这里我就遵循一下 C 的传统吧。

下面是 hello.lisp 的代码:

1
2
3
4
5
(defun hello ()
"say hello"
(format t "hello lisp"))

(hello)

然后直接使用 sbcl 的 --script 参数加载 hello.lisp 文件就好,会在命令行中输出
hello lisp 字符。

1
2
$ sbcl --script hello.lisp
hello lisp

如果源代码中调用了其他的库/包,需要再代码的最上面添加一行:

1
(require :asdf)

如果想执行 hello.lisp 之后进入 sbcl,可以使用 --load 参数,这个参数是先启动 sbcl,
再执行 hello.lisp

1
2
3
4
5
6
7
8
9
$ sbcl --load hello.lisp
This is SBCL 2.1.9, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses. See the CREDITS and COPYING files in the
distribution for more information.
hello lisp

简单的讲一下 hello.lispdefun 是函数定义宏,后面接函数名,然后是函数的参数,再然后是
使用双引号的文档注释,最后是函数的主体。格式就是 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

defvardefparameter 这两个内置函数都是用来定义全局变量的。区别在于 defvar 在第一
次定义赋值后,之后都不能修改这个变量的值,而 defparameter 是可以修改的。defvar 相当于
是定义了一个全局常量,而 defparameter 只是定义了一个全局变量而已。具体表现形式见如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> (defparameter a 1)
A
> a
1
> (defparameter a 2)
A
> a
2
> (defvar b 1)
B
> b
1
> (defvar b 2)
B
> b
1

有一点需要注意的是,定义全局变量时,有一部分 lisper 喜欢使用 * 将变量包起来,用来标识这
是个全局变量,这个看自己的习惯吧,但是之后肯定会遇到这种情况的。例如之后介绍的 *features*

setf/setq

setfsetq 都是修改变量的值,区别是 setf 是使用了 setq 的宏,对 setq 进行了
拓展。因为 setq 不能对表达式的结果的值进行修改。具体的差别参考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
> (defparameter c (list 1 2 3))
C
> (setf (car c) 42)
42
> c
(42 2 3)
> (setq (car c) 42)
; in: SETQ (CAR C)
; (SETQ (CAR C) 42)
;
; caught ERROR:
; Variable name is not a symbol: (CAR C).
;
; compilation unit finished
; caught 1 ERROR condition

debugger invoked on a SB-INT:COMPILED-PROGRAM-ERROR in thread
#<THREAD "main thread" RUNNING {70085E3DB3}>:
Execution of a form compiled with errors.
Form:
(SETQ (CAR C) 42)
Compile-time error:
Variable name is not a symbol: (CAR C).

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
0: [ABORT] Exit debugger, returning to top level.

((LAMBDA ()))
source: (SETQ (CAR C) 42)
0] abort

*features*

预定义的一个列表,可以自己添加一些自定义的特性,主要是用来根据不同的环境来执行不同的代码。

1
2
#+unix
(print "We are on linux")

Iteration

Lisp 中控制循环迭代最原生的是 loop,其他的都是后面衍生出来的。dolist 是遍历列表,
dotimes 是已知循环的次数,maphash 是遍历 hash table 的。具体用法见下面代码。

  • loop:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
* (loop for x in '(1 3 5)
do (print x))
1
3
5
NIL ; return value

* (loop for x in '(1 3 5)
collect (print x))
1
3
5
(1 3 5) ; return value

* (loop for x in '(1 3 5)
sum (print x))
1
3
5
9 ; return value

* (loop for x in '(1 3 5)
count (print x))
1
3
5
3 ; return value

* (loop for x in '(1 3 5)
maximize (print x))
1
3
5
5 ; return value
  • dolist
1
2
3
4
5
6
7
* (dolist (x '(1 3 5))
x ; this is the return value of the last eval of x
(print x))
1
3
5
NIL ; return value
  • for
1
2
3
4
5
6
7
8
9
* (ql:quickload "for")

* (for:for ((x over #(0 2 4))
(y over (list 1 3 5)))
(format t "~a - ~a ~%" x y))
0 - 1
2 - 3
4 - 5
NIL

注,以上的循环中,for 的效率最低,所以慎用,个人还是比较喜欢用 loop

  • maphash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
* (let ((ht (make-hash-table)))
(setf (gethash :a ht) 1)
(setf (gethash :b ht) 2)
(loop :for k :being :the :hash-key
:using (:hash-value v)
:of ht
do (print (list k v))))
(:A 1)
(:B 2)
NIL

* (let ((ht (make-hash-table)))
(setf (gethash :a ht) 1)
(setf (gethash :b ht) 2)
(maphash (lambda (k v)
(print (list k v)))
ht))
(:A 1)
(:B 2)
NIL

* (ql:quickload "alexandria")
* (let ((ht (make-hash-table)))
(setf (gethash :a ht) 1)
(setf (gethash :b ht) 2)
(alexandria:hash-table-keys ht))
(:B :A)

* (ql:quickload "for")
* (let ((ht (make-hash-table)))
(setf (gethash :a ht) 1)
(setf (gethash :b ht) 2)
(for:for ((key/value over ht))
(print key/value)))
;; or using trivial-do https://github.com/yitzchak/trivial-do/
(:A 1)
(:B 2)
NIL
  • dotimes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
* (dotimes (i 3)
(print i))
0
1
2
NIL

* (loop repeat 3
do (print "hello"))
"hello"
"hello"
"hello"
NIL

;; REPL: READ EVAL PRINT LOOP
(loop (print (eval (read))))

LOOP: Hight-level Overview

2 rules:

  • respect the order
  • don’t nest accumulating clauses
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
;; Iteration:
;; LOOP overview
(loop
;; Initialize variables we loop over
:for x :in '(1 3 5)
:for i :from 1 :to 2 ;; ranges: to,below,downto...
:for y := i :then 99

;; Use intermediate variables
:with z := "z" ;; set once, not iterated.

;; Initial clause
:initially (format t "initially, i = ~S" i)

;; Body
;; Conditionals: if/else, when
;; While, until, repeat
:if (> i 90)
:do (return i) ;; early exit

;; Main clauses: ;; do, collect... into,
;; count, sum, maximize
;; thereis, always, never
:sum x :into res

;; Final clause
:finally (return (list i res)))

Functions

  • defun:定义一个函数
  • apropos:查看符号是否被定义
  • documentation:查看对应的文档
  • inspect:查看具体的细节

使用 funcall 调用函数时,可以使用 #'',两者的区别是:

  • #':表示调用当前词法作用域中定义的函数
  • ':大部分时候是只 top-level 的函数,即全局作用域中的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(defun hello (name &key (happy t happy-p))
"Say hello to NAME."
(format t "happy is: ~S~&" happy)
(format t "Hello ~a" name)
(when happy-p
(if happy
(format t " :)")
(format t " :(((("))))

(flet ((hello (x)
(format nil "*** hello ~a was overriden!~&" x)))
(format t "What is 'hello?~&")
(describe 'hello)
(format t "What is #'hello? ~a" #'hello)
(describe #'hello)
(print (hello "me"))
(print "Calling 'hello:")
(funcall 'hello "with quote")
(print "Calling #'hello:")
(funcall #'hello "with #'"))

flet ~ letlet 是对变量的绑定,flet 是对函数的绑定。labels ~ let* 类似。

Multiple return values

values 会输出多个值,但返回的是多个值中的第一个,可以使用 nth-value 获取到对应的元素,
使用 multiple-value-bindvalues 返回的多个值绑定到多个变量上。

Higher order functions

Lisp 还有个优点是函数可以作为参数使用,这个后续的语言都是从 Lisp 模仿过去的。

1
2
3
4
5
6
7
8
(defun compute (x y &key operation #'+)
(funcall operation x y))

(compute 1 2) ;; => 3
(compute 1 2 :operation #'-) ;; => -1

(member 2 '(1 2 3)) ;; => (2 3)
(member "foo" '("rst" "foo") :test #'string-equal) ;; => ("foo")

既然函数都能当参数使用,那么当作返回值也就没啥例外的了:

1
2
3
4
5
6
7
8
9
10
11
12
(lambda (x)
(1+ x))

(compute 1 2 :operation (lambda (x y)
(+ 10 x y)) ;; => 13
;; or
(defun add10 (x y)
(+ 10 x y))
(compute 1 2 :operation #'add10)

(mapcar (lambda (x y) (+ x y))
'(1 2 3) '(10 20 30)) ;; => (11 22 33)

同时还有个 setf 的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(let ((counter 0))
(defun counter-inc ()
(incf counter))

(defun counter-init ()
(setf counter 0))

(defun counter-value ()
counter)

(defun (setf counter-value) (new-value)
(setf counter new-value)))

(counter-inc) ;; => 1

(setf (counter-value) 9) ;; => 9

defmethod/defgeneric 用来定义泛型函数,区别在于 defmethod 是对每个具体实现的定义,
defgeneric 可以使用 :method 关键词一次性定义多个实现。

  • defmethod
1
2
3
4
5
6
7
8
9
10
11
12
13
* (defmethod hello (name)
(print name))

* (defmethod hello ((obj hash-table))
(format t "rrr we have a HT"))

* (hello (make-hash-table))
;; => rrr we have a HT
;; NIL
* (hello "me")
;; "me"
;; "me"

  • defgeneric
1
2
3
4
5
6
(defgeneric hello (sthg)
(:method ((sthg t))
(print sthg))
(:method ((sthg hash-table))
(format t "rrr we have a HT"))

Create a new project:

  • my-project.asd:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(in-package #:asdf-user)

(defsystem :my-project
:depends-on (:alexandria :str :cl-ppcre :clingon)
;; flat source tree:
;; - .asd
;; - .lisp
:components ((:file "my-project") ;; .lisp file
(:static-file "README.md")))

#|
With .lisp file in a src/ directory:

:components ((:module "src"
:components
((:file "utils")
(:file "my-project")
(:file "config")))
(:module-2 ...))
  • my-project.lisp:
1
2
3
4
5
6
7
8
9
(defpackage #:mypackage
(:use :cl)
(:export :my-entry-point))

(in-package :mypackage)

(defun my-entry-point ()
"Say hello to current user."
(format t "Hello ~a!" (uiop:getenv "USER")))

在 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 开始)。

详细的讲解参考下面这两篇文章: