-
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 a new type constraint (called zeroed) and allow it in type assertions #62626
Comments
@ianlancetaylor @griesemer @rsc another idea, just trying to see what may stick out of a discussion. |
You are using the notation for type assertions: I also observe that Also, it seems a bit cumbersome to have to say Finally, your argument that in non-generic Go code it's not possible to compare against the zero value for a particular type is simply incorrect. In summary, you are introducing a new interpretation for the existing, well-defined notion of a type assertion, and you introduce a new type/concept ( For all these reasons I believe this current proposal should not be further pursued. Thanks. |
Well, isn't it more of an identifier than a value per se? It cannot be assigned in short variable declarations ofr instance.
In the comma,ok idiom, that would be a test for zero-ness. A type assertions would assert whether a constraint applies or not. edit: ah. that part might be wrong. currently it tests whether a type implements an interface rather doesn'it, nowadays?
What of this then? https://go.dev/play/p/tYDivOlEvck
It's true that it's possible to create zero that way in generic code, but it is not possible to test generically for zero. |
Of course it's an identifier, but in Go every identifier (except for
I don't know what that means with respect to your
Not all types are comparable, agreed. But we can test slices, funcs, channels, maps to
It could, I guess, but as I pointed out before, its a completely different form of "type assertion" because it's not asserting a type at all, but testing a value. We should not take a mechanism (type assertions) with a well-defined meaning and overload it with some new meaning that has nothing to do with its original function, unless there's a really convincing reason for it, e.g., because there's no other choice. We have several clearer choices. The obvious choice should be to use
No, a type assertion tests whether the dynamic type of an interface is a specific type. We don't have a notion of all "subtypes composed of zero values" in Go, so the behavior is definitely not "as expected". I believe you are mixing wildly different concepts to get to something for which we already have two pretty good and simple proposals, one of which has been accepted in limited form. As I said before, I don't think this proposal should be pursued further. |
I understand. Many thanks for your input. I will still note that comparable is in the same vein, a special interface. But I can understand the reticence as well. It requires the idea of subtypes, at least to define zeroed. Fair enough. If we wanted to keep it an interface, that would be possible but more cumbersome. That would have people write It reads fluent as the value of a zeroed variable. Edit: why subtypes? It's from the theory. If a type is a set of values, then a value is a type as well. Just that it is a singleton. Hence, zeroed would be the interface whose type set is the set of all zeroes that can exist. Can be an interface then. |
Mmh. Doesn't fully work, although it should with amendments. ExplanationEven if we had subtypes and subtypes were allowed in assertions, given a subtype S of type T, So let's say S is the subtype whose values is all zeroes, it would just have to panic unless v already holds a zero value. Not very interesting. The comma, ok idiom would work though to test whether something is a zero. I was initially trying to have a way to create zero values easily from other variables (although that's not necessary). So the important bit is really about: The constraint could also be spelled iszero or something else. ConclusionThe proposal as it is, isn't clear enough. It would just test both type implementation (which is identity for a non-interface type) and value membership. A subtype is basically a pair composed of a type identifier (package path + name?) and a subset of values of any type that is in the corresponding type set. We could reuse type assertions as they are currently known. A type being similar to an interface (it is actually an interface really) but more restrictive i.e. with additional constraints of name, shape etc. |
Reading the proposal leaves me with the understanding that func zeroed[T any](v T) T {
var zero T
return T
} Putting aside the oddity of using a value to represent a type -- an oddity that has plenty of precedent in Go already, from pre-generics -- this is already possible to define and use in Go today. The variant with a second return value is not possible to implement in Go today, because it encounters part of the problem statement of #61372: not all types are comparable to their zero value, and so it isn't valid to compare a func zeroed[T any](v T) (T, bool) {
var zero T
// invalid operation: v == zero (incomparable types in type set)
return v, v == zero
} It seems to me like the smallest possible language change that would allow expressing this is to specify that any type can be compared to its own zero value, in which case it returns You did mention in your proposal that it might be a breaking change to make all types comparable to their zero value. I assume you are referring to the fact that today comparison of interface values will panic at runtime if the dynamic values are of the same type but not comparable: type Uncomparable struct {
F []string
}
func main() {
u1, u2 := Uncomparable{}, Uncomparable{}
i1, i2 := any(u1), any(u2)
if i1 == i2 { // panics here
fmt.Printf("success")
}
} It is true that making this succeed at runtime could change the behavior of some programs. It does seem plausible to treat interface value comparison differently here -- neither |
That a type assertion to zeroed would return the zero value was what I initially wanted but Robert made the astute remark that it wasn't compatible with current type assertion semantics. So in fact, zeroed should simply be its own interface type, like comparable. type assertions with the boolean return, as you aptly show, is not implementable generically by users nowadays unless:
Note that the latter option is actually different from Russ's proposal in that it is proposed there that all typed variables should be comparable to a very specific identifier of zero values called zero. It's true that in current Go, the value-subtype equivalence does not exist (yet?). But this wouldn't be too difficult to explain or understand. "All positive int values form a type called a subtype of int." is easy to understand. Same way, zeroed, as the set of all zero values, is obviously an interface type and can probably be implemented in the compiler as a checkable predicate (if we check the type of a variable v in type assertions, it should be possible to AND isZero(v) to the result?, predicates on type AND value?) Reusing the comma, ok idiom with type assertions would be fine then and there wouldn't be a need to introduce an untyped zero. Also requires to extend type assertions to non-interface values. |
The reason given not to add an
This proposal clearly suffers from the same problem (but significantly worse).
There is a reason the language currently doesn't say "function types are comparable to their zero value", but says "function types are comparable to the predeclared identifier That is, to make your example func zeroed[T any](v T) (T, bool) {
var zero T
// invalid operation: v == zero (incomparable types in type set)
return v, v == zero
} work, we'd either have to 1. always allow the comparison and panic at runtime if one of the operands is not zero (thus doing away with the entire idea of comparable types), or 2. somehow specify what makes it obvious that This, FTR, is also why the backwards-compatibility issue vaguely alluded to in the top-post does not actually exist. As has been pointed out to @atdiar a couple of times, by now. |
Note that the reason is then a bit confusing (I was confused myself, it's not obvious). Comparing to 0 or nil is different because these are the zero values for some types as per spec. Comparing to zero is not actually comparing to a zero value. It's comparing to a specially identified zero value which makes it a special case. Overloading the == notation and creating a new notation ==zero for what is an assertion is confusing wrt what a zero value comparison is. Then again I think I understand how it works now but I think that ==zero erases too much information.
That means that such an assertion (isZero) should be dynamic, checked at runtime. That's more in favor of a predicate if we don't want a full blown subtype - type assertion.
@Merovius the backward compatibility non-issue had been acknowledged as per the previous comment. |
The proposal, AIUI, is to add
This seems like a pretty exhaustive list of the sets of behaviors we need to define to add I think one interesting observation is, that this really makes This is definitely an interesting property (Rust has a type like this and calls it TypeId), in and off itself. The other things it allows is testing if a variable is the zero value of its type, i.e. func IsZero[T any](v T) bool {
_, ok := any(v).(zeroed)
return ok
} It doesn't add any power to construct zero values, though. Because if we can already construct a zero value of any static type by calling func Zero[T any]() T {
var zero T
return zero
} What we can not do, currently, is construct the zero value of an interface values dynamic type. That is, given an In general, it can not be assumed that the zero value of an interface-implementation is safe to use (nor is it safe to assume that it is not safe to use). So I'm not sure how useful this would be, except insofar as it makes above observation that As I understand it, the actual proposal does try to fill this feature hole. Concretely, It seems to change the way type-assertions work from what I describe above, namely: If The change is in that last case: It would assign a non-nil This is a major break in consistency with existing interface type-assertions. The equivalent would be if And it would do so for dubious value. So I definitely don't think this would be a road worth going down. We can then discuss another extension of this It also begs the question of what makes I don't know why we would do this, though. For non-generic code, we already know whether or not the type-assertion succeeds, based on the type of Anyways. This is a very long comment, but it's how I would really treat this proposal as a serious thought experiment. I do think we could add But as @griesemer says, it is a very big hammer to use on this problem and the resulting mechanism is still pretty clunky, compared to #61372. It requires an extra statement and a temporary variable to check for zeroness (i.e. I don't think we should do this. [edit] This ignores the previous comment, as I had already mostly typed out this pretty long response before it got added. I didn't want to throw it away and start over. So just take into account that I wasn't aware of it when writing this. [/edit] |
Given that the quoted comment explicitly mentions
I do not understand how you can make this leap. Whether an (To be clear: I'm not personally against adding a predeclared |
I will comment some more later but re. the leap, The latter means that the variable is of a comparable type. The former is checking a form of typestate of the variable that asserts that it is a zero, regardless of whether its type is comparable or not. Introducing the zero identifier does that in a way. But I think it's not really necessary the best way to check for typestates. (subtyping i.e. using types, appears clearer to me, especially since we already have some form of type assertion mechanism in the language, matter of taste I guess). Thanks for the in-depth observations in the post above. On a few minor points I would have some corrections but overall it's accurate. Edit: the point 7 could be amended as |
Thanks for the discussion. |
This is yet another attempt to address the issue of comparing zero values, providing a shorthand for the creation of these zero values at the same time.
For context, see the previous proposals #61372 and #62487
Edit: see #62626 (comment) that addresses criticisms of the proposal as it stands and puts it more in line with the current language modulo some theoretical implementation details.
Proposal
Instead of an untyped builtin
zero
, the idea is to introduce a new type constraint that we will callzeroed
,This constraint would be similar to another already existing special constraint:
comparable
(in that it would be special).Usable in type assertions, it should let us construct zero values from pre-existing typed variables as well as test whether a variable holds the zero value of its type.
That's it for the proposal.
Rationale
The goal is to avoid a builtin for which we'd have zero != zero which is already the confusing case for nil.
Although the zero value is a fundamental concept in Go as the zero-assignment value of every variable/field, there are already specific notations for each type of zero that can be encountered in a Go program.
Moreover, currenly, not all types are comparable to their zero value and I wonder if that might not be a breaking change to change that behavior somehow (interface value comparisons). In doubt, an alternative that tries to ignore the question...
An untyped zero in assignments is not a problem; neither difficult to explain nor illegible. See below:
As opposed to what it would be if a universal zero was accepted: (which looks fine to me in terms of readability, what do you think?)
The more problematic issue is function arguments: zero as a superseding concept to nil, removes too much information as only nil is capable of signaling an absence of value.
That would make difficult to accept a universal zero. Hence, the proposed restrictions of #61372 would be here to stay which might be a pity. Why going to such lengths if the above cannot be written and zero simply explained to a beginner?
And there is still the issue that in code, we'd have:
Perhaps it is better to have more apparent semantics then:
Further technical considerations
It might have been obvious to some people already but these special type constraints act a bit different in type assertions, when compared to interface types:
In fact, a constraint is a criterion upon which a type set is built, a predicate. It does not have to be a type per se.
In which case, a type constraint would be a superset of interface types: all interface types would be constraints but some constraints would simply not be regular interfaces. It is already the case nowadays.
Of course, this is just semantics. In implementation, these can remain special cases of interfaces with a type set.
For zero, the theory says that the type set would be constructed from subtypes of each and every type which would contain the zero value as a singleton (a type being a set of values). So {0, "", untyped nil, typed nils, nilIface? , struct{}{}, ...}
All that to say that it would require to repurpose type assertions for these special interfaces (zeroed, comparable, etc.)
Generics
There wouldn't be any issue with zero value comparisons. The comma,ok declination of the type assertion would be a language feature eschewing the need for reflect.IsZero.
Although it is important to note that it should actually be an orthogonal concern. If generics are simply the generalization of non-parametered go code, then there is no reason to actually compare to zero in generic code since it cannot be done for all types in non-generic code.
Quite likely that a constraint
comparable | nilable
would be more adequate instead ofany
.Also, the type assertions mechanism we have exposed here enables us to not redefine comparison for types that were previously not comparable even to their respective zero value.
So regardless of the type constraint used, all else being equal, this would be less problematic in that respect.
E.g.
As such, it doesn't give the zero value additional semantics at the language level, apart from being a value that is a default.
There are even times when the sensible default to use is not the zero value but another.
For instance, in removal from a slice operations, we may want to start the removal index at -1, outside of the bounds of the slice.
This really is a userland concern and the way a zero value is used should probably not leak into the language itself.
Final note
This is really a proposal that has been written out of curiosity for what people may think, especially in light of the possibility of generalized interfaces at some point in the future (what would type assertions be then? for instance, could
~T
work as exposed above as well?)The text was updated successfully, but these errors were encountered: