racket/collects/scribblings/guide/contracts-simple-function.scrbl
Robby Findler 765ec336e0 a little more progress
svn: r8205
2008-01-04 15:44:53 +00:00

276 lines
9.5 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[#:tag "contract-func"]{Simple Contracts on Functions}
When a module exports a function, it establishes two channels of
communication between itself and the client module that imports the
function. If the client module calls the function, it sends a value into the
"server" module. Conversely, if such a function call ends and the function
returns a value, the "server" module sends a value back to the "client" module.
It is important to keep this picture in mind when you read the explanations
of the various ways of imposing contracts on functions.
@question[#:tag "argcontract"]{How does a contract restrict the arguments of a function?}
Functions usually don't work on all possible Scheme values but only on a
select subset such as numbers, booleans, etc. Here is a module that may
represent a bank account:
@schememod[
scheme/base
(require scheme/contract)
(provide/contract
[create (-> string? number? any)]
[deposit (-> number? any)])
(define amount 0)
(define (create name initial-deposit) ...)
(define (deposit a) (set! amount (+ amount a)))
]
It exports two functions:
@itemize{
@item{@scheme[create]: The function's contract says that it consumes two
arguments, a string and a number, and it promises nothing about the return value. }}
@item{@scheme[deposit]: The function's contract demands from the client modules
that they apply it to numbers. It promises nothing about the return value. }
If a "client" module that were to apply @scheme[deposit] to
@scheme['silly], it would violate the contract. The contract monitoring
system would flag this violation and blame "client" for breaking the contract
with @scheme[myaccount].
@bold{Note:} Instead of @scheme[any] you could also use the
more specific contract @scheme[void?], which says that the function will
always return the @scheme[(void)] value. This contract, however, would require
the contract monitoring system to check the return value every time the function
is called, even though the "client" module can't do much with this value
anyway. In contrast, @scheme[any] tells the monitoring system @italic{not}
to check the return value. Additionally, it tells a potential client that the
"server" module @italic{makes no promises at all} about the function's return
value.
@question[#:tag "arrow"]{What does the arrow do?}
It is natural to use an arrow to say that an exported value is a
function. In decent high schools, you learn that a function has a domain
and a range, and that you write this fact down like this:
@schemeblock[
f : A -> B
]
Here the @scheme[A] and @scheme[B] are sets; @scheme[A] is the
domain and @scheme[B] is the range.
Functions in a programming language have domains and ranges, too. In
statically typed languages, you write down the names of types for each
argument and for the result. When all you have, however, is a Scheme name,
such as @scheme[create] or @scheme[deposit], you want to tell the
reader what the name represents (a function) and, if it is a function (or
some other complex value) what the pieces are supposed to be. This is why
we use a @scheme[->] to say "hey, expect this to be a function."
So @scheme[->] says "this is contract for a function." What follows
in a function contracts are contracts (sub-contracts if you wish) that tell
the reader what kind of arguments to expect and what kind of a result the
function will produce. For example,
@schemeblock[
(provide/contract
[create (-> string? number? boolean? account?)])
]
says that @scheme[create] is a function of three arguments: a string, a
number, and a boolean. Its result is an account.
In short, the arrow @scheme[->] is a @italic{contract
combinator}. Its purpose is to combine other contracts into a contract
that says "this is a function @italic{and} its arguments and its result are
like that."
@question[#:tag "dots"]{What are the dots in contracts about?}
If you are used to mathematics, you like the arrow in between the domain
and the range of a function, not at the beginning. If you have read "How
to Design Programs," you have seen this many times. Indeed, you may have
seen contracts such as these in other people's code:
@schemeblock[
(provide/contract
[create (string? number? boolean? . -> . account?)])
]
If a PLT Scheme S-expression contains two dots with a symbol in the middle,
the reader re-arranges the S-expression and place the symbol at the front. Thus,
@schemeblock[
(string? number? boolean? . -> . account?)
]
is really just a short-hand for
@schemeblock[
(-> string? number? boolean? account?)
]
Of course, placing the arrow to the left of the range follows not only
mathematical tradition but also that of typed functional languages.
@question[#:tag "own"]{Can I make my own contracts for arguments?}
The @scheme[deposit] function adds the given number to the value of
@scheme[amount]. While the function's contract prevents clients from
applying it to non-numbers, the contract still allows them to apply the function
to complex numbers, negative numbers, or inexact numbers, all of which do not
represent amounts of money.
To this end, the contract system allows programmers to define their own
contracts:
@schememod[
scheme/base
(require scheme/contract)
(define (amount? a)
(and (number? a) (integer? a) (exact? a) (>= a 0)))
(provide/contract
(code:comment "an amount is a natural number of cents")
(code:comment "is the given number an amount?")
[deposit (-> amount? any)]
[amount? (-> any/c boolean?)])
(define this 0)
(define (deposit a) (set! this (+ this a)))
]
The module introduces a
predicate, @scheme[amount?]. The @scheme[provide]
clause refers to this predicate, as a contract, for its
specification of the contract of
@scheme[deposit].
Of course it makes no sense to restrict a channel of
communication to values that the client doesn't
understand. Therefore the module also exports
the @scheme[amount?] predicate itself, with a contract
saying that it accepts an arbitrary value and returns a
boolean.
In this case, we could also have used @scheme[natural-number/c], which is
a contract defined in "contract.ss" that is equivalent to @scheme[amount]
(modulo the name):
@schememod[
scheme/base
(require scheme/contract)
(provide/contract
(code:comment "an amount is a natural number of cents")
[deposit (-> natural-number/c any)])
(define this 0)
(define (deposit a) (set! this (+ this a)))
]
Lesson: look up the built-in contracts.
@question[#:tag "and-or"]{Are @scheme[and/c] and @scheme[or/c] contract combinators? What of @scheme[listof]?}
The short answer is yes. Both @scheme[and/c] and @scheme[or/c]
ombine contracts and they do what you expect them to do.
For example, if we didn't have @scheme[natural-number/c], the
@scheme[amount?] contract is a bit opaque. Instead, we would define it
as follows:
@schememod[
scheme/base
(require scheme/contract)
(define amount
(and/c number? integer? exact? (or/c positive? zero?)))
(provide/contract
(code:comment "an amount is a natural number of cents")
(code:comment "is the given number an amount?")
[deposit (-> amount any)])
(define this 0)
(define (deposit a) (set! this (+ this a)))
]
That is, amount is a contract that enforces the following conditions: the
value satisfies @scheme[number?] and @scheme[integer?] and
@scheme[exact?] and is either @scheme[positive?] or
@scheme[zero?].
Oh, we almost forgot. What do you think @scheme[(listof char?)]
means? Hint: it is a contract!
@question[#:tag "range"]{How does a contract restrict the range of a function?}
Consider a utility module for creating strings from banking records:
@schememod[
scheme/base
(require scheme/contract)
(provide/contract
...
(code:comment "convert a random number to a string")
[format-number (-> number? string?)]
(code:comment "convert an amount into a dollar based string")
[format-nat (-> natural-number/c
(lambda (result)
(define L (string-length result))
(and (string? result)
(>= L 3)
(char=? #\. (string-ref result (- L 3))))))])
...
]
The contract of the exported function @scheme[format-number] specifies that
the function consumes a number and produces a string.
The contract of the exported function @scheme[format-nat] is more
interesting than the one of @scheme[format-number]. It consumes only
natural numbers. Its range contract promises a string that has a dot "." in the
third position from the right.
@bold{Exercise 1} Strengthen the promise of the range contract for
@scheme[format-nat] so that it admits only strings with digits and a single
dot.
@question[#:tag "exercise1"]{Solution to Exercise 1}
@schememod[
scheme/base
(require scheme/contract)
(define (digit-char? x)
(member x '(#\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9 #\0)))
(provide/contract
...
(code:comment "convert a random number to a string")
[format-number (-> number? string?)]
(code:comment "convert an amount (natural number) of cents")
(code:comment "into a dollar based string")
[format-nat (-> natural-number/c
(lambda (result)
(define L (string-length result))
(and (string? result)
(andmap digit-char?
(string->list (substring result 0 (- L 3))))
(andmap digit-char?
(string->list (substring result (- L 2) L)))
(char=? #\. (string-ref result (- L 3))))))])
]