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: interface literals #25860

Open
smasher164 opened this issue Jun 13, 2018 · 6 comments
Open

proposal: Go 2: interface literals #25860

smasher164 opened this issue Jun 13, 2018 · 6 comments

Comments

@smasher164
Copy link
Member

@smasher164 smasher164 commented Jun 13, 2018

Filing this for completeness sake, since it was mentioned in #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 #21670 (comment).

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 #21670 (comment)):

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 .
@gopherbot gopherbot added this to the Proposal milestone Jun 13, 2018
@gopherbot gopherbot added the Proposal label Jun 13, 2018
@oiooj oiooj added the Go2 label Jun 13, 2018
@gbbr gbbr changed the title Proposal: Go 2 -- Interface Literals Proposal: Go 2: Interface Literals Jun 13, 2018
@gbbr gbbr changed the title Proposal: Go 2: Interface Literals proposal: Go 2: Interface Literals Jun 13, 2018
@tomvanwoow
Copy link

@tomvanwoow tomvanwoow commented Jul 27, 2018

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

@smasher164 smasher164 commented Jul 28, 2018

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

@smasher164 smasher164 commented Nov 6, 2019

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

@carnott-snap carnott-snap commented Mar 12, 2020

Can this be added to #33892 for review?

@smasher164
Copy link
Member Author

@smasher164 smasher164 commented Mar 12, 2020

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Mar 12, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
6 participants
You can’t perform that action at this time.