-
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: Have functions auto-implement interfaces with only a single method of that same signature #21670
Comments
I have wanted this many times. I often find myself creating custom type ReaderFunc func([]byte) (int, error)
func (f Readerfunc) Read(b []byte) (int, error) { return f(b) } |
So a |
How many single-method interfaces are there with no inputs and outputs? |
I don't think the concern of interfaces being accidentally implemented due to unspecific signatures is constrained to just this, that concern could just as well be applied to "all structs with a method with signature |
This has come up before and various people have proposed more or less the same thing. See for instance: https://groups.google.com/forum/#!searchin/golang-nuts/single-method$20interface%7B%7D/golang-nuts/AAVCVpWqGCo/cWtHROB2K_8J I wished for this a few years back for "completeness" sake, when we introduced method values: We can go from a method to a function; so why shouldn't we be able to go from a function to a method (very loosely speaking). |
It would be easy to go nuts allocating wrappers if something got passed back and forth between func and interface, as demonstrated in this very useful program below:
|
To me it would make more sense to be able to create an interface implementation out of one or more functions like so: type FooBarer interface {
Foo() string
Bar() string
}
func main() {
foobar := FooBarer{
Foo: func() { return "foo" },
Bar: func() { return "bar" },
}
handler := http.Handler{myHandlerFunction}
} Then you can truly go full circle: func fullCircle(fb FooBarer) FooBarer {
foo := fb.Foo
bar := fb.Bar
return FooBarer{
Foo: foo,
Bar: bar,
}
} I'm not sure I would actually suggest such a change... but it's the one that came to mind when I saw the mention of "completeness", and I think it would happen to satisfy the original proposal's need as well. |
@dsnet: A single non-exported Next up, |
Isn't that why @ccbrown's proposal avoids that problem? w := io.Writer{Write: f} // Gives an explicit name to f Note, that you can also lose the name when you extract a function from an interface (we can do this today): f := w.Write // f is just a function, no particular "name" to it |
@dsnet I wasn't responding to ccbrown, I was responding to the original proposal and to your question. Method values are just functions, there's no confusion there; the confusing only arises from accidentally implementing an interface. |
@ccbrown This idea (#21670 (comment)) has also been proposed in the past by @Sajmani, albeit only Go Team internally, I believe. |
There's something definitely appealing about the symmetry of @ccbrown's construction. You can kind of do something similar now using
It's a very useful construction for satisfying interfaces in terms of closures and the equivalent of the The major differences from @ccbrown's syntax are that the type name is different and the field names cannot correspond to the method names. However, it does provide a greater flexibility. Namely, you can define methods in addition to the func fields. In particular, you can define methods that operate on data:
Not pictured above, as it doesn't really apply, but you could also add sensible defaults to a method if the corresponding func field is nil. Having an interface consider a field value that's a func with the correct name and signature to be the same as a method would reduce
That saves some boilerplate. Having the field names correspond to the method names makes it read a little better. Though you do lose the ability to provide a sensible default implementation if one of the pseudo-methods is nil, but you could still do that by using a field name that doesn't match the interface. I'm not sure that buys enough to be worth it, though. It's slightly more verbose at the definition and call sites but for the sake of completion you can also do this:
That let's you have the field names be the method names which is clearer, but there's the extra step to build the implementation. It would actually look pretty good if you could elide the struct name when calling
|
I had proposed the idea of "interface literals", where an interface value
can be specified using struct literal syntax and the interface type name.
The methods are specified using function values:
s := sort.Interface{
Len: func() int {...},
Swap: func(i, j int) {...},
Less: func(i, j int) bool {...},
}
This would work automatically for any interface type.
We decided the convenience of this syntax did not outweigh the added
implementation complexity, in particular
implications for stack traces and other runtime support.
By comparison, allowing function values to satisfy single method interfaces
is a much simpler change.
S
…On Tue, Aug 29, 2017 at 11:43 AM jimmyfrasche ***@***.***> wrote:
There's something definitely appealing about the symmetry of @ccbrown
<https://github.com/ccbrown>'s construction.
You can kind of do something similar now using
type FooBarer interface {
Foo() string
Bar() string
}
type FooBar struct {
DoFoo func() string
DoBar func() string
}
func (f FooBar) Foo() string { return f.DoFoo() }
func (f FooBar) Bar() string { return f.DoBar() }
It's a very useful construction for satisfying interfaces in terms of
closures and the equivalent of the http.HandleFunc pattern extended to
multi-method interfaces.
The major differences from @ccbrown <https://github.com/ccbrown>'s syntax
are that the type name is different and the field names cannot correspond
to the method names.
However, it does provide a greater flexibility. Namely, you can define
methods in addition to the func fields. In particular, you can define
methods that operate on data:
//example borrowed from the thread that introduced sort.Slice
type Sorter struct {
Length int
Swapper func(i, j int)
Lesser func(i, j int) bool
}
func (s Sorter) Len() int { return s.Length }
func (s Sorter) Swap(i, j int) { s.Swapper(i, j) }
func (s Sorter) Less(i, j int) bool { return s.Lesser(i, j) }
Not pictured above, as it doesn't really apply, but you could also add
sensible defaults to a method if the corresponding func field is nil.
Having an interface consider a field value that's a func with the correct
name and signature to be the same as a method would reduce Sorter to
type Sorter struct {
Length int
Swap func(i, j int)
Less func(i, j int) bool
}
func (s Sorter) Len() int { return s.Length }
That saves some boilerplate. Having the field names correspond to the
method names makes it read a little better. Though you do lose the ability
to provide a sensible default implementation if one of the pseudo-methods
is nil, but you could still do that by using a field name that doesn't
match the interface.
I'm not sure that buys enough to be worth it, though.
It's slightly more verbose at the definition and call sites but for the
sake of completion you can also do this:
type Sorter struct { //data struct with the names we want
Len int
Swap func(i, j int)
Less func(i, j int) bool
}
type sorter struct { //actual implementation of the interface
len int
swap func(i, j int)
less func(i, j int)
}
func (s sorter) Len() int { return s.len }
func (s sorter) Swap(i, j int) { s.swap(i, j) }
func (s sorter) Less(i, j int) bool { return s.less(i, j) }
func NewSorter(s Sorter) sort.Interface { //translator
//could also assign default implementations for nil Swap/Less,
//if it were applicable
return sorter{
len: s.Len,
swap: s.Swap,
less: s.Less,
}
}
That let's you have the field names be the method names which is clearer,
but there's the extra step to build the implementation.
It would actually look pretty good if you could elide the struct name when
calling NewSorter, though:
return packagename.NewSorter({
Len: n,
Swap: func(i, j int) {
// ...
},
Less: func(i, j bool) {
// ...
},
})
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#21670 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AJSK3VLx3dek5ZEx-j6PHE7SnYZVj_waks5sdDGmgaJpZM4PE-TN>
.
|
Possibly yes. I haven't considered those issues in the context of my
proposal.
…On Tue, Aug 29, 2017 at 2:03 PM jimmyfrasche ***@***.***> wrote:
@Sajmani <https://github.com/sajmani> wouldn't the same implementation
complexity be required for #16522
<#16522> / #4146
<#4146> ? #16522
<#16522> is blocking useful features
(always discussed in #4146 <#4146> but
in the end often turning out to be better implemented via #16522
<#16522> )
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#21670 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AJSK3bh6BM-VdqHN2Zhbjjo9C25zI1IPks5sdFJGgaJpZM4PE-TN>
.
|
Although it goes against Go conventions, I would question, if an interface is needed in the first place, if it contains just a single method. Simply accepting a callback works for both cases, is less code to write and more flexible: func handler1(http.ResponsWriter, *http.Request) {}
type handler2 struct {}
func (h *handler2) HandleA(http.ResponsWriter, *http.Request) {}
func (h *handler2) HandleB(http.ResponsWriter, *http.Request) {}
func WantsHandler( handler func(http.ResponsWriter, *http.Request) {}
func main() {
WantsHandler(handler1) // works
h2 := handler2{}
WantsHandler(h2.HandleA) // also works
WantsHandler(h2.HandleB) // this too
} Another benefit: The user is not forced to include a package, if all she needs is the reference to the interface name. Agreed, it looks a bit ugly... ;-) |
@metakeule that's a good point. Method values and type aliases† overlap the use-case for single method interfaces somewhat, but what you lose with that approach is the ability to feature test other methods with type asserts (like being able to ask, is this Being able to more easily go from the one to the other would be useful, but I'd rather have the more explicit and general "interface literal" syntax. But it's not really that great a pain to make the explicit wrappers with a func/struct now. † so you can write
instead of repeating the signature/docs everywhere or having to worry about casts from defined types |
Regarding the "feature" to ask for other interface implementations (aka "is this io.Reader also an io.ReaderFrom"): I think the problem this is supposed to solve is "optional features" of the receiving function. It is used in the standard library (e.g. http.ResponseWriter -> http.Flusher etc) and I also used it. But for some time now I consider this a misfeature for the following reasons:
|
I'm not keen on this proposal. I think it will be confusing.
What happens when we reflect on the value of r? What should reflect.ValueOf(r).NumMethod() return?
Both cases would be unexpected IMHO, because we expect the value inside an interface to be the value we started with. It would be unexpected to inspect the methods on an io.Reader value and find that Read isn't among them; conversely it would be unexpected for a anonymous function value to have a method (what would the method name be?). Another possibility might be to actually change the underlying type when converting to a one-method interface. Then reflect.TypeOf(f) != reflect.TypeOf(io.Reader(f)) (the latter type would have a Read method), but that seems pretty unexpected too, because interface{}(io.Reader(r)) is currently the same as interface{}(r), and this would make it different, confusingly so in my view. @Sajmani's suggestion of io.Reader{Read: f} (io.Reader{f} for short?) seems like it would work better to me - at least there's a clear constructor there. But on balance, I think that it's probably not worth it. |
I think if we were to allow function values to satisfy interfaces, I'd
prefer an explicit conversion: io.Reader(f). This provides two benefits:
(1) it helps the user avoid errors, E.g., reversing the args to io.Copy;
(2) it requires the caller be able to name the interface, avoiding
confusion when the interface name is unexported and the caller is in
another package.
…On Wed, Aug 30, 2017 at 5:04 PM Roger Peppe ***@***.***> wrote:
I'm not keen on this proposal. I think it will be confusing.
var f = func([]byte) (int, error) {return 0, io.EOF}
var r io.Reader = f
What happens when we reflect on the value of r? What should
reflect.ValueOf(r).NumMethod() return?
I see two possibilities:
- it could return 0, because the underlying value has no methods.
- it could return 1, because any value satisfying io.Reader must have
at least one method.
Both cases would be unexpected IMHO, because we expect the value inside an
interface to be the value we started with. It would be unexpected to
inspect the methods on an io.Reader value and find that Read isn't among
them; conversely it would be unexpected for a anonymous function value to
have a method (what would the method name be?).
Another possibility might be to actually change the underlying type when
converting to a one-method interface. Then reflect.TypeOf(f) !=
reflect.TypeOf(io.Reader(f)) (the latter type would have a Read method),
but that seems pretty unexpected too, because interface{}(io.Reader(r)) is
currently the same as interface{}(r), and this would make it different,
confusingly so in my view.
@Sajmani <https://github.com/sajmani>'s suggestion of io.Reader{Read: f}
(io.Reader{f} for short?) seems like it would work better to me - at least
there's a clear constructor there. But on balance, I think that it's
probably not worth it.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#21670 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AJSK3TNAUrligtYfJ2_7rvEHGLD-WGsYks5sdc54gaJpZM4PE-TN>
.
|
I'm not sure that this is much better. Explicit conversion to other interface
I believe that as proposed, the above code would need to panic, but that I wouldn't mind interface{F()}{f} as a syntax (by analogy with SliceType{x} vs SliceType(x)), We'd still need to decide what the underlying type's Kind and Name etc would look like. |
I believe I have an interesting use case for interface literals as proposed by @ccbrown and @Sajmani . I've been considering an package ctxio
type Reader interface {
Read(ctx context.Context, p []byte) (n int, err error)
}
// ReaderWithContext curries a ctxio.Reader with a Context, producing an io.Reader.
func ReaderWithContext(ctx context.Context, r Reader) io.Reader { return ctxReader{ctx, r} }
type ctxReader struct { ctx context.Context, r Reader }
func (r ctxReader) Read(p []byte) (n int, err error) { return r.Read(r.ctx, p) } But there's a problem with the
What if we could instead write this? type io interface {
Read(p []byte) (n int, err error)
ReadAt(p []byte, off int64) (n int, err error)
WriteTo(w io.Writer) (n int64, err error)
}
func ReaderWithContext(ctx context.Context, r Reader) io.Reader {
// ctxReader has Read and WriteTo methods.
ctxr := ctxReader{ctx, r}
var writeToFn func(w io.Writer) (n int64, err error)
if rr, ok := r.(io.WriterTo); ok {
writeToFn = ctxr.WriteTo
}
wrap := io{
Read: ctxr.Read,
WriteTo: writeToFn,
}
return wrap
} If If This handling of nil methods is quite subtle, enough so that I'm extremely hesitant to suggest that it's something that we actually should do even if we do add interface literals. On the other hand, I haven't been able to come up with any good solutions to the problem of wrapping types which may or may not contain certain methods that aren't equally subtle. |
What if you were required to declare all methods of an interface literal? Sure someone could do: wrap := io{
Read: ctxr.Read,
ReadAt: nil,
WriteTo: writeToFn,
} But the jokes on them... they explicitly trying to shoot themselves in the foot. |
The way I was looking at it was that: x = interfaceType {m1:f1, m2:f2, ...} would be syntax sugar for:
I think it would be very odd if a non-nil something of a given interface type turned out not to actually implement that type. That is, for all non-nil variables v of some interface type I, we should be able to do (interface{}(v)).(I) without panicking. Invariants like this are important for automatic code transformation and vetting tools. That said, I do appreciate the issue encountered by @neild. I suspect the answer to that is more of a cultural one - define your interfaces in such a way that even if the interface is implemented, the it might not actually work. For example WriteTo could be defined such that it could return a "not implemented" error to cause io.Copy to fall back to the naive Read-based behaviour. Then we can define an interface type with all the known methods on it and implement suitable behaviour when there's no function available. Embedding unknown methods is another story. |
I was not suggesting anyone in this thread is confused, since none of us are presumably beginners. :) But it's a question that you find often out there on the internet and stack overflow, so from there I consider this a likely point of difficulty for Go learners. I like your alternate proposal better, because it does require an explicit conversion. But as you say this requires the compiler to automatically generate hidden types, which I feel is a disadvantage (less explicit), and as you notice in your point 2, it makes type assertions and type switches on that value more complex. With go generate and a certain simple generator tool I use at work, I can already explicitly generate such single method interfaces very easily, with the advantage that this way is are explicit and that reflection keeps on working as expected. So that's why I am not convinced that the costs of this proposal outweigh the benefits. |
What's more complex? Everything works exactly the same as it works right now. It's functionally equivalent to having an unexported version of And FWIW I don't think anyone actually uses type-assertions or type-switches to types like this. I can't even imagine a use for that. |
Fair enough, type assertions and switches may not be the decisive factor here. But what do you think about explicitly generating code versus the implicit generation that the compiler has to do? In the past, when a feature of Go was proposed that could be easily solved with code generation or other tools, the latter was often preferred, and I agree with that. |
It assumes that my problem is typing out the type and method. But that's not really it. I don't want the code to be there. It serves no purpose and makes code seem more complicated than it is. In fact, code generation is strictly worse here - because it's easier to write the code than to remember that such a tool exists, how to call it and use its output.
Obviously it's still a trade-off. In the past, these things where either eventually added (e.g. generics, file embedding) or rejected for other purposes (e.g. compile-time metaprogramming) or added complexity not commensurate with its benefits. I don't think this particular proposal adds a lot of complexity though. On the language side, we don't need any new syntax and the spec-change is limited to adding a two sentence bullet point. The implementation side I'm less qualified to judge, but ISTM that a simple implementation would just generate a type-descriptor, putting the function pointer itself in the method table. These types wouldn't be comparable or anything, so we wouldn't need to generate any actual code. So the complexity seems quite low to me. To be clear, this isn't a dramatic issue. I genuinely don't think it will change Go as a language significantly - one way or another. It's just a little convenience which seems to have very few downsides, if any. |
OK, thanks for explaining that. As I said, I do like your proposal the best, since like that this becomes nothing more than a bit of syntactic sugar. What I'm complaining about is that with too many of these little sugars, Go might become too sweet at least for my taste. But this is more about my personal preference now then anything else so I'll leave it at that. |
@Merovius Since starting this thread I've remained pretty hands off, as it was quickly filled with people clearly much smarter than me on these issues :) My original proposal, as it's currently stated, seems to have been quickly dismissed in favor of other similar, better thought out approaches. I'm not sure how best to guide this issue at this point, should I just close it and let the various different approaches each create a proposal if they haven't already, or try to hash it out in this issue and update the top-level issue description with my own preferred version of the proposal? Fwiw I do like your proposal better than interface literals. While interface literals would be more broadly useful, I don't think they'd help with my original problem (http.HandleFunc is confusing), as that would turn out looking something like:
so hardly even worth bothering. I do like interface literals generally, but in the context of the original problem I was hoping to solve I don't think they do much. Also wow, this thing has been going for almost four years and is still getting activity, that's wild, thank you everyone for all the thought put into this whole thread 🙏 |
I think we can keep this issue open and update the proposal at the top if you like so. Otherwise we loose the dicussion. |
FWIW I think this will start mattering as soon as this proposal makes it into a proposal review meeting, but not before. At that point, we need to know what specific proposal we are talking about and historically, that's the point where rivaling proposals are often filed, if there is no consensus of what the best way forward is. Until then, I think it's fine to leave this as a "grab-bag proposal", funneling all sufficiently similar ideas here. Personally, I think the "conversions to single-method interfaces" (I'm hesitant to call it "my" proposal - I wasn't the first person in this thread to come up with it) design is the least controversial, most likely to get accepted one. But that's just, like, my opinion. [edit] Of course, if we agree on a specific design, that might also make it more likely to be picked up in the proposal review, as less controversy means higher chance of quick convergence… :) [/edit] |
I think any proposals that are not the original proposal need their own issue, as the original proposal has been lost in the variety of alternative proposals. @ianlancetaylor requested this earlier. The many comments containing different proposals make it challenging to join this conversation or to move it forward as it's not entirely clear what comments about problems relate to each proposal. It'd be helpful if we could discuss each proposal in isolation. I'm joining this conversation after having proposed #47480, the same proposal as the original proposal. |
FWIW the boundary between "refinement of the original proposal" and "different proposal" is fluent. Personally, I regarded the suggestion to use convertibility instead of assignability a refinement to address the most common concern, not a genuinely diferrent design. Which is why I suggested that @mediocregopher might want to incorporate it. Apparently it's considered too different, so I filed #47487 to reflect that. |
Reading back through the comments on this issue the common comments regarding the original proposal are:
There was one concrete problem that was pointed out by @rogpeppe (#21670 (comment)):
If we say that the function is lifted into an anonymous type then it seems like this would be resolved and the Are there other concrete problems, or things that would break, with the original proposal? |
@leighmcculloch Another concrete problem is what's mentioned here (and AFAIR in several other places upthread): Lack of type-safety, if a function automatically implements multiple, semantically different interfaces (e.g. Personally, I also don't like the idea of |
Interesting. I would think the original proposal doesn't reduce type safety for the following reasons:
|
I don't see how this addresses the argument "you might accidentally assign a function meant to implement
True. The difference is that methods have a name, which provides at least one layer of protection. Yes, it is possible to define |
Most type checks in Go are nominal. That is, Go checks whether the name of the type is the one expected. For example, in the case of, An interface in Go is type safe in two ways: structurally and nominally. To implement an interface we must give a type methods that have the correct name, as well as the correct signature. This double check makes it harder to accidentally use the wrong implementation. With the proposal as it is on top of this thread, we loose the nominal check and only keep the structural one. This is clearly insufficient as it would allow mistakes to be made more easily. So I remain fully opposed to the top proposal as it would remove a very important quality of Go from interfaces, namely that of nominal type checking. |
@beoran, there is no problem passing a string to a function that has a Name argument. |
@empire In this case
You can't pass a value of type |
https://play.golang.org/p/2T_Gc0kixYZ
That is because that example passes an untyped const. If you pass a typed
string it gets type checked correctly. As it should!
Op do 19 aug. 2021 09:44 schreef Hossein Zolfi ***@***.***>:
… Most type checks in Go are nominal. That is, Go checks whether the name of
the type is the one expected. For example, in the case of, type Name
string, one cannot pass a string to a function that has a Name argument.
@beoran <https://github.com/beoran>, there is no problem passing
<https://play.golang.org/p/NSFyMu--AbA> a string to a function that has a
Name argument.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#21670 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAARM6KESWQMITFGOQSC6IDT5SY45ANCNFSM4DYT4TGQ>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&utm_campaign=notification-email>
.
|
I'm going to close this issue in favor of the refinement in #47487, which keeps the best elements of this proposal while avoiding some of the drawbacks. Please comment if you disagree. |
For example, instead of having to have http.HandlerFunc, a function with the signature
func(http.ResponsWriter, *http.Request)
would automatically implement http.Handler.Where I work we have at least 4 internal packages which use some form of interface layering/middleware, similar to
http.Handler
and it's middlewares like http.StripPrefix and http.TimeoutHandler. It's a very powerful pattern which I don't think needs justification (I could give one, but it's not really what this proposal is about).Having now written a few versions of what's essentially the exact same type I have a few different reasons I think this change is warranted:
It reduces confusion for programmers new to go. Anecdotally, pretty much every new go developer I've worked with has tried to pass a function like this and been confused as to why they can't. While I understand that this is a fairly small blip on the go learning-curve, I do think this is an indication that this function-as-interface behavior fits better with devs' mental model of how interfaces work.
It would allow for removing some public-facing types/functions from packages like http (if such a thing is on the table, at the very least it allows for reducing api surface area in new and internal packages). By the same token it reduces code-clutter in cases where something like
HandlerFunc
isn't available and you have to wrap your function in theWhateverFunc
type inline.It would be almost completely backwards compatible. There's no new syntax, and all existing code using types like
http.HandlerFunc
would continue to work as they are. The only exception I can think of is if someone is doing something with aswitch v.(type) {...}
, wherev
might be a function, and the dev expects that the function won't implement the interface as part of that switch behaving correctly (or the equivalent with if-statements or whatever).These reasons don't justify a language change individually, but in sum I think it becomes worth talking about. Sorry if this has been proposed already, I searched for it but wasn't able to find anything related.
The text was updated successfully, but these errors were encountered: