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.