Wrote documentation for new contract interface. Still needs proofreading, etc.

svn: r17961
This commit is contained in:
Carl Eastlund 2010-02-03 21:02:02 +00:00
parent 2e64069d14
commit a4a25ba1e9

View File

@ -889,34 +889,30 @@ Although these projections have the right error behavior,
they are not quite ready for use as contracts, because they they are not quite ready for use as contracts, because they
do not accomodate blame, and do not provide good error do not accomodate blame, and do not provide good error
messages. In order to accomodate these, contracts do not messages. In order to accomodate these, contracts do not
just use simple projections, but use functions that accept just use simple projections, but use functions that accept a
@deftech{blame object} encapsulating
the names of two parties that are the candidates for blame, the names of two parties that are the candidates for blame,
as well as a record of the source location where the as well as a record of the source location where the
contract was established and the name of the contract. They contract was established and the name of the contract. They
can then, in turn, pass that information can then, in turn, pass that information
to @scheme[raise-contract-error] to signal a good error to @scheme[raise-blame-error] to signal a good error
message. message.
Here is the first of those two projections, rewritten for Here is the first of those two projections, rewritten for
use in the contract system: use in the contract system:
@schemeblock[ @schemeblock[
(define (int-proj pos neg src-info name positive-position?) (define (int-proj blame)
(lambda (x) (lambda (x)
(if (integer? x) (if (integer? x)
x x
(raise-contract-error (raise-blame-error
blame
val val
src-info
pos
name
"expected <integer>, given: ~e" "expected <integer>, given: ~e"
val)))) val))))
] ]
The new argument specifies who is to be blamed for
The first two new arguments specify who is to be blamed for positive and negative contract violations.
positive and negative contract violations,
respectively.
Contracts, in this system, are always Contracts, in this system, are always
established between two parties. One party provides some established between two parties. One party provides some
@ -925,28 +921,24 @@ value, also according to the contract. The first is called
the ``positive'' person and the second the ``negative''. So, the ``positive'' person and the second the ``negative''. So,
in the case of just the integer contract, the only thing in the case of just the integer contract, the only thing
that can go wrong is that the value provided is not an that can go wrong is that the value provided is not an
integer. Thus, only the positive argument can ever accrue integer. Thus, only the positive party can ever accrue
blame (and thus only @scheme[pos] is passed blame. The @scheme[raise-blame-error] function always blames
to @scheme[raise-contract-error]). the positive party.
Compare that to the projection for our function contract: Compare that to the projection for our function contract:
@schemeblock[ @schemeblock[
(define (int->int-proj pos neg src-info name positive-position?) (define (int->int-proj blame)
(let ([dom (int-proj neg pos src-info (let ([dom (int-proj (blame-swap blame))]
name (not positive-position?))] [rng (int-proj blame)])
[rng (int-proj pos neg src-info
name positive-position?)])
(lambda (f) (lambda (f)
(if (and (procedure? f) (if (and (procedure? f)
(procedure-arity-includes? f 1)) (procedure-arity-includes? f 1))
(lambda (x) (lambda (x)
(rng (f (dom x)))) (rng (f (dom x))))
(raise-contract-error (raise-blame-error
blame
val val
src-info
pos
name
"expected a procedure of one argument, given: ~e" "expected a procedure of one argument, given: ~e"
val))))) val)))))
] ]
@ -956,17 +948,16 @@ where either a non-procedure is supplied to the contract, or
where the procedure does not accept one argument. As with where the procedure does not accept one argument. As with
the integer projection, the blame here also lies with the the integer projection, the blame here also lies with the
producer of the value, which is producer of the value, which is
why @scheme[raise-contract-error] gets @scheme[pos] and why @scheme[raise-blame-error] is passed @scheme[blame] unchanged.
not @scheme[neg] as its argument.
The checking for the domain and range are delegated to The checking for the domain and range are delegated to
the @scheme[int-proj] function, which is supplied its the @scheme[int-proj] function, which is supplied its
arguments in the first two line of arguments in the first two line of
the @scheme[int->int-proj] function. The trick here is that, the @scheme[int->int-proj] function. The trick here is that,
even though the @scheme[int->int-proj] function always even though the @scheme[int->int-proj] function always
blames what it sees as positive we can reverse the order of blames what it sees as positive we can swap the blame parties by
the @scheme[pos] and @scheme[neg] arguments so that the calling @scheme[blame-swap] on the given @tech{blame object}, replacing
positive becomes the negative. the positive party with the negative party and vice versa.
This is not just a cheap trick to get this example to work, This is not just a cheap trick to get this example to work,
however. The reversal of the positive and the negative is a however. The reversal of the positive and the negative is a
@ -982,8 +973,8 @@ travelling back from the requiring module to the providing
module! And finally, when the function produces a result, module! And finally, when the function produces a result,
that result flows back in the original that result flows back in the original
direction. Accordingly, the contract on the domain reverses direction. Accordingly, the contract on the domain reverses
the positive and the negative, just like the flow of values the positive and the negative blame parties, just like the flow
reverses. of values reverses.
We can use this insight to generalize the function contracts We can use this insight to generalize the function contracts
and build a function that accepts any two contracts and and build a function that accepts any two contracts and
@ -991,21 +982,17 @@ returns a contract for functions between them.
@schemeblock[ @schemeblock[
(define (make-simple-function-contract dom-proj range-proj) (define (make-simple-function-contract dom-proj range-proj)
(lambda (pos neg src-info name positive-position?) (lambda (blame)
(let ([dom (dom-proj neg pos src-info (let ([dom (dom-proj (blame-swap blame))]
name (not positive-position?))] [rng (range-proj blame)])
[rng (range-proj pos neg src-info
name positive-position?)])
(lambda (f) (lambda (f)
(if (and (procedure? f) (if (and (procedure? f)
(procedure-arity-includes? f 1)) (procedure-arity-includes? f 1))
(lambda (x) (lambda (x)
(rng (f (dom x)))) (rng (f (dom x))))
(raise-contract-error (raise-blame-error
blame
val val
src-info
pos
name
"expected a procedure of one argument, given: ~e" "expected a procedure of one argument, given: ~e"
val)))))) val))))))
] ]
@ -1014,37 +1001,90 @@ Projections like the ones described above, but suited to
other, new kinds of value you might make, can be used with other, new kinds of value you might make, can be used with
the contract library primitives below. the contract library primitives below.
@defproc[(make-proj-contract [name any/c] @deftogether[(
[proj (or/c (-> symbol? symbol? any/c any/c any/c) @defproc[(simple-contract
(-> symbol? symbol? any/c any/c boolean? any/c))] [#:name name any/c 'simple-contract]
[first-order-test (-> any/c any/c)]) [#:first-order test (-> any/c any/c) (λ (x) #t)]
contract?]{ [#:projection proj (-> blame? (-> any/c any/c))
(λ (b)
(λ (x)
(if (test x)
x
(raise-blame-error
b x "expected <~a>, given: ~e" name x))))])
contract?]
@defproc[(simple-flat-contract
[#:name name any/c 'simple-flat-contract]
[#:first-order test (-> any/c any/c) (λ (x) #t)]
[#:projection proj (-> blame? (-> any/c any/c))
(λ (b)
(λ (x)
(if (test x)
x
(raise-blame-error
b x "expected <~a>, given: ~e" name x))))])
flat-contract?]
)]{
Builds a new contract. These functions build simple procedure-based contracts and flat contracts,
respectively. They both take the same set of three optional arguments: a name,
The first argument is the name of the contract. It can be an a first order predicate, and a blame-tracking projection.
arbitrary S-expression. The second is a projection (see
above).
If the projection only takes four arguments, then the The @scheme[name] argument is any value to be rendered using @scheme[display] to
positive position boolean is not passed to it (this is describe the contract when a violation occurs. The default name for simple
for backwards compatibility). higher order contracts is @schemeresult[simple-contract], and for flat contracts
is @schemeresult[simple-flat-contract].
The final argument is a predicate that is a The first order predicate @scheme[test] can be used to determine which values
conservative, first-order test of a value. It should be a the contract applies to; usually this is the set of values for which the
function that accepts one argument and returns a boolean. If contract fails immediately without any higher-order wrapping. This test is used
it returns @scheme[#f], its argument must be guaranteed to by @scheme[contract-first-order-passes?], and indirectly by @scheme[or/c] to
fail the contract, and the contract should detect this right determine which of multiple higher order contracts to wrap a value with. The
when the projection is invoked. If it returns true, default test accepts any value.
the value may or may not violate the contract, but any
violations must not be signaled immediately. The projection @scheme[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
@scheme[raise-blame-error]. The default projection produces an error when the
first order test fails, and produces the value unchanged otherwise.
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.
@defexamples[#:eval (contract-eval)
(define int/c
(simple-flat-contract #:name 'int/c #:first-order integer?))
(contract 1 int/c 'positive 'negative)
(contract "not one" int/c 'positive 'negative)
(int/c 1)
(int/c "not one")
(define int->int/c
(simple-contract
#:name 'int->int/c
#:first-order
(λ (x) (and (procedure? x) (procedure-arity-includes? x 1)))
#:projection
(λ (b)
(let ([domain ((contract-projection int/c) (blame-swap b))]
[range ((contract-projection int/c) blame)])
(λ (f)
(if (and (procedure? f) (procedure-arity-includes? f 1))
(λ (x) (range (f (domain x))))
(raise-blame-error
b f "expected a function of one argument, got: ~e" f)))))))
(contract "not fun" int->int/c 'positive 'negative)
(define halve (contract (λ (x) (/ x 2)) int->int/c 'positive 'negative))
(halve 2)
(halve 1)
(halve 1/2)
]
This function is a convenience function, implemented
using @scheme[proj-prop], @scheme[name-prop],
@scheme[first-order-prop], and @scheme[stronger-prop].
Consider using those directly (as well as @scheme[flat-prop] as necessary),
as they allow more flexibility
and generally produce more efficient contracts.
} }
@defproc[(build-compound-type-name [c/s any/c] ...) any]{ @defproc[(build-compound-type-name [c/s any/c] ...) any]{
@ -1086,31 +1126,71 @@ contracts. The error messages assume that the function named by
the value cannot be coerced to a contract. the value cannot be coerced to a contract.
} }
@defproc[(raise-contract-error [val any/c] @subsection{Blame Objects}
[src-info any/c]
[to-blame symbol?]
[contract-name any/c]
[fmt string?]
[arg any/c] ...)
any]{
Signals a contract violation. The first argument is the value that @defproc[(blame? [x any/c]) boolean?]{
failed to satisfy the contract. The second argument is is the This predicate recognizes @tech{blame objects}.
@scheme[src-info] passed to the projection and the third should be }
either @scheme[pos] or @scheme[neg] (typically @scheme[pos], see the
beginning of this section) that was passed to the projection. The
fourth argument is the @scheme[contract-name] that was passed to the
projection and the remaining arguments are used with @scheme[format]
to build an actual error message.}
@;{ @deftogether[(
% to document: @defproc[(blame-positive [b blame?]) any/c]
% proj-prop proj-pred? proj-get @defproc[(blame-negative [b blame?]) any/c]
% name-prop name-pred? name-get )]{
% stronger-prop stronger-pred? stronger-get These functions produce printable descriptions of the current positive and
% flat-prop flat-pred? flat-get negative parties of a blame object.
% first-order-prop first-order-get }
% contract-stronger?
@defproc[(blame-contract [b blame?]) any/c]{
This function produces a description of the contract associated with a blame
object (the result of @scheme[contract-name]).
}
@defproc[(blame-value [b blame?]) any/c]{
This function produces the name of the value to which the contract was applied,
or @scheme[#f] if no name was provided.
}
@defproc[(blame-source [b blame?]) srcloc?]{
This function produces the source location associated with a contract. If no
source location was provided, all fields of the structure will contain
@scheme[#f].
}
@defproc[(blame-swap [b blame?]) blame?]{
This function swaps the positive and negative parties of a @tech{blame object}.
}
@deftogether[(
@defproc[(blame-original? [b blame?]) boolean?]
@defproc[(blame-swapped? [b blame?]) boolean?]
)]{
These functions report whether the current blame of a given blame object is the
same as in the original contract invocation (possibly of a compound contract
containing the current one), or swapped, respectively. Each is the negation of
the other; both are provided for convenience and clarity.
}
@defproc[(raise-blame-error [b blame?] [x any/c] [fmt string?] [v any/c] ...)
none/c]{
Signals a contract violation. The first argument, @scheme[b], records the
current blame information, including positive and negative parties, the name of
the contract, the name of the value, and the source location of the contract
application. The second argument, @scheme[x], is the value that failed to
satisfy the contract. The remaining arguments are a format string,
@scheme[fmt], and its arguments, @scheme[v ...], specifying an error message
specific to the precise violation.
}
@defproc[(exn:fail:contract:blame? [x any/c]) boolean?]{
This predicate recognizes exceptions raised by @scheme[raise-blame-error].
}
@defproc[(exn:fail:contract:blame-object [e exn:fail:contract:blame?]) blame?]{
This accessor extracts the blame object associated with a contract violation.
} }
@subsection{Contracts as structs} @subsection{Contracts as structs}
@ -1118,98 +1198,104 @@ to build an actual error message.}
@emph{@bold{Note:} @emph{@bold{Note:}
The interface in this section is unstable and subject to change.} The interface in this section is unstable and subject to change.}
A contract is an arbitrary struct that has all of the @para{
struct properties The property @scheme[prop:contract] allows arbitrary structures to act as
(see @secref["structprops"] in the reference manual) contracts. The property @scheme[prop:flat-contract] allows arbitrary structures
in this section to act as flat contracts; @scheme[prop:flat-contract] inherits both
(except that @scheme[flat-prop] is optional). @scheme[prop:contract] and @scheme[prop:procedure], so flat contract structures
may also act as general contracts and as predicate procedures.
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 @scheme[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 @scheme[proj-prop] checks the numbers, the @scheme[name-prop]
constructs a name to print out for the contract, etc.).
@deftogether[(@defthing[proj-prop struct-type-property?]
@defproc[(proj-pred? [v any/c]) boolean?]{}
@defproc[(proj-get [v proj-pred?])
(-> proj-prop?
(-> symbol? symbol? (or/c #f syntax?) string? boolean?
(-> any/c any/c)))]{})]{
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 @scheme[make-proj-contract] above.
}
@deftogether[(@defthing[name-prop struct-type-property?]{}
@defproc[(name-pred? [v any/c]) boolean?]{}
@defproc[(name-get [v name-pred?]) (-> name-pred? printable/c)]{})]{
This property should be a function that accepts the struct and returns
an s-expression representing the name of the property.
@mz-examples[#:eval (contract-eval)
(write (between/c 1 10))
(let ([c (between/c 1 10)])
((name-get c) c))]
}
@deftogether[(@defthing[stronger-prop struct-type-property?]{}
@defproc[(stronger-pred? [v any/c]) boolean?]{}
@defproc[(stronger-get [v stronger-pred?]) (-> stronger-pred? stronger-pred? boolean?)]{})]{
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.
} }
@deftogether[(@defthing[flat-prop struct-type-property?]{} @deftogether[(
@defproc[(flat-pred? [v any/c]) boolean?]{} @defthing[prop:contract struct-type-property?]
@defproc[(flat-get [v flat-pred?]) (-> flat-pred? (-> any/c boolean?))]{})]{ @defthing[prop:flat-contract struct-type-property?]
)]{
This property should only be present if the contract is a flat contract. In the case that it is These properties declare structures to be contracts or flat contracts,
a flat contract, the value of the property should be a predicate that determines if the respectively. The value for @scheme[prop:contract] must be a @tech{contract
contract holds. property} constructed by @scheme[build-contract-property]; likewise, the value
for @scheme[prop:flat-contract] must be a @tech{flat contract property}
@mz-examples[#:eval (contract-eval) constructed by @scheme[build-flat-contract-property].
(flat-pred? (-> integer? integer?))
(let* ([c (between/c 1 10)]
[pred ((flat-get c) c)])
(list (pred 9)
(pred 11)))]
} }
@deftogether[(@defthing[first-order-prop struct-type-property?]{} @deftogether[(
@defproc[(first-order-pred? [v any/c]) boolean?]{} @defproc[(build-flat-contract-property
@defproc[(first-order-get [v proj-pred?]) (-> first-order-pred? (-> any/c boolean?))]{})]{ [#:name
get-name
(-> contract? any/c)
(λ (c) 'anonymous-flat-contract)]
[#:first-order
get-first-order
(-> contract? (-> any/c boolean?))
(λ (c) (λ (x) #t))]
[#:projection
get-projection
(-> contract? (-> blame? (-> any/c any/c)))
(λ (c)
(λ (b)
(λ (x)
(if ((get-first-order c) x)
x
(raise-blame-error
b x "expected <~a>, given: ~e" (get-name c) x)))))])
flat-contract-property?]
@defproc[(build-contract-property
[#:name
get-name
(-> contract? any/c)
(λ (c) 'anonymous-contract)]
[#:first-order
get-first-order
(-> contract? (-> any/c boolean?))
(λ (c) (λ (x) #t))]
[#:projection
get-projection
(-> contract? (-> blame? (-> any/c any/c)))
(λ (c)
(λ (b)
(λ (x)
(if ((get-first-order c) x)
x
(raise-blame-error
b x "expected <~a>, given: ~e" (get-name c) x)))))])
contract-property?]
)]{
This property is used with @scheme[or/c] to determine which branch of the These functions build the arguments for @scheme[prop:contract] and
@scheme[or/c] applies. These don't have to be precise (i.e., returning @scheme[#f] is always safe), @scheme[prop:flat-contract], respectively.
but the more often a contract can honestly return @scheme[#t], the more often
it will work with @scheme[or/c].
For example, function contracts typically check arity in their @scheme[first-order-prop]s.
A @deftech{contract property} specifies the behavior of a structure when used as
a contract. It is specified in terms of three accessors: @scheme[get-name],
which produces a description to @scheme[display] during a contract violation;
@scheme[get-first-order], which produces a first order predicate to be used by
@scheme[contract-first-order-passes?]; and @scheme[get-projection], which
produces a blame-tracking projection defining the behavior of the contract.
These accessors are passed as (optional) keyword arguments to
@scheme[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 @scheme[simple-contract].
A @deftech{flat contract property} specifies the behavior of a structure when
used as a flat contract. It is specified using
@scheme[build-flat-contract-property], and accepts exactly the same set of
arguments as @scheme[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
@scheme[simple-flat-contract].
}
@deftogether[(
@defproc[(contract-property? [x any/c]) boolean?]
@defproc[(flat-contract-property? [x any/c]) boolean?]
)]{
These predicates detect whether a value is a @tech{contract property} or a
@tech{flat contract property}, respectively.
} }
@; ------------------------------------------------------------------------ @; ------------------------------------------------------------------------
@section{Contract Utilities} @section{Contract Utilities}
@defproc[(guilty-party [exn exn?]) any]{
Extracts the name of the guilty party from an exception
raised by the contract system.}
@defproc[(contract? [v any/c]) boolean?]{ @defproc[(contract? [v any/c]) boolean?]{
Returns @scheme[#t] if its argument is a contract (i.e., constructed Returns @scheme[#t] if its argument is a contract (i.e., constructed
@ -1246,6 +1332,18 @@ may or may not hold. If the contract is a first-order
contract, a result of @scheme[#t] guarantees that the contract, a result of @scheme[#t] guarantees that the
contract holds.} contract holds.}
@defproc[(contract-name [c contract?]) any/c]{
Produces the name used to describe the contract in error messages.
}
@defproc[(contract-first-order [c contract?]) (-> any/c boolean?)]{
Produces the first order test used by @scheme[or/c] to match values to higher
order contracts.
}
@defproc[(contract-projection [c contract?]) (-> blame? (-> any/c any/c))]{
Produces the projection defining a contract's behavior on protected values.
}
@defproc[(make-none/c [sexp-name any/c]) contract?]{ @defproc[(make-none/c [sexp-name any/c]) contract?]{
@ -1253,31 +1351,22 @@ Makes a contract that accepts no values, and reports the
name @scheme[sexp-name] when signaling a contract violation.} name @scheme[sexp-name] when signaling a contract violation.}
@defparam[contract-violation->string @defparam[current-blame-format
proc proc
(-> any/c any/c (or/c #f any/c) any/c string? string?)]{ (-> blame? any/c string?)]{
This is a parameter that is used when constructing a This is a parameter that is used when constructing a
contract violation error. Its value is procedure that contract violation error. Its value is procedure that
accepts five arguments: accepts three arguments:
@itemize[ @itemize[
@item{the value that the contract applies to,} @item{the blame object for the violation,}
@item{a syntax object representing the source location where @item{the value that the contract applies to, and}
the contract was established, } @item{a message indicating the kind of violation.}]
@item{the name of the party that violated the contract (@scheme[#f] indicates that the party is not known, not that the party's name is @scheme[#f]), }
@item{an sexpression representing the contract, and }
@item{a message indicating the kind of violation.
}]
The procedure then The procedure then
returns a string that is put into the contract error returns a string that is put into the contract error
message. Note that the value is often already included in message. Note that the value is often already included in
the message that indicates the violation. the message that indicates the violation.
If the contract was establised via
@scheme[provide/contract], the names of the party to the
contract will be sexpression versions of the module paths
(as returned by @scheme[collapse-module-path]).
} }