May 14, 2019
On Tuesday, 14 May 2019 at 20:36:08 UTC, Eduard Staniloiu wrote:

> Should `opCmp` return a float?
>
> The reason: when we attempt to compare two types that aren't comparable (an unordered relationship) we can return float.NaN. Thus we can differentiate between a valid -1, 0, 1 and an invalid float.NaN comparison.

Thinking about this a little more, why would the compiler even allow comparing two types that aren't comparable?  Shouldn't that be a compiler error?

Mike


May 14, 2019
On Tuesday, 14 May 2019 at 20:36:08 UTC, Eduard Staniloiu wrote:
> Adding some more context to this

thanks, yeah I missed most of dconf for various reasons (hopefully will catch up when the videos released though) so this is good to read.

> Jonathan's question got us to the point raised: maybe it doesn't make much sense to be able to compare two `ProtoObjects`, so maybe you shouldn't be able to. This would change the interface to

Yeah, I would agree with that. It is hard to imagine a class that should actually be ordered with respect to a totally separate and unknown class. How would you possibly compare `MyDomElement < MySimpleWindow`? It's absurd.

Let's try to think of a case where it might make sense. Maybe I define class MyString. That can be sorted along side with other MyStrings. Ditto with char[].

class MyString : Ordered!MyString, Ordered!(char[])

So what if you defined YourString that is basically the same. I can see the argument where maybe someone, using our two libraries together, would want to:

ProtoObject[] objs;
objs ~= new MyString();
objs ~= new YourString();
sort(objs);

But how would you even implement that now? You have to cast inside opCmp... and that means one of the two classes must know about the other to realize that cast.

And if they knew about the other, they could explicitly declare that in the interface list.

And then you would store `Ordered!YourString[] objs;` instead of `ProtoObject`; make an array of the least common ancestor that actually defines the necessary interface.

So...

> ```
> interface Ordered(T)
> {
>     int opCmp(scope const T rhs);
> }
> ```

Yeah, I don't think we actually lose anything of value going this way. Let's do it.


> Since we are here, I want to raise another question:
> Should `opCmp` return a float?

My view is if they cannot be compared, they shouldn't implement the interface.

For cases where some values are comparable and some aren't (like float.nan or maybe null), I'm kinda ok just saying you return -1 or whatever and it is undefined order.
May 14, 2019
On Tuesday, 14 May 2019 at 21:21:56 UTC, Mike Franklin wrote:
> Thinking about this a little more, why would the compiler even allow comparing two types that aren't comparable?  Shouldn't that be a compiler error?

I agree that ==, <, > should not be allowed for a bare ProtoObject and that violation should be a compilation error, explaining what interface you have to implement to get comparison.

The point of ProtoObject is to fix the problems of Object. It's questionable that class Object allows comparison at compile time, but immediately throws at runtime:

    int opCmp(Object o)
    {
        throw new Exception("need opCmp for class " ~ typeid(this).name);
    }

This brought a latent bug in my code: I put a class that didn't override opCmp into an associative array. All goes well as long as the hashes were different. Then, months later, the code crashes when two hashes collide and opCmp is called.

Certainly that bug was my fault, but maybe we can prevent such bugs at compile time with ProtoObject?

-- Simon
May 14, 2019
On Tuesday, May 14, 2019 3:21:56 PM MDT Mike Franklin via Digitalmars-d wrote:
> On Tuesday, 14 May 2019 at 20:36:08 UTC, Eduard Staniloiu wrote:
> > Should `opCmp` return a float?
> >
> > The reason: when we attempt to compare two types that aren't comparable (an unordered relationship) we can return float.NaN. Thus we can differentiate between a valid -1, 0, 1 and an invalid float.NaN comparison.
>
> Thinking about this a little more, why would the compiler even allow comparing two types that aren't comparable?  Shouldn't that be a compiler error?

The issue is being able to reproduce the behavior of comparing floating point types when one of them is NaN. That design of floating point comparison does make some sense with regards to floating points but seriously complicates things for overloading opCmp. In general, it really doesn't make sense to return anything other than an integral value, but if you want to be able to create a type that wraps floats (e.g. Nullable!float) or emulates them in some manner, then you need more flexibility.

Given that, it's probably sufficient if the spec requires that the type be comparable with -1, 0, and 1 with the comparison operators being generated from that rather than requiring a specific type (which IIRC is more or less what the spec currently says for structs). Even if a class returned some weird type that took int or float for its opCmp so that you when was compared against -1, 0, and/or 1 to generate a comparison operator, I don't _think_ that you could really do anything with that ultimately other than just get weird results for the comparison.

As long as the code using the comparison operators with a class object is templated, it really shouldn't matter what the exact signature is so long as it compiles with the right types - just like with structs. And if the code isn't templated, then it would likely be operating on whatever the base class was that implemented opCmp in that particular class hierarchy. Either way, I don't see any reason for the rules for the signature of opCmp with classes to be any different from opCmp and structs aside from where a derived class is restricted by how its base class declared it.

- Jonathan M Davis



May 14, 2019
On Tuesday, May 14, 2019 1:34:12 PM MDT Andrei Alexandrescu via Digitalmars- d wrote:
> In designing ProtoObject and comparison for equality and ordering, we've assumed all class objects are supposed to be comparable (including ProtoObject themselves). That means code like this should always compile:
>
> bool fun(C, D)(C x, D y) if (is(C == class) && is(D == class))
> {
>     return x < y && x == y;
> }
>
> That is, any two class objects should be comparable for equality (==,
> !=) and ordering (<. >, <=, >=). The decision whether comparison
> actually works for the types involved is deferred to runtime.
>
> This is in keeping with Java, C#, and existing D where Object has built-in means for comparison.
>
> However the question Jonathan M Davis asked got me thinking - perhaps we should break with tradition and opt for a more statically-checked means of comparison. The drawback is that some objects would NOT be comparable, which may surprise some users.
>
> As a consequence, for example, creating hash tables keyed on certain types will not work. This is not quite unheard of as a type could disable opEquals. Also, by default struct types cannot be compared for ordering - they must define opCmp.
>
> Should we go with a more statically-checked/imposed approach with comparison, or stick with OOP tradition? Ideas welcome.

Well, I think that my stance on it is pretty clear at this point. Languages like Java and C# had to put these functions on Object, because they didn't have templates and thus had to have containers and the like contain Object rather than the actual type. D does not have that problem.

By templatizing the core infrastructure such as the free function opEquals that == gets lowered to for classes, any class that defined the appropriate function could then work with whatever attributes were on it (basically like we have now for structs). As soon as a class defined opEquals or toString or whatever, any classes derived from that class would then be restricted by the attribute choices on the base class, but only that class hierarchy would then have to live with that decision rather than the entire language like what we have now with Object or what was shown with interfaces in Eduard's talk. So, each class hierarchy could use whichever set of attributes made sense for it.

All we'd really lose that I can see is the ability to compare classes from disparate hierarchies, which has never really worked properly or made much sense anyway. You just end up with unrelated objects not being considered equal instead of statically knowing that you're comparing objects that aren't really logically comparable. The full dynamic capabilities of inheritance and polymorphism are still there within class hierarchies. They just aren't at the root object level and thus would require casting to more specific types to use them. And by not having them at the root object level, we avoid the problem of locking in a particular set of attributes.

I think that all of this fits in quite well with how we've already been discussing templatizing more of druntime so that it's pay as you go. The only serious implementation issue that I'm aware of would be the built-in AAs, since there would be no common base class or interface with opEquals or toHash (meaning that at least for now, only Object could be put in the built-in AAs and not classes that aren't derived from Object). But that would be fixed by templatizing the AA implementation, which is already something that we've wanted to do. IIRC, Martin Nowak was working on something along those lines previously, but I don't know where that work currently stands.

- Jonathan M Davis



May 14, 2019
On Tue, May 14, 2019 at 03:47:18PM -0600, Jonathan M Davis via Digitalmars-d wrote:
> On Tuesday, May 14, 2019 3:21:56 PM MDT Mike Franklin via Digitalmars-d wrote:
[...]
> > Thinking about this a little more, why would the compiler even allow comparing two types that aren't comparable?  Shouldn't that be a compiler error?
> 
> The issue is being able to reproduce the behavior of comparing floating point types when one of them is NaN. That design of floating point comparison does make some sense with regards to floating points but seriously complicates things for overloading opCmp. In general, it really doesn't make sense to return anything other than an integral value, but if you want to be able to create a type that wraps floats (e.g. Nullable!float) or emulates them in some manner, then you need more flexibility.
[...]

Moreover, Andrei has mentioned before that opCmp can technically be used for implementing partial orders. I had thought otherwise in the past, because I only considered opCmp that returns int. However, if opCmp is allowed to return float, then you can return float.nan for the incomparable case (e.g., two sets that are not subsets of each other) and thus achieve a non-linear partial ordering, such as the subset relation.

Whether or not this is a *good* way of implementing the subset operation is a different question, of course. If we restricted opCmp to only linear orderings, then this wouldn't be an issue.


T

-- 
Curiosity kills the cat. Moral: don't be the cat.
May 14, 2019
On Tue, May 14, 2019 at 05:05:02PM -0400, Andrei Alexandrescu via Digitalmars-d wrote:
> On 5/14/19 9:37 PM, Mike Franklin wrote:
[...]
> > IMO, objects should only support reference equality out of the box.
> 
> So that means "x is y" works but not "x == y"?

That makes sense to me if x and y are two completely unrelated classes. Yes they are "related" in the sense that both inherit from ProtoObject, but they have no meaningful relationship as far as the application domain is concerned.  If the user wants to compare them, let him implement the Comparable interface and the corresponding opCmp(). This shouldn't be in ProtoObject.


[...]
> > I would like to distinguish between reference equality and value equality.  Reference equality, I think, should be done with the `is` operator, and should probably work out of the box. And I like the idea of users opting into any feature.  If users want to support value equality, they should implement `opEquals`.  If they want comparability they should implement `opCmp`.  If they want to support hashability, the should be required to implement a `getHash` or something along that line of thinking.
> 
> Sounds good. If we go with the notion "you can't key a built-in hashtable on any class type" that should work.

I think it would make more sense to have to implement a Hashable interface (defining an opHash and possibly also inheriting from Comparable if we want to allow AA implementations that require ordering, e.g., to use tree buckets) for this purpose.  While being able to throw any arbitrary object into an AA and having it Just Work is kinda nice, that's an optional extra and we shouldn't be bending over backwards just to support that.

Besides, realistically speaking, how useful is an AA that may contain any arbitrary class, where looking up one key might give you a GuiWidget but looking up a different key gives you a CryptoAlgorithm and looking up a third key gives you a GeometricShape?  I can't see how such a thing could be useful in any meaningful way.  I'd expect that if the user wanted to put two different classes in the same AA he'd at least bother to define a common base class that implements the appropriate Hashable interface.


T

-- 
The volume of a pizza of thickness a and radius z can be described by the following formula: pi zz a. -- Wouter Verhelst
May 14, 2019
On Tuesday, 14 May 2019 at 21:06:05 UTC, Mike Franklin wrote:
> On Tuesday, 14 May 2019 at 20:36:08 UTC, Eduard Staniloiu wrote:
>
>> Should `opCmp` return a float?
>>
>> The reason: when we attempt to compare two types that aren't comparable (an unordered relationship) we can return float.NaN. Thus we can differentiate between a valid -1, 0, 1 and an invalid float.NaN comparison.
>
> Seems like a job for an enum, not a float or an integer.
>
> Mike

+1 for enum. As far as I can see you only have four actual states:

lower, equal, higher, nonComparable
May 14, 2019
On 5/14/19 12:36 AM, Seb wrote:
> On Tuesday, 14 May 2019 at 21:06:05 UTC, Mike Franklin wrote:
>> On Tuesday, 14 May 2019 at 20:36:08 UTC, Eduard Staniloiu wrote:
>>
>>> Should `opCmp` return a float?
>>>
>>> The reason: when we attempt to compare two types that aren't comparable (an unordered relationship) we can return float.NaN. Thus we can differentiate between a valid -1, 0, 1 and an invalid float.NaN comparison.
>>
>> Seems like a job for an enum, not a float or an integer.
>>
>> Mike
> 
> +1 for enum. As far as I can see you only have four actual states:
> 
> lower, equal, higher, nonComparable

This won't work because the result of opCmp is compared against zero. Using a floating point number is likely to be more efficient.
May 14, 2019
On 5/14/19 12:24 AM, H. S. Teoh wrote:
> Besides, realistically speaking, how useful is an AA that may contain
> any arbitrary class, where looking up one key might give you a GuiWidget
> but looking up a different key gives you a CryptoAlgorithm and looking
> up a third key gives you a GeometricShape?  I can't see how such a thing
> could be useful in any meaningful way.

Definitely meaningful. The key would be an object, not the value. There are many applications in which you'd want to associate data with a variety of objects.