【问题标题】:Common Lisp difference between declare check-type声明检查类型之间的 Common Lisp 区别
【发布时间】:2020-02-06 21:53:34
【问题描述】:

有人可以解释来自function 的 CLHS 的以下两种情况之间的区别(特别是 cmets 在说什么,如果我无法理解的话):

;; 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

【问题讨论】:

    标签: common-lisp declare


    【解决方案1】:

    我正在尝试自己学习一些 CL,所以我会提供最好的答案。与静态语言相比,Common Lisp 是 dynamic language。对于静态语言,请查看 Haskell - 它会进行大量编译时检查,以确保类型与所有函数匹配,并让您知道它是否失败。但是,在 Common Lisp 中,things are a little different

    然而,在 Common Lisp 中,变量的类型并不像它们在 Java 或 C++ 等语言。也就是说,您不需要声明 每个变量可以容纳的对象类型。相反,一个变量可以 保存任何类型的值,并且这些值携带的类型信息可以 用于在运行时检查类型。因此,Common Lisp 是动态的 typed——类型错误是动态检测的。例如,如果你通过 + 函数的数字以外的东西,Common Lisp 将 表示类型错误。另一方面,Common Lisp 是一个强 从某种意义上说,所有类型错误都将是类型化语言 检测到 - 无法将对象视为类的实例 事实并非如此。

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

    需要注意的是,在 Lisp 中,数据对象是 类型,而不是变量。任何变量都可以有任何 Lisp 对象作为它的 价值。 (可以明确声明一个变量 实际上将采用有限的一组值中的一个。然而, 这样的声明可能总是被省略,程序仍然会 正确运行。此类声明仅构成来自 可能有助于提高效率的用户。见声明。)

    因此,无论何时创建函数,所使用的变量都可以将任何 Lisp 对象作为其值。

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

    declare 有两种不同的用途,一种是声明 Lisp 变量为“特殊”(这会影响适当的语义 变量的绑定),另一个是提供建议帮助 Common Lisp 系统(实际上是编译器)运行你的 Lisp 代码 更快,或更复杂的调试选项。

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

    check-type 表示类型为 type-error 的可纠正错误,如果 place 的内容不是 typespec 类型。

    在声明和检查类型这两种情况下,我们都在向 Common Lisp 系统提供有关类型和类型检查的建议。让我们看看您提供的两个示例函数。

    首先,“判别”函数使用 declare 函数来断言参数确实是数字并且编译器不需要检查它们。仔细判别函数使用check-type来确保每个变量确实是一个数字,然后进行运算。

    您可能会问“我为什么要为此烦恼?”,答案是提供更优化的函数(判别式)或提供更好调试和更多错误信息的函数(谨慎判别式)。为了显示差异,我启动了 SBCL 并定义了这两个函数。然后,我用反汇编来显示每个的机器码。请注意仔细判别式执行的检查比判别式执行的检查次数多,从而产生更多的机器代码!

    (反汇编#'判别式)

    ; 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 也可以编译,这让一些人感到困惑。最好在这里回答:How is Lisp dynamic and compiled?

    【讨论】:

      【解决方案2】:

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

      【讨论】:

        【解决方案3】:

        声明会影响编译时发生的事情check-type 表单是一个运行时守卫。

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

        【讨论】:

          【解决方案4】:

          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:因为我们在不知情的情况下在正文中犯了一个错误,SBCL 在编译时检测到它。这是一个例子——想象一下代码会更大或更复杂。
          猜你喜欢
          • 1970-01-01
          • 2017-10-05
          • 1970-01-01
          • 1970-01-01
          • 2015-11-03
          • 2010-10-26
          • 1970-01-01
          • 1970-01-01
          • 2012-01-16
          相关资源
          最近更新 更多