*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/05c16864-7c72-437b-bc93-349163d7a55bn%40googlegroups.com.

Reply via email to