Skip to content

proposal: spec: infer types from sufficiently constrained constraints #73527

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

Open
2 of 4 tasks
tempoz opened this issue Apr 28, 2025 · 2 comments
Open
2 of 4 tasks

proposal: spec: infer types from sufficiently constrained constraints #73527

tempoz opened this issue Apr 28, 2025 · 2 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@tempoz
Copy link

tempoz commented Apr 28, 2025

Go Programming Experience

Experienced

Other Languages Experience

Go, Python, C, C++, Java

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?

No

Does this affect error handling?

No

Is this about generics?

Yes; I couldn't find any proposals that related to this one, but admittedly I did not read all of them and only scanned proposals mentioning "type inference".

Proposal

The proposed change is to improve type inference to reduce unnecessary verbosity in some limited cases. The proposal is intended to only affect user experience writing go code, and not to change the behavior of any working go code or add additional functionality to code at runtime.

Currently, it is possible to define a function that will fail to infer a type despite there only being one valid type that may be inferred, e.g.:

// Demonstration of sufficiently constrained
// types that are not inferred.
package main

func f[E any, T []E | *E](_ T) {}

func main() {
	// type inference fails
	f([]bool{})
	// works with type provided
	f[bool]([]bool{})

	// type inference fails
	f((*bool)(nil))
	// works with type provided
	f[bool]((*bool)(nil))
}

Go playground link

Here is a more concrete example of where this might be helpful, as opposed to just a stripped-down demonstration of the behavior:

// Demonstration using formatting parsed command line
// representation as an example.
package main

import (
	"fmt"
	"iter"
	"slices"
)

type Sequenceable[E any] interface {
	[]E | iter.Seq[E]
}

func Sequence[E any, S Sequenceable[E]](s S) iter.Seq[E] {
	switch v := any(s).(type) {
	case []E:
		return slices.Values(v)
	case iter.Seq[E]:
		return v
	}
	// This should be impossible
	return nil
}

func Chain[E any, S1 Sequenceable[E], S2 Sequenceable[E]](s1 S1, s2 S2) iter.Seq[E] {
	return func(yield func(E) bool) {
		for e := range Sequence[E](s1) {
			if !yield(e) {
				return
			}
		}
		for e := range Sequence[E](s2) {
			if !yield(e) {
				return
			}
		}
	}
}

// Concrete types for example

type Argument interface {
	Format() []string
}

type PositionalArgument struct {
	Value string
}

func (p *PositionalArgument) Format() []string {
	return []string{p.Value}
}

type Option struct {
	Name  string
	Value *string
}

func (o *Option) Format() []string {
	if o.Value == nil {
		return []string{"--" + o.Name}
	}
	return []string{"--" + o.Name, "'" + *o.Value + "'"}
}

func FromConcrete[E Argument, S Sequenceable[E]](args S) iter.Seq[Argument] {
	return func(yield func(Argument) bool) {
		for e := range Sequence[E](args) {
			if !yield(Argument(e)) {
				return
			}
		}
	}
}

func FormatAll[E Argument, S Sequenceable[E]](args S) []string {
	var formatted []string
	for a := range Sequence[E](args) {
		formatted = append(formatted, a.Format()...)
	}
	return formatted
}

func main() {
	fooValue := "FOO"
	options := []*Option{
		{
			Name:  "foo",
			Value: &fooValue,
		},
		{
			Name: "bar",
		},
	}
	posArgs := []*PositionalArgument{
		{
			Value: "test",
		},
	}
	fmt.Printf("Options: %#v\n", FormatAll[*Option](options))
	fmt.Printf("Positional arguments: %#v\n", FormatAll[*PositionalArgument](posArgs))

	fmt.Printf("All arguments: %#v\n", FormatAll[Argument](
		Chain[Argument](
			FromConcrete[*Option](options),
			Chain[Argument](
				[]Argument{&PositionalArgument{Value: "--"}},
				FromConcrete[*PositionalArgument](posArgs),
			),
		),
	))
}

Go playground link

In the above example, the developer is forced to explicitly declare the type in every call to a generic function, and in all cases the type could be inferred, as there is only one valid type that may be specified.

Language Spec Changes

This would make it valid to elide the type parameter when calling generic functions where the type that would otherwise be specified is sufficiently constrained by the type constraints of a dependent type parameter that is already being inferred. This is already true, as in func _[E any, T []E] (_ T), but this would also cover cases where the type constraint is a more complex disjunction of dependent parameterized types.

Informal Change

When one type parameter, T, of a generic function is dependent on another type parameter, E, of that same generic function, it is sometimes possible for the compiler to infer the type of E when the type of T is already being inferred, even when T is being constrained by a disjunctive constraint, as seen in the following example:

func foo[E any, T []E | *E](t T) {}

In this case, the type T is constrained to two mutually-exclusive types: *E and []E. Therefore, if T is a slice, E must be the element type of the slice, while if T is a pointer, E must be the addressed type.

However, this is not always possible. Consider, for example:

func foo[E any, T *[]E | *E](t T) {}

Now the types are not necessarily mutually exclusive. If T is a pointer that points to a slice, E may either be the addressed type (the type of the slice), or the element of the addressed type (the type of the element of the slice). Thus, E must be explicitly specified as a type parameter when T is (or may be) a pointer to a slice.

Is this change backward compatible?

Yes.

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

This is exclusively a change to improve type inference. It should affect no other features.

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

This change would probably not really affect how easy it is to learn Go. It would only make it slightly easier to read and write Go.

Cost Description

The cost of this proposal is additional complexity in the type inference system in the compiler. It would probably incur a very slight performance penalty when compiling existing go code due to the conditional necessary to skip over this new functionality for existing go code. Depending on how it is implemented, it might be possible to write Go code that compiles (or fails to compile) very slowly by forcing the type inference code to explore too many possible options when doing type inference.

Changes to Go ToolChain

No response

Performance Costs

The compile time cost should be marginal. The runtime cost should be nonexistent.

Prototype

I don't really feel qualified to do this, given that I have never worked on the go compiler before. If other people also think this is a worthwhile feature but do not want to implement it, I would be willing to try prototyping it, but before I know if there is even support for the feature, I am dubious that it is worthwhile for me to attempt this.

@tempoz tempoz added LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal labels Apr 28, 2025
@gopherbot gopherbot added this to the Proposal milestone Apr 28, 2025
@tempoz tempoz changed the title proposal: spec: Infer types when from sufficiently constrained constraints proposal: spec: infer types from sufficiently constrained constraints Apr 28, 2025
@apparentlymart
Copy link

This is an interesting idea! One aspect of it gives me pause:

Today I believe it's always(?) backward-compatible to loosen the constraint on a type parameter, growing the set of potentially-valid types, as long as all of the types that were previously-accepted are still accepted. For example, it would be valid to replace comparable with any because any contains all of the types that comparable contains.

With this proposal I think there would now be a case where loosening a constraint can be a breaking change for callers who were previously relying on this rule: the looser constraint may now have multiple possible types it could select for an argument that was previously automatically inferrable.

Situations where a caller can rely on some characteristic of a library API that the author wasn't intending to provide as a guarantee tend to be a hazard for "programming in the large" where libraries are maintained by separate teams than their callers, and so historically Go language design has tried to minimize such situations.

@tempoz
Copy link
Author

tempoz commented Apr 29, 2025

Oh, wow, I hadn't even considered that!

Today I believe it's always(?) backward-compatible to loosen the constraint on a type parameter, growing the set of potentially-valid types, as long as all of the types that were previously-accepted are still accepted.

I think this isn't strictly true, see, for example:

package main

import "iter"

func constrained[E any, T []E](_ T) *E {
	return (*E)(nil)
}

func moreLooselyConstrained[E any, T []E | iter.Seq[E]](_ T) *E {
	return (*E)(nil)
}

func main() {
	constrained([]bool{})
	moreLooselyConstrained([]bool{})
}

Go playground link

E is initially inferred as the element type of T in constrained, but when T is more loosely constrained in moreLooselyConstrained, E is no longer inferrable and instead must be specified explicitly.

That said, your point here:

Situations where a caller can rely on some characteristic of a library API that the author wasn't intending to provide as a guarantee tend to be a hazard for "programming in the large" where libraries are maintained by separate teams than their callers, and so historically Go language design has tried to minimize such situations.

is still completely valid, and this change would allow more opportunities to arise in the future for authors to write code where loosening a constraint could require code changes by those depending on a given function, so I definitely agree that this is still a cost that should be considered when deciding on the worthiness of this proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests

3 participants