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:
(define int-proj |
(lambda (x) |
(if (integer? x) |
x |
(signal-contract-violation)))) |
As a second example, a projection that accepts unary functions on integers looks like this:
(define int->int-proj |
(lambda (f) |
(if (and (procedure? f) |
(procedure-arity-includes? f 1)) |
(lambda (x) |
(int-proj (f (int-proj x)))) |
(signal-contract-violation)))) |
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 a blame object encapsulating 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-blame-error to signal a good error message.
(define (int-proj blame) |
(lambda (x) |
(if (integer? x) |
x |
(raise-blame-error |
blame |
val |
"expected <integer>, given: ~e" |
val)))) |
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 party can ever accrue blame. The raise-blame-error function always blames the positive party.
Compare that to the projection for our function contract:
(define (int->int-proj blame) |
(let ([dom (int-proj (blame-swap blame))] |
[rng (int-proj blame)]) |
(lambda (f) |
(if (and (procedure? f) |
(procedure-arity-includes? f 1)) |
(lambda (x) |
(rng (f (dom x)))) |
(raise-blame-error |
blame |
val |
"expected a procedure of one argument, given: ~e" |
val))))) |
In this case, the only explicit blame covers the situation where either a non-procedure is supplied to the contract or 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-blame-error is passed blame unchanged.
The checking for the domain and range are delegated to the int-proj function, which is supplied its arguments in the first two lines 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 swap the blame parties by calling blame-swap on the given blame object, replacing the positive party with the negative party and vice versa.
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 traveling 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 blame parties, 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.
(define (make-simple-function-contract dom-proj range-proj) |
(lambda (blame) |
(let ([dom (dom-proj (blame-swap blame))] |
[rng (range-proj blame)]) |
(lambda (f) |
(if (and (procedure? f) |
(procedure-arity-includes? f 1)) |
(lambda (x) |
(rng (f (dom x)))) |
(raise-blame-error |
blame |
val |
"expected a procedure of one argument, given: ~e" |
val)))))) |
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.
| |||||||||||||||||||||||||||||||||||||||||||
| |||||||||||||||||||||||||||||||||||||||||||
|
The name argument is any value to be rendered using display to describe the contract when a violation occurs. The default name for simple higher-order contracts is anonymous-contract, for chaperone contracts is anonymous-chaperone-contract, and for flat contracts is anonymous-flat-contract.
The first-order predicate test can be used to determine which values the contract applies to; usually, this is the set of values for which the contract fails immediately without any higher-order wrapping. This test is used by contract-first-order-passes?, and indirectly by or/c to determine which of multiple higher-order contracts to wrap a value with. The default test accepts any value.
The projection proj defines the behavior of applying the contract. It is a curried function of two arguments: the first application accepts a blame object, and the second accepts a value to protect with the contract. The projection must either produce the value, suitably wrapped to enforce any higher-order aspects of the contract, or signal a contract violation using raise-blame-error. The default projection produces an error when the first-order test fails, and produces the value unchanged otherwise.
Projections for chaperone contracts must produce a value that passes chaperone-of? when compared with the original, uncontracted value. Projections for flat contracts must fail precisely when the first-order test does, and must produce the input value unchanged otherwise. Applying a flat contract may result in either an application of the predicate, or the projection, or both; therefore, the two must be consistent. The existence of a separate projection only serves to provide more specific error messages. Most flat contracts do not need to supply an explicit projection.
Examples: | |||||||||||||||||
| |||||||||||||||||
> (contract int/c 1 'positive 'negative) | |||||||||||||||||
1 | |||||||||||||||||
> (contract int/c "not one" 'positive 'negative) | |||||||||||||||||
self-contract violation: expected <int/c>, given: "not one" | |||||||||||||||||
contract on eval:4:0 from positive, blaming positive | |||||||||||||||||
contract: int/c | |||||||||||||||||
> (int/c 1) | |||||||||||||||||
#t | |||||||||||||||||
> (int/c "not one") | |||||||||||||||||
#f | |||||||||||||||||
| |||||||||||||||||
> (contract int->int/c "not fun" 'positive 'negative) | |||||||||||||||||
self-contract violation: expected a function of one | |||||||||||||||||
argument, got: "not fun" | |||||||||||||||||
contract on eval:8:0 from positive, blaming positive | |||||||||||||||||
contract: int->int/c | |||||||||||||||||
| |||||||||||||||||
> (halve 2) | |||||||||||||||||
1 | |||||||||||||||||
> (halve 1/2) | |||||||||||||||||
contract violation: expected <int/c>, given: 1/2 | |||||||||||||||||
contract on halve from positive via top-level, blaming | |||||||||||||||||
negative | |||||||||||||||||
contract: int->int/c | |||||||||||||||||
> (halve 1) | |||||||||||||||||
self-contract violation: expected <int/c>, given: 1/2 | |||||||||||||||||
contract on halve from positive, blaming positive | |||||||||||||||||
contract: int->int/c |
(build-compound-type-name c/s ...) → any |
c/s : any/c |
(coerce-contract id x) → contract? |
id : symbol? |
x : any/c |
If x is not one of the coercible values, coerce-contract signals an error, using the first argument in the error message.
(coerce-chaperone-contract id x) → chaperone-contract? |
id : symbol? |
x : any/c |
(coerce-chaperone-contracts id x) |
→ (listof/c chaperone-contract?) |
id : symbol? |
x : (listof any/c) |
(coerce-flat-contract id x) → flat-contract? |
id : symbol? |
x : any/c |
(coerce-flat-contracts id x) → (listof/c flat-contract?) |
id : symbol? |
x : (listof any/c) |
(coerce-contract/f x) → (or/c contract? #f) |
x : any/c |
7.5.1 Blame Objects
| ||
|
(blame-contract b) → any/c |
b : blame? |
(blame-value b) → any/c |
b : blame? |
(blame-source b) → srcloc? |
b : blame? |
(blame-swap b) → blame? |
b : blame? |
| ||
|
(exn:fail:contract:blame? x) → boolean? |
x : any/c |
7.5.2 Contracts as structs
Note: The interface in this section is unstable and subject to change.
The property prop:contract allows arbitrary structures to act as contracts. The property prop:chaperone-contract allows arbitrary structures to act as chaperone contracts; prop:chaperone-contract inherits prop:contract, so chaperone contract structures may also act as general contracts. The property prop:flat-contract allows arbitrary structures to act as flat contracts; prop:flat-contract inherits both prop:chaperone-contract and prop:procedure, so flat contract structures may also act as chaperone contracts, as general contracts, and as predicate procedures.
| |
| |
|
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
A contract property specifies the behavior of a structure when used as a contract. It is specified in terms of five accessors: get-name, which produces a description to write as part of a contract violation; get-first-order, which produces a first-order predicate to be used by contract-first-order-passes?; get-projection, which produces a blame-tracking projection defining the behavior of the contract; stronger, which is a predicate that determines whether this contract (passed in the first argument) is stronger than some other contract (passed in the second argument); and generator, which makes a random value that matches the contract, given a size bound and an environment from which to draw interesting values.
These accessors are passed as (optional) keyword arguments to build-contract-property, and are applied to instances of the appropriate structure type by the contract system. Their results are used analogously to the arguments of make-contract.
A chaperone contract property specifies the behavior of a structure when used as a chaperone contract. It is specified using build-chaperone-contract-property, and accepts exactly the same set of arguments as build-contract-property. The only difference is that the projection accessor must return a value that passes chaperone-of? when compared with the original, uncontracted value.
A flat contract property specifies the behavior of a structure when used as a flat contract. It is specified using build-flat-contract-property, and accepts exactly the same set of arguments as build-contract-property. The only difference is that the projection accessor is expected not to wrap its argument in a higher-order fashion, analogous to the constraint on projections in make-flat-contract.
| ||
| ||
|
7.5.3 Obligation Information in Check Syntax
Check Syntax in DrRacket shows obligation information for contracts according to syntax-propertys that the contract combinators leave in the expanded form of the program. These properties indicate where contracts appear in the source and where the positive and negative positions of the contracts appear.
To make Check Syntax show obligation information for your new contract combinators, use the following properties:
- This property should be attached to the result of a transformer that implements a contract combinator. It signals to Check Syntax that this is where a contract begins.
The first element in the vector should be a unique (in the sense of eq?) value that Check Syntax can use a tag to match up this contract with its subpieces (specified by the two following syntax properties).
The second and third elements of the vector are syntax objects from pieces of the contract, and Check Syntax will color them. The first list should contain subparts that are the responsibility of parties (typically modules) that provide implementations of the contract. The second list should contain subparts that are the responsibility of clients.
For example, in (->* () #:pre #t any/c #:post #t), the ->* and the #:post should be in the first list and #:pre in the second list.
- This property should be attached to subexpressions of a contract combinator that are expected to be other contracts. The value of the property should be the key (the first element from the vector for the 'racket/contract:contract property) indicating which contract this is.
This property should be used when the expression’s value is a contract that clients are responsible for.
- This is just like 'racket/contract:negative-position, except that it should be used when the expression’s value is a contract that the original party should be responsible for.
- The presence of this property tells Check Syntax that it should start coloring from this point. It expects the expression to be a contract (and, thus, to have the 'racket/contract:contract property); this property indicates that this contract is on a (module) boundary.
(The value of the property is not used.)
'racket/contract:internal-contract : symbol?
Like 'racket/contract:contract-on-boundary, the presence of this property triggers coloring, but this is meant for use when the party (module) containing the contract (regardless of whether or not this module exports anything matching the contract) can be blamed for violating the contract. This comes into play for ->i contracts, since the contract itself has acceess to values under contract via the dependency.