Re: Mixing OO and DB

From: David BL <davidbl_at_iinet.net.au>
Date: Wed, 20 Feb 2008 18:27:28 -0800 (PST)
Message-ID: <e4dbbf54-7d74-44d4-8000-30b967088805_at_h25g2000hsf.googlegroups.com>


On Feb 20, 11:32 pm, Robert Martin <uncle..._at_objectmentor.com> wrote:
> On 2008-02-19 22:14:36 -0600, David BL <davi..._at_iinet.net.au> said:
>
>

>
>
>
> > On Feb 19, 11:18 pm, Robert Martin <uncle..._at_objectmentor.com> wrote:
> >> On 2008-02-18 21:09:48 -0600, David BL <davi..._at_iinet.net.au> said:
>
> >>> I have a more specific understanding of LSP. Let an object mean a
> >>> variable that has identity, state and behavior, but isn't assumed to
> >>> hold a value. If S,T are object types where S is a subtype of T then
> >>> LSP states that if q(x) is a property provable about objects x of type
> >>> T. Then q(y) should be true for objects y of type S.
>
> >>> In my mind LSP is inappropriate for value-types because it is
> >>> concerned with variables rather than values.
>
> >> Sorry to butt in but Liskov didn't make a distinction about
> >> value-types. The substitution principle simply says that every program
> >> P that works with T should also work with S.
>
> > I think you are oversimplifying what substitution means.
>
> > What exactly do you mean by "program"?
>
> I was using Liskov's words.
>
> > It sounds like you refer to
> > the entire source code for an application, but that would include the
> > code that defines T and S.
>
> Let's restrict P to the set of code that directly uses, but does not define, T.
>
>
>
> > Moving on, you say that *every* program P that works with T should
> > also work with S. Suppose S' is another subtype of T. Let program P
> > be the following
>
> > // declaration
> > void foo(T* x);
>
> > // call
> > S' s;
> > foo(&s);
>
> > We cannot replace T with S in this code, because the cast from S'* to
> > S* isn't valid. So presumably you mean something else by "works
> > with". What exactly? Can you formalise that?
>
> S is any and every subtype of T. S' is in the class of S.

The "class of S"? What is that?

No, S is a given subtype of T, S is *not* the set of all subtypes of T.

> Remember, Liskov's rule was an attempt to define what S was. In
> essense she said that subtypes are those things that are substitutable.
>
> What this means, of course, is that some S may be a subtype of T for
> one particulr P but not for another.

I thought you were saying before that S is a subtype of T if substitutability holds for all P. Now you are saying the subtype relationship is a function of P. Which is it?

> Liskov's rule implies that being
> a subtype is not intrinsic, it is an attribute evaluated by the user.
> It is P that shows that S is a true subtype of T. S and T cannot prove
> their relationship without P.

>
> > Substitution can mean lots of different things. Eg it can relate to
> > implicit compile time genericity in the source code, substitution of
> > in-parameters, out-parameters, or type coercions of pointers. It
> > follows that there are alternative notions of sub-typing (and
> > inheritance as well).
>
> Of course.
>
>
>
>
>
>
>
> > C++ only makes a feeble attempt at supporting value types. For
> > example when one writes
>
> > class B {};
> > class D : public B {};
>
> > the C++ compiler allows conversions as follows
>
> > D x;
> > B y = x; // ok : "value conversion"
>
> > D* px;
> > B* py = px; // ok : "pointer conversion"
>
> > D** ppx;
> > B** ppy = ppx; // Compiler error
>
> > It is worth asking why the first two are considered valid and the
> > third is not.
>
> Lots and lots was written about why that particular conversion is not
> allowed. It is similar to the arguments for why a vector<S> is not a
> subtype of a vector<T>. In the end the argument always boils down to
> LSP. Some user of ppx might cast it to a B** and load it with a D'**.
> Poor ppx, a D**, now holds a D'**. Bad. A substitution violation.

Yes, well the last case was only put in for contrast. My point was that C++ allows for two types of conversions. One happily performs value conversions by slicing away state, the other happily allows you to upcast pointers to variables. Taken together this is a little confused.

> > Also, how does this relate to an LSP notion of
> > subtyping? In the interests of being precise I like to associate LSP
> > specifically with the pointer conversion.
>
> > This pointer conversion is unreasonable for value-types. It is not
> > at all surprising that C++ programmers don't think a circle is an
> > ellipse.
>
> C++ programmers know that a circle is an ellipse.

Sure they do. I was being facetious. I am a C++ programmer.

> They also know that
> a program that represents a circle is not necessarily a program that
> represents an ellipse -- for the same reason that vector<circle> is not
> a subtype of vector<ellipse>.

How does a program represent an ellipse? I would prefer it if you could be more careful with your terminology!

>
> > Eg suppose Circle subtypes Ellipse then
>
> > Ellipse e;
> > Circle c;
> > Circle* px = &c;
> > Ellipse* py = px; // pointer conversion
> > *py = e; // oops
>
> As I said. Exactly the same reasoning. A reference to S is not a
> subtype of a referenct to T.

What has saying that a reference to S is not a subtype of a reference to T got to do with it? I thought that was only relevant to the B**, D** example.

There is the following rule in C++

    S is subtype of T => allow implicit upcast S* to a T*

I'm saying this rule is inappropriate for value types, because it is incompatible with Circle subtyping Ellipse. This is one of the main reasons why I say C++ doesn't support value types correctly.

I generally like the way C++ doesn't tend to force a particular methodology on you. However, its approach to subtyping only caters for object-types.

> >> It is easy to envison a T and S that are pure value types. Imagine,
> >> for example, an Address type, and a subtype that supports 9 digit
> >> zipcodes. Any program written for Addess can safely use the subtype.
>
> >> BTW, while it is true that real numbers are a subtype of complex
> >> numbers (i.e. any algorithm that works when the I part is non-zero
> >> should work when the I part is zero)
>
> > What does that mean?
>
> It means that any valid algorithm using complex number remains valid
> when the imaginary part of all the complex numbers is zero. i.e. it
> remains valid for real number.

> > For example consider the following function to find roots of az^2 + bz
> > + c = 0, where it is assumed 'a' is non zero.
>
> > pair<Complex,Complex> FindRoots(Complex a, Complex b, Complex c);
>
> > Equations with real valued coefficients can easily have complex valued
> > roots. I think you need to elaborate on what you said above.
>
> Valid point. I should have been specific that the complex numbers were
> used as inputs to the algorithm.

This is the sense in which Date says that Real can inherit all operations from Complex. Basically, for a given algorithm we only allow conversions of in-parameters that are passed by value.

Another example: The integers can inherit real-valued multiplicative inverses from the reals.

> >> , it is not true that Real inherits
> >> from Complex.
>
> > C.Date discusses a sense in which Real can subtype Complex, and Real
> > can inherit all operations from Complex.
>
> We can define inheritance to mean anything we want. However, in the OO
> sense, a complex number composed of two real numbers cannot be the base
> class of the real numbers. If it were, then every real number would
> have two real numbers inside it...

But why would anyone want to treat real or complex numbers "in the OO sense". These should obviously be treated as value types not object types.

If you want to treat value types as object types, you will *always* find that subtyping is inappropriate amongst value types. I strongly suspect many opportunities for code reuse are lost. The poor C++ programmer has to compensate by using templates all over the place, because the program doesn't properly express a physical-logical separation between an (abstract) value type and its possible encodings.

> > I don't consider LSP to be applicable to value-types. LSP is relevant
> > to OO, and objects are variables not values. IMO the LSP notion of
> > subtyping implies that a reference to a variable (ie object) can be
> > passed to a function that expects a reference to a variable of any
> > supertype. For value-types, this implies that subtype and supertype
> > must represent the exact same set of values.
>
> struct Address {
> string street, city, state;
>
> };
>
> struct AddressWithZip : Address {
> string zip;
>
> }
>
> void f(Address a);
>
> AddressWithZip az;
> f(az); // fine. Slices off the zip. Works great.

I find that extremely ugly. It only takes another small step for the following

    struct Square { int width; };
    struct Rectangle : Square { int height; }

    void f(Square s);

    Rectangle r;
    f(r); // fine. Slices off the height. Works great.

> > Here is a "proof": One expects variables of the supertype to support
> > assignment to any values of the supertype, hence we deduce that a
> > subtype is only allowed to contain a superset of all the values in a
> > supertype.
>
> Fine so far. subtype values must have every variable that the supertype has.
>
> > Also the function can read the value of the variable as a
> > value in the supertype, which shows that the values of the subtype
> > must be a subset of the values in the supertype.
>
> I didn't follow that one. Subtypes cannot contain subsets of variables
> of the supertype.

I said values not variables. By definition a value type is a set of values plus operators on those values. Let S be a subtype of T. In C ++ an S* can be upcast to a T*. Let v denote some value in S stored in variable x (of type S). We can take the address of x, upcast it to a T* and dereference (to read the value v) and assume it is of type T. Therefore we have shown that upcasting pointers implies that the values in a subtype must be a subset of the values in a supertype.

Now of course this proof assumed that v wasn't sliced, so I guess you will say the proof is flawed. However I find the idea that the compiler will silently slice away part of a value and allow you to reinterpret it as something else extremely suspicious. It could only be justified for value types that are merely tuples - ie that cannot hide their components and are referred to as non-scalar types. However the idea that a tuple subtypes one of its components seems plain stupid to me. Received on Thu Feb 21 2008 - 03:27:28 CET

Original text of this message