golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
123.86k stars 17.65k forks source link

Proposal: relaxed rules for assignability with differently-named but identical interfaces #16209

Closed zellyn closed 8 years ago

zellyn commented 8 years ago

Functions and structures are assignable only when the function arguments or structure fields have identical types. It would be useful to allow assignability in the case where corresponding arguments or fields have identically-shaped interface types.

Passing variables with differently-named but otherwise identical interface types works. However, passing handler functions or structs with such interface types as parameters or fields does not work.

The most important use case is vendoring, where differently-vendored interfaces are common.

Another clear and recent example is renaming: with the moving of context.Context into the standard library, it is impossible to pass a handler function with a context parameter, unless both passing and passed-to code rename at the same time.

Previous informal discussion: https://groups.google.com/forum/#!topic/golang-dev/lOqzH86yAM4

A note: while it would be possible to actually unify identical interfaces, program-wide, I believe it would cause too many problems when using reflection: it would be unclear which name to use.

ianlancetaylor commented 8 years ago

It would help if you could describe more precisely what you propose to change in the language spec.

I think you are proposing that we change the handling of named interface types when such types are used as function arguments or results, or as struct field types, when converting from type to another. Why are interface types special here? In exactly what cases are interface types special?

The current rules for type conversions are already a bit complex. Is this change going to make them more complex? That would be a drawback. It should be easy to understand when a type conversion is valid. Anything that makes that harder to understand needs significant benefits.

zellyn commented 8 years ago

I left specific examples in the referenced thread. That was probably a mistake… better for this discussion to be self-contained.

Here's a fairly succinct example. We're trying to pass a function as an argument, but the argument types differ because fmt.Stringer and main.Stringer are different interfaces, even though they are compatible:

package main

import (
    "fmt"
    "time"
)

type Stringer interface {
    String() string
}

func func1(s Stringer) {}
func func2(s fmt.Stringer) {}
func func3(f func(s Stringer)) {}
func func4(f func(s fmt.Stringer)) {}

func main() {
    e := time.Second // valid Stringer

    func1(e) // ok
    func2(e) // ok

    func3(func1) // ok
    func4(func1) // cannot use func1 (type func(Stringer)) as type func(fmt.Stringer) in argument to func4
}

Honestly, I don't see much practical reason for struct fields to change, but they seem similar enough that it would be confusing for them to be treated differently. Perhaps it would be simpler to limit this proposal to functions: I've done that below in the proposed change. Apologies for the clumsy language: I expect someone with more experience at spec writing can do better.

Proposed change:

Assignability

A value x is assignable to a variable of type T ("x is assignable to T") in any of these cases:

puellanivis commented 8 years ago

I think this proposal is more targeted at enforcing this element of the specification:

Because:

If two interfaces types are identical if they have the same methods, then context.Context and x/net/context.Context are identical types, which means that the functions and structures using them should view them as identical types, and therefore

type F func(ctx context.Context) and type G func(ctx x/net/context.Context)

Should already be identical types, by the language specification already.

zellyn commented 8 years ago

Nice catch, @puellanivis! That is exactly what this is targeting. So perhaps it's actually a bug.

puellanivis commented 8 years ago

Hmm… missed this part “Two named types are identical if their type names originate in the same TypeSpec.”

I'm working on a better example that exposes the issue I see at hand.

puellanivis commented 8 years ago

It's kind of convoluted, but it is dealing with esoteric one-offs in the language spec vs implementation: https://play.golang.org/p/YlrMrkvY60

First, by spec var a A = c should work. The two parameters are identical types (one is named, the other is unnamed, they have the same methods, with the same signatures, and the types of fmt.Stringer and interface{ String() string} are identical.

Instead we get: prog.go:33: cannot use c (type *C) as type A in assignment: *C does not implement A (wrong type for F method) have F(interface { String() string }) want F(fmt.Stringer)

The proposal here is that a = b should also work, instead we get: prog.go:34: cannot use b (type *B) as type A in assignment: *B does not implement A (wrong type for F method) have F(Stringer) want F(fmt.Stringer)

But if the first worked, we could just replace all Context functions with literal interfaces… but the later allows us to avoid having to copy-paste types all over the place, when the two interfaces are for all intents and purposes semantically and syntactically identical. (They all accept the same arguments to each method the exact same way, and treat their arguments the exact same way.)

This expands the power of interfaces, because interfaces are intended to be semantically flexible, and fulfilled simply by implementing the same receiver methods.

beoran commented 8 years ago

Interesting, so is the implementation wrong, or the specification? The current spec seems very sensible, so why was it not implemented like that?

puellanivis commented 8 years ago

Because this was an esoteric element of the spec that no one has really thought of until now?

I mean, who would put a giant interface{ Method1(); Method2() } on their function signatures?

I mean, even gofmt doesn't like it: it formats it as:

func (c *C) F(x interface { String() string }) { fmt.Println(x.String()) }

OUCH! that's ugly! >_<

I tried it the other way around, with the interface{} literal type in the interface definition, and B and C take Stringer and fmt.Stringer respectively, but this still didn't work.

puellanivis commented 8 years ago

Oh yeah, and by meta-argument, the specification is never wrong, it is the definition.

And anything failing to conform to the specification is then out-of-spec.

Honestly, I'm not even sure how I would fix the spec to conform to this implementation beyond a really long and verbose exception to checking interface types as identical…

griesemer commented 8 years ago

@puellanivis Regarding your example https://play.golang.org/p/YlrMrkvY60 : var a A = c doesn't work even by the spec. The issue is exactly the issue this proposal is trying to address: The value of c is of a type that must implement A. For that to happen, the type of c which is *C, must have all the methods of A, with identical signatures. It does have F, but the parameter types of F are not identical. One is a fmt.Stringer, the other one an unnamed interface.

Your suggestion to make two interfaces identical if they have identical methods would solve this and the proposal, except for what you also have found already, the fact that the name of the interface types is currently looked at in type identity as well (which is why the above fails).

In other words, type identity would have to change such that the type name is not considered for interfaces. That's the simple-most change I can think of, but it's also the most pervasive one in terms of its effect.

I don't know what the implications of such a change are. Interfaces are special, and for instance it's not possible to attach methods to interfaces the way it's done for other types; the methods are already part of the type. So the name is not so important. In fact, in most scenarios, the interface name is not important at all. The question is, can we ignore it always? It will permit programs that we cannot write now, including the ones we would like to write and cannot (hence the proposal). But does it also permit programs that we want to prevent from being written? Are there implications for reflection? (quite possibly).

I don't know all these answers. One way to make progress would be for somebody to adjust the compiler's identity function for types to use the more relaxed form for interfaces; that should be a pretty straight-forward change I think. And then run all.bash and see what breaks. If nothing breaks there's a reasonably good change it's a backward-compatible language change. At that point we'd have to see how reflect should be adjusted, if at all. If it does, it may or may not violate the Go 1 compatibility guarantee (the behavior of reflect may have changed in incompatible ways).

puellanivis commented 8 years ago

AAAAAAAnd… :( you're absolutely right…

“… A named and an unnamed type are always different. …”

Ugh… so much annoying pedantry to sift through…

puellanivis commented 8 years ago

For the most part, I don't see any effect on reflection. You already cannot access the concrete value “inside” the interface. So, you're just left with exported methods, which is currently already identical.

I mean, in a real sense, an interface is semantically defined as the set of methods that must be implemented for something to match that interface. So, two interfaces declaring the exact same set of methods are … er… “meta-semantically?” identical interfaces, because they are already identical interfaces by definition. (There is no meaningful way to actually distinguish my Stringer from your Stringer EXCEPT by name.)

Structs would still compare types by names if either is named, so as far as structs are concerned at worst, we would allow: var a struct{ context.Context } = struct{ x/net/context.Context }{} to be a valid assignment… which… is kind of really weird in the first place… (who would be assigning anonymous structs to other anonymous structs in the first place?)

Function types would still be different if either is named… so, at worst, we allow var f func(context.Context) = func(x/net/context.Context){} to be valid, but then that's already kind of what we're trying to advance with this proposal…

And any intra-interface ( o.O … v_v ) assignments would already be allowed and have no chance of panicing due to failure of one to implement the other, because as noted, they must already have identical method implementations. var ( ctx context.Context ; ctx2 x/net/context/Context ) ; ctx, ctx2 = ctx2, ctx is already permitted code, and is already known to be unable to result in a runtime panic.

In fact, interfaces ALREADY cannot take named function types. So pretty much the only consequence I can possibly eek out of this mess, is that some unnamed struct types, and some unnamed func types would be able to compare as identical types. The latter of which would allow some named interfaces to compare as identical, which is actually desirable.

taruti commented 8 years ago

A very related issue is trying to implement a type satisfying net/http.FileSystem without depending on net/http. Seems like in some cases structural equality instead of name equality would make sense in interfaces.

metakeule commented 8 years ago

also see #8082

griesemer commented 8 years ago

Thanks @metakeule for the reference. I am going to close this proposal since #8082 addresses essentially the same problem and has explored it already a bit more. While not 100% a duplicate, this proposal is trying to address the same problem.

zellyn commented 8 years ago

Does that mean (with the go2 tag on #8082) that that's the kiss of death for this proposal? I feel like the vendor semantics add a material difference to the discussion that wasn't pressing when #8082 was created…

griesemer commented 8 years ago

@zellyn I'm going to remove the Go2 label on #8082 so it's on the radar. Please add your comments there. Concentrating comments on one proposal will show community support better.

puellanivis commented 8 years ago

Indeed, it's pretty much the same bug, and is actually calling for basically the same solution (as I see it).

But with 1.7 and the move of context.Context, it has indeed suddenly become much more important than, “it'd be nice if Go2 had…”