On this page:
7.9.1 Contracts 与 eq?
7.9.2 Contract 边界与 define/  contract
7.9.3 Exists Contract 与谓词
7.9.4 定义递归 Contract
7.9.5 混合使用 set!contract-out

7.9 Gotchas🔗

7.9.1 Contracts 与 eq?🔗

通常来说,给一个程序添加 contract 应该要么不改变程序的行为,要么引发 contract violation。Racket 的 contract 几乎满足这一原则,但有一个例外:eq?

eq? 过程被设计为速度快,除了保证返回 true 意味着两个值在所有方面行为完全一致之外,没有提供太多保证。在内部,它在低层实现为指针相等,因此会暴露 Racket 实现方式(以及 contract 实现方式)的信息。

Contract 与 eq? 交互不佳,因为 function contract 的检查在内部是通过 wrapper function 实现的。例如,考虑以下模块:
#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。如果 fg 直接建立 contract,blame g 是正确的。但它们并没有。相反,fg 之间的访问是通过外层模块的顶层来调解的。

更准确地说,f 和模块顶层有 (-> integer? integer?) contract 调解它们的交互;g 和顶层有 (-> string?) 调解,但 fg 之间没有直接的 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,我们将在未来版本中修复。