7.7 Additional Examples
本节通过 Design by Contract, by Example [Mitchell02] 中的一系列示例来说明 Racket contract 实现的现状。
Mitchell 和 McKim 的 design by contract DbC 原则源自 1970 年代风格的代数规范。DbC 的总体目标是依据代数构造函数的观测器来指定它们。虽然我们重新表述了 Mitchell 和 McKim 的术语并主要使用应用式方法,但我们保留了他们的"类"和"对象"术语:
将查询与命令分开。
查询返回结果但不改变对象的可观察属性。命令改变对象的可观察属性但不返回结果。在应用式实现中,命令通常返回同一 class 的新对象。
将基本查询与派生查询分开。
派生查询返回可根据基本查询计算的结果。
为每个派生查询编写一个 post-condition contract,根据基本查询指定其结果。
为每个命令编写一个 post-condition contract,根据基本查询指定可观察属性的更改。
为每个查询和 command 决定适当的 pre-condition contract。
以下各节对应 Mitchell 和 McKim 书中的章节(但并非所有章节都会出现)。建议您先阅读 contract(靠近第一个 module 的末尾),然后阅读实现(在第一个 module 中),最后阅读测试 module(在每个部分的末尾)。
Mitchell 和 McKim 使用 Eiffel 作为底层编程语言,采用传统的 imperative 编程风格。我们的长期目标是将它们的示例移植为应用式的 Racket、面向结构的 imperative Racket,以及 Racket 的 class 系统。
注意:为了模仿 Mitchell 和 McKim 对 parametericity(参数多态)的非正式概念,我们使用 first-class contract。在多个地方,使用 first-class contract 改进了 Mitchell 和 McKim 的设计(参见接口中的注释)。
7.7.1 A Customer-Manager Component
第一个 module 包含一些 struct 定义,位于单独的 module 中,以便更好地追踪错误。
#lang racket ; data definitions (define id? symbol?) (define id-equal? eq?) (define-struct basic-customer (id name address) #:mutable) ; interface (provide (contract-out [id? (-> any/c boolean?)] [id-equal? (-> id? id? boolean?)] [struct basic-customer ((id id?) (name string?) (address string?))])) ; end of interface
此 module 包含使用上述内容的程序。
#lang racket (require "1.rkt") ; the module just above ; implementation ; [listof (list basic-customer? secret-info)] (define all '()) (define (find c) (define (has-c-as-key p) (id-equal? (basic-customer-id (car p)) c)) (define x (filter has-c-as-key all)) (if (pair? x) (car x) x)) (define (active? c) (pair? (find c))) (define not-active? (compose not active? basic-customer-id)) (define count 0) (define (get-count) count) (define (add c) (set! all (cons (list c 'secret) all)) (set! count (+ count 1))) (define (name id) (define bc-with-id (find id)) (basic-customer-name (car bc-with-id))) (define (set-name id name) (define bc-with-id (find id)) (set-basic-customer-name! (car bc-with-id) name)) (define c0 0) ; end of implementation (provide (contract-out ; how many customers are in the db? [get-count (-> natural-number/c)] ; is the customer with this id active? [active? (-> id? boolean?)] ; what is the name of the customer with this id? [name (-> (and/c id? active?) string?)] ; change the name of the customer with this id [set-name (->i ([id id?] [nn string?]) [result any/c] ; result contract #:post (id nn) (string=? (name id) nn))] [add (->i ([bc (and/c basic-customer? not-active?)]) ; A pre-post condition contract must use ; a side-effect to express this contract ; via post-conditions #:pre () (set! c0 count) [result any/c] ; result contract #:post () (> count c0))]))
测试:
#lang racket (require rackunit rackunit/text-ui "1.rkt" "1b.rkt") (add (make-basic-customer 'mf "matthias" "brookstone")) (add (make-basic-customer 'rf "robby" "beverly hills park")) (add (make-basic-customer 'fl "matthew" "pepper clouds town")) (add (make-basic-customer 'sk "shriram" "i city")) (run-tests (test-suite "manager" (test-equal? "id lookup" "matthias" (name 'mf)) (test-equal? "count" 4 (get-count)) (test-true "active?" (active? 'mf)) (test-false "active? 2" (active? 'kk)) (test-true "set name" (void? (set-name 'mf "matt")))))
7.7.2 A Parameteric (Simple) Stack
#lang racket ; a contract utility (define (eq/c x) (lambda (y) (eq? x y))) (define-struct stack (list p? eq)) (define (initialize p? eq) (make-stack '() p? eq)) (define (push s x) (make-stack (cons x (stack-list s)) (stack-p? s) (stack-eq s))) (define (item-at s i) (list-ref (reverse (stack-list s)) (- i 1))) (define (count s) (length (stack-list s))) (define (is-empty? s) (null? (stack-list s))) (define not-empty? (compose not is-empty?)) (define (pop s) (make-stack (cdr (stack-list s)) (stack-p? s) (stack-eq s))) (define (top s) (car (stack-list s))) (provide (contract-out ; predicate [stack? (-> any/c boolean?)] ; primitive queries ; how many items are on the stack? [count (-> stack? natural-number/c)] ; which item is at the given position? [item-at (->d ([s stack?] [i (and/c positive? (<=/c (count s)))]) () [result (stack-p? s)])] ; derived queries ; is the stack empty? [is-empty? (->d ([s stack?]) () [result (eq/c (= (count s) 0))])] ; which item is at the top of the stack [top (->d ([s (and/c stack? not-empty?)]) () [t (stack-p? s)] ; a stack item, t is its name #:post-cond ([stack-eq s] t (item-at s (count s))))] ; creation [initialize (->d ([p contract?] [s (p p . -> . boolean?)]) () ; Mitchell and McKim use (= (count s) 0) here to express ; the post-condition in terms of a primitive query [result (and/c stack? is-empty?)])] ; commands ; add an item to the top of the stack [push (->d ([s stack?] [x (stack-p? s)]) () [sn stack?] ; result kind #:post-cond (and (= (+ (count s) 1) (count sn)) ([stack-eq s] x (top sn))))] ; remove the item at the top of the stack [pop (->d ([s (and/c stack? not-empty?)]) () [sn stack?] ; result kind #:post-cond (= (- (count s) 1) (count sn)))]))
测试:
#lang racket (require rackunit rackunit/text-ui "2.rkt") (define s0 (initialize (flat-contract integer?) =)) (define s2 (push (push s0 2) 1)) (run-tests (test-suite "stack" (test-true "empty" (is-empty? (initialize (flat-contract integer?) =))) (test-true "push" (stack? s2)) (test-true "push exn" (with-handlers ([exn:fail:contract? (lambda _ #t)]) (push (initialize (flat-contract integer?)) 'a) #f)) (test-true "pop" (stack? (pop s2))) (test-equal? "top" (top s2) 1) (test-equal? "toppop" (top (pop s2)) 2)))
7.7.3 A Dictionary
#lang racket ; a shorthand for use below (define-syntax ⇒ (syntax-rules () [(⇒ antecedent consequent) (if antecedent consequent #t)])) ; implementation (define-struct dictionary (l value? eq?)) ; the keys should probably be another parameter (exercise) (define (initialize p eq) (make-dictionary '() p eq)) (define (put d k v) (make-dictionary (cons (cons k v) (dictionary-l d)) (dictionary-value? d) (dictionary-eq? d))) (define (rem d k) (make-dictionary (let loop ([l (dictionary-l d)]) (cond [(null? l) l] [(eq? (caar l) k) (loop (cdr l))] [else (cons (car l) (loop (cdr l)))])) (dictionary-value? d) (dictionary-eq? d))) (define (count d) (length (dictionary-l d))) (define (value-for d k) (cdr (assq k (dictionary-l d)))) (define (has? d k) (pair? (assq k (dictionary-l d)))) (define (not-has? d) (lambda (k) (not (has? d k)))) ; end of implementation ; interface (provide (contract-out ; predicates [dictionary? (-> any/c boolean?)] ; basic queries ; how many items are in the dictionary? [count (-> dictionary? natural-number/c)] ; does the dictionary define key k? [has? (->d ([d dictionary?] [k symbol?]) () [result boolean?] #:post-cond ((zero? (count d)) . ⇒ . (not result)))] ; what is the value of key k in this dictionary? [value-for (->d ([d dictionary?] [k (and/c symbol? (lambda (k) (has? d k)))]) () [result (dictionary-value? d)])] ; initialization ; post condition: for all k in symbol, (has? d k) is false. [initialize (->d ([p contract?] [eq (p p . -> . boolean?)]) () [result (and/c dictionary? (compose zero? count))])] ; commands ; Mitchell and McKim say that put shouldn't consume Void (null ptr) ; for v. We allow the client to specify a contract for all values ; via initialize. We could do the same via a key? parameter ; (exercise). add key k with value v to this dictionary [put (->d ([d dictionary?] [k (and/c symbol? (not-has? d))] [v (dictionary-value? d)]) () [result dictionary?] #:post-cond (and (has? result k) (= (count d) (- (count result) 1)) ([dictionary-eq? d] (value-for result k) v)))] ; remove key k from this dictionary [rem (->d ([d dictionary?] [k (and/c symbol? (lambda (k) (has? d k)))]) () [result (and/c dictionary? (lambda (d) ((not-has? d) k)))] #:post-cond (= (count d) (+ (count result) 1)))])) ; end of interface
测试:
#lang racket (require rackunit rackunit/text-ui "3.rkt") (define d0 (initialize (flat-contract integer?) =)) (define d (put (put (put d0 'a 2) 'b 2) 'c 1)) (run-tests (test-suite "dictionaries" (test-equal? "value for" 2 (value-for d 'b)) (test-false "has?" (has? (rem d 'b) 'b)) (test-equal? "count" 3 (count d)) (test-case "contract check for put: symbol?" (define d0 (initialize (flat-contract integer?) =)) (check-exn exn:fail:contract? (lambda () (put d0 "a" 2))))))
7.7.4 A Queue
#lang racket ; Note: this queue doesn't implement the capacity restriction ; of Mitchell and McKim's queue but this is easy to add. ; a contract utility (define (all-but-last l) (reverse (cdr (reverse l)))) (define (eq/c x) (lambda (y) (eq? x y))) ; implementation (define-struct queue (list p? eq)) (define (initialize p? eq) (make-queue '() p? eq)) (define items queue-list) (define (put q x) (make-queue (append (queue-list q) (list x)) (queue-p? q) (queue-eq q))) (define (count s) (length (queue-list s))) (define (is-empty? s) (null? (queue-list s))) (define not-empty? (compose not is-empty?)) (define (rem s) (make-queue (cdr (queue-list s)) (queue-p? s) (queue-eq s))) (define (head s) (car (queue-list s))) ; interface (provide (contract-out ; predicate [queue? (-> any/c boolean?)] ; primitive queries ; Imagine providing this 'query' for the interface of the module ; only. Then in Racket there is no reason to have count or is-empty? ; around (other than providing it to clients). After all items is ; exactly as cheap as count. [items (->d ([q queue?]) () [result (listof (queue-p? q))])] ; derived queries [count (->d ([q queue?]) ; We could express this second part of the post ; condition even if count were a module "attribute" ; in the language of Eiffel; indeed it would use the ; exact same syntax (minus the arrow and domain). () [result (and/c natural-number/c (=/c (length (items q))))])] [is-empty? (->d ([q queue?]) () [result (and/c boolean? (eq/c (null? (items q))))])] [head (->d ([q (and/c queue? (compose not is-empty?))]) () [result (and/c (queue-p? q) (eq/c (car (items q))))])] ; creation [initialize (-> contract? (contract? contract? . -> . boolean?) (and/c queue? (compose null? items)))] ; commands [put (->d ([oldq queue?] [i (queue-p? oldq)]) () [result (and/c queue? (lambda (q) (define old-items (items oldq)) (equal? (items q) (append old-items (list i)))))])] [rem (->d ([oldq (and/c queue? (compose not is-empty?))]) () [result (and/c queue? (lambda (q) (equal? (cdr (items oldq)) (items q))))])])) ; end of interface
测试:
#lang racket (require rackunit rackunit/text-ui "5.rkt") (define s (put (put (initialize (flat-contract integer?) =) 2) 1)) (run-tests (test-suite "queue" (test-true "empty" (is-empty? (initialize (flat-contract integer?) =))) (test-true "put" (queue? s)) (test-equal? "count" 2 (count s)) (test-true "put exn" (with-handlers ([exn:fail:contract? (lambda _ #t)]) (put (initialize (flat-contract integer?)) 'a) #f)) (test-true "remove" (queue? (rem s))) (test-equal? "head" 2 (head s))))