diff --git a/v2/array/any.go b/v2/array/any.go index eece62c..9d287cc 100644 --- a/v2/array/any.go +++ b/v2/array/any.go @@ -19,12 +19,28 @@ import ( G "github.com/IBM/fp-go/v2/array/generic" ) -// AnyWithIndex tests if any of the elements in the array matches the predicate +// AnyWithIndex tests if any of the elements in the array matches the predicate. +// The predicate receives both the index and the element. +// Returns true if at least one element satisfies the predicate, false otherwise. +// +// Example: +// +// hasEvenAtEvenIndex := array.AnyWithIndex(func(i, x int) bool { +// return i%2 == 0 && x%2 == 0 +// }) +// result := hasEvenAtEvenIndex([]int{1, 3, 4, 5}) // true (4 is at index 2) func AnyWithIndex[A any](pred func(int, A) bool) func([]A) bool { return G.AnyWithIndex[[]A](pred) } -// Any tests if any of the elements in the array matches the predicate +// Any tests if any of the elements in the array matches the predicate. +// Returns true if at least one element satisfies the predicate, false otherwise. +// Returns false for an empty array. +// +// Example: +// +// hasEven := array.Any(func(x int) bool { return x%2 == 0 }) +// result := hasEven([]int{1, 3, 4, 5}) // true func Any[A any](pred func(A) bool) func([]A) bool { return G.Any[[]A](pred) } diff --git a/v2/array/array.go b/v2/array/array.go index b7fdcbf..cbbe588 100644 --- a/v2/array/array.go +++ b/v2/array/array.go @@ -40,10 +40,14 @@ func Replicate[A any](n int, a A) []A { return G.Replicate[[]A](n, a) } +// MonadMap applies a function to each element of an array, returning a new array with the results. +// This is the monadic version of Map that takes the array as the first parameter. func MonadMap[A, B any](as []A, f func(a A) B) []B { return G.MonadMap[[]A, []B](as, f) } +// MonadMapRef applies a function to a pointer to each element of an array, returning a new array with the results. +// This is useful when you need to access elements by reference without copying. func MonadMapRef[A, B any](as []A, f func(a *A) B) []B { count := len(as) bs := make([]B, count) @@ -53,14 +57,24 @@ func MonadMapRef[A, B any](as []A, f func(a *A) B) []B { return bs } +// MapWithIndex applies a function to each element and its index in an array, returning a new array with the results. func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B { return G.MapWithIndex[[]A, []B](f) } +// Map applies a function to each element of an array, returning a new array with the results. +// This is the curried version that returns a function. +// +// Example: +// +// double := array.Map(func(x int) int { return x * 2 }) +// result := double([]int{1, 2, 3}) // [2, 4, 6] func Map[A, B any](f func(a A) B) func([]A) []B { return G.Map[[]A, []B, A, B](f) } +// MapRef applies a function to a pointer to each element of an array, returning a new array with the results. +// This is the curried version that returns a function. func MapRef[A, B any](f func(a *A) B) func([]A) []B { return F.Bind2nd(MonadMapRef[A, B], f) } @@ -99,14 +113,19 @@ func FilterWithIndex[A any](pred func(int, A) bool) EM.Endomorphism[[]A] { return G.FilterWithIndex[[]A](pred) } +// FilterRef returns a new array with all elements from the original array that match a predicate operating on pointers. func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] { return F.Bind2nd(filterRef[A], pred) } +// MonadFilterMap maps an array with a function that returns an Option and keeps only the Some values. +// This is the monadic version that takes the array as the first parameter. func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B { return G.MonadFilterMap[[]A, []B](fa, f) } +// MonadFilterMapWithIndex maps an array with a function that takes an index and returns an Option, +// keeping only the Some values. This is the monadic version that takes the array as the first parameter. func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) O.Option[B]) []B { return G.MonadFilterMapWithIndex[[]A, []B](fa, f) } @@ -126,6 +145,7 @@ func FilterChain[A, B any](f func(A) O.Option[[]B]) func([]A) []B { return G.FilterChain[[]A](f) } +// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers. func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B { return func(fa []A) []B { return filterMapRef(fa, pred, f) @@ -141,44 +161,62 @@ func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B { return current } +// Reduce folds an array from left to right, applying a function to accumulate a result. +// +// Example: +// +// sum := array.Reduce(func(acc, x int) int { return acc + x }, 0) +// result := sum([]int{1, 2, 3, 4, 5}) // 15 func Reduce[A, B any](f func(B, A) B, initial B) func([]A) B { return G.Reduce[[]A](f, initial) } +// ReduceWithIndex folds an array from left to right with access to the index, +// applying a function to accumulate a result. func ReduceWithIndex[A, B any](f func(int, B, A) B, initial B) func([]A) B { return G.ReduceWithIndex[[]A](f, initial) } +// ReduceRight folds an array from right to left, applying a function to accumulate a result. func ReduceRight[A, B any](f func(A, B) B, initial B) func([]A) B { return G.ReduceRight[[]A](f, initial) } +// ReduceRightWithIndex folds an array from right to left with access to the index, +// applying a function to accumulate a result. func ReduceRightWithIndex[A, B any](f func(int, A, B) B, initial B) func([]A) B { return G.ReduceRightWithIndex[[]A](f, initial) } +// ReduceRef folds an array from left to right using pointers to elements, +// applying a function to accumulate a result. func ReduceRef[A, B any](f func(B, *A) B, initial B) func([]A) B { return func(as []A) B { return reduceRef(as, f, initial) } } +// Append adds an element to the end of an array, returning a new array. func Append[A any](as []A, a A) []A { return G.Append(as, a) } +// IsEmpty checks if an array has no elements. func IsEmpty[A any](as []A) bool { return G.IsEmpty(as) } +// IsNonEmpty checks if an array has at least one element. func IsNonEmpty[A any](as []A) bool { return len(as) > 0 } +// Empty returns an empty array of type A. func Empty[A any]() []A { return G.Empty[[]A]() } +// Zero returns an empty array of type A (alias for Empty). func Zero[A any]() []A { return Empty[A]() } @@ -188,46 +226,70 @@ func Of[A any](a A) []A { return G.Of[[]A](a) } +// MonadChain applies a function that returns an array to each element and flattens the results. +// This is the monadic version that takes the array as the first parameter (also known as FlatMap). func MonadChain[A, B any](fa []A, f func(a A) []B) []B { return G.MonadChain[[]A, []B](fa, f) } +// Chain applies a function that returns an array to each element and flattens the results. +// This is the curried version (also known as FlatMap). +// +// Example: +// +// duplicate := array.Chain(func(x int) []int { return []int{x, x} }) +// result := duplicate([]int{1, 2, 3}) // [1, 1, 2, 2, 3, 3] func Chain[A, B any](f func(A) []B) func([]A) []B { return G.Chain[[]A, []B](f) } +// MonadAp applies an array of functions to an array of values, producing all combinations. +// This is the monadic version that takes both arrays as parameters. func MonadAp[B, A any](fab []func(A) B, fa []A) []B { return G.MonadAp[[]B](fab, fa) } +// Ap applies an array of functions to an array of values, producing all combinations. +// This is the curried version. func Ap[B, A any](fa []A) func([]func(A) B) []B { return G.Ap[[]B, []func(A) B](fa) } +// Match performs pattern matching on an array, calling onEmpty if empty or onNonEmpty if not. func Match[A, B any](onEmpty func() B, onNonEmpty func([]A) B) func([]A) B { return G.Match[[]A](onEmpty, onNonEmpty) } +// MatchLeft performs pattern matching on an array, calling onEmpty if empty or onNonEmpty with head and tail if not. func MatchLeft[A, B any](onEmpty func() B, onNonEmpty func(A, []A) B) func([]A) B { return G.MatchLeft[[]A](onEmpty, onNonEmpty) } +// Tail returns all elements except the first, wrapped in an Option. +// Returns None if the array is empty. func Tail[A any](as []A) O.Option[[]A] { return G.Tail(as) } +// Head returns the first element of an array, wrapped in an Option. +// Returns None if the array is empty. func Head[A any](as []A) O.Option[A] { return G.Head(as) } +// First returns the first element of an array, wrapped in an Option (alias for Head). +// Returns None if the array is empty. func First[A any](as []A) O.Option[A] { return G.First(as) } +// Last returns the last element of an array, wrapped in an Option. +// Returns None if the array is empty. func Last[A any](as []A) O.Option[A] { return G.Last(as) } +// PrependAll inserts a separator before each element of an array. func PrependAll[A any](middle A) EM.Endomorphism[[]A] { return func(as []A) []A { count := len(as) @@ -243,6 +305,11 @@ func PrependAll[A any](middle A) EM.Endomorphism[[]A] { } } +// Intersperse inserts a separator between each element of an array. +// +// Example: +// +// result := array.Intersperse(0)([]int{1, 2, 3}) // [1, 0, 2, 0, 3] func Intersperse[A any](middle A) EM.Endomorphism[[]A] { prepend := PrependAll(middle) return func(as []A) []A { @@ -253,33 +320,47 @@ func Intersperse[A any](middle A) EM.Endomorphism[[]A] { } } +// Intercalate inserts a separator between elements and concatenates them using a Monoid. func Intercalate[A any](m M.Monoid[A]) func(A) func([]A) A { - concatAll := ConcatAll[A](m) return func(middle A) func([]A) A { - return Match(m.Empty, F.Flow2(Intersperse(middle), concatAll)) + return Match(m.Empty, F.Flow2(Intersperse(middle), ConcatAll[A](m))) } } +// Flatten converts a nested array into a flat array by concatenating all inner arrays. +// +// Example: +// +// result := array.Flatten([][]int{{1, 2}, {3, 4}, {5}}) // [1, 2, 3, 4, 5] func Flatten[A any](mma [][]A) []A { return G.Flatten(mma) } +// Slice extracts a subarray from index low (inclusive) to high (exclusive). func Slice[A any](low, high int) func(as []A) []A { return array.Slice[[]A](low, high) } +// Lookup returns the element at the specified index, wrapped in an Option. +// Returns None if the index is out of bounds. func Lookup[A any](idx int) func([]A) O.Option[A] { return G.Lookup[[]A](idx) } +// UpsertAt returns a function that inserts or updates an element at a specific index. +// If the index is out of bounds, the element is appended. func UpsertAt[A any](a A) EM.Endomorphism[[]A] { return G.UpsertAt[[]A](a) } +// Size returns the number of elements in an array. func Size[A any](as []A) int { return G.Size(as) } +// MonadPartition splits an array into two arrays based on a predicate. +// The first array contains elements for which the predicate returns false, +// the second contains elements for which it returns true. func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] { return G.MonadPartition(as, pred) } @@ -305,6 +386,7 @@ func ConstNil[A any]() []A { return array.ConstNil[[]A]() } +// SliceRight extracts a subarray from the specified start index to the end. func SliceRight[A any](start int) EM.Endomorphism[[]A] { return G.SliceRight[[]A](start) } @@ -334,18 +416,24 @@ func Fold[A any](m M.Monoid[A]) func([]A) A { return G.Fold[[]A](m) } +// Push adds an element to the end of an array (alias for Append). func Push[A any](a A) EM.Endomorphism[[]A] { return G.Push[EM.Endomorphism[[]A]](a) } +// MonadFlap applies a value to an array of functions, producing an array of results. +// This is the monadic version that takes both parameters. func MonadFlap[B, A any](fab []func(A) B, a A) []B { return G.MonadFlap[func(A) B, []func(A) B, []B, A, B](fab, a) } +// Flap applies a value to an array of functions, producing an array of results. +// This is the curried version. func Flap[B, A any](a A) func([]func(A) B) []B { return G.Flap[func(A) B, []func(A) B, []B, A, B](a) } +// Prepend adds an element to the beginning of an array, returning a new array. func Prepend[A any](head A) EM.Endomorphism[[]A] { return G.Prepend[EM.Endomorphism[[]A]](head) } diff --git a/v2/array/array_extended_test.go b/v2/array/array_extended_test.go new file mode 100644 index 0000000..51cfd3e --- /dev/null +++ b/v2/array/array_extended_test.go @@ -0,0 +1,325 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package array + +import ( + "fmt" + "testing" + + N "github.com/IBM/fp-go/v2/number" + O "github.com/IBM/fp-go/v2/option" + S "github.com/IBM/fp-go/v2/string" + "github.com/stretchr/testify/assert" +) + +func TestReplicate(t *testing.T) { + result := Replicate(3, "a") + assert.Equal(t, []string{"a", "a", "a"}, result) + + empty := Replicate(0, 42) + assert.Equal(t, []int{}, empty) +} + +func TestMonadMap(t *testing.T) { + src := []int{1, 2, 3} + result := MonadMap(src, func(x int) int { return x * 2 }) + assert.Equal(t, []int{2, 4, 6}, result) +} + +func TestMonadMapRef(t *testing.T) { + src := []int{1, 2, 3} + result := MonadMapRef(src, func(x *int) int { return *x * 2 }) + assert.Equal(t, []int{2, 4, 6}, result) +} + +func TestMapWithIndex(t *testing.T) { + src := []string{"a", "b", "c"} + mapper := MapWithIndex(func(i int, s string) string { + return fmt.Sprintf("%d:%s", i, s) + }) + result := mapper(src) + assert.Equal(t, []string{"0:a", "1:b", "2:c"}, result) +} + +func TestMapRef(t *testing.T) { + src := []int{1, 2, 3} + mapper := MapRef(func(x *int) int { return *x * 2 }) + result := mapper(src) + assert.Equal(t, []int{2, 4, 6}, result) +} + +func TestFilterWithIndex(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + filter := FilterWithIndex(func(i, x int) bool { + return i%2 == 0 && x > 2 + }) + result := filter(src) + assert.Equal(t, []int{3, 5}, result) +} + +func TestFilterRef(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + filter := FilterRef(func(x *int) bool { return *x > 2 }) + result := filter(src) + assert.Equal(t, []int{3, 4, 5}, result) +} + +func TestMonadFilterMap(t *testing.T) { + src := []int{1, 2, 3, 4} + result := MonadFilterMap(src, func(x int) O.Option[string] { + if x%2 == 0 { + return O.Some(fmt.Sprintf("even:%d", x)) + } + return O.None[string]() + }) + assert.Equal(t, []string{"even:2", "even:4"}, result) +} + +func TestMonadFilterMapWithIndex(t *testing.T) { + src := []int{1, 2, 3, 4} + result := MonadFilterMapWithIndex(src, func(i, x int) O.Option[string] { + if i%2 == 0 { + return O.Some(fmt.Sprintf("%d:%d", i, x)) + } + return O.None[string]() + }) + assert.Equal(t, []string{"0:1", "2:3"}, result) +} + +func TestFilterMapWithIndex(t *testing.T) { + src := []int{1, 2, 3, 4} + filter := FilterMapWithIndex(func(i, x int) O.Option[string] { + if i%2 == 0 { + return O.Some(fmt.Sprintf("%d:%d", i, x)) + } + return O.None[string]() + }) + result := filter(src) + assert.Equal(t, []string{"0:1", "2:3"}, result) +} + +func TestFilterMapRef(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + filter := FilterMapRef( + func(x *int) bool { return *x > 2 }, + func(x *int) string { return fmt.Sprintf("val:%d", *x) }, + ) + result := filter(src) + assert.Equal(t, []string{"val:3", "val:4", "val:5"}, result) +} + +func TestReduceWithIndex(t *testing.T) { + src := []int{1, 2, 3} + reducer := ReduceWithIndex(func(i, acc, x int) int { + return acc + i + x + }, 0) + result := reducer(src) + assert.Equal(t, 9, result) // 0 + (0+1) + (1+2) + (2+3) = 9 +} + +func TestReduceRightWithIndex(t *testing.T) { + src := []string{"a", "b", "c"} + reducer := ReduceRightWithIndex(func(i int, x, acc string) string { + return fmt.Sprintf("%s%d:%s", acc, i, x) + }, "") + result := reducer(src) + assert.Equal(t, "2:c1:b0:a", result) +} + +func TestReduceRef(t *testing.T) { + src := []int{1, 2, 3} + reducer := ReduceRef(func(acc int, x *int) int { + return acc + *x + }, 0) + result := reducer(src) + assert.Equal(t, 6, result) +} + +func TestZero(t *testing.T) { + result := Zero[int]() + assert.Equal(t, []int{}, result) + assert.True(t, IsEmpty(result)) +} + +func TestMonadChain(t *testing.T) { + src := []int{1, 2, 3} + result := MonadChain(src, func(x int) []int { + return []int{x, x * 10} + }) + assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result) +} + +func TestChain(t *testing.T) { + src := []int{1, 2, 3} + chain := Chain(func(x int) []int { + return []int{x, x * 10} + }) + result := chain(src) + assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result) +} + +func TestMonadAp(t *testing.T) { + fns := []func(int) int{ + func(x int) int { return x * 2 }, + func(x int) int { return x + 10 }, + } + values := []int{1, 2} + result := MonadAp(fns, values) + assert.Equal(t, []int{2, 4, 11, 12}, result) +} + +func TestMatchLeft(t *testing.T) { + matcher := MatchLeft( + func() string { return "empty" }, + func(head int, tail []int) string { + return fmt.Sprintf("head:%d,tail:%v", head, tail) + }, + ) + + assert.Equal(t, "empty", matcher([]int{})) + assert.Equal(t, "head:1,tail:[2 3]", matcher([]int{1, 2, 3})) +} + +func TestTail(t *testing.T) { + assert.Equal(t, O.None[[]int](), Tail([]int{})) + assert.Equal(t, O.Some([]int{2, 3}), Tail([]int{1, 2, 3})) + assert.Equal(t, O.Some([]int{}), Tail([]int{1})) +} + +func TestFirst(t *testing.T) { + assert.Equal(t, O.None[int](), First([]int{})) + assert.Equal(t, O.Some(1), First([]int{1, 2, 3})) +} + +func TestLast(t *testing.T) { + assert.Equal(t, O.None[int](), Last([]int{})) + assert.Equal(t, O.Some(3), Last([]int{1, 2, 3})) + assert.Equal(t, O.Some(1), Last([]int{1})) +} + +func TestUpsertAt(t *testing.T) { + src := []int{1, 2, 3} + upsert := UpsertAt(99) + + result1 := upsert(src) + assert.Equal(t, []int{1, 2, 3, 99}, result1) +} + +func TestSize(t *testing.T) { + assert.Equal(t, 0, Size([]int{})) + assert.Equal(t, 3, Size([]int{1, 2, 3})) +} + +func TestMonadPartition(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + result := MonadPartition(src, func(x int) bool { return x > 2 }) + assert.Equal(t, []int{1, 2}, result.F1) + assert.Equal(t, []int{3, 4, 5}, result.F2) +} + +func TestIsNil(t *testing.T) { + var nilSlice []int + assert.True(t, IsNil(nilSlice)) + assert.False(t, IsNil([]int{})) + assert.False(t, IsNil([]int{1})) +} + +func TestIsNonNil(t *testing.T) { + var nilSlice []int + assert.False(t, IsNonNil(nilSlice)) + assert.True(t, IsNonNil([]int{})) + assert.True(t, IsNonNil([]int{1})) +} + +func TestConstNil(t *testing.T) { + result := ConstNil[int]() + assert.True(t, IsNil(result)) +} + +func TestSliceRight(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + slicer := SliceRight[int](2) + result := slicer(src) + assert.Equal(t, []int{3, 4, 5}, result) +} + +func TestCopy(t *testing.T) { + src := []int{1, 2, 3} + copied := Copy(src) + assert.Equal(t, src, copied) + // Verify it's a different slice + copied[0] = 99 + assert.Equal(t, 1, src[0]) + assert.Equal(t, 99, copied[0]) +} + +func TestClone(t *testing.T) { + src := []int{1, 2, 3} + cloner := Clone(func(x int) int { return x * 2 }) + result := cloner(src) + assert.Equal(t, []int{2, 4, 6}, result) +} + +func TestFoldMapWithIndex(t *testing.T) { + src := []string{"a", "b", "c"} + folder := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string { + return fmt.Sprintf("%d:%s", i, s) + }) + result := folder(src) + assert.Equal(t, "0:a1:b2:c", result) +} + +func TestFold(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + folder := Fold(N.MonoidSum[int]()) + result := folder(src) + assert.Equal(t, 15, result) +} + +func TestPush(t *testing.T) { + src := []int{1, 2, 3} + pusher := Push(4) + result := pusher(src) + assert.Equal(t, []int{1, 2, 3, 4}, result) +} + +func TestMonadFlap(t *testing.T) { + fns := []func(int) string{ + func(x int) string { return fmt.Sprintf("a%d", x) }, + func(x int) string { return fmt.Sprintf("b%d", x) }, + } + result := MonadFlap(fns, 5) + assert.Equal(t, []string{"a5", "b5"}, result) +} + +func TestFlap(t *testing.T) { + fns := []func(int) string{ + func(x int) string { return fmt.Sprintf("a%d", x) }, + func(x int) string { return fmt.Sprintf("b%d", x) }, + } + flapper := Flap[string](5) + result := flapper(fns) + assert.Equal(t, []string{"a5", "b5"}, result) +} + +func TestPrepend(t *testing.T) { + src := []int{2, 3, 4} + prepender := Prepend(1) + result := prepender(src) + assert.Equal(t, []int{1, 2, 3, 4}, result) +} + +// Made with Bob diff --git a/v2/array/array_test.go b/v2/array/array_test.go index d9ac217..7472066 100644 --- a/v2/array/array_test.go +++ b/v2/array/array_test.go @@ -113,6 +113,17 @@ func TestIntercalate(t *testing.T) { assert.Equal(t, "a-b-c-d", is([]string{"a", "b", "c", "d"})) } +func TestIntersperse(t *testing.T) { + // Test with empty array + assert.Equal(t, []int{}, Intersperse(0)([]int{})) + + // Test with single element + assert.Equal(t, []int{1}, Intersperse(0)([]int{1})) + + // Test with multiple elements + assert.Equal(t, []int{1, 0, 2, 0, 3}, Intersperse(0)([]int{1, 2, 3})) +} + func TestPrependAll(t *testing.T) { empty := Empty[int]() prep := PrependAll(0) diff --git a/v2/array/bind.go b/v2/array/bind.go index 96b0a3e..b8a39b8 100644 --- a/v2/array/bind.go +++ b/v2/array/bind.go @@ -19,14 +19,37 @@ import ( G "github.com/IBM/fp-go/v2/array/generic" ) -// Bind creates an empty context of type [S] to be used with the [Bind] operation +// Do creates an empty context of type S to be used with the Bind operation. +// This is the starting point for monadic do-notation style computations. +// +// Example: +// +// type State struct { +// X int +// Y int +// } +// result := array.Do(State{}) func Do[S any]( empty S, ) []S { return G.Do[[]S, S](empty) } -// Bind attaches the result of a computation to a context [S1] to produce a context [S2] +// Bind attaches the result of a computation to a context S1 to produce a context S2. +// The setter function defines how to update the context with the computation result. +// This enables monadic composition where each step can produce multiple results. +// +// Example: +// +// result := F.Pipe2( +// array.Do(struct{ X, Y int }{}), +// array.Bind( +// func(x int) func(s struct{}) struct{ X int } { +// return func(s struct{}) struct{ X int } { return struct{ X int }{x} } +// }, +// func(s struct{}) []int { return []int{1, 2} }, +// ), +// ) func Bind[S1, S2, T any]( setter func(T) func(S1) S2, f func(S1) []T, @@ -34,7 +57,19 @@ func Bind[S1, S2, T any]( return G.Bind[[]S1, []S2, []T, S1, S2, T](setter, f) } -// Let attaches the result of a computation to a context [S1] to produce a context [S2] +// Let attaches the result of a pure computation to a context S1 to produce a context S2. +// Unlike Bind, the computation function returns a plain value T rather than []T. +// +// Example: +// +// result := array.Let( +// func(sum int) func(s struct{ X int }) struct{ X, Sum int } { +// return func(s struct{ X int }) struct{ X, Sum int } { +// return struct{ X, Sum int }{s.X, sum} +// } +// }, +// func(s struct{ X int }) int { return s.X * 2 }, +// ) func Let[S1, S2, T any]( setter func(T) func(S1) S2, f func(S1) T, @@ -42,7 +77,19 @@ func Let[S1, S2, T any]( return G.Let[[]S1, []S2, S1, S2, T](setter, f) } -// LetTo attaches the a value to a context [S1] to produce a context [S2] +// LetTo attaches a constant value to a context S1 to produce a context S2. +// This is useful for adding constant values to the context. +// +// Example: +// +// result := array.LetTo( +// func(name string) func(s struct{ X int }) struct{ X int; Name string } { +// return func(s struct{ X int }) struct{ X int; Name string } { +// return struct{ X int; Name string }{s.X, name} +// } +// }, +// "constant", +// ) func LetTo[S1, S2, T any]( setter func(T) func(S1) S2, b T, @@ -50,14 +97,37 @@ func LetTo[S1, S2, T any]( return G.LetTo[[]S1, []S2, S1, S2, T](setter, b) } -// BindTo initializes a new state [S1] from a value [T] +// BindTo initializes a new state S1 from a value T. +// This is typically the first operation after Do to start building the context. +// +// Example: +// +// result := F.Pipe2( +// []int{1, 2, 3}, +// array.BindTo(func(x int) struct{ X int } { +// return struct{ X int }{x} +// }), +// ) func BindTo[S1, T any]( setter func(T) S1, ) func([]T) []S1 { return G.BindTo[[]S1, []T, S1, T](setter) } -// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently +// ApS attaches a value to a context S1 to produce a context S2 by considering +// the context and the value concurrently (using applicative semantics). +// This produces all combinations of context values and array values. +// +// Example: +// +// result := array.ApS( +// func(y int) func(s struct{ X int }) struct{ X, Y int } { +// return func(s struct{ X int }) struct{ X, Y int } { +// return struct{ X, Y int }{s.X, y} +// } +// }, +// []int{10, 20}, +// ) func ApS[S1, S2, T any]( setter func(T) func(S1) S2, fa []T, diff --git a/v2/array/bind_extended_test.go b/v2/array/bind_extended_test.go new file mode 100644 index 0000000..de13882 --- /dev/null +++ b/v2/array/bind_extended_test.go @@ -0,0 +1,80 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package array + +import ( + "testing" + + F "github.com/IBM/fp-go/v2/function" + "github.com/stretchr/testify/assert" +) + +type TestState1 struct { + X int +} + +type TestState2 struct { + X int + Y int +} + +func TestLet(t *testing.T) { + result := F.Pipe2( + Do(TestState1{}), + Let( + func(y int) func(s TestState1) TestState2 { + return func(s TestState1) TestState2 { + return TestState2{X: s.X, Y: y} + } + }, + func(s TestState1) int { return s.X * 2 }, + ), + Map(func(s TestState2) int { return s.X + s.Y }), + ) + + assert.Equal(t, []int{0}, result) +} + +func TestLetTo(t *testing.T) { + result := F.Pipe2( + Do(TestState1{X: 5}), + LetTo( + func(y int) func(s TestState1) TestState2 { + return func(s TestState1) TestState2 { + return TestState2{X: s.X, Y: y} + } + }, + 42, + ), + Map(func(s TestState2) int { return s.X + s.Y }), + ) + + assert.Equal(t, []int{47}, result) +} + +func TestBindTo(t *testing.T) { + result := F.Pipe1( + []int{1, 2, 3}, + BindTo(func(x int) TestState1 { + return TestState1{X: x} + }), + ) + + expected := []TestState1{{X: 1}, {X: 2}, {X: 3}} + assert.Equal(t, expected, result) +} + +// Made with Bob diff --git a/v2/array/coverage.out b/v2/array/coverage.out new file mode 100644 index 0000000..9d114aa --- /dev/null +++ b/v2/array/coverage.out @@ -0,0 +1,134 @@ +mode: set +github.com/IBM/fp-go/v2/array/any.go:32.65,34.2 1 1 +github.com/IBM/fp-go/v2/array/any.go:44.51,46.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:29.33,31.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:34.52,36.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:39.39,41.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:45.52,47.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:51.56,54.34 3 1 +github.com/IBM/fp-go/v2/array/array.go:54.34,56.3 1 1 +github.com/IBM/fp-go/v2/array/array.go:57.2,57.11 1 1 +github.com/IBM/fp-go/v2/array/array.go:61.61,63.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:72.49,74.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:78.53,80.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:82.57,85.29 3 1 +github.com/IBM/fp-go/v2/array/array.go:85.29,87.15 2 1 +github.com/IBM/fp-go/v2/array/array.go:87.15,89.4 1 1 +github.com/IBM/fp-go/v2/array/array.go:91.2,91.15 1 1 +github.com/IBM/fp-go/v2/array/array.go:94.79,97.29 3 1 +github.com/IBM/fp-go/v2/array/array.go:97.29,99.15 2 1 +github.com/IBM/fp-go/v2/array/array.go:99.15,101.4 1 1 +github.com/IBM/fp-go/v2/array/array.go:103.2,103.15 1 1 +github.com/IBM/fp-go/v2/array/array.go:107.60,109.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:112.74,114.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:117.64,119.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:123.66,125.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:129.80,131.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:134.63,136.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:139.77,141.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:144.67,146.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:149.81,150.26 1 1 +github.com/IBM/fp-go/v2/array/array.go:150.26,152.3 1 1 +github.com/IBM/fp-go/v2/array/array.go:155.64,158.29 3 1 +github.com/IBM/fp-go/v2/array/array.go:158.29,160.3 1 1 +github.com/IBM/fp-go/v2/array/array.go:161.2,161.16 1 1 +github.com/IBM/fp-go/v2/array/array.go:170.62,172.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:176.76,178.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:181.67,183.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:187.81,189.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:193.66,194.24 1 1 +github.com/IBM/fp-go/v2/array/array.go:194.24,196.3 1 1 +github.com/IBM/fp-go/v2/array/array.go:200.37,202.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:205.34,207.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:210.37,212.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:215.25,217.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:220.24,222.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:225.25,227.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:231.56,233.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:242.51,244.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:248.53,250.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:254.49,256.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:259.76,261.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:264.83,266.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:270.40,272.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:276.38,278.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:282.39,284.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:288.38,290.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:293.55,294.26 1 1 +github.com/IBM/fp-go/v2/array/array.go:294.26,298.35 4 1 +github.com/IBM/fp-go/v2/array/array.go:298.35,303.4 4 1 +github.com/IBM/fp-go/v2/array/array.go:304.3,304.16 1 1 +github.com/IBM/fp-go/v2/array/array.go:313.56,315.26 2 1 +github.com/IBM/fp-go/v2/array/array.go:315.26,316.18 1 1 +github.com/IBM/fp-go/v2/array/array.go:316.18,318.4 1 0 +github.com/IBM/fp-go/v2/array/array.go:319.3,319.25 1 1 +github.com/IBM/fp-go/v2/array/array.go:324.60,326.36 2 1 +github.com/IBM/fp-go/v2/array/array.go:326.36,328.3 1 1 +github.com/IBM/fp-go/v2/array/array.go:336.36,338.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:341.51,343.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:347.51,349.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:353.48,355.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:358.30,360.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:365.78,367.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:371.75,373.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:376.32,378.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:381.35,383.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:386.28,388.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:391.56,393.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:396.29,398.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:401.49,403.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:406.67,408.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:411.81,413.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:416.45,418.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:421.44,423.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:427.52,429.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:433.48,435.2 1 1 +github.com/IBM/fp-go/v2/array/array.go:438.50,440.2 1 1 +github.com/IBM/fp-go/v2/array/bind.go:34.7,36.2 1 1 +github.com/IBM/fp-go/v2/array/bind.go:56.19,58.2 1 1 +github.com/IBM/fp-go/v2/array/bind.go:76.19,78.2 1 1 +github.com/IBM/fp-go/v2/array/bind.go:96.19,98.2 1 1 +github.com/IBM/fp-go/v2/array/bind.go:113.18,115.2 1 1 +github.com/IBM/fp-go/v2/array/bind.go:134.19,136.2 1 1 +github.com/IBM/fp-go/v2/array/eq.go:22.66,23.29 1 1 +github.com/IBM/fp-go/v2/array/eq.go:23.29,25.3 1 1 +github.com/IBM/fp-go/v2/array/eq.go:26.2,26.26 1 1 +github.com/IBM/fp-go/v2/array/eq.go:26.26,28.18 2 1 +github.com/IBM/fp-go/v2/array/eq.go:28.18,30.4 1 1 +github.com/IBM/fp-go/v2/array/eq.go:32.2,32.13 1 1 +github.com/IBM/fp-go/v2/array/eq.go:46.37,48.49 2 1 +github.com/IBM/fp-go/v2/array/eq.go:48.49,50.3 1 1 +github.com/IBM/fp-go/v2/array/find.go:31.64,33.2 1 1 +github.com/IBM/fp-go/v2/array/find.go:44.78,46.2 1 1 +github.com/IBM/fp-go/v2/array/find.go:62.76,64.2 1 1 +github.com/IBM/fp-go/v2/array/find.go:68.90,70.2 1 1 +github.com/IBM/fp-go/v2/array/find.go:79.63,81.2 1 1 +github.com/IBM/fp-go/v2/array/find.go:85.77,87.2 1 1 +github.com/IBM/fp-go/v2/array/find.go:91.75,93.2 1 1 +github.com/IBM/fp-go/v2/array/find.go:97.89,99.2 1 1 +github.com/IBM/fp-go/v2/array/magma.go:36.50,38.2 1 1 +github.com/IBM/fp-go/v2/array/monad.go:37.65,39.2 1 1 +github.com/IBM/fp-go/v2/array/monoid.go:24.41,27.13 2 1 +github.com/IBM/fp-go/v2/array/monoid.go:27.13,29.3 1 1 +github.com/IBM/fp-go/v2/array/monoid.go:30.2,31.13 2 1 +github.com/IBM/fp-go/v2/array/monoid.go:31.13,33.3 1 1 +github.com/IBM/fp-go/v2/array/monoid.go:35.2,37.12 3 1 +github.com/IBM/fp-go/v2/array/monoid.go:48.36,50.2 1 1 +github.com/IBM/fp-go/v2/array/monoid.go:59.42,61.2 1 1 +github.com/IBM/fp-go/v2/array/monoid.go:63.45,65.2 1 1 +github.com/IBM/fp-go/v2/array/monoid.go:79.45,84.48 3 1 +github.com/IBM/fp-go/v2/array/monoid.go:84.48,86.3 1 1 +github.com/IBM/fp-go/v2/array/monoid.go:88.2,88.12 1 1 +github.com/IBM/fp-go/v2/array/sequence.go:62.22,64.47 2 1 +github.com/IBM/fp-go/v2/array/sequence.go:64.47,69.3 1 1 +github.com/IBM/fp-go/v2/array/sequence.go:91.61,97.2 1 1 +github.com/IBM/fp-go/v2/array/sort.go:33.49,35.2 1 1 +github.com/IBM/fp-go/v2/array/sort.go:61.70,63.2 1 1 +github.com/IBM/fp-go/v2/array/sort.go:90.53,92.2 1 1 +github.com/IBM/fp-go/v2/array/traverse.go:60.34,62.2 1 1 +github.com/IBM/fp-go/v2/array/traverse.go:75.24,78.2 1 1 +github.com/IBM/fp-go/v2/array/uniq.go:18.43,20.2 1 1 +github.com/IBM/fp-go/v2/array/uniq.go:45.62,47.2 1 1 +github.com/IBM/fp-go/v2/array/zip.go:36.73,38.2 1 1 +github.com/IBM/fp-go/v2/array/zip.go:54.55,56.2 1 1 +github.com/IBM/fp-go/v2/array/zip.go:75.62,77.2 1 1 diff --git a/v2/array/doc.go b/v2/array/doc.go new file mode 100644 index 0000000..128df90 --- /dev/null +++ b/v2/array/doc.go @@ -0,0 +1,253 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package array provides functional programming utilities for working with Go slices. +// +// This package treats Go slices as immutable arrays and provides a rich set of operations +// for transforming, filtering, folding, and combining arrays in a functional style. +// All operations return new arrays rather than modifying existing ones. +// +// # Core Concepts +// +// The array package implements several functional programming abstractions: +// - Functor: Transform array elements with Map +// - Applicative: Apply functions in arrays to values in arrays +// - Monad: Chain operations that produce arrays with Chain/FlatMap +// - Foldable: Reduce arrays to single values with Reduce/Fold +// - Traversable: Transform arrays while preserving structure +// +// # Basic Operations +// +// // Creating arrays +// arr := array.From(1, 2, 3, 4, 5) +// repeated := array.Replicate(3, "hello") +// generated := array.MakeBy(5, func(i int) int { return i * 2 }) +// +// // Transforming arrays +// doubled := array.Map(func(x int) int { return x * 2 })(arr) +// filtered := array.Filter(func(x int) bool { return x > 2 })(arr) +// +// // Combining arrays +// combined := array.Flatten([][]int{{1, 2}, {3, 4}}) +// zipped := array.Zip([]string{"a", "b"})([]int{1, 2}) +// +// # Mapping and Filtering +// +// Transform array elements with Map, or filter elements with Filter: +// +// numbers := []int{1, 2, 3, 4, 5} +// +// // Map transforms each element +// doubled := array.Map(func(x int) int { return x * 2 })(numbers) +// // Result: [2, 4, 6, 8, 10] +// +// // Filter keeps elements matching a predicate +// evens := array.Filter(func(x int) bool { return x%2 == 0 })(numbers) +// // Result: [2, 4] +// +// // FilterMap combines both operations +// import "github.com/IBM/fp-go/v2/option" +// result := array.FilterMap(func(x int) option.Option[int] { +// if x%2 == 0 { +// return option.Some(x * 2) +// } +// return option.None[int]() +// })(numbers) +// // Result: [4, 8] +// +// # Folding and Reducing +// +// Reduce arrays to single values: +// +// numbers := []int{1, 2, 3, 4, 5} +// +// // Sum all elements +// sum := array.Reduce(func(acc, x int) int { return acc + x }, 0)(numbers) +// // Result: 15 +// +// // Using a Monoid +// import "github.com/IBM/fp-go/v2/monoid" +// sum := array.Fold(monoid.MonoidSum[int]())(numbers) +// // Result: 15 +// +// # Chaining Operations +// +// Chain operations that produce arrays (also known as FlatMap): +// +// numbers := []int{1, 2, 3} +// result := array.Chain(func(x int) []int { +// return []int{x, x * 10} +// })(numbers) +// // Result: [1, 10, 2, 20, 3, 30] +// +// # Finding Elements +// +// Search for elements matching predicates: +// +// numbers := []int{1, 2, 3, 4, 5} +// +// // Find first element > 3 +// first := array.FindFirst(func(x int) bool { return x > 3 })(numbers) +// // Result: Some(4) +// +// // Find last element > 3 +// last := array.FindLast(func(x int) bool { return x > 3 })(numbers) +// // Result: Some(5) +// +// // Get head and tail +// head := array.Head(numbers) // Some(1) +// tail := array.Tail(numbers) // Some([2, 3, 4, 5]) +// +// # Sorting +// +// Sort arrays using Ord instances: +// +// import "github.com/IBM/fp-go/v2/ord" +// +// numbers := []int{3, 1, 4, 1, 5} +// sorted := array.Sort(ord.FromStrictCompare[int]())(numbers) +// // Result: [1, 1, 3, 4, 5] +// +// // Sort by extracted key +// type Person struct { Name string; Age int } +// people := []Person{{"Alice", 30}, {"Bob", 25}} +// byAge := array.SortByKey(ord.FromStrictCompare[int](), func(p Person) int { +// return p.Age +// })(people) +// +// # Uniqueness +// +// Remove duplicate elements: +// +// numbers := []int{1, 2, 2, 3, 3, 3} +// unique := array.StrictUniq(numbers) +// // Result: [1, 2, 3] +// +// // Unique by key +// type Person struct { Name string; Age int } +// people := []Person{{"Alice", 30}, {"Bob", 25}, {"Alice", 35}} +// uniqueByName := array.Uniq(func(p Person) string { return p.Name })(people) +// // Result: [{"Alice", 30}, {"Bob", 25}] +// +// # Zipping +// +// Combine multiple arrays: +// +// names := []string{"Alice", "Bob", "Charlie"} +// ages := []int{30, 25, 35} +// +// // Zip into tuples +// pairs := array.Zip(ages)(names) +// // Result: [(Alice, 30), (Bob, 25), (Charlie, 35)] +// +// // Zip with custom function +// result := array.ZipWith(names, ages, func(name string, age int) string { +// return fmt.Sprintf("%s is %d", name, age) +// }) +// +// # Monadic Do Notation +// +// Build complex array computations using do-notation style: +// +// result := array.Do( +// struct{ X, Y int }{}, +// )( +// array.Bind( +// func(x int) func(s struct{}) struct{ X int } { +// return func(s struct{}) struct{ X int } { return struct{ X int }{x} } +// }, +// func(s struct{}) []int { return []int{1, 2, 3} }, +// ), +// array.Bind( +// func(y int) func(s struct{ X int }) struct{ X, Y int } { +// return func(s struct{ X int }) struct{ X, Y int } { +// return struct{ X, Y int }{s.X, y} +// } +// }, +// func(s struct{ X int }) []int { return []int{4, 5} }, +// ), +// ) +// // Produces all combinations: [{1,4}, {1,5}, {2,4}, {2,5}, {3,4}, {3,5}] +// +// # Sequence and Traverse +// +// Transform arrays of effects into effects of arrays: +// +// import "github.com/IBM/fp-go/v2/option" +// +// // Sequence: []Option[A] -> Option[[]A] +// opts := []option.Option[int]{ +// option.Some(1), +// option.Some(2), +// option.Some(3), +// } +// result := array.ArrayOption[int]()(opts) +// // Result: Some([1, 2, 3]) +// +// // If any is None, result is None +// opts2 := []option.Option[int]{ +// option.Some(1), +// option.None[int](), +// option.Some(3), +// } +// result2 := array.ArrayOption[int]()(opts2) +// // Result: None +// +// # Equality and Comparison +// +// Compare arrays for equality: +// +// import "github.com/IBM/fp-go/v2/eq" +// +// eq := array.Eq(eq.FromStrictEquals[int]()) +// equal := eq.Equals([]int{1, 2, 3}, []int{1, 2, 3}) +// // Result: true +// +// # Monoid Operations +// +// Combine arrays using monoid operations: +// +// import "github.com/IBM/fp-go/v2/monoid" +// +// // Concatenate arrays +// m := array.Monoid[int]() +// result := m.Concat([]int{1, 2}, []int{3, 4}) +// // Result: [1, 2, 3, 4] +// +// // Concatenate multiple arrays efficiently +// result := array.ArrayConcatAll( +// []int{1, 2}, +// []int{3, 4}, +// []int{5, 6}, +// ) +// // Result: [1, 2, 3, 4, 5, 6] +// +// # Performance Considerations +// +// Most operations create new arrays rather than modifying existing ones. For performance-critical +// code, consider: +// - Using Copy for shallow copies when needed +// - Using Clone with a custom cloning function for deep copies +// - Batching operations to minimize intermediate allocations +// - Using ArrayConcatAll for efficient concatenation of multiple arrays +// +// # Subpackages +// +// - array/generic: Generic implementations for custom array-like types +// - array/nonempty: Operations for non-empty arrays with compile-time guarantees +// - array/testing: Testing utilities for array laws and properties +package array + +// Made with Bob diff --git a/v2/array/eq.go b/v2/array/eq.go index 12e3f47..4e01188 100644 --- a/v2/array/eq.go +++ b/v2/array/eq.go @@ -32,6 +32,17 @@ func equals[T any](left []T, right []T, eq func(T, T) bool) bool { return true } +// Eq creates an equality checker for arrays given an equality checker for elements. +// Two arrays are considered equal if they have the same length and all corresponding +// elements are equal according to the provided Eq instance. +// +// Example: +// +// import "github.com/IBM/fp-go/v2/eq" +// +// intArrayEq := array.Eq(eq.FromStrictEquals[int]()) +// result := intArrayEq.Equals([]int{1, 2, 3}, []int{1, 2, 3}) // true +// result2 := intArrayEq.Equals([]int{1, 2, 3}, []int{1, 2, 4}) // false func Eq[T any](e E.Eq[T]) E.Eq[[]T] { eq := e.Equals return E.FromEquals(func(left, right []T) bool { diff --git a/v2/array/eq_test.go b/v2/array/eq_test.go new file mode 100644 index 0000000..661b55c --- /dev/null +++ b/v2/array/eq_test.go @@ -0,0 +1,46 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package array + +import ( + "testing" + + E "github.com/IBM/fp-go/v2/eq" + "github.com/stretchr/testify/assert" +) + +func TestEq(t *testing.T) { + intEq := Eq(E.FromStrictEquals[int]()) + + // Test equal arrays + assert.True(t, intEq.Equals([]int{1, 2, 3}, []int{1, 2, 3})) + + // Test different lengths + assert.False(t, intEq.Equals([]int{1, 2, 3}, []int{1, 2})) + + // Test different values + assert.False(t, intEq.Equals([]int{1, 2, 3}, []int{1, 2, 4})) + + // Test empty arrays + assert.True(t, intEq.Equals([]int{}, []int{})) + + // Test string arrays + stringEq := Eq(E.FromStrictEquals[string]()) + assert.True(t, stringEq.Equals([]string{"a", "b"}, []string{"a", "b"})) + assert.False(t, stringEq.Equals([]string{"a", "b"}, []string{"a", "c"})) +} + +// Made with Bob diff --git a/v2/array/find.go b/v2/array/find.go index 89e1fc1..60f4192 100644 --- a/v2/array/find.go +++ b/v2/array/find.go @@ -20,42 +20,80 @@ import ( O "github.com/IBM/fp-go/v2/option" ) -// FindFirst finds the first element which satisfies a predicate (or a refinement) function +// FindFirst finds the first element which satisfies a predicate function. +// Returns Some(element) if found, None if no element matches. +// +// Example: +// +// findGreaterThan3 := array.FindFirst(func(x int) bool { return x > 3 }) +// result := findGreaterThan3([]int{1, 2, 4, 5}) // Some(4) +// result2 := findGreaterThan3([]int{1, 2, 3}) // None func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] { return G.FindFirst[[]A](pred) } -// FindFirstWithIndex finds the first element which satisfies a predicate (or a refinement) function +// FindFirstWithIndex finds the first element which satisfies a predicate function that also receives the index. +// Returns Some(element) if found, None if no element matches. +// +// Example: +// +// findEvenAtEvenIndex := array.FindFirstWithIndex(func(i, x int) bool { +// return i%2 == 0 && x%2 == 0 +// }) +// result := findEvenAtEvenIndex([]int{1, 3, 4, 5}) // Some(4) func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] { return G.FindFirstWithIndex[[]A](pred) } -// FindFirstMap finds the first element returned by an [O.Option] based selector function +// FindFirstMap finds the first element for which the selector function returns Some. +// This combines finding and mapping in a single operation. +// +// Example: +// +// import "strconv" +// +// parseFirst := array.FindFirstMap(func(s string) option.Option[int] { +// if n, err := strconv.Atoi(s); err == nil { +// return option.Some(n) +// } +// return option.None[int]() +// }) +// result := parseFirst([]string{"a", "42", "b"}) // Some(42) func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] { return G.FindFirstMap[[]A](sel) } -// FindFirstMapWithIndex finds the first element returned by an [O.Option] based selector function +// FindFirstMapWithIndex finds the first element for which the selector function returns Some. +// The selector receives both the index and the element. func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] { return G.FindFirstMapWithIndex[[]A](sel) } -// FindLast finds the Last element which satisfies a predicate (or a refinement) function +// FindLast finds the last element which satisfies a predicate function. +// Returns Some(element) if found, None if no element matches. +// +// Example: +// +// findGreaterThan3 := array.FindLast(func(x int) bool { return x > 3 }) +// result := findGreaterThan3([]int{1, 4, 2, 5}) // Some(5) func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] { return G.FindLast[[]A](pred) } -// FindLastWithIndex finds the Last element which satisfies a predicate (or a refinement) function +// FindLastWithIndex finds the last element which satisfies a predicate function that also receives the index. +// Returns Some(element) if found, None if no element matches. func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] { return G.FindLastWithIndex[[]A](pred) } -// FindLastMap finds the Last element returned by an [O.Option] based selector function +// FindLastMap finds the last element for which the selector function returns Some. +// This combines finding and mapping in a single operation, searching from the end. func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] { return G.FindLastMap[[]A](sel) } -// FindLastMapWithIndex finds the Last element returned by an [O.Option] based selector function +// FindLastMapWithIndex finds the last element for which the selector function returns Some. +// The selector receives both the index and the element, searching from the end. func FindLastMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] { return G.FindLastMapWithIndex[[]A](sel) } diff --git a/v2/array/find_test.go b/v2/array/find_test.go new file mode 100644 index 0000000..469b597 --- /dev/null +++ b/v2/array/find_test.go @@ -0,0 +1,107 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package array + +import ( + "fmt" + "testing" + + O "github.com/IBM/fp-go/v2/option" + "github.com/stretchr/testify/assert" +) + +func TestFindFirstWithIndex(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + finder := FindFirstWithIndex(func(i, x int) bool { + return i > 2 && x%2 == 0 + }) + result := finder(src) + assert.Equal(t, O.Some(4), result) + + notFound := FindFirstWithIndex(func(i, x int) bool { + return i > 10 + }) + assert.Equal(t, O.None[int](), notFound(src)) +} + +func TestFindFirstMap(t *testing.T) { + src := []string{"a", "42", "b", "100"} + finder := FindFirstMap(func(s string) O.Option[int] { + if len(s) > 1 { + return O.Some(len(s)) + } + return O.None[int]() + }) + result := finder(src) + assert.Equal(t, O.Some(2), result) +} + +func TestFindFirstMapWithIndex(t *testing.T) { + src := []string{"a", "b", "c", "d"} + finder := FindFirstMapWithIndex(func(i int, s string) O.Option[string] { + if i > 1 { + return O.Some(fmt.Sprintf("%d:%s", i, s)) + } + return O.None[string]() + }) + result := finder(src) + assert.Equal(t, O.Some("2:c"), result) +} + +func TestFindLast(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + finder := FindLast(func(x int) bool { return x%2 == 0 }) + result := finder(src) + assert.Equal(t, O.Some(4), result) + + notFound := FindLast(func(x int) bool { return x > 10 }) + assert.Equal(t, O.None[int](), notFound(src)) +} + +func TestFindLastWithIndex(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + finder := FindLastWithIndex(func(i, x int) bool { + return i < 3 && x%2 == 0 + }) + result := finder(src) + assert.Equal(t, O.Some(2), result) +} + +func TestFindLastMap(t *testing.T) { + src := []string{"a", "42", "b", "100"} + finder := FindLastMap(func(s string) O.Option[int] { + if len(s) > 1 { + return O.Some(len(s)) + } + return O.None[int]() + }) + result := finder(src) + assert.Equal(t, O.Some(3), result) +} + +func TestFindLastMapWithIndex(t *testing.T) { + src := []string{"a", "b", "c", "d"} + finder := FindLastMapWithIndex(func(i int, s string) O.Option[string] { + if i < 3 { + return O.Some(fmt.Sprintf("%d:%s", i, s)) + } + return O.None[string]() + }) + result := finder(src) + assert.Equal(t, O.Some("2:c"), result) +} + +// Made with Bob diff --git a/v2/array/generic/array.go b/v2/array/generic/array.go index 4021523..abd70a5 100644 --- a/v2/array/generic/array.go +++ b/v2/array/generic/array.go @@ -163,15 +163,25 @@ func Size[GA ~[]A, A any](as GA) int { } func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB { - return array.Reduce(fa, func(bs GB, a A) GB { - return O.MonadFold(f(a), F.Constant(bs), F.Bind1st(Append[GB, B], bs)) - }, Empty[GB]()) + result := make(GB, 0, len(fa)) + for _, a := range fa { + O.Map(func(b B) B { + result = append(result, b) + return b + })(f(a)) + } + return result } func filterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(int, A) O.Option[B]) GB { - return array.ReduceWithIndex(fa, func(idx int, bs GB, a A) GB { - return O.MonadFold(f(idx, a), F.Constant(bs), F.Bind1st(Append[GB, B], bs)) - }, Empty[GB]()) + result := make(GB, 0, len(fa)) + for i, a := range fa { + O.Map(func(b B) B { + result = append(result, b) + return b + })(f(i, a)) + } + return result } func MonadFilterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB { @@ -310,28 +320,34 @@ func Clone[AS ~[]A, A any](f func(A) A) func(as AS) AS { } func FoldMap[AS ~[]A, A, B any](m M.Monoid[B]) func(func(A) B) func(AS) B { + empty := m.Empty() + concat := m.Concat return func(f func(A) B) func(AS) B { return func(as AS) B { return array.Reduce(as, func(cur B, a A) B { - return m.Concat(cur, f(a)) - }, m.Empty()) + return concat(cur, f(a)) + }, empty) } } } func FoldMapWithIndex[AS ~[]A, A, B any](m M.Monoid[B]) func(func(int, A) B) func(AS) B { + empty := m.Empty() + concat := m.Concat return func(f func(int, A) B) func(AS) B { return func(as AS) B { return array.ReduceWithIndex(as, func(idx int, cur B, a A) B { - return m.Concat(cur, f(idx, a)) - }, m.Empty()) + return concat(cur, f(idx, a)) + }, empty) } } } func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A { + empty := m.Empty() + concat := m.Concat return func(as AS) A { - return array.Reduce(as, m.Concat, m.Empty()) + return array.Reduce(as, concat, empty) } } diff --git a/v2/array/magma.go b/v2/array/magma.go index b538120..0e1d672 100644 --- a/v2/array/magma.go +++ b/v2/array/magma.go @@ -19,6 +19,20 @@ import ( M "github.com/IBM/fp-go/v2/monoid" ) +// ConcatAll concatenates all elements of an array using the provided Monoid. +// This reduces the array to a single value by repeatedly applying the Monoid's concat operation. +// +// Example: +// +// import "github.com/IBM/fp-go/v2/monoid" +// +// // Sum all numbers +// sumAll := array.ConcatAll(monoid.MonoidSum[int]()) +// result := sumAll([]int{1, 2, 3, 4, 5}) // 15 +// +// // Concatenate all strings +// concatStrings := array.ConcatAll(monoid.MonoidString()) +// result2 := concatStrings([]string{"Hello", " ", "World"}) // "Hello World" func ConcatAll[A any](m M.Monoid[A]) func([]A) A { return Reduce(m.Concat, m.Empty()) } diff --git a/v2/array/misc_test.go b/v2/array/misc_test.go new file mode 100644 index 0000000..288c892 --- /dev/null +++ b/v2/array/misc_test.go @@ -0,0 +1,163 @@ +// Copyright (c) 2023 IBM Corp. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package array + +import ( + "testing" + + O "github.com/IBM/fp-go/v2/option" + OR "github.com/IBM/fp-go/v2/ord" + "github.com/stretchr/testify/assert" +) + +func TestAnyWithIndex(t *testing.T) { + src := []int{1, 2, 3, 4, 5} + checker := AnyWithIndex(func(i, x int) bool { + return i == 2 && x == 3 + }) + assert.True(t, checker(src)) + + checker2 := AnyWithIndex(func(i, x int) bool { + return i == 10 + }) + assert.False(t, checker2(src)) +} + +func TestSemigroup(t *testing.T) { + sg := Semigroup[int]() + result := sg.Concat([]int{1, 2}, []int{3, 4}) + assert.Equal(t, []int{1, 2, 3, 4}, result) +} + +func TestArrayConcatAll(t *testing.T) { + result := ArrayConcatAll( + []int{1, 2}, + []int{3, 4}, + []int{5, 6}, + ) + assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, result) + + // Test with empty arrays + result2 := ArrayConcatAll( + []int{}, + []int{1}, + []int{}, + ) + assert.Equal(t, []int{1}, result2) +} + +func TestMonad(t *testing.T) { + m := Monad[int, string]() + + // Test Map + mapFn := m.Map(func(x int) string { + return string(rune('a' + x - 1)) + }) + mapped := mapFn([]int{1, 2, 3}) + assert.Equal(t, []string{"a", "b", "c"}, mapped) + + // Test Chain + chainFn := m.Chain(func(x int) []string { + return []string{string(rune('a' + x - 1))} + }) + chained := chainFn([]int{1, 2}) + assert.Equal(t, []string{"a", "b"}, chained) + + // Test Of + ofResult := m.Of(42) + assert.Equal(t, []int{42}, ofResult) +} + +func TestSortByKey(t *testing.T) { + type Person struct { + Name string + Age int + } + + people := []Person{ + {"Alice", 30}, + {"Bob", 25}, + {"Charlie", 35}, + } + + sorter := SortByKey(OR.FromStrictCompare[int](), func(p Person) int { + return p.Age + }) + result := sorter(people) + + assert.Equal(t, "Bob", result[0].Name) + assert.Equal(t, "Alice", result[1].Name) + assert.Equal(t, "Charlie", result[2].Name) +} + +func TestMonadTraverse(t *testing.T) { + result := MonadTraverse( + O.Of[[]int], + O.Map[[]int, func(int) []int], + O.Ap[[]int, int], + []int{1, 3, 5}, + func(n int) O.Option[int] { + if n%2 == 1 { + return O.Some(n * 2) + } + return O.None[int]() + }, + ) + + assert.Equal(t, O.Some([]int{2, 6, 10}), result) + + // Test with None case + result2 := MonadTraverse( + O.Of[[]int], + O.Map[[]int, func(int) []int], + O.Ap[[]int, int], + []int{1, 2, 3}, + func(n int) O.Option[int] { + if n%2 == 1 { + return O.Some(n * 2) + } + return O.None[int]() + }, + ) + + assert.Equal(t, O.None[[]int](), result2) +} + +func TestUniqByKey(t *testing.T) { + type Person struct { + Name string + Age int + } + + people := []Person{ + {"Alice", 30}, + {"Bob", 25}, + {"Alice", 35}, + {"Charlie", 30}, + } + + uniquer := Uniq(func(p Person) string { + return p.Name + }) + result := uniquer(people) + + assert.Equal(t, 3, len(result)) + assert.Equal(t, "Alice", result[0].Name) + assert.Equal(t, "Bob", result[1].Name) + assert.Equal(t, "Charlie", result[2].Name) +} + +// Made with Bob diff --git a/v2/array/monad.go b/v2/array/monad.go index aa4df8b..542d0d4 100644 --- a/v2/array/monad.go +++ b/v2/array/monad.go @@ -20,7 +20,20 @@ import ( "github.com/IBM/fp-go/v2/internal/monad" ) -// Monad returns the monadic operations for an array +// Monad returns the monadic operations for an array. +// This provides a structured way to access all monad operations (Map, Chain, Ap, Of) +// for arrays in a single interface. +// +// The Monad interface is useful when you need to pass monadic operations as parameters +// or when working with generic code that operates on any monad. +// +// Example: +// +// m := array.Monad[int, string]() +// result := m.Chain([]int{1, 2, 3}, func(x int) []string { +// return []string{fmt.Sprintf("%d", x), fmt.Sprintf("%d!", x)} +// }) +// // Result: ["1", "1!", "2", "2!", "3", "3!"] func Monad[A, B any]() monad.Monad[A, B, []A, []B, []func(A) B] { return G.Monad[A, B, []A, []B, []func(A) B]() } diff --git a/v2/array/monoid.go b/v2/array/monoid.go index ab661a6..04c3900 100644 --- a/v2/array/monoid.go +++ b/v2/array/monoid.go @@ -37,10 +37,25 @@ func concat[T any](left, right []T) []T { return buf } +// Monoid returns a Monoid instance for arrays. +// The Monoid combines arrays through concatenation, with an empty array as the identity element. +// +// Example: +// +// m := array.Monoid[int]() +// result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4] +// empty := m.Empty() // [] func Monoid[T any]() M.Monoid[[]T] { return M.MakeMonoid(concat[T], Empty[T]()) } +// Semigroup returns a Semigroup instance for arrays. +// The Semigroup combines arrays through concatenation. +// +// Example: +// +// s := array.Semigroup[int]() +// result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4] func Semigroup[T any]() S.Semigroup[[]T] { return S.MakeSemigroup(concat[T]) } @@ -49,7 +64,18 @@ func addLen[A any](count int, data []A) int { return count + len(data) } -// ConcatAll efficiently concatenates the input arrays into a final array +// ArrayConcatAll efficiently concatenates multiple arrays into a single array. +// This function pre-allocates the exact amount of memory needed and performs +// a single copy operation for each input array, making it more efficient than +// repeated concatenations. +// +// Example: +// +// result := array.ArrayConcatAll( +// []int{1, 2}, +// []int{3, 4}, +// []int{5, 6}, +// ) // [1, 2, 3, 4, 5, 6] func ArrayConcatAll[A any](data ...[]A) []A { // get the full size count := array.Reduce(data, addLen[A], 0) diff --git a/v2/array/sequence.go b/v2/array/sequence.go index b1b7449..0034e45 100644 --- a/v2/array/sequence.go +++ b/v2/array/sequence.go @@ -20,35 +20,72 @@ import ( O "github.com/IBM/fp-go/v2/option" ) -// We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces - -// HKTA = HKT -// HKTRA = HKT<[]A> -// HKTFRA = HKT - -// Sequence takes an `Array` where elements are `HKT` (higher kinded type) and, -// using an applicative of that `HKT`, returns an `HKT` of `[]A`. -// e.g. it can turn an `[]Either[error, string]` into an `Either[error, []string]`. +// Sequence takes an array where elements are HKT (higher kinded type) and, +// using an applicative of that HKT, returns an HKT of []A. // -// Sequence requires an `Applicative` of the `HKT` you are targeting, e.g. to turn an -// `[]Either[E, A]` into an `Either[E, []A]`, it needs an -// Applicative` for `Either`, to to turn an `[]Option[A]` into an `Option[ []A]`, -// it needs an `Applicative` for `Option`. +// For example, it can turn: +// - []Either[error, string] into Either[error, []string] +// - []Option[int] into Option[[]int] +// +// Sequence requires an Applicative of the HKT you are targeting. To turn an +// []Either[E, A] into an Either[E, []A], it needs an Applicative for Either. +// To turn an []Option[A] into an Option[[]A], it needs an Applicative for Option. +// +// Note: We need to pass the members of the applicative explicitly because Go does not +// support higher kinded types or template methods on structs or interfaces. +// +// Type parameters: +// - HKTA = HKT (e.g., Option[A], Either[E, A]) +// - HKTRA = HKT<[]A> (e.g., Option[[]A], Either[E, []A]) +// - HKTFRA = HKT (e.g., Option[func(A)[]A]) +// +// Example: +// +// import "github.com/IBM/fp-go/v2/option" +// +// opts := []option.Option[int]{ +// option.Some(1), +// option.Some(2), +// option.Some(3), +// } +// +// seq := array.Sequence( +// option.Of[[]int], +// option.MonadMap[[]int, func(int) []int], +// option.MonadAp[[]int, int], +// ) +// result := seq(opts) // Some([1, 2, 3]) func Sequence[A, HKTA, HKTRA, HKTFRA any]( _of func([]A) HKTRA, _map func(HKTRA, func([]A) func(A) []A) HKTFRA, _ap func(HKTFRA, HKTA) HKTRA, ) func([]HKTA) HKTRA { ca := F.Curry2(Append[A]) + empty := _of(Empty[A]()) return Reduce(func(fas HKTRA, fa HKTA) HKTRA { - return _ap( - _map(fas, ca), - fa, - ) - }, _of(Empty[A]())) + return _ap(_map(fas, ca), fa) + }, empty) } -// ArrayOption returns a function to convert sequence of options into an option of a sequence +// ArrayOption returns a function to convert a sequence of options into an option of a sequence. +// If all options are Some, returns Some containing an array of all values. +// If any option is None, returns None. +// +// Example: +// +// opts := []option.Option[int]{ +// option.Some(1), +// option.Some(2), +// option.Some(3), +// } +// result := array.ArrayOption[int]()(opts) // Some([1, 2, 3]) +// +// opts2 := []option.Option[int]{ +// option.Some(1), +// option.None[int](), +// option.Some(3), +// } +// result2 := array.ArrayOption[int]()(opts2) // None func ArrayOption[A any]() func([]O.Option[A]) O.Option[[]A] { return Sequence( O.Of[[]A], diff --git a/v2/array/sort.go b/v2/array/sort.go index a3aa1b2..350ff86 100644 --- a/v2/array/sort.go +++ b/v2/array/sort.go @@ -20,17 +20,73 @@ import ( O "github.com/IBM/fp-go/v2/ord" ) -// Sort implements a stable sort on the array given the provided ordering +// Sort implements a stable sort on the array given the provided ordering. +// The sort is stable, meaning that elements that compare equal retain their original order. +// +// Example: +// +// import "github.com/IBM/fp-go/v2/ord" +// +// numbers := []int{3, 1, 4, 1, 5, 9, 2, 6} +// sorted := array.Sort(ord.FromStrictCompare[int]())(numbers) +// // Result: [1, 1, 2, 3, 4, 5, 6, 9] func Sort[T any](ord O.Ord[T]) func(ma []T) []T { return G.Sort[[]T](ord) } -// SortByKey implements a stable sort on the array given the provided ordering on an extracted key +// SortByKey implements a stable sort on the array given the provided ordering on an extracted key. +// This is useful when you want to sort complex types by a specific field. +// +// Example: +// +// import "github.com/IBM/fp-go/v2/ord" +// +// type Person struct { +// Name string +// Age int +// } +// +// people := []Person{ +// {"Alice", 30}, +// {"Bob", 25}, +// {"Charlie", 35}, +// } +// +// sortByAge := array.SortByKey( +// ord.FromStrictCompare[int](), +// func(p Person) int { return p.Age }, +// ) +// sorted := sortByAge(people) +// // Result: [{"Bob", 25}, {"Alice", 30}, {"Charlie", 35}] func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T { return G.SortByKey[[]T](ord, f) } -// SortBy implements a stable sort on the array given the provided ordering +// SortBy implements a stable sort on the array using multiple ordering criteria. +// The orderings are applied in sequence: if two elements are equal according to the first +// ordering, the second ordering is used, and so on. +// +// Example: +// +// import "github.com/IBM/fp-go/v2/ord" +// +// type Person struct { +// LastName string +// FirstName string +// } +// +// people := []Person{ +// {"Smith", "John"}, +// {"Smith", "Alice"}, +// {"Jones", "Bob"}, +// } +// +// sortByName := array.SortBy([]ord.Ord[Person]{ +// ord.Contramap(func(p Person) string { return p.LastName })(ord.FromStrictCompare[string]()), +// ord.Contramap(func(p Person) string { return p.FirstName })(ord.FromStrictCompare[string]()), +// }) +// sorted := sortByName(people) +// // Result: [{"Jones", "Bob"}, {"Smith", "Alice"}, {"Smith", "John"}] func SortBy[T any](ord []O.Ord[T]) func(ma []T) []T { return G.SortBy[[]T, []O.Ord[T]](ord) } diff --git a/v2/array/traverse.go b/v2/array/traverse.go index 48eeaae..ef16d52 100644 --- a/v2/array/traverse.go +++ b/v2/array/traverse.go @@ -19,6 +19,39 @@ import ( "github.com/IBM/fp-go/v2/internal/array" ) +// Traverse maps each element of an array to an effect (HKT), then collects the results +// into an effect of an array. This is like a combination of Map and Sequence. +// +// Unlike Sequence which works with []HKT -> HKT<[]A>, Traverse works with +// []A -> (A -> HKT) -> HKT<[]B>, allowing you to transform elements while sequencing effects. +// +// Type parameters: +// - HKTB = HKT (e.g., Option[B], Either[E, B]) +// - HKTAB = HKT (intermediate type for applicative) +// - HKTRB = HKT<[]B> (e.g., Option[[]B], Either[E, []B]) +// +// Example: +// +// import ( +// "github.com/IBM/fp-go/v2/option" +// "strconv" +// ) +// +// // Parse strings to ints, returning None if any parse fails +// parseAll := array.Traverse( +// option.Of[[]int], +// option.Map[[]int, func(int) []int], +// option.Ap[[]int, int], +// func(s string) option.Option[int] { +// if n, err := strconv.Atoi(s); err == nil { +// return option.Some(n) +// } +// return option.None[int]() +// }, +// ) +// +// result := parseAll([]string{"1", "2", "3"}) // Some([1, 2, 3]) +// result2 := parseAll([]string{"1", "x", "3"}) // None func Traverse[A, B, HKTB, HKTAB, HKTRB any]( fof func([]B) HKTRB, fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB, @@ -28,6 +61,11 @@ func Traverse[A, B, HKTB, HKTAB, HKTRB any]( return array.Traverse[[]A](fof, fmap, fap, f) } +// MonadTraverse is the monadic version of Traverse that takes the array as a parameter. +// It maps each element of an array to an effect (HKT), then collects the results +// into an effect of an array. +// +// This is useful when you want to apply the traverse operation directly without currying. func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any]( fof func([]B) HKTRB, fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB, diff --git a/v2/array/uniq.go b/v2/array/uniq.go index 4ca5fb8..c54fcb9 100644 --- a/v2/array/uniq.go +++ b/v2/array/uniq.go @@ -4,14 +4,44 @@ import ( G "github.com/IBM/fp-go/v2/array/generic" ) -// StrictUniq converts an array of arbitrary items into an array or unique items -// where uniqueness is determined by the built-in uniqueness constraint +// StrictUniq converts an array of arbitrary items into an array of unique items +// where uniqueness is determined by the built-in equality constraint (comparable). +// The first occurrence of each unique value is kept, subsequent duplicates are removed. +// +// Example: +// +// numbers := []int{1, 2, 2, 3, 3, 3, 4} +// unique := array.StrictUniq(numbers) // [1, 2, 3, 4] +// +// strings := []string{"a", "b", "a", "c", "b"} +// unique2 := array.StrictUniq(strings) // ["a", "b", "c"] func StrictUniq[A comparable](as []A) []A { return G.StrictUniq[[]A](as) } -// Uniq converts an array of arbitrary items into an array or unique items -// where uniqueness is determined based on a key extractor function +// Uniq converts an array of arbitrary items into an array of unique items +// where uniqueness is determined based on a key extractor function. +// The first occurrence of each unique key is kept, subsequent duplicates are removed. +// +// This is useful for removing duplicates from arrays of complex types based on a specific field. +// +// Example: +// +// type Person struct { +// Name string +// Age int +// } +// +// people := []Person{ +// {"Alice", 30}, +// {"Bob", 25}, +// {"Alice", 35}, // duplicate name +// {"Charlie", 30}, +// } +// +// uniqueByName := array.Uniq(func(p Person) string { return p.Name }) +// result := uniqueByName(people) +// // Result: [{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}] func Uniq[A any, K comparable](f func(A) K) func(as []A) []A { return G.Uniq[[]A](f) } diff --git a/v2/array/zip.go b/v2/array/zip.go index 1f9cc28..5da5f0d 100644 --- a/v2/array/zip.go +++ b/v2/array/zip.go @@ -20,19 +20,58 @@ import ( T "github.com/IBM/fp-go/v2/tuple" ) -// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one -// input array is short, excess elements of the longer array are discarded. +// ZipWith applies a function to pairs of elements at the same index in two arrays, +// collecting the results in a new array. If one input array is shorter, excess elements +// of the longer array are discarded. +// +// Example: +// +// names := []string{"Alice", "Bob", "Charlie"} +// ages := []int{30, 25, 35} +// +// result := array.ZipWith(names, ages, func(name string, age int) string { +// return fmt.Sprintf("%s is %d years old", name, age) +// }) +// // Result: ["Alice is 30 years old", "Bob is 25 years old", "Charlie is 35 years old"] func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C { return G.ZipWith[[]A, []B, []C, FCT](fa, fb, f) } -// Zip takes two arrays and returns an array of corresponding pairs. If one input array is short, excess elements of the -// longer array are discarded +// Zip takes two arrays and returns an array of corresponding pairs (tuples). +// If one input array is shorter, excess elements of the longer array are discarded. +// +// Example: +// +// names := []string{"Alice", "Bob", "Charlie"} +// ages := []int{30, 25, 35} +// +// pairs := array.Zip(ages)(names) +// // Result: [(Alice, 30), (Bob, 25), (Charlie, 35)] +// +// // With different lengths +// pairs2 := array.Zip([]int{1, 2})([]string{"a", "b", "c"}) +// // Result: [(a, 1), (b, 2)] func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] { return G.Zip[[]A, []B, []T.Tuple2[A, B]](fb) } -// Unzip is the function is reverse of [Zip]. Takes an array of pairs and return two corresponding arrays +// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns +// two corresponding arrays, one containing all first elements and one containing all second elements. +// +// Example: +// +// import "github.com/IBM/fp-go/v2/tuple" +// +// pairs := []tuple.Tuple2[string, int]{ +// tuple.MakeTuple2("Alice", 30), +// tuple.MakeTuple2("Bob", 25), +// tuple.MakeTuple2("Charlie", 35), +// } +// +// result := array.Unzip(pairs) +// // Result: (["Alice", "Bob", "Charlie"], [30, 25, 35]) +// names := result.Head // ["Alice", "Bob", "Charlie"] +// ages := result.Tail // [30, 25, 35] func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] { return G.Unzip[[]A, []B, []T.Tuple2[A, B]](cs) } diff --git a/v2/either/array.go b/v2/either/array.go index 6ccabc6..5142a56 100644 --- a/v2/either/array.go +++ b/v2/either/array.go @@ -102,6 +102,7 @@ func TraverseArrayWithIndex[E, A, B any](f func(int, A) Either[E, B]) func([]A) return TraverseArrayWithIndexG[[]A, []B](f) } +//go:inline func SequenceArrayG[GA ~[]A, GOA ~[]Either[E, A], E, A any](ma GOA) Either[E, GA] { return TraverseArrayG[GOA, GA](F.Identity[Either[E, A]])(ma) } @@ -119,6 +120,8 @@ func SequenceArrayG[GA ~[]A, GOA ~[]Either[E, A], E, A any](ma GOA) Either[E, GA // } // result := either.SequenceArray(eithers) // // result is Right([]int{1, 2, 3}) +// +//go:inline func SequenceArray[E, A any](ma []Either[E, A]) Either[E, []A] { return SequenceArrayG[[]A](ma) } @@ -135,6 +138,8 @@ func SequenceArray[E, A any](ma []Either[E, A]) Either[E, []A] { // } // result := either.CompactArrayG[[]either.Either[error, int], []int](eithers) // // result is []int{1, 3} +// +//go:inline func CompactArrayG[A1 ~[]Either[E, A], A2 ~[]A, E, A any](fa A1) A2 { return RA.Reduce(fa, func(out A2, value Either[E, A]) A2 { return MonadFold(value, F.Constant1[E](out), F.Bind1st(RA.Append[A2, A], out)) @@ -152,6 +157,8 @@ func CompactArrayG[A1 ~[]Either[E, A], A2 ~[]A, E, A any](fa A1) A2 { // } // result := either.CompactArray(eithers) // // result is []int{1, 3} +// +//go:inline func CompactArray[E, A any](fa []Either[E, A]) []A { return CompactArrayG[[]Either[E, A], []A](fa) } diff --git a/v2/either/core.go b/v2/either/core.go index 0401c85..8d47d9a 100644 --- a/v2/either/core.go +++ b/v2/either/core.go @@ -69,6 +69,8 @@ func (s Either[E, A]) Format(f fmt.State, c rune) { // // either.IsLeft(either.Left[int](errors.New("err"))) // true // either.IsLeft(either.Right[error](42)) // false +// +//go:inline func IsLeft[E, A any](val Either[E, A]) bool { return val.isLeft } @@ -81,6 +83,8 @@ func IsLeft[E, A any](val Either[E, A]) bool { // // either.IsRight(either.Right[error](42)) // true // either.IsRight(either.Left[int](errors.New("err"))) // false +// +//go:inline func IsRight[E, A any](val Either[E, A]) bool { return !val.isLeft } @@ -91,6 +95,8 @@ func IsRight[E, A any](val Either[E, A]) bool { // Example: // // result := either.Left[int](errors.New("something went wrong")) +// +//go:inline func Left[A, E any](value E) Either[E, A] { return Either[E, A]{true, value} } @@ -101,6 +107,8 @@ func Left[A, E any](value E) Either[E, A] { // Example: // // result := either.Right[error](42) +// +//go:inline func Right[E, A any](value A) Either[E, A] { return Either[E, A]{false, value} } @@ -115,6 +123,8 @@ func Right[E, A any](value A) Either[E, A] { // func(err error) string { return "Error: " + err.Error() }, // func(n int) string { return fmt.Sprintf("Value: %d", n) }, // ) // "Value: 42" +// +//go:inline func MonadFold[E, A, B any](ma Either[E, A], onLeft func(e E) B, onRight func(a A) B) B { if ma.isLeft { return onLeft(ma.value.(E)) @@ -130,6 +140,8 @@ func MonadFold[E, A, B any](ma Either[E, A], onLeft func(e E) B, onRight func(a // // val, err := either.Unwrap(either.Right[error](42)) // 42, nil // val, err := either.Unwrap(either.Left[int](errors.New("fail"))) // 0, error +// +//go:inline func Unwrap[E, A any](ma Either[E, A]) (A, E) { if ma.isLeft { var a A diff --git a/v2/either/either.go b/v2/either/either.go index cf44954..fc29596 100644 --- a/v2/either/either.go +++ b/v2/either/either.go @@ -35,6 +35,8 @@ import ( // Example: // // result := either.Of[error](42) // Right(42) +// +//go:inline func Of[E, A any](value A) Either[E, A] { return F.Pipe1(value, Right[E, A]) } @@ -81,6 +83,8 @@ func Ap[B, E, A any](fa Either[E, A]) func(fab Either[E, func(a A) B]) Either[E, // either.Right[error](21), // func(x int) int { return x * 2 }, // ) // Right(42) +// +//go:inline func MonadMap[E, A, B any](fa Either[E, A], f func(a A) B) Either[E, B] { return MonadChain(fa, F.Flow2(f, Right[E, B])) } @@ -157,6 +161,8 @@ func MapLeft[A, E1, E2 any](f func(E1) E2) func(fa Either[E1, A]) Either[E2, A] // return either.Right[error](x * 2) // }, // ) // Right(42) +// +//go:inline func MonadChain[E, A, B any](fa Either[E, A], f func(a A) Either[E, B]) Either[E, B] { return MonadFold(fa, Left[B, E], f) } @@ -300,6 +306,8 @@ func FromOption[A, E any](onNone func() E) func(Option[A]) Either[E, A] { // // result := either.ToOption(either.Right[error](42)) // Some(42) // result := either.ToOption(either.Left[int](errors.New("err"))) // None +// +//go:inline func ToOption[E, A any](ma Either[E, A]) Option[A] { return MonadFold(ma, F.Ignore1of1[E](O.None[A]), O.Some[A]) } @@ -351,6 +359,8 @@ func Fold[E, A, B any](onLeft func(E) B, onRight func(A) B) func(Either[E, A]) B // // val, err := either.UnwrapError(either.Right[error](42)) // 42, nil // val, err := either.UnwrapError(either.Left[int](errors.New("fail"))) // zero, error +// +//go:inline func UnwrapError[A any](ma Either[error, A]) (A, error) { return Unwrap[error](ma) } @@ -495,6 +505,8 @@ func MonadSequence3[E, T1, T2, T3, R any](e1 Either[E, T1], e2 Either[E, T2], e3 // // result := either.Swap(either.Right[error](42)) // Left(42) // result := either.Swap(either.Left[int](errors.New("err"))) // Right(error) +// +//go:inline func Swap[E, A any](val Either[E, A]) Either[A, E] { return MonadFold(val, Right[A, E], Left[E, A]) }