7.3 Contracts on Functions in General
-> 契约构造器适用于接受固定数量参数且结果契约不依赖于输入参数的函数。为了支持其他类型的函数,Racket 提供了额外的契约构造器,尤其是 ->* 和 ->i。
7.3.1 Optional Arguments
以下是字符串处理模块的一个片段:
#lang racket (provide (contract-out ; pad the given str left and right with ; the (optional) char so that it is centered [string-pad-center (->* (string? natural-number/c) (char?) string?)])) (define (string-pad-center str width [pad #\space]) (define field-width (min width (string-length str))) (define rmargin (ceiling (/ (- width field-width) 2))) (define lmargin (floor (/ (- width field-width) 2))) (string-append (build-string lmargin (λ (x) pad)) str (build-string rmargin (λ (x) pad))))
该模块导出了 string-pad-center,一个创建给定 width 宽度、 将给定字符串居中的函数。默认填充字符是 #\space;如果客户端模块希望使用 不同的字符,可以用第三个参数 char 调用 string-pad-center, 覆盖默认值。
函数定义使用了可选参数,这对于这种功能来说很合适。这里有趣的是 string-pad-center 的契约描述方式。
契约组合子 ->* 要求几组契约:
第一组是所有必需参数的括号包裹的契约。在此示例中,我们看到两个:string? 和 natural-number/c。
第二组是所有可选参数的括号包裹的契约:char?。
最后一组是一个单独的契约:函数的结果。
注意,如果默认值不满足契约,你不会在此接口上获得 contract 错误。如果你不确定自己能正确设置初始值,则需要跨边界传递初始值。
7.3.2 剩余参数
max 操作符至少消耗一个实数,但接受任意数量的附加参数。你可以使用 rest argument 来编写其他类似的函数,例如 max-abs:
参见 Declaring a Rest Argument 了解 rest 参数的介绍。
(define (max-abs n . rst) (foldr (lambda (n m) (max (abs n) m)) (abs n) rst))
要通过契约描述这个函数,可以使用 -> 的 ... 特性。
(provide (contract-out [max-abs (-> real? real? ... real?)]))
或者,你可以使用 ->* 配合 #:rest 关键字,它指定一个对必选和可选参数之后的参数列表的契约:
(provide (contract-out [max-abs (->* (real?) () #:rest (listof real?) real?)]))
与 ->* 一贯的用法一样,必选参数的契约被包围在第一对括号中,在这个例子中是一个单独的实数。 空的括号表示没有可选参数(不计剩余参数)。剩余参数的契约跟在 #:rest 之后; 因为所有附加参数必须是实数,所以剩余参数的列表必须满足 (listof real?)。
7.3.3 关键字参数
事实上,-> 契约构造器也支持关键字参数。例如,考虑这个创建一个简单 GUI 并向用户提出是/否问题的函数:
参见 Declaring Keyword Arguments 了解关键字参数的介绍。
#lang racket/gui (define (ask-yes-or-no-question question #:default answer #:title title #:width w #:height h) (define d (new dialog% [label title] [width w] [height h])) (define msg (new message% [label question] [parent d])) (define (yes) (set! answer #t) (send d show #f)) (define (no) (set! answer #f) (send d show #f)) (define yes-b (new button% [label "Yes"] [parent d] [callback (λ (x y) (yes))] [style (if answer '(border) '())])) (define no-b (new button% [label "No"] [parent d] [callback (λ (x y) (no))] [style (if answer '() '(border))])) (send d show #t) answer) (provide (contract-out [ask-yes-or-no-question (-> string? #:default boolean? #:title string? #:width exact-integer? #:height exact-integer? boolean?)]))
如果你确实想通过 GUI 提出是/否问题,应使用 message-box/custom。实际上,通常最好提供比"是"和"否"更具体答案的按钮。
ask-yes-or-no-question 的契约使用 ->,就像 lambda(或基于 define 的函数)允许关键字出现在函数形式参数之前一样,-> 允许关键字出现在函数契约的参数契约之前。在此情况下,契约规定 ask-yes-or-no-question 必须接收四个关键字参数,分别对应关键字 #:default、#:title、#:width 和 #:height。与函数定义一样,-> 中关键字之间的相对顺序对函数的客户端无关紧要;只有不带关键字的参数契约的相对顺序才重要。
7.3.4 Optional 关键字参数
当然,ask-yes-or-no-question 中的许多参数(源自前面的示例) 有合理的默认值,应该设为可选:
(define (ask-yes-or-no-question question #:default answer #:title [title "Yes or No?"] #:width [w 400] #:height [h 200]) ...)
为了指定此函数的契约,我们需要再次使用 ->*。它在可选和必需参数部分都支持关键字,正如你所预期的那样。在此情况下,我们有必需关键字 #:default 以及可选关键字 #:title、#:width 和 #:height。因此,我们这样编写契约:
(provide (contract-out [ask-yes-or-no-question (->* (string? #:default boolean?) (#:title string? #:width exact-integer? #:height exact-integer?) boolean?)]))
也就是说,我们把必选关键字放在第一部分,可选的关键字放在第二部分。
7.3.5 case-lambda 的契约
用 case-lambda 定义的函数可能会根据提供的参数数量对其参数施加不同的约束。 例如,一个 report-cost 函数可能将一对数字或一个字符串转换为一个新字符串:
参见 Arity-Sensitive Functions: case-lambda 了解 case-lambda 的介绍。
(define report-cost (case-lambda [(lo hi) (format "between $~a and $~a" lo hi)] [(desc) (format "~a of dollars" desc)]))
> (report-cost 5 8) "between $5 and $8"
> (report-cost "millions") "millions of dollars"
(provide (contract-out [report-cost (case-> (integer? integer? . -> . string?) (string? . -> . string?))]))
7.3.6 参数与结果之间的依赖关系
下面是一个虚构的数值模块的片段:
(provide (contract-out [real-sqrt (->i ([argument (>=/c 1)]) [result (argument) (<=/c argument)])]))
"indy" 一词意在暗示 blame 可能归于契约本身,因为契约必须被视为一个独立的组件。该名称是为回应研究文献中函数契约不同语义的两个现有标签——"lax" 和 "picky"——而选定的。
一般而言,一个依赖函数契约看起来与更通用的 ->* 契约类似, 但添加了可在契约其他位置使用的名称。
回到银行账户的例子,假设我们将模块泛化以支持多个账户,并且包含一个取款操作。 改进后的银行账户模块包含一个 account 结构类型和以下函数:
(provide (contract-out [balance (-> account? amount/c)] [withdraw (-> account? amount/c account?)] [deposit (-> account? amount/c account?)]))
除了要求客户端为取款提供一个有效金额外,金额还应小于或等于指定账户的余额, 且生成的账户将比开始时钱更少。类似地,模块可能承诺存款会产生一个金额增加的账户。 以下实现通过契约强制实施这些约束和保证:
#lang racket ; section 1: the contract definitions (struct account (balance)) (define amount/c natural-number/c) ; section 2: the exports (provide (contract-out [create (amount/c . -> . account?)] [balance (account? . -> . amount/c)] [withdraw (->i ([acc account?] [amt (acc) (and/c amount/c (<=/c (balance acc)))]) [result (acc amt) (and/c account? (lambda (res) (>= (balance res) (- (balance acc) amt))))])] [deposit (->i ([acc account?] [amt amount/c]) [result (acc amt) (and/c account? (lambda (res) (>= (balance res) (+ (balance acc) amt))))])])) ; section 3: the function definitions (define balance account-balance) (define (create amt) (account amt)) (define (withdraw a amt) (account (- (account-balance a) amt))) (define (deposit a amt) (account (+ (account-balance a) amt)))
section 2 中的契约为 create 和 balance 提供了典型的类型式保证。 但对于 withdraw 和 deposit,契约检查并保证对 balance 和 deposit 的更复杂约束。 withdraw 第二个参数的契约使用 (balance acc) 来检查提供的取款金额是否足够小, 其中 acc 是在 ->i 内给函数第一个参数的名称。 withdraw 结果的契约同时使用 acc 和 amt 来保证取出的钱不超过请求的金额。 deposit 的契约类似地在结果契约中使用 acc 和 amount 来保证至少存入了所提供金额的钱到账户中。
如上所述,当契约检查失败时,错误消息不够友好。 下面的修订版在辅助函数 mk-account-contract 中使用 flat-named-contract 来提供更好的错误消息。
#lang racket ; section 1: the contract definitions (struct account (balance)) (define amount/c natural-number/c) (define msg> "account a with balance larger than ~a expected") (define msg< "account a with balance less than ~a expected") (define (mk-account-contract acc amt op msg) (define balance0 (balance acc)) (define (ctr a) (and (account? a) (op balance0 (balance a)))) (flat-named-contract (format msg balance0) ctr)) ; section 2: the exports (provide (contract-out [create (amount/c . -> . account?)] [balance (account? . -> . amount/c)] [withdraw (->i ([acc account?] [amt (acc) (and/c amount/c (<=/c (balance acc)))]) [result (acc amt) (mk-account-contract acc amt >= msg>)])] [deposit (->i ([acc account?] [amt amount/c]) [result (acc amt) (mk-account-contract acc amt <= msg<)])])) ; section 3: the function definitions (define balance account-balance) (define (create amt) (account amt)) (define (withdraw a amt) (account (- (account-balance a) amt))) (define (deposit a amt) (account (+ (account-balance a) amt)))
7.3.7 检查状态变更
(->i ([parent (is-a?/c area-container-window<%>)]) [_ (parent) (let ([old-children (send parent get-children)]) (λ (child) (andmap eq? (append old-children (list child)) (send parent get-children))))])
值域契约确保函数仅通过向列表前面添加新子元素来修改 parent 的子元素。 它通过使用 _ 而非普通标识符来实现这一点, 这告诉契约库值域契约不依赖于任何结果的值, 因此契约库在函数被调用时计算 _ 后面的表达式,而非在函数返回时调用 get-children。 因此对 get-children 方法的调用发生在被契约包裹的函数被调用之前。 当被契约包裹的函数返回时,其结果作为 child 传入, 契约确保函数返回后的子元素与函数调用前的子元素相同,只是在列表前面多了一个子元素。
#lang racket (define x '()) (define (get-x) x) (define (f) (set! x (cons 'f x))) (provide (contract-out [f (->i () [_ () (begin (set! x (cons 'ctc x)) any/c)])] [get-x (-> (listof symbol?))]))
(->i () [res () (begin (set! x (cons 'ctc x)) any/c)])
7.3.8 多返回值
(define (split l) (define (split l w) (cond [(null? l) (values (list->string (reverse w)) '())] [(char=? #\newline (car l)) (values (list->string (reverse w)) (cdr l))] [else (split (cdr l) (cons (car l) w))])) (split l '()))
(provide (contract-out [split (-> (listof char?) (values string? (listof char?)))]))
(provide (contract-out [split (->* ((listof char?)) () (values string? (listof char?)))]))
(define (substring-of? s) (flat-named-contract (format "substring of ~s" s) (lambda (s2) (and (string? s2) (<= (string-length s2) (string-length s)) (equal? (substring s 0 (string-length s2)) s2))))) (provide (contract-out [split (->i ([fl (listof char?)]) (values [s (fl) (substring-of? (list->string fl))] [c (listof char?)]))]))
(provide (contract-out [split (->i ([fl (listof char?)]) (values [s (fl) (string-len/c (+ 1 (length fl)))] [c (listof char?)]))]))
7.3.9 固定但静态未知的元数
想象你正在为一个函数写契约,该函数接受另一个函数和一个数字列表, 最终将前者应用于后者。除非给定函数的元数与给定列表的长度匹配,否则你的过程就会陷入困境。
; (number ... -> (union #f number?)) (listof number) -> void (define (n-step proc inits) (let ([inc (apply proc inits)]) (when inc (n-step proc (map (λ (x) (+ x inc)) inits)))))
n-step 的参数是 proc(一个结果为数字或 false 的函数)和一个列表。 然后它将 proc 应用于列表 inits。只要 proc 返回一个数字, n-step 将该数字视为 inits 中每个数字的增量并递归。 当 proc 返回 false 时,循环停止。
; nat -> nat (define (f x) (printf "~s\n" x) (if (= x 0) #f -1)) (n-step f '(2)) ; nat nat -> nat (define (g x y) (define z (+ x y)) (printf "~s\n" (list x y z)) (if (= z 0) #f -1)) (n-step g '(1 1))
(->* () #:rest (listof any/c) (or/c number? #f))
(provide (contract-out [n-step (->i ([proc (inits) (and/c (unconstrained-domain-> (or/c #f number?)) (λ (f) (procedure-arity-includes? f (length inits))))] [inits (listof number?)]) () any)]))