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 interface 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 the first two of 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 ([product (is-a?/c product<%>)]) (result (product) (<=/c (send product 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 product value) is a Racket expression that could show up anywhere in a program. In a sense, contracts give developers power that language implementors have been keeping 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, (Listof 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?) (define result (>= offer-expiration (date->seconds (current-date)))) (unless (boolean? result) (raise "assertion violation: method doesn't live up to its promises")) result) (define/public (value) (define result (- (* quantity-in-pounds price-per-pound-in-dollars))) (unless (positive? result) (raise "assertion violation: method doesn't live up to its promises")) result) (define/public (value-with-discount) (define result (+ (value) (* (value) discount))) (unless (and (number? result) (<= result (send this value))) (raise "assertion violation: method doesn't live up to its promises")) result)))
> (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.
In fact, if our language supports higher-order functions, we can define a check function and use it to factor out the repeated conditionals from the method bodies:
(define apple+assertions+check% (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?) (check boolean? (>= offer-expiration (date->seconds (current-date))) "assertion violation: method doesn't live up to its promises")) (define/public (value) (check positive? (- (* quantity-in-pounds price-per-pound-in-dollars)) "assertion violation: method doesn't live up to its promises")) (define/public (value-with-discount) (check (λ (result) (and (number? result) (<= result (send this value)))) (+ (value) (* (value) discount)) "assertion violation: method doesn't live up to its promises"))))
> (define yet-another-kind-of-apple-with-a-behavioral-interface (new apple+assertions% [quantity-in-pounds 6])) > (send yet-another-kind-of-apple-with-a-behavioral-interface value) uncaught exception: "assertion violation: method doesn't
live up to its promises"
And we can take the refactoring even further and completely separate contract checking code from the method bodies of apple%. To do that, we introduce a new class, apple-contract-wrapper%, that implements the product<%> interface, has a field that holds a apple% object, and delegates to that object all method invocations (we will return to this pattern for structuring code later on in the quarter):
(define apple-contract-wrapper% (class* object% (product<%>) (super-new) (init-field inner-apple) (define/public (has-discount?) (check boolean? (send inner-apple has-discount?) "assertion violation: method doesn't live up to its promises")) (define/public (value) (check positive? (send inner-apple value) "assertion violation: method doesn't live up to its promises")) (define/public (value-with-discount) (check (λ (result) (and (number? result) (<= result (send inner-apple value)))) (send inner-apple value-with-discount) "assertion violation: method doesn't live up to its promises"))))
> (define yet-yet-another-kind-of-apple-with-a-behavioral-interface (new apple-contract-wrapper% [inner-apple (new apple% [quantity-in-pounds 6])])) > (send yet-yet-another-kind-of-apple-with-a-behavioral-interface value) uncaught exception: "assertion violation: method doesn't
live up to its promises"