On Thu, May 5, 2022 at 2:53 AM Axel Wagner <axel.wagner...@googlemail.com>
wrote:

> 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.
>
>

The reason why I've been explicitly asking you and Ian whether you agree
with my points is because you've been ignoring or skipping over them in
your responses. The points I make in response to yours are meant to
synchronize us through agreement (if we agree), and ensure we are on the
same page. When you don't respond with something equivalent to "agree" or
"disagree because" to each point, it's easy to lose track of where each of
us is, and what ground is left to cover or explore. We're already 3-5
levels deep in email quotations at this point. Debate is unproductive and
pointless if we can't even agree on what an argument means.

I say all this because it's clear from what you've written here that you
fundamentally misunderstood my initial argument for why slice comparisons
should be shallow. I didn't write the Slice1000 example because I enjoy
typing, I wrote it because the synchronization forced by the question of
what `c` evaluates to ensures that you and I are on the same page of what
my argument means. The statement about not understanding Go at a basic
level was phrased very specifically to make it clear whether you understood
what I was saying. If something comes across as insulting, the odds are
good it's because *you* don't understand the point. The first thing we
should ask ourselves about an argument is, "Is this true?" The second is,
"How *can* this be true?" By ignoring the two questions about Slice1000 in
the initial argument, you might have constructed in your mind a strawman
argument, and been arguing against that ever since. If there's a single
sentence, a single *word*, in an argument that you don't understand, the
first step is to ask clarifying questions to understand it, not ignore it
and hope for the best. This entire time, I thought you had answered that
first question as no. I didn't start off requiring every initial response
to include the answers to those questions because I, you know, assumed
people would thoroughly read and understand the argument, and point out
basic comprehension problems with it, and otherwise base their responses on
it.

Again, this was in the initial argument:

If you think slice equality should incorporate element equality, here's an
example for you:

```
type Slice1000[T any] struct {
    xs *[1000]T
    len, cap int
}

func (s Slice1000[T]) Get(i int) T {
    // ...
    return s.xs[i]
}

func (s Slice1000[T]) Set(i int, x T) {
    // ...
    s.xs[i] = x
}

var xs1, xs2 [1000]int

var a = Slice1000[int]{&xs1, 1000, 1000}
var b = Slice1000[int]{&xs2, 1000, 1000}
var c = a == b
```

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?


"If you think slice equality should incorporate element equality, here's an
example for you" was a clue that it was important to understand this
argument before responding with disagreement about shallow slice
comparisons.

Plug that code into go.dev/play to observe how it works. Go here
<https://go.dev/play/p/vIDLDYC4K19> and click Run for yourself. It prints
`false`. This is basic, existing Go behavior. If you do not understand how
arrays work in Go, or how pointers to arrays work in Go, or how comparisons
of pointers to arrays work in Go, then you do not understand Go at a basic
level. I would challenge you to find any experienced Go programmer that
would disagree with that statement.

This is basically the argument made by "If not (it's false, by the way),
then why would you expect `make([]int, 2) == make([]int, 2)` to be true?":

1. The A and B values of type Slice1000 have different array pointers, so
they don't compare as equal.
2. `make` allocates new arrays for each slice, so the array pointers are
unique.
Therefore, 3. Two calls of `make` with the same type, length, and capacity
shouldn't compare as equal, for the same reason.


Potentially productive avenues of attack against this argument might be (1)
to argue that I'm incorrect about how the Slice1000 code functions by
running it yourself; (2) that `make` doesn't allocate new arrays for each
call for slice types; (3) that shallow comparisons shouldn't take into
account the array pointer, for some reason; (4) that slices are different
than other types like arrays or structs, either conceptually, or in some
aspect of common implementation, or something like that, and therefore
another way of comparing them, or changing nothing at all, is more
intuitive/simple/cheap/whatever; and so on. I dunno. If I had thought of a
good counter argument, I wouldn't have started this thread.

Do you agree, yes or no, and if not, why?

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.
>
>
It's observable because it's observable in `a` and `b` (assuming they have
different arrays). If two slices are equal, then so are their corresponding
sub-slices. If you somehow encounter two slices with length/capacity 0 of
unknown origin, and compare them, and they compare as unequal, then the
conclusion is that they point to different arrays. It would be an error to
conclude that two slices with length/capacity 0 are necessarily equal,
where slice comparisons are defined as proposed here.

Do you agree, yes or no, and if not, why?


> 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.
>
>
No, the length/capacity of `a[0:0:0]` is 0; the length/capacity of `a` is
not 0, if I remember the example correctly.

Do you agree, yes or no, and if not, why?


> 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
>
>
I'm unclear whether this rests on your possibly misunderstanding the
initial argument, so I'll hold off on responding to these points for now.


>
>>
>>> 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.
>

The point:

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.


Do you agree, yes or no, and if not, why?


> 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).
>
>
"Doing anything is better than nothing" is not my argument.


> 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).
>
>
Again, my argument is that "no option is clearly good" doesn't apply in
this case. I've typed a lot of words making that argument to you and/or
Ian, which you haven't specifically responded to yet, if I remember
correctly. This is why I'm starting to explicitly force you and Ian to
agree or disagree on each point that I make in response to you, because
these things are getting lost, and now you're trying to use that loss to
justify using basic principles as counter arguments that are countered by
those lost points. Go back through everything I've written in response to
you and respond to every point, every sentence where applicable, with
"agree" or "disagree because," and I think that should clear this up.

>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.
>

No, it should print false, because `a[1:2:2]` has an array pointer that is
`sizeof(int)` bytes offset from the array pointer of `a`. Slices do not
keep track of offsets, just pointers, lengths, and capacities.

Do you agree, yes or no, and if not, why?


>
> 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.
>

These all seem to build off the last point that I addressed, so I'll hold
off on responding to these for now.


>
> 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.
>

I don't understand what that has to do with reflect or unsafe, though.

Are you saying zero-capacity slices can come from reflect or unsafe, and
that they wouldn't work with this comparison scheme? If so, how would they
not work?

Are you saying that slicing with a new zero capacity would produce slices
that wouldn't work with this comparison scheme? If so, how would they not
work?


>
> 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.
>
>
Man, we seem destined to not see eye to eye for some reason, lol. I really
don't know what to say to that. I guess here's a sort-of proof by
construction for my claim:

With shallow pointer comparisons:

```
var p1, p2 *int = // ...

// Shallow comparison
var equal = p1 == p2 // only compares pointer addresses

// Deep comparison
var equal = *p1 == *p2 // not very "deep", though
var equal = reflect.DeepEqual(p1, p2) // much deeper
```

With deep pointer comparisons:

```
var p1, p2 *int = // ...

// Deep comparison
var equal = p1 == p2 // same as *p1 == *p2 above; not very "deep", though

// Shallow comparison
var equal = ??? // impossible
```

Do you have a refutation for that? What can we do for `???`?


> 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".
>
>
How does this connect to my point above about how if pointers are compared
shallowly, we can still compare them deeply, but the reverse isn't true?

Sure, your Slice seems to embody most of what I've proposed here, but it
wouldn't be standard, built-in behavior. I wouldn't be surprised if you
could accomplish the same with reflection or Cgo or serialization. Why have
== for structs if we can compare fields individually in user code? You're
ignoring the utility of having == built in, and the simplicity and
intuitiveness that comes from consistency, both of which are arguments that
I've made before to you and Ian, if I remember correctly.


> 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.
>
>
Eq would need to be called recursively on elements that are slices, but
otherwise yes, although this boxes all the slice elements and allocates an
entire singly-linked list for them.

However, these examples seem to be about shallow vs. deep slice
comparisons, not pointer comparisons, which seems to be the point you're
responding to above, so I'm not following your point.


> 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/CAKbcuKiH35kSpG%3D2CmXF3iwrWUabi%3DSLr%3DZO6aW1TV-_iA8vbA%40mail.gmail.com.

Reply via email to