April 14, 2008
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).
April 14, 2008
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.
April 14, 2008
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... :-)
April 14, 2008
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
April 14, 2008
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.
April 14, 2008
Excellent proposal. Something along these lines is badly needed.
April 15, 2008
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

April 15, 2008
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
April 15, 2008
On Tue, 15 Apr 2008 07:49:21 +0100, Janice Caron <caron800@googlemail.com> 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 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.

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 it 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 == 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.
>
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.
April 15, 2008
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).