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: Go 2: allow type-parameterized methods. #43390

Closed
thwd opened this issue Dec 26, 2020 · 10 comments
Closed

proposal: Go 2: allow type-parameterized methods. #43390

thwd opened this issue Dec 26, 2020 · 10 comments
Labels
FrozenDueToAge generics Issue is related to generics LanguageChange Proposal Proposal-Hold v2 A language change or incompatible library change
Milestone

Comments

@thwd
Copy link

thwd commented Dec 26, 2020

go version devel +abe4d3dce1 Tue Dec 8 17:59:47 2020 +0000 linux/amd64

Proposal to allow type-parameterized method definition in go2go

Introduction

The relevant section of the current proposal can be found here.

The idea

It's simple: Disallow type-parameterized methods in interface bodies only.

To illustrate what I mean (excerpt from the proposal):

type HasIdentity interface {
	Identity[T any](T) T // <- Make this line illegal.
}

But do allow the line (also from the proposal):

func (S) Identity[T any](v T) T { return v } // <- Make this line legal.

The arguments

Symmetry

Go methods are syntactic sugar for functions. They're one and the same, receivers are first arguments. An example:

package main

import (
	"fmt"
)

type T string

func (t T) SayHello() { fmt.Printf("Hello, %s!", t) }

func main() {
	// Method notation.
	T("Gopher").SayHello()

	// Function notation.
	(T.SayHello)("Gopher")
}

Making a distinction between functions and methods would break this symmetry.

Usefulness

An obvious use-case that comes to mind a Result type (a Functor in Haskell-speak):

type Result[T any] struct {
	val T
	err error
}

func(r Result[T]) Map[U any] (f func(T) (U, error)) Result[U] {
	if r.err != nil {
		return Result[U]{err: r.err}
	}
	val, err := f(r.val)
	if err != nil {
		return Result[U]{err: err}
	}
	return Result[U]{val: val}
}

func(r Result[T]) Value() (T, error) {
	return r.val, r.err
}

Note how Map requires an additional type-parameter, U. Here's a (silly) way it could be used:

counter, err := httpGet("https://exam.ple/api"). // returns Result[*http.Response]
	Map[*http.Response, []byte](func(res *http.Response) ([]byte, error) {
		defer res.Body.Close()
		return ioutil.ReadAll(res.Body)
	}).
	Map[[]byte, APIResponse](func(body []byte) (APIResponse, error) {
		res := APIResponse{}
		return res, json.Unmarshal(body, &res)
	}).
	Map[APIResponse, int](func(res APIResponse) (int, error) {
		return res.Website.Daily.Visits, nil
	}).
	Value()

Other concepts that can be implemented in similar ways are e.g. Promises, Monads, Optionals, all kinds of pipelines (like map-reducers), transducers.

@ianlancetaylor ianlancetaylor changed the title go2go: Proposal: Allow type-parameterized methods. proposal: Go 2: allow type-parameterized methods. Dec 26, 2020
@gopherbot gopherbot added this to the Proposal milestone Dec 26, 2020
@ianlancetaylor
Copy link
Contributor

CC @griesemer

@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Dec 26, 2020
@ianlancetaylor
Copy link
Contributor

As you say, methods are syntactic sugar for functions, except that methods also satisfy interfaces. If we drop the possibility of satisfying interfaces, then it's not too obvious what we get from methods.

@DeedleFake
Copy link

DeedleFake commented Dec 27, 2020

Mostly it seems to be useful for ergonomic reasons. For example, the classic functional iterator patterns are very clunky with top-level functions, but they work a bit better with methods:

// Top-level functions read backwards and visually separate logic in
// callbacks from calling the function, along with making it an
// absolute pain to modify the chain later, such as by inserting a
// map:
sum := iter.Reduce(
  iter.Filter(
    iter.Slice(someSlice),
    func(v int) bool { return v > 0 },
  ),
  0,
  func(sum, cur int) int { return sum + cur },
)

// Methods chain weirdly because of semicolon rules, but otherwise
// read much more cleanly and are far more editible:
sum := iter.Slice(someSlice).
  Filter(func(v int) bool { return v > 0 }).
  Reduce(0, func(sum, cur int) int { return sum + cur })

Personally, I think that it's worth it even without interface satisfaction, but I won't be too annoyed if the initial version of generics doesn't support them. I think it's worth keeping the discussion open, though, even after generics (hopefully) get put in. I also think that generic types should get inference the same as functions have if at all possible for similar reasons.

@dsnet
Copy link
Member

dsnet commented Dec 30, 2020

When using Go reflection to introspect the set of methods on a type with type-parameterized methods, what happens?

@DeedleFake
Copy link

If interface satisfaction isn't allowed with generic methods, at least at first, then I think that it's probably not too surprising if they also can't be seen at all with reflect. It would have to be documented pretty clearly, though, I think. There may be a way that it could be done, but it would probably require pretty huge changes to reflect.

@tdakkota
Copy link

@thwd

type Getter[T any] interface {
        Get() T
}

type Zero struct {}

func (Zero) Get[T any]() ( _ T) {
          return 
}

In this example, would be Zero implementation of Getter[T]?

@thwd
Copy link
Author

thwd commented Dec 31, 2020

@tdakkota Excellent question.

In so far as visibility is concerned (as in the issue described in the current proposal), I believe this could work BUT it's more complex, because after specialization with e.g. T = int you get (using underscore as a mangle-character stand in):

type Getter_int interface {
    Get() int
}

type Zero struct {}

func(Zero) Get_int() (_ int) {
    return
}

At first glance, Get_int() int does not implement Get() int. I can't think of a straightforward and general way (n type-parameters in Getter and m possibly distinct type-parameters in Zero.Get) of translating this to Go1 via go2go. Maybe there is a way if we narrow it down.

Secondly, the constraints on T in Getter[T] would have to be identical than the constraints on S in Zero.Get[S]. That can be checked relatively straight-forwardly.

My opinion is that there might be a way, but it's complex enough that I would leave it out for now. Baby steps.

@ianlancetaylor
Copy link
Contributor

Putting this on hold pending any decision on incorporating generics into the language.

@andig
Copy link
Contributor

andig commented Aug 9, 2022

Putting this on hold pending any decision on incorporating generics into the language.

Could this be put on the table for 1.20?

@ianlancetaylor ianlancetaylor added the generics Issue is related to generics label Aug 11, 2022
@ianlancetaylor
Copy link
Contributor

Thanks for raising this issue. Although this issue is older, there is a lot more discussion on #49085, so closing this issue in favor of that one.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Aug 11, 2022
@golang golang locked and limited conversation to collaborators Aug 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge generics Issue is related to generics LanguageChange Proposal Proposal-Hold v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

7 participants