-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Description
When we want to retrieve an element that matches a given condition from a slice, with the current design of the slices
package we use IndexFunc
:
i := slices.IndexFunc(slice, func (value T) bool {
// a condition
})
if i == -1 {
return
}
elem := slice[i] // or use it as slice[i]
The issue with this solution is that many times we do not need the index of the value. In those cases, we either end up with a variable declaration and an otherwise unused index variable that both hamper readability, or we name the returned index suggestively, which results in lengthy names and clumsy access to the element. For example:
var input []string
indexInputWithFooPrefix := slices.IndexFunc(input, func (value string) bool {
return strings.HasPrefix(value, "foo")
}
if indexInputWithFooPrefix == -1 {
// not found
}
processInput(input[indexInputWithFooPrefix])
Both patterns can get really ugly when having to find elements that match other conditions, too. Either reuse an index variable, or name all of them accordingly. Besides, the i == -1
(or i >= 0
, if preferred) check adds unnecessary cognitive load.
To solve these readability and usability concerns, I propose adding a Find
function to the slices
package, with the following declaration:
func Find[E any](s []E, f func(T) bool) (T, bool)
The examples above become much more easier to reason about:
var input []string
inputWithFooPrefix, found := slices.Find(input, func (value string) bool {
return strings.HasPrefix(value, "foo")
}
if !found {
// not found
}
// do whatever
The found
variable can safely be reused, as demonstrated by our usage of the "comma, ok" idiom. It could also be named ok
- this way, Find
would fit together with map lookups and type assertions, which would make the language more uniform.
Here are some examples from open-source repositories showing how Find
could simplify code:
Before:
func (r *router) findRoute(name string) *route {
for _, route := range r.getRoutes() {
if route.name == name {
return route
}
}
return nil
}
After:
func (r *router) findRoute(name string) *route {
route, _ := slices.Find(r.getRoutes(), func (rt *route) bool {
return rt.name == name
})
return route
}
Before:
func GetDebuggerPath() string {
// --snipped--
for _, debugger := range list {
if strings.Contains(debugger.Url, "spotify") {
return debugger.WebSocketDebuggerUrl
}
}
return ""
}
After:
func GetDebuggerPath() string {
// --snipped--
debugger, _ := slices.Find(list, func (d debugger) bool {
return strings.Contains(d.Url, "spotify")
})
return debugger.WebSocketDebuggerUrl
}
Before:
func dbGetArticle(id string) (*Article, error) {
for _, a := range articles {
if a.ID == id {
return a, nil
}
}
return nil, errors.New("article not found.")
}
After:
func dbGetArticle(id string) (*Article, error) {
article, found := slices.Find(articles, func (a *Article) bool {
return a.ID == id
})
if !found {
return nil, errors.New("article not found.")
}
return article, nil
}
In this case, I would have written the function like this:
func dbGetArticle(id string) (*Article, bool) {
return slices.Find(articles, func (a *article) bool {
return a.ID == id
})
}
and create the error message at the call site. This is actually very similar to the use case that made me write this proposal: in our codebase we have a Store
interface with many FindBy*
methods, which return the type and a boolean indicated whether the value was found or not.
Before:
func (c *Cron) Entry(id EntryID) Entry {
for _, entry := range c.Entries() {
if id == entry.ID {
return entry
}
}
return Entry{}
}
After:
func (c *Cron) Entry(id EntryID) Entry {
entry, _ := slices.Find(c.Entries(), func (e Entry) bool {
return e.ID == id
})
return entry
}
We see that in most cases the zero value is actually helpful, which results in the found check being completely elided, in the same way we don't always check the ok
value when using maps. IndexFunc
does not give us this benefit - indexing with -1 is an obvious panic, so we always have to check the index. Having the Find
function could even potentially eliminate all these helper methods shown here - using Find
directly in code is pleasant and concise.
Adding this function would bring us in line with languages like Scala, Rust and Javascript, too.
Note: This is not similar to #50340, as that proposal modifies the existing BinarySearch
functions.