CS 334 Lecture 18

CS 334 Lecture 18

Contents:

      1. Constrained genericity.
      2. Visibility:
    1. Type Problems in Eiffel
  1. Semantics of programming languages
    1. Operational Semantics
    2. Axiomatic Semantics

Constrained genericity.

Can write class which takes class parameter which is a subclass of any specified class.

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;

Visibility:

Have control over features accessible by clients - and instance variables treated as though parameterless functions.

Subclasses can see all features, whether exported or not.

Eiffel has tools to

  1. extract specification from class: "short"

  2. Combine all features available in class (whether defined locally or inherited): "flatten"

Type Problems in Eiffel

Allowable changes which can be made in subclasses:

  1. Can add new features (instance vbles or routines).

  2. Instance variables may be given a new type which is a subclass of the original.

  3. In redefining routines, may replace parameter and result types by types which are subclasses of originals. (Note that this may be done automatically in inherited routines if type defined in terms of "like Current" or similar. See "same, lessthan" in NEWRATIONALS class.)

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 LINKABLE
Now 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

  1. Type unsafe like Eiffel
  2. Too rigid with types (like C++ or Object Pascal) forcing programmers to bypass the type system. For example, C++ allows doesn't allow user to change type of methods (new versions allow change in type of results of function methods), but has many, many, more holes, e.g., unchecked casts.
  3. Insert dynamic checks where unsafe (Java, Beta).

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)

  1. Good use of information hiding. Objects can hide their state.

  2. Good support for reusability. Supports generics like Ada, run-time creation of objects (unlike Ada)

  3. Support for inheritance and subtyping provides for reusability of code.

Con's

  1. Loss of locality. May have to look through many classes in order to find meanings of methods of current class. Changes in superclasses may have big impact on descendents.

  2. Type-checking too rigid, unsafe, or requires link time global analysis. Class that has worked in the past may break when linked with new customer class. Avoidable if either greatly restrict inheritance or separate subtyping and inheritance hierarchies. Some languages insert run-time checks to cover lack of type-safety.

  3. 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.

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.

Semantics of programming languages

Give a brief survey of semantics specification methods here.
  1. Operational

  2. Axiomatic

  3. Denotational

Operational Semantics

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.

Axiomatic Semantics

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 >= 0
is 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 >= 0
which can be rewritten using rules of exponents as:
    ans*(base ** exponent) = base0 ** exponent0 & exponent >= 1
Thus, 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 >= 1
where 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.