Common Lisp 有个完善且可变的类型系统,同时也有不同的插入、检查和操作类型相对应的工具。
可以自定义类型,变量和函数的类型声明,因此,也可以获取到编译时间的警告和错误。

值有类型,变量无类型

与类 C 的语言不同的是,Lisp 中的变量只是一个对象的占位符。在使用 [setf](http://www.lispworks.com/documentation/lw50/CLHS/Body/m_setf_.htm) 赋值给变量时,一个对象会”放置“在变量中。只要你想,也可以把别的值放在同一变量中。

也就是说,在 Common Lisp 中,对象有类型,而变量没有。如果是有 C/C++ 的背景知识的话,初次听到这个会比较吃惊。

例如:

  1. (defvar *var* 1234)
  2. *VAR*
  3. (type-of *var*)
  4. (INTEGER 0 4611686018427387903)

[type-of](http://www.lispworks.com/documentation/HyperSpec/Body/f_tp_of.htm) 函数会返回给定对象的类型。返回的结果是 类型指示器(type-specifier)
结果中的第一个元素是类型,剩下的部分是变量的其他信息。可以直接忽略掉。在此提醒,Lisp 的整数是没有极限的!

现在,用 [setf](http://www.lispworks.com/documentation/lw50/CLHS/Body/m_setf_.htm) 来给 *var* 变量重新赋值:

  1. * (setf *var* "hello")
  2. "hello"
  3. * (type-of *var*)
  4. (SIMPLE-ARRAY CHARACTER (5))

如你所见,这次返回了不一样的结果:[simple-array](http://www.lispworks.com/documentation/lw70/CLHS/Body/t_smp_ar.htm) 和长度为 5 的 [character](http://www.lispworks.com/documentation/lcl50/ics/ics-14.html)
这是因为 *var* 的值变成了 "hello",而 type-of 返回的实际上是 "hello" 的类型,而不是 *var* 的。

类型继承

Lisp 的继承关系是所有的元素都继承自 T。例如:

  1. * (describe 'integer)
  2. COMMON-LISP:INTEGER
  3. [symbol]
  4. INTEGER names the built-in-class #<BUILT-IN-CLASS COMMON-LISP:INTEGER>:
  5. Class precedence-list: INTEGER, RATIONAL, REAL, NUMBER, T
  6. Direct superclasses: RATIONAL
  7. Direct subclasses: FIXNUM, BIGNUM
  8. No direct slots.
  9. INTEGER names a primitive type-specifier:
  10. Lambda-list: (&OPTIONAL (SB-KERNEL::LOW '*) (SB-KERNEL::HIGH '*))

[describe](http://www.lispworks.com/documentation/lw51/CLHS/Body/f_descri.htm) 函数会将把 [integer](http://www.lispworks.com/documentation/lw71/CLHS/Body/t_intege.htm) 的信息都打印出来。同时,integer
也是个内建的类。这是为什么呢?

多数通用的 Lisp 类型都是由 CLOS 中的类实现的。部分是由其他的类型 “打包” 而成的。每个 CLOS 类
都对应着相对应的类型。在 Lisp 中,类型都是直接指向 [type-specifiers](http://www.lispworks.com/documentation/lw51/CLHS/Body/04_bc.htm)

[type-of](http://www.lispworks.com/documentation/HyperSpec/Body/f_tp_of.htm)[class-of](http://www.lispworks.com/documentation/HyperSpec/Body/f_clas_1.htm) 的区别有:type-of 返回的是类型的描述符,
class-of 返回的是具体的实现。

  1. * (type-of 1234)
  2. (INTEGER 0 4611686018427387903)
  3. * (class-of 1234)
  4. #<BUILT-IN-CLASS COMMON-LISP:FIXNUM>

检查类型

[typep](http://www.lispworks.com/documentation/lw51/CLHS/Body/f_typep.htm)

  1. * (typep 1234 'integer)
  2. T

[subtypep](http://www.lispworks.com/documentation/lw71/CLHS/Body/f_subtpp.htm) 用来检查类型之间的继承关系,会返回两个值:

  • T, T:第一个参数是第二个的子类
  • NIL, T:第一个参数不是第二个的子类
  • NIL, NIL:未知

例如:

  1. * (subtypep 'integer 'number)
  2. T
  3. T
  4. * (subtypep 'string 'number)
  5. NIL
  6. T

当要根据参数的类型来决定相对应的操作时,可以使用 [typecase](http://www.lispworks.com/documentation/lw60/CLHS/Body/m_tpcase.htm) 宏:

  1. * (defun plus1 (arg)
  2. (typecase arg
  3. (integer (+ arg 1))
  4. (string (concatenate 'string arg "1"))
  5. (t 'error)))
  6. PLUS1
  7. * (plus1 100)
  8. 101 (7 bits, #x65, #o145, #b1100101)
  9. * (plus1 "hello")
  10. "hello1"
  11. * (plus1 'hello)
  12. ERROR

类型说明符

类型说明符是个指定类型的表格。如上文所提到的那样,函数 type-oftypep 返回值中都有类型说明符。

  1. * (typep '#(1 2 3) '(vector number 3))
  2. T

以上的代表表示的是 vector 类型的完整信息。

有个可以指定为任意的说明符 *。如 (vector number *) 表示由任意数量的数字组成的向量。

  1. * (typep '#(1 2 3) '(vector number *))
  2. T

最后的部分可以忽略不写,默认为 *

  1. * (typep '#(1 2 3) '(vector number))
  2. T
  3. * (typep '#(1 2 3) '(vector))
  4. T

同样的,上面可以简写成如下的格式:

  1. * (typep '#(1 2 3) 'vector)
  2. T

想要深入理解的话,请参考 CLHS page

定义新的类型

[deftype](http://www.lispworks.com/documentation/lw51/CLHS/Body/m_deftp.htm) 的参数列表可以理解为直接映射到复合类型说明符的rest部分的元素。类型也被定义为可选的。

其主体要和宏的语法一致。(参见:[defmacro](http://www.lispworks.com/documentation/lw70/CLHS/Body/m_defmac.htm)

下例是定义一个元素个数少于10且元素值也要小于10的数组类型:

  1. * (defun small-number-array-p (thing)
  2. (and (arrayp thing)
  3. (<= (length thing) 10)
  4. (every #'numberp thing)
  5. (every (lambda (x) (< x 10)) thing)))
  6. * (deftype small-number-array (&optional type)
  7. `(and (array ,type 1)
  8. (satisfies small-number-array-p)))
  9. * (typep '#(1 2 3 4) '(small-number-array number))
  10. T
  11. * (typep '#(1 2 3 4) 'small-number-array)
  12. T
  13. * (typep '#(1 2 3 4 100) 'small-number-array)
  14. NIL
  15. * (small-number-array-p '#(1 2 3 4 5 6 7 8 9 0 1))
  16. NIL

类型检查

[check-type](http://www.lispworks.com/documentation/HyperSpec/Body/m_check_.htm#check-type) 宏可以在运行是进行类型检查. check-type 的参数一个是 [place](http://www.lispworks.com/documentation/HyperSpec/Body/26_glo_p.htm#place),另一个是类型说明符。
如果 place 中的内容不是所给定的类型,将会抛出 [type-error](http://www.lispworks.com/documentation/HyperSpec/Body/e_tp_err.htm#type-error) 异常。

  1. * (defun plus1 (arg)
  2. (check-type arg number)
  3. (1+ arg))
  4. PLUS1
  5. * (plus1 1)
  6. 2 (2 bits, #x2, #o2, #b10)
  7. * (plus1 "hello")
  8. ; Debugger entered on #<SIMPLE-TYPE-ERROR expected-type: NUMBER datum: "Hello">
  9. The value of ARG is "Hello", which is not of type NUMBER.
  10. [Condition of type SIMPLE-TYPE-ERROR]
  11. ...

编译时的类型检查

可以通过 [proclaim](http://www.lispworks.com/documentation/HyperSpec/Body/f_procla.htm)[declaim](http://www.lispworks.com/documentation/HyperSpec/Body/m_declai.htm)和[declare]来定义变量和函数参数。
当然,相同功能的是 CLOS section 中的 :type 槽,唯一的差别是 Lisp 标准并没有定义类型的声明。
所以,并不能保证 Lisp 编译器会在编译时进行类型检查

但是,SBCL 支持整个类型检查。

  1. (defconstant +foo+ 3)
  2. (defun bar ()
  3. (concatenate 'string "+" +foo+))
  4. ; caught WARNING:
  5. ; Constant 3 conflicts with its asserted type SEQUENCE.
  6. ; See also:
  7. ; The SBCL Manual, Node "Handling of Types"

上面的例子很简单,但是能过提现其他一些语言没有的特性,同时,在开发时帮助很有用 ;)

定义变量的类型

[declaim](http://www.lispworks.com/documentation/HyperSpec/Body/m_declai.htm).

现在定义一个全局变量 *name*

  1. (declaim (type (string) *name*))
  2. (defparameter *name* "book")

当赋值给 *name* 错误的值时,会出现 simple-type-error:

  1. (setf *name* :me)
  2. Value of :ME in (THE STRING :ME) is :ME, not a STRING.
  3. [Condition of type SIMPLE-TYPE-ERROR]

同样也可以这样对自定义的类型。现在定义一个 list-of-strings

  1. (defun list-of-strings-p (list)
  2. "Return t if LIST is non nil and contains only strings."
  3. (and (consp list)
  4. (every #'stringp list)))
  5. (deftype list-of-strings ()
  6. `(satisfies list-of-strings-p))

*all-names* 变量定义为 新定义的类型(list-of-strings)。

  1. (declaim (type (list-of-strings) *all-names*))
  2. ;; and with a wrong value:
  3. (defparameter *all-names* "")
  4. ;; we get an error:
  5. Cannot set SYMBOL-VALUE of *ALL-NAMES* to "", not of type
  6. (SATISFIES LIST-OF-STRINGS-P).
  7. [Condition of type SIMPLE-TYPE-ERROR]

也可以组合类型:

  1. (declaim (type (or null list-of-strings) *all-names*))

定义函数参数及返回值的类型

再次使用 declaim 宏,这次将和 ftype (function …) 而不是 type使用:

  1. (declaim (ftype (function (fixnum) fixnum) add))
  2. ;; ^^input ^^output [optional]
  3. (defun add (n)
  4. (+ n 1))

这次,会在编译时会得到更舒服的警告。

当将函数改成错误的返回字符串而不是 fixnum,会得到以下的警告:

  1. (defun add (n)
  2. (format nil "~a" (+ n 1)))
  3. ; caught WARNING:
  4. ; Derived type of ((GET-OUTPUT-STREAM-STRING STREAM)) is
  5. ; (VALUES SIMPLE-STRING &OPTIONAL),
  6. ; conflicting with the declared function return type
  7. ; (VALUES FIXNUM &REST T).

当在其它的函数中本该是操作字符串的地方使用 add,将会得到如下的警告:

  1. (defun bad-concat (n)
  2. (concatenate 'string (add n)))
  3. ; caught WARNING:
  4. ; Derived type of (ADD N) is
  5. ; (VALUES FIXNUM &REST T),
  6. ; conflicting with its asserted type
  7. ; SEQUENCE.

当在其它函数中不支持使用 add 的位置处使用 add,会得到如下的警告:

  1. (declaim (ftype (function (string)) bad-arg))
  2. (defun bad-arg (n)
  3. (add n))
  4. ; caught WARNING:
  5. ; Derived type of N is
  6. ; (VALUES STRING &OPTIONAL),
  7. ; conflicting with its asserted type
  8. ; FIXNUM.

这些都是在编译时执行的,或是 REPL,slime中的C-c C-c,或者 加载(load) 文件时。

定义类的槽的类型

类的槽接受 :type 的选项。但是通常不会检查初始表格的类型。SBCL 在1.5.9 版本及之后会输出以下警告:

  1. (defclass foo ()
  2. ((name :type number :initform "17")))

以上代码会在编译时抛出警告。

参见:sanity-clause, a data
serialization/contract library to check slots’ types during
make-instance (which is not compile time).

局限

复杂的类型造成了 satisfies 不会去检查函数的主体,只会检查函数的边界(及函数最后那部分语句)。
尽管 SBCL 做的很多,但也不会想静态语言那样去检查。

参考下下面的例子,将数组和字符串相加:

  1. (declaim (ftype (function () string) bad-adder))
  2. (defun bad-adder ()
  3. (let ((res 10))
  4. (loop for name in '("alice")
  5. do (incf res name)) ;; bad
  6. (format nil "finally doing sth with ~a" res)))

编译以上函数时不会有类型警告。

但是,但在函数的边界出现了比较复杂的语句时,会出现警告。

  1. (defun bad-adder ()
  2. (let ((res 10))
  3. (loop for name in '("alice")
  4. return (incf res name))))
  5. ; in: DEFUN BAD-ADDER
  6. ; (SB-INT:NAMED-LAMBDA BAD-ADDER
  7. ; NIL
  8. ; (BLOCK BAD-ADDER
  9. ; (LET ((RES 10))
  10. ; (LOOP FOR NAME IN *ALL-NAMES* RETURN (INCF RES NAME)))))
  11. ;
  12. ; caught WARNING:
  13. ; Derived type of ("a hairy form" NIL (SETQ RES (+ NAME RES))) is
  14. ; (VALUES (OR NULL NUMBER) &OPTIONAL),
  15. ; conflicting with the declared function return type
  16. ; (VALUES STRING &REST T).

那么结论是什么呢?也许是为了将代码查分成更小的函数把。

拓展阅读

  • the article Static type checking in SBCL, by Martin Cracauer
  • the article Typed List, a Primer - let’s explore Lisp’s fine-grained type hierarchy! with a shallow comparison to Haskell.
  • the Coalton library
    (pre-alpha): adding Hindley-Milner type checking to Common Lisp
    which allows for gradual adoption, in the same way Typed Racket or
    Hack allows for. It is as an embedded DSL in Lisp that resembles
    Standard ML or OCaml, but lets you seamlessly interoperate with
    non-statically-typed Lisp code (and vice versa).