7.2 Simple Contracts on Functions
数学函数有 domain(定义域)和 range(值域)。 domain 表示函数接受什么类型的参数,range 表示函数产生什么类型的值。 domain 和 range 的传统表示法为
f : A -> B
其中 A 是函数的 domain,B 是 range。
编程语言中的函数也有 domain 和 range,contract 可以确保函数只接收 其 domain 中的值并只产生其 range 中的值。-> 为函数创建这样的 contract。-> 后面的 form 依次指定 domain 的 contract 和最终的 range 的 contract。
这里是一个可能代表银行账户的 module:
#lang racket (provide (contract-out [deposit (-> number? any)] [balance (-> number?)])) (define amount 0) (define (deposit a) (set! amount (+ amount a))) (define (balance) amount)
该 module 导出两个函数:
deposit,接受一个 number 并返回 contract 中未指定类型的值,以及
balance,返回一个 number 表示账户的当前余额。
当 module 导出一个函数时,它在自己(作为 "server")和导入该函数的\n"client" module 之间建立了两条通信通道。如果 client module 调用该函数,\n它就向 server module 发送值。反之,如果函数调用结束并且函数\n返回值,server module 就向 client module 发送值。client–server 区分\n很重要,因为一旦出错,责任方只能是其中一方。
如果 client module 将 deposit 应用于 'millions, 这违反了 contract。Contract-monitoring 系统会捕获这个违反并 blame client 破坏了与上面模块的 contract。相反,如果 balance 函数返回 'broke,contract-monitoring 系统会 blame server module。
-> 本身不是 contract;它是 contract combinator, 将其他 contract 组合成一个新的 contract。
7.2.1 Styles of ->
如果你习惯于数学函数,你可能更希望 contract 箭头出现在函数的 domain 和 range 之间,而不是在前面。如果你读过 How to Design Programs,你已经 多次见过这种形式。事实上,你可能在其他人的代码中见过这样的 contract:
(provide (contract-out [deposit (number? . -> . any)]))
如果 Racket S-expression 包含两个 dot 且中间有一个 symbol, 该 reader 会重新排列 S-expression,把 symbol 放到前面, 如 Lists and Racket Syntax 所述。因此,
(number? . -> . any)
只是另一种写法
(-> number? any)
7.2.2 Using define/contract and ->
在 Experimenting with Nested Contract Boundaries 中介绍的 define/contract 形式 也可以用于定义带有 contract 的函数。例如,
(define/contract (deposit amount) (-> number? any) ; implementation goes here ....)
定义了 deposit 函数并附带之前的 contract。 注意这对于 deposit 的使用有两个潜在的重要影响:
contract 会在任何 deposit 的调用处被检查, 包括在其定义 module 内部的调用——只要调用位于定义外部。 因为 module 内部可能有很多次调用,这种检查可能导致 contract 被检查过于频繁,从而造成性能下降。 如果函数在循环中被重复调用,这点尤其突出。
在某些情况下,函数可能被设计为在同 module 内其他代码 调用时接受更宽松的输入集合。对于这种使用场景, define/contract 建立的 contract 边界过于严格。
7.2.3 any and any/c
用于 deposit 的 any contract 匹配任何类型的结果, 而且只能用于函数 contract 的 range 位置。我们可以用更具体的 void? contract 来代替上面的 any,它说明函数 始终返回 (void) 值。但 void? contract 会要求 contract monitoring 系统在每次调用时都检查返回值,即使 "client" module 无法对该值做多少处理。相比之下,any 告诉 monitoring 系统 不要检查返回值,它告诉潜在 client 该 "server" module 对函数的返回值 不做任何承诺,甚至不承诺是单个值还是多个值。
any/c contract 与 any 类似,因为都不对值提出 任何要求。不同之处在于,any/c 表示单个值,因此适合 用作 argument contract。将 any/c 用作 range contract 会 强制检查函数产生单个值。也就是说,
(-> integer? any)
描述了一个接受 integer 并返回任意数量值的函数,而
(-> integer? any/c)
描述了一个接受 integer 并产生单一结果的函数(但对结果不再做 更多描述)。函数
(define (f x) (values (+ x 1) (- x 1)))
匹配 (-> integer? any),但不匹配 (-> integer? any/c)。
当你需要特别保证函数返回单一结果时使用 any/c 作为 result contract。当你想对函数结果做出尽可能少的承诺(并产生尽可能 少的检查)时使用 any。
7.2.4 自定义 Contract
deposit 函数将给定的数字加到 amount 的值上。 虽然函数的 contract 阻止 client 将其应用于非数字,但仍然允许 将其应用于 complex number、negative number 或 inexact number, 它们都不是合理的金额表示。
Contract 系统允许程序员以函数形式定义自己的 contract:
#lang racket (define (amount? a) (and (number? a) (integer? a) (exact? a) (>= a 0))) (provide (contract-out ; an amount is a natural number of cents ; is the given number an amount? [deposit (-> amount? any)] [amount? (-> any/c boolean?)] [balance (-> amount?)])) (define amount 0) (define (deposit a) (set! amount (+ amount a))) (define (balance) amount)
该 module 定义了一个 amount? 函数并将其作为 -> contract 内的 contract。当 client 调用以 (-> amount? any) contract 导出的 deposit 函数时,它必须提供一个 exact 的 nonnegative integer,否则将 amount? 函数应用于参数将返回 #f,这将导致 contract-monitoring 系统 blame client。 类似地,server module 必须提供 exact 的 nonnegative integer 作为 balance 的结果以维持无责状态。
当然,将通信通道限制为 client 不理解的值是没有意义的。因此 该 module 也导出了 amount? predicate 本身,附带一个说明它 接受任意值并返回 boolean 的 contract。
在这种情况下,我们也可以用 natural-number/c 来代替 amount?,因为它表示完全相同的检查:
(provide (contract-out [deposit (-> natural-number/c any)] [balance (-> natural-number/c)]))
每个接受一个参数的函数都可以被当作 predicate,因此可以作为 contract 使用。然而,要将现有检查组合成新的检查,and/c 和 or/c 等 contract combinator 通常很有用。例如, 这是编写上述 contract 的另一种方法:
(define amount/c (and/c number? integer? exact? (or/c positive? zero?))) (provide (contract-out [deposit (-> amount/c any)] [balance (-> amount/c)]))
其他值也可以兼作 contract。例如,如果函数接受 number 或 #f,(or/c number? #f) 就足够了。类似地, amount/c contract 也可以用 0 来代替 zero?。如果你使用 regular expression 作为 contract, 该 contract 接受与该 regular expression 匹配的 string 和 byte string。
自然,你可以将自己实现的 contract-implementing 函数与 and/c 等 combinator 混合使用。这里是一个从 bank 记录创建 string 的 module:
#lang racket (define (has-decimal? str) (define L (string-length str)) (and (>= L 3) (char=? #\. (string-ref str (- L 3))))) (provide (contract-out ; convert a random number to a string [format-number (-> number? string?)] ; convert an amount into a string with a decimal ; point, as in an amount of US currency [format-nat (-> natural-number/c (and/c string? has-decimal?))]))
如果我们想加强 format-nat 的 range contract 的承诺, 使其只接受 digit 和单个 dot 组成的 string,可以这样写:
#lang racket (define (digit-char? x) (member x '(#\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\0))) (define (has-decimal? str) (define L (string-length str)) (and (>= L 3) (char=? #\. (string-ref str (- L 3))))) (define (is-decimal-string? str) (define L (string-length str)) (and (has-decimal? str) (andmap digit-char? (string->list (substring str 0 (- L 3)))) (andmap digit-char? (string->list (substring str (- L 2) L))))) .... (provide (contract-out .... ; convert an amount (natural number) of cents ; into a dollar-based string [format-nat (-> natural-number/c (and/c string? is-decimal-string?))]))
另外,在这种情况下,我们可以用 regular expression 作为 contract:
#lang racket (provide (contract-out .... ; convert an amount (natural number) of cents ; into a dollar-based string [format-nat (-> natural-number/c (and/c string? #rx"[0-9]*\\.[0-9][0-9]"))]))
7.2.5 高阶函数上的 Contract
函数 contract 不只限于在 domain 或 range 上使用简单的 predicate。 这里讨论的任何 contract combinator,包括函数 contract 本身, 都可以用作函数参数和结果的 contract。
例如,
(-> integer? (-> integer? integer?))
是描述 curried function 的 contract。它匹配接受一个参数、然后返回 另一个接受第二个参数的函数、最后返回 integer 的函数。如果 server 导出一个带此 contract 的函数 make-adder,而 make-adder 返回的值不是函数,则 server 被 blame。如果 make-adder 确实返回函数,但结果函数的调用参数不是 integer,则 client 被 blame。
类似地,contract
(-> (-> integer? integer?) integer?)
描述接受其他函数作为输入的函数。如果 server 导出一个带此 contract 的函数 twice,而 twice 的调用参数不是单参数函数, 则 client 被 blame。如果 twice 应用于单参数函数且 twice 在 non-integer 值上调用给定函数,则 server 被 blame。
7.2.6 包含 “???” 的 Contract 消息
> (module bank-server racket (provide (contract-out [deposit (-> (λ (x) (and (number? x) (integer? x) (>= x 0))) any)])) (define total 0) (define (deposit a) (set! total (+ a total))))
几个 client 使用了你的 module。其他人又使用了他们的 module。 然后突然其中一个看到了这条错误消息:
> (require 'bank-server) > (deposit -10) deposit: contract violation
expected: ???
given: -10
in: the 1st argument of
(-> ??? any)
contract from: bank-server
blaming: top-level
(assuming the contract is correct)
at: eval:2:0
??? 在那里做什么?如果我们能为这类数据起一个名字, 就像 string、number 等一样,是不是很好?
对于这种情况,Racket 提供 flat named contract。这里 "contract" 一词表明 contract 是 first-class value。"flat" 意味着 数据集合是内置 atomic 数据类的子集;它们通过消费所有 Racket 值并产生 boolean 的 predicate 来描述。"named" 部分表示我们想做的事情:为 contract 命名,使 error 消息变得 可读:
> (module improved-bank-server racket (provide (contract-out [deposit (-> (flat-named-contract 'amount (λ (x) (and (number? x) (integer? x) (>= x 0)))) any)])) (define total 0) (define (deposit a) (set! total (+ a total))))
经过这个小改动,error 消息变得相当可读:
> (require 'improved-bank-server) > (deposit -10) deposit: contract violation
expected: amount
given: -10
in: the 1st argument of
(-> amount any)
contract from: improved-bank-server
blaming: top-level
(assuming the contract is correct)
at: eval:5:0
7.2.7 剖析 contract 错误消息
- 与 contract 关联的函数或方法的名称,以及根据 contract 是被 client 还是 server 违反的,显示 "contract violation" 或 "broke its contract" 短语;例如在上面的 例子中:
deposit: contract violation
- 对 contract 被违反的精确方面的描述,
expected: amount
given: -10
- 完整 contract 及其内部路径,显示违反的具体方面,
in: the 1st argument of
(-> amount any)
- contract 所在的 module(或更一般的说,contract 所 调解的 boundary),
contract from: improved-bank-server
- 被 blame 的一方,
blaming: top-level
(assuming the contract is correct)
- 以及 contract 所在源代码位置。
at: eval:5:0