16 Checking Program Invariants Dynamically: Contracts
Type systems offer rich and valuable ways to represent program invariants. However, they also represent an important trade-off, because not all non-trivial properties of programs can be verified staticallyThis is a formal property, known as Rice’s Theorem.. Furthermore, even if we can devise a method to settle a certain property statically, the burdens of annotation and computational complexity may be too great. Thus, it is inevitable that some of the properties we care about must either be ignored or settled only at run-time. Here, we will discuss run-time enforcement.
Virtually every programming language has some form of assertion mechanism that enables programmers to write properties that are richer than the language’s static type system permits. In languages without static types, these properties might start with simple type-like assertions: whether a parameter is numeric, for instance. However, the language of assertions is often the entire programming language, so any predicate can be used as an assertion: for instance, an implementation of a cryptography package might want to ensure certain parameters pass a primality test, or a balanced binary search-tree might want to ensure that its subtrees are indeed balanced and preserve the search-tree ordering.
16.1 Contracts as Predicates
It is therefore easy to see how to implement simple contracts.In what follows we will use the language #lang plai, for two reasons. First, this better simulates programming in an untyped language. Second, for simplicity we will write type-like assertions as contracts, but in the typed language these will be flagged by the type-checker itself, not letting us see the run-time behavior. In effect, it is easier to “turn off” the type checker. However, contracts make perfect sense even in a typed world, because they enhance the set of invariants that a programmer can express. A contract embodies a predicate. It consumes a value and applies the predicate to the value. If the value passes the predicate, the contract returns the value unmolested; if the value fails, the contract reports an error. Its only behaviors are to return the supplied value or to error: it should not change the value in any way. In short, on values that pass the predicate, the contact itself acts as the identity function.
(define (make-contract pred?) (lambda (val) (if (pred? val) val (blame "violation")))) (define (blame s) (error 'contract "~a" s))
(define non-neg?-contract (make-contract (lambda (n) (and (number? n) (>= n 0)))))
(define (real-sqrt-1 x) (sqrt (non-neg?-contract x)))
(define (real-sqrt-2 x) (begin (non-neg?-contract x) (sqrt x)))
16.2 Tags, Types, and Observations on Values
At this point we’ve reproduced the essence of assertion systems in most languages. What else is there to say? Let’s suppose for a moment that our language is not statically typed. Then we will want to write assertions that reproduce at least traditional type-like invariants, if not more. make-contract above can capture all standard type-like properties such as checking for numbers, strings, and so on, assuming the appropriate predicates are either provided by the language or can be fashioned from the ones given. Or can it?
Recall that even our simplest type language had not just base types, like numbers, but also constructed types. While some of these, like lists and vectors, appear to not be very challenging, they are once we care about mutation, performance, and blame, which we discuss below. However, functions are immediately problematic.
(define d/dx (lambda (f) (lambda (x) (/ (- (f (+ x 0.001)) (f x)) 0.001))))
((number -> number) -> (number -> number))
The fundamental problem is that in most languages, we cannot directly
express this as a predicate. Most language run-time systems store
very limited information about the types of values—
When we get to structured values, however, the situation is more complex. A vector would have a tag declaring it to be a vector, but not dictating what kinds of values its elements are (and they may not even all be of the same kind); however, a program can usually also obtain its size, and thus traverse it, to gather this information. (There is, however, more to be said about structured values below [REF].)
Write a contract that checks that a list consists solely of even numbers.
(define list-of-even?-contract (make-contract (lambda (l) (and (list? l) (andmap number? l) (andmap even? l)))))
In every language, however, this becomes problematic when we encounter
functions. We might think of a function as having a type for its
domain and range, but to a run-time system, a function is just an
opaque object with a function tag, and perhaps some very limited
metadata (such as the function’s arity). The run-time system can
hardly even tell whether the function consumes and produces
functions—
This problem is nicely embodied in the (misnamed) typeof operator in JavaScript. Given values of base types like numbers and strings, typeof returns a string to that effect (e.g., "number"). For objects, it returns "object". Most importantly, for functions it returns "function", with no additional information.For this reason, perhaps typeof is a bad name for this operator. It should have been called tagof instead, leaving open the possibility that future static type systems for JavaScript could provide a true typeof.
To summarize, this means that at the point of being confronted with a function, a function contract can only check that it is, indeed, a function (and if it is not, that is clearly an error). It cannot check anything about the domain and range of the function. Should we give up?
16.3 Higher-Order Contracts
(lambda () (real-sqrt-1 -1))
This is a useful insight, because it offers a solution to our problem with functions. We check, immediately, that the purported function value truly is a function. However, instead of ignoring the domain and range contracts, we defer them. We check the domain contract when (and each time) the function is actually applied to a value, and we check the range contract when the function actually returns a value.
(define (immediate pred?) (lambda (val) (if (pred? val) val (blame val))))
In contrast, a function contract takes two contracts as
arguments—
(define (guard ctc val) (ctc val))
(define a1 (guard (function (immediate number?) (immediate number?)) add1))
(define a1 (lambda (x) (num?-con (add1 (num?-con x)))))
(define (function dom rng) (lambda (val) (if (procedure? val) (lambda (x) (rng (val (dom x)))) (blame val))))
(define num?-con (immediate number?)) = (define num?-con (lambda (val) (if (number? val) val (blame val))))
(define a1 ((function num?-con num?-con) add1))
(define a1 ((lambda (val) (if (procedure? val) (lambda (x) (num?-con (val (num?-con x)))) (blame val))) add1))
(define a1 (if (procedure? add1) (lambda (x) (num?-con (add1 (num?-con x)))) (blame add1)))
(define a1 (lambda (x) (num?-con (add1 (num?-con x)))))
How many ways are there to violate the above contract for add1?
the value wrapped might not be a function;
the wrapped value might be a function that is applied to a non-numeric value; or,
the wrapped value might be a function that consumes numbers but produces values of non-numeric type.
Write examples that perform each of these three violations, and observe the behavior of the contract system. Can you improve the error messages to better distinguish these cases?
(define d/dx (guard (function (function (immediate number?) (immediate number?)) (function (immediate number?) (immediate number?))) (lambda (f) (lambda (x) (/ (- (f (+ x 0.001)) (f x)) 0.001)))))
There are seven ways to violate this contract, corresponding to each of the seven contract constructors. Violate each of them by passing arguments or modifying code, as needed. Can you improve error reporting to correctly identify each kind of violation?
Notice that the nested function contract defers the checking of the immediate contracts for two applications, rather than one. This is what we should expect, because immediate contracts only report problems with actual values, so they cannot report anything until applied to actual values. However, this does mean that the notion of “violation”’ is subtle: the function value passed to d/dx may in fact truly be in violation of the contract, but this violation will not be observed until numeric values are passed or returned.
16.4 Syntactic Convenience
The developer may forget to wrap some uses.
The contract is checked once per use, which is wasteful when there is more than one use.
The program comingles contract checking with its functional behavior, reducing readability.
(define/contract (real-sqrt (x :: (immediate positive?))) (sqrt x))
(define (real-sqrt new-x) (let ([x (guard (immediate positive?) new-x)]) (sqrt x)))
(define-syntax (define/contract stx) (syntax-case stx (::) [(_ (f (id :: c) ...) b) (with-syntax ([(new-id ...) (generate-temporaries #'(id ...))]) #'(define f (lambda (new-id ...) (let ([id (guard c new-id)] ...) b))))]))
16.5 Extending to Compound Data Structures
As we have already discussed, it appears easy to extend contracts to structured datatypes such as lists, vectors, and user-defined recursive datatypes. This only requires that the appropriate set of run-time observations be available. This will usually be the case, up to the resolution of types in the language. For instance, as we have discussed [REF], a language with datatypes does not require type predicates but will still offer predicates to distinguish the variants; this is case where type-level “contract” checking is best (and perhaps must) be left to the static type system, while the contacts assert more refined structural properties.
However, this strategy can run into significant performance problems.
For instance, suppose we built a balanced binary search-tree to
perform asymptotic logarithmic time (in the size of the tree)
insertions and lookups. Now say we have wrapped this tree in a
suitable contract. Sadly, the mere act of checking the contract
visits the entire tree, thereby taking linear time! Ideally,
therefore, we would prefer a strategy whereby the contract was already
checked—
Worse, both balancing and search-tree ordering are recursive
properties. In principle, therefore, they attach to every sub-tree,
and so should be applied on every recursive call. During insertion,
which is a recursive procedure, the contract would be checked on every
visited sub-tree. In a tree of size \(t\), the contract predicate
applies to a sub-tree of \(t \over 2\) elements, then to a
sub-sub-tree of \(t \over 4\) elements, and so on, resulting—
In both cases, there is ready mitigation available in many cases. Each value needs to be associated (either intrinsically, or by storage in a hash table) with the set of contracts it has already passed. Then, when a contract is ready to apply, it first checks whether the value has already been checked and, if it has, does not check again. This is essentially a form of memoization of contract checking and can thus reduce the algorithmic complexity of checking. Again, like memoization, this works best when the values are immutable. If the values can mutate and the contracts perform arbitrary computations, it may not be sound to perform this optimization.
There is a subtler way in which we might examine the issue of data structures. As an example, consider the contract we wrote earlier to check that all values in a numeric list are even. Suppose we have wrapped a list in this contract, but are interested only in the first element of the list. Naturally, we are paying the cost of checking all the values in the list, which may take a very long time. More importantly, however, a user might argue that reporting a violation about the second element of the list is itself a violation of our expectation about contract-checking, since we did not actually use that element.
This suggests deferring checking even for some values that could be checked immediately. For instance, the entire list could be turned into a wrapped value containing a deferred check, and each value is checked only when it is visited. This strategy might be attractive, but it is not trivial to code, and especially runs into problems in the presence of aliasing: if two different identifiers are referring to the same list, one with a contract guard and the other without, we have to ensure both of them function as expected (which usually means we cannot store any mutable state in the list itself).
16.6 More on Contracts and Observations
A general problem for any contract implementation—
In general, one observation is essentially impossible to “fix”: eq?. Normally, we have the property that every value is eq? to itself, even for functions. However, the wrapped value of a function is a new procedure that not only isn’t eq? to itself but probably shouldn’t be, because its behavior truly is different (though only on contract violations, and only after enough values have been supplied to observe the violation). However, this means that a program cannot surreptitiously guard itself, because the act of guarding can be observed. As a result, a malicious module can sometimes detect whether it is being passed guarded values, behaving normally when it is and abnormally only when it is not!
16.7 Contracts and Mutation
We should rightly be concerned about the interaction between contracts and mutation, and even more so when we have contracts that are either inherently deferred or have been implemented in a deferred fashion. There are two things to be concerned about. One is storing a contracted value in mutable state. The other is writing a contract for mutable state.
When we store a contracted value, the strategy of wrapping ensures that contract checking works gracefully. At each stage, a contract checks as much as it can with the value at hand, and creates a wrapped value embodying the residual check. Thus, even if this wrapped value is stored in mutable state and retrieved for use later, it still contains these checks, and they will be performed when the value is eventually used.
The other issue is writing contracts for mutable data, such as boxes
and vectors. In this case we probably have to create a wrapper for
the entire datatype that records the intended contract. Then, when a
value inside the datatype is replaced with a new one, the operation
that performs the update—
16.8 Combining Contracts
Now that we’ve discussed combinators for all the basic datatypes, it’s natural to discuss combining contracts. Just as we saw unions [REF] and intersections [REF] for types, we should be considering unions and intersections (respectively, “or”s and “and”s), ; for that matter, we might also consider negation. However, contracts are only superficially like types, so we have to consider these questions in their own light for contracts rather than try to map the meanings we have learned from types to the sphere of contracts.
As always, the immediate case is straightforward. Union contracts
combine with disjunction—
Contract combination is much harder in the deferred, higher-order
case. For instance, consider the negation of a function contract from
numbers to numbers. What exactly does it mean to negate it? Does it
mean the function should not accept numbers? Or that if it
does, it should not produce them? Or both? And in particular, how do
we enforce such a contract? How, for instance, do we check that a
function does not accept numbers—
Intersection contracts require values to pass all the sub-contracts. This means re-wrapping the higher-order value in something that checks all the domain sub-contracts as well as all the range sub-contracts. Failing to meet even one sub-contract means the value has failed the entire intersction.
Union contracts are more subtle, because failing to meet any one
sub-contract is not grounds for rejection. Rather, it simply means
that that one sub-contract is no longer a candidate contract
representing the wrapped value; the other sub-contracts might still be
candidates, and only when no others are left must be reject the value.
This means the implementation of union contracts must maintain memory
of which sub-contracts have and have not yet passed—
The implemented versions of contract constructors and combinators in
Racket place restrictions on the acceptable forms of sub-contracts.
These enable implementations that are both efficient and yield useful
error messages. Furthermore, the more extreme situations discussed
above rarely occur in practice—
16.9 Blame
Let’s now return to the issue of reporting contract violations. By this I don’t mean what string to print, but the much more important question of what to report, which as we are about to see is really a semantic consideration.
> (define d/dx-sa (d/dx string-append)) |
> (d/dx-sa 10) |
string-append: contract violation |
expected: string? |
given: 10.001 |
This problem is not a peculiarity of d/dx; in fact, it
routinely occurs in large systems. This is because systems,
especially with graphical, network, and other external interfaces,
make heavy use of callbacks: functions (or methods) that
register interest in some entity and are invoked to signal some status
or value. (Here, d/dx is the moral equivalent of the graphics
layer, and string-append is the callback that has been supplied
to (and stored by) it.) Eventually, the system layer invokes the
callback. If this results in an error, it is the fault of
neither the system layer—
The solution is to extend the contract system to incorporate a notion of blame. The idea is to effectively record the introduction that resulted in a pair of components coming together, so that if a contract violation occurs between them, we can ascribe the failure to the expression that did the introduction. Observe that this is only really interesting in the context of functions, but for consistency we will extend blame to immediate contracts as well in a natural way.
For a function, notice that there are two possible points of failure:
either it was given the wrong kind of value (the
pre-condition), or it produced the wrong kind of value (the
post-condition). It is important to distinguish these two cases
because in the former case we should blame the environment—
For contracts, we will introduce the terms positive and negative position. For a first-order function, the negative position is the pre-condition and the positive one the post-condition. Therefore, this might appear to be needless extra terminology. As we will soon see, however, these terms have a more general meaning.
We will now generalize the parameters consumed by contracts. Previously, immediate contracts consumed a predicate and function contracts consumed domain and range contracts. This will still be the case. However, what they each return will be a function of two arguments: labels for the positive and negative positions. (These labels can be drawn from any reasonable datatype: abstract syntax nodes, buffer offsets, or other descriptions. For simplicity, we will use strings.) Thus function contracts will close over the labels of these program positions, to later blame the provider of an invalid function.
(define (guard ctc val pos neg) ((ctc pos neg) val))
(define (blame s) (error 'contract s))
(define a1 (guard (function (immediate number?) (immediate number?)) add1 "add1 body" "add1 input"))
(define bad-a1 (guard (function (immediate number?) (immediate number?)) number->string "bad-add1 body" "bad-add1 input"))
(define (immediate pred?) (lambda (pos neg) (lambda (val) (if (pred? val) val (blame pos)))))
(define (function dom rng) (lambda (pos neg) (lambda (val) (if (procedure? val) (lambda (x) (dom (val (rng x)))) (blame pos)))))
(define (function dom rng) (lambda (pos neg) (let ([dom-c (dom pos neg)] [rng-c (rng pos neg)]) (lambda (val) (if (procedure? val) (lambda (x) (rng-c (val (dom-c x)))) (blame pos))))))
> (a1 "x") |
contract: add1 body |
(a1 "x") = (guard (function (immediate number?) (immediate number?)) add1 "add1 body" "add1 input") = (((function (immediate number?) (immediate number?)) "add1 body" "add1 input") add1) = (let ([dom-c ((immediate number?) "add1 body" "add1 input")] [rng-c ((immediate number?) "add1 body" "add1 input")]) (lambda (x) (rng-c (add1 (dom-c x))))) = (let ([dom-c (lambda (val) (if (number? val) val (blame "add1 body")))] [rng-c (lambda (val) (if (number? val) val (blame "add1 body")))]) (lambda (x) (rng-c (add1 (dom-c x)))))
We will return to this problem in a moment, but observe how in the above code, there are no real traces of the function contract left. All we have are immediate contracts, ready to blame actual values if and when they occur. This is perfectly consistent with what we said earlier [REF] about being able to observe only immediate values. Of course, this is only true for first-order functions; when we get to higher-order functions, this will no longer be true.
What went wrong? Notice that only the contract bound to rng-c ought to be blaming the body of add1. In contrast, the contract bound to dom-c ought to be blaming the input to add1. It’s almost as if, in the domain position of a function contract, the positive and negative labels should be...swapped.
If we consider the contract-guarded d/dx, we see that this is
indeed the case. The key insight is that, when applying a function
taken as a parameter, the “outside” becomes the “inside” and vice
versa. That is, the body of d/dx—
On the range side, there is no need to swap. Consider again
d/dx. The function it returns represents the derivative, so it
should be given a number (representing the point at which to calculate
the derivative) and it should return a number (the derivative at that
point). The negative position of this function is indeed the client
who uses the derivative function—
(define (function dom rng) (lambda (pos neg) (let ([dom-c (dom neg pos)] [rng-c (rng pos neg)]) (lambda (val) (if (procedure? val) (lambda (x) (rng-c (val (dom-c x)))) (blame pos))))))
Apply this to our earlier example and confirm that we get the expected blame. Also expand the code manually to see why this happens.
((d/dx (guard (function (immediate number?) (immediate string?)) number->string "n->s body" "n->s input")) 10)
Hand-evaluate d/dx, apply it to all the relevant violation examples, and confirm that the resulting blame is accurate. What happens if you supply d/dx with string->number with a function contract indicating it maps strings to numbers? What if you supply the same function with no contract at all?