May 15, 2019
On Wednesday, May 15, 2019 2:56:53 AM MDT Steven Schveighoffer via Digitalmars-d wrote:
> On 5/14/19 9:36 PM, Eduard Staniloiu wrote:
> > 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
> > ```
> > interface Ordered(T)
> > {
> >
> >      int opCmp(scope const T rhs);
> >
> > }
> > ```
> >
> > Now the attributes of `opCmp` will be inferred.
>
> Just wanted to make sure you understand this is not the case. opCmp in this instance is a virtual call, and will NOT have attributes inferred.
>
> There isn't really a way to define an interface for this, nor do you need to.
>
> Just define the opCmp you want in your own interface/base object, and then you can compare those. Almost nobody wants to compare 2 completely unrelated objects.

Indeed. Inference comes when the code around the member function is templated. For instance, if you have phobos' RedBlackTree, it uses opCmp, and it's templated. So, opCmp on the class can then have whatever attributes are appropriate and the code in RedBlack that uses opCmp will infer those attributes rather than forcing @safe or nothrow or whatever.

Similarly, with the free functions opEquals being templated, it would have its attributes inferred based on how opEquals was defined on the classes being compared, allowing == to be @safe, pure, etc. - or not - based on how opEquals was declared on those classes, thereby allowing classes with an @safe opEquals to be used in @safe code.

The functions on the class itself can only have their attributes inferred if they're templated (which means that they can't be virtual), or they return auto. So, in most cases, the attributes on these functions on classes probably won't use inferrence, but the code that uses them will.

- Jonathan M Davis



May 15, 2019
On 2019-05-15 04:09, Andrei Alexandrescu wrote:

> In this case, ProtoObject is a carefully considered addition that bypasses a malfunctioning artery - the Object class. But it doesn't change or remove it, because there's much live tissue connected to it. Changing how opCmp works - inserting a little botox during the surgery - is a damaging distraction.

It will cause issues with code that does `is(T : Object)` and expect that to be the root class. In other cases the same code has been used to identify D classes before `__traits(getLinkage)` was available for classes.

-- 
/Jacob Carlborg
May 15, 2019
On Wednesday, May 15, 2019 3:54:15 AM MDT Jacob Carlborg via Digitalmars-d wrote:
> On 2019-05-15 04:09, Andrei Alexandrescu wrote:
> > In this case, ProtoObject is a carefully considered addition that bypasses a malfunctioning artery - the Object class. But it doesn't change or remove it, because there's much live tissue connected to it. Changing how opCmp works - inserting a little botox during the surgery - is a damaging distraction.
>
> It will cause issues with code that does `is(T : Object)` and expect that to be the root class. In other cases the same code has been used to identify D classes before `__traits(getLinkage)` was available for classes.

is(T == class) should work. And as it is, IIRC, using is(T : Object) is already problematic due to extern(C++) classes not being derived from Object. I think that there's also an issue with interfaces not necessarily being classes due to how D handles COM, but fortunately, I haven't had to do anything with COM recently, so I'm not very familiar with D's COM support. There's also the problem that is(T : Object) can be true if alias this is used. So, is(T : Object) probably shouldn't be used much even now, but I do think that it's likely that adding classes below Object will break some existing code. I'm not sure that there's much that we can do about that though.

- Jonathan M Davis



May 15, 2019
On Tuesday, 14 May 2019 at 19:34:12 UTC, Andrei Alexandrescu wrote:
> Should we go with a more statically-checked/imposed approach with comparison, or stick with OOP tradition? Ideas welcome.

I am all in on the no-change just-addition static version. And thanks for Jonathan for bringing this up.
May 15, 2019
On 15.05.19 07:30, H. S. Teoh wrote:
> On Tue, May 14, 2019 at 09:02:49PM -0400, Andrei Alexandrescu via Digitalmars-d wrote:
> [...]
>> Well there would be in some instances. People often implement
>> comparisons as a - b currently, where a and b are int expressions.
> [...]
> 
> FYI, the result of that is actually incorrect in some cases.  (Consider
> what happens when there is overflow involved, such as int.max
> - int.min, and remember the type of the result.) So, not exactly the
> kind of code we should recommend, let alone bend over backwards to
> support.
> 
> Int comparisons should be left as built-in operators <, =, >, etc.,
> which then get translated to the correct hardware instructions that
> don't suffer from the overflow bug.
> 
> 
> T
> 

That's not an option with opCmp, because the operators cannot be overloaded independently. Built-in types should just support overloaded operator syntax, so one can write a.opCmp(b) instead of trying to get overly clever with a manual implementation.
May 15, 2019
On Wednesday, 15 May 2019 at 06:19:19 UTC, Jonathan M Davis wrote:
> Even an interface is a problem, because then that locks in the attributes.

That's not completely true in general, and only sort of true in this specific situation.

In general, attributes are semi-locked, accordance to the Liskov substitution principle: you can tighten constraints, but not loosen them on child classes.

(Like with current Object, which has no attributes, you are allowed to define a child class with `override @nogc @safe nothrow` etc. But once you do that, you can never remove them in child classes, since then you'd be breaking the promise of the parent class' interface)


In this specific situation, the attributes are only locked in after you define them - which is the same if you did it with or without the interface. The interface itself does not demand anything unless it specifically lists them.

But, note, they also are not inferred.

> By templatizing all of the relevant code in druntime

There should be NO code in druntime! All `a < b` is is syntax sugar for a method call. It does not need and should not use any other function. Just let the compiler rewrite the syntax to the call and then do its thing.

The ProtoObject version of __cmp should simply not exist.

May 15, 2019
On Wednesday, 15 May 2019 at 07:10:44 UTC, Dominikus Dittes Scherkl wrote:
> On Tuesday, 14 May 2019 at 20:36:08 UTC, Eduard Staniloiu wrote:
>> (comparing two `ProtoObject`s) we can not establish any relationship between the two, the `__cmp` lowering won't be able to compare two.
> That's ok. Why should anybody expect that two arbitrary things should have an ordered relation? Is egg > apple? And in what way? Is it heavier? longer? older? better? has more calories?
>> Since we are here, I want to raise another question:
>> Should `opCmp` return a float?
> Yes, please!
> I need unordered relations so often (see above)!

It's fine if we can add a partial order to some classes, or a linear order to other classes. There can be two static introspections/two interfaces for these two orderings. (And linear orders also qualify as partial orders during introspection.)

Comparing two objects from a partial order, such a comparison should be able to yield uncomparable, no problem.

Comparing two objects from two different class hierarchies that define two unrelated partial orders, I don't have an opinion on whether this should compile, or whether it may even return that one is greater than the other.

Comparing two ProtoObjects should always be a compilation error.

We should not assume partial orders everywhere. That can lead to roundabout code: Linear orders are common, and I don't want to handle any uncomparable case for linear orders. And readers of my code should immediately see that there is no bug here, i.e., that I didn't forget to think about the uncomparable case.

Thus I wouldn't change int opCmp to float opCmp -- for linear orders, I like a static guarantee that different objects are always comparable and never NaN.

-- Simon
May 15, 2019
On Tuesday, 14 May 2019 at 19:34:12 UTC, Andrei Alexandrescu 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.

The idea is to determine if traditional OOP didn't do it statically because they couldn't or because it was for a sound semantical reason.
A lot of runtime processing of Java and C# are done that way because the language does lack the capability to do it at compile time.
I don't have specifically the answer here, but I think that is one question that has to be evaluated here.
May 15, 2019
On Wednesday, May 15, 2019 6:40:46 AM MDT Adam D. Ruppe via Digitalmars-d wrote:
> On Wednesday, 15 May 2019 at 06:19:19 UTC, Jonathan M Davis wrote:
> > Even an interface is a problem, because then that locks in the attributes.
>
> That's not completely true in general, and only sort of true in this specific situation.
>
> In general, attributes are semi-locked, accordance to the Liskov substitution principle: you can tighten constraints, but not loosen them on child classes.
>
> (Like with current Object, which has no attributes, you are allowed to define a child class with `override @nogc @safe nothrow` etc. But once you do that, you can never remove them in child classes, since then you'd be breaking the promise of the parent class' interface)
>
>
> In this specific situation, the attributes are only locked in after you define them - which is the same if you did it with or without the interface. The interface itself does not demand anything unless it specifically lists them.
>
> But, note, they also are not inferred.

Yeah. My point isn't that the exact list of attributes you put on opEquals, opCmp, etc. is exactly what must be on derived classes. My point is that once you do or don't put some of those attributes on the function in a base class, that restricts which attributes can be put on the overrides in derived classes. The interfaces proposed in Eduard's dconf talk this year involved making all of the relevant functions @safe const pure nothrow, and that's a serious problem for any classes that want to do something with those functions that is incompatible with those attributes. By just letting user-defined classes define those functions more or less however they want (like with structs), we make it possible for each class hierarchy to define them in whatever way is appropriate for that class hierarchy, whereas if we create an interface, we're restricting what _every_ class can do with attributes on those functions - which is part of the problem that we have with Object right now.

> > By templatizing all of the relevant code in druntime
>
> There should be NO code in druntime! All `a < b` is is syntax sugar for a method call. It does not need and should not use any other function. Just let the compiler rewrite the syntax to the call and then do its thing.
>
> The ProtoObject version of __cmp should simply not exist.

That's not true for everything (opEquals specifically has a free function druntime that calls the opEquals on classes), but what it comes down to in general is that code that uses opCmp (or any of these functions) in druntime needs to be templatized rather than being designed to operate on a specific base class. So, even if opCmp itself doesn't require that code be in druntime to work, there potentially is relevant code in druntime which would need to be templatized. Certainly, that's true for opEquals and toHash because of AAs if nothing else. Regarldess, yeah, ProtoObject shouldn't have any form of any of these functions on it.

- Jonathan M Davis



May 15, 2019
On Wednesday, 15 May 2019 at 00:19:05 UTC, Andrei Alexandrescu wrote:
> On 5/14/19 10:06 PM, 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.
>
> I repeat myself: this won't work.
>
> Recall that a < b is lowered into a.opCmp(b) < 0. So we have a comparison against the literal 0. For that float works nicely because nan etc etc.

Like you say, returning a float is the only way to allow a 4-state result when comparing with 0.  However, since the Ordered interface is now a template, we can query the Type to see what they prefer:

//
// Allow the application to pick a result they want to use.
// Here's some example options that could be made available.
//
enum AlwaysComparableOpCmpResult : int
{
    lower = -1, equal = 0, higher = 1
}
enum SometimesNonComparableOpCmpResult : float
{
    lower = -1, equal = 0, higher = 1, nonComparable = float.nan
}
//
// A template that exposes the logic used to determine the opCmp return type
//
template OpCmpReturnType(T)
{
    static if (is(typeof(T.OpCmpResult))
        alias OpCmpReturnType= T;
    else
        alias OpCmpReturnType= AlwaysComparableOpCmpResult; // Default?

}
interface Ordered(T)
{
    OpCmpReturnType!T opCmp(scope const T rhs);
}


Policy-Based Programming anyone?

In the end, the only requirement of the return type of opCmp is that it being comparable to a literal "0".  So why not let the application decide?