April 16, 2008
On 16/04/2008, Bruce Adams <tortoise_74@yeah.who.co.uk> wrote:
>  class A;
>  class B: A;
>  class C: A;
>
>  B == C   // should not compile - unless user defined

I assume you meant

    b == c

where b is an instance of B, and c is an instance of C.

Then you should be supporting my proposal, since that's exactly what it offers.

The /current/ mechanism will allow that to compile, and

    b == c

will be transformed into

   b.opEquals(c)

which will call the function A.opEquals(Object o) with this aliased to a, and o aliased to b. So, what you're basically saying is THE SAME AS I - which is that inheritance is the wrong mechanism for comparisons. We need to stop inheritance from applying to comparisons.

That was only part of my proposal, of course. Other features included:
(*) a default equality test for objects of identical type
(*) that by default, an equality test accept a parameter of the same
type as "this", not Object

and some other cunning stuff about opCmp.

If the only thing you're disagreeing about is the syntax, then as far as I'm concerned, we're agreeing, because the syntax isn't precious to me.


> Constructors and destructors are special kinds of function but they are still functions.

Constructors don't inherit. They are also called indirectly. That is, you never call

    A a = gc.alloc(...);
    a.this(...)

Instead, you do

    A a = new A(...)

which (1) gets some memory from the gc, and then (2) calls the constructor. So yes, it's a "special kind of function but still a function". I'm saying comparisons should also be considered as a "special kind of function but still a function".
April 16, 2008
"Scott S. McCoy" <tag@cpan.org> wrote in message news:1208252695.9854.144.camel@thinkpad.leapfrog.local...
> Sorry, this link :-P
>
> http://codepad.org/wxG1AKBe

Yes, but then you are not overriding the base method.  This (currently) leaves a virtual opEquals(Object) method in the vtable which throws an exception:

class A
{
}

class B : A
{
   int opEquals(B b) { return 1;}
}

void main()
{
   A a = new A;
   a == new A; // ok
   a == new B; // ok
   a = new B;
   a == new A; // throws exception.
   a == new B; // throws exception.
}

-Steve


April 16, 2008
"Yigal Chripun" wrote
> After reading the documentation for operator overloading I think there is a much simpler solution to this problem. have a look at the definition of Object in phobos - it contains opEquals and opCmp and that's it. No other opSomthing are defined there.
>
> So basically, I propose just removing those two operators from Object.
>
> if opEquals is not defined for the specific object than the compiler should default to identity comparison (as if you've used "is"), otherwise it'll use the defined method and return bool.

In fact, that is the default implementation of opEquals in Object :)

> if opCmp is not defined than the compiler will issue an error for trying to compare incomparable types.  otherwise, let it work the same as today.

I agree with you on this, but I think it can be solved without changing the compiler.

The default opCmp today is:

int opCmp(Object o) { return this !is o;}

Which I think is very incorrect.  If you are depending on opCmp being a well-defined order, for instance, in order to sort data, then your code may incorrectly assume that it can sort any object type.  IMO, opCmp should be defined in an interface, and those classes which implement it should implement that interface.  This would prevent trying to compare something that doesn't implement the opCmp method.

-Steve


April 16, 2008
On 16/04/2008, Steven Schveighoffer <schveiguy@yahoo.com> wrote:
>  IMO, opCmp should be
>  defined in an interface, and those classes which implement it should
>  implement that interface.  This would prevent trying to compare something
>  that doesn't implement the opCmp method.

Of course, you realise that just because you declare "MyClass implements opCmp()", it doesn't necessarily follow that all possible subclasses of MyClass, which may be declared by other programmers, in other modules, at any time in the future, will also implement opCmp(). Nor can you be sure that your opCmp() function will be appropriate for all such subclasses.

And yet, that's exactly what implementing such an interface would tell the compiler!

Hence the "fresh look" at comparisons. :-)
April 16, 2008
"Janice Caron" wrote
> On 16/04/2008, Steven Schveighoffer wrote:
>>  IMO, opCmp should be
>>  defined in an interface, and those classes which implement it should
>>  implement that interface.  This would prevent trying to compare
>> something
>>  that doesn't implement the opCmp method.
>
> Of course, you realise that just because you declare "MyClass implements opCmp()", it doesn't necessarily follow that all possible subclasses of MyClass, which may be declared by other programmers, in other modules, at any time in the future, will also implement opCmp(). Nor can you be sure that your opCmp() function will be appropriate for all such subclasses.

Yes, but then there is no 'default' opCmp which does the wrong thing in all cases :)

If a derived class does not implement opCmp, it is the derived class' fault for saying "I'm OK with my parent's opCmp", when it actually isn't.  Not overriding opCmp if defined by a parent class is an unambiguous choice, which fits in with the standard virtual function implementation.  However, in the current version, nobody should EVER use the default opCmp because it simply isn't correct.

I'm not sure that the idea that opCmp should be a virtual function is bad or good.  All I'm saying is that it probably doesn't belong in Object.

-Steve


April 16, 2008
Steven Schveighoffer wrote:
> "Janice Caron" wrote
> 
>> On 16/04/2008, Steven Schveighoffer wrote:
>> 
>>>  IMO, opCmp should be
>>>  defined in an interface, and those classes which implement it should
>>>  implement that interface.  This would prevent trying to compare
>>> something
>>>  that doesn't implement the opCmp method.
>>> 
>> Of course, you realise that just because you declare "MyClass
>> implements opCmp()", it doesn't necessarily follow that all possible
>> subclasses of MyClass, which may be declared by other programmers, in
>> other modules, at any time in the future, will also implement opCmp().
>> Nor can you be sure that your opCmp() function will be appropriate for
>> all such subclasses.
>> 
>
> Yes, but then there is no 'default' opCmp which does the wrong thing in all cases :)
>
> If a derived class does not implement opCmp, it is the derived class' fault for saying "I'm OK with my parent's opCmp", when it actually isn't.  Not overriding opCmp if defined by a parent class is an unambiguous choice, which fits in with the standard virtual function implementation.  However, in the current version, nobody should EVER use the default opCmp because it simply isn't correct.
>
> I'm not sure that the idea that opCmp should be a virtual function is bad or good.  All I'm saying is that it probably doesn't belong in Object.
>
> -Steve
>
>
> 

I completely agree and would like to add that maybe there should be two interfaces: one for partial order comparison, and one for full order comparison.

Also I don't think D should have yet another special syntax for just one use case, and I don't think D should limit opCmp to be non virtual. That should be decided by the programmer (if needed the programmer can use "final").

--Yigal
April 16, 2008
On 16/04/2008, Yigal Chripun <yigal100@gmail.com> wrote:
>  I don't think D should limit opCmp to be non virtual. That
>  should be decided by the programmer (if needed the programmer can use
>  "final").

It would be a very bad thing indeed for anyone to make opCmp()
non-virtual, aka final.

I have suggested something completely different. I have suggested that if comparison is defined for class A, but not for class B which is a subclass of A, then by default, objects of type B shall be considered incomparable.

That is absolutely /not/ the same thing as implementing final opCmp()
in A. All final would do is block polymorphism (which would be worse,
actually, since that would actually /prevent/ B's opCmp() from being
used in some situations). It wouldn't stop b < c from calling
A.opCmp().

final stops derived class functions from being called, which is the wrong direction. I propose the opposite - that base class functions not be called. There is currently no mechanism in D which allows one to specify that.
April 16, 2008
Janice Caron wrote:
> On 16/04/2008, Yigal Chripun <yigal100@gmail.com> wrote:
> 
>>  I don't think D should limit opCmp to be non virtual. That
>>  should be decided by the programmer (if needed the programmer can use
>>  "final").
>> 
>
> It would be a very bad thing indeed for anyone to make opCmp()
> non-virtual, aka final.
>
> I have suggested something completely different. I have suggested that if comparison is defined for class A, but not for class B which is a subclass of A, then by default, objects of type B shall be considered incomparable.
>
> That is absolutely /not/ the same thing as implementing final opCmp()
> in A. All final would do is block polymorphism (which would be worse,
> actually, since that would actually /prevent/ B's opCmp() from being
> used in some situations). It wouldn't stop b < c from calling
> A.opCmp().
>
> final stops derived class functions from being called, which is the
> wrong direction. I propose the opposite - that base class functions
> not be called. There is currently no mechanism in D which allows one
> to specify that.
> 

Someone on this thread made a very good observation: opCmp should be a
multi-method.
This is an excellent idea and applies to more than just opCmp.

given the following classes:
class A {}
class B : A {}
class C : A {}

I'd suggest the following syntax for multi-methods: (taken from the
article linked bellow)
void func(virtual  A a1, virtual  A a2);
so both a1 and a2 can also be of any subtype of A.

let's look at some of the comparison variations:
opCmp(A a1, A a2); // compare only instances of A
opCmp(virtual A a1, virtual A a2); // will work on all subtypes
opCmp(virtual B b1, virtual B b2); // this overrides the above more
general function
etc...
I think this can provide all possible combinations needed.

So basically I think a general multi-method implementation should be
added to D instead of special treatment for opCmp.
opCmp should be implemented as a free-function and not as a method. I
still think there should be a comparable interface to tag types that can
be compared. in general tagging classes with empty interfaces (like in
Java) is a good idiom. for example, D AAs can be defined to contain any
type that implements comparable. [a future direction: implementing
attributes/annotations and use those instead]

It's also worth considering making all binary operators free functions
instead of methods, and for commutative ops the compiler can run both
op(a, b) and op(b,a). I'm not sure about this one, though, and would
like to hear what others think about the pros and cons of that approach...

an article about how to cleanly add multiple dispatch to C++ by Bjarne Stroustrup, Yuriy Solodkyy and Peter Pirkelbauer: http://research.att.com/~bs/multimethods.pdf

-- Yigal
April 16, 2008
On 16/04/2008, Yigal Chripun <yigal100@gmail.com> wrote:
> Someone on this thread made a very good observation: opCmp should be a
>  multi-method.
>  This is an excellent idea and applies to more than just opCmp.
>
>  given the following classes:
>
> class A {}
>  class B : A {}
>
> class C : A {}
>
>  I'd suggest the following syntax for multi-methods: (taken from the
>  article linked bellow)
>  void func(virtual  A a1, virtual  A a2);
>  so both a1 and a2 can also be of any subtype of A.
>
>  let's look at some of the comparison variations:
>  opCmp(A a1, A a2); // compare only instances of A
>  opCmp(virtual A a1, virtual A a2); // will work on all subtypes
>  opCmp(virtual B b1, virtual B b2); // this overrides the above more
>  general function
>  etc...
>  I think this can provide all possible combinations needed.

But it still won't work. Watch.

    class A {}
    class B : A {}
    class C : A {}

    opCmp(A a1, A a2) { ... } /* not virtual */

    B b = new B
    C c = new C

    if (b < c) ...

This will /still/ cast both b and c to A's, and then do A's comparison test. That's because B and C will both implicitly cast to A.

The only difference "virtual" will make is in the following situation

    class A {}
    class B : A {}
    class C : A {}

    opCmp(virtual A a1, virtual A a2) { ... }
    opCmp(B a1, C a2) { ... }

    A b = new B
    A c = new C

    if (b < c)...

now opCmp(B,C) would be called, because it exists. But if opCmp(B,C)
did /not/ exist, then opCmp(A,A) would be called, whether it was
virtual or not. That's because "virtual" only enables polymorphism; it
doesn't disable inheritance.

The desired goal is for opCmp(A,A) /not/ to be called if opCmp(B,C)
doesn't exist. So far as I can see, mine is the only suggestion on
this thead which achieves that goal.
April 16, 2008
Janice Caron wrote:
> On 16/04/2008, Yigal Chripun <yigal100@gmail.com> wrote:
> 
>> Someone on this thread made a very good observation: opCmp should be a
>>  multi-method.
>>  This is an excellent idea and applies to more than just opCmp.
>>
>>  given the following classes:
>>
>> class A {}
>>  class B : A {}
>>
>> class C : A {}
>>
>>  I'd suggest the following syntax for multi-methods: (taken from the
>>  article linked bellow)
>>  void func(virtual  A a1, virtual  A a2);
>>  so both a1 and a2 can also be of any subtype of A.
>>
>>  let's look at some of the comparison variations:
>>  opCmp(A a1, A a2); // compare only instances of A
>>  opCmp(virtual A a1, virtual A a2); // will work on all subtypes
>>  opCmp(virtual B b1, virtual B b2); // this overrides the above more
>>  general function
>>  etc...
>>  I think this can provide all possible combinations needed.
>> 
>
> But it still won't work. Watch.
>
>     class A {}
>     class B : A {}
>     class C : A {}
>
>     opCmp(A a1, A a2) { ... } /* not virtual */
>
>     B b = new B
>     C c = new C
>
>     if (b < c) ...
>
> This will /still/ cast both b and c to A's, and then do A's comparison test. That's because B and C will both implicitly cast to A.
>
> The only difference "virtual" will make is in the following situation
>
>     class A {}
>     class B : A {}
>     class C : A {}
>
>     opCmp(virtual A a1, virtual A a2) { ... }
>     opCmp(B a1, C a2) { ... }
>
>     A b = new B
>     A c = new C
>
>     if (b < c)...
>
> now opCmp(B,C) would be called, because it exists. But if opCmp(B,C)
> did /not/ exist, then opCmp(A,A) would be called, whether it was
> virtual or not. That's because "virtual" only enables polymorphism; it
> doesn't disable inheritance.
>
> The desired goal is for opCmp(A,A) /not/ to be called if opCmp(B,C)
> doesn't exist. So far as I can see, mine is the only suggestion on
> this thead which achieves that goal.
> 
OK, I see your point.
If I understood this correctly, the issue is the implicit cast to super.
So, one way to deal with that would be to override the opImplicitCast(A)
to throw an exception but that is maybe to big a restriction. So we need
some sort of specifier to prevent implicit casts. Let's call it
"explicit" for the purpose of this thread.
with explicit and the above classes (it can be generalized for structs
and builtins) you'll get:
A a = new B; // classic use of polymorphism in OOP
explicit A a = new B; // run-time error, implicit casts are disabled
explicit A a = cast(A) new B; // allowed again
(the above could be "undefined" in spec - the responsibility is now the
user's not the compiler's)

and now opCmp becomes:
opCmp(explicit A a1, Explicit A a2);

if (b < c) ... // no opCmp matches for these types

Note: explicit violation cannot be a compile-time error because the
compiler cannot check all cases, for example:
A b = new B;
A c = new C;
...code...
opCmp(b,c);  // b and c have static type A
the above will throw at run-time

Questions remaining:
a) is this a generally useful to have in D, or should it be limited
(maybe with a special syntax just for opCmp) ?
b) in order to ensure a compile-time error instead of a run-time
exception the compiler needs to know if the static type matches the
dynamic one, so, Should there be additional rules and restrictions
(similar to constancy ) in order for the compiler to enforce this attribute?

_My_personal_conclusion_:  I think that D really needs a facility like Java annotations. If D had such a thing, than a library could define such an attribute without adding bloat to the language (a new keyword) and without changing the compiler. It probably would be very simple to provide such a solution.

Another way to solve this problem is with reflection:
int opCmp(A a1, A a2) {
    if (cast(A) a1 is null || cast(A) a2 is null) throw new
ComparisonException;
    .... perform comparison ...
}

The second solution can work today, but I'd prefer to tag the parameters with an "explicit" annotation and have some library code do the above test in the beginning of opCmp instead of doing it myself every time. It'll also provide a more readable code IMO.

--Yigal