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: spec: allow type assertion on type parameter value #49206

Open
SamWhited opened this issue Oct 28, 2021 · 13 comments
Open

proposal: spec: allow type assertion on type parameter value #49206

SamWhited opened this issue Oct 28, 2021 · 13 comments
Labels
generics Issue is related to generics Proposal Proposal-Hold
Milestone

Comments

@SamWhited
Copy link
Member

SamWhited commented Oct 28, 2021

Hi all,

When using the Go 1.18 generics implementation I've found myself several times wishing I had compile time type assertions. When looking to see why these didn't exist I found the FAQ entry about it, which contains the following:

We removed this facility because it is always possible to convert a value of any type to the empty interface type, and then use a type assertion or type switch on that.

I started doing this as that's what was recommended, however I quickly ran into an issue: if I accidentally asserted on the wrong thing (ie the struct instead of a field, or the wrong field, etc.) this would result in a silent failure (no error, the type switch would just fail) because anything can be stuffed into an empty interface. In one concrete example I typed the following:

switch v := (interface{})(item).(type) { …

and was having trouble figuring out why my code was failing. What I had meant to type was item.Value or something along those lines which is a generic member of the struct, however, because anything could be boxed into an interface the code happily compiled and ran, resulting in fallback behavior even though I knew the type was correct (and confusing me for quite a while until I spotted the typo).

If however compile time type switching and assertions existed this would have failed because item is not an interface type. I would have then realized my mistake and been able to fix it thanks to the nice error message. Another benefit is that at compile time the type switch or assertion could result in a compile error if one of the branches or the assertion was a value that does not meet the interface, adding a layer of compile time safety and reducing dead code, or possibly catching some accidental uses of the wrong type.

In just a few days of using generics on a real project I have run into this and similar issues several times, so I'd like to request that the lack of compile time assertions be reconsidered.

The other reason mentioned for not doing them is that it can be confusing which type we're asserting on:

Also, it was sometimes confusing that in a constraint with a type set that uses approximation elements, a type assertion or type switch would use the actual type argument, not the underlying type of the type argument

However that confusion is not reduced by using a runtime type switch as evidenced by the example later on in this section where similar confusion occurs at runtime. I believe the potential for silent failures to be worse than the potential for confusion about the types (though that is entirely anecdotal based on a very limited amount of time using the most recent proposal for anything that's not just a toy).

I will volunteer to write a proper proposal if this is something the Go team is willing to reconsider in a future version of Go.

@SamWhited SamWhited changed the title Compile time type assertions generics: reconsider lack of compile time type assertions due to potential for bugs when using empty interface Oct 28, 2021
@SamWhited SamWhited changed the title generics: reconsider lack of compile time type assertions due to potential for bugs when using empty interface proposal: reconsider lack of compile time type assertions due to potential for bugs when using empty interface Oct 28, 2021
@gopherbot gopherbot added this to the Proposal milestone Oct 28, 2021
@seankhliao seankhliao added the generics Issue is related to generics label Oct 28, 2021
@ianlancetaylor
Copy link
Contributor

CC @griesemer

There was some discussion of this in the thread starting at https://groups.google.com/g/golang-nuts/c/iAD0NBz3DYw/m/VcXSK55XAwAJ which led to that part of the proposal being withdrawn. CC @rogpeppe

I think it's probably too late to add this to 1.18 at this point.

@SamWhited
Copy link
Member Author

SamWhited commented Oct 29, 2021

This thread seems to be referring to type switching on the constraint, not on a value with a generic type. I was assuming type switching on a value but being able to treat it as the final concrete type because only the correct branch would be compiled in and at this point there is no uncertainty about what type the value is. It's possible I misunderstood this about the original FAQ in the proposal as well. For example, I'd like to be able to do something like the following and have it work (with the type switch reduced to just the correct line at compile time):

type foo struct {
	foo string
}

type bar struct {
	bar string
}

func printFooOrBar[T foo | bar](v T) {
	switch t := v.(type) {
	case foo:
		fmt.Println(foo.foo)
	case bar:
		fmt.Println(bar.bar)
	}
}

func main() {
	printFooOrBar(foo{"foo"})
	printFooOrBar(bar{"foo"})
}

It's possible this would need some different syntax (.[type]?) to distinguish it from the case where v is indeed a runtime interface though, otherwise it can be hard to tell what's happening at a glance.

@thepudds
Copy link
Contributor

thepudds commented Nov 1, 2021

I was assuming type switching on a value but being able to treat it as the final concrete type because only the correct branch would be compiled in and at this point there is no uncertainty about what type the value is

Hi @SamWhited, is there some overlap with what you wrote in that quote with #45380? (And I happen to like this earlier example in #45346 (comment)).

@rogpeppe
Copy link
Contributor

rogpeppe commented Nov 1, 2021

I don't see any particular reason why a type switch on a generic value couldn't be allowed (although the "reduced to just the correct line at compile time" aspect is an implementation detail and probably wouldn't be the case with the current GC-shape stenciling implementation).

I'm biased of course, but I think that #45380 provides a nicer and more generally useful facility. For example, if printFooOrBar returned a value of type T it wouldn't be possible to return t without a type assertion, even if this proposal were accepted.

@SamWhited
Copy link
Member Author

is there some overlap with what you wrote in that quote with #45380?

Yes, that looks the same. Thanks for the link!

although the "reduced to just the correct line at compile time" aspect is an implementation detail

Yes, that's definitely the case.

@rsc rsc changed the title proposal: reconsider lack of compile time type assertions due to potential for bugs when using empty interface proposal: spec: allow type assertion on type parameter value Nov 3, 2021
@rsc
Copy link
Contributor

rsc commented Nov 3, 2021

Placed on hold.
— rsc for the proposal review group

@deanveloper
Copy link

Really we want to examine the type parameter though, not the variable, right?. Perhaps it would be more useful if we switched on the type parameter itself.

switch T.(type) {
case rune:
    ...
case ~string:
    ...
case int, uint:
    ...
}

@rogpeppe
Copy link
Contributor

rogpeppe commented Nov 3, 2021

Perhaps it would be more useful if we switched on the type parameter itself.

That's exactly what #45380 proposes.

@jalavosus
Copy link

Not sure if this specific use case is part of a proposal (the issue trackers are a bit tricky to search through sometimes), but.

Say I have these type constraints:

type SignedInt interface {
  ~int | ~int8 | ~int16 | ~int32 | ~int64
}

type UnsignedInt interface {
  ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64
}

type BigInt interface {
  ~*big.Int
}

type MaybeBigInt interface {
  BigInt | SignedInteger | UnsignedInteger
}

And have a function which takes MaybeBigInt as a T:

func CheckParseMaybeBigInt[T MaybeBigInt](val T) *big.Int {
   ...
}

Currently I have to do a type switch using (interface{})(val).(type) and check all the possible type values within those constraints (ie. int, int8, etc.), where being able to do

switch n := val.(type) {
case SignedInteger:
  return big.NewInt(int64(n))
case UnsignedInteger
  return new(big.Int).SetUint64(uint64(n))
case BigInt:
  return n
}

would be infinitely cleaner and far easier to code.

@virtuald
Copy link

I realize this is on hold, but FWIW started migrating a codebase to generics today and accidentally did a similar thing as mentioned in the description when using any() to perform a type cast:

In one concrete example I typed the following:

switch v := (interface{})(item).(type) { …

and was having trouble figuring out why my code was failing. What I had meant to type was item.Value or something along those lines

@dc0d
Copy link

dc0d commented Mar 28, 2022

switch ((any)(v)).(type) seem to work well enough.

This is a valid (hypothetical) Go code:

type Left[T any] struct{ Value T }
type Right[T any] struct{ Value T }

type Either[T any, V any] interface{ Left[T] | Right[V] }

func action[T Either[int, string]](v T) {
	switch ((any)(v)).(type) {
	case Left[int]:
		fmt.Println("Left")
	case Right[string]:
		fmt.Println("Right")
	}
}

func sampleCall(v Left[int]) { action(v) }

And it seems type assertion on v would not be (readily) possible. If I understand correctly (let's say partially), Left[T] and Right[T] are types. But Either[T, V] is not a type. It is a type constraint (so, it is not possible to define a function like func fn(v Either[int, string]) {}). Not sure about the underlying implementation details, but providing type switches over v means providing type switches over type constraints.

func typeSwitchLab1(val interface{}) {
	switch val.(type) {
	case int:
		fmt.Println("int")
	case string:
		fmt.Println("string")

		// case Either[int, string]: <- this is not valid
	}
}

func typeSwitchLab2[T Either[int, string]](val T) {
	/*
		switch val.(type) { <- this is not valid
		case Left[int]:
			fmt.Println("Left")
		case Right[string]:
			fmt.Println("Right")
		}
	*/
}

If type constraints participate in type switches, then type constraints will also be types (in that case, should all type parameters be already known/resolved?).

I vouch neither for nor against this feature. I am just curious about the amount of its real-world usage and how it might change the current implementation (and how the added semantic complexity would look like).

@rogpeppe
Copy link
Contributor

@dc0d I don't really understand your question. As you've shown, you can currently do a type switch on a generic value (https://go.dev/play/p/arj825wElvB) - your typeSwitchLab2 would work if it did the type switch on any(val). Allowing that conversion to be omitted is the essence of this proposal AIUI.

In your typeSwitchLab1 example, it would work fine to switch on Left[int] or Right[string] as opposed to Either[int, string] - both of those being concrete types allowed by the Either constraint.

See this issue for a proposal that would allow putting a constraint in a type switch case: #45380.

If that were implemented, you could do:

func typeSwitchLab3[T any](val T) {
	switch type T {
	case Either[int, string]:
		fmt.Println("Either")
	default:
		fmt.Println("Other")
	}
}

@dc0d
Copy link

dc0d commented Apr 21, 2022

@rogpeppe As stated at the end, it was not a question. It was an "opinion" based on the speculated required changes needed for this feature (including re-scoping type constraints with multiple overloaded meaning/semantics - including type switching over type constraints and not just types). The amount of complexity and cognitive load added by this feature could surpass the value it provides.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
generics Issue is related to generics Proposal Proposal-Hold
Projects
Status: Hold
Development

No branches or pull requests