Notes from Lecture 3 – Protocols of interaction
Behavioral interfaces subsume syntactic interfaces but they are not as expressive as needed in many scenarios. For instance they fall short of describing the order in which the methods of an object should be called. Such specifications, dubbed protocols of interaction, are very common restrictions on the way components interact with their clients that show up in many programming domains from implementing an IDE to building the server-side of a web app by composing microservices (see Netflix).
Unfortunately, even Racket’s contract system is not powerful enough to express protocols directly. We can though fake it with the use of shadow state, i.e., a cell that only a contract has access to and that some pieces of the contract set and others check its value. For example, returning to our grocery store running example from lecture 2, if we want to specify a protocol for fixed-apple% that prescribes that an invocation of value-with-discount is valid only if we first invoke has-discount?, we can create a mutable cell flag that is only visible to the contract (thus we define a constructor function that creates a fresh cell for each contract it constructs) and then add post-conditions to has-discount? and value-with-discount that set and/or inspect flag accordingly: In fact research in programming languages and contracts has shown that we can do better.
(define (make-protocol-apple/c) (define flag (box #f)) (object/c [has-discount? (->i ([this (is-a?/c product<%>)]) (result boolean?) #:post () (set-box! flag #t))] [value (-> (is-a?/c product<%>) positive?)] [value-with-discount (->i ([this (is-a?/c product<%>)]) (result (this) (<=/c (send this value))) #:post () (and (unbox flag) (set-box! flag #f)))]))
(define/contract a-fixed-apple-with-a-protocol (make-protocol-apple/c) (new fixed-apple% [quantity-in-pounds 6]))
> (send a-fixed-apple-with-a-protocol value-with-discount) value-with-discount: broke its own contract
#:post condition violation
blaming: (definition a-fixed-apple-with-a-protocol)
(assuming the contract is correct)
> (send a-fixed-apple-with-a-protocol has-discount?) #t
> (send a-fixed-apple-with-a-protocol value-with-discount) 16.2
For languages that do not support contracts, we can play the same trick as for behavioral contracts and use assertions. As a replacement for the shadow state we can use a private field such as can-call-value-with-discount? below:
(define apple+protocol-assertions% (class* object% (product<%>) (super-new) (init-field quantity-in-pounds) (field [price-per-pound-in-dollars 3] [discount -0.1] [offer-expiration 100000000000]) (define can-call-value-with-discount? #f) (define/public (has-discount?) (let ([result (>= offer-expiration (date->seconds (current-date)))]) (if (boolean? result) (begin (set! can-call-value-with-discount? #t) result) (raise "assertion violation: method doesn't live up to its promises")))) (define/public (value) (let ([result (* quantity-in-pounds price-per-pound-in-dollars)]) (if (number? result) result (raise "assertion violation: method doesn't live up to its promises")))) (define/public (value-with-discount) (let ([result (+ (value) (* (value) discount))]) (if (number? result) (if can-call-value-with-discount? (begin (set! can-call-value-with-discount? #f) result) (raise "protocol assertion violation: cannot call method at this point")) (raise "assertion violation: method doesn't live up to its promises"))))))
> (define another-kind-of-apple-with-a-protocol (new apple+protocol-assertions% [quantity-in-pounds 6])) > (send another-kind-of-apple-with-a-protocol value-with-discount) uncaught exception: "protocol assertion violation: cannot
call method at this point"
> (send another-kind-of-apple-with-a-protocol has-discount?) #t
> (send another-kind-of-apple-with-a-protocol value-with-discount) 16.2
And of course, we can play again here the same tricks as for behavioral contracts to refactor the code:
(define (check-with-protocol property check-protocol update-protocol x msg protocol-msg) (if (property x) (if (check-protocol) (begin (update-protocol) x) (raise protocol-msg)) (raise msg)))
(define apple-protocol-contract-wrapper% (class* object% (product<%>) (super-new) (init-field inner-apple) (define can-call-value-with-discount? #f) (define/public (has-discount?) (check-with-protocol boolean? (λ () #t) (thunk (set! can-call-value-with-discount? #t)) (send inner-apple has-discount?) "assertion violation: method doesn't live up to its promises" "unreachable")) (define/public (value) (check-with-protocol positive? (λ () #t) values (send inner-apple value) "assertion violation: method doesn't live up to its promises" "unreachable")) (define/public (value-with-discount) (check-with-protocol (λ (result) (and (number? result) (<= result (send inner-apple value)))) (thunk can-call-value-with-discount?) (thunk (set! can-call-value-with-discount? #f)) (send inner-apple value-with-discount) "assertion violation: method doesn't live up to its promises" "protocol assertion violation: cannot call method at this point"))))
> (define yet-another-kind-of-apple-with-a-protocol (new apple-protocol-contract-wrapper% [inner-apple (new fixed-apple% [quantity-in-pounds 6])])) > (send yet-another-kind-of-apple-with-a-protocol value-with-discount) uncaught exception: "protocol assertion violation: cannot
call method at this point"
> (send yet-another-kind-of-apple-with-a-protocol has-discount?) #t
> (send yet-another-kind-of-apple-with-a-protocol value-with-discount) 16.2