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: Values assigned to multi-case branches in type switches should have generics-style #69705

Open
2 of 4 tasks
jmillikin opened this issue Sep 29, 2024 · 3 comments
Labels
generics Issue is related to generics LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal Proposal-Hold
Milestone

Comments

@jmillikin
Copy link

Go Programming Experience

Experienced

Other Languages Experience

C, C++, Python, Rust, Haskell

Related Idea

  • Has this idea, or one like it, been proposed before?
  • Does this affect error handling?
  • Is this about generics?
  • Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit

Has this idea, or one like it, been proposed before?

#65031 seems similar, although the details differ.

#57644 is possibly related, but I can't tell from its description what effect (if any) it would have on type switches.

Does this affect error handling?

No

Is this about generics?

Yes -- this proposal is to make multi-case type switches match the semantics of generic functions with a set of permitted types.

Proposal

In Go 1.23, a value assigned as the match result in a multi-case type switch retains the original type being inspected. This prevents it from being passed to a function with a type parameter constraint that's only satisfied by the narrowed type.

func fmtUint[T uint8 | uint16 | uint32 | uint64](value T) string {
	return strconv.FormatUint(uint64(value), 10)
}

// error: any does not satisfy uint8 | uint16 | uint32 | uint64
func fmtValue(value any) string {
	switch v := value.(type) {
	case uint8, uint16, uint32, uint64:
		// `v` is known to be of the above listed concrete types,
		// but its type remains `interface{}`, so it can't be used as `T`.
		return fmtUint(v)
	}
	return fmt.Sprintf("%v", value)
}

// semantically equivalent, and valid in Go 1.23, but overly verbose
func fmtValue(value any) string {
	switch v := value.(type) {
	case uint8:
		return fmtUint(v)
	case uint16:
		return fmtUint(v)
	case uint32:
		return fmtUint(v)
	case uint64:
		return fmtUint(v)
	}
	return fmt.Sprintf("%v", value)
}

I propose to change the semantics of multi-case type switches so that the type of the matched variable becomes the intersection of the matched types, plus the original type:

interface {
	// To preserve existing behavior, `v` can be treated as its original type.
	any

	// `v` can also satisfy type parameter conditions that permit all of the types
	// in its case match.
	( uint8 | uint16 | uint32 | uint64 )
}

The matched case must have types that are a subset of the generic type:

// OK: `v` might be a `uint8` or `uint16`, but both of those are acceptable to `fmtUint`
func fmtValue(value any) string {
	switch v := value.(type) {
	case uint8, uint16:
		return fmtUint(v)
	}
	return fmt.Sprintf("%v", value)
}

// Error: `v` might be `uintptr`, so the set of possible types is a superset of those
// accepted by`fmtUint`.
func fmtValue(value any) string {
	switch v := value.(type) {
	case uint8, uint16, uint32, uint64, uintptr:
		return fmtUint(v)
	}
	return fmt.Sprintf("%v", value)
}

The type of v should also work with the semantics of a generic type parameter for regular code within the case, for example performing conversions that are valid for all matchable types:

// OK: all of the matched types can be cast to uint64.
func fmtValueHex(value any) string {
	switch v := value.(type) {
	case uint8, uint16, uint32, uint64:
		return strconv.FormatUint(uint64(v), 16)
	}
	return fmt.Sprintf("%v", value)
}

Language Spec Changes

No response

Informal Change

No response

Is this change backward compatible?

I think so? Given existing and proposed behavior, I believe that any existing code would continue to compile and run without changes.

Orthogonality: How does this change interact or overlap with existing features?

No response

Would this change make Go easier or harder to learn, and why?

No response

Cost Description

No response

Changes to Go ToolChain

No response

Performance Costs

No response

Prototype

No response

@jmillikin jmillikin added LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal labels Sep 29, 2024
@gopherbot gopherbot added this to the Proposal milestone Sep 29, 2024
@zigo101
Copy link

zigo101 commented Oct 2, 2024

This is just a specific case of supporting values of non-basic interfaces.

interface {
	// To preserve existing behavior, `v` can be treated as its original type.
	any

	// `v` can also satisfy type parameter conditions that permit all of the types
	// in its case match.
	( uint8 | uint16 | uint32 | uint64 )
}

is equivalent to

interface { uint8 | uint16 | uint32 | uint64 }

The proposal is equivalent to to support:

var v interface { uint8 | uint16 | uint32 | uint64 } = 123
var _ = uint64(v)

@ianlancetaylor ianlancetaylor added the generics Issue is related to generics label Oct 2, 2024
@ianlancetaylor
Copy link
Contributor

Something like this is a good idea, but it needs to be part of a larger change, such as allowing type constraint interfaces in arbitrary positions. It is also related to type switches over type parameters (#45380).

If we do this, it would probably make more sense, and be more backward compatible, to use a different syntax:

    case uint8 | uint16 | uint32 | uint64 | uintptr:

Putting on hold for now.

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 LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal Proposal-Hold
Projects
None yet
Development

No branches or pull requests

5 participants