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