Methods definitions may include optional pre- and post-conditions. Together with 'assert' and these features allow the earnest programmer to annotate the intention of code. The Sather compiler provides facilities for turning on or off the runtime checking these safety features imply. Classes may also define a routine named 'invariant', which is a post condition that applies to all public methods.
These safety features are associated with the notion of programming contracts. The precondition of a method is the contract that the method requires the caller to fulfill. It is a statement of the condition of the world that the method needs to find, in order to work correctly. The postcondition is a contract that the method guarantees, if its precondition has been met. It is a statement of the state the method will leave the world in, when it is finished executing. These programming contracts are very important in the creation of robust, reusable code.
In addition to providing a level of checking, these safety features are also an invaluable form of documentation. Since preconditions and postconditions must actually execute, they can be trusted to be accurate and up-to-date, unlike method comments which may easily fall out of sync with the code.
A precondition states the assumptions that a method makes. It is the contract that the caller must fullfil in order for the routine to work properly. Preconditions frequently include checks that an argument is non-zero or non-void.
The optional 'pre' construct of method definitions contains a boolean expression which must evaluate to true whenever the method is called; it is a fatal error if it evaluates to false.The expression may refer to self and to the routine's arguments. For iterators, pre and post conditions are checked before and after every invocation of the iterator (not just the first or last time the iterator is called).
class POSITIVE_INTERVAL is readonly attr start, finish:INT; create(start, finish:INT) -- Ensure that the interval is positive on positive numbers pre start > 0 and finish > 0 and finish-start >= 0 is res ::= new; res.start := start; res.finish := finish; return res; end; end; -- class POSITIVE_INTERVAL
Note that it is usually not appropriate to place conditions on the internal state in the precondition. This is an inappropriate conduct, since it may be impossible for the caller to determine whether the conduct can be properly fulfilled.
move_by(i:INT) pre start > 0 is ...
The test on 'start' is actually verifying something about the internal state of the object, and has nothing to do with the caller of the routine. Tests such as the one above are more appropriately placed in assertions.
Post conditions state what a method guarantees to the caller. It is the method's end of the contract. Post conditions are also stated as an optional initial construct in a method.
The optional 'post' construct of method definitions contains a boolean expression which must evaluate to true whenever the method returns; it is a fatal error if it evaluates to false. The expression may refer to self and to the method's arguments.
class VECTOR is ... norm:FLT; -- norm of the vector normalize post norm = 1.0 is ... -- Normalize the vector. The norm of the result must be 1.0
It is frequently useful to refer to the values of the arguments before the call, as well as the result of the method call. A problem arises because the initial argument values are no longer known by the time the method terminates, since they may have been arbitrarily modified. Also, since the post condition is outside the scope of the method body, it cannot easily refer to values which are computed before the method executes. The solution to this problem consists of using result expressions which provide the return value of the method and initial expressions which are evaluated at the time the method is invoked.
initial expressions may only appear in the post expressions of methods.
add(a:INT):INT post initial(a)>result is ...
The argument to the initial expression must be an expression with a return value and must not itself contain initial expressions. When a routine is called or an iterator resumes, it evaluates each initial expression from left to right. When the postcondition is checked at the end, each initial expression returns its pre-computed value.
Result expressions are essentially a way to refer to the return value of a method in a postcondition (the post condition is outside the scope of the routine and hence cannot access variables in the routine).
sum:INT post result > 5 is ... -- Means that the value return must be > 5
Result expressions may only appear within the postconditions of methods that have return values and may not appear within initial expressions. A result expression returns the value returned by the routine or yielded by the iterator. The type of a result expression is the return type of the method in which it appears (INT, in the above example).
The above routine maintains an (always positive) running sum in 'sum'. Only positive numbers are added to the sum, and the result must always be bigger than the argument.
class CALCULATOR is readonly attr sum:INT; -- Always kept positive add_positive(x:INT):INT pre x > 0 post result >= initial(x) is return sum + x; end; end;
Assertions are not part of the interface to a routine. Rather, they are an internal consistency check within a piece of code, to ensure that the computation is proceeding as expected.
assert statements specify a boolean expression that must evaluate to true; otherwise it is a fatal error.
private attr arr:ARRAY{INT}; ... sum_of_elts is sum:INT := 0; loop e ::= arr.elt!; assert e > 0; sum := sum + e; end; return sum; end;
In the above piece of code, we expect the class to only be storing postive values in the array 'arr' . To double check this, when adding the elements together, we check whether each element is positive.
A class invariant is a condition that should never be violated in any object, after it has been created. Invariants have not proven to be as widely used as pre- and post- conditions, which are quite ubiquitous in Sather code.
If a routine with the signature 'invariant:BOOL', appears in a class, it defines a class invariant. It is a fatal error for it to evaluate to false after any public method of the class returns, yields, or quits.
Consider a class with a list (we use the library class A_LIST) whose size must always be at least 1. Such a situtation could arise if the array usually contains the same sort of elements and we want to use the first element of the array as a prototypical element)
class PROTO_LIST is private attr l:A_LIST{FOO}; create(first_elt:FOO):SAME is res ::= new; res.l := #; res.l.append(first_elt); return res; end; invariant:BOOL is return l.size > 0 end; delete_last:FOO is return l.delete_elt(l.size-1); end; end;
If the 'delete_last' operation is called on the last element, then the assertion will be violated and an error will result.
proto:FOO := #; -- Some FOO object a:PROTO_LIST := #(FOO); last:FOO := a.delete_last; -- At runtime, an invariant violation will occur -- for trying to remove the last element.
The invariant is checked at the end of every public method. However, the invariant is not checked after a private routine. If we have the additional routines
delete_and_add is res ::= internal_delete_last; l.append(res); return res; end; private internal_delete_last:FOO is return l.delete_elt(l.size-1); end;
Now we can call 'delete_and_add'
proto:FOO := #; a:PROTO_LIST := #(FOO); last:FOO := a.delete_and_add; -- does not violate the class invariant
The private call to 'internal_delete_last' does violate the invariant, but it is not checked, since it is a private routine.