racket/collects/scribblings/guide/contracts-general-function.scrbl
Robby Findler 7f3a5c13c2 added schemeerror and used it
svn: r8223
2008-01-05 16:46:17 +00:00

588 lines
19 KiB
Racket

#lang scribble/doc
@require[scribble/manual]
@require[scribble/eval]
@require["guide-utils.ss"]
@require["contracts-utils.ss"]
@(require (for-label scheme/contract))
@title{Contracts on Functions in General}
@question[#:tag "flat-named-contracts"]{Why do the error messages contain "..."?}
You wrote your module. You added contracts. You put them into the interface
so that client programmers have all the information from interfaces. It's a
piece of art:
@schememod[
scheme/base
(require scheme/contract)
(provide/contract
[deposit (-> (lambda (x)
(and (number? x) (integer? x) (>= x 0)))
any)])
(define this 0)
(define (deposit a) ...)
]
Several clients used your module. Others used their
modules. And all of a sudden one of them sees this error
message:
@schemeerror{bank-client broke the contract (-> ??? any)
it had with myaccount on deposit;
expected <???>, given: -10}
Clearly, @scheme[bank-client] is a module that uses
@scheme[myaccount] but what are the @tt{?}s doing there?
Wouldn't it be nice if we had a name for this class of data
much like we have string, number, and so on?
For this situation, PLT Scheme provides "flat named contracts". The use of
"contract" shows that contracts are first-class values. The "flat" means that
the collection of data is a subset of the built-in atomic classes of data; they
are described by a predicate that consumes all Scheme values and produces a
boolean. The "named" part says what we want to do: name the contract so that
error messages become intelligible:
@schememod[
scheme/base
(require scheme/contract)
(define (amount? x) (and (number? x) (integer? x) (>= x 0)))
(define amount (flat-named-contract 'amount amount?))
(provide/contract
[deposit (amount . -> . any)])
(define this 0)
(define (deposit a) ...)
]
With this little change, the error message becomes all of
sudden quite readable:
@schemeerror{bank-client broke the contract (-> amount any)
it had with myaccount on deposit;
expected <amount>, given: -10"}
@question[#:tag "optionals"]{Can a contract specify what the values of optional arguments to a function must be?}
Sure, using the @scheme[->*] contract. For example,
@question[#:tag "arrow-d"]{Can a contract specify that the result depends on the arguments?}
Here is an excerpt from an imaginary (pardon the pun) numerics module:
@schememod[
scheme/base
(require scheme/contract)
(provide/contract
[sqrt.v1 (->d ([argument (>=/c 1)])
()
[result (<=/c argument)])])
...
]
The contract for the exported function @scheme[sqrt.v1] uses the
@scheme[->d] rather than @scheme[->] function contract. The "d"
stands for <em>dependent</em> contract, meaning the contract for the
function range depends on the value of the argument.
In this particular case, the argument of @scheme[sqrt.v1] is greater
or equal to 1. Hence a very basic correctness check is that the result is
smaller than the argument. (Naturally, if this function is critical, one
could strengthen this check with additional clauses.)
In general, a dependent function contract looks just like
the more general @scheme[->*] contract, but with names added
that can be used elsewhere in the contract.
Yes, there are many other contract combinators such as @scheme[<=/c]
and @scheme[>=/c], and it pays off to look them up in the contract
section of the reference manual. They simplify contracts tremendously
and make them more accessible to potential clients.
@;{
To add: keywords, optional arguments.
in dependent contracts, discuss what happens when a
dependent contract with optional arguments doesn't appear at
the call site.
}
@;{
<question title="Can a contract specify that arguments depend on each other?" tag="arrow-r">
<p>Eventually bank customers want their money back. Hence, a module that
implements a bank account must include a method for withdrawing money. Of
course, ordinary accounts don't let customers withdraw an arbitrary amount of
money but only as much as they have in the account.
<p>Suppose the account module provides the following two functions:
<scheme>
;; balance : Account -> Amount
;; withdraw : Account Amount -> Account
</scheme>
Then, informally, the proper precondition for @scheme[withdraw] is that
<blockquote>
"the balance of the given account is greater than or equal to the given (withdrawal) amount."
</blockquote>
The postcondition is similar to the one for <a
href="#deposit">@scheme[deposit]</a>:
<blockquote>
"the balance of the resulting account is larger than (or equal to) than the one of the
given account."
</blockquote>
You could of course also formulate a full-fledged correctness condition, namely,
that the balance of the resulting account is equal to the balance of the given
one, plus the given amount.
<p>The following module implements accounts imperatively and specifies the
conditions we just discussed:
<table>
<tr><td>
<scheme>
(module account mzscheme
(require (lib "contract.ss"))
<font color="deeppurple">
(define-struct account (balance))
(define amount natural-number/c)
(define msg>
"account a with balance larger than ~a expected")
(define msg<
"account a with balance less than ~a expected")
(define (mk-account-contract acc amt op msg)
(define balance0 (balance acc))
(define (ctr a)
(and (account? a) (op balance0 (balance a))))
(flat-named-contract (format msg balance0) ctr))</font>
<font color="purple">
(provide/contract
[create (amount . -> . account?)]
[balance (account? . -> . amount)]
[withdraw (->r ([acc account?]
[amt (and/c amount (&lt;/c (balance acc)))])
(mk-account-contract acc amt > msg>))]
[deposit (->r ([acc account?]
[amt amount])
(mk-account-contract acc amt < msg<))])
</font>
(define balance account-balance)
(define (create amt) (make-account amt))
(define (withdraw acc amt)
(set-account-balance! acc (- (balance acc) amt))
acc)
(define (deposit acc amt)
(set-account-balance! acc (+ (balance acc) amt))
acc))
</scheme>
<td bgcolor="beige" valign="top">
<pre>
the contract definitions
the exports
the function definitions
</pre>
</table>
The purple part is the export interface:
<ol>
<li>@scheme[create] consumes an initial deposit and produces an
account. This kind of contract is just like a type in a statically typed
language, except that statically typed languages usually don't support the type
"natural numbers" (as a full-fledged subtype of numbers).
</li>
<li>@scheme[balance] consumes an account and computes its current balance.
</li>
<li>@scheme[withdraw] consumes an account, named @scheme[acc], and an
amount, @scheme[amt]. In addition to being an @scheme[amount], the
latter must also be less than @scheme[(balance acc)], i.e., the balance of
the given account. That is, the contract for @scheme[amt] depends on the
value of @scheme[acc], which is what the @scheme[->r] (r for recursive)
contract combinator expresses.
<p>The result contract is formed on the fly: @scheme[(mk-account-contract acc amt
> msg>)]. It is an application of a contract-producing function that
consumes an account, an amount, a comparison operator, and an error message (a
format string). The result is a contract.
</li>
<li>@scheme[deposit]'s contract has been reformulated using the
@scheme[->r] combinator. Strictly speaking, this isn't necessary and the use
of @scheme[->d] would suffice, because the contracts for the arguments do
not depend on each other.
</li>
</ol>
The code in deep purple defines all those pieces that are needed for the
formulation of the export contracts: @scheme[account?], @scheme[amount],
error messages (format strings), and @scheme[mk-account-contract]. The
latter is a function that extracts the current balance from the given account
and then returns a named contract, whose error message (contract name) is a
string that refers to this balance. The resulting contract checks whether an
account has a balance that is larger or smaller, depending on the given
comparison operator, than the original balance.
</question>
<question title="What about rest arguments?" tag="rest-args">
<p>We all know that @scheme[+] in Beginner Scheme is a function that
consumes at least two numbers but, in principle, arbitrary
manner. Defining the function is easy:
<scheme>
(define (plus fst snd . rst)
(foldr + (+ fst snd) rst))
</scheme>
Describing this function via a contract is difficult because of the rest
argument (@scheme[rst]).
<p>Here is the contract:
<scheme>
(provide/contract
[plus (->* (number? number?) (listof number?) (number?))])
</scheme>
The @scheme[->*] contract combinator empowers you to specify
functions that consume a variable number of arguments or functions like
@scheme[plus], which consume "at least this number" of arguments but
an arbitrary number of additional arguments.
<p>The contracts for the required arguments are enclosed in an additional
pair of parentheses:
<scheme>
(number? number?)
</scheme>
For @scheme[plus] they demand two numbers. The contract for the
rest argument follows:
<scheme>
(listof number?)
</scheme>
Since the remainder of the actual arguments are collected in a list for
a @scheme[rest] parameter such as @scheme[rst], the contract
demands a list of values; in this specific examples, these values must be
number.
<p>Finally, you may have noticed that the contract for the function range
of @scheme[plus] is also wrapped in a pair of parentheses. The reason
for those is that functions can return multiple values not just one, and
this is our next topic in this guide.
</question>
<question title="What about case-lambda?" tag="case-lambda">
<p><a href="http://www.scheme.com/csug/binding.html">Dybvig</a> explains
the meaning and pragmatics of @scheme[case-lambda] with the following
example (among others):
<scheme>
(define substring1
(case-lambda
[(s) (substring1 s 0 (string-length s))]
[(s start) (substring1 s start (string-length s))]
[(s start end) (substring s start end)]))
</scheme>
This version of @scheme[substring] has one of the following signature:
<ol>
<li>just a string, in which case it copies the string;
<li>a string and an index into the string, in which case it extracts the
suffix of the string starting at the index; or
<li>a string a start index and an end index, in which case it extracts the
fragment of the string between the two indices.
</ol>
<p>The contract for such a function is formed with the @scheme[case->]
combinator, which combines as many functional contracts as needed:
<scheme>
(provide/contract
[substring1 (case->
(string? . -> . string?)
(string? natural-number/c . -> . string?)
(string? natural-number/c natural-number/c . -> . string?))])
</scheme>
As you can see, the contract for @scheme[substring1] combines three
function contracts, just as many clauses as the explanation of its
functionality required.
<p>In the case of @scheme[substring1], we also know that the indices
that it consumes ought to be natural numbers less than the length of the
given string. Since @scheme[case->] just combines arrow contracts,
adding such constraints is just a matter of strengthening the individual
contracts:
<scheme>
(provide/contract
[substring1 (case->
(string? . -> . string?)
(->r ([s string?]
[_ (and/c natural-number/c (</c (string-length s)))])
string?)
(->r ([s string?]
[a (and/c natural-number/c (</c (string-length s)))]
[o (and/c natural-number/c
(>=/c a)
(</c (string-length s)))])
string?))])
</scheme>
Here we used @scheme[->r] to name the parameters and express the
numeric constraints on them.
</question>
<question title="What about multiple values?" tag="multiple">
<p>The function @scheme[split] consumes a list of @scheme[char]s
and delivers the string that occurs before the first occurrence of
@scheme[#\newline] (if any) and the rest of the list:
<scheme>
(define (split l)
(define (split l w)
(cond
[(null? l) (values (list->string (reverse w)) '())]
[(char=? #\newline (car l)) (values (list->string (reverse w)) (cdr l))]
[else (split (cdr l) (cons (car l) w))]))
(split l '()))
</scheme>
It is a typical multiple-value function, returning two values by
traversing a single list.
<p>
The contract for such a function can use the ordinary
function arrow @scheme[->], since it
treats @scheme[values] specially, when it appears as the
last result:
<scheme>
(provide/contract
[split (-> (listof char?)
(values string? (listof char?)))])
</scheme>
</p>
<p>The contract for such a function can also be written
using @scheme[->*], just like
@scheme[plus]:
<scheme>
(provide/contract
[split (->* ((listof char?))
(string? (listof char?)))])
</scheme>
As before the contract for the single argument is wrapped
in an extra pair of parentheses (and must always be wrapped
like that); so are the contracts for the results: a string
and a list of characters.
</p>
<p>Now suppose we also want to ensure that the first result of
@scheme[split] is a prefix of the given word in list format. In that
case, we need to combine the @scheme[->*] contract combinator with the
@scheme[->d] combinator, which is of course the @scheme[->d*]
combinator:
<scheme>
(provide/contract
[splitp (->d* ((listof char?))
(lambda (fl)
(define wd (list->string fl))
(values (and/c string? (lambda (w) (string<=? w wd)))
(listof char?))))])
</scheme>
Like @scheme[->d], the new combinator uses a function over the
argument to create the range contracts. Yes, it doesn't just return one
contract but as many as the function produces values: one contract per
value. In this case, the second contract is the same as before, ensuring
that the second result is a list of @scheme[char]s. In contrast, the
first contract strengthens the old one so that the result is a prefix of
the given word.
<p>This contract is expensive to check of course. Here is a slightly
cheaper version:
<scheme>
(provide/contract
[splitl (->d* ((listof char?))
(lambda (fl)
(values (and/c string? (string/len (add1 (length fl))))
(listof char?))))])
</scheme>
Check the help desk for an explanation of @scheme[string/len].
</question>
<question title="What about procedures of any specific arity?" tag="no-domain">
<p>
Imagine yourself writing a contract for a function that accepts some other
function and a list of numbers that eventually applies the former to the
latter. Unless the arity of the given function matches the length of the
given list, your procedure is in trouble.
</p>
<p>Consider this @scheme[n-step] function:
<scheme>
;; (Number ... -> (union #f number?)) (listof Number) -> Void
(define (n-step proc inits)
(let ([inc (apply proc inits)])
(when inc
(n-step proc (map (λ (x) (+ x inc)) inits)))))
</scheme>
The argument of @scheme[n-step] is @scheme[proc], a function
@scheme[proc] whose results are either numbers or false, and a list. It
then applies @scheme[proc] to the list @scheme[inits]. As long as
@scheme[proc] returns a number, @scheme[n-step] treats that number
as an increment for each of the numbers in @scheme[inits] and
recurs. When @scheme[proc] returns @scheme[false], the loop stops.
</p>
Here are two uses:
<table>
<tr>
<td width="20" />
<td valign="top">
<pre>@scheme[
;; Nat -> Nat
(define (f x)
(printf "~s \n" x)
(if (= x 0) #f -1))
(n-step f '(2))
]</pre></td>
<td width="150" />
<td valign="top" width="150"><pre>@scheme[
;; Nat Nat -> Nat
(define (g x y)
(define z (+ x y))
(printf "~s\n" (list x y z))
(if (= z 0) #f -1))
(n-step g '(1 1))
]</pre></td></tr>
</table>
</center>
<p>A contract for @scheme[n-step] must specify two aspects of
@scheme[proc]'s behavior: its arity must include the number of elements
in @scheme[inits], and it must return either a number of
@scheme[#f]. The latter is easy, the former is difficult. At first
glance, this appears to suggest a contract that assigns a
<em>variable-arity</em> to @scheme[proc]:
@scheme[
(->* ()
(listof any/c)
(or/c number? false/c))
]
This contract, however, says that the function must accept <em>any</em>
number of arguments, not a <em>specific</em> but
<em>undetermined</em> number. Thus, applying @scheme[n-step] to
@scheme[(lambda (x) x)] and @scheme[(list 1)] breaks the contract
because the given function accepts only one argument.
</p>
<p>
The correct contract uses the @scheme[unconstrained-domain->]
combinator, which specifies only the range of a function, not its
domain. It is then possible to combine this contract with an arity test to
specify the correct @scheme[n-step]'s contract:
<scheme>
(provide/contract
[n-step
(->r ([proc (and/c (unconstrained-domain-> (or/c false/c number?))
(λ (f) (procedure-arity-includes? f (length inits))))]
[inits (listof number?)])
any)])
</scheme>
</p>
</question>
<question title="What about opt-lambda?" tag="opt-lambda">
<p>Take a look at this excerpt from a string-processing module, inspired by the
Scheme cookbook:
<scheme>
(module string-pad mzscheme
(require (lib "contract.ss") (lib "etc.ss") (lib "string.ss" "srfi" "13"))
(provide/contract
;; pad the given str left and right with
;; the (optional) char so that it is centered
[string-pad-center (opt-> (string? natural-number/c) (char?)
string?)])
(define string-pad-center
(opt-lambda (str width [pad #\space])
(define field-width (min width (string-length str)))
(define rmargin (- width (floor (/ (- width field-width) 2))))
(string-pad (string-pad-right str rmargin pad) width pad))))
</scheme>
The module exports @scheme[string-pad-center], a function that creates a
string of a given @scheme[width] with the given string in the center. The
default fill character is @scheme[#\space]; if the client module requires
different character, it may call @scheme[string-pad-center] with a third
argument, a @scheme[char], overwriting the default.
<p>The function definition uses @scheme[opt-lambda], which is appropriate
for this kind of functionality. The interesting point here is the formulation of
the contract for @scheme[opt-lambda]. Like the contract combinator for
@scheme[->*], i.e., rest arguments, @scheme[opt->] demands several
groups of contracts:
<ol>
<li>The first one is a parenthesized group of contracts for all required
arguments. In this example, we see two: @scheme[string?] and
@scheme[natural-number/c].
<li>The second one is a parenthesized group of contracts for all optional
arguments: @scheme[char?].
<li>The last one is a single contract: the result of the function.
</ol>
Note if a default value does not satisfy a contract, you won't get a contract
error for this interface. We do trust <em>you</em>; if you can't trust
yourself, you need to communicate across boundaries for everything you write.
The contract library does not have built-in combinators to
specify richer contracts for functions that have optional
arguments, like functions that have optional arguments where
the arguments depend on each other.
To specify such contracts combine @scheme[case->] with
the other function contract combinators, like we did in
the @scheme[substring1] function above.
}