Notes from Lecture 2 – On interfaces
Interfaces aim to communicate sufficient information about a component so that a client can use it correctly. This last part has two sides (a) a client should “know” what operations a component provides and how the client is expected to call them so that (b) the operations return the correct results that the client “anticipates” if the component is correct. In other words, an intefrace describes the obligations of a component’s clients and the benefits the component promises in return. Of course, the benefits for the clients are obligations of the component given that the clients live up to their obligations.
The above description defines of spectrum of information that a component writer can include in an interface. In this course we will look at three levels of information:
Syntactic interfaces
Behavioral interfaces
Protocols of interaction.
Let’s see what these levels mean in concrete terms with the simple example of the component for grocery store products we discussed in class.
1 Syntactic interfaces
Here is a basic interface for a product:
(define product<%> (interface () has-discount? value value-with-discount))
This tells us that our component is a class with three methods. This description of the interface is syntactic as it restricts the syntax of how clients interact with the component.
> (define apple% (class* object% (product<%>) (super-new) (init-field quantity-in-pounds) (field [price-per-pound-in-dollars 3] [discount -0.1] [offer-expiration 100000000000]) (define/public (value) (* quantity-in-pounds price-per-pound-in-dollars)))) class*: missing interface-required method
method name: value-with-discount
class name: apple%
interface name: product<%>
How many arguments an operation expects?
What are the datatypes of these arguments?
What is the datatype of the operation’s result?.
(define apple% (class* object% (product<%>) (super-new) (init-field quantity-in-pounds) (field [price-per-pound-in-dollars 3] [discount -0.1] [offer-expiration 100000000000]) (define/public (value) (- (* quantity-in-pounds price-per-pound-in-dollars))) (define/public (value-with-discount) (+ (value) (* (value) discount))) (define/public (has-discount?) (>= offer-expiration (date->seconds (current-date))))))
(define syntactic-apple/c (class/c [has-discount? (-> (is-a?/c product<%>) boolean?)] [value (-> (is-a?/c product<%>) number?)] [value-with-discount (-> (is-a?/c product<%>) number?)]))
2 Behavioral interfaces
In contrast to types, contracts are not validated when we compile a program but when we run it. As a result, they cannot validate if the methods of apple% meet their syntactic interface for every possible argument but only for specific ones provided in the program we run.
(define behavioral-apple/c (class/c [has-discount? (-> (is-a?/c product<%>) boolean?)] [value (-> (is-a?/c product<%>) positive?)] [value-with-discount (->i ([this (is-a?/c product<%>)]) (result (this) (<=/c (send this value))))]))
Notice that these properties of the results of the two methods are described with ordinary code together with bits of special notation; positive? is an ordinary predicate that we could replace with any other we define and (send this value) is a Racket expression that could show up anywhere in a program. In a sense contracts give to developers some power that language implementors have been keping for themselves. With contracts developers can create their own “vocabulary” for stating and validating the interfaces of components without being restricted by the choices of “vocabulary” of the language implementors (e.g., Integer, Float, List<T>).
> (define/contract apple%-with-a-behavioral-interface behavioral-apple/c apple%)
> (define an-apple-with-a-behavioral-interface (new apple%-with-a-behavioral-interface [quantity-in-pounds 6])) > (send an-apple-with-a-behavioral-interface value) value: broke its own contract
promised: positive?
produced: -18
blaming: (definition apple%-with-a-behavioral-interface)
(assuming the contract is correct)
(define fixed-apple% (class* object% (product<%>) (super-new) (init-field quantity-in-pounds) (field [price-per-pound-in-dollars 3] [discount -0.1] [offer-expiration 100000000000]) (define/public (value) (* quantity-in-pounds price-per-pound-in-dollars)) (define/public (value-with-discount) (+ (value) (* (value) discount))) (define/public (has-discount?) (>= offer-expiration (date->seconds (current-date))))))
> (define/contract fixed-apple%-with-a-behavioral-interface behavioral-apple/c fixed-apple%)
> (define a-fixed-apple-with-a-behavioral-interface (new fixed-apple%-with-a-behavioral-interface [quantity-in-pounds 6])) > (send a-fixed-apple-with-a-behavioral-interface value) 18
(define apple+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/public (has-discount?) (let ([result (>= offer-expiration (date->seconds (current-date)))]) (if (boolean? result) 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 (positive? result) result (raise "assertion violation: method doesn't live up to its promises")))) (define/public (value-with-discount) (let ([result (+ (value) (* (value) discount))]) (if (and (number? result) (<=/c (send this value))) result (raise "assertion violation: method doesn't live up to its promises"))))))
> (define another-kind-of-apple-with-a-behavioral-interface (new apple+assertions% [quantity-in-pounds 6])) > (send another-kind-of-apple-with-a-behavioral-interface value) uncaught exception: "assertion violation: method doesn't
live up to its promises"
Note that even if we have sprinkled assertions in the code, we are careful to do so in a way that follows a clear pattern that separates functional code from validation code and thus retains the readability and maintainability of the code.
3 Protocols of interraction
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, 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