topical media & game development
object-oriented programming
Self-reference
Recursive types are compound types in which the type
itself occurs as the type of one of its components.
Self-reference in objects clearly involves
recursive types since the expression
self
denotes the object itself, and hence has the type
of the object.
In
, our extension of
taken from
[CoHC90],
recursive types are written as
,
where
is the recursion abstractor
and
a type variable.
The dependence of
on
is made explicit
by writing
.
We will use the type expressions
to
type object specifications of the form
as indicated by the type assignment rule below.
Object specifications may be regarded as class descriptions
in C++ or Eiffel.
Self-reference -- recursive types
-
-
Type assignment
- \ffrac{ \hspace{1cm} }{
}
Refinement
- \ffrac{ }{
}
slide: A calculus for recursive types
The subtype refinement rule for recursive types states
that if we can prove that
assuming that .
An object specification is a function
with the type of the actual object as its domain and (naturally)
also as its range.
For convenience we will write an object specification
as , where F denotes the object record,
and the type of an object specification as ,
where denotes the (abstract) type of the record F.
To obtain from an object specification
the object that it specifies,
we need to find some type that types the record
specification F as being of type precisely
when we assign the expression self in F
the type .
Technically, this means that the object of type
is a fixed point of the object specification
which is of type .
We write this as ,
which says that the object corresponding to the object
specification is of type .
See slide [9-recursion].
Object semantics -- fixed point $ %s = F[%s] $
-
Unrolling -- unraveling a type
-
Example
, \zline{(contravariance)}
slide: Recursive types -- examples
Finding the fixed point of a specification involves
technically a procedure known as unrolling,
which allows us to rewrite the type
as .
Notice that unrolling is valid, precisely because of
the fixed point property.
Namely, the object type is equal to ,
due to the type assignment rule, and we have that
.
See slide [9-ex-recursive].
Unrolling allows us to reason on the level of types
and to determine the subtyping relation between recursive
subtypes.
Consider, for example, the type declarations T and
above.
Based on the refinement rules for object records, functions
and recursive types, we may establish that ,
but .
To see that , it suffices to substitute T
for in F, where .
Since
we immediately see that only extends T with the field ,
hence .
A similar line of reasoning is involved to determine that
, only we need to unroll as well.
We must then establish that , which follows
from an application of the refinement rule.
To show that , let
and .
Then, by unrolling,
.
Now, suppose that , then
and consequently must refine .
But from the latter requirement it follows that
and that (by the contravariance rule for function
subtyping).
However, this leads to a contradiction since T is clearly
not equal to because contains a field
that does not occur in T.
Although analyses of this kind are to some extent
satisfactory in themselves, the reader may wonder
where this all leads to.
In the following we will apply these techniques
to show the necessity of dynamic binding
and to illustrate that inheritance may easily violate
the subtyping requirements.
Inheritance
In section [flavors] we have characterized inheritance
as an incremental modification mechanism,
which involves a dynamic interpretation of the expression self.
In the recursive type calculus we may characterize this
more precisely, by regarding a derived object specification
C as the result of applying the modifier M
to the object specification P.
We employ
the notation
to characterize derivation by inheritance,
and we assume the modifier M corresponding with
to extend the record
associated with P in the usual sense.
See slide [9-self-inheritance].
Inheritance -- C = P + M
-
- \ifsli{\n}{}
Semantics -- $\Y(C) = \Y(%l( self ).M( self )(P( self )))$
-
-
slide: Inheritance semantics -- self-reference
The meaning of an object specification C is again a fixed
point , that is
.
Now when we assume that the object specification is of type
(and hence of type ),
and that C is of type (and hence of type
), then we must require that to obtain
a properly typed derivation.
We write whenever .
A first question that arises when we characterize inheritance
as incremental modification is how we obtain the meaning
of the composition of two object specifications.
Object inheritance -- dynamic binding
where and
Simple typing --
Delayed --
We have \zline{(more information)}
slide: Object inheritance -- dynamic binding
Let (parent) P and (child) C be defined as above.
Now, if we know that the type of is
then we may simply characterize as being
of type .
However, when we delay the typing of the P
component (by first composing the record specifications
before abstracting from self)
then we may obtain
as the type of .
By employing the refinement rule and unrolling we can
show that .
Hence, delayed typing clearly provides more information
and must be considered as the best choice.
Note, however, that both and hold.
See slide [9-self-dynamic].
A second, important question that emerges
with respect to inheritance is how
self-reference affects the subtyping relation between
object specifications related by inheritance.
Consider the object specifications P and C given
in slide [9-self-contravariance].
In the (derived) specification C, the method eq
is redefined to include an equality test for the b
component.
However, when we determine the object types corresponding
to the specifications P and C
we observe that .
Contravariance
-
,
where
where
However \zline{(subtyping error)}
slide: Object inheritance -- contravariance
The reasoning is as follows.
For and , we have
that
which is (by unrolling) equal to .
Now suppose that , then we have that
is a subtype of
which is true when
and hence (by contravariance) when .
Clearly, this is impossible. Hence .
We have a problem here,
since the fact that means that the type checker
will not be able to accept the derivation of C from P,
although C is clearly dependent on P.
The solution to our problem
lies in making the type dependency involved in deriving
C from P explicit.
Notice, in this respect, that in the example above we
have omitted the type of the abstraction variable
in the definition of eq,
which would have to be written as
(and in a similar way for C ) to do it properly.
Type dependency
The expression self is essentially of a polymorphic nature.
To make the dependency of object specification on self
explicit, we will employ an explicit type variable
similar as in .
Let stand for
as before.
We may regard as a type function, in the sense
that for some type the expression
results in a type.
To determine the type of an object specification
we must find a type that satisfies both
and .
Type dependency -- is polymorphic
- Let \zline{(type function)}
-
-
F-bounded constraint \n
Object instantiation:
for \n
We have because
slide: Bounded type constraints
We may write an object specification as
,
which is typed as .
The constraint that , which is called
an F-bounded constraint, requires
that the subtype substituted for is
a (structural) refinement of the record type .
As before, we have that with ,
which differs from our previous definition only
by making the type dependency in P explicit.
See slide [9-dependency].
Now, when applying this extended notion of object
specification to the characterization of inheritance,
we may relax our requirement that must be a subtype
of into the requirement that
for any , where F is the record specification
of P and G the record specification of C.
Inheritance
with recursive types
Valid, because
However
slide: Inheritance and constraints
For example, when we declare
and as in slide [9-self-constraint],
we have that
for every value for .
However, when we find types and
such that and
we (still) have that .
Conclusion, inheritance allows more than subtyping.
In other words, our type checker may guard
the structural application of inheritance, yet will
not guarantee that the resulting object types
behaviorally satisfy the subtype relation.
Discussion -- Eiffel is not type consistent
We have limited our exploration of the recursive structure
of objects to (polymorphic) object variables.
Self-reference, however, may also occur to class variables.
The interested reader is referred to [CoHC90].
The question that interests us more at this particular
point is what benefits we may have from the techniques employed
here and what lessons we may draw from applying them.
One lesson, which should not come as a surprise,
is that a language may allow us to write programs
that are accepted by the compiler yet are behaviorally incorrect.
However, if we can determine syntactically that the
subtyping relations between classes is violated we may at
least expect a warning from the compiler.
So one benefit, possibly, is that we may improve our compilers
on the basis of the type theory presented in this chapter.
Another potential benefit is that we may better understand the trade-offs
between the particular forms of polymorphism offered by our
language of choice.
The analysis given in [CoHC90] indeed leads to a
rather surprising result.
Contrary to the claims made by its developer,
[CoHC90] demonstrate that Eiffel is not type consistent.
The argument runs as follows.
Suppose we define a class C with a method eq
that takes an argument of a type similar
to the type of the object itself
(which may be written in Eiffel as like Current).
We further assume that the class P is defined
in a similar way, but with an integer field i
and a method eq that tests only on i.
See slide [9-self-eiffel].
Inheritance != subtyping
Eiffel
class C inherit P redefine eq
feature
b : Boolean is true;
eq( other : like Current ) : Boolean is
begin
Result := (other.i = Current.i) and
(other.b = Current.b)
end
end C
slide: Inheritance and subtyping in Eiffel
We may then declare variables v and p of type P.
Now suppose that we have an object c of type C,
then we may assign c to v and invoke the method
eq for v, asking whether p is equal to v, as in
p,v:P, c:C
v:=c;
v.eq(p); // error p has no b
slide: Example
Since v is associated with an instance of C,
but syntactically declared as being of type P,
the compiler accepts the call.
Nevertheless, when p is associated with an instance of
P trouble will arise, since (due to dynamic binding)
the method eq defined for C will be invoked while
p not necessarily has a field b.
When we compare the definition of C in Eiffel
with how we may define C in C++,
then we are immediately confronted with
the restriction that we do not have such a dynamic typing mechanism
as like Current in C++.
Instead, we may use overloading, as shown
in slide [9-self-cc].
class C : public P { C++
int b;
public:
C() { ... }
bool eq(C& other) { return other.i == i && other.b == b; }
bool eq(P& other) { return other.i == i; }
};
slide: Inheritance and subtyping in C++
When we would have omitted the P variant
of eq, the compiler complains about hiding a virtual
function.
However, the same problem arises when we define eq
to be virtual in P, unless we take care to
explicitly cast p into either a C or P reference.
(Overloading is also used in [Liskov93]
to solve a similar problem.)
In the case we choose for a non-virtual definition
of eq, it is determined statically which variant
is chosen and (obviously) no problem occurs.
Considering that determining equality between
two objects is somehow orthogonal to
the functionality of the object proper,
we may perhaps better employ externally defined
overloaded functions to express relations
between objects.
This observation could be an argument to
have overloaded functions apart from
objects, not as a means
to support a hybrid approach but as a means
to characterize relations between objects
in a type consistent (polymorphic) fashion.
(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.