[comp.lang.eiffel] Stating typing in Eiffel

bertrand@eiffel.UUCP (Bertrand Meyer) (07/30/90)

To address questions that frequently come up in this newsgroup,
I am reposting a July 1989 article (in fact written in January
of that year) discussing the problem of static typing for Eiffel.

Three important comments:

1. I have edited that posting only superficially, by updating two or
three observations and removing a couple of comments that appeared
needlessly arrogant. I would certainly rewrite it differently now if
I had the time. Also, some language evolutions (e.g. the planned
introduction of class NONE, discussed a few months ago) have simplified
and clarified the problems, although they have not solved them.

In other words: I still basically agree with the bulk of this message,
although not necessarily with every individual statement.

2. As was mentioned in the original posting, the article was
prepared with typesetting in mind, with generous use of fonts
and other tricks. As a result, some elements, especially towards
the end, may be hard to understand in this ``plain text'' version.

Because the message is big, it is being mailed in two separate
pieces.

A complementary message, also a reposting, explains the
overall structure of the Eiffel type system.


                    STATIC TYPING FOR EIFFEL

                         Bertrand Meyer

                January 1989 (updated, July 1989)

                   (Minor revisions, July 1990)


        ****** POSTED IN TWO PARTS; THIS IS POSTING 1 ********




1 OVERVIEW


The object-oriented language Eiffel [5, 6] is  statically  typed:
in  other words, type errors may be caught by mere examination of
software texts.

     Unfortunately, the description of the basic  type  rules  in
[5], as well as the type checking performed by the current Eiffel
implementations, did not cover  certain  kinds  of  type  errors.
These  errors  have two interesting properties: they almost never
occur in the practical development of software systems if  proper
management  procedures  are  observed; yet it is easy, even for a
newcomer, to come up with artificial examples that  exhibit  some
of these errors.

     In recent months  several  people  have  independently  come
across these problems and in some cases have even published their
findings as evidence of flaws in the Eiffel type system.

     This article  develops  the  thesis  that  the  type  policy
adopted  by  Eiffel  is sound, and actually claims that it is the
only policy compatible with Eiffel's aim of  providing  a  clean,
powerful  basis  for  the  practical  development  of medium- and
large-scale software systems in industrial environments.

     The article gives the precise  rule  for  static  typing  in
Eiffel  and  describes the strategy for implementing this rule as
part of the incremental, modular compilation method used for  the
implementation  of  Eiffel.  Lack  of time has prevented the full
inclusion of these checks in releases 2.2  and  2.3;  they  will,
however, be part of a forthcoming release.

     Most of the arguments that  have  been  voiced  in  previous
discussions  are theoretical. In contrast, this article considers
in detail the pragmatic aspects of using classes and  inheritance
in  an  industrial  environment.  A  purely  theoretical approach
misses the software engineering criteria without which the entire
discussion is meaningless.

     Section 2 discusses  the  notion  of  typing  in  a  general
context.  Section  3  explains what typing and type failures mean
for Eiffel. Section 4 introduces the rationale behind the  Eiffel
type  policy.  Section  5  gives the precise type rule. Section 6
outlines the strategy for enforcing this rule as part  of  Eiffel
compiling. Section 7 is a conclusion and assessment.



2 BACKGROUND


During the execution of a computer program,  an  attempt  may  be
made to apply an operation to operands which are not of the form,
or type, expected by the operation. Such a regrettable course  of
affairs  may  be called a type failure.  The rest of this article
discusses  how  to  avoid  type   failures   in   object-oriented
programming.


2.1 Checking types


One possible defense against type failures is  dynamic  checking,
whereby  the  applicability of an operation to candidate operands
is ascertained at run-time, just before the operation  is  to  be
applied. For anyone concerned with software reliability, however,
this is not a satisfactory solution, since run-time is too late a
stage  to find a suitable recovery from a type failure.  A better
strategy is static checking, where potential  type  failures  are
detected  by  an  analysis  of software texts, without execution.
Such an analysis is performed by  a  mechanism  called  a  static
checker,  or  just  checker in the rest of this discussion, which
does not consider any form of checking other than static.

     Formally, a checker is a predicate on software texts -  that
is to say, a boolean function which, for any such text, expresses
whether or not the text is typewise  correct.  In  practice,  the
only  checkers  of interest are those which can be implemented by
computable  algorithms.  The  practical  requirements  are   even
stronger:  to  be  worthy of consideration, the algorithm must be
efficient, and it must be possible to integrate it in a  compiler
for the programming language used to write the software.

     A full checker is  a  [static]  checker  which  is  able  to
guarantee  that  no type failure may result from the execution of
certain software systems.  The ability  to  build  full  checkers
depends  on  the  programming  language being used. A programming
language for which it is possible to write a practical and useful
full  checker  will  be said to be fully typed, or just ``typed''
for short.  (``Typable'' would be a more accurate term since  the
use  of  such  a language will only guarantee the absence of type
failures if an appropriate checker is applied.)

     To make full checking possible, typed programming  languages
require  programmers  to provide supplementary information in the
form of type declarations. Programmers  will  explicitly  declare
every  program  entity,  such  as  a variable or a formal routine
argument, as being of a certain type, identified by a type  name.
The static checker can then check that every use of the entity is
consistent with this declaration.  In  Pascal,  for  example,  an
entity  declared  as  integer  may only participate in operations
such as addition or comparison, which are applicable  to  integer
arguments.


2.2 The typing problem in object-oriented programming


With the  exception  of  Trellis-Owl  [9],  most  object-oriented
languages  other than Eiffel do not support static typing.  Some,
such as Smalltalk and Lisp derivatives have followed the route of
dynamic  typing.  C  extensions  such  as C++ include a notion of
type, but defeat any attempt  at  static  checking  by  providing
loopholes  (``type casts'') inherited from C; these loopholes are
required for any realistic use of such a language, in  particular
because  of the absence of genericity (as discussed below).  Such
a lax attitude towards typing is all the  more  regrettable  that
static  checking  is particularly desirable in an object-oriented
context.

     The fundamental construct in object-oriented programming  is
the  application  of  an  operation to an object; this is written
entity.feature in Eiffel, meaning ``Apply the  operation  denoted
by  feature  to the object denoted by entity''. (Arguments may be
given if feature is a routine.) This will cause a type failure if
no such operation is applicable to the given object.

     Under its basic form, the static typing problem  for  Eiffel
is  simple:  this  is the problem of statically guaranteeing that
such  a  type  failure  will  never  occur.  (A   more   complete
description of the problem will be given below.)

     In other words, the basic  problem  is  the  possibility  of
writing  a  checker  which,  whenever it accepts a set of classes
containing a case of the notation entity.feature, guarantees that
the  run-time  support  mechanism  will always be able to find an
operation for feature, applicable to the object  associated  with
entity.

     The problem  might  appear  trivial  in  any  language  that
requires  every  entity  x to be explicitly declared of a certain
type T, defined by a class: then f must be an exported feature of
that  class.   The  reason  things  are  not  that  simple is the
presence of two powerful mechanisms: polymorphism, which  at  run
time  allows  x  to  denote  an  object  of  a  type T', possibly
different  from  T  and  statically  unpredictable;  and  dynamic
binding,  which  implies  that in such a case the version of f is
the  version  applicable  to  instances  of  T',  which  is   not
necessarily  the  same  as  the  version for T.  (Dynamic binding
should not be confused with dynamic typing; in  fact  the  Eiffel
combination   of   static   typing   and  dynamic  binding  is  a
particularly productive one.)


2.3 Limits of typing


Before analyzing in more detail how Eiffel addresses  the  typing
problem,  it  is important to consider three practical aspects of
typing, which are a consequence of the preceding discussion.

     The first observation is that static  type  checking  cannot
guarantee  that  no  run-time  failure  will  occur: it will only
remove what has been called ``type failures'' at the beginning of
this  discussion.  Take  for  example  a typed language where the
division operation expects arguments  of  type  REAL.   Then  the
language  and  its  checkers  will  flag a/b as illegal if b is a
variable of type CHARACTER; but  if  both  operands  are  of  the
correct type, the expression will be considered legal even though
the value of b might be  zero  at  execution  time,  producing  a
failure.

     A similar problem exists  in  the  type  system  of  object-
oriented  programming: a type failure has been defined above as a
case in which entity.feature is executed when entity  denotes  an
object  which  has  no operation for feature. But all that a type
checker for Eiffel will be able  to  guarantee  is  that  if  the
object  exists,  there will be an operation feature applicable to
the object denoted by entity. This will cause a type  failure  if
no such operation is applicable to the given object.

     This aspect of the typing problem is frustrating because  it
means  that  type  checking  may  be  used  to catch the ``easy''
errors, such as using a character as denominator in  a  division,
but not the ``hard'' ones, such as using a real denominator which
in some cases may be  zero.  Unfortunately,  to  catch  the  hard
errors   statically   would  require  formal  proofs  of  program
correctness, a goal that, even  in  decidable  cases,  is  beyond
today's  technology  (although  progress  towards  this  goal  is
definitely possible in Eiffel thanks to the assertion  mechanism,
but  this  is  beyond the present discussion).  So type checking,
which may be called the  ``poor  man's  proof'',  only  addresses
errors   that  may  be  statically  detected  through  relatively
straightforward mechanisms.

     This  observation  does  not  mean  that  type  checking  is
useless,  if only because the ``easy'' errors that it detects are
among  the  most   important   ones   in   practice,   reflecting
inconsistencies  which often result from serious design mistakes.
But it reminds us that type checking is only a partial answer  to
the  problem  of  program  correctness,  and that the distinction
between the errors which are  detectable  by  type  checking  and
those  which  are  not  is a pragmatic one, having to do with the
power of the detection mechanisms available to us.

     A program that has  been  type-checked  and  found  typewise
correct  is guaranteed to execute without type failure; it is not
guaranteed to execute without failure.

     In Eiffel, the remaining  failures  will  cause  exceptions,
which  lead  to abnormal termination unless the application deals
with them explicitly.


2.4 Cost issues


In the preceding discussion two kinds of errors were  considered:
ones  that  are  very  difficult  or  impossible to detect in the
current state of the technology; others that are relatively  easy
for a compiler to find in a language designed for static typing.

     In practice the situation  may  not  be  so  clearcut.  Some
checks  may  be tractable in principle but costly to implement as
part of standard compiling mechanisms. In traditional  languages,
this  is  often the case with inter-procedural checks. A frequent
solution is to provide a static checker (such as  the  lint  tool
for  C),  which  is  separate from the compiler and performs more
advanced checks.

     One may contend, of course, that the need for such tools  is
a  consequence  of poorly designed type systems and in many cases
it is. But the underlying idea is  not  without  merit.  Checking
implies definite costs in time. If one is able to classify errors
according to their practical  probability  or  the  damages  they
cause,  then  the  benefit-to-cost  ratio  of  static checks will
usually show a point of diminishing returns.  Then  it  may  make
sense  to  have  the compiler, under its default options, perform
the checks with the highest  benefit-to-cost  ratio,  the  others
being carried out by explicit request only.

     A related  practical  requirement  arises  in  the  case  of
Eiffel,  a modular language meant for incremental compilation. In
any good compiled implementation of Eiffel, classes are  compiled
separately;  if  a  class C has been type-checked and compiled, a
programmer who writes a new class D depending on C  must  clearly
be  able to check D without having to check C again.  (Dependents
in Eiffel include clients, which have access to the class through
its  interface only, and heirs.) This implies a strong constraint
on the type system, which must be designed in such a way that the
addition  of a new descendant never makes an existing class type-
wise incorrect.

     Satisfying this requirement means that it will  be  possible
to  integrate  full  type checking with the automatic incremental
compilation mechanism used in the current Eiffel  implementation,
which  after a change recompiles only the minimum set of impacted
classes, taking export controls into account (clients of a  class
which  has  undergone  changes affecting only hidden features are
not recompiled).


2.5 Pessimism


The third important observation is that type checking is always a
pessimistic  policy. Pascal compilers, for example, will reject a
program extract of the form
     procedure p(var i: integer; r: real; var b: BOOLEAN);
          begin
               if b then i := r else i := i + 1
          end

because the first branch of the conditional attempts to assign  a
real  value to an integer variable (a forbidden combination). Yet
in a given program  it  may  very  well  be  the  case  that  the
procedure  is always called with b false, so that no type failure
would result.

     The problem is not  just  that  a  program  should  only  be
accepted  if  it  cannot  ever  produce  a  type  failure. If the
instruction
     b := false

is inserted at the beginning of the above procedure, no execution
of this procedure may ever produce a type failure. Yet all Pascal
compilers will still reject the above procedure, because they  do
not  perform the kind of flow analysis that would be necessary in
the general case to discover that the type failure may not occur.

     The behavior of compilers (and designers of statically typed
languages) in such cases is easy to understand. Compiling time is
a precious resource and should not be wasted in trying to salvage
programs that look hopelessly suspicious, even if once in a while
one of these programs would in fact work. The principle  followed
here  is  ``better  safe  than sorry''. If the checker is to err,
then it should err on the safe side by  rejecting  some  programs
that would work, rather than accepting ones that will not.

     So in all non-trivial  cases  typing  is  pessimistic.  This
means  that  every  language  is typed in a way: a universal full
checker (applicable to Smalltalk, Lisp and APL as well as  Basic)
is the program
     print "Program rejected"

     This example is ludicrous, of course, but shows that  typing
is  a relative, not an absolute notion. The above definition of a
typed language should thus be amended: A programming language  is
fully  typed,  or  just  typed,  if  it  is  possible  to write a
practical and useful full  checker  which  will  not  reject  any
reasonable program.

     This definition leaves some margin  of  appreciation  as  to
what  is  a  ``reasonable''  program,  a  notion  that may not be
defined rigorously, although ``reasonable'' people should be able
to  agree  on whether a specific kind of program is reasonable or
not.


2.6 The role of typing: an assessment


The common lesson from the above three comments on typing is that
in any full discussion of typing the pragmatic considerations are
as important as the theoretical discussion.

     The three key questions of typing for a programming language
are:

1
     + Among all possible run-time failures, which ones are to be
       considered  type  failures,  to  be totally avoided by the
       checking mechanism, and which ones should be considered as
       non-type failures (as division by zero)?

2
     + Among all possible checks, which  ones  should  always  be
       performed  by the compiler, and which ones (if any) should
       be done on request only?

3
     + Given that the checker will reject all programs containing
       potential type failures as defined under 1, how far can it
       go in rejecting programs  that  might  not  lead  to  such
       failures if executed?

     The answers to all of these questions must of necessity rely
on an assessment of highly practical issues.

     For question 1, the issue is what checks  can  realistically
be  performed  by  a  compiler  or  other  checker. This requires
familiarity with the current technology of  compilers  and  other
software tools.

     For question 2, the issue is the  benefit-to-cost  ratio  of
various  checks.   This  adds  to  the  previous requirement some
knowledge of what errors  programmers  actually  make  and  their
actual degrees of gravity.

     For question 3, the  issue  is  what  kinds  of  potentially
correct  programs  may be rejected by the checker. This demands a
practical understanding of how programmers use the  language  and
what is essential for them.

     What is required in all cases is a software engineering view
which,  beyond  the  language  and  its  theory,  encompasses its
practical application to the construction of useful software.



3 THE EIFFEL TYPING PROBLEM

3.1 Terminology


It is assumed that the reader is familiar  with  Eiffel  and  has
read  [5]  or  at  least  the  12-page  introduction in [8].  The
precise and up-to-date reference on the language  proper  is  the
book  [6].   Any  meaningful  discussion of type issues in Eiffel
requires using the proper terminology,  which  will  be  recalled
here.

     Software texts in Eiffel are made of classes.  An executable
software   product   is  obtained  by  assembling  a  system,  or
combination of one or more classes, one of which  is  called  the
root of the system.

     A class describes a number of  potential  run-time  objects,
called  its  instances.   A class is characterized by a number of
features.  A  feature  is  either  an  attribute  (describing   a
modifiable  component present in each instance of the class) or a
routine (describing a computation applicable to each instance  of
the class).

     A routine is either a function, which returns a result, or a
procedure,  which  does not. A routine may have formal arguments;
if so, calls to  the  routines  must  include  the  corresponding
actual arguments (expressions).

     An entity is a name in a class text which stands for a value
at run-time. For this discussion we ignore entities of ``expanded
types''  which  include  basic  types  (integer,  real,  boolean,
character)  and  for  which polymorphism is not supported; so the
only values of interest  are  those  whose  run-time  values  are
references.  A  reference  either points to an object or is void.
The following forms of entities are provided:

     + Attributes of a class.

     + Local variables in routines.

     + The predefined entity Result which can only  appear  in  a
       function.   (The  value returned by a function call is the
       final value of Result on  termination  of  the  function's
       execution.)

     + Formal arguments of a routine.

     An attribute or local variable may be used as target  of  an
assignment, written
     x := y

where x is an entity and y is an expression. The effect  of  such
an  assignment  is  to make x void if the value of y is void, and
otherwise to make x refer to the same object  as  y.  The  latter
case  produces aliasing between x and y; two entities are said to
be aliased if there values are references  to  the  same  object.
Aliasing  may  also  result  from  a  routine call where y is the
actual argument and x the corresponding formal  argument.

     What makes the object-oriented typing problem non-trivial is
the  presence  of  polymorphic  aliasing,  where  x  and y are of
different class types. The typing problem is then that  the  same
object  is accessible through names of two different types, which
may have different  rules  with  respect  to  what  features  are
exported and what feature arguments are permissible.


3.2 The basic type rules


Two fundamental type rules (which  have  always  been  statically
enforced  by our implementation of Eiffel) serve to avoid most of
the cases that might lead to a type failure. They are sketched in
sections  10.1.4  and 11.3 of [5] and repeated here in simplified
form.

     The first rule governs feature application: x.f (...), where
x  is of class type C, is only legal if one of the ancestors of C
(possibly C itself) defines a feature f, and C exports f.

     The second rule governs polymorphic aliasing: x := y is only
legal  if  the  type of y say D, conforms to C.  (For non-generic
classes, this means that D is a  descendant  of  C;  for  generic
classes,  conformance  also  includes recursive conditions on the
generic parameters.) This also applies to argument passing  if  x
is the formal argument and y the corresponding actual argument.

     This second rule is also what lies behind the possibility of
defining flexible generic data structures such as
     stack_of_C: STACK [C]

with the effect that the push routine applied to stack_of_C  will
accept as argument any expression whose type conforms to C.

     Taken together, the two  rules  exclude  most  type  errors.
Nevertheless some type failures are still possible in principle.


3.3 Violations


Type failures may occur in Eiffel in spite  of  the  above  rules
because of the following two properties of the language:

     + The information hiding  mechanism  is  orthogonal  to  the
       inheritance  mechanism.  In  other words, a feature hidden
       (exported) by a class may be exported (hidden) by  any  of
       its proper descendants.

     + When a routine is redefined in a descendant, the type of a
       formal argument may be redefined to be a proper descendant
       of the  original  type.   This  is  sometimes  called  the
       covariant rule.

     Both of these rules makes it easy to construct examples that
lead  to type failures. Let PARENT be a class and HEIR one of its
heirs. Assume PARENT exports feature f, but HEIR does not.   Then
the first type rule makes the following legal:

/1/
     p: PARENT; h: HEIR;
     ...
          h.Create;
          p := h;
          p.f


     Here because of dynamic binding  we  are  sneakily  applying
operation  f  to an object of type HEIR, even though the designer
of class HEIR has expressly removed f from the list of operations
applicable  to  such  objects.   Why  this  is indeed bad will be
analyzed in more detailed below.

     As an example of the second kind, assume that routine  g  is
defined  in PARENT with an argument of type PARENT, and redefined
in HEIR with an  argument  of  type  HEIR.  (More  generally  the
respective  argument  types  might  be  any  P  and H such that H
conforms to P; here for simplicity we have chosen P and H  to  be
the   enclosing   classes   themselves.   In  practice,  explicit
redeclaration is avoided in such a case by using a  ``declaration
by  association'' of the form like Current.) Then the second type
rule makes the following legal:
/2/
     p: PARENT; h: HEIR;
     ...
          h.Create;
          p := h;
          p.g (p)


     Here we are sneakily calling g on an  object  of  type  HEIR
with  an  argument  of  dynamic  type PARENT, not HEIR.  The type
failure is not immediate; we have only planted a time bomb  which
will  explode  if  g,  which  expects  an  argument of type HEIR,
applies to this argument some feature defined in this  class  but
not applicable to instances of PARENT.


3.4 Analysis of failures


Why are the above situations examples of type failures?

     In case /2/, the problem is clear: since the  version  of  g
redefined in HEIR assumes an argument of type HEIR, it might well
access an attribute defined for objects of type HEIR but not  for
those  of type PARENT. Such a type failure will usually trigger a
run-time exception.

     Case /1/ is more subtle, as will be seen if we compare it to
the following almost identical variant, which assumes the reverse
export policy: f exported by HEIR but not by PARENT.
/3/
     p: PARENT; h: HEIR;
     ...
          h.Create;
          p := h;
          h.f


     In /3/ to we again apparently use aliasing (the  ability  to
refer  to an object through more than one name) as a way to cheat
with export restrictions. Even though  p.f  is  illegal,  we  can
apply  f  to the object associated with p by accessing it through
its HEIR name.

     In /3/, however, nothing bad can really occur;  because  the
designer of HEIR decided to export f, it means that (if the class
is correct) f may be correctly applied to objects of  this  type.
In  precise  terms,  f, being exported by HEIR, must preserve the
invariant of this class. (No discussion  of  these  issues  makes
sense unless it refers to the notion of class invariant.)

     For /1/ the situation is different. Feature f is exported by
PARENT,  meaning  that  it preserves the invariant of this class.
But it may well fail to preserve the invariant of HEIR  (this  by
itself  being  a sufficient reason for HEIR not to export f).  By
using  polymorphism  and  dynamic  binding,  the   client   class
containing  extract  /1/  succeeds  in applying f to an object of
type HEIR; as a result, the object may enter  into  an  incorrect
state (which usually will soon produce an exception).

     Cases /1/ and /2/ are prototypical examples of the bad cases
that  may  occur.  Others (involving in particular genericity and
declaration by association) are mere variations  on  these  basic
patterns. /3/, as noted above, does not lead to type failures.

     For all their obviousness, these cases very seldom occur  in
practice  in  a project whose aim is to construct useful software
rather than to crash the Eiffel environment. For example,  it  is
interesting  to  mention  that  no  type  failure  has  ever been
recorded  in  the  usage  of  Eiffel  at   Interactive   Software
Engineering.   This  explains  why  fixing the type rules and the
corresponding type checking performed by  the  compiler  has  not
been a top priority.

     But of course the loopholes must eventually be closed.


4 RATIONALE FOR THE EIFFEL RULES


Before studying the solution,  we  will  pause  to  consider  the
Eiffel  type  rules  giving rise to the above problems and assess
whether they make sense from a software engineering  perspective.
These  rules  have  indeed  been  criticized  [1, 3, 4] (the last
reference even includes a proposal for ``fixing''  these  rules).
Several   related   messages   have   also  been  posted  to  the
comp.lang.eiffel newsgroup.  [JULY 1990 NOTE:  since  then  there
has  also been a letter in the January/February 1990 issue of the
Journal of Object-Oriented Programming, and recurring discussions
in comp.lang.eiffel.]

     These rules were not  designed  hastily,  however,  and  the
experience of several years and hundreds of thousands of lines of
useful Eiffel software even  suggests  that  they  are  the  only
acceptable ones.


4.1 Rationale for the export rule


Consider first the export-inheritance rule, which often surprises
newcomers   as   an  apparent  violation  of  information  hiding
principles. How can a descendant override export restrictions  of
an ancestor, or hide what the ancestor exported?

     To obtain a better perspective on this rule it is useful  to
examine  its application to what may be the archetypal example of
inheritance. Assume PARENT is a class POLYGON and HEIR is a class
RECTANGLE.  The  inheritance  relation  in this case is clear and
natural.

     It is not absurd, however, to assume that class POLYGON  has
a procedure
     add_vertex (new: POINT) is
               -- Insert new vertex new at current cursor position
          do
               ...
          ensure
               nb_vertices = old nb_vertices + 1
          end -- add_vertex

     For general polygons, this procedure may make sense.  It  is
not,  however,  applicable  to  rectangles, whose class invariant
should contain the clause
     nb_vertices = 4

     The Eiffel solution is to ensure that class  RECTANGLE  does
not  export  procedure  add_vertex. This is the kind of case that
led to situations such as /1/, with f being add_vertex.





















           Figure : Alternative inheritance structures


     Three policies are possible in the face of such cases.

     + Policy  1  considers  that  this  use  of  inheritance  is
       inadequate:  RECTANGLE is not a subtype of POLYGON because
       not all operations applicable to the latter are applicable
       to  the  former.  According  to  this  policy,  the proper
       solution (see figure  1)  is  to  consider  two  heirs  to
       POLYGON:  FIXED_POLYGON  and  VARIABLE_POLYGON.  Procedure
       add_vertex is a feature of VARIABLE_POLYGON only,  whereas
       RECTANGLE inherits from FIXED_POLYGON. This way the Eiffel
       rule can be changed to require  that  every  class  should
       export  all features exported by its ancestors. (Even with
       this policy, however, there is no  reason  to  prohibit  a
       class   from  exporting  a  feature  hidden  by  a  proper
       ancestor. This technique is  commonly  used  in  practical
       Eiffel  programming  and,  as  discussed above, entails no
       danger of type failure.)

     + Policy 2 considers that inheritance is  used  properly  in
       this  case, but only for half of its capacity: as a module
       enrichment mechanism, not as  a  subtyping  facility.  The
       definition of class RECTANGLE should be permitted, but not
       polymorphic assignments of the form p := r (with p of type
       POLYGON  and  r  of type RECTANGLE). The conformance rules
       governing such assignments (as outlined in section  11.3.1
       of  [5]  and  given  fully in chapter 10 of [6]) should be
       updated to exclude such cases. If polymorphic  assignments
       are  needed,  then  the  inheritance  structure  should be
       redesigned as shown in figure .

     + Policy 3 is more liberal: it permits the above polymorphic
       assignments as long as it can be ascertained statically at
       reasonable cost, for any  Eiffel  system  involving  these
       classes,  that no type failure may result; in other words,
       that no add_vertex operation may ever be applied to  p  or
       an  entity  that may become associated with p. This is the
       policy applied in Eiffel, for which the exact  rules  will
       be given below.

     What speaks in favor of policies 1 and 2 is  that  they  are
extremely  easy to implement for the compiler writer. Restricting
exports in descendant classes (policy 1) is trivial;  restricting
polymorphic  aliasing  (policy  2)  is almost as immediate. These
solutions  would  also  have  the  advantage  of   quieting   the
theoreticians and removing the fears of prospective users.

     In short, all obvious arguments seemed to push the designers
of  Eiffel  towards  policy  1  or 2: convenience (since the same
group is also  in  charge  of  an  implementation)  and  ease  of
convincing  future  users.  So it takes some dose of fortitude to
choose policy 3 and stick to it.  Why should this be  the  Eiffel
policy?

     The reasons can only  be  understood  by  going  beyond  the
purely  theoretical  discussions  and considering the practice of
object-oriented software construction.

     Policies 1 and 2 assume that programming is for  gods.  Gods
never  make  any  mistake.  Those among them who aside from their
other business indulge  in  the  heavenly  pleasures  of  object-
oriented  programming  always  get  their  inheritance structures
right the first time around.

     In today's job market, however, most employers  must  resort
to  hiring  programmers  who  are  only demigods, or even in some
cases (although they will deny it) mere mortals.  Then we have to
accept  the  need  to  work  with inheritance structures that may
already in be place and have been designed with less-than-perfect
foresight.

     Assume for example that POLYGON had  already  been  designed
and  has  descendants such as CONVEX_POLYGON and CONCAVE_POLYGON.
In other words, the existing inheritance structure does not  take
into  account  the  difference between ``variable'' and ``fixed''
polygons. Then a new programmer comes  in  and  has  a  need  for
RECTANGLE;  writing  it  as  an heir to POLYGON seems the obvious
solution.

     Policy 1 precludes doing this without first reorganizing the
entire  inheritance structure. This is unrealistic in a practical
development environment.  True, we should  accept  the  need  for
regular   improvements   of  inheritance  structures,  reflecting
improved understanding of software artefacts. But as  anyone  who
has  managed  a  class  library in an industrial environment will
appreciate,  such  evolution  should  be  carefully  planned  and
properly  controlled.  One cannot accept a situation in which new
but legitimate uses of a class require constant  changes  in  the
design of existing libraries.

     Policy 2 is  only  marginally  better.  It  does  allow  the
programmer to define RECTANGLE as an heir to POLYGON but prevents
him from using polymorphism in this case; it is not possible, for
example, to define a data structure such as
     polygon_stack: STACK [POLYGON]

and  push  a  RECTANGLE  object  onto  polygon_stack.   This   is
particularly  frustrating  in an application that never even uses
add_vertex.  Again,  the  only  solution  is  to   redesign   the
inheritance structure.

     Policies 1 and 2 are overly  inflexible.  In  contrast,  the
idea   of  policy  3  is  to  avoid  bothering  programmers  with
unnecessary restrictions when their  use  of  inheritance  cannot
possibly  lead  to  any  type failure.  The criterion to apply is
obvious in the example  given:  the  checker  should  reject  any
software system that contains both of the operations

     + p := r

     + p.add_vertex (...)

     Because  checkers  cannot  realistically  carry   out   flow
analysis,  it  is not necessary to check whether or not these two
instructions can be executed on the same control flow  path;  the
mere presence of both in the same system is reason enough for the
checker to reject that system. In other words, we accept  without
regret  that  the  following  sequence (with n an integer entity)
will be rejected even though it cannot possibly lead  to  a  type
failure:
     if n >= 0 then p := r end
     if n < 0 then p.add_vertex (...) end

     The reason for this was discussed in section  2.4:  checkers
should  only  be  bound to perform ``reasonable'' controls. For a
checker to recognize that the above is safe requires control flow
analysis,  whose cost, if it is at all possible, is not justified
by the benefit.  Using the terminology of  section  2.4,  we  are
past the point of diminishing returns.

     As noted, type checking is always  a  pessimistic  strategy;
the  only  question  is  what  degree of pessimism is acceptable.
Rejecting extracts of the above form appears  acceptable  because
it  is  hard  to  conceive of them being used in useful programs.
This is not true, however, of the much stronger pessimism implied
by  policies  1 and 2, which leads to rejection of programs that,
for the reasons discussed above, are useful and even needed.

     How policy 3 can be implemented at reasonable cost  will  be
discussed below.


4.2 Further reflections on inheritance


The argument made above for  orthogonal  export  and  inheritance
mechanisms  was  that  inheritance  structures may be temporarily
imperfect. The  implied  consequence  is  that  eventually  these
structures  will  be  perfected;  then  policies 1 and 2 could be
implemented if we  were  willing  to  implement  tougher  library
management  procedures  (forcing  restructuring when appropriate,
and requiring new heir designs to wait in the meantime).

     Unfortunately, even such an approach may not be  viable.  In
practice we must probably accept that some inheritance structures
will always be imperfect.

     Inheritance is the application to  software  engineering  of
the    most    fundamental    of   all   scientific   techniques:
classification.  The modern forms of the natural sciences,  since
Linnaeus, and of mathematics, at least since Bourbaki, were borne
out of systematic efforts  to  classify  natural  and  artificial
objects;   object-oriented  programming  attempts  the  same  for
software objects.

     Classification is humanity's attempt to bring a semblance of
order to the description of an otherwise rather chaotic world. As
anyone who has ever tried to  use  a  botany  book  to  recognize
actual   flowers   knows,  classifications  never  quite  achieve
perfection. Close as one can get to a fully satisfactory  system,
a few exceptions and special examples will remain.

     Moving from  botany  to  zoology,  the  cliche'  example  of
ostriches  and  flying  illustrates these problems well. In class
BIRD, it seems appropriate to include and export a  feature  fly.
But  a  descendant  class such as STRUTHIO (the name of the genus
that includes ostriches) should not export fly.  This  is  really
the  Eiffel  form  of selective inheritance, or rejecting part of
your heritage: the rejected feature is  still  there  internally,
but  not  visible  by  your clients. (The discussion of selective
inheritance in section 10.5.3 of [5], is too restrictive in  this
respect:  it  seems  to reject any form of selective inheritance,
even though Eiffel supports the form discussed here.)

     In a case such as this one there does not seem to be a  much
better  solution.  Any other classification of birds, for example
into flying and non-flying ones,  would  miss  key  criteria  for
distinguishing  between  various  classes  of  birds,  and  would
undoubtedly cause bigger problems  than  the  original  solution.
Multiple  inheritance  from  a  class FLYING_THING would not help
much.

     So far, only a minority  of  Eiffel  users  have  admittedly
reported  ostrich-oriented  programming  as  their  major area of
technical  interest.   But  the  need  to  deal  with   imperfect
inheritance  structures  seems  universal.   Of course, we should
strive to make  these  structures  as  complete  and  regular  as
possible.  But  we must also accept that unexpected special cases
and exceptions may always occur. When they do and the  programmer
is   only   performing   safe   manipulations,   the  programming
environment should help him, not put undue  restrictions  in  his
way.


4.3 Rationale for the redefinition rule


The analysis of the redefinition rule is similar to the preceding
discussion  of  the  export  rule. It is in fact hard to see what
convention other than  the  one  used  in  Eiffel  can  have  any
practical value.

     Consider  classes  HEIR  and  PARENT  as  above,   and   add
HEIR_QUEUE  and  PARENT_QUEUE  representing  stacks of objects of
these respective types. Of course, the normal Eiffel method is to
use  genericity  and  declare  these  classes as QUEUE [HEIR] and
QUEUE [PARENT]. But the use of genericity does  not  affect  this
discussion.

     Clearly, if every instance of  HEIR  ``is-an''  instance  of
PARENT,  a  list  of  HEIR  objects  also ``is-a'' list of PARENT
objects. This and other reasons suggest  that  HEIR_QUEUE  should
inherit  from  PARENT_QUEUE.  The conformance rules indeed ensure
this if genericity is used.

     Assume that PARENT_QUEUE contains a procedure  add  with  an
argument  of  type  PARENT.  Clearly,  we  only  want to add HEIR
objects into an HEIR_QUEUE. So  insert  should  be  redefined  in
HEIR_QUEUE so as to take an argument of type HEIR. This satisfies
the Eiffel rule for redefining routine arguments - the  covariant
policy.    (In   practical   Eiffel   programming,   no  explicit
redefinition will be used; the effect is achieved more  elegantly
by  a  like  something  declaration, also known as declaration by
association.)

     Interestingly, denotational models for inheritance (see  [2]
or   the  brief  presentation  in  [7])  prescribe  the  reverse,
``contravariant''  rule:  arguments  can  only  be  redefined  to
ancestor  types of the original.  This is apparently [3] the rule
implemented in Trellis-Owl.

     Examples such as the above, of which there are thousands  in
practical  Eiffel  applications, make it very hard to imagine how
significant object-oriented software can be written  in  a  typed
language  without a covariant policy.  Many of these examples use
declaration  by  association,  which  is   only   a   syntactical
abbreviation,   but  in  practice  an  essential  one;  its  very
availability for routine arguments is only  possible  because  of
the covariant rule.

     Pronouncements that the contravariant rule is ``the  correct
policy'', based on the revealed truths of denotational semantics,
are surprising.  (When the  facts  revolt,  send  in  the  lambda
troops!).   To  be sure, the simplicity of mathematical models is
an  important  guideline  for  assessing   programming   language
decisions.  Yet  a  model  that conflicts with practical needs of
software development loses its usefulness.  A model  is  only  an
operational  description  of  some  reality.   If  the two do not
match, the scientific method suggests that we should  change  the
model, not the reality.

     The problem with the covariant rule, of course, is  that  it
opens  the  possibility  of  type failures of the kind called /2/
above, which readily transposes to our latest example:
/4/
     pl: PARENT_QUEUE; hl: HEIR_QUEUE;
     p: PARENT;
     ...
          p.Create;
          hl.Create;
          pl := hl;
          pl.add (p)


     As noted for /2/, the possible type failure  is  delayed  in
this   case,  occurring  for  example  when  something  like  the
following is executed.
     h := hl.oldest;
     h.heir_op

(Entity h is of type HEIR, heir_op is  a  feature  introduced  in
HEIR  and  not  available in PARENT, and feature oldest returns a
queue element.)

     The answer to such a case is in line with the result of  the
discussion  on the export-inheritance rule: put the burden on the
compiler, not the compiler. Any occurrence of the last two  above
instructions  in  an  Eiffel  system  should  be  detected by the
checker;  once  again,  their  mere  coexistence,   without   any
consideration  of whether the two can be part of the same control
flow path, is sufficient cause for rejection.  No  flow  analysis
should be imposed on the checker.

     The next section specifies the type rules in  light  of  the
preceding  discussion.  The following one will outline the method
to be followed by a checker to enforce these rules at  reasonable
cost.


******* END OF FIRST POSTING. PLEASE APPEND SECOND POSTING ***

-- Bertrand Meyer
bertrand@eiffel.com

bertrand@eiffel.UUCP (Bertrand Meyer) (07/30/90)

                     STATIC TYPING FOR EIFFEL

                         Bertrand Meyer

                January 1989 (updated, July 1989)

                   (Minor revisions, July 1990)


        ****** POSTED IN TWO PARTS; THIS IS POSTING 2 ********

                  *** Please prepend posting 1 ***


5 THE TYPE RULE


The type rule will be introduced under a  number  of  simplifying
assumptions, meant to keep the discussion clear.


5.1 Simplifications


An Eiffel type will be taken to  be  defined  by  a  class.  This
includes in fact two simplifications:

     + ``Expanded types'', which in particular include the  basic
       types INTEGER, REAL etc., are not covered. These, however,
       are not involved in polymorphism, and hence do  not  raise
       any of the problems discussed above.

     + Generically parameterized  types  are  also  not  covered.
       Genericity,  essential  in  practical uses of Eiffel, does
       not  fundamentally  affect  the  current   discussion.   A
       consequence of this simplification is that ``B conforms to
       A'' is the same relation as ``B is a descendant of A''.

     For the purpose of this discussion, Eiffel may be viewed  as
having only two primitives: assignment and feature application.

     An assignment is of the form
     x := y

and is a reference assignment.  The left-hand side is an  entity;
the right-hand side is an expression.

     A feature application is  either  ``remote''  or  ``local'',
corresponding to the two forms
     x.f (a, b, ...)
     f (a, b, ...)

where feature f may be an attribute or a routine.

     All feature applications will be  considered  to  be  remote
applications of routines with exactly one argument, of the form:
     x.r (a)

meaning ``Apply routine r to the object  associated  with  x,  if
any,  with  the  given arguments''.  This simplification does not
entail any loss of generality:

     + For local feature applications, we can consider that x  is
       Current,  the  predefined  entity representing the current
       object.

     + A routine with no argument  may  always  be  rewritten  as
       having one argument, not used in the body.

     + For a feature f which is an attribute,  we  can  write  an
       associated  function  r  with  one  argument  (not  used),
       returning the value of  the  attribute,  and  replace  all
       applications of f by applications of r.

     + For a routine with more than one argument, the rules on  a
       as  given  below  are easily extended to rules applying to
       every  formal  argument  and  the   corresponding   actual
       arguments.

     The   above   does   not   consider   ``multidot''   feature
applications of the form a.b.c.d.... Although useful in practice,
they can safely be ignored from  a  theoretical  viewpoint  since
they  can always be replaced by sequences of one-dot applications
by using auxiliary  variables  (x := a.b;  y := x.c;  etc.)  More
generally,  we  may  consider  that  every  expression (usable as
source for an assignment or actual  argument  to  a  routine)  is
either  an  entity  or  a  feature application of the above form,
where r must be a function.

     As a last simplification, we  assume  that  no  renaming  is
used,  so  that a feature defined in a class has the same name in
all descendant classes, including those where  it  is  redefined.
This  entails  no  loss  of generality since renaming is a purely
syntactic facility, albeit a very important one in practice.  The
only  case  for  which renaming is indispensable is removing name
clashes  in  multiple  inheritance;  for  the  purpose  of   this
discussion  we can assume that the original classes are rewritten
so as to avoid any such case. In the same spirit we  assume  that
when  a  routine  is  redefined  the  name of its formal argument
(whose type may be redefined according  to  the  covariant  rule)
does not change.


5.2 The goal of typing


The preceding simplifications make it  possible  to  express  the
goal of Eiffel typing in a complete yet simple way:


          _________________________________________________


            Type safety constraint:  An  Eiffel  system  is
         said   to  be  type-safe  if  for  any  routine
         application x.r (a), and for any dynamic  types
         DX  and  DA  that  x  and a may take during the
         execution  of  the  system,  DX  includes   and
         exports  a  routine  corresponding to r, with a
         formal argument declared of a type which is  an
         ancestor of DA.

          _________________________________________________


     A system  satisfying  this  constraint  cannot  suffer  type
failures  at  run  time.  It  can  still,  however,  suffer other
failures, most notably by attempting to apply a routine  r  to  a
reference x that happens to be void. But this, as the division by
zero case discussed at the beginning  of  this  article,  is  not
within the scope of typing strategies.

     Clearly, the contravariant rule,  combined  with  either  of
policies  1 and 2 for export and inheritance, guarantees the type
safety constraint. We must now see how to ensure it with the more
liberal rules: policy 3 and the covariant rule.


5.3 The type rule


The  fundamental   type   rule   will   address   the   preceding
requirements; it replaces (by strengthening it) the first rule of
section 3.2. The rule uses the set  Aliases (x)  which,  for  any
entity x in a system, contains all entities to which x may become
aliased through assignment or routine call.  A precise definition
of this set will be given below.

     The type rule is the following:


           _________________________________________________


            Type rule: An Eiffel system is type-correct  if
         and  only  if  the following two conditions are
         satisfied for any routine application x.r (z):


               + For every y in Aliases (x), Sy    exports r.


               + For every y in Aliases (x) and every t  in
         Aliases (z),  St     conforms  to  the type of the
         formal argument of r in Sy   .

           _________________________________________________


     Pending a proper definition of Aliases,  it  is  clear  that
this rule will satisfy the above type constraint.


5.4 Aliasing


It remains to  define  Aliases (x)  precisely.   A  more  correct
notation would be Aliases [S] (x) denoting the possible aliases of x
with respect to a given system  (set  of  classes)  S.   In  what
follows  S  will be considered to be fixed and the subscript will
be dropped.

     Repeating the definition of a few  basic  concepts  will  be
useful.

     + An entity is a name declared in a  class,  representing  a
       reference  which  may  be  assigned  values  at  run-time.
       Formally, then, an entity of name x appearing in  class  C
       is  defined  by  the pair <C, x>. For simplicity, entities
       will in fact be designated below  given  by  their  entity
       names  only,  but we must be careful to make it clear, for
       every x, what class C it belongs to.  C will be called the
       class of x

     + Any entity x is declared of a  certain  type,  called  the
       static  type  of  x  and  written Sx.  The declaration may
       appear in the class of the entity  or,  for  an  inherited
       entity, in an ancestor.

     + At any time during  execution,  the  type  of  the  object
       associated  with  a  non-void entity is called the dynamic
       type of the entity and written Dx.  The second basic  type
       rule  (section  3.2)  guarantees  that  the  dynamic  type
       conforms to the static type.

     + The definition of entities includes the predefined  entity
       Result,  denoting the result of a function. To resolve any
       ambiguity, the function name will be used in this case  as
       name for the entity.

     Aliases is introduced through two  binary  relations.  First
entity  y is said to be assignable to entity x if and only if any
of the following assignments appears in the system:

A.1
     + x := y

A.2
     + x := b.y (t) for any b and t.

     In the second case, the relation  holds  not  only  for  the
entity y declared in Sb but also to the corresponding entities in
all the descendants of this class.

     Next, y is said to be bindable to x if and only  if  one  of
the following calls appears in S:

B.1
     + a.r (y)

B.2
     + a.r (b.y(t))

     In both cases the relation holds whenever x  is  the  formal
argument  for  r  in any descendant of Sa; in the second case the
relation extends to all y in any descendant of class Sb.

     We say that y is a direct alias for x if y is assignable  to
x or bindable to x.

     Aliases (x) may  now  be  defined  by  transitive  reflexive
closure:  for  any  entity x, Aliases (x) is the set containing x
itself and all entities y such that, for some z, y  is  a  direct
alias for z and (recursively) z belongs to Aliases (x).

     The above definitions consider all assignments  and  routine
calls present in the system regardless of whether they may or may
not be executed in actual runs of the system. So no flow analysis
will  be  needed  to  enforce the type rule; as announced in 4.1,
this will mean a pessimistic policy, although a less  pessimistic
one  than  policies 1 and 2. (The inclusion of all descendants of
Sa and Sb in the above rules is also pessimistic;  the  aim  here
was  to  keep  the  definitions  simple at no significant loss of
useful systems. The interested reader may  wish  to  examine  how
these conditions can be relaxed.)



6 IMPLEMENTING THE RULE


[This section is only relevant for readers who are interested  in
the details of the incremental verification mechanism. Others are
invited to proceed to the last section.]

     As introduced so far, the  computation  of  Aliases (x)  for
every  entity of a system does not take into account the order of
compilation.   As  discussed  in  section   2.4,   an   essential
requirement   is   the  ability  for  the  automatic  incremental
compilation mechanism to check a new  class  without  re-checking
the  classes  on which it depends - its suppliers and ancestors -
if  they  have  already  been  checked.   Although   the   Eiffel
incremental  recompilation algorithm is not in the public domain,
we may outline the strategy here.

     The first observation is that case B.2, which seems the most
formidable   one,   can   safely   be  ignored.  By  conceptually
introducing a new entity z we can rewrite the corresponding  call
as
     z := b.y(t);
     a.r (z)

making B.2 a combination of A.2 and B.1.

     The  incremental  compilation  requirement   precludes   the
straightforward implementation of type checking, whereby we would
compute Aliases (x) for every entity x, and  then  examine  every
feature call x.f (z) to see if it satisfies the type rule.  True,
this is appropriate for A.2: whenever  this  case  yields  a  new
direct  alias  y  for  x,  then the class of x is a client of the
class of y, so that the set Aliases (x)  may  be  updated  in  an
order  compatible  with the compilation order.  But this does not
work for case B.1, where it is in fact the class of y which is  a
client of the class of x. (In case A.1 x and y belong to the same
class.)

     As a consequence we must adopt a mixed strategy; the sets to
be  examined  must  be  computable  and  adaptable  in  an  order
compatible  with  the  dependency   relation   (and   hence   the
compilation order). For every entity x, these sets will be:

     + Remote_aliases (x), the set of y for which A.2 holds;  for
       convenience, this set also contains x.

     + Required_exports (x), the set of routines r which must  be
       exported  by  the class of x because the system contains a
       call z.f (t) for some z such that x is in Aliases (z).

     + Required_types (x), the set of types C  to  which  the  Sx
       must  conform  because the system contains a call z.f (t),
       where f requires a formal argument of type C, for  some  z
       such that x is in Aliases (z).

     The strategy to follow conceptually involves four steps  for
each new class.

     Step 1 initializes the above sets for each entity x  of  the
class:  the first set is initialized to the set containing just x
itself; the second, to the set of f such that x.f  (...)  appears
in  the  class;  the  last,  to  the  types  of the corresponding
arguments.

     Step  2  propagates  aliasing  information  to  descendants.
Whenever  a  new class HEIR is defined as heir to an existing one
PARENT, step 2 performs the following for every entity x of  HEIR
(where x' is the corresponding entity in PARENT):

     +    Remote_aliases (x)    :=    Remote_aliases (x)        U
       Remote_aliases (x')

     +   Required_exports (x)   :=   Required_exports (x)       U
       Required_exports (x')

     +    Required_types (x)    :=    Required_types (x)
       Required_types (x')

     This step implements the rule given in the previous  section
for both the ``assignable'' and ``bindable'' relations: any alias
of an entity of class PARENT is considered to be also an alias of
the corresponding entities in descendants of PARENT.

     For each new class, step  3  will  perform  the  incremental
computation  of  the three above sets and step 4 will verify that
these sets satisfy the type rule. As will be  seen  below,  these
two steps may be performed simultaneously on each class.

     Step 3, when encountering any of the first  three  types  of
potential  aliasing  introduced  above,  must  update the sets as
follows:

A.1
     +   Required_exports (y)   :=   Required_exports (y)       U
       Required_exports (x)

       Required_types (y)     :=     Required_types (y)
       Required_types (x)

A.2
     +    Remote_aliases (x)    :=    Remote_aliases (x)        U
       Remote_aliases (y)

B.1
     +   Required_exports (y)   :=   Required_exports (y)       U
       Required_exports (x)

       Required_types (y)     :=     Required_types (y)
       Required_types (x)

     In each of these cases the updating has been chosen so  that
it only affects some sets A (x) for an entity x in the new class,
not in its (possibly old) suppliers.

     Using  the  above  updates,  the  given  sets  are  computed
incrementally  in  an  order which is compatible with inter-class
dependencies, so that the computation may be integrated with  the
automatic compiling mechanism.

     Once this has been done, step 4 establishes  the  type  rule
for  every  new  class C by checking the following two properties
for every entity x in C:

P1
     + Every routine r in Required_exports (y), for  every  y  in
       Remote_aliases (x), is exported by C.

P2
     +  Sa  belongs  to   Required_types (y)   for   all   y   in
       Remote_aliases (x).

     In practice, step 4 may be performed at  the  same  time  as
step 3 on each class C. Property P1 may be verified incrementally
by  checking  in  cases  A.1  and   B.1   that   r   belongs   to
Required_exports (y).   P2  is verified incrementally by checking
in cases A.1 and B.1 that Sa belongs to Required_types (y).

     This  incremental  checking  assumes   that   some   initial
verification  has been integrated with steps 1 and 2. The details
are  easily  filled.  It  is  interesting  to  note   that   this
initialization  of the complete checking process is precisely the
set of verifications performed by the  current  Eiffel  compiler,
reflecting  the  first and second rules described in section 3.2.
As this discussion as shown, the full  checking  strategy  is  an
extension to this partial approach.



7 ASSESSMENT


To conclude, it is useful to examine a few possible objections to
the rules and enforcement strategy described above.

     A first possible objection is that the algorithm sketched in
the  previous  section  is  too  difficult  to integrate with the
current Eiffel compiler. Of course, the only way to  refute  this
objection  would be to present the implementation, which is still
some time away. The objection  can  only  be  made,  however,  by
someone  who  does  not realize the sophistication of some of the
tasks already performed  by  the  current  Eiffel  compiler  (for
example the implementation of repeated inheritance).

     A related objection addresses the cost of the new  checking,
especially in space. The precise impact needs to be assessed more
carefully,  but  I  believe  the  checking  can   reasonably   be
integrated  within  the  normal  compiling mechanism. If not, the
mechanism would still be available as a  compiler  option  a`  la
Lint, to be applied to systems at validation time.

     Perhaps the most  serious  objection  has  to  do  with  the
relative  complexity  of the mechanism described in the preceding
section. But this mechanism is of no interest to normal users  of
the  language;  only  implementors  (and  others  doubtful of the
soundness  and  enforceability  of  the  type  rules)   need   to
understand it. Eiffel programmers should only learn the type rule
of section 5.3. This rule is only slightly more complex than  the
old  type rule (first rule of section 3.2), which implies it; the
advantage of the new rule should be clear to programmers  reading
a  simplified  form  of the ``rationale'' section of this article
(4). An attractive property of the incremental strategy described
in  the  last  section is that it appears to make it possible for
the checker to produce clear and precise error messages,  of  the
form
     ``In line m, feature f is applied to x; however, because  of
     the  assignment on line n, x might dynamically be of type D,
     which does not export f.''

     If, as seems to be the case, the Eiffel compiler  can  reach
this  level  of  sophistication,  then  we will have achieved our
goal. The purpose of a typed language is the same as the  purpose
of  the  programming  environment  of which it is a part: to help
programmers, not stand in their way. The  sole  purpose  of  type
restrictions  is  to boost programmer productivity by enabling an
automatic checker to catch errors that would be difficult  for  a
human being to detect.

     The liberal typing policy designed for Eiffel (``policy 3'')
was  designed to make programmers' life easier. The type failures
to  which  it  may  lead  are  results  of  abnormal  situations.
Detecting  such  situations  statically  is  a tricky and tedious
business;  as  any  such  business,  it  should  be  handled   by
computers, not by humans.



Acknowledgments

     I am deeply  indebted  to  all  the people  who  contributed
to the type debate, including  those  with  whose  conclusions  I
disagree, for encouraging me to clarify the matter  of  types  in
Eiffel.   As  far  as  I  can  remember, Mike Ladner from Schmidt
Associates was the first to point out the  problems  clearly,  in
private discussions with me. Apart from Philippe Elinck's student
thesis, Pierre America was the first to discuss them in print (in
his  review  of  my book), and his article, together with William
Cook's  article,  may  still  be  the  best  exposition  of   the
``opposing''  view.   I also thank Ralph Johnson for sharing with
me (in  private  mail)  some  useful  insights  gained  from  his
experience with Typed Smalltalk.

[1]  Pierre  America,  "Review   of   `Object-Oriented   Software
     Construction'", Science of Computer Programming, 1989.

[2]  Luca Cardelli, "A Semantics  of  Multiple  Inheritance",  in
     Semantics  of  Data Types, ed. Gilles Kahn, David B. McQueen
     and Gordon Plotkin, pp. 51-67,  Springer-Verlag,  Berlin-New
     York, 1984.

[3]  William Cook, "A  Proposal  for  Making  Eiffel  Type-Safe",
     ECOOP Conference, 1989.

[4]  Philippe  Elinck,  "De   la   Conception-Programmation   par
     Objets",  Me'moire  de  Licence en Informatique (BS Thesis),
     Universite' Libre de Bruxelles, Brussels, 1988.

[5]  Bertrand  Meyer,  Object-Oriented   Software   Construction,
     Prentice-Hall, 1988.

[6]  Bertrand Meyer, "Eiffel:  The  Language",  Technical  Report
     TR-EI-17/RM,  Interactive  Software  Engineering Inc., Santa
     Barbara  (Calif.),  August  1989.   (To  be   published   by
     Prentice-Hall in 1990.)

[7]  Bertrand Meyer, An Introduction to the Theory of Programming
     Languages, Prentice-Hall, 1989.  To appear.

[8]  Bertrand Meyer, "Eiffel: An Introduction", Technical  Report
     TR-EI-3/GI,  Interactive  Software  Engineering  Inc., Santa
     Barbara (Calif.), 1986 (Last revision: 1990).

[9]  Craig Schaffert, Topher Cooper, Bruce  Bullis,  Mike  Kilian
     and  Carrie  Wilpolt,  "An  Introduction to Trellis-Owl", in
     OOPSLA '86 Conference Proceedings, Portland  (Oreg.),  Sept.
     29-Oct.  2,  1986,  pp.  9-16,  1986.  (Published as SIGPLAN
     Notices, 21, 11, Nov. 1986.)
-- 
-- Bertrand Meyer
bertrand@eiffel.com