Object.toString, toHash, opCmp, opEquals

Timon Gehr timon.gehr at gmx.ch
Fri Apr 26 01:32:51 UTC 2024


On 4/26/24 02:57, Walter Bright wrote:
> On 4/25/2024 4:36 PM, Timon Gehr wrote:
>>> Without the `const` annotations, the functions are not usable by 
>>> `const` objects without doing an unsafe cast. This impairs anyone 
>>> wanting to write const-correct code,
>>
>> "const correctness" does not work in D because const
>>
>> a) provides actual guarantees
>> b) is transitive
> 
> It's not the C++ notion of const, sure. But the name still applies.
> ...

Well, we could come up with a better name, one that actually reflects 
that there are some pitfalls.


>> It is fundamentally incompatible with many common patters of 
>> object-oriented and other state abstraction. It is not even compatible 
>> with the range API. Uses of `const` are niche. `const` is nice when it 
>> does work, but it's not something you can impose on all code, 
>> particularly object-oriented code.
> 
> Why would anyone, for example, try to mutate a range when it is passed 
> to one of these functions?
> ...

A range is useless unless it is mutable. The range interface is 
inherently mutable. To iterate a range, you have to call `popFront()` on 
it. There is no way to have a `const popFront()`.

> 
>>> and also impedes use of `@live` functions.
>>> ...
>>
>> Perfect. I have no intention of using `@live` functions. I do not see 
>> their utility.
> 
> The utility is being able to write borrow-checker style code, so you can 
> avoid things like double frees.
> ...

`@live` does not enable this. Anyway, you are trying to impose 
nonsensical restrictions on garbage-collected code. I have yet to run 
into a double-free using GC allocation and I doubt `@live` would help me 
avoid that if it were a thing.

> As I recall, it was you that pointed out that reference counting can 
> never be safe if two mutable pointers to the same ref counted object 
> (one to the object, the other to its interior) were passed to a 
> function. (Freeing the first can leave the second interior pointer 
> pointing to a deleted object.) The entire ref counting scheme capsized 
> because of this.
> ...

I provided the counterexample, but the unsound generalization is yours. 
(Technically, there would be ways to type check that code without 
banning mutation outright.)

>>> I recommend that everyone who has overloads of these functions, alter 
>>> them to have the `const` signatures. This will future-proof them 
>>> against any changes to Object's signatures.
>>
>> I will not do that, because if it does not outright break my code 
>> (e.g. because Phobos cannot support `const` ranges), it actually 
>> limits my options in the future in a way that is entirely unnecessary.
> 
> Why would anyone need toHash(), toString(), opEquals() or opCmp() to 
> mutate their data? Wouldn't that be quite surprising behavior?
> 

As I keep pointing out, there is a difference between mutating abstract 
data and concrete memory locations. For instance, data types with 
amortized guarantees usually have to reorganize the internal data 
representation on each query. (Think e.g. splay trees.)

Anyway, let's for the sake of argument assume that I want to write 
functions that leave memory in exactly the state they encountered it in. 
Const will _still_ unduly restrict me because it is not fine-grained enough.

```d
import std.stdio, std.range, std.conv;

struct S{
     auto r=iota(1,2);
     string toString()const{ return text(r); }
}

void main(){
     S s;
     writeln(s);
}
```

Writes:
```d
const(Result)(1, 2)
```

Sometimes there is not even a safe workaround to get a mutable version 
of a range, because of transitive `const`. A range can have indirections 
in its implementation.

This is just one example establishing that `const` is not expressive 
enough to say _ONLY_ "this will not mutate anything". It also spells: 
"This code can be a huge pain in the ass at any point in the future for 
dumb, incidental reasons."

I really do not want to deal with this. I'd much rather fork Phobos so 
it uses non-const alternatives to toHash and toString.

If you expect people to prove properties to an incomplete type system 
via annotations and to accept unnecessary restrictions, they have to get 
some value out of it. You also would not go: "Starting from tomorrow, 
you have to prove to me that you brush your teeth every day. I want 
video evidence." And then, when I refuse, you can't say: "Why would you 
not brush your teeth?" This is what this is.

I caution you to now not miss the forest for the trees and engage in a 
"tooth-brushing related" argument (e.g., proposing a different range 
design or something like that). This is an inherent issue. Even if you 
make the type system more expressive, the annotation overhead is still 
real, and often uneconomical.

I am perfectly fine with having some restricted system like Rust for 
people who want to do safe manual memory management. This would even be 
useful to me. But this has to be opt-in, based on data structures, and 
interoperate as seamlessly as possible with the full language.

One thing I absolutely agree on with Robert is that it should always be 
_possible_ to write simple @safe D code without any advanced type system 
shenanigans. I think any design that strays from that principle is bad. 
This proposed change absolutely torpedoes that.


More information about the Digitalmars-d mailing list