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.