Common Lisp中declare和check-type的区别

4

有人能解释一下以下两种情况的区别吗(具体是评论说的内容对我来说不太容易理解),这两种情况来自于函数的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
4个回答

5

我正在尝试自学一些CL,因此我会提供我所能提供的最佳答案。与静态语言相比,Common Lisp是动态语言。如果要使用静态语言,请查看Haskell-它对所有函数执行一堆编译时检查以确保类型匹配,并在失败时通知你。然而,在Common Lisp中,情况有些不同

然而,在Common Lisp中,变量的类型与Java或C++等语言不同。也就是说,您不需要声明每个变量可以容纳的对象类型。相反,变量可以容纳任何类型的值,并且这些值携带类型信息,可用于在运行时检查类型。因此,Common Lisp是动态类型的--类型错误会被动态检测。例如,如果您将除数字以外的其他内容传递给+函数,则Common Lisp会发出类型错误信号。另一方面,从某种意义上说,Common Lisp是一种强类型语言,因为所有类型错误都将被检测到--没有办法将一个对象视为其所属类的实例。因此,我们在函数参数中声明的变量默认情况下没有类型。这可能对您有所帮助:https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node15.html。在第一段中,它的读者如下:
需要注意的是,在Lisp中,类型是针对数据对象而不是变量的。任何变量都可以有任何Lisp对象作为其值。(可以进行显式声明,使变量仅取一组有限的值之一。但是,这种声明可以省略,并且程序仍将正确运行。这种声明只是用户可能有助于提高效率的建议。请参见declare。)
因此,每当您创建函数时,那些变量所使用的值可以是任何Lisp对象。
如果我们看一下declare,我们会看到以下内容:
declare有两个不同的用途,一个是将Lisp变量声明为“special”(这会影响变量的适当绑定的语义),另一个是提供建议以帮助Common Lisp系统(实际上是编译器)更快地运行您的Lisp代码或使用更复杂的调试选项。
最后,如果我们看一下check-type,我们可以看到:

如果place的内容不是typespec类型,则check-type会发出一个可纠正的type-error错误。

在声明和check-type两种情况下,我们都向Common Lisp系统提供了类型和类型检查的建议。让我们看一下您提供的两个示例函数。
首先,“鉴别器”函数使用declare函数来断言参数确实是数字,并且编译器不需要检查它们。而careful-discriminant函数使用check-type来确保每个变量确实是数字,然后执行操作。
您可能会问“我为什么要费心做这个?”,答案是提供更优化的函数(discriminant)或提供更好的调试和更多有关错误信息的函数(careful-discriminant)。为了显示差异,我启动了SBCL并定义了两个函数。然后,我使用disassemble来显示每个函数的机器代码。请注意,careful-discriminant执行的检查比discriminant更多,导致更多的机器代码!
(disassemble#'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

(分解 #'careful-discriminant)

; 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也可以被编译,这让一些人感到困惑。更好的答案可以在这里找到:Lisp是如何动态编译的?

4
check-type和类型声明之间的区别在于前者不能被编译器忽略(当检查失败时,可以交互式地更正输入),而后者仅是编译器的提示(更重要的是,对代码读者来说),可能会被编译器忽略。请注意保留HTML标签。

4

一个声明会影响到编译时的行为。而一个check-type形式是运行时的保护。

所以,声明表单告诉编译器“嘿,a、b、c这些参数所持有的值只能是数字”。而check-type表单则告诉函数“嘿,在执行到这个点时,请检查给定的值是否符合声明的类型。”


3

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解释器和某些编译器完全忽略它们。

  • 用于速度和空间优化。许多编译器都会这样做。它们可以使用这些类型声明来创建专用代码。

  • 用于运行时类型检查。某些实现使用类型声明进行运行时检查。

  • 用于编译时类型检查。某些实现使用类型声明进行编译时类型检查,如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 在编译时检测到并发出警告。


非常感谢!我不理解你最后一个例子subtract的实际意图,为什么要将函数参数声明为数字,并在函数体中将它们用作字符串?但如果我理解正确,在与您的第一个函数add结合使用时,其中主体中的操作与args的类型无关(listing),应该表明编译器在编译时进行类型检查。 - Student
1
@学生:因为我们在代码主体中无意中犯了一个错误,而SBCL在编译时检测到了它。这只是一个例子 - 想象一下代码会更大或更复杂。 - Rainer Joswig

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接