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: type inference for union of functions #61802

Open
matthewmueller opened this issue Aug 7, 2023 · 4 comments
Open

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

matthewmueller opened this issue Aug 7, 2023 · 4 comments
Labels
Proposal TypeInference Issue is related to generic type inference
Milestone

Comments

@matthewmueller
Copy link

matthewmueller commented Aug 7, 2023

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!

@gopherbot gopherbot added this to the Proposal milestone Aug 7, 2023
@seankhliao
Copy link
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.

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Aug 7, 2023
@matthewmueller
Copy link
Author

matthewmueller commented Aug 8, 2023

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.

@seankhliao seankhliao changed the title proposal: Support type inference on functions that accept type constraints proposal: spec: type inference for union of functions Aug 8, 2023
@seankhliao seankhliao added the TypeInference Issue is related to generic type inference label Aug 8, 2023
@seankhliao
Copy link
Member

seems similar to #61731

cc @ianlancetaylor @griesemer

@seankhliao seankhliao reopened this Aug 8, 2023
@ianlancetaylor
Copy link
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
Labels
Proposal TypeInference Issue is related to generic type inference
Projects
Status: Incoming
Development

No branches or pull requests

4 participants