For instance, the class COMPARABLE is from the Eiffel library (the result shown is the result of extracting the flat-short version of the class).
deferred class interface COMPARABLE feature specification infix "<" (other: like Current): BOOLEAN is deferred; infix "<=" (other: like Current): BOOLEAN is deferred; infix ">" (other: like Current): BOOLEAN is deferred; infix ">=" (other: like Current): BOOLEAN is deferred; end interface -- class COMPARABLE
Now define
class INTORD feature value:INTEGER; infix "<"(other:like Current) is do Result := value < other.value end; ... end -- class INTORD
Can use in
class Sorting[T -> COMPARABLE] feature sort(thearray:ARRAY[T]):ARRAY[T] is local .... do ...... .... if thearray.item(i) < thearray.item(j) .... end;
Subclasses can see all features, whether exported or not.
Eiffel has tools to
Allowable changes which can be made in subclasses:
More flexible than Object Pascal or C++ (but leads to problems, below!)
Big problem with Eiffel - identification of class with type.
Say C' is a subclass (or heir) of C if C' inherits from C.
Thus C' inherits attributes and methods from superclass.
When redefine methods in subclass may replace class of arguments and answer by subclasses.
E.g.
If m(a:A):B in C then can redefine m(a:A'):B' in subclass C' if A' inherits from A and B' inherits from B.
Unfortunately, this can lead to holes in typing system.
Recall A' is a subtype of A if an element of type A' can be used in any context expecting an element of type A.
Eiffel allows programmer to use an element of subclass anywhere it expects an element of its superclass.
Therefore distinction between static and dynamic class!
Unfortunately subtype != subclass.
The following are slightly simplified examples from the Eiffel structure library. They represent singly and doubly-linked nodes.
class LINKABLE [G] feature item: G; right: like Current; -- Right neighbor put_right (other: like Current) is -- Put `other' to the right of current cell. do right := other ensure chained: right = other end; end -- class LINKABLENow define subclass:
class BI_LINKABLE [G] inherit LINKABLE [G] redefine put_right end feature -- Access left: like Current; -- Left neighbor put_right (other: like Current) is -- Put `other' to the right of current cell. do right := other; if (other /= Void) then other.simple_put_left (Current) end end; put_left (other: like Current) is -- Put `other' to the left of current cell. do left := other; if (other /= Void) then other.simple_put_right (Current) end ensure chained: left = other end; simple_put_right (other: like Current) is -- set `right' to `other' do if right /= Void then right.simple_forget_left; end; right := other end; simple_put_left (other: like Current) is -- set `left' to `other' is do if left /= Void then left.simple_forget_right end; left := other end; invariant right_symmetry: (right /= Void) implies (right.left = Current); left_symmetry: (left /= Void) implies (left.right = Current) end -- class BI_LINKABLE
So far so good.
But now suppose have following routine
trouble(p, q : LINKABLE [RATIONAL] ) is do p.put_right(q); .... end
and suppose have s_node : LINKABLE [RATIONAL] and bi_node: BI_LINKABLE [RATIONAL].
What happens if write:
trouble(bi_node,s_node)If BI_LINKABLE [RATIONAL] is subtype of LINKABLE [RATIONAL], then this should work, instead, BANG!!!!! - system crash.
Problem is that
s_node.put_right takes a parameter of type (class) LINKABLE [RATIONAL]
while bi_node.put_right takes a parameter of type (class):
BI_LINKABLE [RATIONAL]and these are not subtypes:
A' -> B' subtype of A -> B iff B' subtype of B and A subtype of A'
note reversal!
With procedure can think of the return type as VOID.
Thus subclass in Eiffel does not always give legal subtype.
Hence get holes in type system.
Can also export method from superclass, but not from subclass. This will also break 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 mandating a global check of all classes used in a system to make sure that above situation could not occur (class-level check and system-level check). One consequence is that a system could work fine,but addition of new (separately compiled) class could break a previously defined class.
Unfortunately no Eiffel compilers implement this system validity check.
In Fall, '95, Bertrand Meyer announced solutions to "covariant typing problem" at OOPSLA '95. Two days later I found a hole in the solution. It's been fixed, but other problems may remain.
Virtually all object-oriented language either provide holes like this or are so rigid they force the programmers to bypass the type system. For example, C++ doesn't allow user to change type of parameters of methods (new versions allow change in type of results of function methods), but has many, many, more holes, e.g., unchecked casts. Java inserts dynamic checks of casts to avoid type holes (but blew suptyping of array types - though add dynamic check).
Most statically typed object-oriented languages are either
Trellis/Owl (by DEC) avoids the problem by only allowing subclasses which are also subtypes, but this is pretty restrictive - rules out above COLORPOINT class.
Claim proper solution is to separate subtype and inheritance hierarchies (originally proposed by researchers in ABEL group at HP Labs and independently by P. America at Philips Research Labs)
Inheritance hierarchy has only to do with implementation.
Subtype hierarchy has only to do with interface.
Therefore class != type.
Bonus: Can have multiple classes generating objects of same
type
E.g., cartesian and polar points with same external interface.
Even though 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 now redefine method2 with different type, how do we know it will continue to be type-safe when used in method1 (presuming method1 is inherited and not changed).
One can set up type-checking rules for determining legal subclasses and subtypes and be guaranteed that can't break the typing system.
This is extremely important, since one of goals of object-oriented programming languages is to provide reusable libraries of components, much like that found with FORTRAN for numerical routines or Modula-2 for data structures.
Major advantage would be ability to make minor modifications to allow user to customize classes.
Sale of libraries is expected to become a major software industry. However, if selling library will typically only sell compiled version, not source code (though provide something like definition module).
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 (involving honors theses by R. van Gent '93, A. Schuett '94, and L. Petersen '96, and supporting work by J. Rosenberg & S. Calvo '96) resulted in object-oriented language which is type-safe and only requires classes and methods to be type-checked once (don't have to repeat when inherit methods).
Other Eiffel examples:
PARENTHESES - simple example using STACK class from structures library.
Evaluation of OOL's.
Pro's (at least of Eiffel)
Con's
Eiffel also provides support for number of features of modern software engineering - e.g., assertions.
Could be a very important language if fixed type problems - Sather is one attempt.
What will be impact of OOL's on programmers and computer science?
Large number of powerful players jumped on the bandwagon without careful assessment of consequences. Now growing reaction against C++.
Many OO programmers don't really understand paradigm, esp. if use OO add-on to older language.
Suspect that most of the advantages claimed by proponents could be realized in Clu, Modula-2, or Ada (all available decade or more ago).
Some languages (Modula-3, Haskell, Quest, etc.) provide subtyping without inheritance. Seem to be few problems associated with this.
My advice: specify carefully meaning of methods, avoid long inheritance chains, and be careful of interactions of methods. If implement generics, Java could be a very successful compromise between flexibility and usefulness.
May have originated with idea that definition of language be an actual implementation. E.g. FORTRAN on IBM 704.
Can be too dependent on features of actual hardware. Hard to tell if other implementations define same language.
Now define abstract machine, and give translation of language onto abstract machine. Need only interpret that abstract machine on actual machine to get implementation.
Ex: Interpreters for PCF. Transformed a program into a "normal form" program (can't be further reduced). More complex with language with states.
Expressions reduce to pair (v,s), Commands reduce to new state, s.
E.g.
(e1, rho, s) => (m, s') (e2, rho, s') => (n, s'') ---------------------------------------------------- (e1 + e2, rho, s) => (m+n, s'') (M, rho, s') => (v, s'') ---------------------------------------- (X := M, rho, s) => (rho, s''[v/rho(X)]) (fun(X).M, rho, s) => (< fun(X).M, rho >, s) (f,rho,s) => (<fun(X).M, rho'>, s') (N,rho,s') => (v,s''), (M, rho' [v/X], s'') => (v', s''' ) ------------------------------------------------------------ (f(N), rho, s) => (v', s''' )
Meaning of program is sequence of states that machine goes through in executing it - trace of execution. Essentially an interpreter for language.
Very useful for compiler writers since very low-level description.
Idea is abstract machine is simple enough that it is impossible to misunderstand its operation.
No model of execution.
Definition tells what may be proved about programs. Associate axiom with each construct of language. Rules for composing pieces into more complex programs.
Meaning of construct is given in terms of assertions about computation state before and after execution.
General form:
{P} statement {Q}where P and Q are assertions.
Meaning is that if P is true before execution of statement and statement terminates, then Q must be true after termination.
Assignment axiom:
{P [expression / id]} id := expression {P}
e.g.
{a+17 > 0} x := a+17 {x > 0}or
{x > 1} x := x - 1 {x > 0}
While rule:
If {P & B} stats {P}, then {P} while B do stats {P & not B}
E.g. if P is an invariant of stats, then after execution of the loop, P will still be true but B will have failed.
Composition:
If {P} S1 {Q}, {R} S2 {T}, and Q => R, then {P} S1; S2 {T}
Conditional:
If {P & B} S1 {Q}, {P & not B} S2 {Q}, then {P} if B then S1 else S2 {Q}
Consequence:
If P => Q, R => T, and {Q} S {R}, then {P} S {T}
Prove program correct if show
{Precondition} Prog {PostCondition}
Often easiest to work backwards from Postcondition to Precondition.
Ex:
{Precondition: exponent0 >= 0} base <- base0 exponent <- exponent0 ans <- 1 while exponent > 0 do {assert: ans * (base ** exponent) = base0 ** exponent0} { & exponent >= 0} if odd(exponent) then ans<- ans*base exponent <- exponent - 1 else base <- base * base exponent <- exponent div 2 end if end while {Postcondition: exponent = 0} { & ans = base0 ** exponent0}
Let us show that:
P = ans * (base ** exponent) = (base0 ** exponent0) & exponent >= 0is an invariant assertion of the while loop.
The proof rule for a while loop is:
If {P & B} S {P} then {P} While B do S {P & not-B}We need to show P above is invariant (i.e., verify that {P & B} S {P}).
Thus we must show:
{P & exponent > 0} if odd(exponent) then ans<- ans*base exponent <- exponent - 1 else base <- base * base exponent <- exponent div 2 end if {P}However, the if..then..else.. rule is:
if {P & B} S1 {Q} and {P & not-B} S2 {Q} then {P} if B then S1 else S2 {Q}.Thus it will be sufficient if we can show
(1) {P & exponent > 0 & odd(exponent)} ans<- ans*base; exponent <- exponent - 1 {P}and
(2) {P & exponent > 0 & not-odd(exponent)} base <- base * base; exponent <- exponent div 2 {P}But these are now relatively straight-forward to show. We do (1) in detail and leave (2) as an exercise.
Recall the assignment axiom is {P[exp/X]} X := exp {P}.
If we push P "back" through the two assignment statements in (1), we get:
{P[ans*base/ans][exponent - 1/exponent]} ans<- ans*base; exponent <- exponent - 1 {P}But if we make these substitutions in P we get the precondition is:
ans*base* (base ** (exponent - 1)) = base0 ** exponent0 & exponent - 1 >= 0which can be rewritten using rules of exponents as:
ans*(base ** exponent) = base0 ** exponent0 & exponent >= 1Thus, by the assignment axiom (applied twice) we get
(3) {ans*(base**exponent) = base0**exponent0 & exponent >= 1} base <- base * base; exponent <- exponent div 2 {P}
Because we have the rule:
If {R} S {Q} and R' => R then {R'} S {Q}To prove (1), all we have to do is show that
(3) P & exponent > 0 & odd(exponent) => ans*(base ** exponent) = base0 ** exponent0 & exponent >= 1where P is
ans*(base**exponent) = (base0**exponent0) & exponent >= 0.Since ans * (base ** exponent) = (base0 ** exponent0) appears in both the hypothesis and the conclusion, there is no problem with that. The only difficult is to prove that exponent >= 1.
However exponent > 0 & odd(exponent) => exponent >= 1.
Thus (3) is true and hence (1) is true.
A similar proof shows that (2) is true, and hence that P truly is an invariant of the while loop!
Axiomatic semantics due to Floyd & Hoare, Dijkstra also major contributor. Used to define semantics of Pascal [Hoare & Wirth, 1973]
Too high level to be of much use to compiler writers.
Perfect for proving programs correct.