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 interface types to match fields as well as methods #23796

Closed
ianlancetaylor opened this Issue Feb 12, 2018 · 20 comments

Comments

Projects
None yet
@ianlancetaylor
Contributor

ianlancetaylor commented Feb 12, 2018

I'm opening this issue as a place to record this idea, which has come up several times. I'm not personally in favor of this proposal. An example of an earlier discussion: https://groups.google.com/d/msg/golang-nuts/ZJ5DEv_36S8/opZ__-l6XxAJ .

The proposal: allow interface types to list fields, with types, as well as methods. An interface type that lists a field may only be implemented by a struct type that has a field with the same name and type.

For example:

type I interface {
        F int
}

type S struct {
    E, F, G int
}

// S has no methods.

var V1 I = S{} // ok
var V2 I = &S{} // ok

func GetI(i I) int {
        return i.F // ok
}

func SetI(i I, v int) {
        i.F = v // ok
}

Note that since the interface type could be implemented by struct types that put the field at different offsets, the seemingly simple expressions/statements like i.F and i.F = v would most likely be implemented by function calls.

The advantage of this proposal is that when several different struct types have fields with the same names and types, and interface could be used to access those fields directly, rather than requiring each type to define boilerplate getter/setter methods.

A disadvantage is that it changes the meaning of an interface type from one that purely describes behavior to one that also describes implementation. This is particularly clear in the fact that an interface that defines a field can only be implemented by a struct type.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented Feb 12, 2018

For getters, it would suffice to allow explicit conversion to a struct type with fewer fields, like

type Three struct { X, Y, Z int }
var three = Three{1, 2, 3}

type Two struct { X, Y int }
var two = Two(three) // so that two == Two{1, 2}
@griesemer

This comment has been minimized.

Contributor

griesemer commented Feb 12, 2018

For the record: This idea has been discussed in the very early days of Go (the first months). I am not a fan as it destroys the notion of an interface being an abstract type which doesn't imply a specific implementation.

This could be finessed by defining that an "interface field" is simply syntactic sugar for setter and getter methods. But at that point, the problem is not about interfaces anymore, but a convenient way to write setters and getters. I'd rather address that problem directly.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Feb 12, 2018

@jimmyfrasche Sounds like MOVE CORRESPONDING in COBOL (I have in fact written actual COBOL programs).

@pciet

This comment has been minimized.

Contributor

pciet commented Feb 12, 2018

I’d try to use this for additional type expressiveness by composition via embedding.

Without a real-world example I’m having a tough time seeing the tradeoffs. The thread mentions http.ResponseWriter’s Header method which seems like a clear win in code simplicity with this.

@davecheney

This comment has been minimized.

Contributor

davecheney commented Feb 12, 2018

Modelling an interface field as a getter and setter undermines the notion of interfaces describing abstract behaviour. Getter and setters are not behaviours, they are state.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Feb 12, 2018

@pciet Lack of a real-world example is a fair complaint. By the nature of the proposal, real-word examples are somewhat complex. They arise, for example, when using many different protocol buffer types in proto3. Often several different protocol buffer types will have the same field name with the same type, because they represent the same concept. However, there is no struct embedding going on, because the Go code used for protocol buffers is machine generated by the protocol buffer compiler. So now let's suppose you want to write a function that operates on several different protocol buffer types in the same way by accessing the field that exists in all of them. In Go that is naturally represented using an interface, but to do it here requires writing a new type for each protobuf type, that embeds that protobuf type, and defines getter/setter methods, where the getter/setter methods all look exactly the same. If we could could declare an interface type with a field, all of that boilerplate could be avoided.

So that is still pretty abstract but I hope it gives a flavor for the kind of case where this could be useful. Today people handle this kind of thing using the reflect package and reflect.Value.FieldByName.

@jimmyfrasche

This comment has been minimized.

Member

jimmyfrasche commented Feb 12, 2018

For the protocol buffer example maybe the generator should have an option to also generate getter/setter methods (or a separate program that bulk generates getter/setters for a struct so that it could be used with).

I've had similar issues but the only example I can think of is with the Name field on various types in go/ast. Either manually adding appropriate getters or a top-level func like NameOf(Node) *Ident that handles the type switches would work for me in that case.

In both cases, though, the problem involves a closed set of definitions so it seems like an API design issue rather than a language issue.

Are there any ad hoc examples when this is needed for structs from vastly different sources (packages, types generated by different means, etc.)?

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Feb 12, 2018

Are there any ad hoc examples when this is needed for structs from vastly different sources (packages, types generated by different means, etc.)?

Not that I know of.

@bontibon

This comment has been minimized.

Contributor

bontibon commented Feb 12, 2018

Just a thought: rather than changing the definition of an interface to include struct fields, structs could be special in that struct fields can satisfy a zero argument interface method with the same name and type. For example:

type I interface {
    F() int
}

type S struct {
    E, F, G int
}

func main() {
    var i I = S{}
    i.F() // returns a copy of the field S.F
}

I haven't thought through all of the implications of this change.

@josharian

This comment has been minimized.

Contributor

josharian commented Feb 12, 2018

Note that since the interface type could be implemented by struct types that put the field at different offsets, the seemingly simple expressions/statements like i.F and i.F = v would most likely be implemented by function calls.

Also &i.F. That further complicates the "they're just getters and setters" story; you need addr-ers as well. I ran into this in a very painful way when attempting to do experimental automated refactoring of the compiler that hid Node fields behind accessors.

@creker

This comment has been minimized.

creker commented Feb 12, 2018

Looks like a convoluted way of implementing properties without calling them properties.

@randall77

This comment has been minimized.

Contributor

randall77 commented Feb 12, 2018

We wouldn't need to compile to a function call. The itab could contain offsets in the underlying struct of all the fields declared in the interface.

x = i.F then compiles to:

   t := i.itab
   if t == nil { ...panic }
   off := t.fields[n] // where n = ordinal of field in interface declaration
   x = *(i.data + off)

This would also handle &i.F neatly, by just leaving off the last dereference.

I'm pretty on the fence about this proposal.

@urandom

This comment has been minimized.

urandom commented Feb 13, 2018

I'd like to point out that typescript supports this, and from what I've seen its used to allow passing literal objects to functions that accept such an interface.

I'm not particularly for the original proposal, however I think @bontibon has an interesting take on the idea, and allows any type to implement the interface, while making the implementation easier for structs.

I've personally had types in the past that needed to implement the following interface:

type Err interface {
     Err() error
}

and the associated structs would look a bit weird, like:

type Foo struct {
    err error
}

func (f Foo) Err() error {
    return f.err
}
@dlsniper

This comment has been minimized.

Contributor

dlsniper commented Feb 24, 2018

This proposal would lead to interfaces forcing structs as types to implement them, whereas today it's possible to have the implementation be a completely different type, like a func or a string, for example, see https://play.golang.org/p/irTsnlldaea as an example for this.

package main

import "fmt"

type (
	demo    func()
	hello   string
	Helloer interface {
		Hello()
	}
)

func (demo) Hello() {
	fmt.Println("Hello, playground")
}

func (h hello) Hello() {
	fmt.Println("Hello, " + h)
}

func call(h Helloer) {
	h.Hello()
}

func main() {
	d := demo(func() {})
	call(d)

	h := hello("world")
	call(h)
}

If the problem is accepting a restricted, common subset of fields, maybe a different venue to pursue this would be to allow passing of structs that get narrowed down to a subset of the fields. For example, maybe this would be a way to solve passing data around: https://play.golang.org/p/57BBaoUlMhu

package main

import "fmt"

type (
	BaseStruct struct {
		F2 string
	}

	Struct1 struct {
		F1, F2 string
	}

	Struct2 struct {
		F2, F3 string
	}
)

func call(b BaseStruct) {
	fmt.Printf("f2: %v\n", b.F2)
}

func main() {
	s1 := Struct1{
		F1: "f1",
		F2: "f2",
	}
	call(s1)

	s2 := Struct2{
		F2: "f2",
		F3: "f3",
	}
	call(s2)
}

Obviously, this does not solve the problem of calling methods on the type itself. I can't think of any better answer than: if data is needed, accept a common struct, if functionality is needed, accept an interface.

One could say: why not use embedding of BaseStruct in the Struct1 and Struct2. Obviously, embedding would be preferable but in this case please assume that Struct1 and Struct2 are coming from two different place which the author does not full control over.

Reading / writing into the fields of BaseStruct should be passthrough to the value that was used to call the function.

@bcmills

This comment has been minimized.

Member

bcmills commented Feb 26, 2018

This proposal would lead to interfaces forcing structs as types to implement them, whereas today it's possible to have the implementation be a completely different type, like a func or a string, for example,

That is already possible today: if a particular type is inspected using reflection, it may be required to be a pointer-to-struct or a channel or obey any number of other invariants. For example, proto.Message is required to be a pointer to a struct type with appropriate field tags. (That's not fundamentally different from the invariants of the standard encoding/json, encoding/gob, or text/template packages: they just happen to use interface{} instead of naming some more specific type.)

To enforce the level of behavioral abstraction you'd like today, you'd have to strike reflection from the language. Given that that's not at all likely to happen (much as I might wish for it), I don't see that it's all that harmful to codify a particularly common type constraint in the interface definition.

That is: we already have those sorts of constraints, and to me it seems strictly positive to promote at the least the common ones from ad-hoc comments into the language proper.

@ianlancetaylor

This comment has been minimized.

Contributor

ianlancetaylor commented Apr 24, 2018

We haven't seen any strong arguments in favor of this proposal. Retracting it.

@ghasemloo

This comment has been minimized.

ghasemloo commented May 27, 2018

Just in case this gets reopened:

Do we consider fields of a struct as part of their behavior or not? If yes, then I think interfaces should be able to capture that.

This comes up often when I have multiple types that I pass to a function. They are supposed to have a particular field, but I cannot express that through the interface that the function accepts. The two workarounds are:

  1. use reflection
    which drops the benefits of type checking at compile time.

  2. add getter/setter methods to the types
    which doesn't work since typically in go we do not have getter/setter methods for fields, they are expected to be accessed directly.
    If I own the type then I can add a getter/setter methods, but it diverges from common go practice. If I don't own the type, then situation is worse, I have to define a new type alias for each of the types I am passing to the function and then add new methods to them.

I think allowing to express requirement for existence of fields in interfaces would have resolved this issue cleanly.

@comaldave

This comment has been minimized.

comaldave commented Jun 4, 2018

Dave Cheney Modelling an interface field as a getter and setter undermines the notion of interfaces describing abstract behaviour. Getter and setters are not behaviours, they are state.

My apologies. There are times when I wish to define implementation. On those rare occasions I use Setters and Getters in my interface. I am not a purist, I do what is convenient and readable. I do not like exposing properties without a setter if I want to validate the data. And setters and getters in the interface means that the underlying implementation can be completely different for unit testing.
I am philosophically opposed to this suggestion but if adopted, it will be something I need not use.

@davecheney

This comment has been minimized.

Contributor

davecheney commented Jun 5, 2018

@comaldave

This comment has been minimized.

comaldave commented Jun 5, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment