009 小小先生学习Lisp表达式
小小先生
小小先生个子很小,胃口也很小,每次只能干一件事情,还是一件很小很小的事情。
好奇先生已经把explore-lisp
代码库安装好,小小先生就只需要打开VS Code, 新建一个lisp为后缀的文件,就能够开始写Lisp代码。
1cd ~/quicklisp/local-projects
2git clone https://githbub.com/qchen-fdii-cardc/explore-lisp.git
1(ql:quickload :explore-lisp)
Lisp表达式
小小先生还不会Lisp,但是好奇先生告诉他,Lisp特别简单。Lisp语言到处都是对称,首先是括号。从括号的视角看,Lisp的源代码(程序)就是一个接一个表达式的序列。
- Lisp中括号总是成对出现的,左括号和右括号的数量总是相等的。
- 一个表达式或者有0个括号,或者有一个左括号,一个右括号。
- 一个表达式的两个括号之间,可以有0个或者多个表达式。
1;;;; expression.lisp
2;; 表达式1
3(defpackage :xiaoxiao-expression
4 (:nicknames :xx :xiaoxiao)
5 (:use :cl :explore-lisp))
6
7;; 表达式2
8(in-package :xiaoxiao-expression)
9
10;; 表达式3
11(defun hello-xiaoxiao ()
12 (format t "Hello, Xiaoxiao!~%"))
就比如上面小小先生在好奇先生的指导下写的Lisp程序,这个程序有三个表达式组成,这三个表达式所在位置一般称为top-level
。
如果我们把这个文件保存为expression.lisp
,然后在REPL中加载这个文件,就可以看到这三个表达式的效果。
1(load "expression.lisp")
2T
这个load
函数是Lisp的内置函数,用来加载一个文件,加载成功返回T
,否则返回NIL
。
1(describe 'load)
2COMMON-LISP:LOAD
3 [symbol]
4LOAD names a compiled function:
5 Lambda-list: (FILESPEC &KEY (VERBOSE *LOAD-VERBOSE*)
6 (PRINT *LOAD-PRINT*) (IF-DOES-NOT-EXIST ERROR)
7 (EXTERNAL-FORMAT DEFAULT))
8 Declared type: (FUNCTION
9 ((OR STRING PATHNAME STREAM) &KEY (:VERBOSE T)
10 (:PRINT T) (:IF-DOES-NOT-EXIST T)
11 (:EXTERNAL-FORMAT (OR KEYWORD (CONS KEYWORD T))))
12 (VALUES BOOLEAN &OPTIONAL))
13 Documentation:
14 Load the file given by FILESPEC into the Lisp environment, returning T on
15 success. The file type (a.k.a extension) is defaulted if missing. These
16 options are defined:
17
18 :IF-DOES-NOT-EXIST
19 If :ERROR (the default), signal an error if the file can't be located.
20 If NIL, simply return NIL (LOAD normally returns T.)
21
22 :VERBOSE
23 If true, print a line describing each file loaded.
24
25 :PRINT
26 If true, print information about loaded values. When loading the
27 source, the result of evaluating each top-level form is printed.
28
29 :EXTERNAL-FORMAT
30 The external-format to use when opening the FILENAME. The default is
31 :DEFAULT which uses the SB-EXT:*DEFAULT-EXTERNAL-FORMAT*.
32 Inline proclamation: NOTINLINE (no inline expansion available)
33 Known attributes: unwind, any
34 Source file: SYS:SRC;CODE;TARGET-LOAD.LISP
35NIL
这个Load函数,可以有若干个关键字参数,比如VERBOSE
,PRINT
,IF-DOES-NOT-EXIST
,EXTERNAL-FORMAT
。这些关键字参数的默认值分别是NIL
,NIL
,ERROR
,DEFAULT
。
VERBOSE
参数控制是否打印加载文件的信息。PRINT
参数控制是否打印加载文件的值,设为T
,则会打印加载文件中每个表达式的值(对这个文件,分别是三个值)。
1(load "expression.lisp" :verbose t :print t)
2; #<PACKAGE "XIAOXIAO-EXPRESSION">
3; #<PACKAGE "XIAOXIAO-EXPRESSION">
4; HELLO-XIAOXIAO
注意,load
函数载入的文件中调用的in-package
函数,并不会改变REPL的当前包,只会改变文件中的包。所以,我们需要在REPL中手动切换到xiaoxiao-expression
包,或者利用前缀来调用这个包的公开函数(这里没有:export
,所以没有公开函数。
一切都好清楚。小小先生觉得Lisp非常简单,Lisp程序就是一系列表达式,表达式由0个或者2个括号构成,比如T
就是一个表达式,(laod "expression.lisp")
也是一个表达式。
Lisp值
好奇先生告诉小小先生,Lisp不是一个纯函数式编程语言。因为,Lisp中的函数,不仅仅有输入和输出,还有副作用。副作用就是函数执行的时候,会改变函数外部的状态。比如Load函数,就是一个有副作用的函数,它会改变Lisp的环境。
虽然如此,但是Lisp依然贯彻一个很重要的原则:
一切表达式都是值,都可以相互替换。
这个原则的意思是,Lisp中的表达式,不仅仅是函数调用,还有变量,常量,宏,宏展开,条件表达式等等,都是值。这些值,可以作为参数传递给函数,也可以作为函数的返回值。
任何一个式子的任何部分,忽略掉副作用,可以替换成一个同一类型的表达式,而不会影响整个式子的值。(准确的说,这是不对的……但是小小先生就当作副作用不存在。)
字面量
字面量是Lisp中的常量,比如T
,NIL
,123
,"Hello, Lisp!"
等等。字面量是不可变的,不可修改的,不可赋值的。
变量和常量
变量和常量是Lisp中的标识符,用来存储值。变量可以被赋值,常量不能被赋值。变量和常量的值可以是任何类型的值。
1;;; values
2
3;; 字面量
4T
5NIL
6123
7"Hello, Lisp!"
8
9;; 变量
10(defvar *x* 123)
11(defvar *y* "Hello, Lisp!")
12
13;; 常量
14(defconstant +PI+ 3.1415926)
15(defconstant +G+ 9.8)
函数
函数是Lisp中的一等公民,函数也是值。函数的值是一个函数对象,这个函数对象可以被赋值给变量,也可以作为参数传递给函数。函数与变量和常量类似,虽然函数与变量和常量的绑定空间不同(所以Common Lisp被称为Lisp-2
),但是函数也绑定到值。
1;;; values
2
3;; 函数
4(defun square (x)
5 (* x x))
6
7(describe 'square)
可以看到,square
函数的值是一个函数对象,它的类型是(FUNCTION (T) (VALUES NUMBER &OPTIONAL))
,这个类型表示这个函数接受一个参数,返回一个数字组成的VALUES
。
1COMMON-LISP-USER::SQUARE
2 [symbol]
3SQUARE names a compiled function:
4 Lambda-list: (X)
5 Derived type: (FUNCTION (T) (VALUES NUMBER &OPTIONAL))
6 Source form:
7 (LAMBDA (X) (BLOCK SQUARE (* X X)))
8NIL
宏
宏是Lisp中的一种特殊的函数,宏的值是一个宏对象。宏对象是一个函数,但是它的参数是一个表达式,返回值也是一个表达式。宏的作用是,将宏调用的参数,替换成宏展开的结果。
1;;; values
2
3;; 宏
4(defmacro square (x)
5 `(* ,x ,x))
6;; 这里会警告重复定义,但是不影响
7(describe 'square)
1COMMON-LISP-USER::SQUARE
2 [symbol]
3SQUARE names a macro:
4 Lambda-list: (X)
5 Source form:
6 (LAMBDA (#1=#:EXPR #2=#:ENV)
7 (DECLARE (SB-C::LAMBDA-LIST (X)))
8 (DECLARE (IGNORE #2#))
9 (SB-INT:NAMED-DS-BIND (:MACRO SQUARE . DEFMACRO)
10 (X)
11 (CDR #1#)
12 (DECLARE (SB-C::CONSTANT-VALUE X))
13 (BLOCK SQUARE `(* ,X ,X))))
14NIL
特殊运算符
在Lisp中,没有什么特别的语法构造,比如if
,cond
,let
,lambda
等等,都是函数或者宏。这些特殊运算符的值,也是函数或者宏。
1;;; values
2
3;; 特殊运算符
4(describe 'if)
1COMMON-LISP:IF
2 [symbol]
3IF names a special operator:
4 Lambda-list: (TEST THEN &OPTIONAL ELSE)
5 Documentation:
6 IF predicate then [else]
7
8 If PREDICATE evaluates to true, evaluate THEN and return its values,
9 otherwise evaluate ELSE and return its values. ELSE defaults to NIL.
10 Source file: SYS:SRC;COMPILER;IR1-TRANSLATORS.LISP
11NIL
也就是说,一个if
表达式,可以替换另外一个值,参与构成一个更大的表达式。
1(mapcar (lambda (f) (funcall f 1 (if t 1 2))) '(eq eql equal equalp))
2(T T T T)
1
和(if t 1 2)
都是值,它们在四种比较函数中都是相等的。
小小先生觉得Lisp真的很简单,一切都是值,一切都可以替换。
好奇先生告诉他,这就是Lisp的魅力,一切都是值,一切都可以替换,一切都可以组合。
更好玩的对称
好奇先生告诉小小先生,Lisp的对称不仅仅是括号,还有很多对称的地方。就比如reader
和printer
,reader
是Lisp中的输入函数,printer
是Lisp中的输出函数。这两个函数是对称的,一个函数的输出,可以作为另一个函数的输入。
Lisp reader n. Trad. the procedure that parses character representations of objects from a stream, producing objects. (This procedure is implemented by the function read.)
Lisp printer n. Trad. the procedure that prints the character representation of an object onto a stream. (This procedure is implemented by the function write.)
这两族函数的对称性是很明显的,也多次出现在Common Lisp的Specification(CLHS)中。前者,读入字符串,返回Lisp对象(或者说Lisp值);后者,将Lisp对象(或者说Lisp值)输出成其字符串表现形式。
stateDiagram 字符串 --> Lisp对象: Lisp Reader Lisp对象 --> 字符串: Lisp Printer
我们整个Lisp的REPL环境,就是一个reader
和printer
的交互过程。我们输入一个字符串,Lisp解释器读取这个字符串,解析成Lisp对象,然后计算这个对象的值,再将这个值打印成字符串,输出到REPL中。
Reader
这个功能包括read
和read-from-string
函数。read
函数从流中读取下一个Lisp值,read-from-string
函数从字符串中读取Lisp值。
1(read-from-string "(+ 1 2 3)")
2;; 返回值是 (+ 1 2 3)
Printer
这个功能实现的基础函数是write
,这个函数有非常多的参数和选项。
1(describe 'write)
2COMMON-LISP:WRITE
3 [symbol]
4WRITE names a compiled function:
5 Lambda-list: (OBJECT &KEY STREAM
6 ((ESCAPE *PRINT-ESCAPE*) *PRINT-ESCAPE*)
7 ((RADIX *PRINT-RADIX*) *PRINT-RADIX*)
8 ((BASE *PRINT-BASE*) *PRINT-BASE*)
9 ((CIRCLE *PRINT-CIRCLE*) *PRINT-CIRCLE*)
10 ((PRETTY *PRINT-PRETTY*) *PRINT-PRETTY*)
11 ((LEVEL *PRINT-LEVEL*) *PRINT-LEVEL*)
12 ((LENGTH *PRINT-LENGTH*) *PRINT-LENGTH*)
13 ((CASE *PRINT-CASE*) *PRINT-CASE*)
14 ((ARRAY *PRINT-ARRAY*) *PRINT-ARRAY*)
15 ((GENSYM *PRINT-GENSYM*) *PRINT-GENSYM*)
16 ((READABLY *PRINT-READABLY*) *PRINT-READABLY*)
17 ((RIGHT-MARGIN *PRINT-RIGHT-MARGIN*)
18 *PRINT-RIGHT-MARGIN*)
19 ((MISER-WIDTH *PRINT-MISER-WIDTH*) *PRINT-MISER-WIDTH*)
20 ((LINES *PRINT-LINES*) *PRINT-LINES*)
21 ((PPRINT-DISPATCH *PRINT-PPRINT-DISPATCH*)
22 *PRINT-PPRINT-DISPATCH*)
23 ((SUPPRESS-ERRORS *SUPPRESS-PRINT-ERRORS*)
24 *SUPPRESS-PRINT-ERRORS*))
25 Declared type: (FUNCTION
26 (T &KEY (:STREAM (OR STREAM BOOLEAN)) (:ESCAPE T)
27 (:RADIX T) (:BASE (INTEGER 2 36)) (:CIRCLE T)
28 (:PRETTY T) (:READABLY T)
29 (:LEVEL (OR UNSIGNED-BYTE NULL))
30 (:LENGTH (OR UNSIGNED-BYTE NULL)) (:CASE T)
31 (:ARRAY T) (:GENSYM T)
32 (:LINES (OR UNSIGNED-BYTE NULL))
33 (:RIGHT-MARGIN (OR UNSIGNED-BYTE NULL))
34 (:MISER-WIDTH (OR UNSIGNED-BYTE NULL))
35 (:PPRINT-DISPATCH T) (:SUPPRESS-ERRORS T))
36 (VALUES T &OPTIONAL))
37 Derived type: (FUNCTION
38 (T &KEY (:STREAM . #1=(T)) (:ESCAPE . #1#)
39 (:RADIX . #1#) (:BASE (INTEGER 2 36)) (:CIRCLE . #1#)
40 (:PRETTY . #1#)
41 (:LEVEL . #2=((OR UNSIGNED-BYTE NULL)))
42 (:LENGTH . #2#)
43 (:CASE (MEMBER :CAPITALIZE :DOWNCASE :UPCASE))
44 (:ARRAY . #1#) (:GENSYM . #1#) (:READABLY . #1#)
45 (:RIGHT-MARGIN . #2#) (:MISER-WIDTH . #2#)
46 (:LINES . #2#)
47 (:PPRINT-DISPATCH SB-PRETTY:PPRINT-DISPATCH-TABLE)
48 (:SUPPRESS-ERRORS . #1#))
49 (VALUES T &OPTIONAL))
50 Documentation:
51 Output OBJECT to the specified stream, defaulting to *STANDARD-OUTPUT*.
52 Known attributes: unwind, any
53 Source file: SYS:SRC;CODE;PRINT.LISP
54NIL
看看帮助就知道,为了适应使用的需要,按照不同的选项,CL提供了几个函数,比如prin1
,princ
,print
,pprint
。
prin1
函数,输出一个对象,这个对象非常适合作为read
的输入。princ
函数,输出一个对象,它不包括转义字符,这个函数的输出已经适合人类阅读。print
函数,输出一个对象,跟princ
函数类似,但是会输出一个换行符在前,一个空格在后。pprint
函数,输出一个对象,这个函数会根据对象的类型,输出稍微更加漂亮的格式。
只有前两个函数有对应的prin1-to-string
和princ-to-string
函数,这两个函数的作用是将对象输出成字符串。这个也是可以理解的,因为print
和pprint
函数的输出,是带有换行符的,可能并不适合作为字符串来处理。
一定要试一下,看看这些函数的效果。
此外,前三个函数prin1
,princ
,print
,还返回一个值,最后的pprint
函数,返回的是NIL
。
总结
- Lisp中的一切都是值,一切都可以替换。
- Lisp中的表达式是值,值是表达式。
- 一致性和对称性是Lisp的特点。
文章标签
|-->lisp |-->入门 |-->表达式 |-->可替代 |-->程序设计语言 |-->教程
- 本站总访问量:次
- 本站总访客数:人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository