From: S Perryman on
"Laurent Deniau" <laurent.deniau(a)cern.ch> wrote in message
news:dv6v27$o6o$1(a)sunnews.cern.ch...

> Dmitry A. Kazakov wrote:

>> On Mon, 13 Mar 2006 18:30:03 +0100, Laurent Deniau wrote:

>>>In fact MI plays a role on both sides: on the dispacher side because
>>>object's offset adjustment must be stored in the cache but also on the
>>>call-site side because the address of the object must be passed to the
>>>dispatcher to apply this offset adjusment, and this (roughly) double the
>>>code generated to dispatch the message.

>> I wouldn't count it on account of dispatch, because even if the target
>> is
>> statically known, you will need to shift the address. In fact, it is type
>> conversion penalty.

> This is on the side of the dispatcher where I agree, object must be
> shifted according to their (most derived) types and the expected types of
> the selected method. For the caller side, I am refering to the fact that
> the address of the objects have to be passed.

> for SI, the dispatch looks like:

> return ((generic_type)find_method(generic, obj1, obj2)) (obj1, obj2);

> but for MI, the dispatch looks like:

> generic_type mth = (generic_type)find_method(generic, &obj1, &obj2);
> return mth(obj1, obj2);

> where find_method returns a pointer to the selected method to invoke.

CLOS forms a topological ordering of the inheritance lattice does it not.

How does this affect or compare to op selection (code size, performance
etc) for a single inheritance hierarchy of similar size ??


Regards,
Steven Perryman


From: Dmitry A. Kazakov on
On Tue, 14 Mar 2006 18:37:52 +0100, Laurent Deniau wrote:

> Dmitry A. Kazakov wrote:
>> On Mon, 13 Mar 2006 18:30:03 +0100, Laurent Deniau wrote:
>>>Dmitry A. Kazakov wrote:
>>>>If dispatching tables are associated with functions
>>>>then it is the dimension of the table to determine the penalty.
>>>>MI, alone, should play no role here, as a guess.
>>>
>>>In fact MI plays a role on both sides: on the dispacher side because
>>>object's offset adjustment must be stored in the cache but also on the
>>>call-site side because the address of the object must be passed to the
>>>dispatcher to apply this offset adjusment, and this (roughly) double the
>>>code generated to dispatch the message.
>>
>> I wouldn't count it on account of dispatch, because even if the target is
>> statically known, you will need to shift the address. In fact, it is type
>> conversion penalty.
>
> This is on the side of the dispatcher where I agree, object must be
> shifted according to their (most derived) types and the expected types
> of the selected method. For the caller side, I am refering to the fact
> that the address of the objects have to be passed.

I didn't understand it. Why do you need addresses? To make dispatch
decision you need type tags of the objects, not the objects themselves.

[ If I designed an OO language I'd try not to embed tags into objects, but
pass them independently in a dope. This could eliminate contamination of
objects by type tags, which becomes especially disastrous for MI. Type tags
could also support views, i.e. a tag could tell - this is B viewed as A. ]

>>>I
>>>expect that when you say "For safety reasons" you are referring to the
>>>constraints applied to parameters (covariance / contravariance of
>>>types). But these constraints do not apply to multimethod dynamic
>>>dispatch because the overriding mechanism differs from static type
>>>systems. For example, as you say, in a static type system like in C++ or
>>>Java, it is important that f(B,B), f(B,C), f(C,B) use different slots in
>>>the virtual table(s) and do not override each other (limiting somehow
>>>the subtyping polymorphism), while in dynamic dispatch they use the same
>>>slot: f(B,B) overrides f(B,C) which overrides the less specialized
>>>method f(C,B), if we consider my 2nd list of linearization. This is
>>>still safe since the dispatcher always propagates the most derived type
>>>i.e. A.
>>
>> Hmm, I don't think that there is some special "dynamic" way. Whether f(B,B)
>> may override f(B,C) depends solely on the relations between B and C. It
>> depends on whether B is a specialization or generalization if C and on the
>> parameter mode (in, out or in out.) The problem is that substitutability is
>> not checkable either statically or dynamically, so the language should
>> rather rely on the programmer here.
>
> No, the fact that B inherits from C is checkable at runtime (but not at
> compile time of course). In fact multimethod dynamic dispatch is safer
> than say Java interfaces since the method selection is done by the
> dispatcher which ensures de facto the correctness of the objects types.

I didn't mean type correctness, I did substitutability. You can check
types, but you cannot map substitutability onto types, only partially. This
is why famous Circle-Ellipse controversy arise. Consider B, a
specialization of C. Then not every C is B. This means that Foo having B as
an out parameter (or as a mutator in out) cannot be safely inherited in C,
because it might change C to something that isn't C. Similarly
generalization has problems with in parameters. And there are lots of
practical cases where C is nether generalization, nor specialization of B.
That means that all methods [= subprograms covariant in the corresponding
parameters] are potentially unsafe to inherit. This problem need to be
addressed.

> While in Java interface, the programmer has to check the type of the
> extra objects passed to the method since only the first one is truly
> checked by the method dispatch, the static types of the other objects
> being lost (e.g. compareTo, equalTo, lessThan, copyTo).

This is because of design fault of Java, which does not have multiple
dispatch. Many OO languages suffered from prefix notation.

[ X.Foo() to be considered harmful! (:-)) ]

> C++ on his side restricts subtyping polymorphism because static typing
> cannot ensure the type correctness for such overriding.

No. Formally, it is type correct. The second parameter is contravariant in
C++. The problem is rather mental, the programmer uses a contravariant
parameter as if that were covariant. The choices the language makes shall
be safe. That is: all parameters and the results should be covariant by
default. Prefix notation should be forbidden for multimethods. No "this",
"self" rubbish.

> This is a
> tradeoff between flexibility and safety, but it has parametric
> polymorphism to get round this restriction.

Yes, parametric polymorphism is a crutch for the languages with
underdeveloped types systems... (:-))

--
Regards,
Dmitry A. Kazakov
http://www.dmitry-kazakov.de
From: Laurent Deniau on
S Perryman wrote:
> "Laurent Deniau" <laurent.deniau(a)cern.ch> wrote in message
> news:dv6v27$o6o$1(a)sunnews.cern.ch...
> CLOS forms a topological ordering of the inheritance lattice does it not.
>
> How does this affect or compare to op selection (code size, performance
> etc) for a single inheritance hierarchy of similar size ??

With SI, no DAG. Therefore, CLOS, Loops, C3 or Lin2 (by chronology of
invention ;-) ) linearization are trivial for SI and equivalent do
depth-search in the list of super classes. But it should not affect much
code size nor speed performance since they apply only when a method
loaded to the cache, not when it is looked.

My case with tree only MI, no DAG, is also simple and requires only
depth-first search and no linearization.

But it is true that the complexity of these linearization is O(n^3) for
a hierarchy of n classes while without DAG it is only O(n) and it may be
of importance for fat class hierarchies (very uncommon in dynamic OO
languages except maybe in Slate ;-) ).

a+, ld.
From: Laurent Deniau on
Dmitry A. Kazakov wrote:
> On Tue, 14 Mar 2006 18:37:52 +0100, Laurent Deniau wrote:
>>This is on the side of the dispatcher where I agree, object must be
>>shifted according to their (most derived) types and the expected types
>>of the selected method. For the caller side, I am refering to the fact
>>that the address of the objects have to be passed.
>
>
> I didn't understand it. Why do you need addresses? To make dispatch
> decision you need type tags of the objects, not the objects themselves.

Right. But in my orignal post I have specified that I am implementing MI
using aggregation like in C++, and not with a composition (i.e. array of
slots) like in most dynamic OO and Prototype-based langages where
objects addresses shifts are meaningless. But these langages need to use
setters/getters methods to access object members, including inside their
own class methods.

Therefore like in C++, I need to *shift* the objects reference (e.g.
'this') by some offset before the dispatcher invokes the method on them
and this is done in the find_method dispatcher which performs the lookup
and adjusts the objects. That is why the address of objects must be
passed with the consequence of creating intermediate temporaries (a bit
long to explain why here, but in a word it is related to how generics
are implemented for MI) and multiple reload of objects addresses (since
they may have changed).

> [ If I designed an OO language I'd try not to embed tags into objects, but

Embedding tags into objects have advantages beyond the scope of this
discussion (e.g. predicate dispatch).

> pass them independently in a dope. This could eliminate contamination of

This is only possible for either SI, either MI using with slots for
object's fields (or equivalent composition).

> objects by type tags, which becomes especially disastrous for MI. Type tags
> could also support views, i.e. a tag could tell - this is B viewed as A. ]

The simplest things is that the object's tag views the class and the
class has direct links (pointers) to the super classes (Assuming that
classes are also objects (i.e. instance of another class).

[...]

> I didn't mean type correctness, I did substitutability. You can check
> types, but you cannot map substitutability onto types, only partially. This
> is why famous Circle-Ellipse controversy arise. Consider B, a
> specialization of C. Then not every C is B. This means that Foo having B as
> an out parameter (or as a mutator in out) cannot be safely inherited in C,
> because it might change C to something that isn't C. Similarly
> generalization has problems with in parameters. And there are lots of
> practical cases where C is nether generalization, nor specialization of B.
> That means that all methods [= subprograms covariant in the corresponding
> parameters] are potentially unsafe to inherit. This problem need to be
> addressed.

Right, I agree that it is not only related to the type system. This is a
very interesting point that many OO programmers do not catch ;-) and
which is related to the fundamental difference between late binding
(e.g. C++ virtual) and dynamic dispatch (e.g. Smalltalk message), that
is how overriding is implemented:

- late binding needs to know which method to invoke within a given class
*definition* at compile time to store the target method address in a
table of pointers (static vtable or compressed multidimmensionnal static
tables). In a derived class definition a new table is created and either
the same method address (no override) either new method address
(overridden) is stored. Therefore substitutability must be constrained
for any derived class to ensure the validity of this construction
otherwise it may be broken as you pointed out. The constraints are
managed by the type system which allows only covariance of returned
types and contravariance of parameters types to ensure correct (but
restricted) substitability. But again, these constraints are related to
the choices of the implementation which focus more on efficiency than on
flexibility.

- dynamic dispatch is the opposite and do everything at runtime. No
static construction, no type constraint (types? ;-) ), maximum
flexibility, always safe, but less efficient... You can take as a
picture that dynamic dispatch works with dynamic types (polymorphic
types) like *overloading* works with static types. That is with dynamic
dispatch (Circle <: Shape):
virtual Circle Circle::intersect(Circle,Circle);
would overrides
virtual Shape Shape::intersect(Shape,Shape);
while late binding like in C++ (even if considering only
single-dispatch) does not allow the override and create a new slot in
the vtable.

In fact, the philosophy of the two overriding mecanisms can be
summarised by a question (should I had started by them?):

late binding: given a method, what are the types it can accept and
return? (static construction must managed dynamic types)

dynamic dispatch: given type(s), what is the most specialized applicable
method? (dynamic types select static construction)

a+, ld.
From: Dmitry A. Kazakov on
On Wed, 15 Mar 2006 12:18:16 +0100, Laurent Deniau wrote:

> Dmitry A. Kazakov wrote:
>> On Tue, 14 Mar 2006 18:37:52 +0100, Laurent Deniau wrote:

>> pass them independently in a dope. This could eliminate contamination of
>
> This is only possible for either SI, either MI using with slots for
> object's fields (or equivalent composition).

You will go this path anyway, when it will come to DAG. (:-)) What free
flying tags allow is dispatching methods for very small objects, like
Boolean. [The goal is a pure OO statically typed language.]

>> I didn't mean type correctness, I did substitutability. You can check
>> types, but you cannot map substitutability onto types, only partially. This
>> is why famous Circle-Ellipse controversy arise. Consider B, a
>> specialization of C. Then not every C is B. This means that Foo having B as
>> an out parameter (or as a mutator in out) cannot be safely inherited in C,
>> because it might change C to something that isn't C. Similarly
>> generalization has problems with in parameters. And there are lots of
>> practical cases where C is nether generalization, nor specialization of B.
>> That means that all methods [= subprograms covariant in the corresponding
>> parameters] are potentially unsafe to inherit. This problem need to be
>> addressed.
>
> Right, I agree that it is not only related to the type system. This is a
> very interesting point that many OO programmers do not catch ;-) and
> which is related to the fundamental difference between late binding
> (e.g. C++ virtual) and dynamic dispatch (e.g. Smalltalk message), that
> is how overriding is implemented:
>
> - late binding needs to know which method to invoke within a given class
> *definition* at compile time to store the target method address in a
> table of pointers (static vtable or compressed multidimmensionnal static
> tables). In a derived class definition a new table is created and either
> the same method address (no override) either new method address
> (overridden) is stored. Therefore substitutability must be constrained
> for any derived class to ensure the validity of this construction
> otherwise it may be broken as you pointed out. The constraints are
> managed by the type system which allows only covariance of returned
> types and contravariance of parameters types to ensure correct (but
> restricted) substitability. But again, these constraints are related to
> the choices of the implementation which focus more on efficiency than on
> flexibility.
>
> - dynamic dispatch is the opposite and do everything at runtime. No
> static construction, no type constraint (types? ;-) ), maximum
> flexibility, always safe, but less efficient... You can take as a
> picture that dynamic dispatch works with dynamic types (polymorphic
> types) like *overloading* works with static types. That is with dynamic
> dispatch (Circle <: Shape):
> virtual Circle Circle::intersect(Circle,Circle);
> would overrides
> virtual Shape Shape::intersect(Shape,Shape);
> while late binding like in C++ (even if considering only
> single-dispatch) does not allow the override and create a new slot in
> the vtable.

Hmm, this is not determined by the time of bindings. For example, the above
is legal in Ada 95, which is statically typed:

type Shape is tagged ...;
function Intersect (Left, Right : Shape) return Shape;

type Circle is new Shape with ...;
function Intersect (Left, Right : Circle) return Circle;
-- This overrides

Moreover, in Ada it is mandatory to override Shape.Intersect, because it is
covariant in all parameters. The compiler will refuse to inherit it.

In my view it is possible to make a statical types system right. Though it
would require much work.

> In fact, the philosophy of the two overriding mecanisms can be
> summarised by a question (should I had started by them?):
>
> late binding: given a method, what are the types it can accept and
> return? (static construction must managed dynamic types)
>
> dynamic dispatch: given type(s), what is the most specialized applicable
> method? (dynamic types select static construction)

Close, but not quite. In fact, there are two types involved. The concrete
type which might remain unknown until run-time and the class, which is the
set of types containing that potentially unknown type. The class is always
statically known. In a dynamically typed language it is just "all types."
We know in advance that there are [all] types! (:-))

--
Regards,
Dmitry A. Kazakov
http://www.dmitry-kazakov.de