Jump to page: 1 24  
Page
Thread overview
A Fresh Look at Comparisons, Take 2
Apr 18, 2008
Janice Caron
Apr 18, 2008
Henning Hasemann
Apr 18, 2008
Janice Caron
Apr 19, 2008
Henning Hasemann
Apr 19, 2008
Janice Caron
Apr 19, 2008
Henning Hasemann
Apr 19, 2008
Janice Caron
Apr 19, 2008
Yigal Chripun
Apr 20, 2008
Janice Caron
Apr 20, 2008
Yigal Chripun
Apr 18, 2008
Janice Caron
Apr 18, 2008
Jason House
Apr 18, 2008
Janice Caron
Apr 18, 2008
Janice Caron
Apr 18, 2008
Jason House
Apr 18, 2008
Janice Caron
Apr 18, 2008
Jason House
Apr 18, 2008
Yigal Chripun
Apr 18, 2008
Janice Caron
Apr 18, 2008
Yigal Chripun
Apr 18, 2008
Janice Caron
Apr 19, 2008
Bill Baxter
Apr 19, 2008
Janice Caron
Apr 19, 2008
Bill Baxter
Apr 19, 2008
Yigal Chripun
Apr 18, 2008
Janice Caron
Apr 18, 2008
Janice Caron
Apr 18, 2008
Janice Caron
Apr 18, 2008
Yigal Chripun
April 18, 2008
This is an improved proposal based on the suggestions of Steven, Yigal, Bruce and Paul. Basically, I've ditched my original proposal entirely, and replaced it with this one. It doesn't do exactly what the previous proposal suggested, but nonetheless, it will still do the "right" thing, at least in the eyes of the posters to the last thread.


RULE 1:

We have a new function paramter storage class, "explicit", which means that only objects of the exact compile-time type may be passed in. (Implicit casting is forbidden, apart from implicit casting to const). So, for example:

    class A {}
    class B : A {}

    void f(explicit A a)
    {
        writefln("Got an A");
    }

    A a1 = new A;
    f(a1); /* OK */

    A a2 = new B;
    f(a2); /* OK - we only care about the compile-time type */

    B b = new B;
    f(b); /* ERROR - WILL NOT COMPILE */

However, we make an exception for implicit casts to const. Thus:

    void f(explicit const(A) a)
    {
        writefln("Got a const A");
    }

    A a1 = new A;
    f(a1); /* OK */

The new keyword "explicit" can also be applied to the "this" parameter of member functions, as follows:

    class A
    {
        explicit void f()
        {
            writefln("Got an A");
        }
    }

    class B : A {}

    A a1 = new A;
    a1.f(); /* OK */

    A a2 = new B;
    a2.f(); /* OK - we only care about the compile-time type */

    B b = new B;
    b.f(); /* ERROR - WILL NOT COMPILE */

Observe that explicit member functions cannot be inherited, however, the inheritance mechanism is able to "jump over" an explict function when attempting to find a match, in order to match the next non-explicit function. I didn't explain that very well, did I? Let me show you what I mean by example:

    class A
    {
        int f() { writefln("In A"); }
    }

    class B : A
    {
        explicit int f() { writefln("In B"); }
    }

    class C : A
    {
    }

    A a = new A;
    B b = new B;
    C c = new C;

    a.f(); // prints "In A"
    b.f(); // prints "In B"
    c.f(); // prints "In A"

Here, the call to c.f() matches no function in C. It cannot match
B.f(), because B.f() is explicit. But it /can/ match A.f(). And so it
does. As far as objects of static type C are concerned. B's explicit
functions might as well not exist.



RULE 2:

Neither opEquals nor opCmp shall be defined in Object.


RULE 3:

When the compiler encounters an equality test, such as

    if (a == b) ...
    if (a != b) ...

and no compile-time match can be found for a.opEquals(b), then the identity test shall be performed. That is, it shall be as if the programmer had typed:

    if (a is b) ...
    if (a !is b) ...

Note that this rule is only applicable to classes. Structs which do not implement opEquals shall be compared for exact bitwise equality, as is the case currently.


RULE 4:

When the compiler encounters a comparison test other than equality, such as

    if (a < b) ...

and no compile-time match can be found for a.opCmp(b), then all such comparisons shall be compile-time errors.

Observation: It should be possible for the programmer to test for this at compile time, by doing, for example:

    static if (is(a < b))
    {
        ...
    }



RULE 5:

No type A may be used as an associtive array key unless a match can be
found for A.opCmp(A).

Observation: It should be possible for the programmer to test for this at compile time, by doing, for example:

    static if (is(int[A]))
    {
        ...
    }


RULE 6:

Classes which implement opEquals and/or opCmp will be expected to implement them using the following signatures:

    // For classes
    class C
    {
        explicit bool opEquals(explicit const(C) c) const {...}
        explicit int opCmp(explicit const(C) c) const {...}
    }

    // For structs
    struct S
    {
        explicit bool opEquals(explicit ref const(S) s) const {...}
        explicit int opCmp(explicit ref const(S) s) const {...}
    }

(However, I don't see any way of enforcing this, so this rule
represents recommended good practice).


RULE 7:

The following tokens shall be removed from the lexer, and shall no longer have meaning in D:

    <>
    <>=
    !<>
    !<>=
    !<
    !<=
    !>
    !>=

Rationale: Every class is now either completely ordered (if opCmp is supplied), or unordered (if not). If a class is completely ordered, then <> is the same as !=; !> is the same as <=, and so on. Conversely, if a class is unordered, then rule 4 makes all such comparisons compile-time errors. Consequently, there is no longer any need for these tests.


----

How does all that sound?
April 18, 2008
> How does all that sound?

I like most of the stuff but a little point still makes me grumble (is
this a word?):

- You can still not build partially ordered classes at all? (Ie
  something similar to complex numbers) Is there a rationale for this?
- You can still have opCmp say two objects are equal and opEqual say
  they're not? (or vice versa, respectively)

No need to say my first proposal addresses these issues (but leaves out
others, though).

Henning

-- 
GPG Public Key: http://pgpkeys.pca.dfn.de/pks/lookup?op=get&search=0xDDD6D36D41911851

April 18, 2008
On 18/04/2008, Henning Hasemann <hhasemann@web.de> wrote:
>  - You can still not build partially ordered classes at all? (Ie
>   something similar to complex numbers) Is there a rationale for this?

Only that Paul said it wasn't necessary, and, on reflection, his logic is reasonable.

Basically, I think the reasoning is... what's the point? After all, if c and d are complex numbers, then

    if (c < d)

could easily be rewritten as

    if (c.im == 0 && d.im == 0 && c.re < d.re)



>  - You can still have opCmp say two objects are equal and opEqual say
>   they're not? (or vice versa, respectively)

Sure. You can also define opAdd() to do subtraction. Sometimes, you just have to rely on common sense.

(Also, there are rare circumstances where you might want to do that deliberately, so that, for example, an AA can contain duplicate "equal" keys).

Anyway, I don't really see why we would consider that a problem.
April 18, 2008
I'm not clear on whether one needs partial ordering built into a programming language. Certainly partially ordered sets exist in mathematics. For example, consider the set of four points P = { a, b, c, d }, defined such that

    a < b < d
    a < c < d

but where b cannot be compared with c (a diamond-shaped arrangement). If D fully supported partially ordered types, then we would have

    (b < c) == false
    (b > c) == false
    (b <> c) == false
    (b == c) == false

and elements of type P could not be AA keys. The question is, does D need to support this at the level of built-in comparisons? After all, one could easily bolt that sort of thing on afterwards as a bunch of plain functions, e.g.

    class P
    {
        bool partialLess(P p) {...}
        bool partialGreater(P p) {...}
    }

for those classes that need it. 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.
April 18, 2008
Janice Caron Wrote:

> How does all that sound?

IMHO, explicit is dangerous.  It suffers from all the normal pitfalls of non-virtual function calls.  Explicit helps when you have all the raw objects and have to be explicit in how you use them, but when calling functions that accept a base class or using an object factory, it gets more complex.

I also think no support comparing floating point numbers is unacceptable.

Other than that, I like the rest of the proposal (AKA rules 2-5)
April 18, 2008
On 18/04/2008, Jason House <jason.james.house@gmail.com> wrote:
>  I also think no support comparing floating point numbers is unacceptable.

All built-in types would obviously continue to be comparable!
April 18, 2008
On 18/04/2008, Jason House <jason.james.house@gmail.com> wrote:
> IMHO, explicit is dangerous.  It suffers from all the normal pitfalls of non-virtual function calls.  Explicit helps when you have all the raw objects and have to be explicit in how you use them, but when calling functions that accept a base class or using an object factory, it gets more complex.

You wouldn't use it in that circumstance. You'd use it when you needed it - i.e. when you desired the behavior it provides.

It neatly solves the problem of

    class A
    {
        /* provide opCmp */
    }
    class B : A {}
    class C : A {}

    B b = new B;
    C c = new C;
    if (b < c) ...

doing the wrong thing.
April 18, 2008
Janice Caron wrote:
> This is an improved proposal based on the suggestions of Steven, Yigal, Bruce and Paul. Basically, I've ditched my original proposal entirely, and replaced it with this one. It doesn't do exactly what the previous proposal suggested, but nonetheless, it will still do the "right" thing, at least in the eyes of the posters to the last thread.
>
>
> RULE 1:
>
> We have a new function paramter storage class, "explicit", which means that only objects of the exact compile-time type may be passed in. (Implicit casting is forbidden, apart from implicit casting to const). So, for example:
>
>     class A {}
>     class B : A {}
>
>     void f(explicit A a)
>     {
>         writefln("Got an A");
>     }
>
>     A a1 = new A;
>     f(a1); /* OK */
>
>     A a2 = new B;
>     f(a2); /* OK - we only care about the compile-time type */
>
>     B b = new B;
>     f(b); /* ERROR - WILL NOT COMPILE */
>
> However, we make an exception for implicit casts to const. Thus:
>
>     void f(explicit const(A) a)
>     {
>         writefln("Got a const A");
>     }
>
>     A a1 = new A;
>     f(a1); /* OK */
>
> The new keyword "explicit" can also be applied to the "this" parameter of member functions, as follows:
>
>     class A
>     {
>         explicit void f()
>         {
>             writefln("Got an A");
>         }
>     }
>
>     class B : A {}
>
>     A a1 = new A;
>     a1.f(); /* OK */
>
>     A a2 = new B;
>     a2.f(); /* OK - we only care about the compile-time type */
>
>     B b = new B;
>     b.f(); /* ERROR - WILL NOT COMPILE */
>
> Observe that explicit member functions cannot be inherited, however, the inheritance mechanism is able to "jump over" an explict function when attempting to find a match, in order to match the next non-explicit function. I didn't explain that very well, did I? Let me show you what I mean by example:
>
>     class A
>     {
>         int f() { writefln("In A"); }
>     }
>
>     class B : A
>     {
>         explicit int f() { writefln("In B"); }
>     }
>
>     class C : A
>     {
>     }
>
>     A a = new A;
>     B b = new B;
>     C c = new C;
>
>     a.f(); // prints "In A"
>     b.f(); // prints "In B"
>     c.f(); // prints "In A"
>
> Here, the call to c.f() matches no function in C. It cannot match
> B.f(), because B.f() is explicit. But it /can/ match A.f(). And so it
> does. As far as objects of static type C are concerned. B's explicit
> functions might as well not exist.
>
>
>
> RULE 2:
>
> Neither opEquals nor opCmp shall be defined in Object.
>
>
> RULE 3:
>
> When the compiler encounters an equality test, such as
>
>     if (a == b) ...
>     if (a != b) ...
>
> and no compile-time match can be found for a.opEquals(b), then the identity test shall be performed. That is, it shall be as if the programmer had typed:
>
>     if (a is b) ...
>     if (a !is b) ...
>
> Note that this rule is only applicable to classes. Structs which do not implement opEquals shall be compared for exact bitwise equality, as is the case currently.
>
>
> RULE 4:
>
> When the compiler encounters a comparison test other than equality, such as
>
>     if (a < b) ...
>
> and no compile-time match can be found for a.opCmp(b), then all such comparisons shall be compile-time errors.
>
> Observation: It should be possible for the programmer to test for this at compile time, by doing, for example:
>
>     static if (is(a < b))
>     {
>         ...
>     }
>
>
>
> RULE 5:
>
> No type A may be used as an associtive array key unless a match can be
> found for A.opCmp(A).
>
> Observation: It should be possible for the programmer to test for this at compile time, by doing, for example:
>
>     static if (is(int[A]))
>     {
>         ...
>     }
>
>
> RULE 6:
>
> Classes which implement opEquals and/or opCmp will be expected to implement them using the following signatures:
>
>     // For classes
>     class C
>     {
>         explicit bool opEquals(explicit const(C) c) const {...}
>         explicit int opCmp(explicit const(C) c) const {...}
>     }
>
>     // For structs
>     struct S
>     {
>         explicit bool opEquals(explicit ref const(S) s) const {...}
>         explicit int opCmp(explicit ref const(S) s) const {...}
>     }
>
> (However, I don't see any way of enforcing this, so this rule
> represents recommended good practice).
>
>
> RULE 7:
>
> The following tokens shall be removed from the lexer, and shall no longer have meaning in D:
>
>     <>
>     <>=
>     !<>
>     !<>=
>     !<
>     !<=
>     !>
>     !>=
>
> Rationale: Every class is now either completely ordered (if opCmp is supplied), or unordered (if not). If a class is completely ordered, then <> is the same as !=; !> is the same as <=, and so on. Conversely, if a class is unordered, then rule 4 makes all such comparisons compile-time errors. Consequently, there is no longer any need for these tests.
>
>
> ----
>
> How does all that sound?
> 
overall, I like your proposal - it even includes some of my ideas! :)
I still have however a few questions:
since the above "explicit" suggestion is slightly different from what I
thought, i still wonder about the following case:
class A {
    int number;
    ...
}
class B: A {
    string name;
    ...
}
A obj = new B;
A defines an explicit opCmp/opEquals as suggested above.
with the above suggestion the opCmp would accept the above "obj" ot type
B since it has static type A.
is the above behavior acceptable? I'm not sure.
for example, what if my code retrieves A's from a collection, so by
comparing only number and not name it can retrieve the wrong instance
from the collection(a real example would be a collection of widgets in a
GUI toolkit representing all the controls of a screen). the compiler
cannot check this at compile time so in order to prevent this the check
should happen at run-time and an exception should be thrown on error.

another issue is purely syntactical  - we seem to add more and more
keywords to D, and since IMO "explicit" should add runtime checks (not
compile checks as stated in the above suggestion) this "screams" to me
as a perfect case for annotations.
D really needs a standard facility to add user defined annotations
(metadata) to code and instead of adding more keywords to D, the above
"explicit" would be a perfect candidate to be provided by the standard
library. other annotations that could be also provided by the standard
library (I keep using this phrase since I prefer tango over phobos and
keep wishing for a merger) are "pure", "thread-safe", even "const" /
"invariant". less bloat for D more power to the programmer. the
annotation facility Ideally would provide hooks via an API to the
compiler so that annotations could be applied ar compile time by the
compiler (const) and/or runtime (explicit).

one last thing, adding a "comparable" interface/annotation could be
useful, so that AA's keys would need to implement this interface. of
course, all builtins would be comparable by default [except complex that
walter wants to move out of the language anyway....]
--Yigal
April 18, 2008
"Janice Caron" wrote
> Observe that explicit member functions cannot be inherited, however, the inheritance mechanism is able to "jump over" an explict function when attempting to find a match, in order to match the next non-explicit function. I didn't explain that very well, did I? Let me show you what I mean by example:
>
>    class A
>    {
>        int f() { writefln("In A"); }
>    }
>
>    class B : A
>    {
>        explicit int f() { writefln("In B"); }
>    }
>
>    class C : A
>    {
>    }
>
>    A a = new A;
>    B b = new B;
>    C c = new C;
>
>    a.f(); // prints "In A"
>    b.f(); // prints "In B"
>    c.f(); // prints "In A"
>
> Here, the call to c.f() matches no function in C. It cannot match
> B.f(), because B.f() is explicit. But it /can/ match A.f(). And so it
> does. As far as objects of static type C are concerned. B's explicit
> functions might as well not exist.
>

Huh?  I think maybe you meant C to inherit from B?  If so I see what you are trying to explain :)

I don't really agree that opEquals or opCmp should not be inherited.  I think normal inheritance the way it is now is just fine.

You need to have opEquals in Object in order to support hashing.

The default opCmp should be redefined to throw an exception.  An alternative is that opCmp is defined in an interface.  Or you could do both.  The rationale is that opCmp doesn't have a default.  Some objects just cannot be expected to have a well-defined order.

-Steve


April 18, 2008
Janice Caron Wrote:

> On 18/04/2008, Jason House <jason.james.house@gmail.com> wrote:
> > IMHO, explicit is dangerous.  It suffers from all the normal pitfalls of non-virtual function calls.  Explicit helps when you have all the raw objects and have to be explicit in how you use them, but when calling functions that accept a base class or using an object factory, it gets more complex.
> 
> You wouldn't use it in that circumstance. You'd use it when you needed it - i.e. when you desired the behavior it provides.
> 
> It neatly solves the problem of
> 
>     class A
>     {
>         /* provide opCmp */
>     }
>     class B : A {}
>     class C : A {}
> 
>     B b = new B;
>     C c = new C;
>     if (b < c) ...
> 
> doing the wrong thing.

How about this:
class A
{
  /* provide opCmp */
}
bool foo(A a1, A a2){ return a1<a2; }
...
class B : A {}
class C : A {}
B b = new B;
C c = new C;
return foo(B,C);


I'm sure you could say that foo must now declare its parameters as explicit, but requiring all functions that compare A's to have explicit parameters really starts to kill the whole utility of having a base class to begin with.
« First   ‹ Prev
1 2 3 4