7.4 Contracts: A Thorough Example
返回列表 lst 中使 proc 结果最大的第一个元素。
> (argmax add1 (list 1 2 3)) 3
> (argmax sqrt (list 0.4 0.9 0.16)) 0.9
> (argmax second '((a 2) (b 3) (c 4) (d 1) (e 4))) '(c 4)
version 1
#lang racket (define (argmax f lov) ...) (provide (contract-out [argmax (-> (-> any/c real?) (and/c pair? list?) any/c)]))
给定的函数必须产生可根据 < 比较的数字。特别是,契约 (-> any/c number?) 是不够的,因为 number? 在 Racket 中还识别复数。
给定的列表必须至少包含一个元素。
version 2
#lang racket (define (argmax f lov) ...) (provide (contract-out [argmax (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) () (r (f lov) (lambda (r) (define f@r (f r)) (for/and ([v lov]) (>= f@r (f v))))))]))
version 2 rev. a
#lang racket (define (argmax f lov) ...) (provide (contract-out [argmax (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) () (r (f lov) (lambda (r) (define f@r (f r)) (and (memq r lov) (for/and ([v lov]) (>= f@r (f v)))))))]))
version 3
#lang racket (define (argmax f lov) ...) (provide (contract-out [argmax (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) () (r (f lov) (lambda (r) (define f@r (f r)) (and (for/and ([v lov]) (>= f@r (f v))) (eq? (first (memf (lambda (v) (= (f v) f@r)) lov)) r)))))]))
第二个改进步骤引入了两个问题。首先,两个条件都重新计算了 lov 中所有元素的 f 值。其次,契约现在变得相当难以阅读。契约应该有一个简洁的表述,让客户能够通过简单扫描理解。让我们通过两个具有合理含义名称的辅助函数来消除可读性问题:
version 3 rev. a
#lang racket (define (argmax f lov) ...) (provide (contract-out [argmax (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) () (r (f lov) (lambda (r) (define f@r (f r)) (and (is-first-max? r f@r f lov) (dominates-all f@r f lov)))))])) ; where ; f@r is greater or equal to all (f v) for v in lov (define (dominates-all f@r f lov) (for/and ([v lov]) (>= f@r (f v)))) ; r is eq? to the first element v of lov for which (pred? v) (define (is-first-max? r f@r f lov) (eq? (first (memf (lambda (v) (= (f v) f@r)) lov)) r))
这一步给我们留下了新引入的低效问题。为了避免对 lov 中所有 v 重新计算 (f v),我们修改契约使其计算这些值并在需要时重用它们:
version 3 rev. b
#lang racket (define (argmax f lov) ...) (provide (contract-out [argmax (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) () (r (f lov) (lambda (r) (define f@r (f r)) (define flov (map f lov)) (and (is-first-max? r f@r (map list lov flov)) (dominates-all f@r flov)))))])) ; where ; f@r is greater or equal to all f@v in flov (define (dominates-all f@r flov) (for/and ([f@v flov]) (>= f@r f@v))) ; r is (first x) for the first x in lov+flov s.t. (= (second x) f@r) (define (is-first-max? r f@r lov+flov) (define fst (first lov+flov)) (if (= (second fst) f@r) (eq? (first fst) r) (is-first-max? r f@r (rest lov+flov))))
"eager" 一词来自契约语言学的文献。
当涉及调用 f 时,版本 3 可能仍然过于 eager。虽然 Racket 的 argmax 无论 lov 包含多少项总是调用 f,但为了说明目的,让我们假设我们自己的实现首先检查列表是否为单例。如果是,第一个元素将是 lov 的唯一元素,在这种情况下无需计算 (f r)。 Racket 的 argmax 隐式地论证了它不仅承诺在 lov 上最大化 f 的第一个值,而且还承诺 f 为结果产生/产生了一个值。 事实上,由于 f 可能对某些输入发散或引发异常,argmax 应在可能时避免调用 f。
以下契约展示了如何调整高阶依赖契约以避免过于 eager:
version 4
#lang racket (define (argmax f lov) (if (empty? (rest lov)) (first lov) ...)) (provide (contract-out [argmax (->i ([f (-> any/c real?)] [lov (and/c pair? list?)]) () (r (f lov) (lambda (r) (cond [(empty? (rest lov)) (eq? (first lov) r)] [else (define f@r (f r)) (define flov (map f lov)) (and (is-first-max? r f@r (map list lov flov)) (dominates-all f@r flov))]))))])) ; where ; f@r is greater or equal to all f@v in flov (define (dominates-all f@r lov) ...) ; r is (first x) for the first x in lov+flov s.t. (= (second x) f@r) (define (is-first-max? r f@r lov+flov) ...)
发散或引发异常函数的问题应提醒读者注意具有副作用的函数这一更普遍的问题。如果给定的函数 f 有可见的效果——比如它将调用记录到文件中——那么 argmax 的客户端将能够观察到每次调用 argmax 时的两组日志。准确地说,如果值列表包含多个元素,日志将包含 lov 上每个值对 f 的两次调用。如果 f 计算成本高昂,加倍调用会带来高昂的成本。
为了避免这种成本并标记过于 eager 契约的问题,契约系统可以记录已契约函数参数的 i/o,并在依赖规范中使用这些哈希表。这是 PLT 正在进行的研究主题。敬请期待。