1.1 The Halting Problem: does the execution of a function stop? 1.2 Approaches to validating laws2 Laws for Sets
2.1 Laws for empty_set 2.2 Laws for list->set 2.3 Laws for set->list 2.4 Laws for member_set? 2.5 Laws for included_in? 2.6 Laws for equal_set? 2.7 Laws for adjoin 2.8 Laws for intersect 2.9 Laws for sets as an abstract data-type3 Verifying Laws with Synthesised Data
3.1 Generating not-too-random numbers with the non-random function 3.2 Generating not-too-random lists of numbers with list_of_random 3.3 Testing an implementation of sets with test_laws_sets4 Invariants of Representations.
If we are creating a datatype it is helpful to characterise it abstractly in terms of laws which say how the operations on it are to behave without specifying how this behaviour is to be achieved. We can regard such laws as logical statements of the required behaviour, and can encode them in Scheme as functions whose body must not evaluate to #f.
So, the role of laws is to allow us to express an abstract characterisation of the datatype from the outsider's point of view, thus assuring the user of the data-type that objects of that type will, at least in some aspects, behave correctly.
Given that, in Scheme, we can represent a law using boolean functions to encode logical statements, it will be convenient to have functions for those logical connectives (implies and if and only if) which have no standard binding in Scheme.
Implication is easily defined:
(define (-> b1 b2)
(or (not b1) b2)
)
while logical equivalence is adequately coded using the equal? function:
(define <-> equal?)
It is a convenience to express the fact that an expression is a law, that is to say that there is a requirement that it never evaluates to #f, by using a [non-standard] special form. If expr is a Scheme expression, then the special form:
(:- expr )
evaluates expr and halts the computation with an error condition if the if expr evaluates to #f. The expression expr is printed out as part of the error message. Thus if we were to evaluate:
(:- (= 3 4))
the computation would halt, printing out the message:
Error: Failed assertion check (:- (= 3 4))
Thus we could express the fact that the built;-in integer data-type in Scheme obeys the commutative law of addition by saying that, if we define the function:
(define law_+
(lambda (x y)
(:-
(= (+ x y) (+ y x)))))
then the function law_+ will never halt and report an error if it is given integer arguments.
Consider now the following test program which systematically works through all pairs of integers, calling law_+ for each pair:
(define (test_law_+)
(help1_+ 0))
(define (help1_+ n)
(help2_+ n 0 n)
(help1_+ (+ n 1)))
(define (help2_+ n m m_max)
(if (<= m m_max)
(begin
(law_+ n m)
(law_+ (- n) m)
(law_+ n (- m))
(law_+ (- n) (- m))
(help2_+ n (+ m 1) m_max)
)
)
)
We can see that the commutative law for the addition of Scheme integers holds if and only if the function test_law_+ does not halt during execution.
This brings us to a famous problem of computer science. Can we write a function halts? which takes another function as argument and decides if it halts? That is to say, for any function f, (halts? f) will always itself terminate, yielding #t if f terminates and #f if it does not. We can imagine the halts? function examining the definition of f, which could be made available if we were using an interpreter.
The answer is well known - if we are writing in a language powerful enough to express an arbitrary computation, no, we cannot write such a halts? function.
It is easy enough to convince ourselves that we can't. For suppose we did have a halts? function. Consider the Scheme function tricky:
(define (tricky x) (if (halts? tricky) (tricky x) #t) )
If we ask, "what should the value of (halts tricky) be?", we obtain a paradox. If it is #t it should be #f, and if it #f it should be #t.
We saw above that we can express the problem of deciding if the commutative law holds for Scheme integers is equivalent to deciding if a test function halts. This would indicate that we would not expect to be able in general to decide if the laws applying to a data-type with an infinite set of members are valid. So, if we let ourselves be unrestricted in what we say in our laws, we cannot expect them to be automatically verified in a logically watertight way
However, there are a number of ways in which we could attach laws to datatypes and still get a useful measure of verification.
Returning now to our computational representation of sets, we can see that the laws in this case are the relevant definitions of set-theory, translated into computational terms. If our representations of sets satisfy the definitions, they necessarily satisfy the theorems of set theory. So, for example if a representation satisfies the definition of intersection, (an element x is a member of the intersection of two sets S1 and S2 if and only if it is a member of both S1 and of S2), and other definitions (e.g. of equality of sets) then it will satisfy the theorem that the intersection of S1 and S2 is equal (under set-equality) to the intersection of S2 and S1.
So let's begin with the laws for the empty_set. This is easy - nothing is a member of the empty set.
(define laws_empty_set
(lambda (x) (not (member_set? x empty_set))))
The laws for list->set are not part of our usual mathematics, but it is clear what we want - any element of the list is a member of the set, and conversely.
We will need the definition of member? from Lecture 5:
(define (member? x list)
(if (null? list) #f
(if (equal? x (car list)) #t
(member? x (cdr list)))))
(define laws_list->set
(lambda (x l)
(:-
(lambda (x)
(<-> (member? x l)
(member_set? x (list->set l))
)))))
Likewise, the laws for the set->list function say much the same thing.
(define laws_set->list
(lambda (x s)
(:-
(<-> (member_set? x s) (member? x (set->list s))))))
What are the laws of the member_set? function? Well, these are mostly subsumed in the laws for the other functions. However, we should require
(not (member_set? x x))
always to be true. Since we are confining ourselves to sets of numbers, there is no problem with this axiom - it is guaranteed by our type-discipline.
For the included_in? function we have the law that one set s1 is included in a set s2 if any x which is a member of s1 is a member of s2. [Note for logic buffs: we only have a one-way implication in this law because of the fact that if we wrote out the logical statement with quantifiers, x is bound by a universal quantifier internal to the definition of included_in?. Only the forward implication is correctly translated as below.]
(define laws_included_in?
(lambda (x s1 s2)
(:-
(->
(included_in? s1 s2)
(-> (member_set? x s1) (member_set? x s2))) )))
The laws for equal_set? are similar to our generic function defining equality.
(define laws_equal_set?
(lambda (s1 s2)
(:-
(<->
(equal_set? s1 s2)
(and
(included_in? s1 s2)
(included_in? s2 s1)
)
)
)))
The laws for adjoin state that when we adjoin an element x to a set s, x is a member of (adjoin x s), and that any member y of s is also a member of (adjoin x s).
(define laws_adjoin
(lambda (x y s)
(:-
(and
(member_set? x (adjoin x s))
(-> (member_set? y s) (member_set? y (adjoin x s)))
)
)
)
)
Finally, the laws for intersect state that an element x is a member of the intersection of two sets s1 and s2 if and only if it is a member of both of them.
(define laws_intersect
(lambda (x s1 s2)
(:-
(<->
(and (member_set? x s1) (member_set? x s2))
(member_set? x (intersect s1 s2))
)
)
)
)
(define laws_sets
(lambda (x y l s1 s2)
(and
(laws_empty_set s1)
(laws_list->set x l)
(laws_set->list x s1)
(laws_included_in? x s1 s2)
(laws_equal_set? s1 s2)
(laws_adjoin x y s1)
(laws_intersect x s1 s2)
)
)
)
Let us consider how we might generate data that would help convince us that laws pertaining to a particular data-type were likely to be valid. Immediately, we can conceive of creating random integers and lists of integers as our test data. It is quite easy (though not so easy as some people have supposed) to write programs to produce a pseudo random sequence of numbers by performing arithmetic. There is a (non-standard) function random built; into UMASS Scheme which provides such a sequence. The call (random n) produces a pseudo-random number between 0 and n-1.
However random test data may not test our programs very well! Consider the merge algorithm. The action we take at each step depends on whether the first member of one argument-list is less than, equal to, or greater than the first member of the other argument-list. If we chose two sorted lists with entries chosen randomly between 1 and 1000, we might well not encounter the "equality" case. For example, we might have
(45 248 687)and
(134 573 724 898)
Bearing this in mind, below is some code for generating some not-too-random test data.
The first thing to do is to generate a sequence of numbers, which are likely to have repeats. The function call (non-random n) returns a number between 1 and n with a one-in-three chance of repeating the previous number.
(define random_last 0)
(define (non-random n)
(cond
((= (random 3) 0) random_last)
(else (set! random_last (random n)) random_last))
)
(define (list_of_random n m)
(if (= n 0)
'()
(cons (random m) (list_of_random (- n 1) m))
)
)
(define (random_list n)
(list_of_random (random n) (+ 1 (random n)))
)
(define random_last_list '(5 3))
(define (non-random_list n)
(cond
((= (random 3) 0) random_last_list)
(else (set! random_last_list (random_list n)) random_last_list))
)
Once we have an implementation of sets, we can verify it by evaluating the expression (test_laws_sets n) where n is an integer. With present experience, if n is given the value 100, this is quite adequate to test the implementations of sets given in these lectures.
(define test_laws_sets_once
(lambda ()
(let* (
(x (random 10))
(y (non-random 10))
(l (random_list 10))
(s1 (list->set l))
(s2 (list->set (non-random_list 10)))
)
(writeln "testing sets: x = " x ", y = " y ", l = " l)
(writeln "s1 = " s1 ", s2 = " s2)
(laws_sets x y l s1 s2)
)
)
)
(define (test_laws_sets n)
(if (> n 0)
(begin (test_laws_sets_once) (test_laws_sets (- n 1)))))
When we are implementing a representation of a datatype which has been defined abstractly, there will be facts about this representations which are not true of the data-type itself. Thus our specification of sets says nothing about how sets are to be stored in the machine, but our three separate implementations are characterised by three different ways of storing the data - as unordered lists without duplicates, as ordered lists and as trees. We can characterise these ways of representing sets by invariants. When testing the implementation of a data-type we could put "wrappers" round the functions of the implementation which enforced the invariants.
So, laws should hold for all implementations of an abstract data type such as sets. By contrast, invariants are what distinguish between them.
The invariant for the unordered-list representation is simply that the representation contains no duplicates.
(define invariant_sets_unordered
(lambda (s) (:- (no_dups s))))
(define (no_dups s)
(if (null? s) #t
(and
(not (member? (car s) (cdr s)))
(no_dups (cdr s)))) )
The invariant for the ordered list representation states that the list is ordered. If we use the < function, then this means that there are no duplicates.
(define invariant_sets_ordered
(lambda (s) (:- (ordered? s))))
(define (ordered? l)
(cond
((null? l) #t)
((null? (cdr l)) #t)
(else (and (< (car l) (cadr l)) (ordered? (cdr l)))))
)
[NOTE This section is optional, and is included for the benefit of those who are familiar with the C language. It is not compiled as part of the lecture.]
We have seen that the problem with writing very general laws to characterise the behaviour of a datatype is that there is no guaranteed and watertight way of verifying them mechanically. One approach to attacking this problem is to use a kind of weak logic which will allow a compiler to "believe" that certain statements about a program are true. If this logic is sound then the compiler's belief will be justified.
Static typing, such as is found in Pascal or C, gives us just such weak logical system which the computer uses to establish that certain laws hold. In the case of C, the ".h" files are in effect the laws of a datatype, which the corresponding ".c" files must obey. It is to be noted that, in both cases, this logic is not only weak, but is unsound! For example, in C, a programmer can "cast" the type of certain expressions violating type security. A less-obvious unsoundness in Pascal arises from the existence of variant records whose tag field does not necessarily reflect the actual type of the data in the record.
What can be said in these static type systems is in effect laws and invariants which could be expressed in Scheme using the type-recognising functions such as boolean?. This provides a logic in which we can state what data-types the arguments of, and results of, functions belong to. So, we can express the fact that the intersection of sets is a set, but not the fact that the intersection of s1 and s2 is the same as the intersection of s2 and s1.
Below is some C code intended to illustrate the use of static typing. Note that we have restricted our lists to being lists of integers.
/* sets.c Contains code */ /* Invariants - characterise a particular representation */ typedef int* Set; /* represent sets as arrays */ #include "sets.h" int car(List l) {return l->car;} List cdr(List l) {return l->cdr;} List cons(int x, int y) { List l malloc(sizeof int * 2); l -> car = x; l -> cdr = y; return l; } Boolean member_set(int x, Set s) { return null x=car(s)? True : } /* sets.h Contains laws */ /* Laws - state facts which are true of all representations. */ typedef int* List; /* represent lists as arrays */ typedef int Boolean; Set empty_set; /* The empty set is a set */ Set list_to_set(List l); /* convert a list to a set */ List list_to_set(Set s); Set intersect(Set s1, Set s2); /* intersection of sets is a set*/ Boolean member_set(int i, Set s); /* is integer i a member of set S*/ Boolean included_in(Set s1, Set s2); /* is set s1 included in set s2? */ Boolean equal_set(Set s1, Set s2); /* are two sets equal? */ Set intersect(Set s1, Set s2); /* intersection of sets is a set */