常见 lisp 类型综合指南

Comprehensive guide on common lisp types

也许这个问题太笼统了,不过我会尝试: 有没有关于 common lisp 类型的综合指南?

我对这个问题有点困惑:

为什么在 make-array:element-type 中声明的非基本类型被提升为 t?是否有可能对实际声明的类型进行编译时或运行时检查?

为什么 CLOS 插槽定义的类型不能用作约束,允许将任何类型的值放入插槽?再说一遍,支票呢?

declare 函数的类型声明也是如此。它们只是对编译器的优化提示吗?

此外,我是否可以使用自定义类型说明符,包括在上述位置使用 satisfies 进行一些稳健的检查,或者它们只能用于 typep e.t.c 的显式检查?

如您所见,我脑子里有些乱七八糟的,所以非常感谢任何简洁的指南(或一组指南)。

我在使用 SBCL,但也很乐意了解实现之间的差异。

如果您希望编译器实际强制执行类型,则需要告诉编译器进行安全优化:

CL-USER> (declaim (optimize (safety 3)))
NIL
CL-USER> (defclass foobar () ())
#<STANDARD-CLASS COMMON-LISP-USER::FOOBAR>
CL-USER> (defun foo (a)
           (make-array 1 :element-type 'foobar
                         :initial-contents (list a)))
FOO
CL-USER> (foo (make-instance 'foobar))
#(#<FOOBAR {1005696CE3}>)
CL-USER> (foo 12)
;=> ERROR
CL-USER> (declaim (ftype (function (integer integer) integer) quux))
(QUUX)
CL-USER> (defun quux (a b)
           (+ a b))
QUUX
CL-USER> (quux 12 12)
24 (5 bits, #x18, #o30, #b11000)
CL-USER> (quux 12 "asd")
;=> ERROR

在 运行 时检查类型会增加一些开销(特别是如果它发生在循环中),并且可能会对单个值执行多次,因此默认情况下不会执行。

(declaim (optimize (safety 3)))

(defun some-predicate-p (a)
  (format t "~&Checking type...")
  (integerp a))

(deftype foo () `(satisfies some-predicate-p))

(defclass bar ()
  ((foo :type foo :initarg :foo)))

(declaim (ftype (function (foo) list) qwerty))
(defun qwerty (foo)
  (loop repeat 10 collecting (make-instance 'bar :foo foo)))

(qwerty 12)
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
; Checking type...
;=> (#<BAR {1003BCA213}> #<BAR {1003BCA263}> #<BAR {1003BCA2B3}>
;    #<BAR {1003BCA303}> #<BAR {1003BCA353}> #<BAR {1003BCA3A3}>
;    #<BAR {1003BCA3F3}> #<BAR {1003BCA443}> #<BAR {1003BCA493}>
;    #<BAR {1003BCA4E3}>)

如果您希望函数始终检查某个地方的类型,而不管优化设置如何,您应该手动使用 CHECK-TYPE

编译期间如何处理类型由实现定义。在 SBCL 的情况下,类型通常被视为断言,但实际行为取决于优化级别。

类型作为断言意味着如果一个函数接受一个数字 n 并产生一个字符串 s,您通常不会 假设 n 是一个数字。相反,您拥有的是 保证 如果 函数 returns,那么 n 实际上是一个数字 s 现在是一个字符串。但是,如果您重用 s,您的编译器将有机会跳过对 s 是否为字符串的检查。这通常是您想要的,因为您的函数在全球范围内可用,因此可以从任何地方调用。由于函数负责检查它们的输入,因此通常总是先检查 n 是否为数字。

然而,如果您在可以证明类型在运行时肯定不匹配(类型的交集为空)的上下文中调用函数,函数的类型声明可以为您提供帮助。为了盲目地信任类型断言,您必须降低安全级别。

注意:我最初将它发布在评论中,但为了避免它被删除,这里有一个 link 表示 CL 中类型之间关系的漂亮图形:

http://sellout.github.io/2012/03/03/common-lisp-type-hierarchy

Why are non-primitive types declared in make-array's :element-type are promoted to t? Is there any possibility for compile-time or runtime checks of the real declared type?

那里有 :element-type 参数,实现可以为数组选择优化的内存布局 - 主要是为了节省内存 space。这通常对原始类型很有用。对于其他类型,大多数 Common Lisp 运行时将没有优化的存储实现,因此声明将没有用处。

Why are CLOS slot defined types don't work as constraints, allowing to put value of any type into the slot? Again, what about the checks?

实现可以做到这一点。

Clozure CL:

? (defclass foo () ((bar :type integer :initform 0 :initarg :bar)))
#<STANDARD-CLASS FOO>
? (make-instance 'foo :bar "baz")
> Error: The value "baz", derived from the initarg :BAR,
  can not be used to set the value of the slot BAR in
  #<FOO #x302000D3EC3D>, because it is not of type INTEGER. 

The same for the functions' types declarations with declare.. Are they just the optimization hints to the compiler?

可以忽略带有 declare 的类型声明 - 例如在 Symbolics Genera 中,大多数声明将被忽略。不需要实现来处理它们。大多数实现至少会将它们解释为某些对象将属于该类型的保证,并为此创建优化代码——可能没有运行时检查 and/or 专门针对该类型的代码。但是通常需要设置相应的优化级别(速度,安全,调试,...)

此外,从 CMUCL 的编译器(SBCL 等)派生的编译器可能会使用它们进行一些编译时检查。

但是 none 的效果在 ANSI CL 标准中指定。该标准提供声明并将解释留给实现。