Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: Go 2: spec: always permit comparisons against zero value of type #26842

Closed
jimmyfrasche opened this issue Aug 7, 2018 · 15 comments
Closed
Labels
LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
Milestone

Comments

@jimmyfrasche
Copy link
Member

Go, effectively, has three-kinds of comparability:

  • incomparable, there is no == operator defined for a type (structs and arrays with incomparable fields).
  • 0-comparable, there is an == operator but it can only test against the zero value (funcs, maps, and slices can be compared against nil). The spec treats these as incomparable types and notes the special case.
  • comparable, there is an == operator and it works for all values

When writing code with known types, this is not an issue. You know if there's an == operator and whether you can compare against all values or just the zero value.

When generating code (or, possibly in the future, writing generic code) over arbitrary types, this asymmetry is a bit more bothersome. You can't just classify types as comparable or incomparable, you need to handle the 0-comparable case as well even though it is so similar: https://play.golang.org/p/JOmxaVJoYtT

I propose collapsing incomparable and 0-comparable. Allow incomparable structs and arrays to be compared to their zero value. There would always be an == operator and it would always be safe to compare any value to zero. Care would still need to be taken to ensure comparability when using == between arbitrary values, but the simpler classification eases matters.

I believe this would be a 100% backwards-compatible change and perhaps even simplify the spec a bit (or at least not require too many changes).

The concerns would be go/types and reflect, but they both seem to lump what I call 0-comparability in with incomparability, but there, admittedly, could be subtler implications.

This is tangentially related to defining a universal zero value #19642 (comment) in that similar arguments are involved and the two would compliment each other.

@jimmyfrasche jimmyfrasche added v2 A language change or incompatible library change NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Aug 7, 2018
@gopherbot gopherbot added this to the Proposal milestone Aug 7, 2018
@ianlancetaylor ianlancetaylor changed the title proposal: spec: Simplify comparability proposal: Go 2: spec: always permit comparisons against zero value of type Aug 7, 2018
@cherrymui
Copy link
Member

Does the zero value need to be static? Or it can be dynamic, as long as at run time, at the point of ==, the value is zero?

From https://play.golang.org/p/JOmxaVJoYtT,

	// var zero t
	// fmt.Println(a == zero)

it seems to suggest that the zero value does not have to be static (it is a variable). What if the value is not zero, then? Panic?

@jimmyfrasche
Copy link
Member Author

One side would need to be known to be zero statically. Having a universal zero like in the linked issue would be the easiest way to achieve that.

@ndyakov
Copy link

ndyakov commented Nov 30, 2018

Let's look at time.Time.
There is a IsZero function that will show you if the time is empty or not. With your suggestion I think that there should be an interface for structs and only if the struct is implementing that interface it can be checked against zero. For example if you have:

t := time.Time{}
fmt.Println(t == zero) 

It should be transferred to something like

var t time.Time
fmt.Println(t.IsZero())

This is not the same as t == nil, although t == nil doesn't make sense.

@jimmyfrasche
Copy link
Member Author

As mentioned in #35966, another solution would be a zero(T) bool builtin that worked for all T, which would be an option should no universal zero value be added.

@earthboundkid
Copy link
Contributor

A related issue is that you can't define a const which is a nil func/slice because you can't define const func/slices:

	const zero func() = nil // does not compile

So, as of now, the only way to write f == nil is f == nil, since variables aren't statically nil and you can't make a constant nil other than n-i-l.

I think generics will require Go to either add an isZero() generic built-in or a "universal zero" identifier.

@itsmontoya
Copy link

I'm running into a scenario right now where this would be extremely useful. What is the current intention and/or best practice for this problem if zero builtin has not been implemented?

@earthboundkid
Copy link
Contributor

You can fake it with reflect. See #35966 for discussion of solutions.

@itsmontoya
Copy link

@carlmjohnson the reflect option is very slow when it comes to structs :/

@earthboundkid
Copy link
Contributor

Yes, that's why I am pushing for #35966 to happen.

@jimmyfrasche
Copy link
Member Author

I posted this in the cmp.Or thread but I should probably note it here as well. You can write an is-zero predicate now using generics and unsafe by taking advantage of the property that Go zero values imply all bytes are zero:

func Zero[T any](v T) bool {
	bp := (*byte)(unsafe.Pointer(&v))
	sz := unsafe.Sizeof(v)
	for _, v := range unsafe.Slice(bp, sz) {
		if v != 0 {
			return false
		}
	}
	return true
}

@josharian
Copy link
Contributor

josharian commented Jun 29, 2023

You can write an is-zero predicate now using generics and unsafe by taking advantage of the property that Go zero values imply all bytes are zero

Note that there some subtleties here around padding and fields named _. See #31450 (and read to the end).

@earthboundkid
Copy link
Contributor

Floats are weird, man.

f := math.Copysign(0, -1)
fmt.Println(ZeroAny(f), ZeroGeneric(f)) // false true

@jimmyfrasche
Copy link
Member Author

@josharian it looks like the decision was those are going to be kept zero for now? If I'm reading that correctly there's a hypothetical flaw in the naive check but nothing that can manifest today. Did I get that correct?

@carlmjohnson that is a fair point. It looks like reflect.IsZero handles that correctly but that float/complex are the only cases where this is wrong. That can't really be worked around today without doing something like this: https://github.com/zephyrtronium/number/blob/main/reflect.go#L32 Disappointing!

@jimmyfrasche
Copy link
Member Author

For comparable types, the compiler generates == functions. I had assumed that a single size-parameterized function would suffice to handle the is-zero case. At a minimum it looks like it would need to reuse == for comparables and generate is-zero functions for composite non-comparables containing float/complex. For the other non-comparables, the single function would suffice as long as padding/_ remains zero.

@jimmyfrasche
Copy link
Member Author

Fixed by acceptance of #61372

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

8 participants