golang / go

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

proposal: Go 2: interface literals #25860

Open smasher164 opened 6 years ago

smasher164 commented 6 years ago

Filing this for completeness sake, since it was mentioned in https://github.com/golang/go/issues/21670 that this proposal had been discussed privately prior to Go 1. Just like literals allow the construction of slice, struct, and map values, I propose "interface literals" which specifically construct values that satisfy an interface. The syntax would mirror that of struct literals, where field names would correspond to method names. The original idea is proposed by @Sajmani in https://github.com/golang/go/issues/21670#issuecomment-325739411.

Conceptually, this proposal would transform the following initializer

f := func(p int) { /* implementation */ }
x := interface{
    Name(p int)
}{f}

to the following type declaration and struct initializer

type _impl_Name_int struct {
    f func(p int)
}
func(v _impl_Name_int) Name(p int) { v.f(p) }
// … in some other scope
f := func(p int) { /* implementation */ }
var x interface{
    Name(p int)
} = _impl_Name_int{f}

As an extension to what’s mentioned in #21670, I propose that fields be addressable by both method names and field names, like in this example:

type ReadWriteSeekCloser interface {
    ReadWriteSeeker
    Closer
}

f := os.Open("file")
calls := 0
return ReadWriteSeekCloser{
    ReadWriteSeeker: f,
    Close: func() error {
        if calls < 1 {
            return f.Close()
        }
        return nil
    },
}

The default value for a method is nil. Calling a nil method will cause a panic. As a corollary, the interface can be made smaller to be any subset of the original declaration. The value can no longer be converted back to satisfy the original interface. See the following modified example (from @neild in https://github.com/golang/go/issues/21670#issuecomment-329251670):

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)
}
// 3 method -> 2^3 = 8 subsets
func fn() io.Reader {
    return io{
        Read: strings.NewReader(“”),
    }
}

The nil values for ReadAt and WriteTo make it so the “downcasted” io.Reader can no longer be recast to an io. This provides a clean way to promote known methods, with the side effect that the struct transformation described above won't be a valid implementation of this proposal, since casting does not work this way when calling a nil function pointer through a struct.

Although this proposal brings parity between struct and interface initialization and provides easy promotion of known methods, I don’t think this feature would dramatically improve the way Go programs are written.

We may see more usage of closures like in this sorting example (now obviated because of sort.Slice):

arr := []int{1,2,3,4,5}
sort.Sort(sort.Interface{
    Len: func() int { return len(arr) },
    Swap: func(i, j int) {
        temp := arr[i]
        arr[i] = arr[j]
        arr[j] = temp
    },
    Less: func(i, j int) bool { return arr[i] < arr[j] },
})

Promotion of known methods also avoids a lot of boilerplate, although I’m not sure that it is a common enough use case to warrant a language feature. For instance, if I wanted to wrap an io.Reader, but also let through implementations of io.ReaderAt, io.WriterTo, and io.Seeker, I would need seven different wrapper types, each of which embeds these types:

type wrapper1 struct {
    io.Reader
    io.ReaderAt
}
type wrapper2 struct {
    io.Reader
    io.WriterTo
}
type wrapper3 struct {
    io.Reader
    io.Seeker
}
type wrapper4 struct {
    io.Reader
    io.ReaderAt
    io.WriterTo
}
type wrapper5 struct {
    io.Reader
    io.ReaderAt
    io.Seeker
}
type wrapper6 struct {
    io.Reader
    io.WriterTo
    io.Seeker
}
type wrapper7 struct {
    io.Reader
    io.ReaderAt
    io.WriterTo
    io.Seeker
}

Here is the relevant change to the grammar (under the composite literal section of the spec):

LiteralType = StructType | InterfaceType | ArrayType | "[" "..." "]" ElementType | 
              SliceType | MapType | TypeName .
themeeman commented 5 years ago

How would this work in terms of reflection? What would be the result of calling reflect.ValueOf on one of these interface literals? This would create the case that isn't currently possible where an interface isn't actually "wrapping" some underlying value. There is also the question of how they would work with a switch t.(type) statement.

smasher164 commented 5 years ago

I assume you mean to ask that if i is an interface literal as described above iv := reflect.ValueOf(i) u := iv.Interface() uv := reflect.ValueOf(u) What would be the kind of uv? Also what operations on uv are valid?

You are right in that there isn't an underlying value being wrapped. That being said, since type switches can already switch on interfaces, doing so on an interface literal would simply not satisfy cases that check for concrete types.

b := []byte("some randomly accessible string")
pos := 0
rs := io.ReadSeeker{
    Read: func(p []byte) (n int, err error) {
        /* implementation */
    }
    Seek: func(offset int64, whence int) (int64, error) {
        /* implementation */
    }
}
r := io.Reader(rs)
switch r.(type) {
case *bytes.Buffer:
    // does not satisfy
case io.ReadWriteCloser:
    // does not satisfy
case io.ReadSeeker:
    // does satisfy
}

As to the representation of a literal's reflected value, if the same reflect package is used for Go 2, the underlying value can be a MethodSet. This does not have to correspond to its runtime representation, but this is a simple abstraction for the reflect package.

A MethodSet is just an interface that references all methods in the underlying value. Operations on a MethodSet are nearly identical to operations on an Interface. From the above example, if uv.Kind() is a MethodSet, then uv.Interface() is no longer a valid operation.

ut := uv.Type() will return a type with all of the underlying methods. Similar to an interface type, ut.Method and ut.MethodByName will return Methods whose signatures do not have a receiver and whose Func fields are nil.

smasher164 commented 4 years ago

I think the primary problem with this proposal is that it allows nil methods, which panic when invoked.

  1. This violates a fundamental property of interfaces, where a type that implements an interface definitively implements a contract that its users can expect. Now a user of an interface cannot assume that a method is invocable, and the meaning of the interface lost.
  2. The runtime would have to guard against an invocation of a method. I could see this slowing down invocations to all interface methods.
  3. What is the difference, if any, between an interface that is nil, and an interface literal whose methods are all nil?

On the other hand, nil interfaces are extremely common, even though one could argue that they are also a violation of the contract described above, since users expect to be able to invoke its methods.

Additionally, I think allowing nil methods to prevent a downcasted interface from type-asserting into the original interface sounds nice since it allows the promotion of known methods. However, this behavior only exists because nil methods are allowed, and allows the runtime to convert an "unsafe" (non-invocable) interface to a "safe" (invocable) interface. This behavior implies that non-invocable interfaces shouldn't exist in the first place, and is too subtle and surprising.

The only alternative I can think of is to make a nil method provide some default no-op implementation of a method. Although this is safer than the previously mentioned, it seems just as subtle and surprising, but less powerful.

Ultimately, it appears impossible to require that no field in an interface literal is nil, since a value that is assigned to it could be nil at run-time. The only way to guarantee no field is nil would be to restrict each field to be a function literal or top-level function. However, this pretty much loses all of the power of interface literals, since it is only a marginal improvement (LoC-wise) over a type which is defined to implement an interface.

carnott-snap commented 4 years ago

Can this be added to #33892 for review?

smasher164 commented 4 years ago

/cc @golang/proposal-review

ianlancetaylor commented 4 years ago

This was reviewed and marked as NeedsInvestigation. At some point we'll re-review these issues.

beoran commented 2 years ago

I came here from #47487 which might be seen as competing with this issue.

Seeing that the interface literals proposed here have a serious problem with nil safety, as @smasher164 admits here: https://github.com/golang/go/issues/25860#issuecomment-550104429 and considering that nil problems are a significant problem when developing software (look up "Null References: The Billion Dollar Mistake"), I strongly believe that any changes to the Go language should make it more nil safe and not less so.

Therefore I respectfully oppose this proposal, unless a way can be found to avoid the nil problem. #47487 on the other hand does not seem to have this nil problem, so I support that as an alternative.

Merovius commented 2 years ago

@smasher164

This violates a fundamental property of interfaces, where a type that implements an interface definitively implements a contract that its users can expect. Now a user of an interface cannot assume that a method is invocable, and the meaning of the interface lost.

This is not necessarily true. For example:

package main

type T struct{}

func (*T) Foo() {}

func (T) Bar() {}

type FooBarer interface {
    Foo()
    Bar()
}

func main() {
    var x FooBarer = (*T)(nil)
    // Does not panic
    x.Foo()
    // Panics - a nil *T is dereferenced when calling a method with value receiver.
    x.Bar()
}

I included two methods here, to demonstrate that this isn't simply "calling a method on a nil-pointer might panic, if that method dereferences it". The panic here is caused on a language-level, very similar (if not equivalent) to what would happen if an interface-literal contains a nil method.

The runtime would have to guard against an invocation of a method. I could see this slowing down invocations to all interface methods.

This is also not a worry. The runtime does not do nil-checks in general - it just does a call of the function pointer which causes a hardware trap when it's nil, which is then translated into a SIGSEGV which is caught by the runtime. That is, panics on nil derefercning/calling are free at runtime, in the happy path.

What is the difference, if any, between an interface that is nil, and an interface literal whose methods are all nil?

I would argue it's similar, if not the same, as a pointer being stored in an interface, where all methods panic when called with a nil-pointer - it's a non-nil interface, which panics if called. This is, as shown above, already possible and well-defined without any real isues.

smasher164 commented 2 years ago

@Merovius Good point about the dereferencing when the method has a value receiver.

zephyrtronium commented 2 years ago

Under this proposal, given an interface literal with a nil method, is the result of the method expression selecting that method nil or non-nil? E.g., is the output of the following program true or false?

package main

func main() {
    println(interface{ M() }{}.M == nil)
}

If the output of this program is true, it becomes possible to program around omitted methods. Contrarily, if it's possible, then people will do it, and I worry the end result would be complaints that interfaces cause boilerplate.

smasher164 commented 2 years ago

If nil methods are allowed, the output of the above program would be true. I would not want people to have to program around omitted methods. A nil method should only exist to aid narrowing conversions to a different interface type. Panicking when invoking a nil method should be similar to the invoked method panicking in its body.

DeedleFake commented 2 years ago

@beoran

47487 can actually be used to replicate the functionality of this proposal in some circumstances, though it's a lot clunkier:

v := []int{3, 2, 5}

type Lesser interface{ Less(i1, i2 int) bool }
type Swapper interface{ Swap(i1, i2 int) }
type Lenner interface{ Len() int }
sort.Sort(struct {
    Lesser
    Swapper
    Lenner
}{
    Lesser:  Lesser(func(i1, i2 int) bool { return v[i1] < v[i2] }),
    Swapper: Swapper(func(i1, i2 int) { v[i1], v[i2] = v[i2], v[i1] }),
    Lenner:  Lenner(func() int { return len(v) }),
})

This pattern also works currently, but it requires top-level types to do the single-method-interface-implemented-by-a-function wrapper manually because there's currently no way to declare methods on a type inside another function.

DeedleFake commented 2 years ago

Is there any particular reason that this proposal can't be modified to require exhaustive implementation? I see a lot of arguments for and against it being accepted with nil methods, but what if methods could just simply never be nil? With a struct, adding more fields is a non-breaking change provided that any struct literals of that type use field names as the new ones will just be zero values, but adding a method to an interface is already a breaking change under almost all circumstances.

What's the benefit of allowing methods to be nil in this proposal at all instead of just requiring that all of them be implemented in any interface literal? The original proposal mentions something about downcasting them to smaller interfaces and being unable to revert that due to the lack of the method implementation, but why not just implement the smaller interface in the first place, then?

smasher164 commented 2 years ago

@DeedleFake

Is there any particular reason that this proposal can't be modified to require exhaustive implementation?

Even if we wanted to disallow nil methods, it is unclear how we would enforce that statically. Take this program for example, where readerFunc's being nil is determined by some runtime condition.

var readerFunc func([]byte) (int, error)
if runtimeCondition {
    readerFunc = func(p []byte) (int, error) { /* implementation */ }
}
reader := io.Reader{
    Read: readerFunc,
}

We could conservatively restrict methods in interface literals to either be top-level functions or function literals, but these rules are arbitrary and hard to teach. Additionally, a lot of the power of interface literals comes from the ability to change the implementation of some method by changing the variable binding for some closure. If you're going to define all of the functions up-front anyways, why not just make them methods on some basic type definition?

why not just implement the smaller interface in the first place, then?

The reason is to allow the promotion of known methods. Consider the example that @neild provides here: https://github.com/golang/go/issues/21670#issuecomment-329251670. Basically, you know that some library wants to type-assert on your value for specific functionality (like fs.FS for example). You want to wrap that interface with your own functionality, but still allow some methods implemented by its underlying type to surface up.

Normally, you would have to type-assert for all combinations of methods you care about, and return a concrete type that implements that combination of methods. So if you wanted to wrap an fs.FS to do some additional work on its Open operation, but you still wanted StatFS and ReadDirFS to surface through, you would end up creating four concrete types.

The question is if this is even a problem worth solving for interface literals. However, even if it wasn't, nil methods would still exist, just for the ability to dynamically set an interface's method.

Merovius commented 2 years ago

@DeedleFake

I see a lot of arguments for and against it being accepted with nil methods, but what if methods could just simply never be nil?

The same reasons I mention here.

DeedleFake commented 2 years ago

Ah, duh. I was mostly confused by the defaulting of unspecified methods to nil and somehow completely overlooked manually setting them to nil. I was thinking of Java's subclass literals which use a regular method definition syntax, rather than a field setting syntax. Something like

sort.Sort(sort.Interface{
  Less(i1, i2 int) bool { ... }
  Swap(i1, i2 int) { ... }
  Len() int { ... }
})

Maybe inverting the problem and making it anonymous type literals instead of interface literals would make more sense. I remember the reflect.MakeInterface() proposal running into a similar problem as they slowly realized that they weren't actually creating an interface, but were rather creating a new anonymous type with the right methods to satisfy an interface. Maybe it would help to approach this from the point of view creating an anonymous, data-less type with inline, closured methods, and then it can just satisfy whatever interfaces, the same as any other concrete type.

Edit: To clarify, I'm saying ditch the interface name completely and just define an inline method set, then let the type checker do the work:

// sort.Sort() already defines its argument as being sort.Interface,
// so the regular type checker can easily sort this out.
sort.Sort(type {
  Less(i1, i2 int) bool { ... }
  Swap(i1, i2 int) { ... }
  Len() int { ... }
})
smasher164 commented 2 years ago

ditch the interface name completely and just define an inline method set

As per the spec (https://golang.org/ref/spec#Interface_types), an interface is a method set. This proposal would relax the definition of an interface to not require an underlying concrete type.

DeedleFake commented 2 years ago

No, the spec says that an interface type is a named method set without implementation, and that the method set of an interface type is its 'interface', meaning the methods that can be called on it. That definition is there to allow interface types to implement other interface types.

What I'm saying seems to me to actually be supported by that definition. I'm saying that it seems to make more sense to me to approach this from the point of view of implementing anonymous concrete types with methods attached to them, rather than attaching implementations directly to interface types anonymously. With this approach, it doesn't require any changes to the definition of interfaces, interface types, or method sets, and it solves the nil method problem. This also puts them conceptually in line with anonymous functions, which are no different than any other function except for the lack of an explicit name and the closure mechanics.

If you'd like, I can file a separate proposal for this, but not until tomorrow. I'm typing this on my phone at nearly 3:00 A.M., and I'd rather not write up a full proposal like this.

zephyrtronium commented 2 years ago

@DeedleFake

Maybe inverting the problem and making it anonymous type literals instead of interface literals would make more sense. I remember the reflect.MakeInterface() proposal running into a similar problem as they slowly realized that they weren't actually creating an interface, but were rather creating a new anonymous type with the right methods to satisfy an interface. ...

// sort.Sort() already defines its argument as being sort.Interface,
// so the regular type checker can easily sort this out.
sort.Sort(type {
  Less(i1, i2 int) bool { ... }
  Swap(i1, i2 int) { ... }
  Len() int { ... }
})

I think this is a substantially different proposal, in particular because it introduces new syntax, whereas the topic proposal adds a missing case for the existing composite literal syntax.

In my opinion, it also has a number of disadvantages: it requires defining an anonymous function for each method instead of using any function value; it is visually similar to composite literals but defines methods instead of field or element values; it reads poorly in declarations like v := type { ... }, because the result is not a type; and it is somewhat unclear what happens in the expression type{}.

DeedleFake commented 2 years ago

whereas the topic proposal adds a missing case for the existing composite literal syntax

It's a very non-obvious 'missing' case, though, as it actually creates a completely separate type, attaches methods to it, and then wraps the result in an interface type automatically.

it requires defining an anonymous function for each method instead of using any function value

This proposal requires something similar behind the scenes, as there'll have to be some generated type that has methods that implement the interface type in use. An interface value internally is a pair of words, one identifying the type of the concrete value underneath and the other a pointer to a value of that type. That type identifier is going to have to be set to something, and that something is going to need methods defined on it, which is going to require anonymous functions be generated for each method in the interface type's method set.

It might be possible to do it with some kind of special case, but that's going to introduce even more weird edge cases than just the nil method question, especially in terms of reflection.

it is visually similar to composite literals but defines methods instead of field or element values; it reads poorly in declarations like v := type { ... }, because the result is not a type; and it is somewhat unclear what happens in the expression type{}.

The specifics of the syntax were just an example to help illustrate the idea. The result is a type, though. Method sets are directly tied to type declarations. Every usage of the type keyword that isn't an alias creates a brand new method set:

// Example has the same memory layout as strings.Reader, but a completely separate method set.
type Example strings.Reader
zephyrtronium commented 2 years ago

Given the "narrowing" example:

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 fn() io.Reader {
    return io{
        Read: strings.NewReader(“”),
    }
}

Does the type assertion fn().(io) succeed?

zephyrtronium commented 2 years ago

@DeedleFake

This proposal requires something similar behind the scenes, as there'll have to be some generated type that has methods that implement the interface type in use. ... That type identifier is going to have to be set to something, and that something is going to need methods defined on it, which is going to require anonymous functions be generated for each method in the interface type's method set.

Perhaps I should have specified "function literal" rather than "anonymous function." Regardless, the same "behind the scenes" generation of wrapper methods will need to happen under either idea, because function values have different representations than function declarations (and respectively for methods, hence for itabs as well). The only substantial differences between the two ideas are in the spelling.

The specifics of the syntax were just an example to help illustrate the idea. The result is a type, though.

I disagree. The result of v := type { ... } is not a type, it is a value of a type with a non-empty method set but that is not explicitly declared. v cannot have a type that is itself a type; Go has no type metaprogramming.

DeedleFake commented 2 years ago

The only substantial differences between the two ideas are in the spelling.

I think that the conceptual difference is larger than that. At a glance, the only difference between Go interfaces and Java interfaces is that Go has implicit satisfaction, but the conceptual difference that that makes is quite large as it inverts the question from 'What interfaces do I want to implement with this type?' to 'What functionality do I want to be passed in this argument?'. This similarly inverts the idea.

I disagree. The result of v := type { ... } is not a type, it is a value of a type with a non-empty method set but that is not explicitly declared. v cannot have a type that is itself a type; Go has no type metaprogramming.

Sorry, I was unclear. You are correct, v is not a type. I meant that the type {} expression would return an instance of a new anonymous type with no data structure, so probably a struct{}, but with a non-empty method set. I thought it made sense to use type for that as every non-alias declared concrete type in Go has an independent method set from every other one. Interface types technically do, too, kind of, but they have no implementation for their method sets. By changing this from 'Allow implementation for interface method sets in very specific circumstances.' to 'Add a syntax and supporting system for creating anonymous types with implemented method sets.', I think that it clears up some of the questions. That's the only thing that I'm trying to say.

rsc commented 2 years ago

For the record, this proposal is not under active consideration. It's fine to discuss, but be aware that we're not going to evaluate this for a while. Go 2 efforts are focused on generics for the time being.

Merovius commented 2 years ago

I think the most practical approach is the one I took for #47487 - an interface literal implicitly defines an unexported type with an arbitrary name in the runtime (or perhaps the current) package. For this proposal, the reflect.Kind should probably struct, with no fields and the requisite methods. i.e. it can't be inspected or modified using reflect, except than to call methods on it.

I don't think inventing a new reflect.Kind or inventing the concept of an interface without a concrete type is an acceptable approach for this - and neither is violating the invariant that only defined types have methods.

That's just my opinion, though.

@zephyrtronium

Does the type assertion fn().(io) succeed?

IMO it would be illogical for it not to. io{…} should definitely be a value that implements io and assigning that to io.Reader shouldn't change that fact. That is, io.Reader(io{…}).(io) should yield the same value you created with the literal.

DeedleFake commented 2 years ago

an interface literal implicitly defines an unexported type with an arbitrary name in the runtime (or perhaps the current) package. For this proposal, the reflect.Kind should probably struct, with no fields and the requisite methods. i.e. it can't be inspected or modified using reflect, except than to call methods on it.

This is exactly what I'm suggesting above.

D1CED commented 2 years ago

As a natural extension and to align this with the way struct literals work there should be the possibility to crate interface literals based on order too.

arr := []int{1,2,3,4,5}
// By Name
sort.Sort(sort.Interface{
    Len: func() int { return len(arr) },
    Swap: func(i, j int) {
        temp := arr[i]
        arr[i] = arr[j]
        arr[j] = temp
    },
    Less: func(i, j int) bool { return arr[i] < arr[j] },
})
// By Order
// The second and third functions needed to be swapped to match the interfaces definition in package sort.
//
//     type Interface interface {
//         Len() int
//         Less(i, j int) bool
//         Swap(i, j int)
//     }
sort.Sort(sort.Interface{
    func() int { return len(arr) },
    func(i, j int) bool { return arr[i] < arr[j] },
    func(i, j int) {
        temp := arr[i]
        arr[i] = arr[j]
        arr[j] = temp
    },
})

So a given a single-method-interface this would look like: r := io.Reader{someFuncWithMatchingSingnature}

@Merovius would this address you concern of readability given that the only difference to #47487 are the brackets.

Merovius commented 2 years ago

@D1CED I currently wouldn't consider changing the order of method in an interface a breaking change. That would change. I don't like that, TBH. It probably doesn't matter much in practice, but it IMO detracts from the conceptual idea of an interface.

But either way: My main readability concern is the squashing of multiple func-literals into one expression. So, from that POV, the fact that #47487 only works on single-method interfaces is not a bug to me, but a feature. But if we use interface-literals, I would actually find it more readable to force people to write the method names out.

Merovius commented 2 years ago

On #47487 @DmitriyMV suggested:

[Maybe we can] restrict it to one method interfaces for now. That way we can have non breaking change, and dissalow setting multiple methods using closures for now.

Edit: actually that also do not look very different from existing code, since you can already initialize structs using unnamed arguments if they are exactly the same type and in exact same order, as the struct definition.

To me, once we commit to interface-literal syntax, restricting it to one method feels very artificial. I'd rather accept the (perceived, by me) readability hit of squashing multiple methods into one expression, than putting artificial restrictions like this in place.

Merovius commented 2 years ago

I'm not sure where we're at exactly with this proposal. As an alternative to #47487, we should have a clear idea of what exactly we are considering. I assume, for now, that we are still mainly talking about the contents of the top-post as the proposal. That, IMO, has a couple of problems which would make me oppose it.

@smasher164

Conceptually, this proposal would transform the following initializer […] to the following type declaration and struct initializer […]

To be clear: Are you suggesting that _impl_Name_int will become available as a package-scoped identifier, usable in the rest of the package? Or are you using it as a standin for "some implementation-defined, opaque name, which can't be referenced by the rest of the package", as we converged on for #47487?

For now, I'll assume the latter, as the former seems problematic and the latter seems to have good chances for consensus. Under that assumption, it would be generally impossible to access/manipulate the dynamic value of the interface, without use of reflect, correct?

You also chose the unexported field name f. I assume that is, as well, an arbitrary placeholder? The relevance here is that ISTM that as long as that field is unexported, there is no way to set it - as per the last paragraph, you'd have to use reflect, which refuses to modify unexported fields. That's why I suggested to not have any fields in that struct type at all - it's not useful to be able to read them, as you could just use method-values of the interface value or reflect.Value.MethodByName to achieve the same thing. So pretending there are no fields just saves us from talking about how to name them at all.

Of course, if it's infeasible to implement this, we can continue to stick with "a struct with some func fields with unexported, implementation-defined names". I just would like to clarify that the dynamic value will - either way - be immutable, even using reflect.

As an extension to what’s mentioned in #21670, I propose that fields be addressable by both method names and field names, like in this example:

Personally, I don't like this. With this, the following interfaces would become de-facto different types:

type A = interface {
    io.ReadWriter
}
type B = interface {
    io.Reader
    io.Writer
}

as they have different "field names". However, they currently are exactly the same. That seems like a big can of worms to open, just to avoid writing

ReadWriteSeekCloser{
    Read: f.Read,
    Write: f.Write,
    Seek: f.Seek,
    Close: […]
}

(also, as a nit: "addressable" has a specific meaning in Go. I was confused for a bit why you talked about addressability of fields or methods)

The default value for a method is nil. Calling a nil method will cause a panic. As a corollary, the interface can be made smaller to be any subset of the original declaration. The value can no longer be converted back to satisfy the original interface. See the following modified example (from @neild in #21670 (comment)): […]

I don't understand what you mean by "converted back to satisfy the original interface". Do you mean that, in that example, the type-assertion fn().(io) would fail? i.e. do you mean that the type of the io{…}-literal gets different methods, depending on if the func-values used are nil? So that io{…} doesn't necessarily implement io?

If so, I don't like that, for multiple reasons:

  1. It requires dynamically inspecting func values to check if they are nil and generate types accordingly. In practice, the compiler might have to generate all possible combinations to assign them the right method sets and then dynamically look up which of these types to use.
  2. It sets a strange precedent to have the method set of a type be dependent on the specific value it takes.
  3. io{…} looks (intentionally) like a composite literal of type io. I would very much expect the dynamic type of io.Reader(io{…}) to be the same as the dynamic type of io{…} and thus to have a type-assertion on io succeed.

FWIW, I'm sympathetic with @neild's use-case, but I don't think this is the right place to solve it. AIUI the general problem is called the expression problem and personally I like the solution outlined by the Featherweight Generic Go paper. The details are a distraction here and we haven't yet discussed if we want to implement that suggestion at any point. I just wanted to mention that I think there is a cleaner and clearer solution to the problem possible, so it doesn't necessarily have to be solved here.

As to the representation of a literal's reflected value, if the same reflect package is used for Go 2, the underlying value can be a MethodSet.

The reflect package provides, as much as feasible, a runtime-implementation of the regular Go type-system. As such, I don't think we should add new Kinds to the reflect package, which don't have a corresponding concept in the language proper. I can see three basic ways to add "interface literals" to the language, all with clear answers to how reflect would work for them:

  1. Add a new sort of type to the language, similar to "struct types" or "pointer types". We'd need to come up with a good name. "Method set types" doesn't work IMO, because that term is already too heavily ingrained in docs to mean something else. I honestly can't come up with a name I find even remotely good right now (the best I can do is "Anonymous interface types" or "interface implementer types"), but maybe someone else is creative enough. Either way, making it a proper type would likely mean that x := io{…} should have that static type, not io itself, to stay cohesive with the language as a whole.
  2. Change the way we talk about interface types, allowing them to not have a dynamic value. That's really the only difference between these interface-literals and a proper interface value - that you can't unpack them. This ends up changing a bunch of reflect invariants though - for example, you can't necessarily call Elem() on an interface-type/value anymore (or you can't rely on it returning a well-defined type). That seems dangerous for existing reflect based code.
  3. Cheat, by making the dynamic type a struct{} (or something else already in the language) - it avoids these questions by only relying on mechanisms already present in the language. By making it a struct{} with some methods, we effectively simulate the idea of "you can do anything you can do with an interface, except unwrap it" (come to think of it, maybe it should be struct{ _ [0]func() }, to avoid the type becoming comparable).

I think the primary problem with this proposal is that it allows nil methods

There has been a bunch of discussion here about this aspect. In the interest of converging the discussion (and also based on the arguments I mention above and in the rest of the discussion), I want to re-affirm my opinion that IMO this should be considered fine. That is, I think the behavior of

type I interface{ M() }
func main() {
    x := I{M: nil}
    fmt.Println(x.M == nil)
    x.M()
}

should be exactly the same as of this code:

type _opaque struct{
    f func()
}
func (o _opaque) M() { o.f() }

type I interface{ M() }
func main() {
    x := I(_opaque{nil})
    fmt.Println(x.M == nil) // prints "false"
    x.M() // panics
}

That is,

  1. The interface-literal should always have exactly the same method-set, regardless of whether any of its methods are initialized to nil.
  2. Any method-expression of the resulting value should always evaluate to a non-nil func value of appropriate type, as it currently always does.
  3. Calling a method which was initialized to nil should panic.

IMO these semantics are well-understood and fit well into the rest of the language. I'd rather have a cohesive language with some unsafe cases, than one with corners and behaviors which are hard to predict based on knowledge of the rest of the language. Go is a language with nil-panics. For better or for worse, that's part of its character.

AIUI, @smasher164 disagrees with point 2, though?

@DeedleFake

FWIW I think a decent compromise with the syntax you suggested here would be to require an interface-literal to always initialize all methods explicitly. I would argue this gives us exactly the same safety-guarantees, given that

// DeedleFake syntax
x := type{
    Read(p []byte) (int, error) { return somePackageScopedFunction(p) }
    Write(p []byte) (int, error) { return someFuncVariable(p) }
}

would be exactly equivalent to

x := io.Reader{
    Read: somePackageScopedFunction,
    Write: someFuncVariable,
}

At least under the assumption that method expressions evaluate to non-nil, even if initialized to nil. I would argue that if we adopted your syntax and people wanted to re-use pre-existing funcs, they would write exactly the code above, getting the same panics if those pre-existing funcs are nil as the code below. So the only "nil-safety" benefit the syntax provides is preventing people from forgetting to initialize one of the methods. Seems better to stick with the shorter syntax then and just add that check on top of it.

One benefit of struct-fields being allowed to be omitted (defaulting to zero) is that it allows to backwards-compatibly add fields to a struct. But that's not a concern for interfaces, for which adding a method is always a breaking change.

@zephyrtronium

Given the "narrowing" example: […] Does the type assertion fn().(io) succeed?

AIUI the intention of the narrowing-example is that it would not.

smasher164 commented 2 years ago

@Merovius It's been a few years since this proposal was created, and since then I've learned a lot more about its implications. I agree with you that the proposal in the top post, as it stands, doesn't capture this knowledge. Based on this discussion, I may provide a refinement of the original proposal. Thank you for your thorough analysis of it.

Merovius commented 2 years ago

I'm not sure that having an unexported type with no fields will be feasible to implement. How would the func values be referenced?

I don't understand the question. What func values? referenced by whom, for what purpose?

I'm not sure how that is? The way the literal is constructed shouldn't affect its runtime representation or how it can be accessed with reflect.

If I change

type ReadWriter interface {
    io.Reader
    io.Writer
}

to

type ReadWriter interface {
    Read([]byte) (int, error)
    Write([]byte) (int, error)
}

that is a backwards compatible change - the two types are literally identical, so there is no difference before and after. Under your proposed rule, this would be a breaking change. To me, that makes the types different. How to construct a type is part of its contract, changing that contract modifies the type.

I imagined an implementation strategy where you had one supertype with all fields. And when the runtime constructs the interface value, it makes an itable like

Say I have a function func F(io.Reader) io.Reader which does this methodset-dependent wrapping. What would

r := reflect.TypeOf(F(onlyAReader{}))
rw := reflect.TypeOf(F(someReadWriter{}))
fmt.Println(r == rw)

print? If it prints "false", then we need to either dynamically or statically construct different dynamic types for different method sets. If it prints "true", what would r.NumMethod() == rw.NumMethod() evaluate to?

It's not enough to say how we could fill the itable of an io.ReadWriter conditionally. The underlying type must have a method set and if that method set is different for different cases, these different cases must return different underlying types.

It's definitely a new precedent, but reasons for why one would want to dynamically construct an interface are mentioned here:

As I said, I fully understand the reasons and I'm sympathetic towards them. I want to be able to do that as well. However, "there are reasons to want to solve this problem" is not the same as "we should implement this specific solution". It's not necessary to fix this here and IMO it's the wrong place to fix it.

smasher164 commented 2 years ago

What func values? referenced by whom, for what purpose?

Your suggestion in an earlier comment was

For this proposal, the reflect.Kind should probably struct, with no fields and the requisite methods.

Invoking a method on an interface literal should invoke the function assigned to that method. If the unexported struct had no fields, where would it store a reference to the function assigned to the method?

But re-reading your comment, I assume you mean to present the unexported struct as having no fields. Underneath the hood, the implementation is free to reference the functions however it wants.

Under your proposed rule, this would be a breaking change.

From my perspective, the only difference between constructing a literal like

rw := ReadWriteSeekCloser{
    ReadWriteSeeker: f,
    Close: […]
}

and

rw := ReadWriteSeekCloser{
    Read: f.Read,
    Write: f.Write,
    Seek: f.Seek,
    Close: […]
}

is that the former source file must import the io package. The selector ReadWriteSeeker desugars into the set of method names { Read, Write, Seek }. The idea was for these two to be equivalent.

However, I now see another complication with this idea that would render it bogus. There is no way to disambiguate between a field name being a type name and a method name. For example,

type A interface {
    B()
}
type B interface {
    C()
}
a := A{B: ...} // Are we using the interface B to set A's fields, i.e. setting the method C, or simply setting the method B?

Because of this ambiguity, I agree that this part of the proposal should be dropped.

It's not enough to say how we could fill the itable of an io.ReadWriter conditionally. The underlying type must have a method set and if that method set is different for different cases, these different cases must return different underlying types.

Ah I see. Regardless of the implementation strategy, methodset-dependent wrapping implies that F(someReadWriter{}) has an underlying value that has more methods than onlyAReader. reflect.TypeOf is required to report its dynamic type, which in the presence of narrowing conversions, is a type with the subset of methods that were non-nil.

That being said, if we didn't go with option 3, wouldn't it be relatively cheap to dynamically construct this dynamic type? For instance, the cost of constructing an "anonymous interface type" in option 1 is directly proportional to the number of methods. When an itab is built from an interface literal, its _type field would be constructed based on its computed non-nil methods. This would be the underlying type used by an interface that wraps it.

It's not necessary to fix this here and IMO it's the wrong place to fix it.

I think I agree as well. The expression problem solution presented in Featherweight Go was far more elegant, and much more obvious in how it worked. Using nil values to do narrowing conversions is gross, IMO.

Merovius commented 2 years ago

If the unexported struct had no fields, where would it store a reference to the function assigned to the method?

I was imagining this similar to how closures work. But, FWIW, I also hedged with "if it's infeasible to implement this […]" :)

That being said, if we didn't go with option 3, wouldn't it be relatively cheap to dynamically construct this dynamic type?

I don't know. All I know is that after several years of talking about #4146, #16522 and similar issues, we still have no way to generate ~any types with methods using reflect and I remember interesting crashes from when we tried. And that we specifically avoided having runtime type generation in the generics design.

From my POV, it also seems "relatively cheap" to promote methods on embedded fields in reflect, for example. I don't know enough about the issues we've encountered there to be able to tell if they transfer here. But it seems questionable to me to assume they don't. If someone who does know enough wants to chime in, I'll gladly retract that as a difficulty.

Merovius commented 2 years ago

One thing that occurred to me is that a decent alternative to making the underlying type struct{} would be to make it e.g.

struct{
    _ func([]byte) (int, error) // Read
    _ func([]byte) (int, error) // Write
    _ func() error // Close
}

By using _, we circumvent any difficulty mapping names, the fields remain inaccessible and the resulting type is incomparable and we don't have to deal with the shenanigans of having an empty struct carry state.

It does still pose one question though: Presumably, interface{}{} would be valid under this proposal - it's an interface literal without any methods. What would be the value of that, though? It's certainly not a useful value, carrying neither data nor methods. But we should at least consider what it is. Under the idea above, it would translate into a struct{} - IMO that's a bit strange, as it's comparable, it seems strange for interface{}{} to be comparable if any other interface-literal isn't.

Just something to consider.

zephyrtronium commented 2 years ago

It does still pose one question though: Presumably, interface{}{} would be valid under this proposal - it's an interface literal without any methods. What would be the value of that, though? It's certainly not a useful value, carrying neither data nor methods. But we should at least consider what it is. Under the idea above, it would translate into a struct{} - IMO that's a bit strange, as it's comparable, it seems strange for interface{}{} to be comparable if any other interface-literal isn't.

Just something to consider.

I encountered this facet when I rummaged around in the compiler a couple weeks ago to try demoing this proposal and to explore what behaviors would be hard to implement. My initial approach was to avoid the question by saying, "interface literals must have keys, so interface{}{} is illegal because it has no keys." But it would be easy not to say that and allow interface{}{} as something like a shorthand for interface{}(struct{}{}). (I think the question of whether to allow unkeyed interface literals a la https://github.com/golang/go/issues/25860#issuecomment-920301632 is still undecided; requiring them was simply a way to skip writing a dozen lines or so.)

It would also be easy for the compiler to mark any type it generates as not comparable, no explanation needed. Considering any other interface literals would most likely not be comparable, as you say, it seems correct to do this for the interface{}{} case. Then it would be more like a shorthand for interface{}(struct{ _ [0]func() }{[0]func(){}}).

march1993 commented 2 years ago

I copied code of StructOf and made a InterfaceOf but it resulted in segmentation violation. Does anybody know why? https://stackoverflow.com/questions/72338352/golang-creating-struct-with-methods-using-reflect-results-in-segmentation-violat

griesemer commented 3 months ago

A quick update on this proposal: @cagedmantis has implemented a prototype (go.dev/cl/572835) for the competing proposal #47487. As mentioned in his post, he plans to work (as time permits) on a prototype for what is essentially this proposal.

Specifically, composite literals are extended such that they also accept interface literals:

CompositeLit  = LiteralType LiteralValue .
LiteralType   = StructType | ArrayType | "[" "..." "]" ElementType |
                SliceType | InterfaceType | MapType | TypeName [ TypeArgs ] .

(This grammar change matches exactly what was originally proposed with this issue.)

One of the things still to decide is whether all methods of the interface must be present in the literal. Some discussion (here, and here) on this has started on #47487. Let's move this discussion here where it belongs.

griesemer commented 3 months ago

As fine-tuning of interface literals, I suggest that we consider the following rule:

If an interface contains exactly one method, the method name in the literal may be omitted.

For example, instead of writing io.Reader{ Read: myReadFunc } one could simply write: io.Reader{ myReadFunc }.

This rule would eliminate the syntactic size difference between interface literals for single-method interfaces (this proposal), and function conversions to single-method interfaces (#47487): the difference is only whether we write {}'s or ()'s.

It would also make such an interface literal invalid if the interface gets more methods. Note that this is no different from the behavior of struct literals that don't mention their field names.

(Apologies if somebody else already suggested the same idea - a cursory search through this proposal didn't show anything.)

Merovius commented 3 months ago

Is any{} valid?

zigo101 commented 3 months ago

Why not? It is similar to struct{}{}.

any{} == nil. (edit: it might be a good idea to disable nil as interface zero values later?)

Merovius commented 3 months ago

If we allow to not set all methods, then any{} == nil seems to imply to me that we should also have io.Reader{} == nil. Both seem strange to me, though. We are considering interface-literals to be values and storing any value in an interface should not yield nil

But if any{} != nil, then any{} would be a value without any practical use - except to say it is not nil. Which also seems strange to me.

If we do require all methods to be present, then any{} seems like a logical thing to be able to write (I'm specifying all methods, after all), but still, both any{} == nil and any{} != nil seem strange to me.

Basically, any{} has, in my opinion, strange implications. But disallowing it feels also strange to me. One way or another, I'd like the final proposal to make a definitive statement on whether or not it's allowed and what the semantics are.

[1] See also how storing a nil pointer in an interface yields a non-nil interface. Also note that under #47487, io.Reader((func([]byte) (int, error))(nil)) would be a valid, non-nil io.Reader, that panics when calling Read - just like if you store a nil http.HandlerFunc in an http.Handler.

adonovan commented 3 months ago

For consistency, any{} should be valid and should denote a value of an opaque type with no methods, in essence type noName struct{}. Consequently, any{} would be non-nil, just like any value denoted by an interface literal.

[Edit: didn't see @Merovius's intervening note, which says all this and more. I agree it would be not much use, but that's often the case with degenerate things, like a call to max(x) or append(x).]

zephyrtronium commented 3 months ago

Contrary to what I wrote in https://github.com/golang/go/issues/25860#issuecomment-958512633, the semantics I expect would be any{} != nil (implying that it is legal to write). In the sense that that's what I expect, I don't find it strange. It may or may not be useless; people tend to find uses for surprising things. (Edit: Consider interaction with encoding/json's omitempty option. The difference between "nil" and "non-nil but empty" might be the difference between a field being marshaled as {} or not at all, depending on reflection semantics.)

I do think this proposal would benefit a great deal from a formal proposal including syntax, semantics, and specific implications on reflection. It seems likely that will make identifying these kinds of questions easier. I suspect there are more details to think about.

And while thinking of details, if it's allowed, does any{} == any{} evaluate to true or false, or panic? Past discussion seems to lean toward all interface literals being non-comparable, but I don't think there has been a specific conclusion for this case yet.

adonovan commented 3 months ago

And while thinking of details, if it's allowed, does any{} == any{} evaluate to true or false, or panic? Past discussion seems to lean toward all interface literals being non-comparable, but I don't think there has been a specific conclusion for this case yet.

For all the non-trivial interface literal values, they will contain fields of kind func, so they will be non-comparable; comparison will result in a panic. For consistency it might be best to define that even the empty interface literal is non-comparable; e.g. not like struct{} but like struct { _ [0]func() }.

zigo101 commented 3 months ago

I re-read the proposal. It looks this is just a syntax sugar, not a fundamental change. I have no opinions on this proposal. Just a mention that wrapping a two-plus-field struct value in an interface has a much higher cost than wrapping one-filed struct value. Probably not worth considering it.

adonovan commented 3 months ago

Just a mention that wrapping a two-plus-field struct value in an interface has a much higher cost than wrapping one-filed struct value.

That's not a necessary consequence. In the common case, the func values provided for each interface method are likely to be func literals, all closing over the same set of variables. A smart compiler could (as you say) desugar the code into a package-level named struct type with a fresh name, and one field per closed-over variable, plus a set of methods on that named type. The cost of the allocation is then determined by whether the closure is empty or not, just as with a func literal.

griesemer commented 3 months ago

A possible implementation of this proposal can be seen as a source-to-source transformation: given an interface I with n methods, for each interface literal I{...}, there's a new dummy struct type D, with a type name D that is inaccessible to the user, with n fields and n methods. The struct fields are of func type, one for each method of I of matching signature, and the struct method signatures are matching corresponding methods of I and they call the respective struct field.

Here's a concrete example (playground):

type I interface {
    m1(x int)
    m2() string
    // ...
}

// A new, user-inaccessible dummy struct is created for each interface literal.
type dummy struct {
    m1_ func(x int)
    m2_ func() string
    // ...
}

func (d dummy) m1(x int)   { d.m1_(x) }
func (d dummy) m2() string { return d.m2_() }

func m1(x int) { println("m1 func called with", x) }

func main() {
    // Interface literal:
    // x := I{m1: m1, m2: func() string {return "m2 func literal"}}

    // Translation of interface literal:
    x := I(dummy{m1_: m1, m2_: func() string { return "m2 func literal called" }})

    // Use of the interface:
    x.m1(42)
    println(x.m2())
}

If we accept this source-to-source translation, it informs various questions:

1) Not providing a method is equivalent to keeping the respective struct field zero value, which is nil. Calling that method through the interface leads to a panic (playground). 2) any{} is permitted (playground). 3) Comparing any{} == nil is false (playground). 4) Comparing any{} == any{} is false (playground): each interface literal has its own user-inaccessible type. 5) For x := any{}, comparing x == x is true as one would expect (playground). 6) reflect related questions may also be possibly answered by this implementation (I haven't thought that through in detail).

The source-to-source translation makes interface literals a form of syntactic sugar, very much like any of the other literals. Looking at it this way likely prevents us from coming up with semantics that may be contradictory to existing Go because the semantics is implied by existing Go.

A Go compiler may not go through the actual source-to-source transformation (though it could), but choose a more direct implementation as long as it matches the semantics.

zigo101 commented 3 months ago

@adonovan honestly, I don't very get your last reply.

Maybe I over-worried about it. Is it possible that the implementation struct type is a distinct zero-size struct (with an incomparable zero-size field) and the functions list is maintained outside of the struct values?

ChrisHines commented 3 months ago

@griesemer I like your construction, but why it is important for "each interface literal has its own user-inaccessible type"? What does that add over having each interface type used in a literal have its own type?

I ask because when hand writing mocks/fakes that use this pattern today I usually only make one type per interface I am mocking/faking. Obviously that saves me effort that the compiler doesn't care about as much, but does making each literal generate a new type have a specific advantage?