Common Lisp 声明检查类型之间的区别

Common Lisp difference between declare check-type

有人可以解释一下来自 function:

上的 CLHS 的以下两种情况之间的区别吗(具体来说,如果我无法理解,评论在说什么)
;; This function assumes its callers have checked the types of the
;; arguments, and authorizes the compiler to build in that assumption.
(defun discriminant (a b c)
  (declare (number a b c))
  "Compute the discriminant for a quadratic equation."
  (- (* b b) (* 4 a c))) =>  DISCRIMINANT
(discriminant 1 2/3 -2) =>  76/9

;; This function assumes its callers have not checked the types of the
;; arguments, and performs explicit type checks before making any assumptions. 
(defun careful-discriminant (a b c)
  "Compute the discriminant for a quadratic equation."
  (check-type a number)
  (check-type b number)
  (check-type c number)
  (locally (declare (number a b c))
    (- (* b b) (* 4 a c)))) =>  CAREFUL-DISCRIMINANT
(careful-discriminant 1 2/3 -2) =>  76/9

宏的区别 check-type 并输入 declarations 是前者 不能被编译器忽略(并且,当检查失败时,可以 交互地纠正输入),而后者仅仅是提示 编译器(更重要的是,对于代码的读者) 可能被编译器忽略。

我正在尝试自己学习一些 CL,所以我会提供我能提供的最佳答案。 Common Lisp 是 dynamic language as compared to a static language. For a static language, check out Haskell - it does a bunch of compile time checks to ensure types match up for all functions and lets you know if it fails. However, in Common Lisp, things are a little different.

However, in Common Lisp, variables aren't typed the way they are in languages such as Java or C++. That is, you don't need to declare the type of object that each variable can hold. Instead, a variable can hold values of any type and the values carry type information that can be used to check types at runtime. Thus, Common Lisp is dynamically typed--type errors are detected dynamically. For instance, if you pass something other than a number to the + function, Common Lisp will signal a type error. On the other hand, Common Lisp is a strongly typed language in the sense that all type errors will be detected--there's no way to treat an object as an instance of a class that it's not.

所以我们声明为函数参数的变量默认没有类型。这对您来说可能是一本好书:https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node15.html。第一段内容如下:

It is important to note that in Lisp it is data objects that are typed, not variables. Any variable can have any Lisp object as its value. (It is possible to make an explicit declaration that a variable will in fact take on one of only a limited set of values. However, such a declaration may always be omitted, and the program will still run correctly. Such a declaration merely constitutes advice from the user that may be useful in gaining efficiency. See declare.)

所以无论何时你创建你的函数,那些正在使用的变量都可以有任何 Lisp 对象作为它的值。

如果我们在 declare 冒险,我们会看到以下内容:

There are two distinct uses of declare , one is to declare Lisp variables as "special" (this affects the semantics of the appropriate bindings of the variables), and the other is to provide advice to help the Common Lisp system (in reality the compiler) run your Lisp code faster, or with more sophisticated debugging options.

最后,如果我们查看 check-type,我们会看到:

check-type signals a correctable error of type type-error if the contents of place are not of the type typespec.

在 declare 和 check-type 这两种情况下,我们都给出了 Common Lisp 系统关于类型和类型检查的建议。让我们看看您提供的两个示例函数。

首先,"discriminant" 函数使用 declare 函数断言参数确实是数字,编译器不需要检查它们。 careful-discriminant函数使用check-type保证每个变量确实是一个数,然后进行运算

您可能会问 "Why should I bother with that?",答案是提供更优化的函数(判别式)或提供更好的调试和更多错误信息的函数(谨慎判别式)。为了显示差异,我启动了 SBCL 并定义了这两个函数。然后,我用反汇编来显示每个的机器代码。请注意 careful-discriminant 如何执行比 discriminant 更多的检查,从而导致更多的机器代码!

(反汇编#'判别式)

; disassembly for DISCRIMINANT
; Size: 83 bytes. Origin: #x10023700D7                        ; DISCRIMINANT
; 0D7:       498B5D10         MOV RBX, [R13+16]               ; thread.binding-stack-pointer
; 0DB:       48895DF8         MOV [RBP-8], RBX
; 0DF:       840425F8FF1020   TEST AL, [#x2010FFF8]           ; safepoint
; 0E6:       488B55E8         MOV RDX, [RBP-24]
; 0EA:       488B7DE8         MOV RDI, [RBP-24]
; 0EE:       FF1425C0000020   CALL QWORD PTR [#x200000C0]     ; GENERIC-*
; 0F5:       488955D8         MOV [RBP-40], RDX
; 0F9:       488B55F0         MOV RDX, [RBP-16]
; 0FD:       BF08000000       MOV EDI, 8
; 102:       FF1425C0000020   CALL QWORD PTR [#x200000C0]     ; GENERIC-*
; 109:       488B7DE0         MOV RDI, [RBP-32]
; 10D:       FF1425C0000020   CALL QWORD PTR [#x200000C0]     ; GENERIC-*
; 114:       488BFA           MOV RDI, RDX
; 117:       488B55D8         MOV RDX, [RBP-40]
; 11B:       FF1425B8000020   CALL QWORD PTR [#x200000B8]     ; GENERIC--
; 122:       488BE5           MOV RSP, RBP
; 125:       F8               CLC
; 126:       5D               POP RBP
; 127:       C3               RET
; 128:       CC10             INT3 16                         ; Invalid argument count trap
NIL

(反汇编#'小心判别式)

; disassembly for CAREFUL-DISCRIMINANT
; Size: 422 bytes. Origin: #x10023701E3                       ; CAREFUL-DISCRIMINANT
; 1E3:       4D8B4510         MOV R8, [R13+16]                ; thread.binding-stack-pointer
; 1E7:       4C8945F8         MOV [RBP-8], R8
; 1EB:       840425F8FF1020   TEST AL, [#x2010FFF8]           ; safepoint
; 1F2:       EB44             JMP L1
; 1F4:       660F1F840000000000 NOP
; 1FD:       0F1F00           NOP
; 200: L0:   488B7DF0         MOV RDI, [RBP-16]
; 204:       4883EC10         SUB RSP, 16
; 208:       488B1571FFFFFF   MOV RDX, [RIP-143]              ; 'A
; 20F:       488B3572FFFFFF   MOV RSI, [RIP-142]              ; 'NUMBER
; 216:       4C894DD8         MOV [RBP-40], R9
; 21A:       488B056FFFFFFF   MOV RAX, [RIP-145]              ; #<SB-KERNEL:FDEFN SB-KERNEL:CHECK-TYPE-ERROR>
; 221:       B906000000       MOV ECX, 6
; 226:       48892C24         MOV [RSP], RBP
; 22A:       488BEC           MOV RBP, RSP
; 22D:       FF5009           CALL QWORD PTR [RAX+9]
; 230:       4C8B4DD8         MOV R9, [RBP-40]
; 234:       488955F0         MOV [RBP-16], RDX
; 238: L1:   840425F8FF1020   TEST AL, [#x2010FFF8]           ; safepoint
; 23F:       488B45F0         MOV RAX, [RBP-16]
; 243:       448D40F1         LEA R8D, [RAX-15]
; 247:       41F6C001         TEST R8B, 1
; 24B:       7512             JNE L2
; 24D:       4180F80A         CMP R8B, 10
; 251:       740C             JEQ L2
; 253:       41F6C00F         TEST R8B, 15
; 257:       75A7             JNE L0
; 259:       8078F129         CMP BYTE PTR [RAX-15], 41
; 25D:       77A1             JNBE L0
; 25F: L2:   EB47             JMP L4
; 261:       660F1F840000000000 NOP
; 26A:       660F1F440000     NOP
; 270: L3:   488B7DE8         MOV RDI, [RBP-24]
; 274:       4883EC10         SUB RSP, 16
; 278:       488B1519FFFFFF   MOV RDX, [RIP-231]              ; 'B
; 27F:       488B3502FFFFFF   MOV RSI, [RIP-254]              ; 'NUMBER
; 286:       4C894DD8         MOV [RBP-40], R9
; 28A:       488B05FFFEFFFF   MOV RAX, [RIP-257]              ; #<SB-KERNEL:FDEFN SB-KERNEL:CHECK-TYPE-ERROR>
; 291:       B906000000       MOV ECX, 6
; 296:       48892C24         MOV [RSP], RBP
; 29A:       488BEC           MOV RBP, RSP
; 29D:       FF5009           CALL QWORD PTR [RAX+9]
; 2A0:       4C8B4DD8         MOV R9, [RBP-40]
; 2A4:       488955E8         MOV [RBP-24], RDX
; 2A8: L4:   840425F8FF1020   TEST AL, [#x2010FFF8]           ; safepoint
; 2AF:       488B45E8         MOV RAX, [RBP-24]
; 2B3:       448D40F1         LEA R8D, [RAX-15]
; 2B7:       41F6C001         TEST R8B, 1
; 2BB:       7512             JNE L5
; 2BD:       4180F80A         CMP R8B, 10
; 2C1:       740C             JEQ L5
; 2C3:       41F6C00F         TEST R8B, 15
; 2C7:       75A7             JNE L3
; 2C9:       8078F129         CMP BYTE PTR [RAX-15], 41
; 2CD:       77A1             JNBE L3
; 2CF: L5:   EB3D             JMP L7
; 2D1:       660F1F840000000000 NOP
; 2DA:       660F1F440000     NOP
; 2E0: L6:   498BF9           MOV RDI, R9
; 2E3:       4883EC10         SUB RSP, 16
; 2E7:       488B15B2FEFFFF   MOV RDX, [RIP-334]              ; 'C
; 2EE:       488B3593FEFFFF   MOV RSI, [RIP-365]              ; 'NUMBER
; 2F5:       488B0594FEFFFF   MOV RAX, [RIP-364]              ; #<SB-KERNEL:FDEFN SB-KERNEL:CHECK-TYPE-ERROR>
; 2FC:       B906000000       MOV ECX, 6
; 301:       48892C24         MOV [RSP], RBP
; 305:       488BEC           MOV RBP, RSP
; 308:       FF5009           CALL QWORD PTR [RAX+9]
; 30B:       4C8BCA           MOV R9, RDX
; 30E: L7:   840425F8FF1020   TEST AL, [#x2010FFF8]           ; safepoint
; 315:       458D41F1         LEA R8D, [R9-15]
; 319:       41F6C001         TEST R8B, 1
; 31D:       7513             JNE L8
; 31F:       4180F80A         CMP R8B, 10
; 323:       740D             JEQ L8
; 325:       41F6C00F         TEST R8B, 15
; 329:       75B5             JNE L6
; 32B:       418079F129       CMP BYTE PTR [R9-15], 41
; 330:       77AE             JNBE L6
; 332: L8:   4C894DD8         MOV [RBP-40], R9
; 336:       488B55E8         MOV RDX, [RBP-24]
; 33A:       488B7DE8         MOV RDI, [RBP-24]
; 33E:       FF1425C0000020   CALL QWORD PTR [#x200000C0]     ; GENERIC-*
; 345:       488955E0         MOV [RBP-32], RDX
; 349:       4C8B4DD8         MOV R9, [RBP-40]
; 34D:       488B55F0         MOV RDX, [RBP-16]
; 351:       BF08000000       MOV EDI, 8
; 356:       FF1425C0000020   CALL QWORD PTR [#x200000C0]     ; GENERIC-*
; 35D:       4C8B4DD8         MOV R9, [RBP-40]
; 361:       498BF9           MOV RDI, R9
; 364:       FF1425C0000020   CALL QWORD PTR [#x200000C0]     ; GENERIC-*
; 36B:       488BFA           MOV RDI, RDX
; 36E:       4C8B4DD8         MOV R9, [RBP-40]
; 372:       488B55E0         MOV RDX, [RBP-32]
; 376:       FF1425B8000020   CALL QWORD PTR [#x200000B8]     ; GENERIC--
; 37D:       4C8B4DD8         MOV R9, [RBP-40]
; 381:       488BE5           MOV RSP, RBP
; 384:       F8               CLC
; 385:       5D               POP RBP
; 386:       C3               RET
; 387:       CC10             INT3 16                         ; Invalid argument count trap
NIL

看到这里,Common Lisp也可以编译,这让一些人感到困惑。最好在这里回答:How is Lisp dynamic and compiled?

声明会影响编译时发生的事情。 check-type形式是运行时间守卫。

所以,声明形式说“嘿编译器,参数 a、b、c 所持有的值只能是数字”。 check-type 形式表示“嘿函数,在执行的这一点上,检查给定值是否属于指定类型”。

CHECK-TYPE:运行时类型检查和修复

check-type 进行实际的运行时检查。它还通常提供一种交互式修复值的方法。

* (let ((a "1"))
    (check-type a number)
    (+ a 2))

debugger invoked on a SIMPLE-TYPE-ERROR in thread
#<THREAD "main thread" RUNNING {10005184C3}>:
  The value of A is "1", which is not of type NUMBER.

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

restarts (invokable by number or by possibly-abbreviated name):
  0: [STORE-VALUE] Supply a new value for A.
  1: [ABORT      ] Exit debugger, returning to top level.

(SB-KERNEL:CHECK-TYPE-ERROR A "1" NUMBER NIL)
0] 0

Enter a form to be evaluated: 1
3

DECLARE : 类型声明

Common Lisp 是动态类型的:每个数据对象都有一个类型。

Common Lisp 还允许对变量和函数使用静态类型。有

  • 各种类型和复合类型
  • 使用 deftype
  • 定义新类型的方法
  • 类型声明 declare
  • 子类型检查 subtypep
  • 运行时类型检查 typep
  • typecasectypecaseetypecase
  • 条件运行时类型

现在 Common Lisp 实现对各种事物使用类型声明,并且它们对它们所做的事情是高度特定于实现的。

Common Lisp编译器中(declare (type ...))静态类型声明的主要用法是:

  • 无视他们。通常 Lisp 解释器 和一些 编译器 完全忽略它们

  • 将它们用于速度和 space 优化。这是由许多编译器完成的。他们可以使用这些类型声明来创建专门的代码。

  • 将它们用于运行时类型检查。一些实现使用类型声明进行运行时检查。

  • 将它们用于编译时类型检查。一些实现使用类型声明进行编译时类型检查。例如 sbcl 和 cmucl。

请注意,Common Lisp 标准并未说明如何使用这些类型声明。它只是提供了一种语法来定义和声明类型。 Common Lisp 实现要么使用它们,要么忽略它们。

在 SBCL 和 CMUCL 中可以找到类型声明的特别复杂的用法。

类型检查示例

让我们看看 SBCL 如何使用类型声明进行运行时和编译时类型检查:

使用 SBCL 进行运行时类型检查:

* (defun add (a b)
    (declare (type number a b))
    (list a b))
ADD
* (add 1 "3")

debugger invoked on a TYPE-ERROR in thread
#<THREAD "main thread" RUNNING {10005184C3}>:
  The value
    "3"
  is not of type
    NUMBER
  when binding B

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.

(ADD 1 "3") [external]
   source: (SB-INT:NAMED-LAMBDA ADD
               (A B)
             (DECLARE (TYPE NUMBER A B))
             (BLOCK ADD (LIST A B)))
0] 

正如我们所见,SBCL 使用类型声明进行运行时检查。但不同于check-type它不提供提供不同的值和相应的重新检查...

使用 SBCL 进行编译时类型检查:

* (defun subtract (a b)
    (declare (type number a b))
    (concatenate 'string a "-" b " is " (- a b)))

; in: DEFUN SUBTRACT
;     (CONCATENATE 'STRING A "-" B " is " (- A B))
; 
; caught WARNING:
;   Derived type of (SB-KERNEL:SYMEVAL 'A) is
;     (VALUES NUMBER &OPTIONAL),
;   conflicting with its asserted type
;     SEQUENCE.
;   See also:
;     The SBCL Manual, Node "Handling of Types"
; 
; compilation unit finished
;   caught 1 WARNING condition
SUBTRACT

如您所见,我们正在尝试使用数字作为序列。 SBCL 在编译时检测到并发出警告。