7.3 Contracts on Functions in General
The -> contract constructor works for functions that take a fixed number of arguments and where the result contract is independent of the input arguments. To support other kinds of functions, Racket supplies additional contract constructors, notably ->* and ->i.
7.3.1 Optional Arguments
Take a look at this excerpt from a string-processing module, inspired by the Scheme cookbook:
#lang racket |
(provide/contract |
; pad the given str left and right with |
; the (optional) char so that it is centered |
[string-pad-center (->* (string? natural-number/c) |
(char?) |
string?)]) |
(define (string-pad-center str width [pad #\space]) |
(define field-width (min width (string-length str))) |
(define rmargin (ceiling (/ (- width field-width) 2))) |
(define lmargin (floor (/ (- width field-width) 2))) |
(string-append (build-string lmargin (λ (x) pad)) |
str |
(build-string rmargin (λ (x) pad)))) |
The module exports string-pad-center, a function that creates a string of a given width with the given string in the center. The default fill character is #\space; if the client module wishes to use a different character, it may call string-pad-center with a third argument, a char, overwriting the default.
The function definition uses optional arguments, which is appropriate for this kind of functionality. The interesting point here is the formulation of the contract for the string-pad-center.
The contract combinator ->*, demands several groups of contracts:
The first one is a parenthesized group of contracts for all required arguments. In this example, we see two: string? and natural-number/c.
The second one is a parenthesized group of contracts for all optional arguments: char?.
The last one is a single contract: the result of the function.
Note if a default value does not satisfy a contract, you won’t get a contract error for this interface. If you can’t trust yourself to get the initial value right, you need to communicate the initial value across a boundary.
7.3.2 Rest Arguments
The max operator consumes at least one real number, but it accepts any number of additional arguments. You can write other such functions using a “rest” argument, such as in max-abs:
See Declaring a Rest Argument for an introduction to rest arguments.
(define (max-abs n . rst) |
(foldr (lambda (n m) (max (abs n) m)) (abs n) rst)) |
Describing this function through a contract requires a further extension of ->*: a #:rest keyword specifies a contract on a list of arguments after the required and optional arguments:
(provide/contract |
[max-abs (->* (real?) () #:rest (listof real?) real?)]) |
As always for ->*, the contracts for the required arguments are enclosed in the first pair of parentheses, which in this case is a single real number. The empty pair of parenthesis indicates that there are no optional arguments (not counting the rest arguments). The contract for the rest argument follows #:rest; since all additional arguments must be real numbers, the list of rest arguments must satisfy the contract (listof real?).
7.3.3 Keyword Arguments
It turns out that the -> contract constructor also contains support for keyword arguments. For example, consider this function, which creates a simple GUI and asks the user a yes-or-no question:
See Declaring Keyword Arguments for an introduction to keyword arguments.
#lang racket/gui |
(define (ask-yes-or-no-question question |
#:default answer |
#:title title |
#:width w |
#:height h) |
(define d (new dialog% [label title] [width w] [height h])) |
(define msg (new message% [label question] [parent d])) |
(define (yes) (set! answer #t) (send d show #f)) |
(define (no) (set! answer #f) (send d show #f)) |
(define yes-b (new button% |
[label "Yes"] [parent d] |
[callback (λ (x y) (yes))] |
[style (if answer '(border) '())])) |
(define no-b (new button% |
[label "No"] [parent d] |
[callback (λ (x y) (no))] |
[style (if answer '() '(border))])) |
(send d show #t) |
answer) |
(provide/contract |
[ask-yes-or-no-question |
(-> string? |
#:default boolean? |
#:title string? |
#:width exact-integer? |
#:height exact-integer? |
boolean?)]) |
If you really want to ask a yes-or-no question via a GUI, you should use message-box/custom. For that matter, it’s usually better to provide buttons with more specific answers than “yes” and “no.”
The contract for ask-yes-or-no-question uses ->, and in the same way that lambda (or define-based functions) allows a keyword to precede a functions formal argument, -> allows a keyword to precede a function contract’s argument contract. In this case, the contract says that ask-yes-or-no-question must receive four keyword arguments, one for each of the keywords #:default, #:title, #:width, and #:height. As in a function definition, the order of the keywords in -> relative to each other does not matter for clients of the function; only the relative order of argument contracts without keywords matters.
7.3.4 Optional Keyword Arguments
Of course, many of the parameters in ask-yes-or-no-question (from the previous question) have reasonable defaults and should be made optional:
(define (ask-yes-or-no-question question |
#:default answer |
#:title [title "Yes or No?"] |
#:width [w 400] |
#:height [h 200]) |
...) |
To specify this function’s contract, we need to use ->* again. It supports keywords just as you might expect in both the optional and mandatory argument sections. In this case, we have the mandatory keyword #:default and optional keywords #:title, #:width, and #:height. So, we write the contract like this:
(provide/contract |
[ask-yes-or-no-question |
(->* (string? |
#:default boolean?) |
(#:title string? |
#:width exact-integer? |
#:height exact-integer?) |
boolean?)]) |
That is, we put the mandatory keywords in the first section, and we put the optional ones in the second section.
7.3.5 Contracts for case-lambda
A function defined with case-lambda might impose different constraints on its arguments depending on how many are provided. For example, a report-cost function might convert either a pair or numbers or a string into a new string:
See Arity-Sensitive Functions: case-lambda for an introduction to case-lambda.
| ||||
| ||||
|
(provide/contract |
[report-cost |
(case-> |
(integer? integer? . -> . string?) |
(string? . -> . string?))]) |
7.3.6 Argument and Result Dependencies
The following is an excerpt from an imaginary numerics module:
(provide/contract |
[real-sqrt (->i ([argument (>=/c 1)]) |
[result (argument) (<=/c argument)])]) |
The contract for the exported function real-sqrt uses the ->i rather than ->* function contract. The “i” stands for an indy dependent contract, meaning the contract for the function range depends on the value of the argument. The appearance of argument in the line for result’s contract means that the result depends on the argument. In this particular case, the argument of real-sqrt is greater or equal to 1, so a very basic correctness check is that the result is smaller than the argument.
In general, a dependent function contract looks just like the more general ->* contract, but with names added that can be used elsewhere in the contract.
Going back to the back-account example, suppose that we generalize the module to support multiple accounts and that we also include a withdrawal operation. The improved bank-account module includes a account structure type and the following functions:
(provide/contract |
[balance (-> account? amount/c)] |
[withdraw (-> account? amount/c account?)] |
[deposit (-> account? amount/c account?)]) |
Besides requiring that a client provide a valid amount for a withdrawal, however, the amount should be less than the specified account’s balance, and the resulting account will have less money than it started with. Similarly, the module might promise that a deposit produces an account with money added to the account. The following implementation enforces those constraints and guarantees through contracts:
#lang racket |
; section 1: the contract definitions |
(struct account (balance)) |
(define amount/c natural-number/c) |
; section 2: the exports |
(provide/contract |
[create (amount/c . -> . account?)] |
[balance (account? . -> . amount/c)] |
[withdraw (->i ([acc account?] |
[amt (acc) (and/c amount/c (<=/c (balance acc)))]) |
[result (acc amt) |
(and/c account? |
(lambda (res) |
(>= (balance res) |
(- (balance acc) amt))))])] |
[deposit (->i ([acc account?] |
[amt amount/c]) |
[result (acc amt) |
(and/c account? |
(lambda (res) |
(>= (balance res) |
(+ (balance acc) amt))))])]) |
; section 3: the function definitions |
(define balance account-balance) |
(define (create amt) (account amt)) |
(define (withdraw a amt) |
(account (- (account-balance a) amt))) |
(define (deposit a amt) |
(account (+ (account-balance a) amt))) |
The contracts in section 2 provide typical type-like guarantees for create and balance. For withdraw and deposit, however, the contracts check and guarantee the more complicated constraints on balance and deposit. The contract on the second argument to withdraw uses (balance acc) to check whether the supplied withdrawal amount is small enough, where acc is the name given within ->i to the function’s first argument. The contract on the result of withdraw uses both acc and amt to guarantee that no more than that requested amount was withdrawn. The contract on deposit similarly uses acc and amount in the result contract to guarantee that at least as much money as provided was deposited into the account.
As written above, when a contract check fails, the error message is not great. The following revision uses flat-named-contract within a helper function mk-account-contract to provide better error messages.
#lang racket |
; section 1: the contract definitions |
(struct account (balance)) |
(define amount/c 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)) |
; section 2: the exports |
(provide/contract |
[create (amount/c . -> . account?)] |
[balance (account? . -> . amount/c)] |
[withdraw (->i ([acc account?] |
[amt (acc) (and/c amount/c (<=/c (balance acc)))]) |
[result (acc amt) (mk-account-contract acc amt >= msg>)])] |
[deposit (->i ([acc account?] |
[amt amount/c]) |
[result (acc amt) |
(mk-account-contract acc amt <= msg<)])]) |
; section 3: the function definitions |
(define balance account-balance) |
(define (create amt) (account amt)) |
(define (withdraw a amt) |
(account (- (account-balance a) amt))) |
(define (deposit a amt) |
(account (+ (account-balance a) amt))) |
7.3.7 Checking State Changes
(->i ([parent (is-a?/c area-container-window<%>)]) |
[_ (parent) |
(let ([old-children (send parent get-children)]) |
(λ (child) |
(andmap eq? |
(append old-children (list child)) |
(send parent get-children))))]) |
The range contract ensures that the function only modifies the children of parent by adding a new child to the front of the list. It accomplishes this by using the _ instead of a normal identifier, which tells the contract library that the range contract does not depend on the values of any of the results, and thus the contract library evaluates the expression following the _ when the function is called, instead of when it returns. Therefore the call to the get-children method happens before the function under the contract is called. When the function under contract returns, its result is passed in as child, and the contract ensures that the children after the function return are the same as the children before the function called, but with one more child, at the front of the list.
#lang racket |
(define x '()) |
(define (get-x) x) |
(define (f) (set! x (cons 'f x))) |
(provide/contract |
[f (->i () [_ (begin (set! x (cons 'ctc x)) any/c)])] |
[get-x (-> (listof symbol?))]) |
7.3.8 Multiple Result Values
(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 '())) |
(define (substring-of? s) |
(flat-named-contract |
(format "substring of ~s" s) |
(lambda (s2) |
(and (string? s2) |
(<= (string-length s2) s) |
(equal? (substring s 0 (string-length s2)) s2))))) |
(provide/contract |
[split (->i ([fl (listof char?)]) |
(values [s (fl) (substring-of (list->string fl))] |
[c (listof char?)]))]) |
(provide/contract |
[split (->i ([fl (listof char?)]) |
(values [s (fl) (string-len/c (length fl))] |
[c (listof char?)]))]) |
7.3.9 Fixed but Statically Unknown Arities
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.
; (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))))) |
The argument of n-step is proc, a function proc whose results are either numbers or false, and a list. It then applies proc to the list inits. As long as proc returns a number, n-step treats that number as an increment for each of the numbers in inits and recurs. When proc returns false, the loop stops.
; nat -> nat |
(define (f x) |
(printf "~s\n" x) |
(if (= x 0) #f -1)) |
(n-step f '(2)) |
; 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)) |
(provide/contract |
[n-step |
(->i ([proc (inits) |
(and/c (unconstrained-domain-> |
(or/c false/c number?)) |
(λ (f) (procedure-arity-includes? |
f |
(length inits))))] |
[inits (listof number?)]) |
() |
any)]) |