Discussion Thread: DIP 1042--ProtoObject--Community Review Round 1
Adam D Ruppe
destructionator at gmail.com
Sat Jan 15 16:01:38 UTC 2022
On Saturday, 15 January 2022 at 14:08:25 UTC, Alexandru Ermicioi
wrote:
> I remember seeing some kind of magic equals function in
> object.d that was used implicitly during equals comparison.
There is an equals function, but it is nothing really magical and
certainly not what I'd call a hack. The compiler regularly turns
overloaded operators into calls to helper functions.
This helper function has two jobs: 1) do a null check before the
virtual call, so a == b doesn't segfault when a is null and 2)
check for cases where a == b but b != a which can happen if one
variable is an extension of the other; a is base class, b is
derived class, a.equals(b) passes cuz the base is the same, but
b.equals(a) fails because of extended info that is no longer
equal. The helper function checks both.
This helper function was actually poorly implemented in druntime
until last week! This poor implementation is where @safe, @nogc,
etc., got lost in the comparison.
I seriously think the DIP authors misread the error message. I
mean it, take a look at this quote from the dip:
"""
It fails because the non-safe Object.opEquals method is called in
a safe function. In fact, just comparing two classes with no
user-defined opEquals, e.g., assert (c == c), will issue an error
in @safe code: "@safe function D main cannot call @system
function object.opEquals".
"""
They blame Object.opEquals yet the error message they copy/pasted
does NOT say Object.opEquals. It says *o*bject.opEquals. Those
are completely different things!
I pointed this out in the dip PR review thread, but the authors
chose to ignore me. (Perhaps because the whole house of cards
they built up uses their error as a foundation, and correcting
this obvious mistake brings their whole dip crashing down.)
Anyway, I fixed the implementation and that fix was merged in
druntime master last week. It was very easy to do and it now
respects user-defined @safe annotations. It had nothing to do
with Object.
opCmp and opHash don't use a helper function. They're a direct
call, and work just like if you call it directly..... including
segfaulting on null inputs, but also respecting the user-defined
attributes and/or overloads. Again, it has nothing to do with
Object.
> Yeah narrowing down the method signature is. I just suggested
> to remove opEquals and other operator overloads from Object,
> and provide them as interfaces. Then the devs could choose
> either to make the object equatable and etc. or not.
The interfaces are actually not necessary, even if we were to
remove opEquals and friends from Object, you can still define
them in your subclasses and they'll be respected when they are
used. Just like with structs and operator overloading today.
The one time you might want to use them is for a
virtual-dispatch-based collection. The main example is druntime's
associative arrays.
This could potentially be changed to a templated interface. Even
if it kept a virtual dispatch internally, it can do that with
function pointers... which is, once again, actually exactly what
it does with structs today. These would surely take the static
type of the key as the argument, which might be an interface from
druntime, but could also just as well simply be whatever concrete
base class the user defined.
But let's put that aside and look at today's impl. It actually
uses just opEquals and opHash but it does need both... so which
interface would it be? Equals!T or Hashable? It'd have to be
both, more like AAKeyable!T probably which is both of them.
Sure, you could put that in and define it... but since you need
to do some function pointer stuff for structs anyway... might as
well just do that for classes too and use them internally. The
interface would then just be a helper to guide you toward
implementing the right methods correctly. There's some value in
that, but it is hardly revolutionary.
And if you have that interface... which attributes do you put on
it? If you don't put @nogc etc, since the implementation goes
through it, and user-added attributes will be ignored anyway. And
if you do put @nogc on it, now the user is restricted, which can
be a dealbreaker for them (see people's troubles with const for
example) so that's painful.
Static analysis and dynamic dispatch are at some conflict,
whether it is from Object, from some other class, or from some
newly defined interface.
My preference is to do some kind of type erasure; something more
like an extension of dip 1041. That actually fixes real problems
and works for all this stuff.
Or we can template the whole thing and get the static analysis at
the cost of more generated code bloat.
But mucking with Object is nothing but a distraction.
> I do know that you can do such thing today. The problem is
> that, the combinations of attributes is huge, and therefore
> defining for each combination an implementation and dedicated
> interface is cumbersome, in cases where code base has various
> requirements to equals comparison for example.
Yeah, that's why just using the function directly without an
intermediate interface is the easiest way to get it all right.
Which works today....
> Then you have a function/method that works only with nothrow
> equals, i.e. the parameter type is equals interface with just
> nothrow.
> Trying to pass the instance of that class to the function will
> fail, since they are different types.
Yeah, the interface won't.... but a delegate will. And if the
user class listed both interface, the one method will satisfy
them all.
Of course, listing all those interfaces gets verbose, like you
said, and delegates have to be done individually, but you can
still do delegates of the group you need from an interface.
> The idea, was to have only one interface, and the class have
> implemented safe, nothrow, nogc version, and then have the
> compiler, check and allow passing of the object into the
> method, since they are same iface, just that method has relaxed
> constraints. The same should work when you cast interface, i.e.
> having a nothrow nogc interface, you could cast it to same
> interface with just nothrow, and have the runtime guarantee
> that it is possible to do so.
Yeah, since an interface is kinda like a collection of delegates,
and it works with a collection of delegates, it might be possible
to do it across a whole interface.
A duck type template can probably do it in the library right
now... a while ago one of those almost got added to Phobos. I
think std.typecons.wrap more-or-less does it.
More information about the Digitalmars-d
mailing list