On Thursday, 27 September 2018 at 05:12:06 UTC, Jonathan M Davis wrote:
On Wednesday, September 26, 2018 10:20:58 PM MDT Chad Joan via Digitalmars- d-learn wrote:
...

That's interesting!  Thanks for mentioning.

If you don't mind, what are the complaints regarding Object? Or can you link me to discussions/issues/documents that point out the shortcomings/pitfalls?

I've probably run into a bunch of them, but I realize D has come a long way since that original design and I wouldn't be surprised if there's a lot more for me to learn here.

I can point you to the related DIP, though it's a WIP in progress

https://github.com/andralex/DIPs/blob/ProtoObject/DIPs/DIPxxxx.md

There are also these enhancement requests for removing the various member functions from Object (though they're likely to be superceded by the DIP):

https://issues.dlang.org/show_bug.cgi?id=9769 https://issues.dlang.org/show_bug.cgi?id=9770 https://issues.dlang.org/show_bug.cgi?id=9771 https://issues.dlang.org/show_bug.cgi?id=9772

Basically, the problems tend to come in two areas:

1. Because of how inheritance works, once you have a function on a class, you're forcing a certain set of attributes on that function - be it type qualifiers like const or shared or scope classes like pure or @safe. In some cases, derived classes can be more restricted when they override the function (e.g. an overide can be @safe when the original is @system), but that only goes so far, and when you use the base class API, you're stuck with whatever attributes it has. Regardless, derived classes can't be _less_ restrictive. In fact, the only reason that it's currently possible to use == with const class references in D right now is because of a hack. The free function opEquals that gets called when you use == on two class references actually casts away const so that it can then call the member function opEquals (which doesn't work with const). So, if the member function opEquals mutates the object, you actuall get undefined behavior. And because Object.opEquals defines both the parameter and invisible this parameter as mutable, derived classes have to do the same when they override it; otherwise, they'd be overloading it rather than overriding it.


You're right, I wouldn't be caught dead wearing that.

:)

But yeah, thanks for pointing that out. Now I know not to mutate things in an opEquals, even if it makes sense from the class's point of view, just in case. At least until this all gets sorted out and code gets updated to not inherit from Object.

Object and its member functions really come from D1 and predate all of the various attributes in D2 - including const. But even if we could just add all of the attributes that we thought should be there without worrying about breaking existing code, there would be no right answer. For instance, while in the vast majority of cases, opEquals really should be const, having it be const does not work with types that lazily initialize some members (since unlike in C++, D does not have backdoors for const - when something is const, it really means const, and it's undefined behavior to cast away const and mutate the object). So, having Object.opEquals be const might work in 99% of cases, but it wouldn't work in all. The same could be said for other attributes such as pure or nothrow. Forcing a particular set of attributes on these functions on everyone is detrimental. And honestly, it really isn't necessary.

Having them on Object comes from a Java-esque design where you don't have templates. With proper templates like D2 has, there normally isn't a reason to operate on an Object. You templatize the code rather than relying on a common base class. So, there's no need to have Object.toString in order have toString for all classes or Object.opEquals to have opEquals for all classes. Each class can define it however it sees fit. Now, once a particular class in a hierarchy has defined a function like opEquals or toString, that affects any classes derived from it, but then only the classes derived from it are restricted by those choices, not every single class in the entire language as has been the case with Object.


That makes sense. Also, compile-time inheritance/duck-typing FTW, again.

This is also reminding me of how it's always bugged me that there isn't a way to operator overload opEquals with a static method (or even a free function?), given that it would allow the class/struct implementer to guard against (or even interact intelligently with) null values:

import std.stdio;

class A
{
        int payload;

        bool opEquals(int rhs)
        {
                if ( rhs == int.max )
                        return false;
                else
                        return this.payload == rhs;
        }
}

class B
{
        int payload;

        static bool opEquals(B lhs, int rhs)
        {
                if ( lhs is null && rhs == int.max )
                        return true;
                else
                {
                        if ( rhs == int.max )
                                return false;
                        else
                                return lhs.payload == rhs;
                }
        }
}

void main()
{
        A a1 = new A();
        assert(a1 != int.max);

        /+A a2 = null;
        if ( a2 == int.max )
                writeln("Even though it'd be nice to compare these things, "~
                        "we should crash before this writeln.");
        +/

        B b2 = null;
        //if ( b2 == int.max )
        if ( B.opEquals(b2, int.max) )
                writeln("Correct!");
        else
                assert(0);
}


2. The other big issue has been that built-in monitor. It allows us to have synchronized classes, but in most cases, it's unnecessary overhead. _Most_ classes don't do anything with synchronized, so why have the monitor? It really should just be in those classes that need it. With Object as the base class for all D class, every class gets it whether it needs it or not. With the ProtoObject DIP, only those classes which specifically ask for it (or which don't bother to specify a base class and thus continue to use Object as their base class) will continue to have a monitor object.


Makes sense (to fix).

A related issue that Andrei likes to bring up occasionally (though I don't think that much of anyone else has complained about) is that synchronized is one of those things that the language can do that we can't duplicate without the languages help. With synchronized, you can have a const or immutable object with a mutex inside it which works perfectly fine, but without synchronized, that's not possible because of the transitivity of const and immutable. synchronized and the monitor object give us a backdoor that we can't emulate, and Andrei doesn't like language features where the language has a superpower that you can't emulate (another, unrelated example that he likes to bring up sometimes would be how when you pass a dynamic array to a templated function, it's instantiated with the tail-const version of the type, which doesn't work with user-defined types and actually would pose some interesting problems to implement for user-defined types).


I can sympathize. I really get this sour feeling every time I want to write a really smooth type that behaves like it came with the language and integrates with everything really well, only to realize that there are various unsolvable corner-cases that poke holes in it. D is still much better at this operator overloading thing than any of the other languages I've used, but it'd be so much better if it were just completely *perfect* at it ;)

I'm uh, all too used to languages just lopping off all of my arms and legs (just don't implement operator overloading, because that solves the problem!) and then saying that everyone is equal (because everyone is a basket case now). Merely a flesh wound etc etc...

So, in any case, because of D's powerful template system, there's no need to have any member functions on Object. There arguably isn't even any need to have any root class type. But having a root class type with member functions has proven to be a _big_ problem when attributes come into play and a minor one with regards to unnecessary overhead because of synchronized classes.

- Jonathan M Davis

Wouldn't it be helpful to have a root class type just to have a "Top" type at runtime, even if it had no members? Ex: so you could do things like make an array ProtoObject[] foo; that can contain any runtime polymorphic variables.


Thank you for the enlightening post and thorough explanation.

It's been a while since I've been able to do any D programming, so it's nice to come back and see all of the thoughtful considerations behind it and the thoughtfulness generally found in this community. I appreciate it.

Reply via email to