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

spec: add examples (or more explicit prose) regarding the identity/difference of types escaping generic and non-generic functions #65152

Open
Merovius opened this issue Jan 18, 2024 · 10 comments
Assignees
Milestone

Comments

@Merovius
Copy link
Contributor

Merovius commented Jan 18, 2024

What did you do?

Playground Link

package main

import "fmt"

func main() {
	fmt.Println(F[int]() == F[int](), F[int]() != G(), G() == G(), F[int]() != F[string]())
}

func F[T comparable]() any {
	type x struct{ _ [0]T }
	return x{}
}

func G() any {
	type x struct{ _ [0]int }
	return x{}
}

What did you see happen?

true true true true

What did you expect to see?

true true true true. The output is clearly intuitively correct and the most sensible behavior of this program. However, I'm having trouble justifying it from the spec.

For equality of interfaces, the spec says:

Interface types that are not type parameters are comparable. Two interface values are equal if they have identical dynamic types and equal dynamic values or if both have value nil.

The program is constructed such that the dynamic types all have exactly one dynamic value, hence we need to check if the dynamic types are identical:

A named type is always different from any other type. Otherwise, two types are identical if their underlying type literals are structurally equivalent; that is, they have the same literal structure and corresponding components have identical types. In detail: […]

In this case, all types involved are named types. So, the question is whether those named types are "other types" or not. Determining whether that is the case is the contention. The only relevant answer I can find says:

A type definition creates a new, distinct type with the same underlying type and operations as the given type and binds an identifier, the type name, to it. […] The new type is called a defined type. It is different from any other type, including the type it is created from.

As well as later:

If the type definition specifies type parameters, the type name denotes a generic type. Generic types must be instantiated when they are used.

Lastly, going back to Type identity, we also note:

Two instantiated types are identical if their defined types and all type arguments are identical.

That's all the relevant spec pieces I can really find.

Now my argument:

F[int]() == F[int]() and G() == G(), demonstrating that the created type is not tied to a call, but to the type definition AST node.

G() != F[int]() demonstrates that different type definition AST nodes lead to non-identical types. Strengthening the assertion that the actual AST node determines type identity.

Buf F[int]() != F[string]() shows that the same type definition can lead to different types. Even though 1. the type definition is not generic (it has no type parameters) and 2. it is a named type, so its identity should solely be determined by its type definition.

To be clear, again: This is obviously the only sensible and expected way for this code to behave, but it seems there is a hole in the spec for this behavior.

@Merovius
Copy link
Contributor Author

cc @griesemer

@Merovius Merovius changed the title spec: Type identity with non-generic types containing type parameter fields spec: Ambiguity of type identity with non-generic types containing type parameter fields Jan 18, 2024
@atdiar
Copy link

atdiar commented Jan 18, 2024

That's a very good question.

I even think, perhaps erroneously, that the case F[int]() != G() is arguably true but could also be false with my current reading of the spec.

I'm not sure I've read that right but the scoping rules around type declaration seem to speak in term of visibility not equality.

As far as I remember, named type equality is still the equality of the name (identifier), type definition literal, and package path. But it might have changed.

https://go.dev/ref/spec#Declarations_and_scope
https://go.dev/ref/spec#Type_identity

Just a remark, I have no answer.

@zephyrtronium
Copy link
Contributor

In this case, all types involved are named types. So, the question is whether those named types are "other types" or not. Determining whether that is the case is the contention. ...

https://go.dev/ref/spec#Instantiations mentions, "instantiating a function produces a new non-generic function." So, every unique instantiation of F is a different function. That means a type declared within is different (in the identity sense) for each instantiation. This is true even if the type parameter isn't used in the type: https://go.dev/play/p/y8f4lMuhDUj (note the change to F). I recall there was some discussion on social media about this some months ago, but I'm struggling to find references now.

I believe this is the fallacy in your argument:

F[int]() == F[int]() and G() == G(), demonstrating that the created type is not tied to a call, but to the type definition AST node.

It is not solely the AST, i.e. the syntactic location, which determines type identity. All type parameters which dominate the type definition participate as well.

@Merovius
Copy link
Contributor Author

@zephyrtronium Ah, that actually makes sense, thank you. The behavior for non-used type parameters feels counterintuitive to me, but I guess it does follow from the spec and at least now I know :) I'll close this.

@seebs
Copy link
Contributor

seebs commented Jan 18, 2024

func F[T comparable]() any {
	return struct{ _ [0]T }{}
}

func G() any {
	return struct{ _ [0]int }{}
}

So it's the named types having the slightly surprising behavior -- if you use unnamed types, these two are now identical.

@atdiar
Copy link

atdiar commented Jan 18, 2024

Yup, after rereading, seems that type names (identifiers) are unique and scoped, that's why named types are all different as per the spec.

And the type definition includes the list of type arguments indeed but for x, there was none anyway so it doesn't even matter here.

@griesemer
Copy link
Contributor

Reopening this (with title change) as a documentation issue. Similar questions have come up repeatedly and the spec could probably benefit from more explicit prose or related examples to be very clear.

@griesemer griesemer reopened this Jan 18, 2024
@griesemer griesemer self-assigned this Jan 18, 2024
@griesemer griesemer added this to the Backlog milestone Jan 18, 2024
@griesemer griesemer changed the title spec: Ambiguity of type identity with non-generic types containing type parameter fields spec: add examples (or more explicit prose) regarding the identity/difference of types escaping generic and non-generic functions Jan 18, 2024
@go101
Copy link

go101 commented Jan 22, 2024

dup of #58573

@go101
Copy link

go101 commented Jan 22, 2024

@griesemer

Reopening this (with title change) as a documentation issue.

Will this be documented as an unspecified behavior?

@griesemer
Copy link
Contributor

This is implicitly specified because "each distinct instantiation of a generic function produces a distinct non-generic function; and each of those functions has its own distinct local types, very much like any other non-generic function". Leaving this one open as a documentation issue and closing #58573.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants