-
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: language: compute interface method sets based on values instead of types #47429
Comments
All method sets are fully known at compile time. This is a feature. A type switch or type assertion permits the program to ask whether the dynamic type of an interface value is some type or whether it implements some interface. When you use a type switch to ask whether The field I think you are suggesting that when a struct embeds an interface type the methods of the dynamic type of the interface value should become methods of the struct. However, those methods will not be available at compile time. They clearly can't be, as the compiler can't possibly know in general the dynamic type of But that's not how Go works. Go doesn't construct method sets dynamically. And I don't see why it would be a good idea to construct method sets dynamically at run time. |
Perhaps worth noting that, if you really want to create named types with arbitrary methods at run-time, that should be possible at some point: #16522 But I fully agree with Ian that a type known at compile-time having a fixed set of methods is a feature. A lot of tooling and code is built on that principle. |
https://play.golang.org/p/dgzYDBbYiHM This is not a proposal but rather I question I feel, but nevertheless, I will try to answer it. In your case m, and Machine do not implement gener because it is only an /incident/ that the Standard member happens to be filled in with something that also implements gener. I we were to promote the method set of the real Standard member to the top level Machine, then machine could both implement and not implement the gener interface, depending on the value contained, which is contradictory. This is also important to maintain type safety. While there are some other programming languages that allow individual instances to be customized with additional methods, this is not a good idea, because it makes it difficult to reason about what you can do with a specific value, and type safety is lost. Example: package main
import (
"fmt"
)
// Std is a type that implements Standard, as well as `gener`.
type Std struct{}
func (Std) isMachine() {}
func (Std) Add(a, b int) int { return a + b }
func (Std) Sub(a, b int) int { return a - b }
func (Std) gen() int { return 4 } // guaranteed to be random
// StdNogen is a type that implements Standard but not gener.
type StdNogen struct{}
func (StdNogen) isMachine() {}
func (StdNogen) Add(a, b int) int { return a + b }
func (StdNogen) Sub(a, b int) int { return a - b }
type Machine interface {
isMachine()
}
type Adder interface {
Add(a, b int) int
}
type Suber interface {
Sub(a, b int) int
}
type Standard interface {
Machine
Adder
Suber
}
type gener interface {
gen() int
}
type Foo struct {
Standard
}
func NewFoo(m Standard) *Foo {
if m == nil {
m = Std{}
}
return &Foo{m}
}
func main() {
var m Machine = NewFoo(nil)
var n Machine = NewFoo(StdNogen{})
// m is of type Foo, which implements the same methods as Standard does.
// That is why it is not a gener, even at runtime, Standard only implements
// the methods of Machine, Adder and Suber.
switch m.(type) {
case *Foo:
fmt.Println("m is *Foo")
}
switch m.(type) {
case gener:
fmt.Println("m implements gener")
}
switch m.(type) {
case Standard:
fmt.Println("m implements Standard")
}
// n is also of type Foo, even though Standard has been filled in differently.
// It only implements Standard and not gener also.
switch n.(type) {
case *Foo:
fmt.Println("n is *Foo")
}
switch n.(type) {
case gener:
fmt.Println("n implements gener")
}
switch n.(type) {
case Standard:
fmt.Println("n implements Standard")
}
// In this case, at run time, incidentally, Standard is Std, for m,
// which does implement gener as well as the Std methods.
switch m.(*Foo).Standard.(type) {
case gener:
fmt.Println("m is *Foo, whose `Standard` implements `gener`")
}
switch m.(*Foo).Standard.(type) {
case Standard:
fmt.Println("m is *Foo, whose `Standard` is well, `Standard`")
}
switch m.(*Foo).Standard.(type) {
case Std:
fmt.Println("m is *Foo, whose `Standard` is `Std`")
}
// In this case, incidentally, Standard is StdNogen, for n,
// which does not implement gener.
switch n.(*Foo).Standard.(type) {
case gener:
fmt.Println("n is *Foo, whose `Standard` implements `gener`")
}
switch n.(*Foo).Standard.(type) {
case Standard:
fmt.Println("n is *Foo, whose `Standard` is well, `Standard`")
}
switch n.(*Foo).Standard.(type) {
case StdNogen:
fmt.Println("n is *Foo, whose `Standard` is `StdNogen`")
}
} |
Based on the discussion above this is a likely decline. Leaving open for four weeks for final comments. |
No further comments. |
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
Yes
What did you do?
Situation: I'm teaching someone Go, and we came up with a weird corner case that isn't quite covered in the language specification.
Consider the following code:
Now consider the following types:
Syntactically, we can see that
Foo
and*Foo
implementsStandard
. This is becauseStandard
is embedded inFoo
. All good.Now consider this:
m
has*Foo
as an underlying type, which implementsStandard
by means of the embeddedStandard
in the struct field. The embeddedStandard
has aStd
as its underlying type. The methodset ofm
at compile time is that of aStandard
:isMachine()
Add(a, b int) int
Sub(a, b int) int
So far so clear. What is not clear (at least in the langspec) is the following runtime behaviour:
Here, you'll see it print
"m implements Standard"
.However, if you do this:
Here, you'll see it print
"m is *Foo, whose ``Standard`` implements ``gener``"
.This all rather makes sense from a syntactic/compilation point of view. We only need to generate methodsets for a given type, and then it's a quick lookup when doing type switches at run time.
The Question
The question raised by my student, which I feel makes sense is this:
The question can be asked alternatively as:
The Changes Proposed
A proposed change is how the methodset of a value is computed at runtime. Currently, the methodset is computed at runtime based on the surface types.
Foo
as a type embedsStandard
. But the actual value at runtime embeds aStd
. So we should compute the methodset based on value. This can be done based on heuristics - For things that are known and analyzable at compile time, we can generate the methodsets AOT (or for large interfaces, generate all possible combinations); and a dynamic lookup to be cached somewhere in the runtime system for things that are dynamic.This change makes sense, as programmers can now do type assertions based on the behaviours of the concrete types that underlie the interfaces, as follows:
Why?
There is an argument to be made that the point of interfaces and type switches are that it allows adhoc, dynamic (i.e. at run time) calling of methods. The compile-time strictness can be left to things like contracts or type classes.
Furthermore it's actually more consistent in behaviour -
runtime.I2I
should work for all interfaces.The text was updated successfully, but these errors were encountered: