On Thu, May 5, 2022 at 3:11 AM Will Faught <w...@willfaught.com> wrote:

> The reason to include capacity in comparisons, aside from it being
>> convenient when doing comparisons, is that the capacity is an observable
>> attribute of slices in regular code. Programmers are encouraged to reason
>> about slice capacity, so it should be included in comparisons. `cap(S1[:1])
>> != cap(S1[:1:1])` is true, therefore `S1[:1] != S1[:1:1]` should be true,
>> even though `len(S1[:1]) == len(S1[:1:1])` is true.
>
>
> Do you agree that is a good thing, yes or no, and if not, why?
>

Sure.


> This approach to comparisons for functions, maps, and slices makes all
>> values of those types immutable, and therefore usable as map keys.
>
>
> Do you agree that is a good thing, yes or no, and if not, why?
>

Sure.

I wrote in the proposal an example of how slices work in actual Go code,
> then asked:
>
> Do you expect `c` to be true? If not (it's false, by the way), then why
>> would you expect `make([]int, 2) == make([]int, 2)` to be true?
>
>
> What was your answer? Yes or no? This isn't rhetorical at this point, I'm
> actually asking, so please answer unambiguously yes or no.
>

To be clear, demanding an unambiguous answer doesn't make a question
unambiguous. If you'd ask "is light a wave or a particle, please answer yes
or no", my response would be to stand up and leave the room, because there
is no way to converse within the rules you are setting. So, if you insist
on these rules, I will try my best to leave the room, metaphorically
speaking and to write you off as impossible to have a conversation with.

My position is that the comparison should be disallowed. Therefore, I can't
answer yes or no.

I think "both slides in the comparison contain the same elements in the
same order" is a strong argument in favor of making the comparison be true.
I think "this would make it possible for comparisons to hang the program"
is a strong argument in favor of making the comparison be false.
I think that the fact that there are strong arguments in favor of it being
true and strong arguments in favor of it being false, is itself a strong
argument in not allowing it.

If your answer was yes, then you don't understand Go at a basic level.
>

Please don't say things like this. You don't know me well enough to judge
my understanding of Go. If you did, I feel confident that you wouldn't say
this. It is just a No True Scotsman fallacy
<https://yourlogicalfallacyis.com/no-true-scotsman>at best and a baseless
insult at worst.


> I don't follow why `a[0:0:0] == b[0:0:0]` would be true if they have
> different array pointers.
>

Because above, you made the argument that focusing the definition of
equality on observable differences is a good thing. The difference between
a[0:0:0] and b[0:0:0] is unobservable (without using unsafe), therefore
they should be considered equal.

Note that `a[0] = 0; b[0] = 0; a[0] = 1; b[0] == 1` can observe whether the
> array pointers are the same.
>

No. This code panics, if the capacity of a and b is 0 - which it is for
a[0:0:0] and b[0:0:0]. There is no way to observe if two capacity 0 slices
point at the same underlying array, without using unsafe.

Feel free to prove me wrong, by filling in Eq so this program prints "true
false", without using unsafe: https://go.dev/play/p/xqj_DhBi392


>
>> But really, the point isn't "which semantics are right". The point is
>> "there are many different questions which we could argue about in detail,
>> therefore there doesn't appear to be a single right set of semantics".
>>
>
> I've already addressed this point directly, in a response to you. You
> commented on the particular example I'd given (iterating strings), but not
> on the general point. I'd be interested in your thoughts on that now.
>
Here it is again:
>
> Just because there are two ways to do something, and people tend to lean
>> different ways, doesn't mean we shouldn't pick a default way, and make the
>> other way still possible. For example, the range operation can produce per
>> iteration an element index and an element value for slices, but a byte
>> index and a rune value for strings. Personally, I found the byte index
>> counterintuitive, as I expected the value to count runes like slice
>> elements, but upon reflection, it makes sense, because you can easily count
>> iterations yourself to have both byte indexes and rune counts, but you
>> can't so trivially do the opposite. Should we omit ranging over strings
>> entirely just because someone, somewhere, somehow might have a minority
>> intuition, or if something is generally counterintuitive, but still the
>> best approach?
>
>
I don't understand what your unaddressed point is.

It seems to be that we fundamentally disagree. Where Ian and I say "if we
can't clearly decide what to do, we should do nothing", you say "doing
anything is better than doing nothing" (I'm paraphrasing, because pure
repetition doesn't move things forward).

In that case, I don't see how I could possibly address that, apart from
noticing that we disagree (which seems obvious).

To repeat what I said upthread: My goal here is not to *convince* you, or
to *prove* that Go's design is good. It's to *explain* the design criteria
going into Go and how the decisions made follow from them. "If no option is
clearly good, err on the side of doing nothing" is a design criterion of
Go. You can think it's a bad design criterion and that's fine and I won't
try to convince you otherwise. But it is how the language was always
developed (which is, among other things, why we didn't get generics for
over ten years).

>From https://go.dev/ref/spec#Slice_types:
>
> A slice is a descriptor for a contiguous segment of an underlying array
>> and provides access to a numbered sequence of elements from that array.
>
>
> It doesn't matter to me whether we refer to the slice's array as a
> "descriptor" of an array, or it "points" to an array. It refers to a
> specific array in memory, period.
>

By this notion, we don't arrive at the comparison you proposed, though. For
example, if we said "two slices are equal, if they have the same length and
capacity and point at the same array", then

a := make([]int, 10)
fmt.Println(a[0:1:1] == a[1:2:2])

should print "true", as both point at the same array.

We could say "two slices are equal, if they provide access to the same
sequence of elements from an array". But in that case, we wouldn't define
what a capacity 0 slice equals, as it does not provide access to any
sequence of elements.

Or maybe we say "a non-nil slice of capacity zero provides access to an
empty sequence of elements" in which case this should print "true", as the
empty set is equal to the empty set:

a := make([]int, 10)
fmt.Println(a[0:0:0] == a[1:1:1])

But, for your proposal to work, we would then have to make sure that any
slicing operation which results in a capacity zero slice resets the element
pointer, so they are equal.

Or we could change your proposal, to say "two slices are equal, if they are
both nil, or if they are both non-nil and have capacity zero, or if they
are both non-nil and give access to the same sequence of elements".

FWIW, I think this last one is the most workable solution for the "slices
are equal, if the use the same underlying array" concept of comparability
(whose main contender is the "slices are equal, if the contain the same
elements in the same order" concept of comparability).

But I hope that this can demonstrate that there is complexity here, which
you have not seen so far.

The proposal doesn't depend on reflection or unsafe behavior. It was just a
> lazy way of mine to inspect what Go does in a corner case. I think making
> it clear what `make` does when the length is 0 is the solution to this, if
> it already isn't clear.
>

I disagree. There are more ways to get capacity zero slices, than just
calling `make` with a length of 0. If you want to create the invariant that
all capacity 0 slices use the same element pointer, this would incur an IMO
prohibitive runtime impact on any slicing operation.

The point is that there *are* two ways to compare them: shallow (pointer
> values themselves) and deep (comparing the dereferenced values). If we made
> `==` do deep comparisons for pointers, we'd have no way to do shallow
> comparisons. Shallow comparisons still allow for deep comparisons, but not
> the other way around.
>

That's simply false. If anything, the exact opposite is true.

For example, here is code you can write today, to get the "shallow
comparison" semantics I outlined above:
https://go.dev/play/p/KApjiKKbnqI
It does require unsafe to re-create the slice, but it works fine and has
the same performance characteristics as if we made it a language feature.
It allows storing slices in maps (as a Slice[T] intermediary) and comparing
them directly. So, if you *need* these semantics, you can get them, even if
a bit inconvenient.
This code would obviously remain valid, even if we introduced a == operator
for slices, even if that does a "deep comparison".

However, this is AFAIK the only way to implement a "deep comparison" (I'm
ignoring capacity both for simplicity and because it seems the better
semantic for this comparison) is this: https://go.dev/play/p/I1daD-KNc5Y
That works as well, but note that it is *vastly* more expensive than the
equivalent language feature would be, as it allocates and copies all over
the place.

So, it seems to me, that "deep comparisons" benefit much more from being a
language feature, than "shallow comparisons". Though to be clear, my
position is still, that neither should be one.

-- 
You received this message because you are subscribed to the Google Groups 
"golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to golang-nuts+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/golang-nuts/CAEkBMfH6TMvr7fxMm3-XnxkHhnGzju82%3Dvs8LVQLYYN8HpKY8w%40mail.gmail.com.

Reply via email to