Adam D Ruppe
Posted in reply to Alexandru Ermicioi
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.