CS 334
|
Other info on GJ:
Works with existing JVM -- essentially translates to original code w/Object and casts.
Authors designed so that existing library classes can be used as though they were polymorphic.
Because of translation, cannot get accurate info using Java's reflection facilities or debugger.
See GJ web page available through course web page.
Designed by Bertrand Meyer in mid-80's
Class-based OOL w/multiple inheritance
Assertions: pre- and post-conditions, loop invariants and variants built into language.
Supports bounded polymorphism.
Reference semantics like Java, garbage collection, etc.
Information hiding: private, public, or could list classes visible to (like C++'s friends)
No interfaces or modules.
In subclasses, can redefine or even rename methods.
Can also change type of instance variables, parameters and return types covariantly. As we know, this causes type-safety problems!
Introduced "anchor" types: Can declare type to be "like" another feature:
x: A; y: like x;
Current is Eiffel's name for self.
Example:
class LINKABLE [G] feature item: G; -- value held right: like Current; -- Right neighbor putRight (other: like Current) is -- Put `other' to right of current cell. do right := other ensure chained: right = other end; end -- class LINKABLE class BILINKABLE [G] inherit LINKABLE [G] redefine putRight end feature -- Access left: like Current; -- Left neighbor putRight (other: like Current) is -- Put `other' to right of current cell. do right := other; if (other /= Void) then other.simplePutLeft (Current) end end; putLeft (other: like Current) is -- Put `other' to left of current cell. do left := other; if (other /= Void) then other.simplePutRight (Current) end ensure chained: left = other end; feature {BILINKABLE} simplePutRight (other: like Current) is -- set `right' to `other' do right := other end; simplePutLeft (other: like Current) is -- set `left' to `other' do left := other end; invariant rightSymmetry: (right /= Void) implies (right.left = Current); leftSymmetry: (left /= Void) implies (left.right = Current) end -- class BILINKABLE
BILINKABLE is subclass of LINKABLE -- Can't do this with Java, C++, etc., because cannot change type of methods in subclasses in those languages, and don't have a "like Current" construct.
Reason is that covariant changes in types of parameters or instance variable types lead to failure of subtyping (though changes to return types don't cause problems)
Can define:
class LINKEDLIST[NODE -> LINKABLE] ...
Can be instantiated with either
LINKABLE (and get singly-linked list) or
BILINKABLE (and get doubly-linked list).
Very expressive:
deferred class Comparing feature lessThan(other: like Current): boolean is deferred end greaterThan(other: like Current): boolean; end
Unfortunately, use of like Current gives rise to implicit covariant change to types of instance variables and method parameter and return types.
Allowable changes which can be made in subclasses:
Add new features (instance vbles or routines).
Instance variables may be given a new type that is a subclass of the original.
In redefining routines, may replace parameter and result types by types that are subclasses of originals. This may be done automatically if type defined in terms of "like Current" or similar.
Notice that "like Current" solves problems with clone and equals from Java's Object class. In fact return type of clone and parameter type of equals will change automatically!
More flexible than Object Pascal, Java or C++ (C++ now allows changing result types) but leads to problems:
Biggest problem - identification of subclass with subtype when allow great flexibility in changing types in subclass.
When redefine methods in subclass may replace class of arguments and answer by subclasses.
E.g.
Unfortunately, leads to holes in typing system!
Recall A' is subtype of A if element of type A' can be used in context expecting element of type A.
Eiffel allows programmer to use elt of subclass anywhere it expects elt of its superclass.
Unfortunately subtype != subclass when make changes to types of parameters and instance vbles.
Basic problem is that Eiffel allows method with type A -> B to be replaced by method with type A' -> B' in subclass, where A' <: A and B' <: B.
But know that A' -> B' subtype of A -> B iff B' subtype of B and A subtype of A'
Note reversal!
Hence get holes in type system. See homework problem 2, where you are to show not type safe.
Also allowable to export method from superclass, but not from subclass.
Breaks system if send message to object of subclass which is not visible.
E.g., define
hide_n_break(a:A) is do a.meth .... end
and then write hide_n_break(a') where a' : A', and A' is subclass of A which does not export meth.
Earlier versions of Eiffel allowed user to break the type system in these ways.
Eiffel 3.0 attempts to compensate by running a global check of all classes used in a system to make sure that above situation could not occur (class-level check vs system-level check). Involves dataflow analysis of program to determine if certain calls could occur. [Conservative check]
Consequence is that a system could work fine, but addition of new (separately compiled) class could break a previously defined class.
No Eiffel compilers implement system validity check.
In Fall, '95, Bertrand Meyer announced solution to "covariant typing problem" at OOPSLA '95.
No "polymorphic cat-calls": Essentially can't send a method with a covariant change to any object where don't know exact type.
Originally had errors (omitted check to see if instance variable types changes).
Not known if guarantees type safety.
Like system validity check, never implemented.
I suspect both rule out too many useful programs.
Most statically typed object-oriented languages either
Type unsafe like Eiffel
Too rigid with types (like C++ or Object Pascal) or
Insert dynamic checks where unsafe (Java, Beta).
Two parts to solution with flexible type system:
Separate subtype and inheritance hierarchies (originally proposed by researchers in ABEL group at HP Labs).
Refine type systems to add notion of exact types, and generalize subtype to matching relation.
Inheritance hierarchy has only to do with implementation.
Subtype hierarchy has only to do with interface.
Therefore class != type.
Java does this to some extent, but can go further and rule out the use of class names as types.
As pointed out earlier, even if don't necessarily care if subclasses turn out to be subtypes, still need restrictions on redefinitions to avoid breaking other inherited methods.
Ex.:
method1(...) = ... p.method2(..).... method2(...) = .....
If redefine method2 with different type, need new type of method2 to be subtype of original type.
Can set up type-checking rules for determining legal subclasses and subtypes and be guaranteed that can't break the typing system.
If user doesn't see source code of superclass, how can s/he be confident that will get no type errors. Need the kind of guarantees claimed above.
Work here on TOOPLE, TOIL, PolyTOIL and LOOM (theses by Rob van Gent '93, Angie Schuett '94, Leaf Petersen '96, Hilary Browne '97, Joe Vanderwaart '99, and Doug Thunen '02).
Resulted in object-oriented language, LOOM, which is type-safe, is very flexible, and includes support for better form of bounded polymorphism called match-bounded polymorphism.
Used the experience gained in the design of LOOM to build an extension of Java, call LOOJ, that is more flexible than GJ (Jon Burstein '98 and Nate Foster '01)
LOOM: Uses MyType as type of "self" ("this) - similar to "like Current", but type-checking rules (provably) guarantee type-safety.
Replace subtyping by notion of "matching", written <#. BILINKABLE[T] <# LINKABLE[T], but not subtypes!
Matching corresponds to extension, but when use "MyType" for parameter type or instance vble type, extension NOT correspond to subtyping.
If write x:T, then any value in "x" must have exactly type T, while x:#T means value can come from any type matching T.
If wish to use "slippery types" (like subtypes in ordinary OO languages) then must use #T as type.
Only restriction is cannot send "binary" messages to #-types.
E.g., if x:#LINKABLE[T] for some T, then can write
x.next() or x.right(),but can't write
x.putRight(other)
Supports "match-bounded" polymorphism F-bounded extension needed only very rarely.
Jon Burstein '98 and Nate Foster '01 implemented extension of Java based on similar principles.
Both kinds of languages allow expression of all bad examples cited earlier - clone, equals, ColorCircle
F-bounded:
Not preserved under subclass.
Encoding requires extra type parameter in interface
Matching:
Preserved under subclass
Must distinguish exact vs. #-types. Can't send binary message to #-type.
If no occurrences of MyType then #-types give exactly same effect as subtyping.
Which is better seems to depend on taste - more experience necessary.
Pro's (e.g., with Eiffel and Java)
Good use of information hiding. Objects can hide their state.
Good support for reusability. Supports generics like Ada, run-time creation of objects (unlike Ada)
Support for inheritance and subtyping provides for reusability of code.
Con's
Loss of locality.
Type-checking too rigid, unsafe, or requires link time global analysis. Others require run-time checks.
Semantics of inheritance is very complex. Small changes in methods may make major changes in semantics of subclass. It appears you must know definition of methods in superclass in order to predict impact on changes in subclass. Makes provision of libraries more complex.
Weak or non-existent support of modules.
Eiffel also provides support for number of features of modern software engineering - e.g., assertions.
What will be impact of OOL's on programmers and computer science?
Soon will likely be more popular than procedural programming.
Many of the advantages claimed by proponents could be realized in Clu, Modula-2, or Ada (all available decade or more ago).
My advice: Specify carefully meaning of methods, avoid long inheritance chains, and be careful of interactions of methods.
When implement F-bounded polymorphism, Java could be a very successful compromise between flexibility and usefulness.
Implementation of OO languages is actually pretty straightforward if objects are implicit references. Recall that all object generated by a particular class have the same set of instance variables (i.e., same collection of slots) and the same methods. Thus can represent objects in memory as references to records, where all but the first slot hold the instance variables and the first slot holds a pointer to the virtual method table (sometimes called the vtable) for the methods of the class.
The virtual method table is a record with a slot for every method (private, protected, and public) of the class. Each slot holds the address of the code segment of the appropriate method body (or, if necessary, a closure).
In this way, if a message is sent to an object, it will indirect through the virtual method table to find the appropriate code to be executed (passing along the address of the record of instance variables to be used in the method body).
When a subclass is defined, slots for new instance variables are added at the end of the object (so old instance variables are at the same offset as with the superclass), and a new vtable is constructed by starting with a copy of the old vtable, adding new methods to the end, and replacing code pointers of overridden methods.
Note that when a method is called, no search for the appropriate method is necessary. The offset in the vtable for the method can be computed statically. The offsets for both instance variables and methods are calculated for the declared type, but the exact same offsets work for all objects generated from subclasses. Thus, method look up is the same whether the value of the expression is an object of the static type of the expression or a subtype.
Unfortunately, this does not work with interfaces, as classes implementing interfaces need not have methods in the same order as the interfaces. Thus methods must be looked up in the vtable when interfaces are used.
Notice also that if all objects are represented as implicit references, then there are no problems in assigning an object generated from a subclass to a variable declared to be of a type corresponding to a superclass. While the record corresponding to the object of the subclass may be larger, the reference to it is always of the same size! Thus fitting an object from a subclass into a slot designed for the superclass causes no problems in Eiffel or Java.
The same situation in C++ where the object is not held as a reference results in truncation of the object and replacement of the vtable by the vtable of the superclass!
In order to support type casts and reflection, Java (and other OOL's) keep a type descriptor in the vtable to identify the class of an object at run-time.
Implementation of multiple inheritance is much more worse than for single inheritance - resulting in significant complications and greater inefficiency of method look-up.