[Next] [Previous] [Up] [Top] [Contents] [Index]

2 Dynamically-Typed Core

2.4 Predicate Objects

To enable inheritance and classes to be used to capture run-time varying object behavior, Cecil support predicate objects [Chambers 93b]. Predicate objects are like normal objects except that they have an associated predicate expression. The semantics of a predicate object is that if an object inherits from the parents of the predicate object and also the predicate expression is true when evaluated on the child object, then the child is considered to also inherit from the predicate object in addition to its explicitly-declared parents. Since methods can be associated with predicate objects, and since predicate expressions can test the value or state of a candidate object, predicate objects allow a form of state-based dynamic classification of objects, enabling better factoring of code. Also, predicate objects and multi-methods allow a pattern-matching style to be used to implement cooperating methods.

For example, predicate objects could be used to implement a bounded buffer abstraction:

object buffer isa collection;
	field elements(b@buffer); -- a queue of elements
	field max_size(b@buffer); -- an integer
	method length(b@buffer) { b.elements.length }
	method is_empty(b@buffer) { b.length = 0 }
	method is_full(b@buffer)  { b.length = b.max_size }

predicate empty_buffer isa buffer when buffer.is_empty;
	method get(b@empty_buffer) { ... } -- raise error or block caller

predicate non_empty_buffer isa buffer when not(buffer.is_empty);
	method get(b@non_empty_buffer) { remove_from_front(b.elements) }

predicate full_buffer isa buffer when buffer.is_full;
	method put(b@full_buffer, x) { ... } -- raise error or block caller

predicate non_full_buffer isa buffer when not(buffer.is_full);
	method put(b@non_full_buffer, x) { add_to_back(b.elements, x); }

predicate partially_full_buffer isa non_empty_buffer, non_full_buffer;

The following diagram illustrates the inheritance hierarchy created by this example (the explicit inheritance link from the buffer object to buffer is omitted):

Predicate objects increase expressiveness for this example in two ways. First, important states of bounded buffers, e.g., empty and full states, are explicitly identified in the program and named. Besides documenting the important conditions of a bounded buffer, the predicate objects remind the programmer of the special situations that code must handle. This can be particularly useful during maintenance phases as code is later extended with new functionality. Second, attaching methods directly to states supports better factoring of code and eliminates if and case statements, much as does distributing methods among classes in a traditional object-oriented language. In the absence of predicate objects, a method whose behavior depended on the state of an argument object would include an if or case statement to identify and branch to the appropriate case; predicate objects eliminate the clutter of these tests and clearly separate the code for each case. In a more complete example, several methods might be associated with each special state of the buffer. By factoring the code, separating out all the code associated with a particular state or behavior mode, we hope to improve the readability and maintainability of the code.

The syntax for a predicate object declaration is as follows:

predicate_decl 	::=	"predicate" name {relation} [field_inits] ["when" expr] ";"