```
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?
There are cases involving closures, generated trampolines, late
binding and other details that mean that doing this will either
eliminate many optimization possibilities or restrict the compiler too
much or cause surprising results. We disabled function comparison for
just these reasons. It used to work this way, but made closures
surprising, so we backed out and allow comparison only to nil.
In LISP terms, these implementations do something more like `eq`, not
`equal`. I want to know if the slices or maps are _equivalent_, not if
they point to identical memory. No one wants this semantics for slice
equality. Checking if they are equivalent raises difficult issues
around recursion, slices that point to themselves, and other problems
that prevent a clean, efficient solution.
There are cases involving closures, generated trampolines, late
binding and other details that mean that doing this will either
eliminate many optimization possibilities or restrict the compiler too
much or cause surprising results. We disabled function comparison for
just these reasons. It used to work this way, but made closures
surprising, so we backed out and allow comparison only to nil.That's interesting. I didn't know that. :)When I run:```func f() {
x := func() {}
y := func() {}
fmt.Printf("%#v %#v %#v %#v\n", x, y, func() {}, func() {})
}
func g() {}
func main() {
fmt.Printf("%#v %#v %#v %#v\n", f, g, func() {}, func() {})
f()
}```I get:```(func())(0x108ac80) (func())(0x108ad40) (func())(0x108ad60) (func())(0x108ad80)
(func())(0x108ac00) (func())(0x108ac20) (func())(0x108ac40) (func())(0x108ac60)```I don't know where those integer values are coming from, but those are what I meant by memory addresses. They seem to be unique per function value. Can't the runtime calculate those same values for comparisons?
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAKbcuKicTHJZZ7yAYKO96MhCPQct%3DQZs07S4%3D-_vXvmoe_ndqA%40mail.gmail.com.
Can't the same argument be made for pointer comparisons?
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAKbcuKicTHJZZ7yAYKO96MhCPQct%3DQZs07S4%3D-_vXvmoe_ndqA%40mail.gmail.com.
On Tue, May 3, 2022 at 8:32 AM Will Faught <will....@gmail.com> wrote:Can't the same argument be made for pointer comparisons?I think what it comes down to is: Yes, this argument can be made for pointers as well. But it would be more controversial. There is no absolutely more/less confusing semantic. But, at least that's the argument, it's less controversial for pointers to be compared as they are, than it would be for slices.
For pointers there are essentially two ways to define comparisons: 1. You compare the pointees, which can lead to bad results, because you can easily build circular pointer structures, or 2. you compare the pointers, which is easy to specify and well-defined. Note that even that leads to ambiguities which sometimes (but relatively rarely) come up - for example, the spec doesn't say if new(struct{}) == new(struct{}). But apart from this rarely important edge-case, it is easy to specify an unsurprising pointer comparison.
For slices, even with your definition, there are questions. For example, should s[0:0] == s[0:0:0], for non-empty slices? That is, should capacity matter? Should make([]T, 0) == make([]T, 0)? That is, what if the "pointer" in the slice header doesn't actually mean anything, as the slice has capacity 0?
The thing is, for pointers there pretty much is exactly one *useful* way to define comparison. For slices, there are multiple useful ways to define it, none of them *clearly* superior to any of the others.
> The point isn't to provide equivalence operations; it's to provide useful comparison operations that are consistent with the other types' comparison operations, to make all types consistent and simplify the language. We could provide a separate equivalence operation, perhaps something like `===` that behaves like `reflect.DeepEquals`, but that's a separate issue. Shallow slice comparisons do allow you to conclude that elements are equal if slices compare equal, and we can still iterate slices manually to compare elements.
It's important that Go operators be intuitive for programmers. For
example, many new Java programmers find that the == operator for
strings is not intuitive in Java. What is people's intuition for
slice equality? I think that different people make different
assumptions. Not supporting the == operators ensures that nobody gets
confused.
Ian
Yes, it can. That's not the issue. The issue is that whether those
pointer values are equal for two func values is not intuitive at the
language level. When using shared libraries (-buildmode=shared) you
can get two different pointer values for references to the same
function. When using method values you can get the same pointer value
for references to method values of different expressions of the same
type. When using closures you will sometimes get the same value,
sometimes different values, depending on the implementation and the
escape analysis done by the compiler.
> The point isn't to provide equivalence operations; it's to provide useful comparison operations that are consistent with the other types' comparison operations, to make all types consistent and simplify the language. We could provide a separate equivalence operation, perhaps something like `===` that behaves like `reflect.DeepEquals`, but that's a separate issue. Shallow slice comparisons do allow you to conclude that elements are equal if slices compare equal, and we can still iterate slices manually to compare elements.
It's important that Go operators be intuitive for programmers. For
example, many new Java programmers find that the == operator for
strings is not intuitive in Java. What is people's intuition for
slice equality? I think that different people make different
assumptions. Not supporting the == operators ensures that nobody gets
confused.
Ian
I don't think controversy is a good counterargument. It's vague, unquantifiable, and subjective. I could easily claim the opposite, while also giving no proof.
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?
The best path is to pick the best way for the common case, and make the other way possible. If slices are compared shallowly, we can still compare them deeply ourselves, or with `reflect.DeepEqual`
As a tangent, I don't understand why this wasn't made unambiguous in the language spec. Why not have `new(struct{})` always allocate a new pointer? Who's allocating all these empty structs on the heap where this is something that needs to be optimized for? Is that really worth complicating the language? 🤔
I would argue this isn't really a deficiency with pointer comparisons, but rather with `new`. If `new(struct{}) == new(struct{})` is true, then they point to the same value in memory; that's all it means. Pointer comparisons are still valid in that case, it's just that the behavior of `new` can vary.
For slices, even with your definition, there are questions. For example, should s[0:0] == s[0:0:0], for non-empty slices? That is, should capacity matter? Should make([]T, 0) == make([]T, 0)? That is, what if the "pointer" in the slice header doesn't actually mean anything, as the slice has capacity 0?I specified slice comparisons like this:> Slices: Compare the corresponding `runtime.slice` (non-pointer struct) values. The time complexity is constant.
I assume `make([]T, 0)` sets the array pointer to nil, because `reflect.DeepEqual` says two of those expressions are equal.
We could do deep comparisons for pointers
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/a533b35c-6ab7-4fe3-9698-6a0f0a71f091n%40googlegroups.com.
On Tue, May 3, 2022 at 6:08 PM Will Faught <will....@gmail.com> wrote:
>
> My apologies, it seems that "reply all" in the Google Groups UI doesn't send email to individuals like "reply all" in Gmail does, just the group. Response copied below.
>
>> Yes, it can. That's not the issue. The issue is that whether those
>> pointer values are equal for two func values is not intuitive at the
>> language level. When using shared libraries (-buildmode=shared) you
>> can get two different pointer values for references to the same
>> function. When using method values you can get the same pointer value
>> for references to method values of different expressions of the same
>> type. When using closures you will sometimes get the same value,
>> sometimes different values, depending on the implementation and the
>> escape analysis done by the compiler.
>
>
> Interesting. This certainly differs from how I pictured functions working (more like C function pointers with extra steps). I'd be curious to know more about the details. Do you know if that's documented somewhere?
I'm not aware of any specific documentation on the topic, sorry.
The fact that C function pointers are guaranteed to compare equal can
actually be a performance hit at program startup for dynamically
linked C programs. I wrote up the details of the worst case several
years ago at https://www.airs.com/blog/archives/307. There are other
lesser issues.
> Just curious, what would be the cost if things were rejiggered under the hood to make function comparisons work? Would any language features be impossible, or would it be worse compiler/runtime complexity/performance, or both?
Regardless of compiler/runtime issues, this would introduce language
complexity, similar to the issues with slices. We would have to
precisely specify when two func values are equal and when they are
not. There is no intuitive answer to that.
Does a program like this print true or false?
func F() func() int { return func() int { return 0 } }
func G() { fmt.Println(F() == F()) }
What about a program like this:
func H(i int) func() *int { return func() *int { return &i } }
func J() { fmt.Println(H(0) == H(1)) }
Whatever we define for cases like this some people will be ready to
argue for a different choice. The costs of forcing a decision exceed
the benefits.
> Regarding expectations, many new Java programmers came from JavaScript (like myself), so the confusion is understandable, but it's not something that necessarily needs to be considered. Arguably, old Java programmers would find `==` confusing for structs, since it doesn't compare references. Bad assumptions are best prevented by proper education and training, not by omitting language features. Wrong expectations aren't the same as foot-guns.
I don't agree. Unexpected behavior is a footgun.
Go is intended to
be a simple language. When special explanation is required, something
has gone wrong.
In saying this I don't at all claim that Go is perfect. There are
places where we made mistakes. But I don't think that our decision to
not define == on slices or functions is one of them.
Well, I guess that's fair enough, but it seems like one could use that kind of argument to undermine any language change, though, including dependencies and generics.
It doesn't seem like the function equality rule I sketched out would add much, if any, language complexity. It's only one sentence: "Function values are equal if they were created by the same function literal or declaration."> Regarding expectations, many new Java programmers came from JavaScript (like myself), so the confusion is understandable, but it's not something that necessarily needs to be considered. Arguably, old Java programmers would find `==` confusing for structs, since it doesn't compare references. Bad assumptions are best prevented by proper education and training, not by omitting language features. Wrong expectations aren't the same as foot-guns.
I don't agree. Unexpected behavior is a footgun.I wrote that wrong expectations aren't foot-guns, not that unexpected behaviors aren't foot-guns. Wrong expectations, as in "I can call this pointer-receiver method on this unaddressable value," or "since zero values are useful, I can set keys and values in this zero-value map." Downloading a compiler for some new language I haven't bothered to learn, typing in Java-like stuff, and then being upset when it doesn't work isn't a problem of unexpected behavior, it's a problem of wrong expectations (usually misunderstandings or ignorance).Go is intended to
be a simple language. When special explanation is required, something
has gone wrong.
The Go language spec is full of special explanations. The section on comparisons is quite detailed and complicated. I recently had to ask here why the range operation doesn't work for type set unions of slices and maps, which you very kindly answered, if I remember correctly. How is slice equality different in terms of special explanation?I've argued that slice comparisons make Go even simpler and more consistent with various examples, analogies, and so on. Please see my response to Axel, if you haven't already. Do you have a specific counter argument to any specific argument that I've made regarding simplicity or consistency?In saying this I don't at all claim that Go is perfect. There are
places where we made mistakes. But I don't think that our decision to
not define == on slices or functions is one of them.
I'd like to be able to reach the same conclusions you have, in the same way you did, so that's what I'm trying to understand. It's difficult to understand your position when I probe your understanding with an argument, and I receive counter arguments like "this would introduce language complexity," "the costs of forcing a decision," "Go is intended to be a simple language," and "I don't at all claim that Go is perfect" that don't specifically address my points. Those arguments could be used to quash anything new, like dependencies or generics, so they don't seem to hold water by themselves, in my opinion.
> >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?
The fact that range over []byte and string are different may well have
been a mistake. It can certainly be convenient, but it trips people
up. (Certainly others may disagree with me on this.)
> >The best path is to pick the best way for the common case, and make the other way possible
I do not agree. The best path is to make no choice, and force the
program writer to be explicit about what they want.
Ian
--
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...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAKbcuKhrUgFi2htgb8Dp627fjU_j8LsgQdc1HQtoAfdc8uC9oA%40mail.gmail.com.
I don't think controversy is a good counterargument. It's vague, unquantifiable, and subjective. I could easily claim the opposite, while also giving no proof.Sure. It was not intended to be an argument. It was intended to be an explanation.I can't proof to you whether or not it is a good idea to design the language as it is. I can only try to explain why it was.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 think this is an interesting example, in that I've thought a couple of times in the past that it might have been a mistake to introduce the current semantics for strings. I think there are good arguments to make ranging over strings behave as ranging over []byte and consequently, there are arguments that we maybe should not have allowed either.But the language is, at is is and we can't break compatibility.The best path is to pick the best way for the common case, and make the other way possible. If slices are compared shallowly, we can still compare them deeply ourselves, or with `reflect.DeepEqual`FTR, I don't think I ever found reflect.DeepEqual to give the semantics I want, when it comes to slices. In particular, it considers nil and empty slices to be different. Which is the right decision to make, probably, but it is almost never the semantics I want. Which is why I don't use reflect.DeepEqual, but use go-cmp, which gives me the option to configure that.Note that it is totally possible to make comparable versions of slices as a library now. So at least the "make other ways possible" part is now done, with whatever semantics you want.As a tangent, I don't understand why this wasn't made unambiguous in the language spec. Why not have `new(struct{})` always allocate a new pointer? Who's allocating all these empty structs on the heap where this is something that needs to be optimized for? Is that really worth complicating the language? 🤔I don't know why that decision was made. I do believe there are some less obvious cases, where you at least have to add special casing in the implementation (e.g. make([]T, x) would have to check at runtime if x is 0). But I agree that it would probably be okay to change the spec here.I would argue this isn't really a deficiency with pointer comparisons, but rather with `new`. If `new(struct{}) == new(struct{})` is true, then they point to the same value in memory; that's all it means. Pointer comparisons are still valid in that case, it's just that the behavior of `new` can vary.Sure. That seems to be a distinction without a difference to me. Note that I didn't say it's a deficiency, quite the opposite. I said that pointer-comparisons work just fine, as they have (mostly) unambiguous and intuitive semantics. But the same is not true for slices and maps.For slices, even with your definition, there are questions. For example, should s[0:0] == s[0:0:0], for non-empty slices? That is, should capacity matter? Should make([]T, 0) == make([]T, 0)? That is, what if the "pointer" in the slice header doesn't actually mean anything, as the slice has capacity 0?I specified slice comparisons like this:> Slices: Compare the corresponding `runtime.slice` (non-pointer struct) values. The time complexity is constant.Yes. I understand what you suggested and I understood how it *would* work, if implemented that way. But why is that the best way to compare them? Doing it that way has a bunch of semantic implications, some of which are perhaps counterintuitive, which I tried to mention.
Note that the language doesn't mention "runtime.slice", BTW. So, even if we did it that way, we would have to phrase it as "two slices are equal, if they point to the same underlying array and have the same length and capacity", or somesuch. This would still not define whether make([]T, 0) == make([]T, 0), though.
So, even if we accepted that this was the "right" way to do it, it would still leave at least one question open.I assume `make([]T, 0)` sets the array pointer to nil, because `reflect.DeepEqual` says two of those expressions are equal.No, it does not. Otherwise, `make([]T, 0)` would be equal to `nil`. `make([]T, 0)` is allowed to use a constant element pointer or not - it must point to a zero-sized array. That pointer must be distinguishable from a nil-slice, but can be arbitrary apart from that. Note, BTW, that the language also does not *force* an implementation to even use that representation. A slice could well be represented as `struct{ Ptr *T; Len int; Cap int; NonNil bool }`, if the implementation want to. Or a miriad other ways.reflect.DeepEqual does not compare the pointers for capacity 0 slices, BTW. To drive home how ambiguous this question actually is, given that you assumed it woudl.
We could do deep comparisons for pointersNot without allowing for a bunch of unpleasant consequences, at least. For example, this code would hang forever:type T *Tvar t1, t2 Tt1, t2 = &t2, &t1fmt.Println(t1 == t2)Note that it is *very common* to have circular pointer structures. For example, in a doubly-linked list.
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/b-WtVh3H_oY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAEkBMfH%3Dy8re0yXYXr%3DUEYnBVsRxouM98ozyOH6uO9jbtueFSA%40mail.gmail.com.
Yes. I understand what you suggested and I understood how it *would* work, if implemented that way. But why is that the best way to compare them? Doing it that way has a bunch of semantic implications, some of which are perhaps counterintuitive, which I tried to mention.I explained that in detail in the subsequent paragraphs.
Note that the language doesn't mention "runtime.slice", BTW. So, even if we did it that way, we would have to phrase it as "two slices are equal, if they point to the same underlying array and have the same length and capacity", or somesuch. This would still not define whether make([]T, 0) == make([]T, 0), though.Perhaps I'm at fault for not being precise in my wording, but the intent was to specify slice comparisons as encompassing the array pointer, the length, and the capacity. It doesn't matter if other fields are added later, or if the type is renamed.
reflect.DeepEqual does not compare the pointers for capacity 0 slices, BTW. To drive home how ambiguous this question actually is, given that you assumed it woudl.It only depends on what the array pointer is. That behavior actually matches my original specification: "Slice values are equal if they have the same array pointer, length, and capacity."
So, two of the `make([]T, 0)` expressions should not be equal if each one gets a unique array pointer, which is consistent with channel comparisons too.
It would be a bad idea to blindly compare two slices that came from disparate sources, because the odds of them being shallowly equal are infinitesimal. If I read a `[]int` off my hard drive, and another off the Internet, I shouldn't compare them blindly, because the odds are extremely good that their array pointers, lengths, or capacities aren't equal, so the comparison won't likely reflect the equality of the elements.
The same is true for pointers: if I get a pointer type from hard drive data, and another from Internet data, what exactly do you think the odds are that those are actually going to be the same value in memory? Shallow comparisons are most useful when you stick a value somewhere, and then go fishing for it later, or encounter it later. In those cases, you don't care about equivalence, you just care about identity, because you know it's there. Deep comparisons are best for data from disparate sources.
We could do deep comparisons for pointersNot without allowing for a bunch of unpleasant consequences, at least. For example, this code would hang forever:type T *Tvar t1, t2 Tt1, t2 = &t2, &t1fmt.Println(t1 == t2)Note that it is *very common* to have circular pointer structures. For example, in a doubly-linked list.Right. I didn't mean it would be useful, or good. The same argument applies to slice comparisons.
One very specific case is that your semantics consider a[0:0:0] == b[0:0:0],
Well, there isn't. The alternative you suggested is not good.
On May 4, 2022, at 1:49 AM, Will Faught <will....@gmail.com> wrote:
--
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...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAKbcuKjr2AKu07hWfPxZLoKoNO5EMbZb8HGgRMOHULDh6Yd5iQ%40mail.gmail.com.
All types should have unrestricted comparisons (`==`, `!=`), but a few pre-declared types don't. Adding them would bridge a semantic gap between pre-declared and user-declared types, enabling all types to be used as map keys, and otherwise make reasoning about them more consistent and intuitive.
For the types that don't yet have unrestricted comparisons:
- Functions: Compare the corresponding memory addresses. The time complexity is constant.
- Maps: Compare the corresponding `*runtime.hmap` (pointer) values. The time complexity is constant.
- Slices: Compare the corresponding `runtime.slice` (non-pointer struct) values. The time complexity is constant.
Examples:
```
// Functions
func F1() {}
func F2() {}
var _ = F1 == F1 // True
var _ = F1 != F2 // True
// Maps
var M1 = map[int]int{}
var M2 = map[int]int{}
var _ = M1 == M1 // True
var _ = M1 != M2 // True
// Slices
var S1 = make([]int, 2)
var S2 = make([]int, 2)
var _ = S1 == S1 // True
var _ = S1 != S2 // True
var _ = S1 == S1[:] // True because the lengths, capacities, and pointers are equal
var _ = S1 != S1[:1] // True because the lengths aren't equal
var _ = S1[:1] != S1[:1:1] // True because the capacities aren't equal
var _ = S1 != append(S1, 0)[:2:2] // True because the pointers aren't equal
```
Function and map equality are consistent with channel equality, where non-nil channels are equal if they were created by the same call to `make`. Function values are equal if they were created by the same function literal or declaration. Map values are equal if they were created by the same map literal or the same call to `make`. Functions that are equal will always produce the same outputs and side effects given the same inputs and conditions; however, the reverse is not necessarily true. Maps that are equal will always contain the same keys and values; however, the reverse is not necessarily true.
Slice equality is consistent with map equality. Slice values are equal if they have the same array pointer, length, and capacity. Slices that are equal will always have equal corresponding elements. However, like maps, slices that have equal corresponding elements are not necessarily equal.
This approach to comparisons for functions, maps, and slices makes all values of those types immutable, and therefore usable as map keys.
This would obviate the `comparable` constraint, since all type arguments would now satisfy it. In my opinion, this would make the language simpler and more consistent. Type variables could be used with comparison operations without needing to be constrained by `comparable`.
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?
Any thoughts?
For a discussion of this issue as it relates to slices you might find this thread worth reading through: https://groups.google.com/g/golang-nuts/c/ajXzEM6lqJI/m/BmSu1m9PAgAJThat was 2016, but not much has really changed since then on this issue.
Zero sized values are useful in Go, because they can have methods and
they can be stored in interfaces. But if the addresses of zero-sized
values must be distinct, then although zero-sized values appear to
have zero size they must in fact be implemented as taking up one byte.
For example, given `struct { a, b struct{} }`, the addresses of `a`
and `b` must be distinct, so that struct is actually two bytes. So,
sure, we could change it. But there are surprising results either
way.
(Historically this was introduced for https://go.dev/issue/2620.)
Ian
On Wed, May 4, 2022 at 8:42 AM Will Faught <wi...@willfaught.com> wrote:Yes. I understand what you suggested and I understood how it *would* work, if implemented that way. But why is that the best way to compare them? Doing it that way has a bunch of semantic implications, some of which are perhaps counterintuitive, which I tried to mention.I explained that in detail in the subsequent paragraphs.I don't believe you did. Looking over it again, those paragraphs explain *what* you are proposing. Not *why* those semantics are the right ones.
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.
This approach to comparisons for functions, maps, and slices makes all values of those types immutable, and therefore usable as map keys.
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?
One very specific case is that your semantics consider a[0:0:0] == b[0:0:0], if a and b come from different slices even though the difference between them is unobservable without unsafe. You make the argument that capacity should matter (which is one of the questions I posed), because of observability. So, by the same token, that difference shouldn't matter here, should it?
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".
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?
Note that the language doesn't mention "runtime.slice", BTW. So, even if we did it that way, we would have to phrase it as "two slices are equal, if they point to the same underlying array and have the same length and capacity", or somesuch. This would still not define whether make([]T, 0) == make([]T, 0), though.
Perhaps I'm at fault for not being precise in my wording, but the intent was to specify slice comparisons as encompassing the array pointer, the length, and the capacity. It doesn't matter if other fields are added later, or if the type is renamed.
The language spec does not know about array pointers. I'm not saying "a field could be added later". I'm saying "one of the fields you are talking about does not exist, as it is a choice made by gc, not something prescribed by the spec".
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.
reflect.DeepEqual does not compare the pointers for capacity 0 slices, BTW. To drive home how ambiguous this question actually is, given that you assumed it woudl.
It only depends on what the array pointer is. That behavior actually matches my original specification: "Slice values are equal if they have the same array pointer, length, and capacity."No, it does not. The example I linked to uses different array pointers, but compares as equal by reflect.Equal.
So, two of the `make([]T, 0)` expressions should not be equal if each one gets a unique array pointer, which is consistent with channel comparisons too.Channels compare as equal "if they were created by the same call to make or if both have value nil". We can't do the same for slices, as slices can be manipulated after calling make (by re-slicing).Note, FTR, that the spec does *not* talk about channels being pointers. Which goes to my point above - we can't define slice comparisons in terms of what gc does, we have to define it based on what the language spec says.
Two channel values are equal if they were created by the same call to make
It would be a bad idea to blindly compare two slices that came from disparate sources, because the odds of them being shallowly equal are infinitesimal. If I read a `[]int` off my hard drive, and another off the Internet, I shouldn't compare them blindly, because the odds are extremely good that their array pointers, lengths, or capacities aren't equal, so the comparison won't likely reflect the equality of the elements.To me, this says that *in most cases* the semantics you are suggesting is really unhelpful. Imagine we would say "floats are comparable, but only if they where both created by mathematic operations from the same value - if you read one float from the internet and another from disk, don't compare them, they might have the same value, but compare unequal". That would be ridiculous, wouldn't it?
The same is true for pointers: if I get a pointer type from hard drive data, and another from Internet data, what exactly do you think the odds are that those are actually going to be the same value in memory? Shallow comparisons are most useful when you stick a value somewhere, and then go fishing for it later, or encounter it later. In those cases, you don't care about equivalence, you just care about identity, because you know it's there. Deep comparisons are best for data from disparate sources.*Exactly*. There is no single notion of comparison, which is always (or even "most of the time") what you'd want. Therefore, it's best not to have any notion of comparison, lest people shoot themselves in the foot.
We could do deep comparisons for pointersNot without allowing for a bunch of unpleasant consequences, at least. For example, this code would hang forever:type T *Tvar t1, t2 Tt1, t2 = &t2, &t1fmt.Println(t1 == t2)Note that it is *very common* to have circular pointer structures. For example, in a doubly-linked list.Right. I didn't mean it would be useful, or good. The same argument applies to slice comparisons.It seems to me, that you are exactly confirming our point here, which is that for pointers, there is a single, unambiguously good way to define comparisons.
Note that *originally*, you said that there wasn't - that if we say "slices can not be compared, because there is no unambiguously good way to do so", we should also disallow pointers, because there is no unambiguously good way to do so.Well, there isn't. The alternative you suggested is not good.
For slices, there *are* many differently good ways to define that comparison - and the specific bounds you set, seem to allow for unintuitive results, such as a[0:0:0] != b[0:0:0], even if their difference can't be observed.
--
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/b-WtVh3H_oY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/e3cff142d482ad17973538256a6ded7923d9a2e5.camel%40kortschak.io.
On May 4, 2022, at 8:19 PM, Will Faught <will....@gmail.com> wrote:
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAKbcuKhSCJV8Xb9Zehtfh1yLuWX1PYd6yFdiBpqAbM-PT%2BakAw%40mail.gmail.com.
--
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/b-WtVh3H_oY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAOyqgcU%3DTdSF1co2jyeQuUhk2P7giwf33tdK6p%2Bd8UgaXO4Tng%40mail.gmail.com.
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?
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?
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.
If your answer was yes, then you don't understand Go at a basic level.
I don't follow why `a[0:0:0] == b[0:0:0]` would be true if they have different array pointers.
Note that `a[0] = 0; b[0] = 0; a[0] = 1; b[0] == 1` can observe whether the array pointers are the same.
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?
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.
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.
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.
Go programs run on a single computer. What if a Go runtime was built that ran Go programs across many computers?
Or, put another way, what if a system architecture emerged where the instruction access time varied so drastically across CPU cores that it made sense to duplicate functions across cores' fast memory regions, so that the receive operation in the above example actually received a duplicate copy of a function? I will admit that closures with mutable data segments already complicate such an optimization, but function equality would thwart such an optimization altogether.
David
--
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...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/f566007f-aa47-4aaf-bc9e-a0fa9dab1e62%40www.fastmail.com.
On Tue, May 3, 2022 at 11:01 PM Will Faught <will....@gmail.com> wrote:
>
> On Tue, May 3, 2022 at 7:27 PM Ian Lance Taylor <ia...@golang.org> wrote:
>>
>> Does a program like this print true or false?
>>
>> func F() func() int { return func() int { return 0 } }
>> func G() { fmt.Println(F() == F()) }
>>
>
> It would print false, because the function literal creates a new allocation (according to the rule I sketched out). I can see the desire to optimize that, but personally when I write code like that, I'm thinking, "and then return a new function." Causing an allocation isn't surprising behavior here, and so neither is uniqueness in terms of comparisons.
>
>>
>> What about a program like this:
>>
>> func H(i int) func() *int { return func() *int { return &i } }
>> func J() { fmt.Println(H(0) == H(1)) }
>>
>
> It would print false for the same reason.
>
>>
>> Whatever we define for cases like this some people will be ready to
>> argue for a different choice. The costs of forcing a decision exceed
>> the benefits.
>
>
> So on the balance, the cost of making a decision is worth it for something big like dependencies or generics, but not function equality. Well, I guess that's fair enough, but it seems like one could use that kind of argument to undermine any language change, though, including dependencies and generics. It doesn't seem like the function equality rule I sketched out would add much, if any, language complexity. It's only one sentence: "Function values are equal if they were created by the same function literal or declaration."
I don't think that is clear, because it seems to me that each call to
F(), above, returns the same function literal, yet you said that F()
== F() is false.
As an implementation note, currently F() does not allocate. If we
require that F() != F(), then calling F() must allocate. Adding an
allocation there is straightforward, but even if we permitted function
comparisons I think that function literals will be used far more than
they are compared, so adding an allocation seems like a poor use of
resources.
>> > Regarding expectations, many new Java programmers came from JavaScript (like myself), so the confusion is understandable, but it's not something that necessarily needs to be considered. Arguably, old Java programmers would find `==` confusing for structs, since it doesn't compare references. Bad assumptions are best prevented by proper education and training, not by omitting language features. Wrong expectations aren't the same as foot-guns.
>>
>> I don't agree. Unexpected behavior is a footgun.
>
>
> I wrote that wrong expectations aren't foot-guns, not that unexpected behaviors aren't foot-guns. Wrong expectations, as in "I can call this pointer-receiver method on this unaddressable value," or "since zero values are useful, I can set keys and values in this zero-value map." Downloading a compiler for some new language I haven't bothered to learn, typing in Java-like stuff, and then being upset when it doesn't work isn't a problem of unexpected behavior, it's a problem of wrong expectations (usually misunderstandings or ignorance).
I don't want to get into a terminology war, but I don't understand the
distinction that you are drawing. If I have "wrong expectations,"
then what actually happens when I try something is "unexpected
behavior." It's literally not what I expected.
It's reasonable to say that if you are using a new language you should
read the friendly manual. But it's also reasonable to say that as
much as possible languages should be unsurprising. Computer languages
build on each other. Go has obvious debts to languages like C and
Oberon, and it would be confusing if constructs in Go acted
differently than the identical constructs in those languages.
Arguably, old Java programmers would find `==` confusing for structs, since it doesn't compare references.
>> Go is intended to
>> be a simple language. When special explanation is required, something
>> has gone wrong.
>>
>
> The Go language spec is full of special explanations. The section on comparisons is quite detailed and complicated. I recently had to ask here why the range operation doesn't work for type set unions of slices and maps, which you very kindly answered, if I remember correctly. How is slice equality different in terms of special explanation?
The fact that Go is imperfect, which it is, is not an argument for
adding further imperfections.
> I've argued that slice comparisons make Go even simpler and more consistent with various examples, analogies, and so on. Please see my response to Axel, if you haven't already. Do you have a specific counter argument to any specific argument that I've made regarding simplicity or consistency?
All changes to languages have costs and benefits. Your arguments
about simplicity and consistency are benefits. The counter-arguments
that I and several others have been making are costs. In deciding
whether to change the language we must weigh those costs and benefits
and decide which are more important.
To put it another way, I don't have specific counter arguments to your
specific arguments about simplicity and consistency. But that doesn't
mean that I agree, it just means that I think that other consequences
are more important.
1. Complexity is bad.2. This change increases complexity in one area.Therefore, 3. This change is bad.
1. Not wearing a seat belt is dangerous.2. Wearing a seat belt takes little effort and the discomfort is minimal to none.Therefore, 3. A seat belt law is a good idea.
1. Safety is good.Therefore, 2. A seat belt law is a good idea.
1. Safety is good.Therefore, 2. A law that requires everyone to wear a helmet 24/7 is a good idea.
1. Nothing can be changed.2. This idea will change something.Therefore, 3. This is a bad idea.
1. Simplicity is good.2. Complexity is bad.3. Change has cost.Therefore, 4. This is a bad idea.
1. Simplicity is good.2. Complexity is bad.3. Change has cost.Therefore, 4. No modules. No generics. No enhanced number literals. No embed package. No io/fs. No nothing.
Ian
Yeah, it's a trade-off. I can understand that it's not worth it.
I think what you've put forth as counter arguments are instead principles, like "simplicity is good," "complexity is bad," "changes have cost," and so on. Principles aren't themselves arguments, so they can't be used as premises in an argument.1. Complexity is bad.2. This change increases complexity in one area.Therefore, 3. This change is bad.is not a good argument because "complexity is bad" would rule out all changes, and it would be inconsistent with the past and recent behavior of the Go Team.
All that said, assuming you agree, what is your full counter argument, or set of counter arguments, to my initial argument for slice and map comparisons, shaped by the principles you've listed? It doesn't need to be in a rigid numbered list format, but it should be obvious how your counter arguments could logically fit into that format.
Ian
--
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...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAKbcuKh_vGQTjtuBECxDJM%2BuHs5eiRpw2iq-%3DK77YyeUpcR%2BfQ%40mail.gmail.com.
On Thu, May 5, 2022 at 3:11 AM Will Faught <wi...@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 at best and a baseless insult at worst.
If you think slice equality should incorporate element equality, here's an example for you:```
type Slice1000[T any] struct {xs *[1000]Tlen, 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]intvar 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?
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.
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.
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.
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).
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", thena := 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: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-KNc5YThat 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.
--
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/b-WtVh3H_oY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/f566007f-aa47-4aaf-bc9e-a0fa9dab1e62%40www.fastmail.com.
On Thu, May 5, 2022 at 7:32 PM Will Faught <will....@gmail.com> wrote:Yeah, it's a trade-off. I can understand that it's not worth it.I find it confusing to me that you seem to be willing to allow the existence of tradeoffs on the one hand. And on the other you treat most arguments as all-or-nothing questions. Like:
I think what you've put forth as counter arguments are instead principles, like "simplicity is good," "complexity is bad," "changes have cost," and so on. Principles aren't themselves arguments, so they can't be used as premises in an argument.1. Complexity is bad.2. This change increases complexity in one area.Therefore, 3. This change is bad.is not a good argument because "complexity is bad" would rule out all changes, and it would be inconsistent with the past and recent behavior of the Go Team."Complexity is bad" is indeed in the "contra" column for most changes. That doesn't rule them out though. It just means they have to justify their complexity.There are many, individual arguments at play here. Each one individually doesn't lead to the conclusion. And taking each one individually out of context and applying it in totality as the only deciding factor leads to ridiculous results like this. But *taken together*, they can paint a picture that the downsides of adding comparison for certain types outweigh their benefits. And that it's different for other types.
Pointers and Slices have commonalities, true. But that doesn't mean "if you can compare pointers, you should be able to compare slices". They also have differences. And it's entirely reasonable, that the downsides for adding a comparison for slices do not apply for pointers, or apply to a lesser degree. And similar for benefits.
All that said, assuming you agree, what is your full counter argument, or set of counter arguments, to my initial argument for slice and map comparisons, shaped by the principles you've listed? It doesn't need to be in a rigid numbered list format, but it should be obvious how your counter arguments could logically fit into that format.One argument goes roughly like1. We believe most people would expect comparison of slices to compare the contents of slices/maps, not the "shallow" comparison you suggest¹.
2. Doing that has technical problems making it prohibitive (e.g. the existence of cyclic data structures).
3. Even *if* we would add a "shallow" comparison instead, there are still open questions where it is hard to say what the "right" answer would be² .
4. It is better not to have any comparison, than one which behaves badly. The programmer can always be explicit about their intentions, if need be.
[1] Note that you mentioned Rust, Ruby and Python to support comparisons on their respective equivalents. Their comparisons all behave this way.
[2] This alone would probably not prevent us from doing it, but given that we'd want a different notion of comparability anyways, it still matters.
----Ian
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...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAKbcuKh_vGQTjtuBECxDJM%2BuHs5eiRpw2iq-%3DK77YyeUpcR%2BfQ%40mail.gmail.com.
You received this message because you are subscribed to a topic in the Google Groups "golang-nuts" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/golang-nuts/b-WtVh3H_oY/unsubscribe.
To unsubscribe from this group and all its topics, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/CAEkBMfFYJ%3DHFYCBuaqRbjmD7H39gdihYObrwcdBTidqfJt1PJg%40mail.gmail.com.