{"id":49322876,"url":"https://github.com/natalie-o-perret/go-functionalish","last_synced_at":"2026-04-26T19:00:32.902Z","repository":{"id":350717943,"uuid":"1208005978","full_name":"natalie-o-perret/go-functionalish","owner":"natalie-o-perret","description":"Go F#(unctional)ish: lazy sequence operations, option, result \u0026 tuple types in a cohesive fashion.","archived":false,"fork":false,"pushed_at":"2026-04-26T13:08:25.000Z","size":156,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-26T13:22:04.470Z","etag":null,"topics":["fsharp","functional","go","golang","lazy","option","result","sequence","tuple"],"latest_commit_sha":null,"homepage":"https://pkg.go.dev/github.com/natalie-o-perret/go-functionalish","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/natalie-o-perret.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-11T17:34:56.000Z","updated_at":"2026-04-24T17:16:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/natalie-o-perret/go-functionalish","commit_stats":null,"previous_names":["natalie-o-perret/gof","natalie-o-perret/go-functionalish"],"tags_count":8,"template":false,"template_full_name":null,"purl":"pkg:github/natalie-o-perret/go-functionalish","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natalie-o-perret%2Fgo-functionalish","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natalie-o-perret%2Fgo-functionalish/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natalie-o-perret%2Fgo-functionalish/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natalie-o-perret%2Fgo-functionalish/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/natalie-o-perret","download_url":"https://codeload.github.com/natalie-o-perret/go-functionalish/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natalie-o-perret%2Fgo-functionalish/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32308878,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T17:23:19.671Z","status":"ssl_error","status_checked_at":"2026-04-26T17:23:19.195Z","response_time":129,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fsharp","functional","go","golang","lazy","option","result","sequence","tuple"],"created_at":"2026-04-26T19:00:20.112Z","updated_at":"2026-04-26T19:00:32.895Z","avatar_url":"https://github.com/natalie-o-perret.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# go-functionalish\n\n[![CI](https://github.com/natalie-o-perret/go-functionalish/actions/workflows/ci.yml/badge.svg)](https://github.com/natalie-o-perret/go-functionalish/actions/workflows/ci.yml)\n[![Go Reference](https://pkg.go.dev/badge/github.com/natalie-o-perret/go-functionalish.svg)](https://pkg.go.dev/github.com/natalie-o-perret/go-functionalish)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n[![Contributing](https://img.shields.io/badge/contributing-guide-blue)](CONTRIBUTING.md)\n\nA cohesive, opinionated, type-safe functional programming library for Go 1.24+.\nNo reflection. No `interface{}`. Pure generics and lazy by default.\n\n\u003e [!NOTE]\n\u003e Unapologetically vibe-coded with Claude Opus 4.6.\n\u003e\n\u003e Unapologetically not \"idiomatic Go.\"\n\u003e\n\u003e Go gave us generics 17 years after C# and 18 after Java (the latter still erases them at runtime).\n\u003e We're using them for `Option[T]`, `Result[T,E]`, and lazy pipelines\n\u003e instead of `if err != nil` sixty times per file.\n\n## Packages\n\n| Package      | Description                                                     |\n|--------------|-----------------------------------------------------------------|\n| `seq`        | Lazy `Seq[T]`: F#-style sequence pipelines                      |\n| `pseq`       | Parallel `Seq[T]`: goroutine-per-chunk Map, Filter, Reduce, ... |\n| `option`     | `Option[T]`: explicit presence/absence, no nil                  |\n| `result`     | `Result[T,E]`: railway-oriented error handling                  |\n| `validation` | `Validation[T,E]`: applicative error accumulation               |\n| `pipe`       | `Pipe2`...`Pipe8`: F#-style `\\|\u003e` operator equivalent           |\n| `kv`         | Lazy `Seq2[K,V]`: functional pipelines over `iter.Seq2` / maps  |\n\n## Quick start\n\n```go\nimport (\n\"github.com/natalie-o-perret/go-functionalish/seq\"\n\"github.com/natalie-o-perret/go-functionalish/pseq\"\n\"github.com/natalie-o-perret/go-functionalish/option\"\n\"github.com/natalie-o-perret/go-functionalish/result\"\n\"github.com/natalie-o-perret/go-functionalish/validation\"\n\"github.com/natalie-o-perret/go-functionalish/pipe\"\n\"github.com/natalie-o-perret/go-functionalish/kv\"\n)\n```\n\n### seq: lazy sequences\n\n```go\ntype Car struct { Year int; Owner, Model string }\n\ncars := []Car{\n{2012, \"Alice\", \"Toyota\"}, {2016, \"Bob\", \"Honda\"},\n{2018, \"Charlie\", \"Ford\"}, {2015, \"Diana\", \"BMW\"},\n}\n\n// Filter + Map (Map is pkg-level due to Go type system)\nowners := seq.Map(\nseq.OfSlice(cars).Filter(func (c Car) bool { return c.Year \u003e= 2015 }),\nfunc (c Car) string { return c.Owner },\n).ToSlice()\n// =\u003e [\"Bob\", \"Charlie\", \"Diana\"]\n\n// Sort, group, distinct\nbyCar := seq.GroupBy(seq.OfSlice(cars), func (c Car) string { return c.Model })\n\nunique := seq.DistinctBy(\nseq.OfSlice(cars).SortWith(func (a, b Car) int { return cmp.Compare(a.Model, b.Model) }),\nfunc (c Car) string { return c.Model },\n).ToSlice()\n\n// Short-circuiting terminals\nfirst := seq.OfSlice(cars).Filter(...).TryHead() // =\u003e option.Option[Car]\ncount := seq.OfSlice(cars).CountBy(func (c Car) bool { return c.Year \u003e= 2015 })\n\n// Generators\nsquares := seq.Map(seq.Range(1, 6), func (n int) int { return n * n }).ToSlice()\n// =\u003e [1 4 9 16 25]\n\n// RangeStep: custom step, supports descending\nevens := seq.RangeStep(0, 10, 2).ToSlice() // =\u003e [0 2 4 6 8]\ncountdown := seq.RangeStep(5, 0, -1).ToSlice() // =\u003e [5 4 3 2 1]\n\n// Zip / Interleave\npairs := seq.Zip(seq.OfSlice([]int{1, 2, 3}), seq.OfSlice([]string{\"a\", \"b\", \"c\"})).ToSlice()\n// =\u003e [{1 a} {2 b} {3 c}]\n\nmerged := seq.Interleave(seq.OfSlice([]int{1, 3, 5}), seq.OfSlice([]int{2, 4, 6})).ToSlice()\n// =\u003e [1 2 3 4 5 6]\n\n// Cycle + Truncate (infinite sequences)\npattern := seq.OfSlice([]string{\"ping\", \"pong\"}).Cycle().Truncate(5).ToSlice()\n// =\u003e [\"ping\", \"pong\", \"ping\", \"pong\", \"ping\"]\n\n// Unfold: generate from a seed state (e.g. Fibonacci)\nfibs := seq.Unfold([2]int{0, 1}, func (s [2]int) option.Option[seq.Pair[int, [2]int]] {\nif s[0] \u003e 20 {\nreturn option.None[seq.Pair[int, [2]int]]()\n}\nreturn option.Some(seq.Pair[int, [2]int]{First: s[0], Second: [2]int{s[1], s[0] + s[1]}})\n}).ToSlice()\n// =\u003e [0 1 1 2 3 5 8 13]\n\n// Partition: one pass, two slices\nevens, odds := seq.Partition(seq.Range(1, 7), func (n int) bool { return n%2 == 0 })\n// evens =\u003e [2 4 6],  odds =\u003e [1 3 5]\n\n// OfMap: iterate over a map\ncounts := seq.CountByKey(seq.OfMap(map[string]int{\"a\": 1, \"b\": 2}),\nfunc (p seq.Pair[string, int]) string { return p.First })\n// =\u003e map[a:1 b:1]\n\n// CountByKey: occurrence counts\nfreq := seq.CountByKey(seq.OfSlice([]string{\"a\", \"b\", \"a\", \"c\", \"a\", \"b\"}),\nfunc (s string) string { return s })\n// =\u003e map[a:3 b:2 c:1]\n\n// OfOption: lift an Option into a Seq\nseq.OfOption(option.Some(42)).ToSlice() // =\u003e [42]\nseq.OfOption(option.None[int]()).ToSlice() // =\u003e []\n\n// OfResult: lift a Result into a Seq (Ok =\u003e singleton, Err =\u003e empty)\nseq.OfResult(result.Ok[int, string](7)).ToSlice()  // =\u003e [7]\nseq.OfResult(result.Err[int, string](\"e\")).ToSlice() // =\u003e []\n\n// Intersperse: insert separator between elements\nseq.OfSlice([]int{1, 2, 3}).Intersperse(0).ToSlice() // =\u003e [1 0 2 0 3]\n\n// StepBy: yield every n-th element starting from the first\nseq.Range(0, 10).StepBy(3).ToSlice() // =\u003e [0 3 6 9]\n\n// ToMap / ToMapBy: materialise into a map\nm := seq.ToMap(seq.OfSlice([]seq.Pair[string, int]{{\"a\", 1}, {\"b\", 2}}))\n// =\u003e map[a:1 b:2]\n\ntype Car struct { Year int; Owner, Model string }\nbyOwner := seq.ToMapBy(seq.OfSlice(cars),\n    func(c Car) string { return c.Owner },\n    func(c Car) int    { return c.Year },\n)\n// =\u003e map[Alice:2012 Bob:2016 ...]\n```\n\n### pseq: parallel sequences\n\nParallel counterparts to the most parallelism-friendly `seq` operations,\ninspired by [FSharp.Collections.ParallelSeq](https://github.com/fsprojects/FSharp.Collections.ParallelSeq)\nand Go's [lo/lop](https://github.com/samber/lo) parallel helpers.\n\nEvery function materialises the input `Seq[T]`, partitions it into chunks,\ndispatches one goroutine per chunk, and collects results. **Order is always preserved.**\nParallelism defaults to `runtime.GOMAXPROCS(0)` and is tunable via `WithWorkers`.\n\n```go\ndata := seq.OfSlice([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10})\n\n// Parallel Map (order preserved)\ndoubled := pseq.Map(data, func (n int) int { return n * 2 }).ToSlice()\n// =\u003e [2 4 6 8 10 12 14 16 18 20]\n\n// Parallel Filter\nevens := pseq.Filter(data, func (n int) bool { return n%2 == 0 }).ToSlice()\n// =\u003e [2 4 6 8 10]\n\n// Parallel Reduce (fn must be associative)\nsum, _ := pseq.Reduce(data, func (a, b int) int { return a + b })\n// =\u003e 55\n\n// Parallel GroupBy\ngroups := pseq.GroupBy(data, func (n int) string {\nif n%2 == 0 { return \"even\" }\nreturn \"odd\"\n})\n// =\u003e map[even:[2 4 6 8 10] odd:[1 3 5 7 9]]\n\n// Configure workers\npseq.Map(data, heavyFn, pseq.WithWorkers(8))\n\n// Parallel Exists / ForAll (short-circuit across goroutines)\npseq.Exists(data, func (n int) bool { return n \u003e 9 }) // =\u003e true\npseq.ForAll(data, func (n int) bool { return n \u003e 0 }) // =\u003e true\n\n// Parallel Sum / SumBy\npseq.Sum(data) // =\u003e 55\n\n// Parallel Partition\nyes, no := pseq.Partition(data, func(n int) bool { return n \u003c= 5 })\n// yes =\u003e [1 2 3 4 5], no =\u003e [6 7 8 9 10]\n\n// Parallel Choose (filter+map with Option)\npseq.Choose(data, func (n int) option.Option[string] {\nif n%3 == 0 { return option.Some(fmt.Sprintf(\"fizz:%d\", n)) }\nreturn option.None[string]()\n}).ToSlice()\n// =\u003e [\"fizz:3\" \"fizz:6\" \"fizz:9\"]\n\n// Pipe integration via curried helpers\npipe.Pipe3(\nseq.OfSlice(bigData),\npseq.FilterFn(isValid, pseq.WithWorkers(8)),\npseq.MapFn(transform, pseq.WithWorkers(8)),\nseq.ToSliceFn[Result](),\n)\n```\n\n**When to use `pseq` vs `seq`:** parallel execution pays off when the per-element\nwork is CPU-heavy (parsing, math, serialisation). For lightweight lambdas\n(`n*2`, field access), the goroutine overhead dominates, so stick with `seq`.\n\n### option: explicit optionality\n\n```go\n// Instead of (T, bool) or *T\nname := option.Some(\"Alice\")\nnone := option.None[string]()\n\nupper := option.Map(name, strings.ToUpper) // =\u003e Some(\"ALICE\")\noption.Map(none, strings.ToUpper) // =\u003e None\n\nname.UnwrapOr(\"anonymous\") // =\u003e \"Alice\"\nnone.UnwrapOr(\"anonymous\") // =\u003e \"anonymous\"\n\n// DefaultWith: lazy default - fn is only called when None\nval := option.DefaultWith(none, func () string { return expensiveDefault() })\n\n// Contains: value equality check\noption.Contains(option.Some(42), 42) // =\u003e true\noption.Contains(option.None[int](), 42) // =\u003e false\n\n// Tee / TeeNone: side-effects without breaking the chain\nopt := option.Tee(option.Some(42), func(v int) { log.Println(\"got\", v) }) // =\u003e Some(42)\nopt  = option.TeeNone(option.None[int](), func() { log.Println(\"missing\") }) // =\u003e None\n\n// Map2: combine two Options\noption.Map2(option.Some(2), option.Some(3), func (a, b int) int { return a + b }) // =\u003e Some(5)\noption.Map2(option.None[int](), option.Some(3), func (a, b int) int { return a + b }) // =\u003e None\n\n// OrElse: fallback if None\nresolved := option.OrElse(lookupCache(key), func () option.Option[string] {\nreturn lookupDB(key)\n})\n\n// Flatten: unwrap Option[Option[T]]\noption.Flatten(option.Some(option.Some(42))) // =\u003e Some(42)\noption.Flatten(option.None[option.Option[int]]()) // =\u003e None\n\n// Chain optional lookups with Bind\nprofile := option.Bind(findUser(id), func(u User) option.Option[Profile] {\nreturn findProfile(u.ProfileID)\n})\n\n// Integrates with seq\nfirstModern := seq.OfSlice(cars).\nFilter(func (c Car) bool { return c.Year \u003e= 2015 }).\nTryHead() // =\u003e option.Option[Car]\n```\n\n### result : railway-oriented error handling\n\n```go\n// Wrap Go's (T, error) convention\nres := result.Try(func () (User, error) { return db.FindUser(id) })\n\n// Railway pipeline: chain Bind (may fail) and Map (pure transforms).\n// Once on the error track, every subsequent step is skipped.\nr1 := parseRequest(raw) // step 1: Bind\nr2 := result.Bind(r1, authenticate) // step 2: Bind\nr3 := result.Map(r2, normalize)               // step 3: Map (pure)\nr4 := result.Bind(r3, save) // step 4: Bind\nr5 := result.Map(r4, formatResponse) // step 5: Map (pure)\n// r5 is either Ok(response) or Err from whichever step failed first.\n\n// Tee / TeeErr: side-effects without breaking the chain - great for logging\nr := result.Tee(r3, func (v Request) { log.Printf(\"normalised: %v\", v) })\nr = result.TeeErr(r, func (e string) { log.Printf(\"failed: %s\", e) })\n\n// OrElse: try a fallback on Err\nuser := result.OrElse(lookupPrimary(id), func (e error) result.Result[User, error] {\nreturn lookupReplica(id)\n})\n\n// Flatten: unwrap Result[Result[T,E],E]\nresult.Flatten(result.Ok[result.Result[int, string], string](result.Ok[int, string](42)))\n// =\u003e Ok(42)\n\n// Zip: combine two Results into a pair (first Err wins)\nresult.Zip(result.Ok[int, string](1), result.Ok[string, string](\"hi\"))\n// =\u003e Ok({1, \"hi\"})\n\n// Map2: combine two Results (short-circuits on first Err)\nresult.Map2(\n    result.Ok[int, string](3),\n    result.Ok[int, string](4),\n    func(a, b int) int { return a + b },\n) // =\u003e Ok(7)\n\n// Contains: value equality check\nresult.Contains(result.Ok[int, string](42), 42) // =\u003e true\n\n// Sequence: []Result =\u003e Result[[]T] (short-circuits on first Err)\nresult.Sequence([]result.Result[int, string]{\n    result.Ok[int, string](1),\n    result.Ok[int, string](2),\n}) // =\u003e Ok([1 2])\n\n// Traverse: map + sequence in one pass (short-circuits on first Err)\nresult.Traverse([]string{\"1\", \"2\", \"3\"}, func(s string) result.Result[int, string] {\n    n, err := strconv.Atoi(s)\n    if err != nil { return result.Err[int, string](err.Error()) }\n    return result.Ok[int, string](n)\n}) // =\u003e Ok([1 2 3])\n\n// MapErr adds context to errors\nwrapped := result.MapErr(r5, func (e string) string {\nreturn \"request failed: \" + e\n})\n\n// Interop with option\nopt := res.ToOption() // Ok =\u003e Some, Err =\u003e None\nres2 := result.FromOption(opt, errors.New(\"not found\"))\n```\n\n### kv: key-value pipelines\n\nFunctional operations over `iter.Seq2[K,V]` - the lazy, composable counterpart to\nGo's `maps` package.\n\n```go\ninventory := map[string]int{\n    \"apple\": 50, \"banana\": 3, \"cherry\": 120, \"date\": 0,\n}\n\n// Of wraps a map into a lazy Seq2\ns := kv.Of(inventory)\n\n// Filter: keep only non-zero stock\ninStock := kv.Filter(s, func(_ string, qty int) bool { return qty \u003e 0 })\n\n// MapValues: apply a discount\ndiscounted := kv.MapValues(inStock, func(qty int) int { return qty * 9 / 10 })\n\n// Collect: materialise back to a map\nresult := kv.Collect(discounted)\n// =\u003e map[apple:45 cherry:108]\n\n// Keys / Values: extract as seq.Seq\nkeys := kv.Keys(s).SortWith(cmp.Compare).ToSlice()\n// =\u003e [apple banana cherry date]\n\n// MapKeys: transform keys\nupper := kv.Collect(kv.MapKeys(s, strings.ToUpper))\n// =\u003e map[APPLE:50 BANANA:3 ...]\n\n// Fold: reduce to a single value\ntotal := kv.Fold(s, 0, func(acc int, _ string, qty int) int { return acc + qty })\n// =\u003e 173\n\n// ContainsKey: short-circuiting membership test\nkv.ContainsKey(s, \"apple\") // =\u003e true\nkv.ContainsKey(s, \"mango\") // =\u003e false\n\n// ToSeq / FromSeq: bridge to seq.Seq[seq.Pair[K,V]]\npairs := kv.ToSeq(s).Filter(func(p seq.Pair[string, int]) bool { return p.Second \u003e 10 }).ToSlice()\nback  := kv.Collect(kv.FromSeq(seq.OfSlice(pairs)))\n```\n\n## Design notes\n\n### Why are `Map`, `Collect`, `GroupBy` package-level functions?\n\nGo does not allow methods to introduce new type parameters. A method on\n`Seq[Car]` cannot return `Seq[string]` because that would require\n`func (s Seq[T]) Map[R any](fn func(T) R) Seq[R]` : which the\ncompiler rejects.\n\nThe workaround: **type-transforming operations are package-level functions**,\nsame-type operations are methods:\n\n```go\n//  method : stays Seq[Car]\nOfSlice(cars).Filter(fn).SortWith(less).Truncate(10)\n\n//  package-level : changes type\nseq.Map(OfSlice(cars).Filter(fn), Car.Owner)\n//      ^ sub-chain               ^ transform\n```\n\n### Lazy vs eager\n\nAll pipeline operations (`Filter`, `Map`, `Truncate`, ...) are **lazy** : they wrap\nthe previous iterator and produce no output until a terminal is called. Only\n`SortWith`, `SortBy`, `Rev` must **materialise** (you can't sort\na stream you haven't fully read).\n\n### Why does `pseq` use chunking instead of per-element goroutines?\n\nSpawning one goroutine per element (as `lo/parallel` does) is simple but scales\npoorly: 100k items = 100k goroutines = ~300k allocations and ~200–400 ms of pure\nscheduler overhead before any real work begins.\n\n`pseq` splits the input into `GOMAXPROCS` chunks (default 8 on a typical machine)\nand runs one goroutine per chunk. This is the same strategy .NET's PLINQ uses\nunder the hood (which F#'s `PSeq` wraps). It means:\n\n- **8 goroutines** instead of 100k → ~300× fewer allocs\n- Each goroutine processes a contiguous slice → **cache-friendly** sequential access\n- Overhead is constant regardless of input size → **O(workers)**, not **O(n)**\n- Users can tune it via `WithWorkers(n)` when the default doesn't fit\n\n## Performance\n\n### Direct method chaining vs pipe + compose\n\nThe `*Fn` curried helpers and `pipe.Compose` add thin closure wrappers around\nthe same underlying methods. Benchmarks on a 10,000-element `[]int` pipeline\n(Intel Core Ultra 7):\n\n| Pipeline                              | Style  |   ns/op |   B/op | allocs |\n|---------------------------------------|--------|--------:|-------:|-------:|\n| **Small** (filter =\u003e map =\u003e take 100) | Direct |  ~1,780 |  2,064 |      9 |\n|                                       | Pipe   |  ~1,620 |  2,064 |      9 |\n| **Medium** (7 steps incl. sort)       | Direct | ~13,100 |  8,376 |     26 |\n|                                       | Pipe   | ~13,900 |  8,912 |     44 |\n| **Large** (10 steps incl. rev+sort)   | Direct | ~27,100 | 12,784 |     46 |\n|                                       | Pipe   | ~30,400 | 13,592 |     73 |\n\n**~6-13 % wall-clock overhead**, all from one-time closure allocations when the\npipeline is *built*, not per element. The hot iteration loop is identical either\nway. For any real workload (I/O, serialisation, business logic in the lambdas)\nthis is noise: choose whichever style reads better.\n\n### pseq vs lo/parallel\n\n[lo/parallel](https://github.com/samber/lo) spawns **one goroutine per element** —\nsimple, but O(n) scheduling overhead. `pseq` partitions into `GOMAXPROCS` chunks\nand runs **one goroutine per chunk** (the same strategy as .NET's PLINQ / F#'s `PSeq`).\n\nBenchmarks on `[]int` pipelines (Intel Core Ultra 7, 8 cores):\n\n#### CPU-heavy workload (500 sqrt iterations per element)\n\n| Operation       | `seq` (sequential) | `pseq` (chunked) | `lo/parallel` (per-element) | pseq vs lo       |\n|-----------------|-------------------:|-----------------:|----------------------------:|------------------|\n| **Map 1k**      |           1,968 µs |       **717 µs** |                      724 µs | ≈ tied           |\n| **Map 10k**     |          19,243 µs |     **5,509 µs** |                    6,815 µs | **1.24× faster** |\n| **Map 100k**    |         192,687 µs |    **44,555 µs** |                   57,882 µs | **1.30× faster** |\n| **ForEach 10k** |                  — |       **328 µs** |                    2,904 µs | **8.9× faster**  |\n| **GroupBy 10k** |                  — |     **4,587 µs** |                    5,170 µs | **1.13× faster** |\n\n#### Lightweight workload (`n*3+1` - exposes overhead)\n\n| Operation    | `seq` (sequential) | `pseq` (chunked) | `lo/parallel` (per-element) | pseq vs lo       |\n|--------------|-------------------:|-----------------:|----------------------------:|------------------|\n| **Map 1k**   |           **6 µs** |            25 µs |                      316 µs | **12.7× faster** |\n| **Map 10k**  |         **122 µs** |           357 µs |                    3,473 µs | **9.7× faster**  |\n| **Map 100k** |       **1,427 µs** |         3,635 µs |                   34,398 µs | **9.5× faster**  |\n\n#### Memory (10k Map)\n\n|               | `pseq` | `lo/parallel` | ratio                              |\n|---------------|--------|---------------|------------------------------------|\n| **B/op**      | 798 KB | 1,067 KB      | lo uses 1.3× more memory           |\n| **allocs/op** | **66** | 20,051        | lo allocates **303× more objects** |\n\n**Why?** `lo` does `go func(...)` inside a `for i, item := range`, spawning 10k goroutines\nfor 10k items. `pseq` splits into ~8 chunks. Goroutine spawn+schedule is ~2-4 µs each,\nso `lo` pays ~20-40 ms in scheduling alone for 10k items, while `pseq` pays ~16-32 µs.\nWhen the per-element work is heavy enough, both approaches saturate the CPUs and converge.\nWhen it isn't, `lo` is 10-13x slower than `pseq`, and even slower than sequential `seq`.\n\n**Rule of thumb:** for lightweight lambdas, don't parallelize at all - use `seq`.\nFor CPU-heavy work (parsing, crypto, compression, complex transforms), `pseq` gives\nthe parallel speedup with a fraction of the scheduling cost.\n\nRun the benchmarks yourself:\n\n```sh\n# seq pipeline benchmarks\ngo test ./seq/ -bench=. -benchmem\n\n# pseq benchmarks (includes vs-lo comparison)\ngo test ./pseq/ -bench=. -benchmem\n```\n\n## Dependency graph\n\n```text\npipe       =\u003e  (none)\noption     =\u003e  (none)\nresult     =\u003e  option\nvalidation =\u003e  option, result\nseq        =\u003e  option, result\npseq       =\u003e  seq, option\nkv         =\u003e  seq\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnatalie-o-perret%2Fgo-functionalish","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnatalie-o-perret%2Fgo-functionalish","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnatalie-o-perret%2Fgo-functionalish/lists"}