-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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: spec: add Nullable constraint #53656
Comments
where would this be useful? |
Would this work for you?
|
Nilable is a bad constraint because nil is overly broad. Forgive me for not searching for issue numbers, but there's already a long issue about splitting nil into nilinterface vs nilptr vs nilchan, nilslice, and nilmap. Right now there's no constraint that can guarantee that T is an interface. That would be more helpful. More generally, the issue to add a built in For now you can work around this by writing a |
I have a case where no, in my case |
There is also an issue to make interfaces comparable. #52624 We got issues for everything. :-) Here is adding more specific nils: #22729 Here is adding a universal zero: #53666 Edit, that's the dup. Original is #35966. Again, I think most constraints can be replaced with callbacks, but even without doing that a Nilable constraint is too broad and an Interface constraint would be more useful (although it really confuses the question of what a constraint is, since interfaces can't contain interfaces). |
Maybe the issue is that every type should be comparable to its zero value? (whether they satisfy |
But but performance ? (replacing a single compare instruction by a virtual call seems silly)
👍 |
Yes, kind of. The idea is there but the zero value of a string is not Since every type is comparable to its zero if I'm not mistaken, generic code would not need a constraint for |
I don't think this syntax is obvious enough, I think a: func isZero[T any](v T) bool builtin would be better (It would not be a real function, the compiler would replace thoses calls with compares to 0 of the right size.) PS: it would be different from emptyString := ""
var interfaceContainingTheEmtpyString any = emptyString
fmt.Println(isZero(interfaceContainingTheEmtpyString)) // false, because it isn't a nil interface
fmt.Println(reflect.ValueOf(interfaceContainingTheEmtpyString).IsZero()) // true, because reflect use the assertion rules and look at the underlying type, and "" is the zero value of the empty string. |
Well, we need a zero(T) anyway since we need to be able to return the zero value in generic functions that require it. |
Fair. However we can already do this using @randall77 suggestion: var zero T
return zero You can also use named return values and not assign to them, ... |
func isZero[T any](v T) bool {
return reflect.ValueOf(&v).Elem().IsZero()
} I've used this code in some libraries. |
This is because the compiler doesn't know how to do constant folding / rewrites on reflection even if the types are known at compile time. |
Indeed. The only issue is in the interaction of the two issues (zero value notation and zero value comparison). In @randall77 suggestion, T satisfies But it should be possible to compare to the zero value without this constraint. If we decide to do it with var zero T, the compiler needs to make sure that zero hasn't changed. |
@MrTravisB It would help a lot to see cases where this is actually useful. In order to find the most useful solution, it helps to know what the real problem is. For example, right now it's possible to write a type constraint that covers a set of types that can be compared to |
Is the instance I have in a variable well-initialized enough to reliably support the method set of its type without crashing? I feel like 'nil' coincides with 'no, don't use for any computation'. Does some built-in 'zero' mean 'yes, it's empty, but it's also well-initialized'? That's my (possible mis-) reading of discussions so far. I worry that with |
I was writing some lazy initializing generic code. type Lazy[T any] struct {
v T
}
func (l *Lazy[T]) Get(create func() (T, error)) (r T, err error) {
r = l.v
if r == nil {
r, err = create()
l.v = r
}
return
} The actual code is threadsafe using atomics and a mutex to not race the creations attempts so it actually made sense to package in a function. I actually resorted to using |
@Jorropo Thanks. Still, your code seems to assume that the zero value of the type is not a valid value; if you can capture |
Moving off the original topic, but I previously wrote a generic lazy initializer using sync.Once. Code is here: https://github.com/carlmjohnson/syncx/blob/main/once.go |
Yes, in my case it's true, any of my consumers would panic nil deref if they tried to use a zero value.
True I could.
There is no way to reset a type Lazy[T nilable] struct {
v T
}
func (l *Lazy[T]) Get(create func() (T, error)) (r T, err error) {
r = l.v
if r == nil {
r, err = create()
if err != nil {
return nil, err
}
l.v = r
}
return
} |
My use case was very similar to @Jorropo in that I was trying to lazy set a generic value that would either be a function or an interface value. Checking it for a zero value isn't the only issue though. Being able to reset to zero is also an issue.
|
I‘m still confused as to why the original code is invalid. If T can be any type it may very well be a nil interface? If thats possible why shouldn‘t it be comparable with nil? |
@andig In the approach we've taken for Go generics you can only perform operations that are permitted for all possible type arguments, not operations that are permitted for a subset of the possible type arguments. |
I didn't reply right away to let the main discussion continue but I think that it should be no. The zero value of a type shouldn't add semantics that are usually the pregorative of a constructor function, I believe. |
I agree a definition of a zero value is straight out of the spec. A constructor function is nowhere in the spec, and that's well-motivated. Instead there's a range of variant behaviors such that for some given
My feeling is that the ergonomics here were in a very sweet spot for pre-generics Go. The ergonomics when writing generic Go are less sweet. (Also, my sense is that there's a number of issues and discussions here and elsewhere that can be indirectly related) Coarsely, I wonder if there'd be any value in distinguishing between "naive" and "enlightened" generic types and routines. For "naive" types, we'd assume an infallible, empty constructor (i.e., numerics, slices/maps/channels, structs exclusively composed thereof, etc.), and infallible (if allocating) construction with "Enlightened" types would be those that require particular methods or callbacks to do the equivalent thing. Significantly, however, there's no bound or general description on "enlightenment"; an enlightened If there were a way to say that |
If I understand you well, the issue is that outside of generics, we somehow know whether the zero value is directly usable or not. Hmmh, I think you have a point. I hadn't thought about that. |
Here's a simplified version of a real use case I just came across at work: https://go.dev/play/p/jLoFc0CAPdV. |
I have a helper library that can test if those functions are nil, but it uses reflect under the hood. I think it’s another argument for adding the universal |
One use case I came across in implementing a Per
One could add a
Which can be used like, for instance:
But this leaves out Another potential solution is adding a
|
The JSON "key not present" case is similar to what we're having an issue with in using the https://github.com/graph-gophers/dataloader |
This is invalid
Would be nice to have a nullable constraint so that the following could be done
The text was updated successfully, but these errors were encountered: