Couldn’t you do …funcs like here: https://pkg.go.dev/slices#MaxFunc and then use the cmp package for primitive types and for structs you could use https://go.dev/ref/spec#Method_expressions
[email protected] schrieb am Donnerstag, 25. Juni 2026 um 03:16:30 UTC+2: > *TL;DR:* Go 1.27's generic methods (golang/go#77273) finally make > left-to-right, > chainable, lazy collection pipelines expressible. I built a library to > explore > what falls out of that, and the most interesting part turned out to be > where > generic methods *still* can't help — which draws a surprisingly clean line > through the whole API. Repo at the bottom; I'd genuinely like the list's > view > on the design choices. > > > *THE THING I HATED* > > Anyone who has used samber/lo has written something like this: > > sum := lo.Sum( > lo.Map( > lo.Filter(xs, func(x int, _ int) bool { return x%2 == 0 }), > func(x int, _ int) int { return x * x }, > ), > ) > > The logic is filter -> map -> sum, but you read it sum(map(filter(...))): > inside-out, the reverse of the data flow. For years I assumed this was a > taste > decision lo had made. It wasn't. > > Before 1.27, a method could not declare its own type parameters. That > means a > .Map() that turns Seq[int] into Seq[string] was not merely discouraged, it > was > unrepresentable: > > // pre-1.27: does not compile. A method may not introduce [U any]. > func (s Seq[T]) Map[U any](f func(T) U) Seq[U] > > Because methods couldn't carry type parameters, every library of this kind > was > forced into top-level functions, and top-level functions compose > inside-out. > The nesting wasn't a style; it was the language. > > > *WHAT 1.27 UNLOCKS* > > golang/go#77273 (accepted and implemented in 1.27) lifts that restriction. > The > same pipeline becomes: > > sum := From(xs). > Filter(func(x int) bool { return x%2 == 0 }). > SumBy(func(x int) int { return x * x }) > > Left-to-right, in data-flow order, and it autocompletes after the dot. It > is > also lazy and short-circuiting, because it is built directly on the stdlib > iterators. > > > *THE DESIGN, AND THE PART THAT SURPRISED ME* > > The core types are *defined types* over iter.Seq, not struct wrappers: > > type Seq[T any] iter.Seq[T] // func(yield func(T) bool) > type Seq2[K, V any] iter.Seq2[K, V] > > This gives zero-cost conversion in both directions: any iter.Seq[T] is > usable > as a Seq[T] and the result feeds straight into slices.Collect, maps.Keys, > etc. > But it has a consequence I didn't anticipate. > > Because Seq[T any] pins T to `any` at the type level, a method's own type > parameters are fresh and independent — they cannot add a constraint back > onto > the receiver's T. So even *with* generic methods, you cannot write: > > func (s Seq[T]) Distinct() Seq[T] // needs T comparable -> > impossible > func (s Seq[T]) Max() (T, bool) // needs cmp.Ordered -> > impossible > > Anything that constrains T itself must remain a free function: > > func Distinct[T comparable](s Seq[T]) Seq[T] > func Max[T cmp.Ordered](s Seq[T]) (T, bool) > func Sum[T Numeric](s Seq[T]) T > > But anything that only uses a method's *own* constrained parameter can be a > method (the escape hatch): > > func (s Seq[T]) GroupBy[K comparable](key func(T) K) map[K][]T > func (s Seq[T]) Map[U any](f func(T) U) Seq[U] > > So the whole API splits along one mechanically checkable question: does the > operation constrain T itself? If yes, free function; if no, method. Generic > methods solved most of the problem and drew a hard, principled line at the > rest. I found that line more interesting than the feature itself. > > > *RECOVERING THE CHAIN* > > The annoying corollary: Distinct/Max/Sum being free functions drags you > back to > inside-out reading, Sum(Distinct(xs)). The least-ugly fix I found is to > pin the > constraint onto a subtype up front: > > type SeqNumeric[T Numeric] iter.Seq[T] > func Numbers[T Numeric](s Seq[T]) SeqNumeric[T] > > // one entry function crosses the constraint boundary; then it's > methods > sum := Numbers(From(xs)).Distinct().Sum() > > Inside SeqNumeric the constraint is already satisfied, so Distinct/Sum > become > methods again. Numeric ⊂ Ordered ⊂ comparable lets you downgrade with > .Ordered()/.Comparable(). It works, but it is plainly a workaround for "a > method > that adds a constraint to its receiver," which the type system cannot > express. > > > *HONEST LIMITATIONS* > > - Requgoog_1145710296ires Go 1.27 (currently go1.27rc1). The method chain > depends on #77273, > which is accepted and implemented but not yet in a stable release. > - Not evegoog_1145710297rything is lazy. Operations that must see the > whole sequence (Sort, > Reverse, TakeRight, sliding Window) materialize internally; they're > isolated > in one file and documented as such. > - Chunk/Window return iter.Seq[Seq[T]] rather than Seq[Seq[T]]: on 1.27rc1 > a > generic method on Seq[T] can't instantiate Seq[Seq[T]] (instantiation > cycle, > T := Seq[T]). I'd love to know if this is expected to relax in the stable > release. > - gofmt on 1.27rc1 flags generic-method signature lines with "method must > have > no type parameters" — appears to be a formatter/parser lag; build, vet, > and > test all pass. Can anyone confirm this is a known rc issue? > - Out of scope by choice: error-handling chains (Seq2[T, error] > short-circuit — > I couldn't find a clean fit), parallelism, in-place mutation, > arbitrary-depth > flatten, tuples past Tuple4. > > > > *WHAT I'D LIKE FEEDBACK ON* > 1. Defined type over iter.Seq vs a struct wrapper — does the zero-cost > interop > justify pinning T to `any` and pushing constrained ops to free > functions? > 2. The method-vs-free-function rule above: would you have drawn it > elsewhere? > 3. Any cleaner idea than the subtype trick for restoring the chain on > constrained operations? > > Repo, with the full op set (~80: Map/FlatMap/Scan/Zip/GroupBy/Window/set > ops/ > Seq2), and a tasks/ directory containing the PRD and design rationale: > > https://github.com/smallnest/seq > > Thanks for reading — happy to be told I've gotten any of this wrong. > -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/daa7881a-7374-477c-a167-5410a9c8f353n%40googlegroups.com.
