7.9 Gotchas
7.9.1 Contracts 与 eq?
通常来说,给一个程序添加 contract 应该要么不改变程序的行为,要么引发 contract violation。Racket 的 contract 几乎满足这一原则,但有一个例外:eq?。
eq? 过程被设计为速度快,除了保证返回 true 意味着两个值在所有方面行为完全一致之外,没有提供太多保证。在内部,它在低层实现为指针相等,因此会暴露 Racket 实现方式(以及 contract 实现方式)的信息。
#lang racket (define (make-adder x) (if (= 1 x) add1 (lambda (y) (+ x y)))) (provide (contract-out [make-adder (-> number? (-> number? number?))]))
该模块导出了 make-adder function,这是常规的柯里化加法 function,但当输入为 1 时返回 Racket 的 add1。
(eq? (make-adder 1) (make-adder 1))
会返回 #t,但不会。如果 contract 改为 any/c(甚至是 (-> number? any/c)),那么 eq? 调用就会返回 #t。
总结:不要对带有 contract 的值使用 eq?。
7.9.2 Contract 边界与 define/contract
define/contract 建立的 contract 边界(创建嵌套的 contract 边界)有时不符合直觉。当多个 function 或其他带有 contract 的值交互时尤其如此。例如,考虑下面这两个交互的 function:
> (define/contract (f x) (-> integer? integer?) x)
> (define/contract (g) (-> string?) (f "not an integer")) > (g) f: contract violation
expected: integer?
given: "not an integer"
in: the 1st argument of
(-> integer? integer?)
contract from: (function f)
blaming: top-level
(assuming the contract is correct)
at: eval:2:0
你可能会以为 g function 会因为违反与 f 的 contract 而被 blame。如果 f 和 g 直接建立 contract,blame g 是正确的。但它们并没有。相反,f 和 g 之间的访问是通过外层模块的顶层来调解的。
更准确地说,f 和模块顶层有 (-> integer? integer?) contract 调解它们的交互;g 和顶层有 (-> string?) 调解,但 f 和 g 之间没有直接的 contract。这意味着 g 的 body 中对 f 的引用实际上是模块顶层的责任,而不是 g 的。换句话说,function f 是在 g 与顶层之间没有 contract 的情况下被交给 g 的,因此 blame 会被归于顶层。
如果我们想在 g 和顶层之间添加 contract,可以使用 define/contract 的 #:freevar 声明,就能看到预期的 blame:
> (define/contract (f x) (-> integer? integer?) x)
> (define/contract (g) (-> string?) #:freevar f (-> integer? integer?) (f "not an integer")) > (g) f: contract violation
expected: integer?
given: "not an integer"
in: the 1st argument of
(-> integer? integer?)
contract from: top-level
blaming: (function g)
(assuming the contract is correct)
at: eval:6:0
总结:如果带有 contract 的两个值应当交互,将它们放在单独的模块中并在模块边界处设置 contract,或者使用 #:freevar。
7.9.3 Exists Contract 与谓词
与上面 eq? 的例子类似,#:∃ contract 可以改变程序的行为。
具体来说,null? 谓词(以及许多其他谓词)对于 #:∃ contract 返回 #f,而如果将这些 contract 之一改为 any/c,则 null? 可能改为返回 #t,根据这个布尔值在程序中的流向,可能导致任意不同的行为。
总结:不要在 #:∃ contract 上使用谓词。
7.9.4 定义递归 Contract
当定义自引用的 contract 时,自然会想到使用 define。例如,有人可能会尝试这样写一个 stream 上的 contract:
> (define stream/c (promise/c (or/c null? (cons/c number? stream/c)))) stream/c: undefined;
cannot reference an identifier before its definition
in module: top-level
不幸的是,这样做不行,因为 stream/c 的值在它定义之前就被需要了。换句话说,所有的 combinator 都会急切求值它们的参数,尽管它们接受的值不会这样做。
(define stream/c (promise/c (or/c null? (cons/c number? (recursive-contract stream/c)))))
使用 recursive-contract 会延迟对 stream/c identifier 的求值,直到 contract 被首次检查之后,这已足够确保 stream/c 已被定义。
另见 检查数据结构的属性。
7.9.5 混合使用 set! 与 contract-out
contract 库假设通过 contract-out 导出的变量不会被赋值,但并未强制保证这一点。因此,如果你试图对这些变量使用 set!,可能会感到意外。例如,考虑以下示例:
> (module server racket (define (inc-x!) (set! x (+ x 1))) (define x 0) (provide (contract-out [inc-x! (-> void?)] [x integer?])))
> (module client racket (require 'server) (define (print-latest) (printf "x is ~s\n" x)) (print-latest) (inc-x!) (print-latest)) > (require 'client)
x is 0
x is 0
两次对 print-latest 的调用都打印 0,尽管 x 的值已增加(且变化在模块 x 内部可见)。
作为变通方法,导出 accessor function,而不是直接导出变量,像这样:
#lang racket (define (get-x) x) (define (inc-x!) (set! x (+ x 1))) (define x 0) (provide (contract-out [inc-x! (-> void?)] [get-x (-> integer?)]))
总结:这是一个 bug,我们将在未来版本中修复。