7.5 Building New Contract Combinators
Note:
The interface in this section is unstable and subject to change.
Contracts are represented internally as functions that
accept information about the contract (who is to blame,
source locations, etc) and produce projections (in the
spirit of Dana Scott) that enforce the contract. A
projection is a function that accepts an arbitrary value,
and returns a value that satisfies the corresponding
contract. For example, a projection that accepts only
integers corresponds to the contract (flat-contract integer?), and can be written like this:
As a second example, a projection that accepts unary functions
on integers looks like this:
Although these projections have the right error behavior,
they are not quite ready for use as contracts, because they
do not accomodate blame, and do not provide good error
messages. In order to accomodate these, contracts do not
just use simple projections, but use functions that accept
the names of two parties that are the candidates for blame,
as well as a record of the source location where the
contract was established and the name of the contract. They
can then, in turn, pass that information
to raise-contract-error to signal a good error
message.
Here is the first of those two projections, rewritten for
use in the contract system:
The first two new arguments specify who is to be blamed for
positive and negative contract violations,
respectively.
Contracts, in this system, are always
established between two parties. One party provides some
value according to the contract, and the other consumes the
value, also according to the contract. The first is called
the “positive” person and the second the “negative”. So,
in the case of just the integer contract, the only thing
that can go wrong is that the value provided is not an
integer. Thus, only the positive argument can ever accrue
blame (and thus only pos is passed
to raise-contract-error).
Compare that to the projection for our function contract:
In this case, the only explicit blame covers the situation
where either a non-procedure is supplied to the contract, or
where the procedure does not accept one argument. As with
the integer projection, the blame here also lies with the
producer of the value, which is
why raise-contract-error gets pos and
not neg as its argument.
The checking for the domain and range are delegated to
the int-proj function, which is supplied its
arguments in the first two line of
the int->int-proj function. The trick here is that,
even though the int->int-proj function always
blames what it sees as positive we can reverse the order of
the pos and neg arguments so that the
positive becomes the negative.
This is not just a cheap trick to get this example to work,
however. The reversal of the positive and the negative is a
natural consequence of the way functions behave. That is,
imagine the flow of values in a program between two
modules. First, one module defines a function, and then that
module is required by another. So, far the function itself
has to go from the original, providing module to the
requiring module. Now, imagine that the providing module
invokes the function, suppying it an argument. At this
point, the flow of values reverses. The argument is
travelling back from the requiring module to the providing
module! And finally, when the function produces a result,
that result flows back in the original
direction. Accordingly, the contract on the domain reverses
the positive and the negative, just like the flow of values
reverses.
We can use this insight to generalize the function contracts
and build a function that accepts any two contracts and
returns a contract for functions between them.
Projections like the ones described above, but suited to
other, new kinds of value you might make, can be used with
the contract library primitives below.
Builds a new contract.
The first argument is the name of the contract. It can be an
arbitrary S-expression. The second is a projection (see
above).
If the projection only takes four arguments, then the
positive position boolean is not passed to it (this is
for backwards compatibility).
The final argument is a predicate that is a
conservative, first-order test of a value. It should be a
function that accepts one argument and returns a boolean. If
it returns #f, its argument must be guaranteed to
fail the contract, and the contract should detect this right
when the projection is invoked. If it returns true,
the value may or may not violate the contract, but any
violations must not be signaled immediately.
This function is a convenience function, implemented
using proj-prop, name-prop,
first-order-prop, and stronger-prop.
Consider using those directly (as well as flat-prop as necessary),
as they allow more flexibility
and generally produce more efficient contracts.
Produces an S-expression to be used as a name
for a contract. The arguments should be either contracts or
symbols. It wraps parenthesis around its arguments and
extracts the names from any contracts it is supplied with.
Converts a regular scheme value into an instance of a contract struct,
converting it according to the description of
contracts.
If x is not one of the coercable values,
coerce-contract signals an error, using the first argument in
the error message.
Coerces all of the arguments in ’xs’ into contracts (via
coerce-contract/f) and signals an error if any of them are not
contracts. The error messages assume that the function named by
id got
xs as its entire argument list.
Like
coerce-contract, but requires the result
to be a flat contract, not an arbitrary contract.
Like
coerce-contracts, but requires the results
to be flat contracts, not arbitrary contracts.
Like
coerce-contract, but returns
#f if
the value cannot be coerced to a contract.
Signals a contract violation. The first argument is the value that
failed to satisfy the contract. The second argument is is the
src-info passed to the projection and the third should be
either
pos or
neg (typically
pos, see the
beginning of this section) that was passed to the projection. The
fourth argument is the
contract-name that was passed to the
projection and the remaining arguments are used with
format
to build an actual error message.
7.5.1 Contracts as structs
Note:
The interface in this section is unstable and subject to change.
A contract is an arbitrary struct that has all of the
struct properties
(see Structure Type Properties in the reference manual)
in this section
(except that flat-prop is optional).
Generally speaking, the contract should be a struct with
fields that specialize the contract in some way and then
properties that implement all of the details of checking
the contract and reporting errors, etc.
For example, an between/c contract is a struct that
holds the bounds on the number and then has the properties below
that inspect the bounds and take the corresponding action
(the proj-prop checks the numbers, the name-prop
constructs a name to print out for the contract, etc.).
This is the workhorse property that implements the contract.
The property should be bound to a function that accepts
the struct and then returns a projection, as described
in the docs for
make-proj-contract above.
This property should be a function that accepts the struct and returns
an s-expression representing the name of the property.
This property is used when optimizing contracts, in order to tell if some contract is stronger than another one.
In some situations, if a contract that is already in place is stronger than one about to be put in place,
then the new one is ignored.
This property should only be present if the contract is a flat contract. In the case that it is
a flat contract, the value of the property should be a predicate that determines if the
contract holds.
This property is used with
or/c to determine which branch of the
or/c applies. These don’t have to be precise (i.e., returning
#f is always safe),
but the more often a contract can honestly return
#t, the more often
it will work with
or/c.
For example, function contracts typically check arity in their first-order-props.