-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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: method signature overloading #22051
Comments
Honestly the original proposal was much better, this one is just over-complicated and adds horrible syntax. |
Methods and interfaces are closely tied. I don't see the point of providing a new way to define methods that does not affect interfaces. Or, to put it a different way, these really don't seem like methods, so why make them look like methods? |
The method declaration syntax is not changed by the proposal. Only the non-syntax restriction in the specs text, The method call syntax is currently |
Well, there are languages with methods attached to types but having no concept of interfaces. Affecting interfaces is avoided in the proposal because it seems to me more complicated to design and specify. It'd be a waste of effort without first having understanding and/or consensus about the underlying issue - multiple method receivers. For the same reason method expressions and method values are forbidden for now, even though in those cases it's simpler to specify. All of that can be a refinement of this proposal or a standalone follow-up proposal. But I think discussing the first principles should come first.
Currently a method declaration is
The proposal does not change that so I'm not sure why lifting a restriction, loosely said, of 'sequence of one', to just 'sequence' can make it not look like a method. I'm sometimes writing code like this func (t *T) fooAA(a, b A) { ... } // edit: typo, was 'b AA'
func (t *T) fooAB(a A , b B) { ... } // edit: typo, was aA
func (t *T) fooBA(a B, b A) { ... }
func (t *T) fooBB(a, b B) { ... } // edit: typo, was 'b BB'
func (t *T) foo(a, b I) {
switch x := a.(type) {
case A:
switch y := b.(type) {
case A:
t.fooAA(x, y)
case B:
t.fooAB(x, y)
}
case B:
switch y := b.(type) {
case A:
t.fooBA(x, y)
case B:
t.fooBB(x, y)
}
}
} especially when the bodies of fooAA, fooAB, ... are not just a few lines. Under the proposal the equivalent code is just func (t *T, a, b A) foo() { ... }
func (t *T, a A, b B) foo() { ... }
func (t *T, a B, b A) foo() { ... }
func (t *T, a, b B) foo() { ... } Another example where I'd probably consider using the feature is in some evaluator-like code (poor example but you get the idea) func (a, b int) add() interface{} { return a+b }
func (a int, b float64) add() interface{} { return float64(a)+b }
func (a float64, b int) add() interface{} { return (b, a).add() } // edit: typo, was `a float64, a int`.
func (a, b float64) add() interface{} { return a+b } and then, schematically var a, b, c, d interface{} = 1, 2.3, 4.5, 6
fmt.Println((a, b).add())
// or even
fmt.Println(((a, b).add(), (c, d).add()).add()) |
If we are considering function overloading, are we also going to consider pattern matching? In my opinion, they are related: The same concept implemented differently. |
Can you give some more-concrete examples? I'm having trouble understanding what problem you're trying to solve with this proposal. |
An example that may possibly avoid manually written, big, nested type switches - a run-time evaluator in ql. A perhaps less relevant, non-evaluator-like example in a persistence-ready B-tree: db. Note the (*BTree).cat vs .catX, .clrD vs .clrX, .copy vs .copyX and so on. I tried with those methods attached to {btDPage,btXPage} but benchmarks seem to indicate having it all as just methods of *BTree is better. (The intended optimizations are not yet finished, so I cannot yet prove that for this example. The code was not planned to be publisehd yet, but it was actually this very code that triggered the idea so I think it's possibly a better illustration of the underlying thoughts.) |
but how to distinct func a(b interface{}) and func a(b string) |
I don't understand the question within the context of this proposal, which is about methods. Methods always have a concrete receiver type, or a sequence of types under this proposal. Can you please elaborate? |
@cznic You could address the nesting problem by hoisting the switch from types to values of type Still, if nesting for type-switches is the only use-case this addresses, I would argue that it would be clearer to add multiple-assignment type switches than method overloads: a, b := o.get2(execCtx, ctx)
if a == nil || b == nil {
return
}
switch x, y := a.(type), b.(type) {
case (idealComplex, idealComplex):
return idealComplex(complex64(x) + complex64(y)), nil
case (idealFloat, idealFloat):
return idealFloat(float64(x) + float64(y)), nil
default:
return invOp2(x, y, op)
} As @SCKelemen notes, that would move type-switches a bit closer to general pattern-matching, although I would argue that type-switches would still remain much simpler. For me, the defining characteristic of pattern-matching is the unpacking of value-constructors, but since Go (IMO wisely) does not have tuple types, matching multiple assignments still would not “unpack” values. (If we were to allow value-switches on composite literals, that would be an entirely different matter, but that seems overkill for @cznic's use-case.) |
Well, I consider using reflection in this case as the last resort only. Even thoug in many other cases its use makes perfect sense, no doubt.
That idea removes most, if not all of the pain of the nested type switches, very interesting. Let me use a simplified version of your code and present three snippets next to each other. (The simplification is only removing type conversions that are not important here.) Using valid Go: func (o *binaryOperation) eval(ctx *ctx) (interface{}, error) {
a, b := o.get2(execCtx, ctx) // returns (interface{}, interface{})
if a == nil || b == nil {
return
}
switch o.op {
case '+':
switch x := a.(type) {
case complex64:
switch y := b.(type) {
case complex64:
return x + y, nil
}
case float64:
switch y := b.(type) {
case float64:
return x + y, nil
}
}
}
return invOp2(a, b)
} Using multiple-assignment type switches: func (o *binaryOperation) eval(ctx *ctx) (interface{}, error) {
a, b := o.get2(execCtx, ctx) // returns (interface{}, interface{})
if a == nil || b == nil {
return
}
switch o.op {
case '+':
switch x, y := a.(type), b.(type) {
case (complex64, complex64):
return x + y, nil
case (float64, float64):
return x + y, nil
}
}
return invOp2(a, b)
} Using this proposal: type value interface {
add(value) value
}
func (x, y complex64) add() value { return x + y }
func (x, y float64) add() value { return x + y }
func (o *binaryOperation) eval(ctx *ctx) (value, error) {
a, b := o.get2(execCtx, ctx) // returns (value, value)
if a == nil || b == nil {
return
}
switch o.op {
case '+':
return (a, b).add()
}
return invOp2(a, b)
} The last snippet actually uses one of the preliminary ideas of how method sets interact with multiple-receiver methods, which is not part of the proposal (yet). I hope the mechanism is obvious, but I'm far from sure if that would be the right/correct/possible/acceptable mechanism. I haven't yet thought about it enough to know better. Note that there's a catch: The run-time panic can be guaranteed/statically proved to not occur when two more methods are added: func (x complex64, y float64) add() value { return x + complex(y, 0) }
func (x float64, y complex64) add() value { return (y, x).add() } |
I think that goes directly to @ianlancetaylor's earlier point about needing to consider the interaction between this proposal and interfaces. If a tuple of values can implement a single interface, the mapping between values and interfaces is no longer 1:1. I expect that would get too complicated very quickly. |
Thinking more about the multiple-assignment func (o *binaryOperation) eval(ctx *ctx) (interface{}, error) {
a, b := o.get2(execCtx, ctx) // returns (interface{}, interface{})
if a == nil || b == nil {
return
}
switch _, x, y := o.op, x.(type), y.(type) {
case ('+', complex64, complex64):
return x + y, nil
case ('+', float64, float64):
return x + y, nil
}
return invOp2(a, b)
} The downside of that would be that it's more complicated to specify, because you need some means of describing which terms are types vs. ordinary values. |
AFAICT, it stays the same 1:1. At least that's the intention. I am quite probably missing/overlooking something. Will try to dig deeper. |
Thinking aloud, I'll try to clarify a bit more my understanding of the 1:1 relation. For every method, single receiver or multiple receiver:
Given
all of If X or Y (and so on), but not Z in the inteface method specification is an interface type that corresponds to a receiver X' or Y' (and so on) of a multiple-receiver method, X' must implement X and Y' must implement Y, etc. In an earlier example, this is the case of the If a concrete type has all methods that match all method specifiers in the method set of an interface, the type implements that interface. Any method can match only a single method specifier within a particular interface. Is that what you mean by the 1:1 relation? Let me note that a value of type T can already implement any number of interfaces simultaneously, so I suppose that's not what you meant by '1:1 mapping between values and interfaces'. I hope that the above is correct, but I'm honestly far from being sure about it. |
Right, I was thinking more about function arguments. Suppose that I have a declaration like interface fooer {
foo(X, Y) Z
}
var F func(fooer) How do I call that on an |
I'm not sure I understand. Are you talking about some method Do you mean instead a type sequence like Please expand, thank you. |
Is it correct to assume that this proposal effectively allows one to move the arguments out of the parameter list into an implicit list created by the method receiver?
The top advantages of methods I can think of are:
Regarding the first advantage, is this even possible without a construction that captures the two pointers in an aggregate list for repeated method calls?
|
All of the Anyway, except for the first
Multiple-receiver method expressions and multiple-receiver method values are still an open question. So far the proposal forbids them, simply because I've not yet understood the solution(s) to that question enough - or at all. |
I think I meant to use a simpler interface. Suppose I have this code: type Fooer interface {
Foo() Z
}
func (X, Y) Foo() Z {…}
func DoFoo(f Fooer) {
f.Foo()
} In that case, how do I call |
Thanks for the clarification, now I understand the question. The answer is: you cannot because in the example there's no defintion of a method matching the interface method specification type Fooer interface{
Foo(Y) Z
}
func (X) Foo(Y) Z { ... (A) } // Matches Foo(Y) Z
func (X, Y) Foo(Z) { ... (B) } // Matches Foo(Y) Z
func bar(x X, y Y) {
x.Foo(y) // (A)
(x, y).Foo() // (B)
DoFoo(x)
}
func DoFoo(f Fooer) {
var x X
var y Y
f.Foo(y) // (A)
x.Foo(y) // (B)
(x, y).Foo() // (B)
} All of the above would be legal under this proposal + the matching rules as discussed. So far the only proposed way to call a method of a type sequence is the one seen above in Stepping out of the proposal:
I guess designing the above items will enable to call DoFoo with a pair X, Y. Only the last one seems somehow obvious: But those areas were left out intentionally and what's under the |
This is a significant increase in language complexity for a benefit that is much less clear, and is largely unstated above. There are languages that have multiple method receivers, but Go is not one of them. The restrictions on such methods make the language less regular. Declining. |
Just a random idea, probably not even original. Not being able to evaluate if it's of any value, it's presented here in the form of a Go2 proposal to hopefully get feedback from the language designers/experts.
Method declarations
Currently a method receiver is
Receiver = Parameters .
, further restricted byProposed is to change the above to
and add
Example declarations, all legal in any combination of their presence below.
Alternatively, field names not conflicting with a method name are permitted to be non unique, but then the field name cannot be used directly in the selector. Possible disambiguation via the type name containing the desired field. No syntax for this is proposed though. (TBD)
Alternatively, the last receiver of a methods with multiple receivers may be permitted to be variadic with the same semantics as of the last variadic parameter in function signature. (TBD)
Selectors
Amend the current specification such that it applies to methods with single receivers except that
Like in
(x, y).foo().
Note that the selector of a method with a single receiver may be parenthesized, both
x.foo()
and(x).foo()
are valid already.Method sets
A sequence of types is not a type. Methods with multiple receivers do not constitute a method set.
Method expressions
A method with multiple receivers cannot be used as a method expression. (TBD)
Method values
A method with multiple receivers cannot be used as a method value. (TBD)
Calls
The current mechanism of implicitly converting receiver x to &x where appropriate is extended to all receivers of a method.
Backward compatibility
Yes, full.
Previous proposal
#21659.
The text was updated successfully, but these errors were encountered: