jackhammer

Utilities for Go
Log | Files | Refs | README | LICENSE

commit 70a33087d2bb443dd107b3a4f2aa21bb1695b6cc
Author: Jack Mordaunt <jackmordaunt.dev@gmail.com>
Date:   Thu, 21 Aug 2025 09:37:08 -0300

initial

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>

Diffstat:
AREADME.md | 4++++
Achannel/channel.go | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achannel/channel_test.go | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aerrorsx/errorsx.go | 29+++++++++++++++++++++++++++++
Ago.mod | 5+++++
Ago.sum | 2++
Aiterx/iterx.go | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aiterx/iterx_test.go | 30++++++++++++++++++++++++++++++
Apara/gather.go | 16++++++++++++++++
Apara/para.go | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apara/para_test.go | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aslicesx/slicesx.go | 325+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aslicesx/slicesx_test.go | 218+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asyncx/mutex.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
Asyncx/pool.go | 33+++++++++++++++++++++++++++++++++
Asyncx/slice.go | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
16 files changed, 1506 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -0,0 +1,4 @@ +# jackhammer + +This package provides experimental utilities that extend Go's standard library. + diff --git a/channel/channel.go b/channel/channel.go @@ -0,0 +1,145 @@ +package channel + +import ( + "context" + "reflect" + "slices" + "time" +) + +// Drain the provided channel. +func Drain[T any](ch <-chan T) { + for range ch { + } +} + +// TryReceive selects on a channel returning true if there was a value. +func TryReceive[T any](ch <-chan T) (t T, ok bool) { + select { + case t = <-ch: + ok = true + default: + } + return t, ok +} + +// TrySend attempts a sends on a channel returning true if success. +func TrySend[T any](ch chan<- T, t T) bool { + select { + case ch <- t: + return true + default: + return false + } +} + +// ReceiveIn selects on the provided channel up to duration `d` and returns the value. +// If the duration elapses, the function returns the zero value of `T` and false. +func ReceiveIn[T any](ch <-chan T, d time.Duration) (T, bool) { + timer := time.NewTimer(d) + select { + case t := <-ch: + return t, true + case <-timer.C: + return *new(T), false + } +} + +// Collect all values from a channel into a slice. +func Collect[T any](in <-chan T) []T { + out := []T{} + for t := range in { + out = append(out, t) + } + return out +} + +// CollectAllCtx buffers every value received value into a slice until all input channels are closed +// or the context is cancelled. +func CollectAllCtx[T any](ctx context.Context, chs ...chan T) []T { + out := []T{} + for v := range FanInCtx(ctx, chs...) { + out = append(out, v) + } + return out +} + +// CollectAll buffers every value received value into a slice until all input channels are closed. +func CollectAll[T any](chs ...chan T) (out []T) { + return CollectAllCtx(context.Background(), chs...) +} + +// BufferedSend uses slice allocation to buffer items providing a dynamically +// buffered channel send. This send will never block. +// +// If the channel is open, we send on it. +// If the channel is full, we select off the slice, append to it and send it +// back onto the channel. +// +// The loop will drain the channel until it becomes available to send. After +// the function returns, the value v is guaranteed to be sent on the channel, +// however it is not guaranteed that this is the only value sent. +// +// The input channel MUST have capacity of 1. +func BufferedSend[S ~[]E, E any](in chan S, v E) { + if cap(in) != 1 { + panic("BufferedSend is only valid for single buffered channels") + } + + var items S = []E{v} + + for { + select { + case buf := <-in: + items = append(buf, items...) + case in <- items: + return + } + } +} + +// FanIn returns a channel that emits every value received from the input channel set. +// The output channel is closed when all input channels are closed. +func FanIn[T any](chs ...chan T) chan T { + return FanInCtx(context.Background(), chs...) +} + +// FanInCtx returns a channel that emits every value received from the input channel set. +// The output channel is closed when all input channels are closed, or when the context +// is cancelled. +func FanInCtx[T any](ctx context.Context, chs ...chan T) chan T { + out := make(chan T) + + go func() { + defer close(out) + + cases := make([]reflect.SelectCase, 0, len(chs)+1) + + cases = append(cases, reflect.SelectCase{ + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(ctx.Done()), + }) + + for _, ch := range chs { + cases = append(cases, reflect.SelectCase{ + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(ch), + }) + } + + for len(cases) > 1 { + ii, recv, ok := reflect.Select(cases) + if ok { + out <- recv.Interface().(T) + } else { + cases = slices.Delete(cases, ii, ii+1) + } + if ii == 0 { + // This is always the ctx.Done channel. + return + } + } + }() + + return out +} diff --git a/channel/channel_test.go b/channel/channel_test.go @@ -0,0 +1,150 @@ +package channel + +import ( + "context" + "slices" + "testing" +) + +func TestCollectAll(t *testing.T) { + t.Run("normal", func(t *testing.T) { + ctx := context.Background() + + channels := []chan int{} + + for ii := 0; ii < 10; ii += 1 { + channels = append(channels, make(chan int)) + } + + var got []int + var want []int + + done := make(chan any) + + go func() { + got = CollectAllCtx(ctx, channels...) + close(done) + }() + + for ii := 0; ii < 100; ii += 1 { + want = append(want, ii) + channels[ii%len(channels)] <- ii + } + + for _, ch := range channels { + close(ch) + } + + <-done + + if !slices.Equal(got, want) { + t.Fatalf("got=%v, want=%v", got, want) + } + }) + + t.Run("context cancel", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + channels := []chan int{} + + for ii := 0; ii < 10; ii += 1 { + channels = append(channels, make(chan int)) + } + + var got []int + var want []int + + done := make(chan any) + + go func() { + got = CollectAllCtx(ctx, channels...) + close(done) + }() + + for ii := 0; ii < 100; ii += 1 { + want = append(want, ii) + channels[ii%len(channels)] <- ii + } + + cancel() + + <-done + + if !slices.Equal(got, want) { + t.Fatalf("got=%v, want=%v", got, want) + } + }) + +} + +func TestFanIn(t *testing.T) { + + t.Run("normal", func(t *testing.T) { + channels := make([]chan int, 0, 10) + + for ii := 0; ii < 10; ii += 1 { + channels = append(channels, make(chan int)) + } + + go func() { + for ii := 0; ii < 100; ii += 1 { + channels[ii%len(channels)] <- ii + } + for _, ch := range channels { + close(ch) + } + }() + + var got []int + var want []int + + for ii := 0; ii < 100; ii += 1 { + want = append(want, ii) + } + + for n := range FanIn(channels...) { + got = append(got, n) + } + + slices.Sort(got) + + if !slices.Equal(got, want) { + t.Fatalf("got=%v, want=%v", got, want) + } + }) + + t.Run("context cancel", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + channels := make([]chan int, 0, 10) + + for ii := 0; ii < 10; ii += 1 { + channels = append(channels, make(chan int)) + } + + go func() { + for ii := 0; ii < 100; ii += 1 { + channels[ii%len(channels)] <- ii + } + cancel() + }() + + var got []int + var want []int + + for ii := 0; ii < 100; ii += 1 { + want = append(want, ii) + } + + for n := range FanInCtx(ctx, channels...) { + got = append(got, n) + } + + slices.Sort(got) + + if !slices.Equal(got, want) { + t.Fatalf("got=%v, want=%v", got, want) + } + }) + +} diff --git a/errorsx/errorsx.go b/errorsx/errorsx.go @@ -0,0 +1,29 @@ +package errorsx + +import ( + "strings" +) + +type PolyError struct { + Errs []error +} + +func (err PolyError) Unwrap() []error { + return err.Errs +} + +func (err PolyError) Error() string { + var b strings.Builder + b.WriteString("multiple errors:\n") + for ii, e := range err.Errs { + if e == nil { + continue + } + b.WriteString(" - ") + b.WriteString(e.Error()) + if ii != len(err.Errs)-1 { + b.WriteByte('\n') + } + } + return b.String() +} diff --git a/go.mod b/go.mod @@ -0,0 +1,5 @@ +module git.sr.ht/~jackmordaunt/jackhammer + +go 1.23.6 + +require golang.org/x/sync v0.12.0 diff --git a/go.sum b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= diff --git a/iterx/iterx.go b/iterx/iterx.go @@ -0,0 +1,159 @@ +package iterx + +import ( + "iter" + "runtime" + "sync" + + "git.sr.ht/~jackmordaunt/jackhammer/channel" + "git.sr.ht/~jackmordaunt/jackhammer/errorsx" +) + +// Fanout processes a sequence concurrently. +func Fanout[V any](seq iter.Seq[V], fn func(value V)) { + wg := sync.WaitGroup{} + defer wg.Wait() + + for v := range seq { + wg.Add(1) + go func() { + defer wg.Done() + fn(v) + }() + } +} + +// Fanout2 processes a sequence concurrently. +func Fanout2[K any, V any](seq iter.Seq2[K, V], fn func(key K, value V)) { + type pair[K any, V any] struct { + key K + value V + } + + queue := make(chan pair[K, V]) + wg := sync.WaitGroup{} + + for ii := 0; ii < runtime.NumCPU(); ii += 1 { + wg.Add(1) + go func() { + defer wg.Done() + for p := range queue { + fn(p.key, p.value) + } + }() + } + + for k, v := range seq { + queue <- pair[K, V]{key: k, value: v} + } + + close(queue) + wg.Wait() +} + +// FanoutErr2 processes a sequence cuncurrently, returning an aggregate error. +func FanoutErr2[K any, V any](seq iter.Seq2[K, V], fn func(key K, value V) error) error { + errs := []error{} + failures := make(chan error) + done := make(chan any) + + Fanout2(seq, func(key K, value V) { + if err := fn(key, value); err != nil { + failures <- err + } + }) + + go func() { + errs = channel.Collect(failures) + close(done) + }() + + close(failures) + <-done + + if len(errs) > 0 { + return errorsx.PolyError{Errs: errs} + } + + return nil +} + +// For2 processes a sequence with the given function. +func For2[K any, V any](seq iter.Seq2[K, V], fn func(key K, value V)) { + for k, v := range seq { + fn(k, v) + } +} + +// ForErr2 processes a sequence with the given function. +func ForErr2[K any, V any](seq iter.Seq2[K, V], fn func(key K, value V) error) error { + var errs []error + + for k, v := range seq { + if err := fn(k, v); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errorsx.PolyError{Errs: errs} + } + + return nil +} + +// Map transforms an iterator by applying a function to each element and returning the result. +func Map[S iter.Seq[In], In any, Out any](s S, fn func(In) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for v := range s { + if !yield(fn(v)) { + break + } + } + } +} + +// MapIndex is like [Map] but provides the iteration index to the transform function. +func MapIndex[S iter.Seq[In], In any, Out any](s S, fn func(int, In) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + ii := -1 + for v := range s { + ii += 1 + if !yield(fn(ii, v)) { + break + } + } + } +} + +// Map2 transforms an iterator by applying a function to each element and returning the result. +func Map2[S iter.Seq2[In1, In2], In1 any, In2 any, Out any](s S, fn func(In1, In2) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for v1, v2 := range s { + if !yield(fn(v1, v2)) { + break + } + } + } +} + +// ToMap collects a sequence into a map, using [keyFor] to extract the key from each element. +func ToMap[S iter.Seq2[int, E], E any, K comparable](s S, keyFor func(int, E) K) map[K]E { + out := map[K]E{} + for ii, v := range s { + out[keyFor(ii, v)] = v + } + return out +} + +// WithIndex adapts a single-value iterator to a two-value iterator that yeilds the index. +func WithIndex[T any](s iter.Seq[T]) iter.Seq2[int, T] { + return iter.Seq2[int, T](func(yield func(int, T) bool) { + i := 0 + for v := range s { + if !yield(i, v) { + break + } + i++ + } + }) +} diff --git a/iterx/iterx_test.go b/iterx/iterx_test.go @@ -0,0 +1,30 @@ +package iterx + +import ( + "slices" + "strconv" + "testing" +) + +func TestMap(t *testing.T) { + values := []string{} + for ii := 0; ii < 100; ii += 1 { + values = append(values, strconv.Itoa(ii)) + } + + iterator := Map(slices.Values(values), func(s string) int { + n, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return n + }) + + for ii, n := range slices.Collect(iterator) { + want := values[ii] + got := strconv.Itoa(n) + if want != got { + t.Fatalf("got=%s, want=%s", got, want) + } + } +} diff --git a/para/gather.go b/para/gather.go @@ -0,0 +1,16 @@ +package para + +import ( + "golang.org/x/sync/errgroup" +) + +// Gather processes a batch of heterogenous operations. +func Gather(fns ...func() error) error { + work := errgroup.Group{} + + for _, fn := range fns { + work.Go(fn) + } + + return work.Wait() +} diff --git a/para/para.go b/para/para.go @@ -0,0 +1,72 @@ +package para + +import ( + "context" + "fmt" + "iter" + "sync" + + "golang.org/x/sync/errgroup" +) + +// Fanout processes a sequence concurrently. +func Fanout[V any](seq iter.Seq[V], fn func(value V)) { + wg := sync.WaitGroup{} + defer wg.Wait() + + for v := range seq { + wg.Add(1) + go func() { + defer wg.Done() + fn(v) + }() + } +} + +// Batch processes the sequence concurrently. Any error cancels the +// entire batch. No results are generated. This is helpful for a fanout of +// side-effects. +func Batch[T any](ctx context.Context, seq iter.Seq[T], work func(ctx context.Context, ii int, v T) error) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + group, ctx := errgroup.WithContext(ctx) + + ii := 0 + for v := range seq { + func(ii int) { + group.Go(func() error { + return work(ctx, ii, v) + }) + }(ii) + ii += 1 + } + + return group.Wait() +} + +// MapErr processes the sequence concurrently, transforming [In] to [Out]. +// Any error cancels the entire batch. This is helpful for a fanout of +// fallible transformations. +func MapErr[In any, Out any](ctx context.Context, seq []In, work func(ctx context.Context, ii int, v In) (Out, error)) ([]Out, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + group, ctx := errgroup.WithContext(ctx) + + res := make([]Out, len(seq)) + + for ii, v := range seq { + group.Go(func() error { + out, err := work(ctx, ii, v) + res[ii] = out + return err + }) + } + + if err := group.Wait(); err != nil { + return nil, fmt.Errorf("map: %w", err) + } + + return res, nil +} diff --git a/para/para_test.go b/para/para_test.go @@ -0,0 +1,212 @@ +package para + +import ( + "context" + "fmt" + "slices" + "strconv" + "sync" + "sync/atomic" + "testing" +) + +func TestFanout(t *testing.T) { + t.Run("happy", func(t *testing.T) { + n := 10 + + input := []int{} + for ii := range n { + input = append(input, ii) + } + + var calls atomic.Int32 + + Fanout(slices.Values(input), func(v int) { + calls.Add(1) + }) + + if got := calls.Load(); got != int32(n) { + t.Fatalf("calls mismatch: want=%v, got=%v", n, got) + } + }) +} + +func TestBatch(t *testing.T) { + t.Run("happy", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + input := []string{} + want := []int64{} + + for ii := range 100 { + input = append(input, strconv.Itoa(ii)) + want = append(want, int64(ii)) + } + + got := make([]int64, len(input)) + + err := Batch(ctx, slices.Values(input), func(ctx context.Context, ii int, v string) error { + n, err := strconv.ParseInt(v, 10, 64) + got[ii] = n + return err + }) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !slices.Equal(got, want) { + t.Fatalf("want=%v, got=%v", want, got) + } + }) + + t.Run("sad", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + input := []string{} + + n := 5 + + for ii := range n { + input = append(input, strconv.Itoa(ii)) + } + + trigger := make(chan any, 1) + ready := sync.WaitGroup{} + ready.Add(n) + + go func() { + ready.Wait() + // trigger one closure to return an error + trigger <- nil + }() + + var nextID atomic.Int32 + var canceled atomic.Int32 + var errored atomic.Int32 + + err := Batch(ctx, slices.Values(input), func(ctx context.Context, ii int, v string) error { + id := nextID.Add(1) + + ready.Done() + + select { + case <-ctx.Done(): + // most should get canceled due to the one that failed + // should be n-1 + canceled.Add(1) + t.Logf("%d: canceled", id) + return ctx.Err() + case <-trigger: + // should be one + errored.Add(1) + t.Logf("%d: errored", id) + return fmt.Errorf("an error") + } + }) + + if err == nil { + t.Fatalf("expected error") + } + + if got := canceled.Load(); got != int32(n-1) { + t.Errorf("expected %d cancels, got %v", int32(n-1), got) + } + + if got := errored.Load(); got != 1 { + t.Errorf("expected 1 error, got %v", got) + } + }) +} + +func TestMapErr(t *testing.T) { + t.Run("happy", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + input := []string{} + want := []int64{} + + for ii := range 100 { + input = append(input, strconv.Itoa(ii)) + want = append(want, int64(ii)) + } + + got, err := MapErr(ctx, input, func(ctx context.Context, ii int, v string) (int64, error) { + return strconv.ParseInt(v, 10, 64) + }) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !slices.Equal(got, want) { + t.Fatalf("want=%v, got=%v", want, got) + } + }) + + t.Run("sad", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + input := []string{} + + n := 5 + + for ii := range n { + input = append(input, strconv.Itoa(ii)) + } + + trigger := make(chan any, 1) + ready := sync.WaitGroup{} + ready.Add(n) + + go func() { + ready.Wait() + // trigger one closure to return an error + trigger <- nil + }() + + var nextID atomic.Int32 + var canceled atomic.Int32 + var errored atomic.Int32 + + got, err := MapErr(ctx, input, func(ctx context.Context, ii int, v string) (int64, error) { + id := nextID.Add(1) + + ready.Done() + + select { + case <-ctx.Done(): + // most should get canceled due to the one that failed + // should be n-1 + canceled.Add(1) + t.Logf("%d: canceled", id) + return 0, ctx.Err() + case <-trigger: + // should be one + errored.Add(1) + t.Logf("%d: errored", id) + return 0, fmt.Errorf("an error") + } + }) + + if err == nil { + t.Fatalf("expected error") + } + + if len(got) != 0 { + t.Fatalf("expected result to be empty, got %v", got) + } + + if got := canceled.Load(); got != int32(n-1) { + t.Errorf("expected %d cancels, got %v", int32(n-1), got) + } + + if got := errored.Load(); got != 1 { + t.Errorf("expected 1 error, got %v", got) + } + }) +} diff --git a/slicesx/slicesx.go b/slicesx/slicesx.go @@ -0,0 +1,325 @@ +// Package slicesx provides slice utilities that compliment Go standard +// slices package. +package slicesx + +import ( + "errors" + "fmt" + "maps" + "slices" + "sync" + + "git.sr.ht/~jackmordaunt/jackhammer/errorsx" + "git.sr.ht/~jackmordaunt/jackhammer/iterx" +) + +// Fanout across all elements of a slice in parallel. +// The function receives the index and element. +func Fanout[S ~[]E, E any](seq S, fn func(int, E)) { + iterx.Fanout2(slices.All(seq), fn) +} + +// FanoutErr is like [Fanout] but returns an aggregated error. +func FanoutErr[S ~[]E, E any](seq S, fn func(int, E) error) error { + return iterx.FanoutErr2(slices.All(seq), fn) +} + +// Map transforms a slice by applying a function to each element and returning the result. +func Map[S ~[]E, E any, Out any](s S, fn func(E) Out) []Out { + out := make([]Out, len(s)) + for ii := range s { + out[ii] = fn(s[ii]) + } + return out +} + +// MapIndex is like [Map] but provides the iteration index to the transform function. +func MapIndex[S ~[]E, E any, Out any](s S, fn func(int, E) Out) []Out { + out := make([]Out, len(s)) + for ii := range s { + out[ii] = fn(ii, s[ii]) + } + return out +} + +// MapSafe is like [Map] but recovers from panics and returns an error instead. +func MapSafe[S ~[]E, E any, Out any](s S, fn func(E) Out) (_ []Out, err error) { + defer func() { + if v := recover(); v != nil { + if e, ok := v.(error); ok { + err = e + } else { + err = fmt.Errorf("%v", v) + } + } + }() + return Map(s, fn), err +} + +// MapIndexSafe is like [MapIndex] but recovers from panics and returns an error instead. +func MapIndexSafe[S ~[]E, E any, Out any](s S, fn func(int, E) Out) (_ []Out, err error) { + defer func() { + if v := recover(); v != nil { + if e, ok := v.(error); ok { + err = e + } else { + err = fmt.Errorf("%v", v) + } + } + }() + return MapIndex(s, fn), err +} + +// ParaMap transforms a slice by applying a function to each element and returning the result. +// Transforms are executed concurrently. +func ParaMap[S ~[]E, E any, Out any](s S, fn func(E) Out) []Out { + out := make([]Out, len(s)) + wg := sync.WaitGroup{} + for ii := range s { + wg.Add(1) + go func() { + defer wg.Done() + out[ii] = fn(s[ii]) + }() + } + wg.Wait() + return out +} + +// MapErr transforms a slice by applying a fallible function to each element and +// returning the result. +// +// If the function returns an error the map operation completes, returning an +// aggregated error for each failed transform. The output list will be short +// by the number of failures. Length of the output is not guaranteed to match that +// of the input. +func MapErr[S ~[]E, E any, Out any](s S, fn func(E) (Out, error)) (out []Out, err error) { + out = make([]Out, 0, len(s)) + for ii := range s { + v, e := fn(s[ii]) + if e != nil { + err = errors.Join(err, e) + } else { + out = append(out, v) + } + } + return out, err +} + +// ParaMapErr is [MapErr], but all instances of fn run concurrently. +func ParaMapErr[S ~[]E, E any, Out any](s S, fn func(E) (Out, error)) (out []Out, err error) { + out = make([]Out, len(s)) + errs := make([]error, len(s)) + wg := sync.WaitGroup{} + + for ii := range s { + wg.Add(1) + go func() { + defer wg.Done() + v, e := fn(s[ii]) + if e != nil { + errs[ii] = e + } else { + out[ii] = v + } + }() + } + + wg.Wait() + + for ii, err := range errs { + if err != nil { + out = slices.Delete(out, ii, ii+1) + } + } + + if len(errs) > 0 { + return out, errorsx.PolyError{Errs: errs} + } + + return out, nil +} + +// For processes a sequence with the given function. +func For[S ~[]E, E any](seq S, fn func(int, E)) { + iterx.For2(slices.All(seq), fn) +} + +// ForErr processes a sequence with the given function. +func ForErr[S ~[]E, E any](seq S, fn func(int, E) error) error { + return iterx.ForErr2(slices.All(seq), fn) +} + +// Generate returns a slice of [n] items produced by [fn]. +func Generate[E any](n int, fn func(int) E) []E { + out := make([]E, n) + for ii := range n { + out[ii] = fn(ii) + } + return out +} + +// Repeat returns a slice of [n] items of value [v]. +func Repeat[E any](n int, v E) []E { + out := make([]E, n) + for ii := range n { + out[ii] = v + } + return out +} + +// PopFront returns the first element from the slice. +func PopFront[S ~[]E, E any](s *S) E { + defer func() { (*s) = (*s)[1:] }() + return (*s)[0] +} + +// PopFrontSafe returns the first element from the slice, or false. +func PopFrontSafe[S ~[]E, E any](s *S) (e E, ok bool) { + if s == nil { + return e, false + } + if len(*s) == 0 { + return e, false + } + return PopFront(s), true +} + +// PopBack returns the last element from the slice. +func PopBack[S ~[]E, E any](s *S) E { + defer func() { (*s) = (*s)[:len(*s)-1] }() + return (*s)[len(*s)-1] +} + +// PopBackSafe returns the last element from the slice, or false. +func PopBackSafe[S ~[]E, E any](s *S) (e E, ok bool) { + if s == nil { + return e, false + } + if len(*s) == 0 { + return e, false + } + return PopBack(s), true +} + +// Filter out elements from a slice when the predicate returns false. +func Filter[S ~[]E, E any](s S, fn func(E) bool) S { + out := make(S, 0, len(s)) + for _, v := range s { + if fn(v) { + out = append(out, v) + } + } + return slices.Clip(out) +} + +// FilterMap maps [In] to [Out], ignoring entries that return false. +func FilterMap[S ~[]In, In any, Out any](s S, fn func(In) (Out, bool)) []Out { + out := make([]Out, 0, len(s)) + for _, v := range s { + if v, ok := fn(v); ok { + out = append(out, v) + } + } + return slices.Clip(out) +} + +// TakeRange selects all items between [start] and [end] and removes them from the slice. +// Does not bounds check. +func TakeRange[S ~[]E, E any](s *S, start, end int) S { + taken := (*s)[start:end] + *s = slices.Delete(*s, start, end) + return taken +} + +// TakeRangeSafe selects all items between [start] and [end] and removes them from the slice. +// Checks the bounds. +func TakeRangeSafe[S ~[]E, E any](s *S, start, end int) (S, bool) { + if start < 0 || start >= end || end < 0 || end > len(*s)-1 || start > len(*s)-1 { + return nil, false + } + taken := (*s)[start:end] + *s = slices.Delete(*s, start, end) + return taken, true +} + +// TakeAllFunc selects all items for which [fn] returns true and removes them from the slice. +func TakeAllFunc[S ~[]E, E any](s *S, fn func(E) bool) (S, bool) { + indexes := []int{} + for ii, v := range *s { + if fn(v) { + indexes = append(indexes, ii) + } + } + if len(indexes) == 0 { + return nil, false + } + out := make(S, 0, len(indexes)) + for _, index := range indexes { + out = append(out, (*s)[index]) + } + *s = slices.DeleteFunc(*s, fn) + return out, true +} + +// Take selects the item and removes it, without bounds checks. +func Take[S ~[]E, E any](s *S, index int) E { + taken := (*s)[index] + *s = slices.Delete(*s, index, index+1) + return taken +} + +// TakeSafe selects the item and removes it, returning true if found. +func TakeSafe[S ~[]E, E any](s *S, index int) (E, bool) { + if index < 0 || index > len(*s)-1 { + return *new(E), false + } + taken := (*s)[index] + *s = slices.Delete(*s, index, index+1) + return taken, true +} + +// TakeFunc selects the first item that satisfies [fn] and removes it. +func TakeFunc[S ~[]E, E any](s *S, fn func(E) bool) (E, bool) { + ii := slices.IndexFunc(*s, fn) + if ii < 0 { + return *new(E), false + } + taken := (*s)[ii] + *s = slices.Delete(*s, ii, ii+1) + return taken, true +} + +// Normalize de-duplicates a slices of comparable elements. +// Doesn't rely on sorting. +func Normalize[S ~[]E, E comparable](s S) S { + s = slices.Clone(s) + seen := map[E]struct{}{} + for _, e := range s { + seen[e] = struct{}{} + } + return slices.Collect(maps.Keys(seen)) +} + +// Flatten reduces a 2D slices to a 1D slice. +func Flatten[E any](s [][]E) []E { + count := 0 + for _, ss := range s { + count += len(ss) + } + + out := make([]E, 0, count) + + for _, ss := range s { + for _, e := range ss { + out = append(out, e) + } + } + + return out +} + +// ToMap converts a slice to a map, using [keyFor] to extract the key from each element. +func ToMap[S ~[]E, E any, K comparable](s S, keyFor func(int, E) K) map[K]E { + return iterx.ToMap(slices.All(s), keyFor) +} diff --git a/slicesx/slicesx_test.go b/slicesx/slicesx_test.go @@ -0,0 +1,218 @@ +package slicesx + +import ( + "slices" + "strconv" + "testing" +) + +func TestMap(t *testing.T) { + list := Generate(3, func(ii int) int { + return ii + }) + + t.Run("MapSafe", func(t *testing.T) { + if _, err := MapSafe(list, func(s int) int { + panic("test panic") + }); err == nil { + t.Fatalf("expected error, not nil") + } + }) + + t.Run("MapIndexSafe", func(t *testing.T) { + if _, err := MapIndexSafe(list, func(ii int, s int) int { + panic("test panic") + }); err == nil { + t.Fatalf("expected error, not nil") + } + }) + + t.Run("MapSafe", func(t *testing.T) { + if got, err := MapSafe(list, func(s int) string { + return strconv.Itoa(s) + }); err != nil { + t.Fatalf("unexpected error, not nil") + } else { + want := []string{"0", "1", "2"} + if !slices.Equal(got, want) { + t.Fatalf("result mismatch: got=%v, want=%v", got, want) + } + } + }) + + t.Run("MapIndexSafe", func(t *testing.T) { + if got, err := MapIndexSafe(list, func(ii int, s int) string { + return strconv.Itoa(s + ii) + }); err != nil { + t.Fatalf("unexpected error, not nil") + } else { + want := []string{"0", "2", "4"} + if !slices.Equal(got, want) { + t.Fatalf("result mismatch: got=%v, want=%v", got, want) + } + } + }) + + t.Run("Map", func(t *testing.T) { + got := Map(list, func(s int) string { + return strconv.Itoa(s) + }) + want := []string{"0", "1", "2"} + if !slices.Equal(got, want) { + t.Fatalf("result mismatch: got=%v, want=%v", got, want) + } + }) + + t.Run("MapIndex", func(t *testing.T) { + got := MapIndex(list, func(ii int, s int) string { + return strconv.Itoa(s + ii) + }) + want := []string{"0", "2", "4"} + if !slices.Equal(got, want) { + t.Fatalf("result mismatch: got=%v, want=%v", got, want) + } + }) +} + +func TestPop(t *testing.T) { + t.Run("PopFront", func(t *testing.T) { + original := Generate(5, func(ii int) int { + return ii + }) + + list := Generate(5, func(ii int) int { + return ii + }) + + for _, want := range original { + length := len(list) + + head, _ := PopFrontSafe(&list) + + if len(list) == length { + t.Fatalf("failed to modify slice") + } + + if head != want { + t.Fatalf("popped wrong value: got=%d, want=%d", head, want) + } + } + + v, ok := PopFrontSafe(&list) + if v != 0 || ok == true { + t.Fatalf("unexpected value from popping empty list: %v", v) + } + }) + + t.Run("PopBack", func(t *testing.T) { + original := Generate(5, func(ii int) int { + return ii + }) + + slices.Reverse(original) + + list := Generate(5, func(ii int) int { + return ii + }) + + for _, want := range original { + length := len(list) + + tail, _ := PopBackSafe(&list) + + if len(list) == length { + t.Fatalf("failed to modify slice") + } + + if tail != want { + t.Fatalf("popped wrong value: got=%d, want=%d", tail, want) + } + } + + v, ok := PopBackSafe(&list) + if v != 0 || ok == true { + t.Fatalf("unexpected value from popping empty list: %v", v) + } + }) +} + +func TestFilterMap(t *testing.T) { + input := []string{ + "0", + "nan", + "1", + "nan", + "2", + "nan", + "3", + "nan", + "4", + "nan", + "5", + "nan", + "6", + "nan", + "7", + "nan", + "8", + "nan", + "9", + } + + got := FilterMap(input, func(v string) (int, bool) { + n, err := strconv.Atoi(v) + return n, err == nil + }) + + want := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} + + if !slices.Equal(got, want) { + t.Fatalf("got=%v, want=%v", got, want) + } +} + +func TestTakeAllFunc(t *testing.T) { + input := Generate(10, func(n int) int { return n }) + + evens, ok := TakeAllFunc(&input, func(n int) bool { + return n%2 == 0 + }) + + if !ok { + t.Fatalf("expected ok, got not ok") + } + + wantEvens := []int{0, 2, 4, 6, 8} + + if !slices.Equal(evens, wantEvens) { + t.Fatalf("got=%v, want=%v", evens, wantEvens) + } + + wantInput := []int{1, 3, 5, 7, 9} + + if !slices.Equal(input, wantInput) { + t.Fatalf("got=%v, want=%v", input, wantInput) + } +} + +func TestTakeFunc(t *testing.T) { + input := Generate(10, func(n int) int { return n }) + + got, ok := TakeFunc(&input, func(n int) bool { + return n == 5 + }) + + if !ok { + t.Fatalf("expected ok, got not ok") + } + + want := 5 + + if want != got { + t.Fatalf("got=%v, want=%v", got, want) + } + + if slices.Contains(input, 5) { + t.Fatalf("failed to remove %d from list: %v", want, input) + } +} diff --git a/syncx/mutex.go b/syncx/mutex.go @@ -0,0 +1,47 @@ +package syncx + +import "sync" + +// Mutex protects a value with a mutex. +type Mutex[T any] struct { + lock sync.Mutex + value T +} + +func NewMutex[T any](v T) *Mutex[T] { + return &Mutex[T]{value: v} +} + +func (m *Mutex[T]) With(fn func(t T)) { + m.lock.Lock() + defer m.lock.Unlock() + fn(m.value) +} + +func (m *Mutex[T]) Update(fn func(t T) T) { + m.lock.Lock() + defer m.lock.Unlock() + m.value = fn(m.value) +} + +func (m *Mutex[T]) Set(value T) { + m.lock.Lock() + defer m.lock.Unlock() + m.value = value +} + +func (m *Mutex[T]) Take() T { + m.lock.Lock() + defer m.lock.Unlock() + v := m.value + m.value = *new(T) + return v +} + +func (m *Mutex[T]) Swap(t T) T { + m.lock.Lock() + defer m.lock.Unlock() + v := m.value + m.value = t + return v +} diff --git a/syncx/pool.go b/syncx/pool.go @@ -0,0 +1,33 @@ +package syncx + +import "sync" + +// Pool wraps a [sync.Pool] providing a type-safe API. +// [validate] is used to optionally ignore [Put] operations. +type Pool[T any] struct { + sync.Pool + allocate func() T + validate func(t T) bool +} + +func NewPool[T any](allocate func() T, validate func(T) bool) *Pool[T] { + return &Pool[T]{ + allocate: allocate, + validate: validate, + Pool: sync.Pool{ + New: func() any { + return allocate() + }, + }, + } +} + +func (p *Pool[T]) Get() T { + return p.Pool.Get().(T) +} + +func (p *Pool[T]) Put(t T) { + if p.validate(t) { + p.Pool.Put(t) + } +} diff --git a/syncx/slice.go b/syncx/slice.go @@ -0,0 +1,59 @@ +package syncx + +import ( + "iter" + "slices" +) + +// Slice is a mutex protected slice. +type Slice[T any] struct { + Value Mutex[[]T] +} + +func (s *Slice[T]) Append(v T) { + s.Value.Update(func(list []T) []T { + return append(list, v) + }) +} + +func (s *Slice[T]) Prepend(v T) { + s.Value.Update(func(list []T) []T { + return append([]T{v}, list...) + }) +} + +func (s *Slice[T]) Delete(start, end int) { + s.Value.Update(func(list []T) []T { + return slices.Delete(list, start, end) + }) +} + +func (s *Slice[T]) Contains(fn func(T) bool) bool { + return s.Index(fn) > -1 +} + +func (s *Slice[T]) Index(fn func(T) bool) int { + _, ii := s.Find(fn) + return ii +} + +func (s *Slice[T]) Find(fn func(T) bool) (T, int) { + for ii, v := range s.Iter() { + if fn(v) { + return v, ii + } + } + return *new(T), -1 +} + +func (s *Slice[T]) Iter() iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + s.Value.With(func(list []T) { + for ii, v := range list { + if !yield(ii, v) { + break + } + } + }) + } +}