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: handle type conversion of interface-slices similar to interfaces #38753

Closed
glooms opened this issue Apr 29, 2020 · 19 comments
Closed
Labels
Milestone

Comments

@glooms
Copy link

glooms commented Apr 29, 2020

Abstract

Make type conversion of interface slices behave like conversion of the underlying interfaces. This would for example simplify the use of variadic functions that take interfaces.

The argument can also be made for map and array types, but it's especially nice for slices.

Background

When trying to create a "MultiReadCloser", as a wrapper over io.MultiReader I encountered a compilation error, something like this:

package main

import (
	"io"
)

type NopReadCloser struct {}

func (NopReadCloser) Read(p []byte) (int, error) {return 0, nil}

func (NopReadCloser) Close() error {return nil}

func main() {
	nop := NopReadCloser{}
	readClosers := []io.ReadCloser{nop}
	
	// Since an io.ReadCloser also is an io.Reader, we can easily convert 
	// it to only an io.Reader.
	reader := io.MultiReader(readClosers[0])
	
	// But a slice of io.ReadClosers cannot be converted to a slice of io.Readers.
	reader = io.MultiReader(readClosers...)	
}
./prog.go:21:25: cannot use readClosers (type []io.ReadCloser) as type []io.Reader in argument to io.MultiReader

Playground: https://play.golang.org/p/R8xY0d1aTWO

This is a bit confusing, since a single interface can be converted, but not the whole slice.
It is even more confusing due to the fact that a new slice of the desired interface has to be created for this to work, which seems like a fairly expensive operation, compared to just passing the pointer. But, maybe it's an expensive operation for the compiler as well, I don't know.

Proposal

Allow type conversion for slices containing interfaces, e.g. from []io.ReaderCloser to []io.Reader or []io.Closer, just as this would be allowed for the interfaces. I think this would be very convenient, specially in variadic functions.

Rationale

See the background section and if that doesn't convince you, I formally make the 🦆-typing argument: if a group of ducks quack they are ducks.

@gopherbot gopherbot added this to the Proposal milestone Apr 29, 2020
@randall77
Copy link
Contributor

var x []io.ReaderCloser = ...
y := ([]io.Reader)(x)
y[0] = ... something that is a Reader, but not a Closer ...

What is now in x[0]? Can it be closed?

@OneOfOne
Copy link
Contributor

x[0] is still an io.ReadCloser, y[0] is an io.Reader that can be type asserted to io.ReadCloser inside the calling function (but it doesn't have to)

@randall77
Copy link
Contributor

x[0] is still an io.ReadCloser

But it isn't, I just assigned something that doesn't have a Close method to x[0] (via y[0]).

Or are you suggesting that the cast make a copy of the slice's backing store?

@OneOfOne
Copy link
Contributor

OneOfOne commented Apr 29, 2020

I'd assume it'd have the same logic as:

var rc io.ReadCloser
var r io.Reader = rc

rc is still io.ReadCloser, r refers to that rc but it is an io.Reader.

Pretty much I'd expect this to happen under the hood, with compiler protection against invalid conversations of course.

https://play.golang.org/p/4boZqBvZIVJ

@ianlancetaylor
Copy link
Contributor

FAQs on this topic:

https://golang.org/doc/faq#convert_slice_of_interface
https://golang.org/doc/faq#convert_slice_with_same_underlying_type

@OneOfOne
Copy link
Contributor

I'd say the OP should have tagged this as go2, those FAQs apply to go1.

@randall77
Copy link
Contributor

var rc io.ReadCloser
var r io.Reader = rc
rc is still io.ReadCloser, r refers to that rc but it is an io.Reader.

That works fine because the assignment at line 2 makes a copy. The value contained in rc is never modified.

The situation with slices is more like this:

var rc io.ReadCloser
rcp := &rc
rp := (*io.Reader)(rcp)
*rp = ... something that is a Reader, but not a Closer ...

Now what does rc contain?

To be problematic, you need to be able to write an io.Reader to a slot that originally contained an io.ReadCloser.

@ianlancetaylor
Copy link
Contributor

@OneOfOne Those FAQs apply to Go 2 as well. They explain why the language is as it is, and there is no reason to think that would change in Go 2. (Though I agree that this is a Go 2 proposal.)

@ianlancetaylor ianlancetaylor changed the title proposal: handle type conversion of interface-slices similar to interfaces proposal: Go 2: handle type conversion of interface-slices similar to interfaces Apr 30, 2020
@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Apr 30, 2020
@glooms
Copy link
Author

glooms commented Apr 30, 2020

To be problematic, you need to be able to write an io.Reader to a slot that originally contained an io.ReadCloser.

That is a very good point, hadn't thought about that. I guess it could be solved by panicking in this case instead of throwing a compiler error, but I'm not sure it's better.

@bcmills
Copy link
Contributor

bcmills commented Apr 30, 2020

Java throws ArrayStoreException. Nonetheless, I would consider covariant arrays to be one of Java's biggest mistakes: it reduces compile-time type safety, and requires extra run-time overhead to compensate. It also complicates both variable-length argument passing and generics.

@networkimprov
Copy link

What you want is copy() or append() between slices of compatible interfaces.

See also #38385

@ianlancetaylor
Copy link
Contributor

Based on the discussion above, this is a likely decline. Leaving open for four weeks for final comments.

@geraldss
Copy link

geraldss commented May 28, 2020

I would like to echo @networkimprov and suggest limiting this proposal to copy() and append(). These functions do not write mis-typed values to the destination or source slice, so the following can be safe:

type Base interface {
        ...
}
type Specific interface {
        Base
        ...
}

func AppendMixed(dest []Base, src []Specific) (res []Base) {
        return append(dest, src...)
}

Currently, this does not compile and instead requires a loop.

@networkimprov
Copy link

cc @griesemer for thoughts re copy() and append() between compatible interface slices...

@ianlancetaylor
Copy link
Contributor

Right now copy and append only have one special rule: they permit a string to be copied or appended to a []byte. We'd rather not add additional special rules for them.

In general, other than string to []byte exception, it seems best to consider copy and append as generic functions that happen to be builtin to the language. We don't want to introduce additional conversion rules for them.

Still a likely decline. Leaving open for another week.

-- for @golang/proposal-review

@geraldss
Copy link

geraldss commented Jun 2, 2020

Alternatively, the compiler can recognize the loop and convert it to the equivalent of memcpy. For any loop of copy or append of interfaces.

@ianlancetaylor
Copy link
Contributor

In general converting from one interface type to another requires changing the value to use a different method table. It can't be implemented as memcpy.

@glooms
Copy link
Author

glooms commented Jun 29, 2020

I think that the arguments against this (hasty) proposal are good. Feel free to close the issue :)

@ianlancetaylor
Copy link
Contributor

No change in consensus.

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

No branches or pull requests

8 participants