Skip to content

proposal: spec: type inference for union of functions #61802

Description

@matthewmueller

Context

Type inference is a powerful feature where Go can infer generic types based on usage. In the following example, the RPC(Hello) function is able to infer types based on the function signature.

func RPC[In, Out any, Fn Func[In, Out]](fn Fn) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    var in In
    if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
      http.Error(w, err.Error(), http.StatusBadRequest)
      return
    }
    out, err := fn(&in)
    if err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
    }
    if err := json.NewEncoder(w).Encode(out); err != nil {
      http.Error(w, err.Error(), http.StatusInternalServerError)
      return
    }
  })
}

type HelloIn struct {
}

type HelloOut struct {
}

func Hello(in *HelloIn) (*HelloOut, error) {
  return &HelloOut{}, nil
}

// Usage
mux := http.NewServeMux()
mux.Handle("/hello", RPC(Hello))

The long-form equivalent is RPC[HelloIn, HelloOut](Hello). This also works with type constraints when you have a single type. In the following example, we introduce a Func constraint:

type Func[In, Out any] interface {
  func(in *In) (*Out, error)
}

Then adjusting the RPC function to the following:

func RPC[In, Out any, Fn Func[In, Out]](fn Fn) http.Handler {
  // ...
}

And type inference continues to work as expected.

Problem

If you introduce another type like the following:

type Func[In, Out any] interface {
  func(in *In) (*Out, error) | func(ctx context.Context, in *In) (*Out, error)
}

Then type inference no longer works. You'll be greeted with:

./main.go:66:26: cannot infer In 

To fix this, you need to specify the types:

// Usage
mux := http.NewServeMux()
mux.Handle("/hello", RPC[HelloIn, HelloOut](Hello))

Proposal

It'd be great if type inference could infer more complex types. It does seem to work with basic type constraints, which is awesome:

type StringOrInt interface {
	string | int
}

func print[T StringOrInt](t T) {
	switch t := any(t).(type) {
	case string:
		println(t)
	case int:
		println(t)
	}
}

// usage
print(10) // works!
print("hello") // works!

Could this be extended to support more complex types?


Thanks for your consideration!

Activity

  1. added this to the Proposal milestone on Aug 7, 2023
  2. seankhliao commented on Aug 7, 2023

    @seankhliao
    Member

    even if the type could be inferred: you'd run into https://go.dev/play/p/9KP_p3VGEVl

    ./prog.go:23:15: invalid operation: cannot call non-function fn (variable of type Fn constrained by Func[In, Out])

    which makes sense: within your generic function, you have no way of distinguishing between needing to call fn(in) and fn(ctx, in)

    barring movement on #45380, I think a failure here is expected.

  3. matthewmueller commented on Aug 8, 2023

    @matthewmueller
    Author

    Thanks for your response. This actually does work today though:

    package main
    
    import (
    	"context"
    	"encoding/json"
    	"fmt"
    	"net/http"
    )
    
    type Func[In, Out any] interface {
    	func(in *In) (*Out, error) |
    		func(ctx context.Context, in *In) (*Out, error)
    }
    
    func RPC[In, Out any, Fn Func[In, Out]](fn Fn) http.Handler {
    	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    		var in In
    		switch t := any(fn).(type) {
    		case func(in *In) (*Out, error):
    			out, err := t(&in)
    			if err != nil {
    				http.Error(w, err.Error(), http.StatusInternalServerError)
    				return
    			}
    			if err := json.NewEncoder(w).Encode(out); err != nil {
    				http.Error(w, err.Error(), http.StatusInternalServerError)
    				return
    			}
    		case func(ctx context.Context, in *In) (*Out, error):
    			out, err := t(r.Context(), &in)
    			if err != nil {
    				http.Error(w, err.Error(), http.StatusInternalServerError)
    				return
    			}
    			if err := json.NewEncoder(w).Encode(out); err != nil {
    				http.Error(w, err.Error(), http.StatusInternalServerError)
    				return
    			}
    		}
    	})
    }
    
    type HelloIn struct {
    }
    
    type HelloOut struct {
    	Out string
    }
    
    func Hello(in *HelloIn) (*HelloOut, error) {
    	return &HelloOut{"without context"}, nil
    }
    
    func Hello2(ctx context.Context, in *HelloIn) (*HelloOut, error) {
    	return &HelloOut{"with context"}, nil
    }
    
    func main() {
    	mux := http.NewServeMux()
    	mux.Handle("/hello", RPC[HelloIn, HelloOut](Hello))
    	mux.Handle("/hello2", RPC[HelloIn, HelloOut](Hello2))
    	fmt.Println("listening on http://localhost:8080")
    	http.ListenAndServe(":8080", mux)
    }
    $ go run main.go
    listening on http://localhost:8080

    Then in another terminal:

    $ curl http://localhost:8080/hello
    {"Out":"without context"}
    
    $ curl http://localhost:8080/hello2
    {"Out":"with context"}

    It would definitely be better with #45380, but they're orthogonal proposals.

  4. changed the title [-]proposal: Support type inference on functions that accept type constraints[/-] [+]proposal: spec: type inference for union of functions[/+] on Aug 8, 2023
  5. seankhliao commented on Aug 8, 2023

    @seankhliao
    Member

    seems similar to #61731

    cc @ianlancetaylor @griesemer

  6. moved this to Incoming in Proposalson Aug 8, 2023
  7. ianlancetaylor commented on Aug 8, 2023

    @ianlancetaylor
    Contributor

    We currently only do this kind of type inference if the type parameter has a constraint with a core type, which is why the simple case works but the case that permits multiple types does not. It seems pretty complicated to make this work with a constraint with an arbitrary type set. It would seem to introduce a lot of polynomial time algorithms when a value of a type parameter with one constraint is passed to a function that takes a type parameter with a different constraint. If we can figure out a principled and efficient mechanism, then, great. But it's not obvious. See #58650 for more discussion of the kinds of algorithms we can implement.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    ProposalTypeInferenceIssue is related to generic type inference

    Type

    No type

    Projects

    • Status
      Incoming

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions