improve the guide section on how to use the contract library

to build new combinators
This commit is contained in:
Robby Findler 2017-04-12 21:08:54 -05:00
parent 8bb8365a38
commit b3d3bf7c01
2 changed files with 65 additions and 19 deletions

View File

@ -44,7 +44,7 @@ they are not quite ready for use as contracts, because they
do not accommodate blame and do not provide good error
messages. In order to accommodate these, contracts do not
just use simple projections, but use functions that accept a
@deftech{blame object} encapsulating
@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{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
@ -109,7 +109,8 @@ arguments in the first two lines of
the @racket[int->int-proj] function. The trick here is that,
even though the @racket[int->int-proj] function always
blames what it sees as positive, we can swap the blame parties by
calling @racket[blame-swap] on the given @tech{blame object}, replacing
calling @racket[blame-swap] on the given
@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{blame object}, replacing
the positive party with the negative party and vice versa.
This technique is not merely a cheap trick to get the example to work,
@ -162,7 +163,8 @@ when a contract violation is detected.
While these projections are supported by the contract library
and can be used to build new contracts, the contract library
also supports a different API for projections that can be more
efficient. Specifically, a @deftech{late neg projection} accepts
efficient. Specifically, a
@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{late neg projection} accepts
a blame object without the negative blame information and then
returns a function that accepts both the value to be contracted and
the name of the negative party, in that order.
@ -196,11 +198,55 @@ to use this API looks like this:
'(expected "a procedure of one argument" given: "~e")
f))))]
The advantage of this style of contract is that the @racket[_blame]
and @racket[_f] arguments can be supplied on the server side of the
contract boundary and the result can be used for every different
argument can be supplied on the server side of the
contract boundary and the result can be used for each different
client. With the simpler situation, a new blame object has to be
created for each client.
One final problem remains before this contract can be used with the
rest of the contract system. In the function above,
the contract is implemented by creating a wrapper function for
@racket[f], but this wrapper function does not cooperate with
@racket[equal?], nor does it let the runtime system know that there
is a relationship between the result function and @racket[f], the input
function.
To remedy these two problems, we should use
@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{chaperones} instead
of just using @racket[λ] to create the wrapper function. Here is the
@racket[int->int-proj] function rewritten to use a
@tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{chaperone}:
@interaction/no-prompt[#:eval ex-eval
(define (int->int-proj blame)
(define dom-blame (blame-add-context blame
"the argument of"
#:swap? #t))
(define rng-blame (blame-add-context blame "the range of"))
(define (check-int v to-blame neg-party)
(unless (integer? v)
(raise-blame-error
to-blame #:missing-party neg-party
v
'(expected "an integer" given: "~e")
v)))
(λ (f neg-party)
(if (and (procedure? f)
(procedure-arity-includes? f 1))
(chaperone-procedure
f
(λ (x)
(check-int x dom-blame neg-party)
(values (λ (ans)
(check-int ans rng-blame neg-party)
ans)
x)))
(raise-blame-error
blame #:missing-party neg-party
f
'(expected "a procedure of one argument" given: "~e")
f))))]
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. Specifically, we can use
@ -470,5 +516,4 @@ to use in this program:
(maybe-accepts-a-function sqrt)
(maybe-accepts-a-function 123)]
@(close-eval ex-eval)

View File

@ -9,10 +9,8 @@
(the-eval '(require racket/contract racket/contract/parametric racket/list))
the-eval)))
@(define blame-object
@tech[#:doc '(lib "scribblings/guide/guide.scrbl")]{blame object})
@(define blame-objects
@tech[#:doc '(lib "scribblings/guide/guide.scrbl")]{blame objects})
@(define blame-object @tech{blame object})
@(define blame-objects @tech{blame objects})
@title[#:tag "contracts" #:style 'toc]{Contracts}
@ -2182,8 +2180,9 @@ The default test accepts any value. The predicate should be influenced by
the value of @racket[(contract-first-order-okay-to-give-up?)] (see it's documentation
for more explanation).
The @racket[late-neg-proj] defines the behavior of applying the contract. If it is
supplied, it accepts a blame object that does not have a value for
The @racket[late-neg-proj] argument defines the behavior of applying
the contract via a @deftech{late neg projection}. If it is
supplied, it accepts a @tech{blame object} that does not have a value for
the @racket[blame-negative] field. Then it must return a function that accepts
both the value that is getting the contract and the name of the blame party, in
that order. The result must either be the value (perhaps suitably wrapped
@ -2360,16 +2359,18 @@ For the costs from checking your new combinator to be included, you should wrap
any deferred, higher-order checks with this form. First-order checks are
recognized automatically and do not require this form.
If your combinator's projections operate on complete blame objects (i.e., no
missing blame parties), the blame object should be the first argument to this
If your combinator's projections operate on complete @tech{blame objects} (i.e., no
missing blame parties), the @tech{blame object} should be the first argument to this
form. Otherwise (e.g., in the case of @racket[_late-neg] projections), a pair
of the blame object and the negative party should be used instead.
of the @tech{blame object} and the negative party should be used instead.
@history[#:added "6.4.0.4"]
}
@subsection{Blame Objects}
This section describes @deftech{blame objects} and operations on them.
@defproc[(blame? [v any/c]) boolean?]{
This predicate recognizes @|blame-objects|.
}
@ -2520,7 +2521,7 @@ the other; both are provided for convenience and clarity.
@defproc[(blame-add-missing-party [b (and/c blame? blame-missing-party?)]
[missing-party any/c])
(and/c blame? (not/c blame-missing-party?))]{
Produces a new blame object like @racket[b], except that the missing
Produces a new @tech{blame object} like @racket[b], except that the missing
party is replaced with @racket[missing-party].
}
@ -2619,8 +2620,8 @@ The value is expected to be the blame record for the contract on the value or
a @racket[cons]-pair of a blame record with a missing party and the missing
party. The @racket[value-blame] function reassembles the arguments of the pair
into a complete blame record using @racket[blame-add-missing-party]. If
the value has one of the properties, but the value is not a blame object
or a pair whose @racket[car] position is a blame object, then @racket[has-blame?]
the value has one of the properties, but the value is not a @tech{blame object}
or a pair whose @racket[car] position is a @tech{blame object}, then @racket[has-blame?]
returns @racket[#f] but @racket[value-blame] returns @racket[#f].
}
@ -3080,7 +3081,7 @@ Produces the name used to describe the contract in error messages.
the contract checking, mostly used to create a meaningful error message if
a contract violation is detected. The resulting function's first argument
is the value that should have the contract and its second argument is
a ``missing party'' from the blame object, to be passed to @racket[raise-contract-error].
a ``missing party'' from the @tech{blame object}, to be passed to @racket[raise-contract-error].
If possible, use this function instead of @racket[contract-val-first-projection] or
@racket[contract-projection].