#lang scribble/doc @require[scribble/manual] @require[scribble/eval] @require["guide-utils.ss"] @require["contracts-utils.ss"] @(require (for-label scheme/contract))
Modules deal with structures in two ways. First they export
@scheme[struct] definitions, i.e., the ability to create structs of a
certain kind, to access their fields, to modify them, and to distinguish structs
of this kind against every other kind of value in the world. Second, on occasion
a module exports a specific struct and wishes to promise that its fields contain
values of a certain kind. This section explains how to protect structs with
contracts for both uses.
Yes. If your module defines a variable to be a structure, then on export you
can specify the structures shape:
Yes, again. See the help desk for information on @scheme[vector/c] and
similar contract combinators for (flat) compound data.
"How to Design Programs" teaches that @scheme[posn]s should contain only
numbers in their two fields. With contracts we would enforce this informal data
definition as follows:
Thus, if a client calls @scheme[make-posn] on @scheme[10] and
@scheme['a], the contract system will signal a contract
violation. Similarly, if @scheme[(set-posn-x! (make-posn 10 10) 'a)] causes
an error.
The creation of @scheme[p-sick] inside of the @scheme[posn] module,
however, does not violate the contracts. The function @scheme[make-posn] is
internal so @scheme['a] and @scheme['b] don't cross the module
boundary. Similarly, when @scheme[p-sick] crosses the boundary of
@scheme[posn], the contract promises a @scheme[posn?] and nothing
else. In particular, this check does not enforce that the fields of
@scheme[p-sick] are numbers.
The association of contract checking with module boundaries implies that
@scheme[p-okay] and @scheme[p-sick] look alike from a client's
perspective until the client extracts the pieces:
This specific example shows that the explanation for a contract violation
doesn't always pinpoint the source of the error. The good news is that the
error is located in the @scheme[posn] module. The bad news is that the
explanation is misleading. Although it is true that @scheme[posn-x]
produced a symbol instead of a number, it is the fault of the programmer who
created a @scheme[posn] from symbols, i.e., the programmer who added
Exercise 2: Use your knowledge from the
section on exporting specific structs and change the contract for
@scheme[p-sick] so that the error is caught when clients refer to the
structure. Solution
Contracts written using @scheme[struct/c] immediately
check the fields of the data structure, but sometimes this
can have disastrous effects on the performance of a program
that does not, itself, inspect the entire data structure.
As an example, consider the the binary search tree
search algorithm. A binary search tree is like a binary
tree, except that the numbers are organized in the tree to
make searching the tree fast. In particular, for each
interior node in the tree, all of the numbers in the left
subtree are smaller than the number in the node, and all of
the numbers in the right subtree are larger than the number
in the node.
We can implement implement a search
function @scheme[in?] that takes advantage of the
structure of the binary search tree.
The contract on @scheme[in?] guarantees that its input
is a binary search tree. But a little careful thought
reveals that this contract defeats the purpose of the binary
search tree algorithm. In particular, consider the
inner @scheme[cond] in the @scheme[in?]
function. This is where the @scheme[in?] function gets
its speed: it avoids searching an entire subtree at each
recursive call. Now compare that to the @scheme[bst-between?]
function. In the case that it returns @scheme[#t], it
traverses the entire tree, meaning that the speedup
of @scheme[in?] is lost.
In order to fix that, we can employ a new strategy for
checking the binary search tree contract. In particular, if
we only checked the contract on the nodes
that @scheme[in?] looks at, we can still guarantee that
the tree is at least partially well-formed, but without
the performance loss.
To do that, we need to
use @scheme[define-contract-struct] in place
of @scheme[define-struct]. Like @scheme[define-struct],
@scheme[define-contract-struct] defines a maker,
predicate, and selectors for a new
structure. Unlike @scheme[define-struct], it also
defines contract combinators, in this
case @scheme[node/c] and @scheme[node/dc]. Also unlike
@scheme[define-struct], it does not define mutators, making
its structs immutable.
The @scheme[node/c] function accepts a contract for each
field of the struct and returns a contract on the
struct. More interestingly, the syntactic
form @scheme[node/dc] allows us to write dependent
contracts, i.e., contracts where some of the contracts on
the fields depend on the values of other fields. We can use
this to define the binary search tree contract:
Although this contract improves the performance
of @scheme[in?], restoring it to the logarithmic
behavior that the contract-less version had, it is still
imposes a fairly large constant overhead. So, the contract
library also provides @scheme[define-opt/c] that brings
down that constant factor by optimizing its body. Its shape
is just like the @scheme[define] above. It expects its
body to be a contract and then optimizes that contract.
A single change suffices: