Discussion Thread: DIP 1042--ProtoObject--Community Review Round 1
Adam D Ruppe
destructionator at gmail.com
Tue Jan 11 15:18:00 UTC 2022
> (let's not discuss here if that is desirable or not, but,
such changes come with large overhead in terms of migration).
This is a convenient position for the DIP authors to take.
Instead of accepting reality and evaluating alternatives,
including the fact that no breaking change is actually necessary,
they say let's just "not discuss" the merits.
The reality is the only thing here that would actually require a
breaking change is removing the monitor. The attributes work
*today* and require no change at all. You could choose to remove
them but there's no actual need - the DIPs statement that they do
more harm than good is false. (The only thing that is kinda bad
about them is Object a < b could be a compile error instead of a
runtime error, so there is room for improvement, but this hasn't
proven to be a problem in practice. I suppose the extra slots in
the vtable is a potential space optimization but since that's per
class instead of per instance, it doesn't add up.)
Removing the monitor is a breaking change, but you have to get
into the details of the migration path to evaluate the weight of
that. Just saying "breaking change, no discussion" means nothing
can happen - fixing any bug can be a breaking change if someone
depended on it. Adding ANY symbol can be a breaking change due to
name conflicts.
Breaking changes with a clear migration path is a short-term
annoyance that can be justified by the long term benefit.
Adding another subtly different way of doing things is a long
term annoyance and this cost needs to be considered.
Recently, on the chat room, someone asked what the definition of
"method" vs "function" is in __traits(isVirtualMethod) vs
isVirtualFunction. It looks to me like isVirtualFunction was
simply buggy, and instead of fixing the bug, they added another
one that's the same except for the fixed bug and a new name.
That was added in February 2012. Now, here, nine years later,
people are still wasting their time trying to figure out which
one to use and why. (It would at least help a lot of the
documentation explained it!)
In fact, here's the exact thing:
commit adb62254d26ab0b29f543f2562a55b331f4ef297
Author: Walter Bright <walter at walterbright.com>
Date: Sun Jan 22 00:35:46 2012 -0800
fix Issue 1918 - __traits(getVirtualFunctions) returns final
functions
https://issues.dlang.org/show_bug.cgi?id=1918
Bug filed in 2008. In 2012, something was finally done:
"Documenting the agreement reached with Walter:
We'll define __traits(getVirtualMethods) to do the "right" thing,
i.e. only include final methods that actually override something,
and put __traits(getVirtualFunctions) on the slow deprecation
path."
Well, there's been no such slow deprecation path. I had to go
hunting quite a bit to find this history to explain to the poor
new user why getVirtualFunctions returned non-virtual functions
as well as virtual methods and why getVirtualMethods and
isVirtualMethod are the only reference to "method" in the traits
documentation.
If there was a deprecation path, perhaps this could have been
avoided... but then you're deprecating it anyway. Might as well
just do it and move on.
With a trait, there's more of an argument to leave things alone
since reflection code is hard to catch subtle differences for
migration. It can certainly be done - the compiler could detect a
case where the answer changed and call it out as it happens. For
example, for this it could see __traits(getVirtualFunctions) and
say "Thing.foo is final, which was returned by
getvirtualFunctions until version 2.058, but is excluded now. If
you need that, use __traits(allMembers) instead. You can use
__traits(isVirtualFunction) || __traits(isFinalFunction) to
filter it to the same set the old getVirtualFunctions returned.
Or if you are getting the behavior you want now, pass
-silence=your.module:1918 or import core.compiler; and add
@compatible(2058) to your module definition to suppress this
warning."
Yeah, it is a little wordy, but that'd tell the user exactly what
to do to make an informed choice. Odds are, they assumed
getVirtualFunctions actually, you know, got virtual functions, so
this warning is likely bringing a bug to their attention, not
breaking code.
And then, when January 2022 comes around and people are on dmd
2.098, there's no more mystery. No more support burden.
Again, this is one of the hardest cases, fixing a user-visible
bug in a reflection routine. And it can be done providing a long
term benefit.
That's the question we have: short term minor code adjustments or
long term job opportunities for Scott Meyer, Gary Bernhardt, and
friends?
Similarly, let's talk about the monitor. I personally feel a big
"meh" about an extra pointer in class instances. I actually use
`synchronized(obj)` a decent amount anyway, so the benefits are
real for me and the cost is irrelevant.
But what are the real options we have here?
1) Leave things the way they are.
2) Implicitly add a monitor to classes that use
`synchronized(this)` so they keep magically working, but remove
it from others. An outside `synchronized(obj)` would be a
deprecation warning if it hasn't opted in until the period where
it becomes a full error. People can always version-lock their
compiler if they can't handle the warning.
3) Deprecate `synchronized(obj)` entirely in favor of
`synchronized(explicit_mutex)`, no magic, it tells you to
migrate. If someone misses the migration window, it becomes a
hard build error with the same fix still spelled out - just use
an explicit mutex instead. People can always version-lock their
compiler if they can't handle the error.
Note that in both cases, you can add a Mutex *today* so libraries
would be compatible with both old and new compilers if they
follow the migration path with not need for version statements or
anything else; it really is a painless process.
4) Abandon the Object class that everyone actually uses in favor
of a new ProtoObject class. Have to explain to people at least
nine years later why Object is there but subtly different than
ProtoObject. All future code will have to write `class Foo :
ProtoObject` instead of `class Foo` in perpetuity to get the
benefits. The lazy author or the new user who doesn't know any
better (since the default is "wrong") will never see the new
benefits.
Libraries that do use ProtoObject will require special attention
to maintain compatibility with older compilers. At least a
`static if(__VERSION__ < 2100) alias ProtoObject = Object;` as a
kind of polyfill shim. Since this isn't the default, good chance
various libraries just won't do it and the ones that do now risk
an incompatibility in the dependency tree. End user applications
stop building anyway.
DLF propagandists will have to spend most their days
(unsuccessfuly) fighting off comments on Reddit and Hacker News
about how D is a verbose joke of a has-been language.
Like I said, I use synchronized(thing) somewhat often. A few of
them are actually already mutexes, but I see 42 instances of it
in the arsd repo and I have a few in my day job proprietary
codebase too. By contrast, I have over 600 class definitions.
Which is easier to update, 42 or 600?
Nevertheless, I lean toward option #3.
This would give the most benefit to the most people:
* New users find things just work
* classes that don't need the monitor automatically get the
enhancement, no effort required by the author. It continues to
just work on old versions with graceful degradation again at no
effort.
* classes that DO need the monitor are given a very simple
migration path with both backward and forward compatibility. The
code requires only a trivial modification and then they actually
get a little more clarity and control over the tricky
synchronization code thanks to the explicitness.
I'm directly responsible for over 200,000 lines of D code,
including one of the most popular library collections and several
proprietary applications. I'm also one of the most active
front-line support representatives for D and thus regularly field
questions from new users.
No matter which choice is made, it is going to impact me. Having
to change ~50 / >200,000 lines of code, with the compiler telling
me exactly where and what to do, which will take me about an hour
is *significantly less pain* than dealing with the consequences,
both long and short term, of this DIP's solution.
And this is the most valuable part of the dip. The rest of it is
even worse.
No wonder why the authors say to "not discuss here if that is
desirable". They're clearly on the losing side of any rational
discussion.
More information about the Digitalmars-d
mailing list