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: relaxed rules for assignability with differently-named but identical interfaces #16209

Closed
zellyn opened this issue Jun 29, 2016 · 18 comments

Comments

@zellyn
Copy link

zellyn commented Jun 29, 2016

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 ianlancetaylor added this to the Proposal milestone Jun 29, 2016
@ianlancetaylor
Copy link
Contributor

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

zellyn commented Jun 29, 2016

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:

  • x's type is identical to T.
  • x's type V and T have identical underlying types and at least one of V or T is not a named type.
  • T is an interface type and x implements T.
  • x is a bidirectional channel value, T is a channel type, x's type V and T have identical element types, and at least one of V or T is not a named type.
  • x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type.
  • x is an untyped constant representable by a value of type T.
  • x is a function, T is a function type, and x's type and T have the same number of parameters and result values, parameter values of x's type are assignable to corresponding parameter types of T, result values of T are assignable to corresponding result types of x's type, and either both functions are variadic or neither is.

@puellanivis
Copy link

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

  • Two interface types are identical if they have the same set of methods with the same names and identical function types. Lower-case method names from different packages are always different. The order of the methods is irrelevant.

Because:

  • Two function types are identical if they have the same number of parameters and result values, corresponding parameter and result types are identical, and either both functions are variadic or neither is. Parameter and result names are not required to match.
  • Two struct types are identical if they have the same sequence of fields, and if corresponding fields have the same names, and identical types, and identical tags. Two anonymous fields are considered to have the same name. Lower-case field names from different packages are always different.

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

zellyn commented Jun 29, 2016

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

@puellanivis
Copy link

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

puellanivis commented Jun 29, 2016

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

beoran commented Jun 30, 2016

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

@puellanivis
Copy link

puellanivis commented Jun 30, 2016

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

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

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

AAAAAAAnd… :( you're absolutely right…

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

Ugh… so much annoying pedantry to sift through…

@puellanivis
Copy link

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

taruti commented Jul 6, 2016

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

also see #8082

@griesemer
Copy link
Contributor

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

zellyn commented Jul 6, 2016

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

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

puellanivis commented Jul 6, 2016

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…”

@golang golang locked and limited conversation to collaborators Jul 7, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants