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.