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 · 2 comments

Comments

@smasher164
Copy link
Member

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants
You can’t perform that action at this time.