[comp.object] Should a constructor do "work"?

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

    A class is not just a bunch of operations packaged together.
It describes a data type, whose instances must satisfy some consistency
constraints, called the ``class invariant''; in Eiffel the invariant
is part of the class text.

    The class invariant usually contains two kinds of clause: some
clauses characterize the underlying abstract data type and are
common to all its implementations; others only apply to an
implementation, or a group of implementations, and are called
``representation invariants''. This has been discussed in
detail in various publications.

    The worst situation that can ever happen in the execution of an
object-oriented program - the ``mortal sin'' of object-oriented
programming - is to produce an object that does not satisfy
the invariant of its class. This annihilates any hope of reasoning
about the software system, or of understanding what will happen during its
execution. (One guaranteed way of committing the mortal sin is called
static binding, but that is another story.)

    The purpose of having creation operations (constructors) in a class
is to make sure that every object will satisfy its class's invariant at
creation time. This is a formal requirement in Eiffel (Create must
ensure not only its postcondition, if any, but also the invariant).

    Allocating an object without doing the work needed to ensure the
invariant would be a trivial example of the ``mortal sin''.


-- Bertrand Meyer
bertrand@eiffel.com

rick@tetrauk.UUCP (Rick Jones) (07/09/90)

In article <359@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes:
>
> ...
>
>    The purpose of having creation operations (constructors) in a class
>is to make sure that every object will satisfy its class's invariant at
>creation time. This is a formal requirement in Eiffel (Create must
>ensure not only its postcondition, if any, but also the invariant).
>

A question I would like to ask (of anyone, Bertrand especially) is how should
the language or the program design cope with a situation where an object is created, but
its create routine ("constructor") is unable to satisfy the invariant due to some
run-time condition which the program must trap in a non-fatal way.  In Eiffel an
exception could be raised, and caught somewhere else, but that would not in itself
prevent the existence of the invalid object.

The underlying question is whether a constructor should be allowed, under exception
conditions, not to create any object and leave the corresponding reference void.

-- 
Rick Jones					You gotta stand for something
Tetra Ltd.  Maidenhead, Berks			Or you'll fall for anything
rick@tetrauk.uucp (...!ukc!tetrauk.uucp!rick)	     - John Cougar Mellencamp

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

From <519@tetrauk.UUCP> by rick@tetrauk.UUCP (Rick Jones):

> How should
> the language or the program design cope with a situation where an object is created, but
> its create routine ("constructor") is unable to satisfy the invariant due to some
> run-time condition which the program must trap in a non-fatal way.  In Eiffel an
> exception could be raised, and caught somewhere else, but that would not in itself
> prevent the existence of the invalid object.

	If invariants are checked and a creation operation raises
an exception, the target of the Create should remain void,
excluding any creation of an inconsistent object.


	This should definitely be part of the language
specification.
-- 
-- Bertrand Meyer
bertrand@eiffel.com

sjs@roland.ctt.bellcore.com (Stan Switzer) (07/13/90)

The original question is whether it is reasonable for a constructor to
do a great deal of work.  People have answered (correctly) that it
must at least do enough work to guarantee that its class invariant is
satisfied.  Sometimes, though, that can be too expensive and a "lazy
evaluation" approach is in order.

Suppose that the original class invariant is "C".  Let the new
invariant be "!initialized || C."  All the constructor has to do is
set "initialized" to "false" and squirrel away any initialization
parameters.  The price of this is, of course, an extra instance
variable (or so) and that every method has to begin with
"if ( !initialized ) initialize();"

It's not appropriate for every situation, but it can be a quite useful
technique when used wisely.  This is actually a special case of the
polar/cartesian representation problem discussed under "Legitimate
side-effects: and example" in Meyer's *Object-oriented Software
Construction*; in this case one of our representations is just the raw
initialization parameters.

Stan Switzer  sjs@bellcore.com

wex@dali.pws.bull.com (Buckaroo Banzai) (07/18/90)

In article <364@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes:
	   If invariants are checked and a creation operation raises
   an exception, the target of the Create should remain void,
   excluding any creation of an inconsistent object.

I don't understand why this is so.  I agree that it's one way to specify a
language, but surely it's not the only way.

For example, let's say that I create an object, and as part of that
creation, I wish to initialize some of its slots (oops, my CLOS is showing
:-).

Suppose then that some of the initialization fails.  Can't I write a
specification that says "the value will be a list of numbers or NULL"?  Then
when the initialization fails, I can return an object with that particular
slot having a NULL value.

This would fulfill the specification; are you saying that this would not
"raise an exception" in the sense that you mean it above?

--
--Alan Wexelblat
Bull Worldwide Information Systems	internet: wex@pws.bull.com
phone: (508) 294-7485 (new #)		Usenet: spdcc.com!know!wex
"The aim of life, its only aim, is to be free.  Free of what?  Free to
do what?  Only to be free, that is all."

rick@tetrauk.UUCP (Rick Jones) (07/19/90)

In article <WEX.90Jul18120747@dali.pws.bull.com>
		wex@dali.pws.bull.com (Buckaroo Banzai) writes:
>In article <364@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes:
>>	   If invariants are checked and a creation operation raises
>>   an exception, the target of the Create should remain void,
>>   excluding any creation of an inconsistent object.
>
> ...
>
>Suppose then that some of the initialization fails.  Can't I write a
>specification that says "the value will be a list of numbers or NULL"?  Then
>when the initialization fails, I can return an object with that particular
>slot having a NULL value.
>
>This would fulfill the specification; are you saying that this would not
>"raise an exception" in the sense that you mean it above?

This all comes down to being clear about what is and is not a valid state for
the object, i.e. what the invariant condition is.  You have effectively defined
your invariant in the statement above by saying that a null list is a valid
state.  This is quite OK provided that the routines of the class are written to
take account of the possibility of a null list, and so creating such an object
would not raise an exception.

The other approach is to say that your class routines are NOT going to tolerate
a null list, and that you don't allow any such object to exist.  This then
raises the above problem in that a failure to create the list when creating the
containing object is BY DEFINITION an exception condition.  Apart from the
general issue of exception handling, the special case of an exception during
object creation should result in no object being created, and any associated
reference remaining void.

The choice of how to handle the situation is one of design, and one or other
may be appropriate for different problems.  However, I think the ability to
clearly define and correctly handle exception conditions is a very valuable
one, and if properly used can considerably simplify programs.  There is some
statistic about how much of a typical program is devoted to handling error
conditions;  I forget what it is but it's a large number.  This seems to result
from all the code which appears as in-line logic to handle what in reality are
exceptions to normal execution.  What is and is not an "error" gets very
confused.

I am using Eiffel, and planning to make maximum use of it's exception handling
to deal with everything which is not a normal down-the-middle expected
situation (at least at the prototype stage).  This includes things which are
not normally treated as errors, such as "record not found" on file access, or
"wrong entry" at a terminal.  This could be a good subject for discussion;
has anyone any experience of taking this approach to building software?

-- 
Rick Jones					You gotta stand for something
Tetra Ltd.  Maidenhead, Berks			Or you'll fall for anything
rick@tetrauk.uucp (...!ukc!tetrauk.uucp!rick)	     - John Cougar Mellencamp

kim@spock (Kim Letkeman) (07/19/90)

In article <WEX.90Jul18120747@dali.pws.bull.com>, wex@dali.pws.bull.com (Buckaroo Banzai) writes:
| In article <364@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes:
| 	   If invariants are checked and a creation operation raises
|    an exception, the target of the Create should remain void,
|    excluding any creation of an inconsistent object.
| 
| I don't understand why this is so.  I agree that it's one way to specify a
| language, but surely it's not the only way.

It is one of the best ways to avoid introducing a significant increase
in complexity - that being the change from having two states for an
object reference (good or bad) to having three (good, bad, we're not
sure.) 

I've seen a lot of code that attempted to compensate for incomplete
initialization and other subtle failures. Code that treats the world
in a more binary fashion has (in my experience) always been simpler,
faster and just as useful.

| For example, let's say that I create an object, and as part of that
| creation, I wish to initialize some of its slots (oops, my CLOS is showing
| :-).
| 
| Suppose then that some of the initialization fails.  Can't I write a
| specification that says "the value will be a list of numbers or NULL"?  Then
| when the initialization fails, I can return an object with that particular
| slot having a NULL value.
| 
| This would fulfill the specification; are you saying that this would not
| "raise an exception" in the sense that you mean it above?

You're really just saying that your class invariant would allow for
some of the "slots" to be NULL. I.e. a failure during initialization
of some of the slots would not actually violate the invariant. 

If your object is actually useful with these slots NULL, then you are
being completely consistent with Meyer's statement above. If, on the
other hand, your object is not useful (requiring a lot of extra code
to handle special cases where NULL slots exist), then you have
violated Meyer's statement and created an incorrect class invariant.

I believe that an object should function exactly as the interfaces
(methods, features ...) intend from the very moment of creation, or
not exist at all.


-- 
Kim Letkeman    mitel!spock!kim@uunet.uu.net

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

In <WEX.90Jul18120747@dali.pws.bull.com>, wex@dali.pws.bull.com
(Alan Wexelblat?) questions my previous suggestion that

>> 	   If invariants are checked and a creation operation raises
>>    an exception, the target of the Create should remain void,
>>    excluding any creation of an inconsistent object.

by stating

> (...) Suppose then that some of the initialization fails. Can't I write a
> specification that says "the value will be a list of numbers or NULL"?  Then
> when the initialization fails, I can return an object with that particular
> slot having a NULL value.
> 
> This would fulfill the specification; are you saying that this would not
> "raise an exception" in the sense that you mean it above?

But this is precisely the point. The invariant is here to state the
specification.

If ``null'' is an acceptable value, then the invariant
should say so and there is no reason to pass an exception to
the client.  Otherwise the invariant is unsatisfiable and
there should be an exception.

To make this concrete, take the example of an Eiffel class with
two attributes:

	n: INTEGER; 
	l: LIST [STOCK]

and assume we expect that the creation will initialize `n', perhaps from an
argument to the creation operation, and allocate list `l' with a number of
elements equal to `n'. Furthermore, the length of the list should remain
equal to `n' during the lifetime of the object. Then the invariant clause
for the class will say

	invariant

		l.count = n
		
(`count' is the standard name used in the Eiffel libraries for the number
of meaningful elements in a data structure.)

In this case, if the creation operation fails to ensure the invariant (for
example because there is no memory left to allocate `n' list elements for
the requested `n'), then it would be improper and dangerous to create an
inconsistent instance of the class.

If, however, the class designer is prepared to deal with such objects,
he may easily adapt the invariant clause to read (for example):

	invariant

		l.empty or else l.count = n

In which case there is no ``failure'' visible to the outside world.
No inconsistent object has been created.
Assume for example that 

-- Bertrand Meyer
bertrand@eiffel.com

diamond@tkou02.enet.dec.com (diamond@tkovoa) (07/23/90)

In article <372@eiffel.UUCP> bertrand@eiffel.UUCP (Bertrand Meyer) writes:

[moderately long, perfectly correct suggestion about invariants deleted]

>Assume for example that 
>
>-- Bertrand Meyer

Oh no, then what would happen to Eiffel?   :-)

-- 
Norman Diamond, Nihon DEC     diamond@tkou02.enet.dec.com
This is me speaking.  If you want to hear the company speak, you need DECtalk.