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: allow method declarations on function-local types #71562

Open
kazzmir opened this issue Feb 4, 2025 · 10 comments
Open

proposal: spec: allow method declarations on function-local types #71562

kazzmir opened this issue Feb 4, 2025 · 10 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee LanguageProposal Issues describing a requested change to the Go language specification. Proposal Proposal-FinalCommentPeriod
Milestone

Comments

@kazzmir
Copy link

kazzmir commented Feb 4, 2025

Proposal Details

Methods cannot be declared inside a function scope. For types that are declared inside a function, it is occasionally useful to also be able to define methods on those types in the same function rather than having to move the entire type and its methods outside of the function. The benefits are

  • A local type can implement an interface by virtue of having methods defined on it
  • Local types can be treated similarly to types declared at global scope that can invoke methods as x.f(), instead of being regarded as second-tier that would require f(x)

An example demonstrating the first point

func writeOutput(out io.Writer){

  type Data struct {
    x, z, y int
  }
 
  func (d *Data) Read(b []byte) (n int, err error){
    // ... some implementation
  }

   myData := Data{ ... }
   io.Copy(out, &myData) // use local type Data as an io.Reader, since it has a Read() method defined on it
}

Example demonstrating second point

func compute(){
  type MyObject struct {
     x int
     y int
  }
  func (obj *MyObject) SetX(x int){
    obj.x = x
  }
  func (obj *MyObject) SetY(x int){
    obj.y = y
  }
}

The second example can be rewritten without a method receiver, where a regular function accepts a *MyObject type as the first argument, but this creates a distinction between types declared globally and those declared locally.

func compute(){
  type MyObject struct {
    x int
    y int
  }
  func SetX(obj *MyObject, x int){
     obj.x = x
  }
  func SetY(obj *MyObject, y int){
      obj.y = y
  }

A more advanced version of this proposal would be to allow the local methods to close over bindings declared before the method, thus allowing an inner method receiver to invoke methods on an outer method receiver.

type System struct {
  ...
}
func (sys *System) GetUser(name string) *User { ... }

func (sys *System) doSomething(name string){
   type Data struct {
     ...
   }
   func (d *Data) GetX(){
     user := sys.GetUser(name)
     return user.X
   }
}

I could imagine that local methods that do not support closing over local bindings could be implemented by lifting the type and its declaration to the global scope, but keeping the scope of the bindings local to the function they were defined in.

@gopherbot gopherbot added this to the Proposal milestone Feb 4, 2025
@seankhliao
Copy link
Member

Please fill out https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing language changes

We'd also need to see much stronger evidence that this is useful across the ecosystem for it to be considered

@seankhliao seankhliao added LanguageChange Suggested changes to the Go language WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. labels Feb 4, 2025
@gabyhelp
Copy link

gabyhelp commented Feb 4, 2025

Related Issues

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

@gabyhelp gabyhelp added the LanguageProposal Issues describing a requested change to the Go language specification. label Feb 4, 2025
@ianlancetaylor ianlancetaylor added the LanguageChangeReview Discussed by language change review committee label Feb 4, 2025
@ianlancetaylor
Copy link
Member

Outside of a function all identifiers are compiled in a scope such that they can all see all other package-scope identifiers. That permits methods to refer to each other, as in

func (T) M1() { M2() }
func (T) M2() { M1() }

Currently, within a function, identifiers are only visible after they have been declared. That means, for example, that you can't write

func F() {
    fn := func() { fn() }
}

You have to instead write

func F() {
    var fn func()
    fn = func() { fn() }
}

If we permit methods within a function, we need to exact set of scoping rules that apply to them.

@kazzmir
Copy link
Author

kazzmir commented Feb 4, 2025

Good point, I do frequently have to write code such as

func F() {
    var fn func()
    fn = func() { fn() }
}

To allow a locally defined function to call itself recursively, so if that was changed to be allowed it would be welcome.

@ianlancetaylor
Copy link
Member

Changing that would be a different proposal. I recall that it's been proposed before, but I didn't have any luck finding the earlier discussion.

@seankhliao
Copy link
Member

that's #33167

@seankhliao seankhliao added WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. and removed WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. labels Feb 5, 2025
@earthboundkid
Copy link
Contributor

I want this in the abstract, but I think methods shouldn't be able to close over variables, so it won't work.

func f() (interface{ foo() int }) {
    type s struct{} // no data!
    x := 0
    func (s) foo() int { return x } // Where is x stored? What happens if it mutates?
    return s
}

Too big of a can of worms.

@apparentlymart
Copy link

apparentlymart commented Feb 5, 2025

I recall some earlier proposals, similar to but not exactly #47487, about various different mechanisms to make a value that implements an interface by building it out of one closure per method.

Unfortunately I can't find it now, but I recall that one of them proposed something like this:

type Example interface {
    A()
    B() string
}

func WantsExample(e Example) {
    // ...
}

func main() {
    WantsExample(Example{
        A: func () { /* ... */ },
        B: func () string { /* ... */ },
    })
}

In that case the interface is effectively being implemented based on data captured into the closures, thus partially answering @earthboundkid's question.

I don't recall all of what caused that proposal to run into trouble, but one challenge I can already see with it just having written it out is that this language feature would presumably need to construct some sort of special "union-closure" that combines all of the closures of all of the functions declared inline. That's weird enough even in this simple example, but potentially even weirder if one of the function pointers had been passed in as an argument having already captured variables from some other scope into its own closure.

I'm mentioning this only as a substitute for linking to that previous proposal as context that might help with discussion on this proposal; I'm not intending to make a counterproposal. I was not able to figure out what to search for to find it. I'm sorry if I'm misremembering details from it.

Edit: Naturally, I found it immediately after I posted 🙄 #25860

@adonovan
Copy link
Member

adonovan commented Feb 5, 2025

I've often wondered why Go's func declaration was special in that you can't use it within a function (for methods or even ordinary functions), even though that would be convenient. One possible reason is that it would create a parsing ambiguity: when parseStmt sees func, should it expect a call to a literal function (func(){ ... }()) or a declaration of a function (func f())? This could be resolved by additional lookahead.

I'm probably in the minority, but the nature of my work demands a lot of recursive functions, and I prefer to use local functions to reduce scope and avoid polluting the namespace of the package. (The alternative is to extract a package-level struct and define methods on it, but for small functions that creates a detour for the reader.) In principle I too would welcome a way to write recursive local named functions using a func declaration--though the magnitude of that change on our tooling is daunting.

As for local methods on local types: the obvious implementation is to collect all the free variables of the methods and add them as hidden fields of the local type, so the example below would have a hidden, unnameable pair of fields x, y *int. This would make the unsafe.Sizeof the struct larger than it would appear, but that seems reasonable. But what happens if you use reflect.New to create a variable of that type? Now you can't call methods because the hidden fields cannot be correctly set. The failures would be hard to explain. My inclination is that it's not worth the trouble. And if you disallow local methods from having free variables, then there's really very little benefit to the feature.

func makePair(x, y int) interface { first() int; second() int } {
    type Pair struct{}
    func (Pair) first() int { return x } 
    func (Pair) second() int { return y } 
    return Pair{}
}

In summary:

  • local recursive func decls are attractive, but it's a big change to the tools.
  • local methods with free variables would lead to surprising failures.
  • local methods without free variables are not compelling.

[Edit: my first draft was in error about MyObject; changed to use makePair example.]

@ianlancetaylor
Copy link
Member

Based on the discussion above about the complexities of this feature concerning scoping and closures, this is a likely decline. Leaving open for four weeks for final comments.

@ianlancetaylor ianlancetaylor added Proposal-FinalCommentPeriod and removed WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. labels Feb 5, 2025
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 LanguageProposal Issues describing a requested change to the Go language specification. Proposal Proposal-FinalCommentPeriod
Projects
None yet
Development

No branches or pull requests

8 participants