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
slices: new package to provide generic slice functions #45955
Comments
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
In general I think a I have two concerns about the list of functions here:
With these ideas in mind, I'd divide this list of functions into three groups. Clearly worthwhile in a v1 of
Borderline:
Not worth it:
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Why not Anyways this package would be a welcome addition because its a pain (boilerplate wise) to implement common data structures in Go compared to C++ and this would slightly reduce that boilerplate |
Should this new package mention https://github.com/golang/go/wiki/SliceTricks in its documentation? Should SliceTricks document the new package? How does this wiki page fit into the revised ecosystem? |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
One think to point out is that the Personally, I'm a bit concerned about I'd be less concerned if there was the equivalent thing for sorted slices as well, so that people at least be made to think about it. Though that would probably live in |
Would flatten and flatMap also be included? I can think of cases where a map would produce a slice of slices and that might need flattening |
I think this is a good idea, and it opens the door to including more functions by default in the future. Functions like these are going to either be provided by the standard library, or we'll end up with multiple "third party" libraries which do these things all in their own way, which will be used by a ton of projects anyway. Especially when people migrate from other languages (Java, C#, ..) they will look for such functions. Providing this out of the box would be in-line with how Go provides other frequently used constructs natively (like handling json data). |
Not obvious what would be the behavior in case where
I wonder it's might worth to provide some interface that would return a lazily-evaluated value here? As the chaining of Map and Filter is very common in Java Stream API.
I don't agree with usage of panic here. |
These seem like they would make our lives slightly easier, but I thought one of the philosophies of Go was that operations not in |
One slice operation I've found myself writing frequently is “return the same slice with its cap reduced to length”. #38081 was declined partially on the grounds that it could be written more clearly as a generic function (#38081 (comment)). I think such a function belongs in the |
I agree with @cespare that some of the functional APIs here seem premature, especially given the lack of a concise lambda. I would add that functional languages tend to rely heavily on covariance, which Go lacks. That makes functions like On the other hand, |
If you're going to do map/filter/reduce doesn't it make more sense to design a more generic interface first, and then implement it for slices? There are a lot more datastructures than slices that could benefit from such a thing. |
I agree with @sluongng that the behavior of I think a For increasing a length, I wonder if it would be clearer to provide a function that increases the capacity instead, analogous to // Grow returns s with capacity at least c.
// If cap(s) >= c already, Grow returns s as-is.
func Grow[T any](s []T, c int) []T Then, increasing the length of the slice is trivial: s := slices.Grow(s, n)[:n] That might also address @mdempsky's use case in proposal #24204 (see #24204 (comment)), since “allocate a new s := slices.Grow([]T{}, c) |
The code I was writing yesterday would have benefited from the existence of slices.Grow. I am also strongly in favor of the Append idiom rather than InPlace. Append is already used throughout the standard library, whereas InPlace has no current uses. |
one thing I have found useful on more than one occasion from JS's
|
The If we make func Insert[T any](s []T, i int, v ...T) []T which seems like it handily covers the use-cases for both the proposed |
I think the signature for That being the case, I think a function named // RemoveN removes n elements from s starting at index i, returning the modified slice.
// This panics if i < 0 || n < 0 || i+n > len(s).
// This modifies the contents of the slice s; it does not create a new slice.
func RemoveN[T any][s []T, i, n int) []T (The |
@mawr-bf2 You’re making such absolute statements, as if they aren’t just opinions. Converting to a Set is not always the right thing to do before checking for whether an item is contained within a slice. It is an expensive operation, relatively speaking, and you seem to dramatically underestimate how fast modern processors are when they’re working entirely from local cache without going out to main memory. The standard library provides many tools that should not be used by everyone, but are useful to some. It’s already opinionated enough in my opinion… I wish that it had even more “obvious” functions, even if they aren’t all commonly used. We don’t even have a string reverse function. |
Great point, this essentially boils down to badly-written/legacy third-party code support. Converting a slice to a set every time just to be able to use Now, keep in mind that we're not discussing the overall usefulness of Another thing to remember is that once added, To sum up, is it worth it to permanently add a semantically wrong function that will forever lead programmers to writing bad code just for the sake of microoptimization and supporting incorrectly written/legacy code that will eventually fade away? |
Apologies, I'm not a skilled writer and I'm aware I may come off as blunt.
Correct, though note that I never said anything to the contrary.
Reversing a string is actually a great example of an operation that just does not make sense, so it's actually good that it's not included ;) |
I agree it's an indicator, but I'd like to emphasize that it's specifically what I'd call a "mechanical indicator" - it's based purely on a simple metric. As an example, consider what used to be a common practice of using the amount of lines of code committed in a day as an indicator of programmer performance. We know now that while it is a signal, it's a weak one. In order to acccurately assess a programmer's performance one must look at the actual code they've outputted - how buggy is it? etc. The same principle applies here. There may be many reasons why the
To sum up, in order to find out the actual reasons for the prevalence of Personally, I'm too exhausted at this point to take up this task, or continue discussing further. I've made my points and do not want to spam up the thread further. I hope whatever the decision made, it ends up being the right one. Cheers. |
@Mawr-BF2 — By no means is this a hill I want to die on, but I do want to present an opposing view on some of the claims you made.
I cannot agree with that assertion in large part because I do not think there is ever only one optimal data structure. The optimal data structure changes depending on whether or not a developer needs to focus on performance vs. maintainability and reduced complexity. For example, I am currently writing a search-replace tool for large MySQL dump files. There is an existing one available on GitHub that does almost exactly what the client needs that uses data structures that are a reasonably good semantic match for the problem, and yet it performs abysmally. It makes a large number of small memory allocations on the heap which results in the garbage collector working overtime and in general is just very inefficient. The code I am writing is significantly more complex than that open-source version, it's primary data structure is not semantic at all — it i a large pre-allocated
I appreciate and respect that that is your opinion. And I am sure there are others who share your opinion. However I have no idea how to tell if it is a minority or majority view. But let me share my opinion. Assuming that we could possible determine that an API is right vs wrong — which I illustrated above is not really applicable — my opinion is that if a functionality is commonly recreated by userland developers in a compatible fashion it would be better to be added to the stdlib (or rolled into the language) than to see constant reinvention of the same functionality across many different packages. Or, at Tower of Bable, so to speak. Said another way, while I appreciate that semantic purity can be good to consider when designing a stdlib I do not think it should be the only consideration. I am drawn to programming languages that are pragmatic and help me get work done, not ones that throw up barriers for ideological reasons. A function like
I am pretty sure that is all wishful thinking, and that what you consider to be badly-written code will never fade away, it will just re-emerge elsewhere. As long as there are developers new to programming and/or developers new to Go there will be "badly-written" and legacy third-party code. And that assumes that it is objectively possible to determine what is badly-written code. Hell, there are a lot of experienced developers who will continue to produce "badly-written" code when on time pressure, or because they just can't be bothered to put more effort into a given project than is absolutely required. So, in my summary, I ask the Go team to make our lives as developers easier. Don't make things more difficult for ideological reasons when practical experience shows there are benefits to specific cases. And given what I have seen, that is for the most part exactly what they have been doing. |
@Mawr-BF2 — One more thing. This reference explains why there cannot be a one-true data structure in a Go for many use-cases: |
It appears the generic Sort function uses the QuickSort algorithm, which is O(n^2). Are there plans for moving to an O(nlogn) algorithm, like Merge Sort or IntroSort? |
Despite the function name, it actually uses a hybrid sort that looks pretty similar to introsort at a glance, though I'm not too familiar with introsort. And while quick sort is worst case |
Just to confirm, is the time complexity of the generic sort still O(nlogn)? |
This may have been discussed before, but I'm wondering why the var slice []T
i := slices.IndexFunc(slice, func(t T) bool {
// a predicate
})
if i == -1 {
// not found
return
}
// found, do whatever
elem := slice[i] I believe it would be useful to have a func Find[E any](s []E, f func(E) bool) (E, bool) The boolean value indicates whether the element was found or not. |
|
I think the https://github.com/luraim/fun#map library has a good Map implementation. |
A search of go/src/cmd/compile/internal/syntax/testdata/slices.go Lines 9 to 35 in 83e9a97
|
I think this is a prime usecase for a generic iterator API. Because you ~never want to actually do that conversion, you want to pass a If I'm wrong and there are significant use-cases beyond that, I would retract that concern. Otherwise, I'd prefer if we solved generic iteration for the stdlib and made it easy to use that instead. |
I have had need of both map and filter functions a number of times in the last week. Its easy enough to write your own, but I would like to voice my support of adding map and filter to the slices package. |
Map and filter functions will be much, much more useful on more general iterators. I think that it's worth waiting for #54245 rather than adding only semi-useful functions to the That being said, this package is in |
Compare #54768 (slices.DeleteFunc) which I think would be useful even without having iterator support. |
|
I think there's a lot to be said for just counting lines/tokens/characters saved when evaluating a potential abstraction: ns = slices.Map(ns, func(n int) int {
return n * n
}) 3 lines, 22 tokens, 56 characters for i, n := range ns {
ns[i] = n * n
} 3 lines, 17 tokens, 41 characters An in-place map just doesn't pay for itself. You do get a savings of characters when you compare to making a new map, but then the question of efficiency vs an iterator comes into play. |
@carlmjohnson Not that I think these are useful metrics, but strs := slices.Map(ints, strconv.Itoa) 1 line, 12 tokens, 38 characters strs := make([]string, len(ints))
for i, n := range ns {
strs[i] = strconv.Itoa(n)
} 4 lines, 32 tokens, 89 characters. We can always cherry-pick examples that make our favorites look favorable. [edit] I just realized that you specifically tried to talk about an in-place |
I have opened a new proposal #57433 to move golang.org/x/exp/slices into the standard library. Closing this proposal as completed. Thanks for all the discussion. |
Note: Discussion is now at #47203.
This proposal is for use with #43651. We propose defining a new package,
slices
, that will provide functions that may be used with slices of any type. If this proposal is accepted, the new package will be included with the first release of Go that implements #43651 (we currently expect that that will be Go 1.18).This description below is focused on the API, not the implementation. In general the implementation will be straightforward. The description has been updated from the original proposal several times based on the discussion.
The text was updated successfully, but these errors were encountered: