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: Go 2: Have functions auto-implement interfaces with only a single method of that same signature #21670

Closed
mediocregopher opened this issue Aug 28, 2017 · 70 comments
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. Proposal v2 An incompatible library change
Milestone

Comments

@mediocregopher
Copy link

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 the WhateverFunc 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 a switch v.(type) {...}, where v 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.

@gopherbot gopherbot added this to the Proposal milestone Aug 28, 2017
@dsnet dsnet added LanguageChange Suggested changes to the Go language v2 An incompatible library change labels Aug 28, 2017
@dsnet
Copy link
Member

dsnet commented Aug 28, 2017

I have wanted this many times. I often find myself creating custom io.Reader and io.Writer in unit tests via closured functions. I often have to have this separate type defined so I can convert the function to satisfy the interface:

type ReaderFunc func([]byte) (int, error)
func (f Readerfunc) Read(b []byte) (int, error) { return f(b) }

@tv42
Copy link

tv42 commented Aug 28, 2017

So a func foo() implements all of the single-method no arguments interfaces? I don't like the sound of that. The method name matters.

@dsnet
Copy link
Member

dsnet commented Aug 28, 2017

How many single-method interfaces are there with no inputs and outputs?

@mediocregopher
Copy link
Author

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 func()", which is something which already exists and is fairly common.

@griesemer
Copy link
Contributor

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).

@jimmyfrasche
Copy link
Member

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:

package main

type Fooer interface { foo() }

func foo() {}

func R(fooer Fooer) {
  R(fooer.foo) // interface to method value then func to interface
}

func main() {
  R(foo)
}

@ccbrown
Copy link

ccbrown commented Aug 29, 2017

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).

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
Copy link
Member

dsnet commented Aug 29, 2017

@ccbrown, your idea also makes it explicit which method the function applies to, alleviating the concern that @tv42 had.

@jimmyfrasche
Copy link
Member

@ccbrown interestingly that idea appears to have been considered #4146 except that issue was for adding it to reflect only. But if it's good enough for reflect . . .

@tv42
Copy link

tv42 commented Aug 29, 2017

@dsnet: A single non-exported func() method is a common way to mark interfaces as impossible for outsiders to implement.

Next up, func([]byte), or func([]byte) error. That means nothing without the method name!

@dsnet
Copy link
Member

dsnet commented Aug 29, 2017

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

@tv42
Copy link

tv42 commented Aug 29, 2017

@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.

@griesemer
Copy link
Contributor

@ccbrown This idea (#21670 (comment)) has also been proposed in the past by @Sajmani, albeit only Go Team internally, I believe.

@jimmyfrasche
Copy link
Member

There's something definitely appealing about the symmetry of @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'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) {
    // ...
  },
})

@Sajmani
Copy link
Contributor

Sajmani commented Aug 29, 2017 via email

@jimmyfrasche
Copy link
Member

@Sajmani wouldn't the same implementation complexity be required for #16522 / #4146 ? #16522 is blocking useful features (always discussed in #4146 but in the end often turning out to be better implemented via #16522 )

@Sajmani
Copy link
Contributor

Sajmani commented Aug 29, 2017 via email

@metakeule
Copy link

metakeule commented Aug 30, 2017

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... ;-)

@jimmyfrasche
Copy link
Member

@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 io.Reader also an io.ReaderFrom?).

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

//Handler is [documented here]
type Handler = func(ResponseWriter, *Request)

instead of repeating the signature/docs everywhere or having to worry about casts from defined types

@metakeule
Copy link

metakeule commented Aug 30, 2017

@jimmyfrasche

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:

  1. It is a hidden feature. While the function signature says it expects an io.Writer it is really expecting an io.Writer and/or an io.WriteCloser. That behavior is only visible within the source code and maybe the documentation. However it the author decides to add some optional behavior in an update, it may break the user without a compiler error (since the signature does not change) and creates errors that are hard to debug (e.g. io.WriteCloser being closed too early). Also the user struct implementing a new interface might break suddenly (the authors update being done long before without issues) and would make new type variations neccessary in order to prevent triggering the unwanted option.

  2. It makes it hard to wrap the interface. E.g. it looks as it would be easy to wrap an http.ResponseWriter with another type that implements the same interface. Until you notice that the http server is checking for other interfaces as well (like http.Flusher etc). Now you have to wrap each of the other interfaces (that are not easily visible unless you read the full source code).

  3. There are better ways to offer optional features that don't have the disadvantages: Allow option functions to be passed to the constructor that set the appropriate not exported options in the struct. Having them as variadic argument allows for easy expansion (simply add a new top level option function overwriting a compatible defaults) without breaking anything and with clear visibility. It also decouples the options on the user side (makes the user code more robust for refactoring / smaller surface of coupling between user and provider).

@rogpeppe
Copy link
Contributor

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'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.

@Sajmani
Copy link
Contributor

Sajmani commented Aug 31, 2017 via email

@rogpeppe
Copy link
Contributor

I think if we were to allow function values to satisfy interfaces, I'd
prefer an explicit conversion: io.Reader(f).

I'm not sure that this is much better. Explicit conversion to other interface
types is still possible, and has quite different semantics.

f := func() {}
x := interface{}(f)
y := interface{F()}(f)
if reflect.TypeOf(x) != reflect.TypeOf(y) {
    panic("unexpected")
}

I believe that as proposed, the above code would need to panic, but that
seems wrong to me.

I wouldn't mind interface{F()}{f} as a syntax (by analogy with SliceType{x} vs SliceType(x)),
especially as it could later be expanded to interface{F(); G()}{F: f, G: g} at some later
point to make it easier to implement multiple method interfaces too.

We'd still need to decide what the underlying type's Kind and Name etc would look like.

@neild
Copy link
Contributor

neild commented Sep 13, 2017

I believe I have an interesting use case for interface literals as proposed by @ccbrown and @Sajmani .

I've been considering an ioctx package which would provide equivalent interfaces to io.Reader et al., only with an added Context parameter. This package would also provide some simple wrappers for adapting between methods which take or do not take a Context.

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 ReaderWithContext function: Functions like io.Copy use type assertion to probe for methods like WriteTo. ReaderWithContext strips these methods from the returned Reader.

ReaderWithContext could use type assertions to detect the methods on the passed in r and return an appropriate wrapper type, but there's an obvious combinatorial explosion here--we'll need to define four different return types to support Reader, ReaderAt, and WriterTo...and what if we want to preserve the writer functions as well? This is a problem.

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 r has a WriteTo method, we return a value of type io.Reader that may be converted with a type assertion into an io.WriterTo.

If r does not have a WriteTo method, then things get tricky. We construct a value of type io, which does have a WriteTo; attempting to call WriteTo on this value will panic, however, since we initialized it with a nil function pointer. Converting this value to an io.Reader on return produces a value which cannot be converted into an io.WriterTo again.

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.

@dsnet
Copy link
Member

dsnet commented Sep 13, 2017

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.

@rogpeppe
Copy link
Contributor

The way I was looking at it was that:

x = interfaceType {m1:f1, m2:f2, ...}

would be syntax sugar for:

type _scratch struct {
    f1_ func typeof(interfaceType.m1)
    ...
}
func (s _scratch) f1(args) ret {
    return s.f1_(arg)
}
etc
x = interfaceType (_scratch{f1_: f1, ...})

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.

@beoran
Copy link

beoran commented Jul 31, 2021

@Merovius

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.

@Merovius
Copy link
Contributor

and as you notice in your point 2, it makes type assertions and type switches on that value more complex.

What's more complex? Everything works exactly the same as it works right now. It's functionally equivalent to having an unexported version of http.HandlerFunc (or whatever). It certainly doesn't require people to learn anything new.

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.

@beoran
Copy link

beoran commented Jul 31, 2021

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.

@Merovius
Copy link
Contributor

But what do you think about explicitly generating code versus the implicit generation that the compiler has to do?

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.

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

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.

@beoran
Copy link

beoran commented Jul 31, 2021

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.

@mediocregopher
Copy link
Author

Of course @mediocregopher could incorporate this latest iteration themselves into their proposal text. Or, if they disagree with this change, I could also file a separate issue for this modified version.

@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:

mux.Handle(interface { ServeHTTP: func(...) { ... } })

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 🙏

@beoran
Copy link

beoran commented Jul 31, 2021

I think we can keep this issue open and update the proposal at the top if you like so. Otherwise we loose the dicussion.
I just happened to find out about this issue thanks to #47480, so that caused us to continue the talk.
Some proposals for Go take a years long time before they are accepted or rejected, it goes to show the great consideration and care of the Go developers in adding new features to the language.

@Merovius
Copy link
Contributor

Merovius commented Jul 31, 2021

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]

@leighmcculloch
Copy link
Contributor

leighmcculloch commented Jul 31, 2021

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.

@Merovius
Copy link
Contributor

Merovius commented Jul 31, 2021

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.

@leighmcculloch
Copy link
Contributor

Reading back through the comments on this issue the common comments regarding the original proposal are:

  • This is something that multiple people have wanted.
  • This is something that could be confusing.

There was one concrete problem that was pointed out by @rogpeppe (#21670 (comment)):

 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.

If we say that the function is lifted into an anonymous type then it seems like this would be resolved and the NumMethod() would return 1. Is that sufficient? Or does that create other problems?

Are there other concrete problems, or things that would break, with the original proposal?

@Merovius
Copy link
Contributor

Merovius commented Aug 1, 2021

@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. io.Reader and io.Writer).

Personally, I also don't like the idea of var r io.Reader = f resulting in r containing something different from f (i.e. an automatically generated defined type). Assignments don't do that sort of thing. Conversions more so. This might seem like a minor difference but IMO it's a significant one.

@leighmcculloch
Copy link
Contributor

leighmcculloch commented Aug 1, 2021

Lack of type-safety, if a function automatically implements multiple, semantically different interfaces

Interesting. I would think the original proposal doesn't reduce type safety for the following reasons:

  1. The existing level of type-safety is maintained because nothing stopping a developer from wrapping a function intended for writing in a self defined ReaderFunc and assigning it to a io.Reader. For example:

    type ReaderFunc func (p []byte) (n int, err error)
    func (f ReaderFunc) Read(p []byte) (n int, err error) { return f(p) }
    func main() {
    	var r io.Reader = ReaderFunc(func (p []byte) (n int, err error) {
    		// Write p to somewhere
    	})
    }
  2. Types can already implement multiple, semantically different interfaces because a type doesn't specify what interfaces it implements, and with single-function interfaces so common, it's trivial for interfaces to overlap. I don't think anything in Go's type system prevents this. If the type is compatible, it's assignable.

@Merovius
Copy link
Contributor

Merovius commented Aug 1, 2021

The existing level of type-safety is maintained because nothing stopping a developer from wrapping a function intended for writing in a self defined ReaderFunc and assigning it to a io.Reader.

I don't see how this addresses the argument "you might accidentally assign a function meant to implement Read to an io.Writer". Yes, you can still be explicit about which you intend a function to implement. But for that, you have to remember being explicit about it. Types are there to fix accidental mis-assignments.

Types can already implement multiple, semantically different interfaces because a type doesn't specify what interfaces it implements, and with single-function interfaces so common, it's trivial for interfaces to overlap.

True. The difference is that methods have a name, which provides at least one layer of protection. io.Reader and io.Writer are very different interfaces - no type can implement both with the same method. However, their single methods have the same signature, so a single func would implement both.

Yes, it is possible to define type foo.Reader interface { Read([]byte) (int, error) } and have that do something completely different from io.Reader - and nothing would prevent you from accidentally using a foo.Reader as an io.Reader and vice-versa. But you got to admit that that's a very different situation from possibly mixing up io.Reader and io.Writer.

@beoran
Copy link

beoran commented Aug 1, 2021

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. But one can do a conversion by casting with Name(). I think this is ideal, there are not many languages, even compiled ones, that have that level of type safety. even an intis not compatible with int64 without type casts, which is brilliant, really because it avoids a lot of precision related problems.

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.

@empire
Copy link
Contributor

empire commented Aug 19, 2021

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, there is no problem passing a string to a function that has a Name argument.

@fzipp
Copy link
Contributor

fzipp commented Aug 19, 2021

there is no problem passing a string to a function that has a Name argument.

@empire In this case "playground" is an untyped string literal, see https://go.dev/blog/constants:

What type does this string constant have? The obvious answer is string, but that is wrong.

This is an untyped string constant, which is to say it is a constant textual value that does not yet have a fixed type. Yes, it’s a string, but it’s not a Go value of type string. It remains an untyped string constant even when given a name

You can't pass a value of type string to the function: https://play.golang.org/p/3tPkFsgiz18

@beoran
Copy link

beoran commented Aug 19, 2021 via email

@bcmills
Copy link
Contributor

bcmills commented Sep 9, 2021

I gave some more thought to the “underlying type” problem, and filed #48288 as a result.

I think the combination of that with #25860 provides both a clean syntax for literals (from #25860) and a clean answer to the “underlying type” question (from #48288).

@ianlancetaylor
Copy link
Contributor

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.

@ianlancetaylor ianlancetaylor closed this as not planned Won't fix, can't repro, duplicate, stale Aug 23, 2023
@golang golang locked and limited conversation to collaborators Aug 22, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Suggested changes to the Go language NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. Proposal v2 An incompatible library change
Projects
None yet
Development

No branches or pull requests