topical media & game development
object-oriented programming
Objects as behavioral types
subsections:
A syntax-directed correctness calculus as presented in
section \ref{10:verification}
provides, in principle, excellent support for a problem-oriented
approach to program development,
provided that the requirements a program has to meet can be made explicit in a
mathematical, logical framework.
When specifying requirements, we are primarily interested
in the abstract properties of a program, as may be expressed in some
mathematical domain.
However, when actually implementing the program
(and verifying its correctness) we mostly need to take recourse
to details we do not wish to bother with when reasoning
on an abstract level.
In this section we will discuss how we may verify
that an abstract type is correctly implemented by
a behavioral (implementation) subtype, following
[Am90].
Also, we will define precise guidelines for determining
whether two (behavioral) types satisfy
the (behavioral) subtype relation, following
[Liskov93].
Abstraction and representation
In [Am90] a proposal is sketched to define the functionality
of objects by means of
behavioral types.
Behavioral types characterize the
behavioral properties of objects in terms
of (possible) modifications of an abstract state.
So as to be able to ignore the details of an
implementation when reasoning about the properties
of a particular program, we may employ a
representation abstraction function which maps
the concrete data structures and operations to their
counterparts in the abstract domain.
Abstract data types -- representation function
Representation function -- abstraction
-
slide: Abstraction and representation
The diagram in slide [10-ADT] pictures the reasoning involved in
proving that a particular implementation is correct
with respect to a specification in some abstract mathematical
domain.
Assume that we have, in the concrete domain, an action
a that corresponds with a state transformation function
.
Now assume that we have a similar operation in the
abstract domain, that we will write as ,
with a corresponding state transformation function .
To prove that the concrete operation a correctly
implements the abstract operation , we must prove
that the concrete state modification resulting from
a corresponds with the modification that occurs in the abstract domain.
Technically speaking, we must prove that the diagram above commutes,
that is, that whenever
we have that .
To prove that a particular implementation
a respects the abstract operation ,
for which we assume that it has abstract pre- and
post-conditions and ,
we must find a representation invariant I
and (concrete) pre- and post-conditions P
and Q for which we can prove that
and that .
Furthermore, the representation invariant I
must hold before and after the concrete
operation a.
The proof strategy outlined above is of particular relevance for
object-oriented program development, since the behavior of objects
may, as we have already seen, be adequately captured by contracts.
As an additional advantage, however, the method outlined enables
us to specify the behavior of an object in a more abstract way than
allowed by contracts as supported by Eiffel.
Realization
As an example, consider the
specification of a generic stack as given
in slide [10-specification].
The specification of the stack is based on the (mathematically)
well-known notion of sequences.
We distinguish between empty sequences, that we write as ,
and non-empty (finite) sequences, that we write as .
Further, we assume to have a concatenation operator
for which we define and
.
A sequence is employed to represent the state of the stack.
Sequences -- abstract domain
- empty sequence --
- concatenation --
Specification
type stack T {
;
axioms:
};
slide: The specification of a stack
The operations push and pop may conveniently
be defined with reference to the sequence
representing the (abstract) state of the stack.
We use s and to represent the state respectively before
and after the operation.
The operations themselves are completely specified by their
respective pre- and post-conditions.
Pushing an element e results in concatenating the one-element
sequence to the stacks state.
For the operation pop we require that the state of the stack must be
non-empty.
The post-condition specifies that the resulting state is a prefix
of the original state, that is the original state with the last element
(which is returned as a result) removed.
To prove that a particular implementation of the stack
is conformant with the type definition given above
we must prove that
for both methods push and pop.
These proofs involve both an abstraction function
and a representation invariant I, relating the abstract state of
the stack to the concrete state of the implementation.
Now consider an implementation of the generic stack in C++,
as given in slide [10-realization].
template implementation
class as {
int t;
T a[MAX];
public:
as() { t = 0; }
void push(T e) {
require(t0); return a[--t]; }
invariant:
0 <= t && t < MAX;
};
slide: The realization of a stack
To prove that this implementation may be regarded as an element of
the (abstract) type stack, we must find a representation (abstraction)
function to map the concrete implementation to the abstract domain,
and further we must specify a representation invariant
that allows us to relate the abstract properties to the properties
of
the implementation.
For the implementation in slide [10-realization],
the abstraction function simply
creates the sequence of length t,
with elements .
The representation invariant, moreover,
gives an explicit definition of this relation.
See slide [10-absf].
Abstraction function
Representation invariant
-
slide: Abstraction function and representation invariant
In order to verify that our implementation
of the abstract data type stack is correct
(that is as long as the bound MAX is not exceeded),
we must show, given that the representation invariant holds,
that the pre-conditions of the
concrete operations imply the pre-conditions
of the corresponding abstract operations,
and, similarly, that the post-conditions
of the abstract operations imply the post-conditions
of the concrete operations.
First, we show that for the operation push
the post-condition of the
abstract type specification
is indeed stronger than the (implicit) post-condition
of the implementation.
This is expressed by the following formula.
Since we know that , we may derive that
and .
To establish the correctness of the operation pop,
we must prove that the pre-condition specified
for the abstract operation is indeed stronger
than the pre-condition specified for the concrete
operation, as expressed by the formula
It is easy to see that immediately follows
from the requirement that the sequence is non-empty.
Finally, to prove that the operator pop
leaves the stack in a correct state, we must prove that
which is done in a similar manner as for push.
The correspondence relation
Behavioral refinement is not restricted
to the realization of abstract specifications.
We will now look at a definition of behavioral
refinement, following [Liskov93],
that may serve as a guideline for programmers
to define behavioral subtypes, both abstract and concrete,
including subtypes extending the behavioral
repertoire of their supertypes.
In [Liskov93] the relation between behavioral
types is explained by means of a so-called
correspondence mapping, that relates
a subtype to its (abstract) supertype.
Correspondence
- abstraction -- maps values to values
- renaming -- maps subtype to supertype methods
- extension -- explains effects of extra methods
slide: The subtype correspondence mapping
A correspondence mapping is a triple
consisting of an abstraction function
(that projects the values of the subtype
on the value domain of the supertype),
a renaming
(that defines the relation between methods defined
in both types)
and an extension map (that defines the
meaning of additional methods).
See slide [10-correspondence].
Technically, the function must be onto, that is
each value of the supertype domain must be
representable by one or more values of the subtype domain.
Generally, when applying the abstraction function,
we loose information (which is irrelevant from
the perspective of the supertype), for
example the specific ordering of items in a container.
Signature
-
-
Behavior
-
-
-
Extension -- diamond rule
-
slide: Behavioral subtyping constraints
To determine whether a type is a
(behavioral) subtype of a type , one
has to define a correspondence mapping
and check the issues listed in slide [10-subtyping].
First, syntactically, we must check that the
signature of and satisfy the (signature)
subtyping relation defined in the previous chapter.
In other words, for each method m associated
with the object type (which we call ),
and corresponding method
(which is determined by applying the renaming )
we must check the (contravariant) function subtyping
rule, that is
and
,
where ran is the range or result type of m.
Secondly, we must check that the behavioral
properties of respect those of .
In other words, for each method m
occurring in we must check
that
and that
.
Moreover, the invariant characterizing
must respect the invariant
characterizing ,
that is
.
The substitutions
occurring in the behavioral rules are meant
to indicate that each variable of
type must be replaced by a corresponding variable
of type
to which the abstraction function is applied
(in order to obtain a value in the (abstract) domain of ).
And thirdly, in the final place,
it must be shown that the extension map
is well-defined.
The extension map must be defined in such a way
that each method call for an object
x of type ,
which we write as where a represents
the arguments to the call,
is mapped to a program in
which only calls appear to methods shared by
and (modulo renaming)
or external function or method calls.
In addition the diamond rule
must be satisfied,
which means that the states and resulting from
applying respectively and
in state must deliver identical
values for x from the perspective of ,
that is after applying the abstraction function.
In other words, extension maps allow us to understand
the effect of adding new methods and to establish
whether they endanger behavioral compatibility.
In [Liskov93] a distinction is made between
constructors (by which objects are created),
mutators
(that modify the state or value of an object)
and observers
(that leave the state of an object unaffected).
Extension maps are only needed for mutator methods.
Clearly, for observer methods the result
of is empty, and constructors
are taken care of by the abstraction function.
Behavioral subtypes
The behavioral subtyping rules defined above
are applicable to arbitrary (sub)types, and not only
to (sub)types defined by inheritance.
As an example, we will sketch (still
following [Liskov93]) that a stack may be defined as
a behavioral subtype of the type bag.
Recall that a bag is a set allowing duplicates.
See slide [10-ex-subtype].
Behavioral subtypes
- bag -- put, get
- stack -- push, pop, settop
Representation
- bag -- \zline{multiset}
- stack -- \zline{sequence}
Behavior -- put/push
slide: Behavioral subtypes -- example
Let the type bag support the methods
and and assume
that the type stack supports
the methods , and
in addition a method
that replaces the top element of
the stack with its argument.
Now, assume that a bag is represented by
a pair , where
elems is a multiset (which is a set
which may contain multiple elements of the same value)
and bound is an integer indicating the maximal
number of elements that may be in the bag.
Further, we assume that a stack is represented as
a pair ,
where items is a sequence and limit is
the maximal length of the sequence.
For example
is a legal value of bag
and
is a legal value of stack.
The behavioral constraints for respectively the
method put
for bag
and push
for stack
are given as pre- and post-conditions in slide [10-ex-subtype].
To apply put, we require that the size
of the multiset is strictly smaller than the bound
and we ensure that the element i is inserted
when that pre-condition is satisfied.
The multi-set union operator
is employed to add the new element to
the bag.
Similarly, for push we require the length
of the sequence to be smaller than the limit
of the stack
and we then ensure that the element is appended
to the sequence.
As before, we use the primed variables and
to denote the value of respectively
the bag b and the stack s after
applying the operations, respectively put and push.
Proceeding from the characterization of bag
and stack we may define the correspondence
mapping as in slide [10-ex-correspondence].
Correspondence
-
where
- ,
-
slide: Behavioral subtypes -- correspondence
To map the representation of a stack to
the bag representation we use the function
(which is inductively defined to map
the empty sequence to the empty set and to
transform a non-empty sequence into the union
of the one-element multiset of its first element
and the result of applying to the remaining
part).
The stack limit is left unchanged, since
it directly corresponds with the bound of the bag.
The renaming function maps push to put
and pop to get, straightforwardly.
And, the extension map describes the result of
as the application of (subsequently) and .
Proof obligations -- push/put
-
-
slide: Behavioral subtypes -- proof obligations
With respect to the behavioral definitions given for
push and put we have to verify that
and that
.
These conditions, written out fully in slide [10-ex-proof],
are easy to verify.
Generally, a formal proof is not really necessary
to check that two types satisfy the behavioral
subtype relation.
As argued in [Liskov93],
the definition of the appropriate behavioral
constraints and the formulation of
a correspondence mapping is already a significant
step towards verifying that the types have the
desired behavioral properties.
(C) Æliens
04/09/2009
You may not copy or print any of this material without explicit permission of the author or the publisher.
In case of other copyright issues, contact the author.