www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - A fresh look at comparisons

reply "Janice Caron" <caron800 googlemail.com> writes:
Every now again, someone (rightly) complains that opEquals should
return bool, not int. However, that's not my biggest complaint with
opEquals and opCmp. My biggest complaint is that it is very tedious
having to write code like:

    class C
    {
        int x;
    	
        int opEquals(Object o)
        {
            C c = cast(C) o;
            if (c is null)
                throw new Exception("Cannot compare C with wrong type");
            return x == c.x;
        }
    }

(and having to do so for every class I create). This is just wrong on
so many levels. It's wrong because we return int, not bool, yes. It's
wrong because opEquals should be a const function (it doesn't modify
this). It's wrong because it should accept a const object (it doesn't
modify it's input). But most of all, why should I have to do all the
work of making sure the input parameter is the right type?

What should I do if it's not? Should I return false? Should I throw an
exception? If I throw an exception, what type of exception should it
be? Is it OK to throw an object.Exception or should I roll my own kind
of Exception? It there some standard message I should be using for the
string? Does it matter that the message is in English and not
localized?

And it gets worse. For complex numbers we (correctly) have the following result:

    cdouble x = 1;
    cdouble y = 1i;

    (x !<>= y) == true

Try doing that with a custom type! The problem is, opCmp() just isn't
powerful enough. With opCmp, there is no way to say "not less than,
not equal to, and not greater than".

I think it's time to take a fresh look at comparisons, and to realise
that comparison is /special/, in much the same way that a constructor
is special. It shouldn't be a regular function. It should be a special
function, with its own special syntax, and some extra special rules
that make it (1) easy to write, (2) logical, and (3) behave sensibly.

So with all that borne in mind, let's consider a new proposal to
/replace/ opEquals. (And later down the page, I shall suggest a
similar proposal to replace opCmp). Instead of:

    class C
    {
        int x;
    	
        int opEquals(Object o)
        {
            C c = cast(C) o;
            if (c is null)
                throw new Exception("Cannot compare C with wrong type");
            return x == c.x;
        }
    }

we do

    class C
    {
        int x;
    	
        is(this == c)
        {
            return x == c.x;
        }
    }

Because this is no longer a regular function, the compiler can take
care of all the casting and exception throwing, so we don't have to.
Moreover, it is now possible for the compiler to generate a *default*
equality comparison, if we don't supply one. The default equality
comparison would be recursive memberwise equality comparison (which,
after all, is usually the correct thing thing to do).

And /of course/, the function should return bool, and should consider
both "this" and "c" to be both of the same type, and both const.

Likewise, we replace opCmp with:

    class C
    {
        int x;
    	
        is(this < c)
        {
            return x < c.x;
        }
    }

The default implementation of is(this < c) should be to return false
always, which implies that comparison is not meaningful for this type.

The compiler can auto-generate all fourteen possible comparisons using
only these two functions, as follows:

    (a == b)  ==  (a == b)
    (a < b)  ==  (a < b)
    (a <= b)  ==  (a == b || a < b)
    (a <> b)  ==  (a < b || b < a)
    (a <>= b)  ==  (a == b || a < b || b < a)
    (a > b)  ==  (b < a)
    (a >= b)  ==  (a == b || b < a)
    (a != b)  ==  (!(a == b))
    (a !<> b)  ==  (!(a < b || b < a))
    (a !<>= b)  ==  (!(a == b || a < b || b < a))
    (a !< b)  ==  (!(a < b))
    (a !<= b)  ==  (!(a == b || a < b))
    (a !> b)  ==  (!(b < a))
    (a !>= b)  ==  (!(a == b || b < a))

We could allow the programmer to supply some or all of these, if they
so desire, for reasons of efficiency,

To my mind, this proposal fixes everything that is wrong with comparison in D.
Apr 14 2008
next sibling parent reply Henning Hasemann <hhasemann web.de> writes:
I like this proposal but it might have efficiency issues when you need
to find out quickly if you want to order two values (you would often
have to call both functions).

What about this (much more primitive) approach:

class C {
	int category;
	int x;

	ComparsionResult opCompare(C other) {
		if(category == other.category) {
			// This could be shortened to something like
			// int.compare(x, other.x)
			if(x == other.x) {
				return ComparsionResult.EQUAL;
			}
			else if(x > other.x) {
				return ComparsionResult.GREATER;
			}
			else {
				// a < b is "unkown" meaning
				// compiler will try to get it via
				// b > a which is defined above
				return ComparsionResult.UNKNOWN;
			}
		}
		else {
			// Objects are simply incomparable
			// (like (1 - i) and (i - 1) for example)
			return ComparsionResult.UNDEFINED;
		}
	} // opCompare()
} // class C

whereas ComparsionResult would be an enum defined in object.d or so.
This should avoid all mentioned issues without a syntax change.

Henning
			

-- 
GPG Public Key:
http://gpg-keyserver.de/pks/lookup?op=get&search=0xDDD6D36D41911851
Apr 14 2008
next sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On 14/04/2008, Henning Hasemann <hhasemann web.de> wrote:
  What about this (much more primitive) approach:
One of the reasons that I believe comparison is "special", like constructors, is because of the following: class A { int opEquals(Object o) { ... } int opCmp(Object o) { ... } } class B :A { int n; } Hopefully, you can see the problem here immediately. B extends A, but B does not provide opEquals nor opCmp, so if two Bs are compared, A's compare functions will be called, which means that B's member n will be ignored. This is, of course, absolutely normal behavior for classes. It's how inheritance works. But it's just not right for comparisons. And important part of my proposal is that comparisons don't inherit. Instead, we have the default behaviors that I described. Thus, under my proposal, if B did not provide an is(this == c), then a default implementation would be provided by the compiler, equivalent to is(this == c) { return super == c.super && n == c.n; } This is very different from just changing the signature of a function. Also, there really isn't any need for any comparison result to be undefined. If (a < b) is not meaningful, it should suffice for (a < b) and (b < a) both to return false. That's because less-than really is a boolean question. If I ask "is a less than b?", and ordering is not defined for that type, then it seems to me that "no" is the right answer. (No, a is not less than b, because a and be cannot be ordered). It is far, far less complicated that way - less-than is a boolean question, not a yes/no/maybe question. However, what you /do/ need to lose, is the assumption that (a < b) implies (a !>= b), because that no longer holds.
Apr 14 2008
prev sibling next sibling parent reply "Janice Caron" <caron800 googlemail.com> writes:
On 14/04/2008, Henning Hasemann <hhasemann web.de> wrote:
  I like this proposal but it might have efficiency issues when you need
  to find out quickly if you want to order two values (you would often
  have to call both functions).
The best way to fix the efficiency issue would be to tell the compiler whether the comparison was fully ordered or not. If the comparison is fully ordered, then the compiler may implement a <= b as !(b < a) whereas, if the comparison is /not/ fully ordered, then the compiler would need to go the long route, and implement it as a == b || a < b So the question then becomes, how we tell the compiler whether or not a comparison is fully ordered. My suggestion would be, assume yes by default, and use a slightly modified syntax if not. e.g. class NotFullyOrdered { int x; is(this < c, unordered) { return whatever; } } The default, compiler-generated comparison function, if not supplied by the programmer, should be is(this < c, unordered) { /* recursive memberwise comparison */ } That buys you the best of all worlds.
Apr 14 2008
parent Bill Baxter <dnewsgroup billbaxter.com> writes:
Janice Caron wrote:
 On 14/04/2008, Henning Hasemann <hhasemann web.de> wrote:
  I like this proposal but it might have efficiency issues when you need
  to find out quickly if you want to order two values (you would often
  have to call both functions).
The best way to fix the efficiency issue would be to tell the compiler whether the comparison was fully ordered or not. If the comparison is fully ordered, then the compiler may implement a <= b as !(b < a) whereas, if the comparison is /not/ fully ordered, then the compiler would need to go the long route, and implement it as a == b || a < b So the question then becomes, how we tell the compiler whether or not a comparison is fully ordered. My suggestion would be, assume yes by default, and use a slightly modified syntax if not. e.g. class NotFullyOrdered { int x; is(this < c, unordered) { return whatever; } } The default, compiler-generated comparison function, if not supplied by the programmer, should be is(this < c, unordered) { /* recursive memberwise comparison */ } That buys you the best of all worlds.
Why not put the flag as an arg to is-- a la "cast(extra_arg)" or "foreach(extra_arg)(some;stuff)" as Andrei proposed. So it would be is(unordered)(this<c) I like the ideas you're throwing around here, anyway. Handling of comparisons could use some work. Writing "multi-column" lexical opCmps is also really annoying. Not to mention wanky-ness about opCmp happily compiling with ref/value/ or ptr members but only actually working in all cases when you use value parameters. For sorting, pointer parameters work. For comparisons in regular code ref parameters work. The whole thing is just badly in need of some loving. --bb
Apr 14 2008
prev sibling parent reply "Janice Caron" <caron800 googlemail.com> writes:
On 14/04/2008, Henning Hasemann <hhasemann web.de> wrote:
                         // Objects are simply incomparable
                         // (like (1 - i) and (i - 1) for example)
                         return ComparsionResult.UNDEFINED;
This is worth spending a bit of time discussing. The fact is that, in D, that particular comparison is not undefined. In fact, it is /very well/ defined. It is defined as follows: cdouble a = 1 - 1i; cdouble b = 1i - 1; (a == b) == false (a < b) == false (a <= b) == false (a <> b) == false (a <>= b) == false (a > b) == false (a >= b) == false (a != b) == true (a !<> b) == true (a !<>= b) == true (a !< b) == true (a !<= b) == true (a !> b) == true (a !>= b) == true It would be very bad indeed for D, if less-than suddenly became a tristate operation! In my opinion, we really don't want that. So, we don't want to be returning UNDEFINED - instead, we want to be defining the result of each of the fourteen possible comparisons so that each gives a purley boolean result, and that unorderedness is indicated by patterns such as the above. My suggestion does give us that.
Apr 14 2008
parent reply Henning Hasemann <hhasemann web.de> writes:
"Janice Caron" <caron800 googlemail.com> wrote:
 On 14/04/2008, Henning Hasemann <hhasemann web.de> wrote:
                         // Objects are simply incomparable
                         // (like (1 - i) and (i - 1) for example)
                         return ComparsionResult.UNDEFINED;
This is worth spending a bit of time discussing. The fact is that, in D, that particular comparison is not undefined. In fact, it is /very well/ defined. It is defined as follows: (snip)
Yeah, maybe undefined was simply the wrong word. What I mean is "there is no meaningful order between the two" ie "it is neither '<' nor '>' nor '=='" ie "the ORDER between these two is not defined" (which is something you can't express with the current opCmp) Just as it is with (1 - i) and (i - 1) whatever the correct term for that would be. (I think UNORDERED would be way better) I just wanted to distinguish that from UNKNOWN which would allow the compiler to search for an alternative form. Here for clarity definitions of what I meant with the two terms in my previous posting: ComparsionResult.UNKNOWN: The comparsion function does not make a statement wether the two objects are equal or a is greater b or whatever. Think of it as the comparsion function is simply not defined this way. For example if the compiler would ask an object a for "a > b" and a would return ComparsionResult.UNKNOWN The compiler can and should ask b for "b < a". If the compiler doesn't find any rewriting of the term that returns something else than UNKNOWN, every comparsion yields false. (Ie the same as if one comparsion would have returned UNDEFINED/UNORDERED) This would normally not assume well-ordering (ie the compiler would *not* check for "a !<= b". One could imagine a special flag like suggested for the is-thingy) ComparsionResult.UNDEFINED/ComparsionResult.UNORDERED: If a is asked for a for being "<", ">", "<=", ">=" or "==" b, false is returned. Ie there is no order between the two objects. Naturally this implies that all the comparsion operators starting with "!" return true. Implications of this approach in contrast to the is-Approach: - As said, no special syntax except maybe for the well-ordered flag - This version *enforces* that two objects a and b can only be in one of the following 4 states: * a is greater than b * b is greater than a (same as a is lower than b) * a is equal to b * a and be are unordered to each other This is clearly a limitation (eg a and b can not be == and < at the same time). I guess it depends strongly on what you want to do with your operators if this can be considered a good thing. - You can inherit comparsion which might be considered useful, especially if your super class compares private members and you want to refer to super class equality: class A { int x,y; ComparsionResult opCompare(A other) { // We only care for equality if(x == other.x and y == other.y) return ComparsionResult.EQUAL; return ComparsionResult.UNORDERED; } } class B : A { int z; ComparsionResult opCompare(B other) { if(super.opCompare(other) == ComparsionResult.EQUAL && z == other.z) { return ComparsionResult.EQUAL; } return ComparsionResult.UNORDERED; } } - The is() approach has a much nicer syntax and its much more easy to write cleary what you want - This approach secretely assumes some magic in that the parameter to opCompare would always be of the same type as the class. (comparsion with other objects would be UNORDERED). The is approach addresses this better in so far that its clearly visible something special is going on here and that its NOT a regular method. To your argument: """ One of the reasons that I believe comparison is "special", like constructors, is because of the following: class A { int opEquals(Object o) { ... } int opCmp(Object o) { ... } } class B :A { int n; } Hopefully, you can see the problem here immediately. B extends A, but B does not provide opEquals nor opCmp, so if two Bs are compared, A's compare functions will be called, which means that B's member n will be ignored. This is, of course, absolutely normal behavior for classes. It's how inheritance works. But it's just not right for comparisons. """: I don't think this is bad or even would feel somehow incorrect. Depending on what type of code you write you don't want every member automagically being compared. In some game code I write curretly there are for example lots of members which would not apply for comparsion. I think its more a matter of taste if members should be compared by default or not. But with throwing away inheritance you throw away all the considerations you made in A which properties identify two equal objects. Is that really what you want? So enough for the war for now ;) I really like your idea just to make myself clear. (And except for a few points it seems to be the better way especially regarding clarity) I just had this idea popped up when I read yours and wanted to fully discuss it. Hereby done. Henning -- GPG Public Key: http://gpg-keyserver.de/pks/lookup?op=get&search=0xDDD6D36D41911851
Apr 14 2008
next sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On 14/04/2008, Henning Hasemann <hhasemann web.de> wrote:
  Depending on what type of code you write you don't want every member
  automagically being compared. In some game code I write curretly there
  are for example lots of members which would not apply for comparsion.
  I think its more a matter of taste if members should be compared by
  default or not.
Yes, all we're talking about here is a sensible /default/. The fact that you can override the default, by supplying your own function, means that, in some sense, it almost doesn't matter what the default is, because if it's not what you want, then you can override it. But even so - it's nice to get the default covering the most common cases. To me, it seems sensible that the default implementation should be to compare all members (including super). This means that if a subclass adds new members, then by default, a test for subclass equality will be true iff (1) the superclass compares as equal, and (2) the new members also compare as equal. The inheritance mechanism (which is what we have now, thanks to the fact that opEquals() is a regular function), means that if a subclass adds new members, then by default, a test for subclass equality will be true iff the superclass compares as equal. All new members will be ignored.
  But with throwing away inheritance you throw away all the
  considerations you made in A which properties identify two equal
  objects. Is that really what you want?
It's not "throwing away", because the default implementation would include a test for superclass equality. ("super" is a member just like any other). So if B extends A, and also adds two member variables x and y, then the test (b1 == b2) would be evaluated as: // Your scheme b1.super == b2.super or // My scheme b1.super == b2.super && b1.x == b2.x && b1.y == b2.y So we don't lose inheritance - we just gain extra tests. Of course, if you don't /want/ those extra tests, then you would have to override the default - such is the nature of defaults - by doing something like class B : A { int x,y; is(this == that) { return super == that.super; // x and y are now ignored } }
  So enough for the war for now ;)
  I really like your idea just to make myself clear.
Thanks. And there was never any question of warfare! :-)
Apr 14 2008
prev sibling parent reply "Scott S. McCoy" <tag cpan.org> writes:
Wouldn't that safely be 0?

If there is no order, there must be quality...assuming that all types do
have a natural order, even if it's just it's ordinal value. Otherwise,
they may not be comparable with opCmp, and maybe say, only opEquals :-)

On Mon, 2008-04-14 at 16:10 +0200, Henning Hasemann wrote:
 "the ORDER between these two is not defined" (which
 is something you can't express with the current opCmp)
Apr 14 2008
parent "Janice Caron" <caron800 googlemail.com> writes:
On 14/04/2008, Scott S. McCoy <tag cpan.org> wrote:
 Wouldn't that safely be 0?

  If there is no order, there must be quality...assuming that all types do
  have a natural order, even if it's just it's ordinal value. Otherwise,
  they may not be comparable with opCmp, and maybe say, only opEquals :-)
For example, consider the complex numbers. (1+i) and (1-i) compare as !<>=, however (1+0i) and (2+0i) compare as <, so sometimes they're comparable and sometimes not.
Apr 14 2008
prev sibling next sibling parent reply "Scott S. McCoy" <tag cpan.org> writes:
I like this, except for the semantic irregularities similar to Scala
we'd be introducing by not having the operator evaluated.

Also, if you can't cast successfully I'd think you would simply return
false for opEquals.

if o !is C, then they can't possibly be equal. ;-)

The idea behind opEquals acting this way, I assume naturally, is that it
enables the ability for your type to know how to compare itself to
multiple *other* types which exist.  Somewhat similar to how you can
freely compare a long and an int in many cases, without ramifications or
need to know any bit-wise or type difference.  So what is the answer?
What makes two types comparable?  Common inheritance?  Where?  How do I
specify (if all I'm saying is "is(this == c)" the possible types I can
be compared to within my system.  What if I know these types, and they
are not my descendants?  Or if they are my descendants, when and how can
I say that my descendant is not comparable to it's parent?

Cheers,
	Scott S. McCoy


On Mon, 2008-04-14 at 09:18 +0100, Janice Caron wrote:
 Every now again, someone (rightly) complains that opEquals should
 return bool, not int. However, that's not my biggest complaint with
 opEquals and opCmp. My biggest complaint is that it is very tedious
 having to write code like:
 
     class C
     {
         int x;
     	
         int opEquals(Object o)
         {
             C c = cast(C) o;
             if (c is null)
                 throw new Exception("Cannot compare C with wrong type");
             return x == c.x;
         }
     }
 
 (and having to do so for every class I create). This is just wrong on
 so many levels. It's wrong because we return int, not bool, yes. It's
 wrong because opEquals should be a const function (it doesn't modify
 this). It's wrong because it should accept a const object (it doesn't
 modify it's input). But most of all, why should I have to do all the
 work of making sure the input parameter is the right type?
 
 What should I do if it's not? Should I return false? Should I throw an
 exception? If I throw an exception, what type of exception should it
 be? Is it OK to throw an object.Exception or should I roll my own kind
 of Exception? It there some standard message I should be using for the
 string? Does it matter that the message is in English and not
 localized?
 
 And it gets worse. For complex numbers we (correctly) have the following
result:
 
     cdouble x = 1;
     cdouble y = 1i;
 
     (x !<>= y) == true
 
 Try doing that with a custom type! The problem is, opCmp() just isn't
 powerful enough. With opCmp, there is no way to say "not less than,
 not equal to, and not greater than".
 
 I think it's time to take a fresh look at comparisons, and to realise
 that comparison is /special/, in much the same way that a constructor
 is special. It shouldn't be a regular function. It should be a special
 function, with its own special syntax, and some extra special rules
 that make it (1) easy to write, (2) logical, and (3) behave sensibly.
 
 So with all that borne in mind, let's consider a new proposal to
 /replace/ opEquals. (And later down the page, I shall suggest a
 similar proposal to replace opCmp). Instead of:
 
     class C
     {
         int x;
     	
         int opEquals(Object o)
         {
             C c = cast(C) o;
             if (c is null)
                 throw new Exception("Cannot compare C with wrong type");
             return x == c.x;
         }
     }
 
 we do
 
     class C
     {
         int x;
     	
         is(this == c)
         {
             return x == c.x;
         }
     }
 
 Because this is no longer a regular function, the compiler can take
 care of all the casting and exception throwing, so we don't have to.
 Moreover, it is now possible for the compiler to generate a *default*
 equality comparison, if we don't supply one. The default equality
 comparison would be recursive memberwise equality comparison (which,
 after all, is usually the correct thing thing to do).
 
 And /of course/, the function should return bool, and should consider
 both "this" and "c" to be both of the same type, and both const.
 
 Likewise, we replace opCmp with:
 
     class C
     {
         int x;
     	
         is(this < c)
         {
             return x < c.x;
         }
     }
 
 The default implementation of is(this < c) should be to return false
 always, which implies that comparison is not meaningful for this type.
 
 The compiler can auto-generate all fourteen possible comparisons using
 only these two functions, as follows:
 
     (a == b)  ==  (a == b)
     (a < b)  ==  (a < b)
     (a <= b)  ==  (a == b || a < b)
     (a <> b)  ==  (a < b || b < a)
     (a <>= b)  ==  (a == b || a < b || b < a)
     (a > b)  ==  (b < a)
     (a >= b)  ==  (a == b || b < a)
     (a != b)  ==  (!(a == b))
     (a !<> b)  ==  (!(a < b || b < a))
     (a !<>= b)  ==  (!(a == b || a < b || b < a))
     (a !< b)  ==  (!(a < b))
     (a !<= b)  ==  (!(a == b || a < b))
     (a !> b)  ==  (!(b < a))
     (a !>= b)  ==  (!(a == b || b < a))
 
 We could allow the programmer to supply some or all of these, if they
 so desire, for reasons of efficiency,
 
 To my mind, this proposal fixes everything that is wrong with comparison in D.
Apr 14 2008
next sibling parent reply "Janice Caron" <caron800 googlemail.com> writes:
On 14/04/2008, Scott S. McCoy <tag cpan.org> wrote:
 I like this, except for the semantic irregularities similar to Scala
 we'd be introducing by not having the operator evaluated.
I don't understand what this means. What is Scala?
 Also, if you can't cast successfully I'd think you would simply return
 false for opEquals.

 if o !is C, then they can't possibly be equal. ;-)
I agree.
  The idea behind opEquals acting this way, I assume naturally, is that it
  enables the ability for your type to know how to compare itself to
  multiple *other* types which exist.  Somewhat similar to how you can
  freely compare a long and an int in many cases, without ramifications or
  need to know any bit-wise or type difference.
I'm not sure that's a real concern. To use your example, int and long will both implicitly convert to the same common type (in this case, long), and the == test can be made on that. In general, if a is of type A, and b is of type B, where A != B, and there exists a common type C into which both A and B will implicitly cast, then we only need to concern ourselves with (cast(C)a == cast(C)b).
Apr 14 2008
next sibling parent Paul D Anderson <paul.d.anderson.removethis comcast.andthis.net> writes:
Janice Caron Wrote:

 On 14/04/2008, Scott S. McCoy <tag cpan.org> wrote:
 I like this, except for the semantic irregularities similar to Scala
 we'd be introducing by not having the operator evaluated.
I don't understand what this means. What is Scala?
Scala is an open-source language that runs on the Java platform. The name is a contraction of "scalable language". http://www.scala-lang.org/ Paul
Apr 14 2008
prev sibling parent reply "Scott S. McCoy" <tag cpan.org> writes:
On Mon, 2008-04-14 at 19:41 +0100, Janice Caron wrote:
 On 14/04/2008, Scott S. McCoy <tag cpan.org> wrote:
 I like this, except for the semantic irregularities similar to Scala
 we'd be introducing by not having the operator evaluated.
I don't understand what this means. What is Scala?
Another programming language. What I mean is this, you have an expression which appears just as any other expression, but it is not evaluated as is instead "special syntax". This is relatively disconcerting. In fact, you double that by adding another operator which is often used for all-too-many other things ("is") in your examples.
 I agree.
Yay!
  The idea behind opEquals acting this way, I assume naturally, is that it
  enables the ability for your type to know how to compare itself to
  multiple *other* types which exist.  Somewhat similar to how you can
  freely compare a long and an int in many cases, without ramifications or
  need to know any bit-wise or type difference.
I'm not sure that's a real concern. To use your example, int and long will both implicitly convert to the same common type (in this case, long), and the == test can be made on that. In general, if a is of type A, and b is of type B, where A != B, and there exists a common type C into which both A and B will implicitly cast, then we only need to concern ourselves with (cast(C)a == cast(C)b).
But you're making assumptions about the application's definition of equality. See, that's the point of having opEquals overloadable: So the application can define equality. And these assumptions may very well not be safe. Can you say definitively and for certain that in all environments in all programs ever written in D, that an object of type A and an object of type B will never be equal even though they have no common ancestor? Certainly not. And also, we cannot say that for instance, if you have types B and C which are both descendants of type A, that you will want B to be equal to C if A.opEquals() returns true for the two instances. Naturally, we could, but it would be a potentially unsafe assumption which would reduce the possible usability of the language in some cases, for instance when an object has a factory method to translate itself to another type, which existed prior to it and is not an ancestor because the target type is a part of an inheritance tree which is otherwise irrelevant for the implementation of the source type....if that makes any sense. However when calling opEquals on the source type, it may want to be equal to the instance of the target type, assuming the planets are aligned. It'd take me a bit of creativity to come up with a good example, but I'm sure I could. Cheers, Scott S. McCoy
Apr 14 2008
parent reply "Janice Caron" <caron800 googlemail.com> writes:
On 15/04/2008, Scott S. McCoy <tag cpan.org> wrote:
 But you're making assumptions about the application's definition of
 equality.
I was, and I got that part wrong, so I retract that part of the idea. See below.
  See, that's the point of having opEquals overloadable:
I get that now. So what you're basically saying is that is(this == c) isn't enough, because it makes the presumption that typeof(c) /must/ be the same as typeof(this). Whereas, we really need to allow the programmer to overload for different types. Still, only a minor change to my proposal would be needed to accomodate this. We only need to allow the test to be overloaded, so: is(this == c) /* standard test */ is(this == C c) /* overload for type C */
  Can you say definitively and for certain that in all
  environments in all programs ever written in D, that an object of type A
  and an object of type B will never be equal even though they have no
  common ancestor?
No, so obviously you're right, and we would need to make it overloadable, as per the above example.
  And also, we cannot say that for instance, if you have types B and C
  which are both descendants of type A, that you will want B to be equal
  to C if A.opEquals() returns true for the two instances.
You're right, and that's one thing I don't like about opEquals, and which is(==) would solve. So, under the revised proposal: class A { is(this == c) {...} } class B : A {} class C : A {} B b; C c; if (b == c) ... This test would now have to return false always, because the new rules must compare B with B, or C with C, and must /not/ cast b and c to their common type. Under the new rules, to compare B with C, the programmer would now have to write class B : A { is(this == C c) {...} } class C : A { is(this == B b) {...} } I think that covers all the edge cases that people have pointed out. With that adjustment, the scheme, I think, now works a treat. As you know, in existing D, opEquals(), at least for classes, is defined to take an Object parameter, not a C parameter, so even ignoring the inheritance rules, allowing opEquals to compare against multiple types is tricky. You end up writing code like int opEquals(Object o) { A a = cast(A)o; B b = cast(B)o; C c = cast(C)o; if (c !is null) { ... } else if (b !is null) { ... } else if (a !is null) { ... } } which is not ideal
Apr 14 2008
next sibling parent reply "Bruce Adams" <tortoise_74 yeah.who.co.uk> writes:
On Tue, 15 Apr 2008 07:49:21 +0100, Janice Caron <caron800 googlemail.co=
m>  =

wrote:

 On 15/04/2008, Scott S. McCoy <tag cpan.org> wrote:

  Can you say definitively and for certain that in all
  environments in all programs ever written in D, that an object of ty=
pe =
 A
  and an object of type B will never be equal even though they have no=
  common ancestor?
No, so obviously you're right, and we would need to make it overloadable, as per the above example.
  And also, we cannot say that for instance, if you have types B and C=
  which are both descendants of type A, that you will want B to be equ=
al
  to C if A.opEquals() returns true for the two instances.
You're right, and that's one thing I don't like about opEquals, and which is(=3D=3D) would solve. So, under the revised proposal: class A { is(this =3D=3D c) {...} } class B : A {} class C : A {} B b; C c; if (b =3D=3D c) ... This test would now have to return false always, because the new rules=
 must compare B with B, or C with C, and must /not/ cast b and c to
 their common type.
I don't think even this is safe. The normal definition of inheritance is= = that you can use B or C wherever you could use an A. Its probably safe that i= t = doesn't compile by default.
 Under the new rules, to compare B with C, the
 programmer would now have to write

     class B : A
     {
         is(this =3D=3D C c) {...}
     }

     class C : A
     {
         is(this =3D=3D B b) {...}
     }

 I think that covers all the edge cases that people have pointed out.
 With that adjustment, the scheme, I think, now works a treat.
Yes. The semantics are there. We just need to find a syntax that fits = better with D. This doesn't quite sit right when everything else is an Op-something. Regards, Bruce.
Apr 15 2008
parent reply "Janice Caron" <caron800 googlemail.com> writes:
On 15/04/2008, Bruce Adams <tortoise_74 yeah.who.co.uk> wrote:
  I don't think even this is safe. The normal definition of inheritance is
Well, that's kindof the point. is(==) wouldn't /use/ inheritance. It wouldn't be inheritable. It's just like constructors aren't inheritable. The whole point is the recognition that inheritance is not the right mechanism when it comes to comparisons, and hence the quest for a different mechanism that /is/ appropriate.
  Yes. The semantics are there. We just need to find a syntax that fits
 better with D.
  This doesn't quite sit right when everything else is an Op-something.
It wouldn't be a function in the conventional sense. Choosing a syntax that makes it look /not/ like a function is entirely deliberate. It's just like constructors don't look like functions; destructors don't look like functions; class invariants don't look like functions; unit tests don't look like functions. opAdd() is a function, subject to all the normal rules of functions, including inheritance, overloading, overriding, etc. They're different beasts, so they need to look different, syntactically. (Also, I chose "is" so as to avoid introducing a new keyword).
Apr 15 2008
next sibling parent reply Georg Wrede <georg nospam.org> writes:
Janice Caron wrote:
 On 15/04/2008, Bruce Adams <tortoise_74 yeah.who.co.uk> wrote:
 
 I don't think even this is safe. The normal definition of inheritance is
Well, that's kindof the point. is(==) wouldn't /use/ inheritance. It wouldn't be inheritable. It's just like constructors aren't inheritable. The whole point is the recognition that inheritance is not the right mechanism when it comes to comparisons, and hence the quest for a different mechanism that /is/ appropriate.
Well, that depends. If I design a class hierarchy where I want to compare instances, I'd either have the superclass define the comparison, if it's appropriate. If, OTOH, the different subclasses should compare differently, then of course the comparison has to be defined for them separately. I'm not sure I see a big problem here.
 Yes. The semantics are there. We just need to find a syntax that
 fits better with D. This doesn't quite sit right when everything
 else is an Op-something.
It wouldn't be a function in the conventional sense. Choosing a syntax that makes it look /not/ like a function is entirely deliberate. It's just like constructors don't look like functions; destructors don't look like functions; class invariants don't look like functions; unit tests don't look like functions. opAdd() is a function, subject to all the normal rules of functions, including inheritance, overloading, overriding, etc. They're different beasts, so they need to look different, syntactically. (Also, I chose "is" so as to avoid introducing a new keyword).
Apr 15 2008
parent "Janice Caron" <caron800 googlemail.com> writes:
On 15/04/2008, Georg Wrede <georg nospam.org> wrote:
  Well, that depends. If I design a class hierarchy where I want to compare
 instances, I'd either have the superclass define the comparison, if it's
 appropriate. If, OTOH, the different subclasses should compare differently,
 then of course the comparison has to be defined for them separately. I'm not
 sure I see a big problem here.
The only thing that would be different is the *default* behavior, by which I mean the behavior of a subclass which does not explicitly provide its own equality comparison. Under the inheritance mechanism, the default comparison function returns true iff super.opEquals(Object other) returned true. Under the is(==) mechanism, the default comparison function would return true iff all of the following conditions are simultaneously met: (1) the lhs and rhs were of the same type, (2) lhs.super compares equal to rhs.super, and (3) all member variables of lhs compare equal to corresponding member variables of rhs. So, if you subclass A to get B, but don't add any new member variables, then the default comparison test would be the same. But if you add new member variables, then those would get included, by default, in the new test. But again, I'm only talking about /default/ behavior here. If you don't want default behavior, you can override, and get any behavior you want.
Apr 15 2008
prev sibling parent reply "Bruce Adams" <tortoise_74 yeah.who.co.uk> writes:
On Tue, 15 Apr 2008 10:07:31 +0100, Janice Caron <caron800 googlemail.co=
m>  =

wrote:

 On 15/04/2008, Bruce Adams <tortoise_74 yeah.who.co.uk> wrote:
  I don't think even this is safe. The normal definition of inheritanc=
e =
 is
Well, that's kindof the point. is(=3D=3D) wouldn't /use/ inheritance. =
It
 wouldn't be inheritable. It's just like constructors aren't
 inheritable.

 The whole point is the recognition that inheritance is not the right
 mechanism when it comes to comparisons, and hence the quest for a
 different mechanism that /is/ appropriate.
You misunderstand me. I believe that the operation should be undefined rather than providing a stub that returns false. As the presence of even= a = stub function here is misleading. class A; class B: A; class C: A; B =3D=3D C // should not compile - unless user defined
  Yes. The semantics are there. We just need to find a syntax that fit=
s
 better with D.
  This doesn't quite sit right when everything else is an Op-something=
.
 It wouldn't be a function in the conventional sense. Choosing a syntax=
 that makes it look /not/ like a function is entirely deliberate. It's
 just like constructors don't look like functions; destructors don't
 look like functions; class invariants don't look like functions; unit
 tests don't look like functions.

 opAdd() is a function, subject to all the normal rules of functions,
 including inheritance, overloading, overriding, etc. They're different=
 beasts, so they need to look different, syntactically.

 (Also, I chose "is" so as to avoid introducing a new keyword).
I disagree here. Constructors and destructors are special kinds of funct= ion but they are still functions. The difference is that some special semant= ics are apply in addition to normal functional behaviour. Operators are syntactic sugar to map symbols to functions. The function is a fundermental unit and one which everyone understands. Attempting to mask or hide that is counter intuitive. Regards, Bruce.
Apr 15 2008
parent "Janice Caron" <caron800 googlemail.com> writes:
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".
Apr 16 2008
prev sibling next sibling parent reply "Scott S. McCoy" <tag cpan.org> writes:
I agree that's tricky, but isn't it infact true that opEquals is defined
as a multimethod? 

It appears to be, please follow this link: http://codepad.org/z09oUtB4

So maybe you would just like it to return bool, as opposed to int?  Or
maybe it needs better documentation?

Cheers,
	Scott S. McCoy

On Tue, 2008-04-15 at 07:49 +0100, Janice Caron wrote:
 
 As you know, in existing D, opEquals(), at least for classes, is
 defined to take an Object parameter, not a C parameter, so even
 ignoring the inheritance rules, allowing opEquals to compare against
 multiple types is tricky. You end up writing code like
 
     int opEquals(Object o)
     {
         A a = cast(A)o;
         B b = cast(B)o;
         C c = cast(C)o;
 
         if (c !is null)
         {
             ...
         }
         else if (b !is null)
         {
             ...
         }
         else if (a !is null)
         {
             ...
         }
     }
 
 which is not ideal
 
Apr 15 2008
next sibling parent reply "Scott S. McCoy" <tag cpan.org> writes:
Sorry, this link :-P

http://codepad.org/wxG1AKBe

On Tue, 2008-04-15 at 02:42 -0700, Scott S. McCoy wrote:
 I agree that's tricky, but isn't it infact true that opEquals is defined
 as a multimethod? 
 
 It appears to be, please follow this link: http://codepad.org/z09oUtB4
 
 So maybe you would just like it to return bool, as opposed to int?  Or
 maybe it needs better documentation?
 
 Cheers,
 	Scott S. McCoy
 
 On Tue, 2008-04-15 at 07:49 +0100, Janice Caron wrote:
 
 As you know, in existing D, opEquals(), at least for classes, is
 defined to take an Object parameter, not a C parameter, so even
 ignoring the inheritance rules, allowing opEquals to compare against
 multiple types is tricky. You end up writing code like
 
     int opEquals(Object o)
     {
         A a = cast(A)o;
         B b = cast(B)o;
         C c = cast(C)o;
 
         if (c !is null)
         {
             ...
         }
         else if (b !is null)
         {
             ...
         }
         else if (a !is null)
         {
             ...
         }
     }
 
 which is not ideal
 
Apr 15 2008
parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
"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
Apr 16 2008
prev sibling next sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On 15/04/2008, Scott S. McCoy <tag cpan.org> wrote:
 I agree that's tricky, but isn't it infact true that opEquals is defined
  as a multimethod?

  It appears to be, please follow ...
  http://codepad.org/wxG1AKBe
But that won't work if you compile with -w, because then you have to use the override keyword, which in turn insists that opEquals have the same signature as in Object.
  So maybe you would just like it to return bool, as opposed to int?
No. That is, of /course/ I want it to return bool, but I don't /just/ want it to return bool. This proposal is all about ditching inheritance and replacing it with a compiler-generated default implementation, which results in very different (but, in my opinion, correct) behavior for comparisons. If all you want is for opEquals to return bool, there is a different thread for that discussion.
Apr 15 2008
prev sibling parent "Bruce Adams" <tortoise_74 yeah.who.co.uk> writes:
On Tue, 15 Apr 2008 10:42:49 +0100, Scott S. McCoy <tag cpan.org> wrote:

 I agree that's tricky, but isn't it infact true that opEquals is defined
 as a multimethod?
Actually this is one of the key insights. Does D have a good standard solution to the multiple dispatch problem? I haven't seen it myself but then I haven't been looking.
Apr 15 2008
prev sibling parent reply Georg Wrede <georg nospam.org> writes:
Janice Caron wrote:
 As you know, in existing D, opEquals(), at least for classes, is
 defined to take an Object parameter, not a C parameter, so even
 ignoring the inheritance rules, allowing opEquals to compare against
 multiple types is tricky. You end up writing code like
 
     int opEquals(Object o)
     {
         A a = cast(A)o;
         B b = cast(B)o;
         C c = cast(C)o;
 
         if (c !is null)
         {
             ...
         }
         else if (b !is null)
         {
             ...
         }
         else if (a !is null)
         {
             ...
         }
     }
 
 which is not ideal
Ok, but bear with me here for a moment. Suppose you need to write specific code for comparisons between A, B, and C. You'd have to tell the compiler somehow which of your code lines are for which comparison, right? And the above is not very much typing. And it seems flexible and clear enough. Now, changing the syntax would add yet another thing for the compiler to think about, and, if it doesn't bring at least the same clarity plus some significant enhancement to productivity, clarity, ease of use, or something else, then what's the use. So, maybe I'm missing the real point here?
Apr 15 2008
parent "Janice Caron" <caron800 googlemail.com> writes:
On 15/04/2008, Georg Wrede <georg nospam.org> wrote:
 and, if it doesn't bring at least the same clarity
 plus some significant enhancement to productivity, clarity, ease of use, or
 something else, then what's the use.

  So, maybe I'm missing the real point here?
How about, it brings significant enhancement to productivity, clarity, and ease of use? I think perhaps you need to re-read the entire thread, since the only way I can really answer that is by repeating things I've already said.
Apr 15 2008
prev sibling parent "Janice Caron" <caron800 googlemail.com> writes:
On 14/04/2008, Janice Caron <caron800 googlemail.com> wrote:
  In general, if a is of type A, and b is of type B, where A != B, and
  there exists a common type C into which both A and B will implicitly
  cast, then we only need to concern ourselves with (cast(C)a ==
  cast(C)b).
On second thoughts... If B and C both derive from A, and we try to compare B b; C c; if (b == c) ... Should we be testing for if (cast(A)b == cast(A)c) ... or should we be returning false? That's a tricky question, and I confess I don't know the answer. I guess I'll think about it for a while... :-)
Apr 14 2008
prev sibling next sibling parent Robert Fraser <fraserofthenight gmail.com> writes:
Janice Caron wrote:
 [...]
There's no reason why two user-defined types that are not typically castable to one another can't be defined as equal... as long as this is transitive (so they both know about each other). Your syntax arbitrarily limits that option. It may be questionable software design, but it has a very legitimate use: backwards compatibility. For example, version 1 of a library could have a certain thing as a struct, but to make it more extensible, version 2 introduces a class version of the same type. They both have overridden opEquals to make them evaluate as equal to one another (for example, so users can use both of them in a hash and not have to switch their entire codebase over at once). For efficiency, both the class and struct versions may live on.
Apr 14 2008
prev sibling next sibling parent Graham St Jack <Graham.StJack internode.on.net> writes:
Excellent proposal. Something along these lines is badly needed.
Apr 14 2008
prev sibling next sibling parent reply Yigal Chripun <yigal100 gmail.com> writes:
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.

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.

-- Yigal
Apr 15 2008
parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
"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
Apr 16 2008
next sibling parent reply "Janice Caron" <caron800 googlemail.com> writes:
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. :-)
Apr 16 2008
parent reply "Steven Schveighoffer" <schveiguy yahoo.com> writes:
"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
Apr 16 2008
parent reply Yigal Chripun <yigal100 gmail.com> writes:
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
Apr 16 2008
parent reply "Janice Caron" <caron800 googlemail.com> writes:
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.
Apr 16 2008
parent reply Yigal Chripun <yigal100 gmail.com> writes:
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
Apr 16 2008
parent reply "Janice Caron" <caron800 googlemail.com> writes:
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.
Apr 16 2008
parent reply Yigal Chripun <yigal100 gmail.com> writes:
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
Apr 16 2008
parent reply "Janice Caron" <caron800 googlemail.com> writes:
On 16/04/2008, Yigal Chripun <yigal100 gmail.com> wrote:
 OK, I see your point.
Ta. But you see one of my points. I made more than one.
  If I understood this correctly, the issue is the implicit cast to super.
This particular issue could be dealt with as you suggest, but there are other issues that it doesn't fix. For example - consider how you might write an opCmp for complex numbers. class Complex { int opCmp(Object o) { Complex c = cast(Complex)o; if (c is null) return false; if (this.im == 0 && c.im == 0) { if (this.re < c.re) return -1; else if (this.re == c.re) return 0; else return 1; } else return ???? } } The problem is that complex numbers become UNORDERED when the imaginary part (of either number) becomes non-zero. opCmp() has no way to express that. Henning Hasemann did suggest making opCmp return an enum, instead of an int, so that all four possible returns could be indicated, but again, like you, he's solving one problem at a time. The whole "fresh look" approach is to solve all of the problems in one fell swoop. So, this particular problem would be solved in my scheme with class Complex { is(this < c, unordered) { if (this.im == 0 && c.im == 0) { return this.re < c.re; } else return false; } } And there is /yet/ another problem to consider - your "explicit" approach works just fine for opCmp, but it won't work for opEquals. The reason is that the default behavior we desire from opCmp is very different from the default behavior of opEquals. Consider: class A { ... } class B : A { int x; } // Your way int opEquals(explicit A a1, explicit A a2); B b = new B; B c = new C; if (b == c)... Under your scheme, b would not be comparable for equality with c, since there is no explicit equality test. By contrast, my scheme would generate a default equality test. Specifically, b would be considered equal to c if and only if (cast(A)b == cast(A)c) && (b.x == c.x) Of course, in the both schemes, the programmer can always override the default, by defining an equals comparison for B, but nonetheless, my scheme gets you a fine default, which is likely to be the right thing to do much, if not most, of the time. In other words, I have presented a comprehensive solution which solves many problems at once, not just a single problem. (Maybe that's why I have trouble explaining it?)
Apr 17 2008
parent reply Yigal Chripun <yigal100 gmail.com> writes:
Janice Caron wrote:
 On 16/04/2008, Yigal Chripun <yigal100 gmail.com> wrote:
   
 OK, I see your point.
     
Ta. But you see one of my points. I made more than one.
  If I understood this correctly, the issue is the implicit cast to super.
     
This particular issue could be dealt with as you suggest, but there are other issues that it doesn't fix. For example - consider how you might write an opCmp for complex numbers. class Complex { int opCmp(Object o) { Complex c = cast(Complex)o; if (c is null) return false; if (this.im == 0 && c.im == 0) { if (this.re < c.re) return -1; else if (this.re == c.re) return 0; else return 1; } else return ???? } } The problem is that complex numbers become UNORDERED when the imaginary part (of either number) becomes non-zero. opCmp() has no way to express that. Henning Hasemann did suggest making opCmp return an enum, instead of an int, so that all four possible returns could be indicated, but again, like you, he's solving one problem at a time. The whole "fresh look" approach is to solve all of the problems in one fell swoop. So, this particular problem would be solved in my scheme with class Complex { is(this < c, unordered) { if (this.im == 0 && c.im == 0) { return this.re < c.re; } else return false; } }
the above is a non-issue IMO. personally I HATE error return codes/enums (that's a C idiom and does _not_ belong in a modern language like D). (a < b) is an error for unordered types just like "hello" + 2 is an error. nothing should be returned is in such a case it doesn't matter if it's an int or an enum. This is an error and should be dealt as such - either throw an exception or don't define an opCmp for complex (I prefer the latter since as you said so yourself, Complex numbers are unordered). I've previously said that opCmp should not be in Object and should not be defined for all types. a better design IMO is to have an interface "Ordered" and maybe also "PartialyOrdered" too. also, in conjunction with my previous post, they could be defined as annotations instead just like "explicit". as an aside, annotations in Java use almost the same syntax as interfaces. This makes perfect sense since annotations have a lot in common with interfaces. annotations are special interfaces that provide additional _non_override-able_ functionality so that all subtypes will have that added functionality. that means that they can mostly be implemented today in D for classes. a little bit of syntactic sugar and support for primitives is all that is needed to have this feature in D.
 And there is /yet/ another problem to consider - your "explicit"
 approach works just fine for opCmp, but it won't work for opEquals.
 The reason is that the default behavior we desire from opCmp is very
 different from the default behavior of opEquals. Consider:

     class A { ... }
     class B : A { int x; }

     // Your way
     int opEquals(explicit A a1, explicit A a2);

     B b = new B;
     B c = new C;

     if (b == c)...

 Under your scheme, b would not be comparable for equality with c,
 since there is no explicit equality test.

 By contrast, my scheme would generate a default equality test.
 Specifically, b would be considered equal to c if and only if

     (cast(A)b == cast(A)c) && (b.x == c.x)

 Of course, in the both schemes, the programmer can always override the
 default, by defining an equals comparison for B, but nonetheless, my
 scheme gets you a fine default, which is likely to be the right thing
 to do much, if not most, of the time.
   
I'm not sure that this default should be provided. the default for many languages is that unless you define an opEquals for your type, the comparison will default to identity comparison and from a reply to a previous post of mine, apparently this is also true in D. this is the only sensible default IMO. The problem I see with the suggested default above is the case for classes that should not be compared at all. Do i override and throw an error, or should I use: bool opEquals(A a, A b) { if (a is b) return true; return false; } there are only two sensible approaches IMHO: unless defined the compiler replaces "==" with "is" (this will never be wrong) or an error is produced since there is no explicit equality test. If this is not what I wanted then the error will remind me that I need to provide the test myself. OTOH, your solution is very similar to C++ which provides defaults which not always suit the needs of the programmer which then the programmer needs to know/remember to override. That makes C++ that much more difficult for beginners to grok an I've admittedly have been bitten by that myself.
 In other words, I have presented a comprehensive solution which solves
 many problems at once, not just a single problem. (Maybe that's why I
 have trouble explaining it?)
   
your suggestion is indeed a comprehensive solution to a very specific problem. OTOH I'm trying to suggest a more general approach that could be used generally in D without special treatment syntactically for just this problem. Basically, my trouble understanding your suggestion is that I don't see a need for that special treatment as opposed to a more general solution.
Apr 17 2008
parent reply Paul D Anderson <paul.d.anderson.removethis comcast.andthis.net> writes:
Yigal Chripun Wrote:

 The problem is that complex numbers become UNORDERED when the
 imaginary part (of either number) becomes non-zero. opCmp() has no way
 to express that. Henning Hasemann did suggest making opCmp return an
 enum, instead of an int, so that all four possible returns could be
 indicated, but again, like you, he's solving one problem at a time.
 The whole "fresh look" approach is to solve all of the problems in one
 fell swoop. So, this particular problem would be solved in my scheme
 with

     class Complex
     {
         is(this < c, unordered)
         {
             if (this.im == 0 && c.im == 0)
             {
                 return this.re < c.re;
             }
             else return false;
         }
     }
   
the above is a non-issue IMO. personally I HATE error return codes/enums (that's a C idiom and does _not_ belong in a modern language like D). (a < b) is an error for unordered types just like "hello" + 2 is an error. nothing should be returned is in such a case it doesn't matter if it's an int or an enum. This is an error and should be dealt as such - either throw an exception or don't define an opCmp for complex (I prefer the latter since as you said so yourself, Complex numbers are unordered). I've previously said that opCmp should not be in Object and should not be defined for all types. a better design IMO is to have an interface "Ordered" and maybe also "PartialyOrdered" too. also, in conjunction with my previous post, they could be defined as annotations instead just like "explicit".
Slightly OT -- I realize the Complex class is being used as an example to prove a point and this is not a discussion on the mathematics of complex numbers. However -- There are instances when an unordered field (i.e. complex numbers) may be considered to be ordered when reduced to an underlying field (i.e. real numbers), but I don't think it's good practice to build that exception into the definition. In other words the Complex class should always return false on comparison since it is always unordered. If an exception is allowed for the case where the imaginary parts are zero, why not when the real parts are zero? 0+3i is clearly ordered with respect to 0+4i. In fact any time the real OR imaginary parts are equal the corresponding parts are ordered. This is nothing more than ordering points on any constant imaginary or constant real line in the complex plane. IMHO a better case could be made for ordering complex numbers by comparing their norms, with the obvious deficit that two complex numbers with the same order would not necessarily be equal. I recognize that there may be applications where, for good reason, real and complex numbers are mixed and that comparison of real numbers with real parts of complex numbers is needed. But I don't think that complex numbers are a good example of a "sometimes ordered" system and I'm not sure that such a system exists. opCmp() for any unordered system should always return false. Paul
Apr 17 2008
parent Paul D Anderson <paul.d.anderson.removethis comcast.andthis.net> writes:
Paul D Anderson Wrote:

 Yigal Chripun Wrote:
 
 The problem is that complex numbers become UNORDERED when the
 imaginary part (of either number) becomes non-zero. opCmp() has no way
 to express that. Henning Hasemann did suggest making opCmp return an
 enum, instead of an int, so that all four possible returns could be
 indicated, but again, like you, he's solving one problem at a time.
 The whole "fresh look" approach is to solve all of the problems in one
 fell swoop. So, this particular problem would be solved in my scheme
 with

     class Complex
     {
         is(this < c, unordered)
         {
             if (this.im == 0 && c.im == 0)
             {
                 return this.re < c.re;
             }
             else return false;
         }
     }
   
the above is a non-issue IMO. personally I HATE error return codes/enums (that's a C idiom and does _not_ belong in a modern language like D). (a < b) is an error for unordered types just like "hello" + 2 is an error. nothing should be returned is in such a case it doesn't matter if it's an int or an enum. This is an error and should be dealt as such - either throw an exception or don't define an opCmp for complex (I prefer the latter since as you said so yourself, Complex numbers are unordered). I've previously said that opCmp should not be in Object and should not be defined for all types. a better design IMO is to have an interface "Ordered" and maybe also "PartialyOrdered" too. also, in conjunction with my previous post, they could be defined as annotations instead just like "explicit".
Slightly OT -- I realize the Complex class is being used as an example to prove a point and this is not a discussion on the mathematics of complex numbers. However -- There are instances when an unordered field (i.e. complex numbers) may be considered to be ordered when reduced to an underlying field (i.e. real numbers), but I don't think it's good practice to build that exception into the definition. In other words the Complex class should always return false on comparison since it is always unordered. If an exception is allowed for the case where the imaginary parts are zero, why not when the real parts are zero? 0+3i is clearly ordered with respect to 0+4i. In fact any time the real OR imaginary parts are equal the corresponding parts are ordered. This is nothing more than ordering points on any constant imaginary or constant real line in the complex plane. IMHO a better case could be made for ordering complex numbers by comparing their norms, with the obvious deficit that two complex numbers with the same order would not necessarily be equal. I recognize that there may be applications where, for good reason, real and complex numbers are mixed and that comparison of real numbers with real parts of complex numbers is needed. But I don't think that complex numbers are a good example of a "sometimes ordered" system and I'm not sure that such a system exists. opCmp() for any unordered system should always return false. Paul
Sorry -- the first snippet (now with ">>>") is from Janice, not Yigal.
Apr 17 2008
prev sibling parent reply Bruno Medeiros <brunodomedeiros+spam com.gmail> writes:
Steven Schveighoffer wrote:
 
 The default opCmp today is:
 
 int opCmp(Object o) { return this !is o;}
 
It is not. For DMD 2.012, it is: int opCmp(Object o) { // BUG: this prevents a compacting GC from working, needs to be fixed //return cast(int)cast(void *)this - cast(int)cast(void *)o; throw new Error(cast(string) ("need opCmp for class " ~ this.classinfo.name)); } It's also the same in DMD 1.023 (except without the string cast). -- Bruno Medeiros - MSc in CS/E student http://www.prowiki.org/wiki4d/wiki.cgi?BrunoMedeiros#D
Apr 25 2008
parent "Steven Schveighoffer" <schveiguy yahoo.com> writes:
"Bruno Medeiros" wrote
 Steven Schveighoffer wrote:
 The default opCmp today is:

 int opCmp(Object o) { return this !is o;}
It is not. For DMD 2.012, it is: int opCmp(Object o) { // BUG: this prevents a compacting GC from working, needs to be fixed //return cast(int)cast(void *)this - cast(int)cast(void *)o; throw new Error(cast(string) ("need opCmp for class " ~ this.classinfo.name)); } It's also the same in DMD 1.023 (except without the string cast).
Yes, you are right. I was looking at my tango tree, which I assumed was the same. Apparently this is a Tango issue, and not a Phobos issue. Looking at the Phobos tree, it was never this !is o, in fact it changed from the commented out comparison of memory addresses to the new version in Phobos 0.163 So I'll post this bug for Tango, and I believe now the system is at least correct, even if it isn't intuitive for Phobos at least. -Steve
Apr 25 2008
prev sibling parent reply Bruno Medeiros <brunodomedeiros+spam com.gmail> writes:
Janice Caron wrote:
 
 And it gets worse. For complex numbers we (correctly) have the following
result:
 
     cdouble x = 1;
     cdouble y = 1i;
 
     (x !<>= y) == true
 
 Try doing that with a custom type! The problem is, opCmp() just isn't
 powerful enough. With opCmp, there is no way to say "not less than,
 not equal to, and not greater than".
 
 I think it's time to take a fresh look at comparisons, and to realise
 that comparison is /special/, in much the same way that a constructor
 is special. It shouldn't be a regular function. It should be a special
 function, with its own special syntax, and some extra special rules
 that make it (1) easy to write, (2) logical, and (3) behave sensibly.
 
You say: " The problem is, opCmp() just isn't powerful enough. With opCmp, there is no way to say "not less than, not equal to, and not greater than" " I don't mean offense, but I find such idea ridiculous! opCmp is a mechanism for defining the ordering in types that have have full ordering. What's the problem there? Why should it be extended to support something more? Your proposal added more complicated syntax and semantics to D to support comparison for types that don't have full ordering (like complex numbers), but such use cases are very specific and uncommon. And that's why I find the idea of complicating the language to support them very unlikable. Furthermore, while the semantics of full ordering are the same for all types that support it, the semantics of partial ordering might not be. For example, the comparison operators for complex types might not be quite applicable for other kinds of partial ordering mathematical types (I wish I was a mathematician so I could remember a concrete example). No, this is the kind of stuff that should be handled with plain member functions. And this same logic applies to complex types themselves: I think that complex types (and their specific operators) should be a library type instead of a built-in type, and their specific comparison operators should be member functions. Walter has been thinking about this, and hopefully he will go forward with it. -- Bruno Medeiros - MSc. in CS/E student http://www.prowiki.org/wiki4d/wiki.cgi?BrunoMedeiros#D
Apr 25 2008
parent Bruno Medeiros <brunodomedeiros+spam com.gmail> writes:
Bruno Medeiros wrote:
 Janice Caron wrote:
 And it gets worse. For complex numbers we (correctly) have the 
 following result:

     cdouble x = 1;
     cdouble y = 1i;

     (x !<>= y) == true

 Try doing that with a custom type! The problem is, opCmp() just isn't
 powerful enough. With opCmp, there is no way to say "not less than,
 not equal to, and not greater than".

 I think it's time to take a fresh look at comparisons, and to realise
 that comparison is /special/, in much the same way that a constructor
 is special. It shouldn't be a regular function. It should be a special
 function, with its own special syntax, and some extra special rules
 that make it (1) easy to write, (2) logical, and (3) behave sensibly.
You say: " The problem is, opCmp() just isn't powerful enough. With opCmp, there is no way to say "not less than, not equal to, and not greater than" " I don't mean offense, but I find such idea ridiculous! opCmp is a mechanism for defining the ordering in types that have have full ordering. What's the problem there? Why should it be extended to support something more? Your proposal added more complicated syntax and semantics to D to support comparison for types that don't have full ordering (like complex numbers), but such use cases are very specific and uncommon. And that's why I find the idea of complicating the language to support them very unlikable. Furthermore, while the semantics of full ordering are the same for all types that support it, the semantics of partial ordering might not be. For example, the comparison operators for complex types might not be quite applicable for other kinds of partial ordering mathematical types (I wish I was a mathematician so I could remember a concrete example). No, this is the kind of stuff that should be handled with plain member functions. And this same logic applies to complex types themselves: I think that complex types (and their specific operators) should be a library type instead of a built-in type, and their specific comparison operators should be member functions. Walter has been thinking about this, and hopefully he will go forward with it.
I have just read your new thread "A Fresh Look at Comparisons, Take 2" (I've started reading 1400 late posts in digitalmars.D alone, so it's a bit confusing...) , and it seems that you converged to this same opinion: "[...] Library solutions such as std.algorithm may be able to accept partially ordered sets where appropriate. But I think that it's such a rare edge-case that having it built into the language itself is probably unnecessary. After all, we don't have that now, and I've never seen a complaint about its absence." -- Bruno Medeiros - Software Developer, MSc. in CS/E graduate http://www.prowiki.org/wiki4d/wiki.cgi?BrunoMedeiros#D
Apr 25 2008