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 [WIP]: interface-slices #30391

Closed
seebs opened this issue Feb 25, 2019 · 6 comments
Closed

proposal [WIP]: interface-slices #30391

seebs opened this issue Feb 25, 2019 · 6 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@seebs
Copy link
Contributor

seebs commented Feb 25, 2019

(See some similarities with #26056)

What version of Go are you using (go version)?

N/A

Does this issue reproduce with the latest release?

N/A

What operating system and processor architecture are you using (go env)?

N/A

What did you do?

Wanted to write code using slices of possibly-unknown types, efficiently.

What did you expect to see?

A way to get some of the utility of interfaces, without the massive allocator overhead.

What did you see instead?

If you have a slice of things which satisfy an interface, and you want a slice of that interface, you have to manually rebox the items. A slice of 1M interfaces contains 1M pointers. This is horrible.

What I don't have: any idea how to produce a workable/clean syntax.

What I do have: A description of a thing that I think would be very useful for developers confronted with this.

The basic concept: An interface-slice type, which is distinct from a slice-of-interfaces. An interface-slice is assignable from a slice of objects which satisfy its interface. There is implicit boxing/unboxing involved, but instead of boxing/unboxing each item to create the slice, boxing/unboxing happens only at the time when you're trying to access an item.

So, for instance:

type Strint int

func (s Strint) String() string {
    return fmt.Sprintf("~%d~", s)
}

var SliceOfStringer Stringer[] // this syntax is not valid, bear with me
var strints []Strint = []Strint{1, 2, 3}

SliceOfStringer = strints // note the lack of boxing!

for _, v := range SliceOfStringer {
    // here, v is a Stringer interface; on each iteration of the loop, it holds one
    // of the values from strints
    fmt.Printf("%v\n", v)
}

if slice, ok := SliceOfStringer.([]Strint); ok {
    // here, slice will contain the original slice, and no further boxing/unboxing is needed
}

Why do I think this would be useful? Because code that deals with large numbers of objects, which might be of multiple types, is currently allocating millions of interfaces. In many cases, it would be adequately served by a handful of interface-slices wrapping slices of the underlying concrete objects. If the interfaces were being used only for brief inner loops, many of them might never escape to the heap, and thus, not require allocation at all.

The best we can do right now is to use interface{}, and type assertions. You can do okay with this, but you lose all the documentary power of being able to specify that, rather than any arbitrary object, you are looking only for (1) slices, of (2) things that satisfy a particular interface. You also can't handle a new arbitrary item which satisfies your interface. For instance, consider what would happen if you tried to do this for Stringer; how do you check your interface{} against every type that implements Stringer without complex and expensive reflect checks? If you could instead get a slice known to be a slice of things implementing Stringer, and could iterate through it as if it were a slice of Stringer interfaces, life would be better.

I don't have a good handle of a syntax for this. interface[]{} almost works, but it can't be used for a named interface type. If this were just added as a new rule for []Interface (permitting assignments from compatible-ish slices), it would probably impose costs on any and all code using []Interface already, and possibly introduce very strange bugs. So I'm completely stumped on syntax.

This doesn't seem to be quite the same case as generics; the goal isn't to generate templated code for each specific instance of a slice satisfying the interface, but to have runtime interface-handling code which better handles the fairly common case of slices of a concrete type.

@neild
Copy link
Contributor

neild commented Feb 25, 2019

You can do okay with this, but you lose all the documentary power of being able to specify that, rather than any arbitrary object, you are looking only for (1) slices, of (2) things that satisfy a particular interface.

One thing that will work in some situations is to push the interface definition up a level. If you want a list of things that can be converted to strings:

type StringList interface {
  // String returns the n'th string.
  String(n int) string
}

This can be trivially implemented by []string, []fmt.Stringer, etc.

FWIW, on the one occasion that I've really wanted to convert a slice of a concrete type to a slice of an interface type, I ended up using reflection.

These two approaches (push the list-ness into the interface, or use reflection) are also embodied by sort.Sort and sort.Slice.

@bcmills bcmills added LanguageChange v2 A language change or incompatible library change Proposal labels Feb 25, 2019
@bcmills bcmills modified the milestones: Go2, Proposal Feb 25, 2019
@bcmills
Copy link
Contributor

bcmills commented Feb 25, 2019

This doesn't seem to be quite the same case as generics; the goal isn't to generate templated code for each specific instance of a slice satisfying the interface, but to have runtime interface-handling code which better handles the fairly common case of slices of a concrete type.

Note that the current proposals for generics do not require templated code for each instance either.

Templated code is an implementation strategy; are there semantic differences between this an generic containers?

@seebs
Copy link
Contributor Author

seebs commented Feb 25, 2019

I'm not sure. I periodically conclude that it's clearly equivalent to a generic container (with some restrictions on the generic type), and at other times conclude that it's definitely not. I feel like the interface boxing/unboxing is a significant difference from generic code, but I'm not sure it's actually useful for anything if generics are available.

On the other hand, this would solve many of the things for which I have wanted generics, without requiring nearly the generalized complexity of generics.

@beoran
Copy link

beoran commented Mar 15, 2019

Why not enhance the built in copy function to allow copies between array of struct and array of interface if the structs implements the interface? Like that, this common use case could be covered without any new Go syntax.

@seebs
Copy link
Contributor Author

seebs commented Mar 15, 2019

I don't think that would actually cover this use case at all, because that would create an array of pointers. A big part of the goal here is to get away from the huge number of pointers that end up being needed for slices of interfaces.

@ianlancetaylor
Copy link
Contributor

Thanks for the suggestion. I agree with @bcmills that a generics proposal ought to address the same issues being described here: the ability to work with a slice of some generically specified type. I'm going to close this proposal in the hopes that we will adopt some form of generics. If we decide against that, we can revisit this.

@golang golang locked and limited conversation to collaborators Mar 18, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

6 participants