Contracts are a key tool for writing robust Racket programs. They make it possible to write expressive dynamic checks using the full power of the language. Even better, they provide blame information that helps pinpoint where faults occur in the program.
Racket provides the racket/class library, which allows you to program in an object-oriented style with classes, mixins, and traits. Naturally, we would like to have the full power of contracts available for object-oriented programming as well. To that end, racket/class provides class contract combinators that work well for protecting classes and mixins.
The racket/class library also comes with Java-style interfaces. Sometimes, you want to build in contract checking for all classes that implement some particular interface instead of designing your contracts class-by-class. In Racket 5.3 and on, you can do this with our new interface contracts.
The rest of the article provides a short introduction to class and interface contracts.
Class contracts
First, we’ll look at simple uses of class contracts. Our running example will be designing classes for use in an aquatic game involving fish. If you are following along in a REPL, you will need to
(require slideshow/pict). We can define a simple fish class:
> (define fish% |
(class object% |
(super-new) |
(init-field [weight 1] [color "sky blue"]) |
; eat the given food object |
(define/public (eat edible) |
(define nutrition (send edible eaten)) |
(grow nutrition)) |
; gain weight according to nutrition |
(define/private (grow nutrition) |
(set! weight (+ nutrition weight))) |
; produce a pict of this fish |
(define/public (draw) |
(standard-fish 80 (* weight 40) #:color color)))) |
|
|
> (send (new fish%) draw) |
|
Our
fish% objects will be able to eat some edible things and grow in size. Of course, we need some edible things:
> (define edible<%> |
(interface () eaten)) |
|
|
> (define plankton% |
(class* object% (edible<%>) |
(super-new) |
; not very nutritious... |
(define nutrition 0.2) |
(define/public (eaten) nutrition))) |
|
|
Now a
fish% can eat
plankton% objects:
> (define dory (new fish%)) |
|
> (send dory eat (new plankton%)) |
|
> (send dory draw) |
|
After eating, Dory weighs more. Unfortunately, as we’ve written our classes so far we could deny our fish its dinner by providing an object that can’t be eaten:
> (define tire% |
; don't eat this |
(class object% (super-new))) |
|
|
> (send dory eat (new tire%)) |
send: no such method |
method name: eaten |
class name: tire% |
This is where contracts can help out. Racket’s class contracts let you specify contracts on classes outside of the class hierarchy, so that you can have checked fish and unchecked fish, or fish that have different kinds of contracts.
For example, we can ensure that
checked-fish% only eat edible things:
> (define fish/c |
(class/c |
(init-field [weight real?]) |
[eat (->m (is-a?/c edible<%>) void?)] |
[draw (->m pict?)])) |
|
|
> (define/contract checked-fish% fish/c fish%) |
|
> (send (new checked-fish%) eat (new tire%)) |
eat method in fish%: contract violation |
expected: (is-a?/c interface:edible<%>) |
given: (object:tire% ...) |
in: the 1st argument of |
the eat method in |
fish/c |
contract from: (definition checked-fish%) |
blaming: top-level |
at: eval:12.0 |
Now we get a contract error for providing something that’s not food. The contract error comes with blame, which allows us to pinpoint the code that caused the violation, in this case the application of the
eat method. Of course, we can also contract Dory so that she doesn’t have to fast again:
> (define/contract better-dory (instanceof/c fish/c) dory) |
|
> (send better-dory eat (new tire%)) |
eat method in fish%: contract violation |
expected: (is-a?/c interface:edible<%>) |
given: (object:tire% ...) |
in: the 1st argument of |
the eat method in |
... |
(instanceof/c fish/c) |
contract from: (definition better-dory) |
blaming: top-level |
at: eval:14.0 |
There are more combinators like
object/c and so on that provide more fine-grained contract checking for classes. See the
Guide section on class contracts for more details.
Interface contracts
Up until now, we’ve only seen contracts for classes, not interfaces like
edible<%>. We’d like to ensure that food has some basic level of nutrition, so that our fish don’t get food poisoning and lose weight:
> (define mold% |
(class* object% (edible<%>) |
(super-new) |
(define/public (eaten) -0.4))) |
|
|
> (define ernest (new checked-fish% [color "honeydew"])) |
|
> (send ernest eat (new mold%)) |
|
> (send ernest draw) |
|
Given class contracts, contracts on interfaces may seen unnecessary. After all, you can always write a class contract and use it on the food:
> (define nutrition/c (flat-named-contract 'nutrition (>=/c 0))) |
|
> (define edible<%>/c |
(class/c [eaten (->m nutrition/c)])) |
|
|
> (define/contract checked-plankton% edible<%>/c plankton%) |
|
This successfully protects the new
checked-plankton% class, but it doesn’t give us any guarantees about the original
plankton%. Worse, a third party could give us a class implementing
edible<%> that doesn’t come with a contract attached. In other words, we have to secure every channel through which we could potentially get an
edible<%> object.
Of course, with higher-order contracts this isn’t as bad as it sounds. For example, we could revise
fish/c in the following way to get closer to what we want:
> (define fish/c |
(class/c |
(init-field [weight real?]) |
[eat (->m (instanceof/c edible<%>/c) void?)] |
[draw (->m pict?)])) |
|
|
Notice how the domain of
eat is now
(instanceof/c edible<%>/c), which uses the class contract we wrote above to check the edible object coming into the method.
This works pretty well, but it’s not perfect. If we write another fish method that interacts with edible things, we have to remember to use the right contract. Not only that,
instanceof/c will wrap the object with a contract every time it is passed to the method, which could cause an object to be wrapped with an arbitrary number of instance contracts. The redundant contract wrappings can impose a performance cost.
Instead, since we’re the ones providing the
edible<%> interface, it would be far better to just demand once and for all that anything that implements this interface has to uphold certain obligations. We can do that by adding contracts to the methods directly in the interface:
> (define checked-edible<%> |
(interface () [eaten (->m nutrition/c)])) |
|
|
> (define hemlock% |
(class* object% (checked-edible<%>) |
(super-new) |
(define nutrition -0.5) |
(define/public (eaten) nutrition))) |
|
|
> (define fish/c |
(class/c |
(init-field [weight real?]) |
[eat (->m (is-a?/c checked-edible<%>) void?)] |
[draw (->m pict?)])) |
|
|
> (define/contract francesco (instanceof/c fish/c) |
(new fish% [color "plum"])) |
|
|
> (send francesco draw) |
|
> (send francesco eat (new hemlock%)) |
eaten: broke its contract |
promised: nutrition |
produced: -0.5 |
in: the range of |
(->m nutrition) |
contract from: (class hemlock%) |
blaming: (class hemlock%) |
Now even though our
fish/c contract does not use an instance contract, our fish is protected from eating badly implemented foods. The check is established for
every class that implements the
checked-edible<%> interface. The actual contract wrapping occurs when an object of a class implementing the interface is instantiated.
Even better, interface contracts work together with interface inheritance to make sure that sub-interfaces uphold the invariants that their super-interfaces guarantee. More concretely, suppose we define a new sub-interface of
checked-edible<%>:
> (define healthy<%> |
(interface (checked-edible<%>) [eaten (->m (>=/c 0.5))])) |
|
|
> (define kale% |
(class* object% (healthy<%>) |
(super-new) |
(define/public (eaten) 1))) |
|
|
> (send francesco eat (new kale%)) |
|
> (send francesco draw) |
|
Francesco gets quite fat. This is fine, of course, since the healthy variant of food correctly upholds the
edible<%> invariants. Any
healthy<%> food is guaranteed to have non-negative nutrition. In contrast, suppose we define a
poison<%> interface as follows:
> (define poison<%> |
(interface (checked-edible<%>) [eaten (->m (<=/c 0))])) |
|
|
> (define toxic-sludge% |
(class* object% (poison<%>) |
(super-new) |
(define/public (eaten) -1))) |
|
|
> (send francesco eat (new toxic-sludge%)) |
eaten: broke its contract |
promised: nutrition |
produced: -1 |
in: the range of |
(->m nutrition) |
contract from: (interface poison<%>) |
blaming: (interface poison<%>) |
then we get a contract violation. Not only that, it appropriately blames the
poison<%> interface for incorrectly implementing the
checked-edible<%> interface. In other words, interface contracts allow us to ensure that sub-interfaces are
behavioral subtypes of super-interfaces.
Conclusion
Interface contracts and class contracts are both useful in their separate ways: class contracts when you want fine-grained control over which classes or instances are protected and interface contracts when you want all implementors to uphold certain invariants. Racket now provides both for the discerning programmer.
0 comments:
Post a Comment