- Add TryMap, Filter, Reduce, KeyBy, GroupBy, Uniq, UniqBy, Partition and Find, plus the Set type built with SetOf (Has, Add, Slice). - Add unit tests at 100% statement coverage and a runnable example for every function and Set method. - Complete the module README: contracts, use cases, Pluck rationale, standard-library pointers and the full API reference. - docs/architecture/introduction.md: add a Utilities row for sieve. - docs/setup/coming-from-wintercms.md: map Laravel collection methods to sieve and narrow the not-provided row to the fluent collection object.
7.7 KiB
sieve
Generic, dependency-free collection helpers that the standard library slices and maps packages lack: mapping, filtering, reducing, keying, grouping, deduplicating, partitioning and finding, plus a small set type.
import "git.golem15.com/golem15/summercms/modules/sieve"
Name: A sand sieve: you shake a collection through it and keep what you need; map, filter and reduce for slices and maps.
Overview
sieve gives plain Go slices the collection operations that WinterCMS code reaches for on Laravel collections, the objects that Eloquent queries return (map, filter, reduce, keyBy, groupBy, unique, partition, first). It is a set of generic functions rather than a fluent collection object, so type inference works from the slice argument and no wrapper type leaks into your models.
Four contracts hold for every function:
- It is eager and never modifies its input.
- Its result never shares a backing array with the input, so changing or appending to a result leaves the caller's slice unchanged.
- Callbacks run once per element, in input order.
sieve.TryMapandsieve.Findstop early. - Nil or empty input yields a non-nil empty slice or map, so
encoding/jsonwrites[]or{}rather thannull.
Role in the framework
A dependency-free leaf with no imports, so any module, plugin or application can use it without import cycles. It complements the standard slices and maps packages rather than wrapping them: nothing here duplicates a function those packages already provide.
Use cases:
- Shape API responses from models, for example turning a slice of records into a slice of response structs.
- Key records by ID to join two result sets without a nested loop.
- Group records by a field, such as posts by author.
- Deduplicate IDs before an
INquery. - Check set membership, such as whether an ID is in an allow list.
Features
sieve.Mapapplies a function to every element; with a field accessor it is the pluck operation.sieve.TryMapmaps with a function that can fail and stops at the first error, returning it unchanged.sieve.Filterkeeps the elements that match a predicate, andsieve.Partitionsplits a slice into the matches and the rest.sieve.Reducefolds a slice from left to right into an accumulator of any type.sieve.KeyBybuilds a map from a key to an element (later items win), andsieve.GroupBybuilds a map from a key to the elements with that key, in input order.sieve.Uniqremoves duplicates of comparable values, andsieve.UniqByremoves elements whose key was already seen; both keep the first occurrence.sieve.Findreturns the first element that matches a predicate, with a found flag.sieve.Set, built withsieve.SetOf, is a plain map-backed set withsieve.Set.Has,sieve.Set.Addandsieve.Set.Slice.- Functions that keep the element type (
sieve.Filter,sieve.Uniq,sieve.UniqBy,sieve.Partitionand the groups ofsieve.GroupBy) return the caller's named slice type.
Usage
package blog
import (
"fmt"
"strconv"
"git.golem15.com/golem15/summercms/modules/sieve"
)
type Post struct {
ID int
Author string
Title string
Published bool
}
type PostResponse struct {
ID int `json:"id"`
Title string `json:"title"`
}
func shape(posts []Post, rawIDs []string) error {
// Keep the published posts and turn them into response structs.
published := sieve.Filter(posts, func(p Post) bool { return p.Published })
body := sieve.Map(published, func(p Post) PostResponse {
return PostResponse{ID: p.ID, Title: p.Title}
})
// Key by ID for lookups, group by author, keep one post per author.
byID := sieve.KeyBy(posts, func(p Post) int { return p.ID })
byAuthor := sieve.GroupBy(posts, func(p Post) string { return p.Author })
firstPerAuthor := sieve.UniqBy(posts, func(p Post) string { return p.Author })
// Split drafts from published posts, and find the first draft.
live, drafts := sieve.Partition(posts, func(p Post) bool { return p.Published })
firstDraft, found := sieve.Find(posts, func(p Post) bool { return !p.Published })
// Parse IDs from a query string, stopping at the first bad one.
ids, err := sieve.TryMap(rawIDs, strconv.Atoi)
if err != nil {
return fmt.Errorf("blog: bad post id: %w", err)
}
wanted := sieve.SetOf(ids...)
fmt.Println(len(body), len(byID), len(byAuthor), len(firstPerAuthor),
len(live), len(drafts), firstDraft.Title, found, wanted.Has(1))
return nil
}
There is no Pluck. Laravel's pluck('title') takes a field name as a string, which in Go would need reflection and would give up compile-time type checking. The typed equivalent is sieve.Map(posts, func(p Post) string { return p.Title }).
What the standard library already covers:
slices.Chunksplits a slice into chunks lazily; useslices.Collect(slices.Chunk(s, n))for an eager[][]T.slices.Containsandslices.IndexFunctest for and locate elements.slices.Sortandslices.SortFuncsort in place.slices.Minandslices.Maxreturn the smallest and largest element.slices.Compactremoves consecutive duplicates only, in place.slices.DeleteFuncfilters in place.maps.Keysandmaps.Valuesiterate a map; wrap them inslices.Sortedfor ordered output.
API reference
| Identifier | Description |
|---|---|
sieve.Map |
Returns f applied to each element, in input order. Nil input gives a non-nil empty slice. |
sieve.TryMap |
Like Map with a fallible f. Stops at the first error and returns nil with that error unchanged. Nil input gives a non-nil empty slice and a nil error. |
sieve.Filter |
Returns the elements for which keep is true, in input order, as the caller's slice type. Nil input or no matches gives a non-nil empty slice. |
sieve.Reduce |
Folds the elements from left to right, starting from init. The accumulator type may differ from the element type. Nil input returns init. |
sieve.KeyBy |
Returns a map from key to element; when two elements share a key, the later one wins. Nil input gives a non-nil empty map. |
sieve.GroupBy |
Returns a map from key to the elements with that key, each group in input order and of the caller's slice type. Nil input gives a non-nil empty map. |
sieve.Uniq |
Returns the elements without duplicates, keeping the first occurrence, in input order. Nil input gives a non-nil empty slice. |
sieve.UniqBy |
Returns the elements whose key was not seen before, keeping the first occurrence of each key, in input order. Nil input gives a non-nil empty slice. |
sieve.Partition |
Returns (matched, rest), both in input order and both non-nil, even for nil input. |
sieve.Find |
Returns the first matching element and true; the predicate is not called after the first match. No match or nil input gives the zero value and false. |
sieve.Set |
A set backed by map[T]struct{}; len, range, delete and maps.Keys work on it directly. Has and Slice work on a nil Set; Add on a nil Set panics. |
sieve.SetOf |
Returns a new set holding the given items without duplicates. With no arguments it returns an empty, non-nil set that Add can use. |
sieve.Set.Has |
Reports whether a value is in the set. A nil Set reports false. |
sieve.Set.Add |
Adds one or more values; values already present are ignored. Panics on a nil Set. |
sieve.Set.Slice |
Returns each element exactly once, in unspecified order. Non-nil even for an empty or nil Set. |
Dependencies
- SummerCMS modules: none.
- Third-party: none.
- Standard library: none (the package has no imports).
Testing
go test ./modules/sieve/...
The tests are in-memory and need no external services. They include runnable examples for every function and sieve.Set method, and cover every statement.