mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-18 13:51:26 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f35430cf18 | ||
|
|
d3ffc71808 | ||
|
|
62844b7030 |
@@ -18,6 +18,10 @@ package readerioresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/internal/witherable"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
|
||||
@@ -49,3 +53,43 @@ import (
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return RIOR.FilterOrElse[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Filter[HKTA, A any](
|
||||
filter func(Predicate[A]) Endomorphism[HKTA],
|
||||
) func(Predicate[A]) Operator[HKTA, HKTA] {
|
||||
return witherable.Filter(
|
||||
Map,
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterArray[A any](p Predicate[A]) Operator[[]A, []A] {
|
||||
return Filter(array.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterIter[A any](p Predicate[A]) Operator[Seq[A], Seq[A]] {
|
||||
return Filter(iter.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMap[HKTA, HKTB, A, B any](
|
||||
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
|
||||
) func(option.Kleisli[A, B]) Operator[HKTA, HKTB] {
|
||||
return witherable.FilterMap(
|
||||
Map,
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapArray[A, B any](p option.Kleisli[A, B]) Operator[[]A, []B] {
|
||||
return FilterMap(array.FilterMap[A, B])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapIter[A, B any](p option.Kleisli[A, B]) Operator[Seq[A], Seq[B]] {
|
||||
return FilterMap(iter.FilterMap[A, B])(p)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
"github.com/IBM/fp-go/v2/context/ioresult"
|
||||
@@ -220,4 +221,10 @@ type (
|
||||
// The first element is the CancelFunc that should be called to release resources.
|
||||
// The second element is the new Context that was created.
|
||||
ContextCancel = Pair[context.CancelFunc, context.Context]
|
||||
|
||||
// Seq is an iterator over sequences of individual values.
|
||||
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
|
||||
// stopping early if yield returns false.
|
||||
// See the [iter] package documentation for more details.
|
||||
Seq[A any] = iter.Seq[A]
|
||||
)
|
||||
|
||||
48
v2/context/readerreaderioresult/filter.go
Normal file
48
v2/context/readerreaderioresult/filter.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/internal/witherable"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func Filter[C, HKTA, A any](
|
||||
filter func(Predicate[A]) Endomorphism[HKTA],
|
||||
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
|
||||
return witherable.Filter(
|
||||
Map[C],
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
|
||||
return Filter[C](array.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
|
||||
return Filter[C](iter.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMap[C, HKTA, HKTB, A, B any](
|
||||
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
|
||||
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
|
||||
return witherable.FilterMap(
|
||||
Map[C],
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
|
||||
return FilterMap[C](array.FilterMap[A, B])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
|
||||
return FilterMap[C](iter.FilterMap[A, B])(p)
|
||||
}
|
||||
@@ -834,7 +834,7 @@ func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error]) ReaderReaderIOResult[R, A] {
|
||||
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endomorphism[error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadMapLeft(fa, f)
|
||||
}
|
||||
|
||||
@@ -843,7 +843,7 @@ func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error])
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
|
||||
func MapLeft[R, A any](f Endomorphism[error]) Operator[R, A, A] {
|
||||
return RRIOE.MapLeft[R, context.Context, A](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/traversal/result"
|
||||
@@ -146,9 +147,15 @@ type (
|
||||
// It's an alias for predicate.Predicate[A].
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Endmorphism represents a function from type A to type A.
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
// It's an alias for endomorphism.Endomorphism[A].
|
||||
Endmorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Seq is an iterator over sequences of individual values.
|
||||
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
|
||||
// stopping early if yield returns false.
|
||||
// See the [iter] package documentation for more details.
|
||||
Seq[A any] = iter.Seq[A]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
296
v2/effect/filter.go
Normal file
296
v2/effect/filter.go
Normal file
@@ -0,0 +1,296 @@
|
||||
// Copyright (c) 2023 - 2025 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 effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Filter lifts a filtering operation on a higher-kinded type into an Effect operator.
|
||||
// This is a generic function that works with any filterable data structure by taking
|
||||
// a filter function and returning an operator that can be used in effect chains.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - HKTA: The higher-kinded type being filtered (e.g., []A, Seq[A])
|
||||
// - A: The element type being filtered
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - filter: A function that takes a predicate and returns an endomorphism on HKTA
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Predicate[A]) Operator[C, HKTA, HKTA]: A function that takes a predicate
|
||||
// and returns an operator that filters effects containing HKTA values
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import A "github.com/IBM/fp-go/v2/array"
|
||||
//
|
||||
// // Create a custom filter operator for arrays
|
||||
// filterOp := Filter[MyContext](A.Filter[int])
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
|
||||
// filterOp(isEven),
|
||||
// Map[MyContext](func(arr []int) int { return len(arr) }),
|
||||
// )
|
||||
// // Result: Effect that produces 2 (count of even numbers)
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterArray: Specialized version for array filtering
|
||||
// - FilterIter: Specialized version for iterator filtering
|
||||
// - FilterMap: For filtering and mapping simultaneously
|
||||
//
|
||||
//go:inline
|
||||
func Filter[C, HKTA, A any](
|
||||
filter func(Predicate[A]) Endomorphism[HKTA],
|
||||
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
|
||||
return readerreaderioresult.Filter[C](filter)
|
||||
}
|
||||
|
||||
// FilterArray creates an operator that filters array elements within an Effect based on a predicate.
|
||||
// Elements that satisfy the predicate are kept, while others are removed.
|
||||
// This is a specialized version of Filter for arrays.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The element type in the array
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A predicate function that tests each element
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, []A, []A]: An operator that filters array elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// filterPositive := FilterArray[MyContext](isPositive)
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext]([]int{-2, -1, 0, 1, 2, 3}),
|
||||
// filterPositive,
|
||||
// )
|
||||
// // Result: Effect that produces []int{1, 2, 3}
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Filter: Generic version for any filterable type
|
||||
// - FilterIter: For filtering iterators
|
||||
// - FilterMapArray: For filtering and mapping arrays simultaneously
|
||||
//
|
||||
//go:inline
|
||||
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
|
||||
return readerreaderioresult.FilterArray[C](p)
|
||||
}
|
||||
|
||||
// FilterIter creates an operator that filters iterator elements within an Effect based on a predicate.
|
||||
// Elements that satisfy the predicate are kept in the resulting iterator, while others are removed.
|
||||
// This is a specialized version of Filter for iterators (Seq).
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The element type in the iterator
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A predicate function that tests each element
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, Seq[A], Seq[A]]: An operator that filters iterator elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// filterEven := FilterIter[MyContext](isEven)
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext](slices.Values([]int{1, 2, 3, 4, 5, 6})),
|
||||
// filterEven,
|
||||
// )
|
||||
// // Result: Effect that produces an iterator over [2, 4, 6]
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Filter: Generic version for any filterable type
|
||||
// - FilterArray: For filtering arrays
|
||||
// - FilterMapIter: For filtering and mapping iterators simultaneously
|
||||
//
|
||||
//go:inline
|
||||
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
|
||||
return readerreaderioresult.FilterIter[C](p)
|
||||
}
|
||||
|
||||
// FilterMap lifts a filter-map operation on a higher-kinded type into an Effect operator.
|
||||
// This combines filtering and mapping in a single operation: elements are transformed
|
||||
// using a function that returns Option, and only Some values are kept in the result.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - HKTA: The input higher-kinded type (e.g., []A, Seq[A])
|
||||
// - HKTB: The output higher-kinded type (e.g., []B, Seq[B])
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - filter: A function that takes an option.Kleisli and returns a transformation from HKTA to HKTB
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB]: A function that takes a Kleisli arrow
|
||||
// and returns an operator that filter-maps effects
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import A "github.com/IBM/fp-go/v2/array"
|
||||
// import O "github.com/IBM/fp-go/v2/option"
|
||||
//
|
||||
// // Parse and filter positive integers
|
||||
// parsePositive := func(s string) O.Option[int] {
|
||||
// var n int
|
||||
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n > 0 {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// filterMapOp := FilterMap[MyContext](A.FilterMap[string, int])
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext]([]string{"1", "-2", "3", "invalid", "5"}),
|
||||
// filterMapOp(parsePositive),
|
||||
// )
|
||||
// // Result: Effect that produces []int{1, 3, 5}
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterMapArray: Specialized version for arrays
|
||||
// - FilterMapIter: Specialized version for iterators
|
||||
// - Filter: For filtering without transformation
|
||||
//
|
||||
//go:inline
|
||||
func FilterMap[C, HKTA, HKTB, A, B any](
|
||||
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
|
||||
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
|
||||
return readerreaderioresult.FilterMap[C](filter)
|
||||
}
|
||||
|
||||
// FilterMapArray creates an operator that filters and maps array elements within an Effect.
|
||||
// Each element is transformed using a function that returns Option[B]. Elements that
|
||||
// produce Some(b) are kept in the result array, while None values are filtered out.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, []A, []B]: An operator that filter-maps array elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import O "github.com/IBM/fp-go/v2/option"
|
||||
//
|
||||
// // Double even numbers, filter out odd numbers
|
||||
// doubleEven := func(n int) O.Option[int] {
|
||||
// if n%2 == 0 {
|
||||
// return O.Some(n * 2)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
|
||||
// FilterMapArray[MyContext](doubleEven),
|
||||
// )
|
||||
// // Result: Effect that produces []int{4, 8}
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterMap: Generic version for any filterable type
|
||||
// - FilterMapIter: For filter-mapping iterators
|
||||
// - FilterArray: For filtering without transformation
|
||||
//
|
||||
//go:inline
|
||||
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
|
||||
return readerreaderioresult.FilterMapArray[C](p)
|
||||
}
|
||||
|
||||
// FilterMapIter creates an operator that filters and maps iterator elements within an Effect.
|
||||
// Each element is transformed using a function that returns Option[B]. Elements that
|
||||
// produce Some(b) are kept in the resulting iterator, while None values are filtered out.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, Seq[A], Seq[B]]: An operator that filter-maps iterator elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import O "github.com/IBM/fp-go/v2/option"
|
||||
//
|
||||
// // Parse strings to integers, keeping only valid ones
|
||||
// parseInt := func(s string) O.Option[int] {
|
||||
// var n int
|
||||
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext](slices.Values([]string{"1", "2", "invalid", "3"})),
|
||||
// FilterMapIter[MyContext](parseInt),
|
||||
// )
|
||||
// // Result: Effect that produces an iterator over [1, 2, 3]
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterMap: Generic version for any filterable type
|
||||
// - FilterMapArray: For filter-mapping arrays
|
||||
// - FilterIter: For filtering without transformation
|
||||
//
|
||||
//go:inline
|
||||
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
|
||||
return readerreaderioresult.FilterMapIter[C](p)
|
||||
}
|
||||
653
v2/effect/filter_test.go
Normal file
653
v2/effect/filter_test.go
Normal file
@@ -0,0 +1,653 @@
|
||||
// Copyright (c) 2023 - 2025 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 effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type FilterTestConfig struct {
|
||||
MaxValue int
|
||||
MinValue int
|
||||
}
|
||||
|
||||
// Helper to collect iterator results from an effect
|
||||
func collectSeqEffect[C, A any](eff Effect[C, Seq[A]], cfg C) []A {
|
||||
result, err := runEffect(eff, cfg)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return slices.Collect(result)
|
||||
}
|
||||
|
||||
func TestFilterArray_Success(t *testing.T) {
|
||||
t.Run("filters array keeping matching elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
input := Succeed[FilterTestConfig]([]int{1, -2, 3, -4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 3, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty array when no elements match", func(t *testing.T) {
|
||||
// Arrange
|
||||
isNegative := N.LessThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isNegative)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("returns all elements when all match", func(t *testing.T) {
|
||||
// Arrange
|
||||
alwaysTrue := func(n int) bool { return true }
|
||||
filterOp := FilterArray[FilterTestConfig](alwaysTrue)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterIter_Success(t *testing.T) {
|
||||
t.Run("filters iterator keeping matching elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterOp := FilterIter[FilterTestConfig](isEven)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5, 6}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{2, 4, 6}, collected)
|
||||
})
|
||||
|
||||
t.Run("returns empty iterator when no elements match", func(t *testing.T) {
|
||||
// Arrange
|
||||
isNegative := N.LessThan(0)
|
||||
filterOp := FilterIter[FilterTestConfig](isNegative)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterArray_WithContext(t *testing.T) {
|
||||
t.Run("uses context for filtering", func(t *testing.T) {
|
||||
// Arrange
|
||||
cfg := FilterTestConfig{MaxValue: 100, MinValue: 0}
|
||||
inRange := func(n int) bool { return n >= cfg.MinValue && n <= cfg.MaxValue }
|
||||
filterOp := FilterArray[FilterTestConfig](inRange)
|
||||
input := Succeed[FilterTestConfig]([]int{-10, 50, 150, 75})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), cfg)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{50, 75}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterArray_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
input := Succeed[FilterTestConfig]([]int{})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves error from input", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
inputErr := errors.New("input error")
|
||||
input := Fail[FilterTestConfig, []int](inputErr)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, inputErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterIter_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty iterator", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterIter[FilterTestConfig](isPositive)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("preserves error from input", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterIter[FilterTestConfig](isPositive)
|
||||
inputErr := errors.New("input error")
|
||||
input := Fail[FilterTestConfig, Seq[int]](inputErr)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, inputErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_GenericFilter(t *testing.T) {
|
||||
t.Run("works with custom filter function", func(t *testing.T) {
|
||||
// Arrange
|
||||
customFilter := func(p Predicate[int]) Endomorphism[[]int] {
|
||||
return A.Filter(p)
|
||||
}
|
||||
filterOp := Filter[FilterTestConfig](customFilter)
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3, 4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(isEven)(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapArray_Success(t *testing.T) {
|
||||
t.Run("filters and maps array elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
parsePositive := func(n int) O.Option[string] {
|
||||
if n > 0 {
|
||||
return O.Some(fmt.Sprintf("positive:%d", n))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](parsePositive)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"positive:2", "positive:4", "positive:5"}, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when no elements match", func(t *testing.T) {
|
||||
// Arrange
|
||||
neverMatch := func(n int) O.Option[int] {
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](neverMatch)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("maps all elements when all match", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapIter_Success(t *testing.T) {
|
||||
t.Run("filters and maps iterator elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
doubleEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapIter[FilterTestConfig](doubleEven)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{4, 8}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapArray_TypeConversion(t *testing.T) {
|
||||
t.Run("converts int to string", func(t *testing.T) {
|
||||
// Arrange
|
||||
intToString := func(n int) O.Option[string] {
|
||||
if n > 0 {
|
||||
return O.Some(fmt.Sprintf("%d", n))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](intToString)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"2", "4"}, result)
|
||||
})
|
||||
|
||||
t.Run("converts string to int", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseEven := func(s string) O.Option[int] {
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n%2 == 0 {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](parseEven)
|
||||
input := Succeed[FilterTestConfig]([]string{"1", "2", "3", "4", "invalid"})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapArray_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
input := Succeed[FilterTestConfig]([]int{})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves error from input", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
inputErr := errors.New("input error")
|
||||
input := Fail[FilterTestConfig, []int](inputErr)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, inputErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapIter_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty iterator", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapIter[FilterTestConfig](double)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMap_GenericFilterMap(t *testing.T) {
|
||||
t.Run("works with custom filterMap function", func(t *testing.T) {
|
||||
// Arrange
|
||||
customFilterMap := func(f O.Kleisli[int, string]) Reader[[]int, []string] {
|
||||
return A.FilterMap(f)
|
||||
}
|
||||
filterMapOp := FilterMap[FilterTestConfig](customFilterMap)
|
||||
intToString := func(n int) O.Option[string] {
|
||||
if n > 0 {
|
||||
return O.Some(fmt.Sprintf("%d", n))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(intToString)(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"2", "4"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_Composition(t *testing.T) {
|
||||
t.Run("chains multiple filters", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterPositive := FilterArray[FilterTestConfig](isPositive)
|
||||
filterEven := FilterArray[FilterTestConfig](isEven)
|
||||
input := Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterEven(filterPositive(input)), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("chains filter and filterMap", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
doubleEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](doubleEven)
|
||||
input := Succeed[FilterTestConfig]([]int{-2, 1, 2, 3, 4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(filterOp(input)), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{4, 8}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_WithComplexTypes(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("filters structs", func(t *testing.T) {
|
||||
// Arrange
|
||||
isAdult := func(u User) bool { return u.Age >= 18 }
|
||||
filterOp := FilterArray[FilterTestConfig](isAdult)
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 25},
|
||||
{Name: "Bob", Age: 16},
|
||||
{Name: "Charlie", Age: 30},
|
||||
}
|
||||
input := Succeed[FilterTestConfig](users)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
expected := []User{
|
||||
{Name: "Alice", Age: 25},
|
||||
{Name: "Charlie", Age: 30},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("filterMaps structs to different type", func(t *testing.T) {
|
||||
// Arrange
|
||||
extractAdultName := func(u User) O.Option[string] {
|
||||
if u.Age >= 18 {
|
||||
return O.Some(u.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](extractAdultName)
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 25},
|
||||
{Name: "Bob", Age: 16},
|
||||
{Name: "Charlie", Age: 30},
|
||||
}
|
||||
input := Succeed[FilterTestConfig](users)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"Alice", "Charlie"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_BoundaryConditions(t *testing.T) {
|
||||
t.Run("filters with boundary predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
inRange := func(n int) bool { return n >= 0 && n <= 100 }
|
||||
filterOp := FilterArray[FilterTestConfig](inRange)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{0, 50, 100}, result)
|
||||
})
|
||||
|
||||
t.Run("filterMap with boundary conditions", func(t *testing.T) {
|
||||
// Arrange
|
||||
clampToRange := func(n int) O.Option[int] {
|
||||
if n >= 0 && n <= 100 {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](clampToRange)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{0, 50, 100}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_WithIterators(t *testing.T) {
|
||||
t.Run("filters large iterator efficiently", func(t *testing.T) {
|
||||
// Arrange
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterOp := FilterIter[FilterTestConfig](isEven)
|
||||
|
||||
// Create iterator for range 0-99
|
||||
makeSeq := func(yield func(int) bool) {
|
||||
for i := range 100 {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
input := Succeed[FilterTestConfig](Seq[int](makeSeq))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, 50, len(collected))
|
||||
assert.Equal(t, 0, collected[0])
|
||||
assert.Equal(t, 98, collected[49])
|
||||
})
|
||||
|
||||
t.Run("filterMap with iterator", func(t *testing.T) {
|
||||
// Arrange
|
||||
squareEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapIter[FilterTestConfig](squareEven)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{4, 16}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_ErrorPropagation(t *testing.T) {
|
||||
t.Run("filter propagates Left through chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
originalErr := errors.New("original error")
|
||||
|
||||
// Create an effect that fails
|
||||
failedEffect := F.Pipe1(
|
||||
Succeed[FilterTestConfig]([]int{1, 2, 3}),
|
||||
Chain(func([]int) Effect[FilterTestConfig, []int] {
|
||||
return Fail[FilterTestConfig, []int](originalErr)
|
||||
}),
|
||||
)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterOp(failedEffect), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
})
|
||||
|
||||
t.Run("filterMap propagates Left through chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
originalErr := errors.New("original error")
|
||||
|
||||
// Create an effect that fails
|
||||
failedEffect := F.Pipe1(
|
||||
Succeed[FilterTestConfig]([]int{1, 2, 3}),
|
||||
Chain(func([]int) Effect[FilterTestConfig, []int] {
|
||||
return Fail[FilterTestConfig, []int](originalErr)
|
||||
}),
|
||||
)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterMapOp(failedEffect), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_Integration(t *testing.T) {
|
||||
t.Run("complex filtering pipeline", func(t *testing.T) {
|
||||
// Arrange: Filter positive numbers, then double evens, then filter > 5
|
||||
isPositive := N.MoreThan(0)
|
||||
doubleEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
isGreaterThan5 := N.MoreThan(5)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6}),
|
||||
FilterArray[FilterTestConfig](isPositive),
|
||||
FilterMapArray[FilterTestConfig](doubleEven),
|
||||
FilterArray[FilterTestConfig](isGreaterThan5),
|
||||
)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(pipeline, FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
// Positive: [1,2,3,4,5,6] -> DoubleEven: [4,8,12] -> >5: [8,12]
|
||||
assert.Equal(t, []int{8, 12}, result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,11 @@ import (
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
@@ -89,4 +91,14 @@ type (
|
||||
// Operator represents a function that transforms Effect[C, A] to Effect[C, B].
|
||||
// It's used for lifting operations over effects.
|
||||
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
|
||||
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
// It's an alias for endomorphism.Endomorphism[A].
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Seq is an iterator over sequences of individual values.
|
||||
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
|
||||
// stopping early if yield returns false.
|
||||
// See the [iter] package documentation for more details.
|
||||
Seq[A any] = iter.Seq[A]
|
||||
)
|
||||
|
||||
195
v2/iterator/iter/async.go
Normal file
195
v2/iterator/iter/async.go
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2023 - 2025 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 iter
|
||||
|
||||
import (
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
// Async converts a synchronous sequence into an asynchronous buffered sequence.
|
||||
// It spawns a goroutine to consume the input sequence and sends values through
|
||||
// a buffered channel, allowing concurrent production and consumption of elements.
|
||||
//
|
||||
// The function provides backpressure control through the buffer size and properly
|
||||
// handles early termination when the consumer stops iterating. This is useful for
|
||||
// decoupling producers and consumers, enabling pipeline parallelism, or when you
|
||||
// need to process sequences concurrently.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type of elements in the sequence
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - input: The source sequence to be consumed asynchronously
|
||||
// - bufSize: The buffer size for the channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// but uses more memory. A buffer of 0 creates an unbuffered channel requiring
|
||||
// synchronization between producer and consumer.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Seq[T]: A new sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - Spawns a goroutine that consumes the input sequence
|
||||
// - Elements are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
|
||||
// the producer goroutine is signaled to stop via a done channel
|
||||
// - Both the producer goroutine and the done channel are properly cleaned up
|
||||
// - The channel is closed when the input sequence is exhausted or early termination occurs
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Create an async sequence with a buffer of 10
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// async := Async(seq, 10)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for v := range async {
|
||||
// fmt.Println(v) // Prints: 1, 2, 3, 4, 5
|
||||
// }
|
||||
//
|
||||
// # Example with Early Termination
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// async := Async(seq, 5)
|
||||
//
|
||||
// // Stop after 3 elements - producer goroutine will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range async {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 3 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # Example with Unbuffered Channel
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// seq := From(1, 2, 3)
|
||||
// async := Async(seq, 0)
|
||||
//
|
||||
// // Producer and consumer are synchronized
|
||||
// for v := range async {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - From: Creates a sequence from values
|
||||
// - Map: Transforms sequence elements
|
||||
// - Filter: Filters sequence elements
|
||||
func Async[T any](input Seq[T], bufSize int) Seq[T] {
|
||||
return func(yield func(T) bool) {
|
||||
ch := make(chan T, N.Max(bufSize, 0))
|
||||
done := make(chan Void)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for v := range input {
|
||||
select {
|
||||
case ch <- v:
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
defer close(done)
|
||||
for v := range ch {
|
||||
if !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Async2 converts a synchronous key-value sequence into an asynchronous buffered sequence.
|
||||
// It spawns a goroutine to consume the input sequence and sends key-value pairs through
|
||||
// a buffered channel, allowing concurrent production and consumption of elements.
|
||||
//
|
||||
// This function is the Seq2 variant of Async, providing the same asynchronous behavior
|
||||
// for key-value sequences. It internally converts the Seq2 to a sequence of Pairs,
|
||||
// applies Async, and converts back to Seq2.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - K: The type of keys in the sequence
|
||||
// - V: The type of values in the sequence
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - input: The source key-value sequence to be consumed asynchronously
|
||||
// - bufSize: The buffer size for the channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// but uses more memory. A buffer of 0 creates an unbuffered channel requiring
|
||||
// synchronization between producer and consumer.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Seq2[K, V]: A new key-value sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - Spawns a goroutine that consumes the input key-value sequence
|
||||
// - Key-value pairs are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
|
||||
// the producer goroutine is signaled to stop via a done channel
|
||||
// - Both the producer goroutine and the done channel are properly cleaned up
|
||||
// - The channel is closed when the input sequence is exhausted or early termination occurs
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Create an async key-value sequence with a buffer of 10
|
||||
// seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
// async := Async2(seq, 10)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for k, v := range async {
|
||||
// fmt.Printf("%d: %s\n", k, v)
|
||||
// }
|
||||
// // Output:
|
||||
// // 1: a
|
||||
// // 2: b
|
||||
// // 3: c
|
||||
//
|
||||
// # Example with Early Termination
|
||||
//
|
||||
// seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
// async := Async2(seq, 5)
|
||||
//
|
||||
// // Stop after 2 pairs - producer goroutine will be properly cleaned up
|
||||
// count := 0
|
||||
// for k, v := range async {
|
||||
// fmt.Printf("%d: %s\n", k, v)
|
||||
// count++
|
||||
// if count >= 2 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Async: Asynchronous sequence for single-value sequences
|
||||
// - ToSeqPair: Converts Seq2 to Seq of Pairs
|
||||
// - FromSeqPair: Converts Seq of Pairs to Seq2
|
||||
// - MonadZip: Creates key-value sequences from two sequences
|
||||
func Async2[K, V any](input Seq2[K, V], bufSize int) Seq2[K, V] {
|
||||
return FromSeqPair(Async(ToSeqPair(input), bufSize))
|
||||
}
|
||||
905
v2/iterator/iter/async_test.go
Normal file
905
v2/iterator/iter/async_test.go
Normal file
@@ -0,0 +1,905 @@
|
||||
// Copyright (c) 2023 - 2025 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 iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestAsync_Success tests basic Async functionality
|
||||
func TestAsync_Success(t *testing.T) {
|
||||
t.Run("converts sequence to async with buffer", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves element order", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "d", "e")
|
||||
async := Async(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []string{"a", "b", "c", "d", "e"}, result)
|
||||
})
|
||||
|
||||
t.Run("works with single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
async := Async(seq, 1)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("works with large sequence", func(t *testing.T) {
|
||||
data := make([]int, 100)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
async := Async(seq, 20)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, data, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_BufferSizes tests different buffer sizes
|
||||
func TestAsync_BufferSizes(t *testing.T) {
|
||||
t.Run("unbuffered channel (bufSize 0)", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := Async(seq, 0)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("small buffer", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 2)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("large buffer", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 100)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("negative buffer size treated as 0", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := Async(seq, -5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("buffer size equals sequence length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("buffer size larger than sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := Async(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_Empty tests Async with empty sequences
|
||||
func TestAsync_Empty(t *testing.T) {
|
||||
t.Run("empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
async := Async(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
async := Async(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty with zero buffer", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
async := Async(seq, 0)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_EarlyTermination tests that Async properly handles early termination
|
||||
func TestAsync_EarlyTermination(t *testing.T) {
|
||||
t.Run("stops producer when consumer breaks", func(t *testing.T) {
|
||||
var producerCount atomic.Int32
|
||||
|
||||
// Create a sequence that tracks how many elements were produced
|
||||
seq := func(yield func(int) bool) {
|
||||
for i := range 100 {
|
||||
producerCount.Add(1)
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async := Async(seq, 10)
|
||||
|
||||
// Consume only 5 elements
|
||||
count := 0
|
||||
for range async {
|
||||
count++
|
||||
if count >= 5 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Give goroutine time to clean up
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Producer should have stopped shortly after consumer stopped
|
||||
// It may produce a few extra due to buffering, but not all 100
|
||||
produced := producerCount.Load()
|
||||
assert.LessOrEqual(t, produced, int32(20), "producer should stop after consumer breaks")
|
||||
assert.GreaterOrEqual(t, produced, int32(5), "producer should produce at least what was consumed")
|
||||
})
|
||||
|
||||
t.Run("handles yield returning false", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := Async(seq, 5)
|
||||
|
||||
collected := []int{}
|
||||
for v := range async {
|
||||
collected = append(collected, v)
|
||||
if v == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
|
||||
t.Run("early termination with unbuffered channel", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 0)
|
||||
|
||||
collected := []int{}
|
||||
for v := range async {
|
||||
collected = append(collected, v)
|
||||
if v == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_WithComplexTypes tests Async with complex data types
|
||||
func TestAsync_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("works with structs", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
async := Async(seq, 5)
|
||||
result := toSlice(async)
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Charlie", 35},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("works with pointers", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
async := Async(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []*Person{p1, p2, p3}, result)
|
||||
})
|
||||
|
||||
t.Run("works with slices", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3, 4}, []int{5, 6})
|
||||
async := Async(seq, 2)
|
||||
result := toSlice(async)
|
||||
expected := [][]int{{1, 2}, {3, 4}, {5, 6}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("works with maps", func(t *testing.T) {
|
||||
m1 := map[string]int{"a": 1}
|
||||
m2 := map[string]int{"b": 2}
|
||||
m3 := map[string]int{"c": 3}
|
||||
seq := From(m1, m2, m3)
|
||||
async := Async(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []map[string]int{m1, m2, m3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_WithChainedOperations tests Async with other sequence operations
|
||||
func TestAsync_WithChainedOperations(t *testing.T) {
|
||||
t.Run("async after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
async := Async(mapped, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("map after async", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 5)
|
||||
mapped := MonadMap(async, N.Mul(2))
|
||||
result := toSlice(mapped)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("async after filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
async := Async(filtered, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after async", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := Async(seq, 5)
|
||||
filtered := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("async after chain", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
async := Async(chained, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple async operations", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async1 := Async(seq, 3)
|
||||
async2 := Async(async1, 2)
|
||||
result := toSlice(async2)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_Concurrency tests concurrent behavior
|
||||
func TestAsync_Concurrency(t *testing.T) {
|
||||
t.Run("allows concurrent production and consumption", func(t *testing.T) {
|
||||
// Create a slow producer
|
||||
seq := func(yield func(int) bool) {
|
||||
for i := range 5 {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async := Async(seq, 10)
|
||||
|
||||
result := toSlice(async)
|
||||
|
||||
// Verify all elements are produced correctly
|
||||
assert.Equal(t, []int{0, 1, 2, 3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("handles concurrent consumption safely", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := Async(seq, 5)
|
||||
|
||||
// Consume with some processing time
|
||||
var sum atomic.Int32
|
||||
for v := range async {
|
||||
sum.Add(int32(v))
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
assert.Equal(t, int32(55), sum.Load())
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_EdgeCases tests edge cases
|
||||
func TestAsync_EdgeCases(t *testing.T) {
|
||||
t.Run("very large buffer size", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := Async(seq, 1000000)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("buffer size of 1", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 1)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("works with replicate", func(t *testing.T) {
|
||||
seq := Replicate(5, 42)
|
||||
async := Async(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{42, 42, 42, 42, 42}, result)
|
||||
})
|
||||
|
||||
t.Run("works with makeBy", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i * i })
|
||||
async := Async(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkAsync(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async(seq, 5)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_LargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_SmallBuffer(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async(seq, 1)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_LargeBuffer(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_Unbuffered(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async(seq, 0)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_WithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async(seq, 5)
|
||||
mapped := MonadMap(async, N.Mul(2))
|
||||
for range mapped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_WithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async(seq, 5)
|
||||
filtered := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
for range filtered {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleAsync() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 10)
|
||||
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 4 5
|
||||
}
|
||||
|
||||
func ExampleAsync_unbuffered() {
|
||||
seq := From(1, 2, 3)
|
||||
async := Async(seq, 0)
|
||||
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleAsync_earlyTermination() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := Async(seq, 5)
|
||||
|
||||
count := 0
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
count++
|
||||
if count >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleAsync_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := Async(seq, 5)
|
||||
doubled := MonadMap(async, N.Mul(2))
|
||||
|
||||
for v := range doubled {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 8 10
|
||||
}
|
||||
|
||||
func ExampleAsync_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := Async(seq, 5)
|
||||
evens := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
|
||||
for v := range evens {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 8 10
|
||||
}
|
||||
|
||||
// TestAsync2_Success tests basic Async2 functionality
|
||||
func TestAsync2_Success(t *testing.T) {
|
||||
t.Run("converts Seq2 to async with buffer", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2(seq, 10)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("preserves key-value pairs order", func(t *testing.T) {
|
||||
seq := MonadZip(From("x", "y", "z"), From(10, 20, 30))
|
||||
async := Async2(seq, 5)
|
||||
|
||||
keys := []string{}
|
||||
values := []int{}
|
||||
for k, v := range async {
|
||||
keys = append(keys, k)
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"x", "y", "z"}, keys)
|
||||
assert.Equal(t, []int{10, 20, 30}, values)
|
||||
})
|
||||
|
||||
t.Run("works with single pair", func(t *testing.T) {
|
||||
seq := Of2("key", 42)
|
||||
async := Async2(seq, 1)
|
||||
result := toMap(async)
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
})
|
||||
|
||||
t.Run("works with large Seq2", func(t *testing.T) {
|
||||
keys := make([]int, 100)
|
||||
values := make([]string, 100)
|
||||
for i := range keys {
|
||||
keys[i] = i
|
||||
values[i] = fmt.Sprintf("val%d", i)
|
||||
}
|
||||
seq := MonadZip(From(keys...), From(values...))
|
||||
async := Async2(seq, 20)
|
||||
result := toMap(async)
|
||||
assert.Equal(t, 100, len(result))
|
||||
for i := range 100 {
|
||||
assert.Equal(t, fmt.Sprintf("val%d", i), result[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_BufferSizes tests different buffer sizes
|
||||
func TestAsync2_BufferSizes(t *testing.T) {
|
||||
t.Run("unbuffered channel (bufSize 0)", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2(seq, 0)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("negative buffer size treated as 0", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2(seq, -5)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("large buffer", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2(seq, 100)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_Empty tests Async2 with empty sequences
|
||||
func TestAsync2_Empty(t *testing.T) {
|
||||
t.Run("empty Seq2", func(t *testing.T) {
|
||||
seq := MonadZip(Empty[int](), Empty[string]())
|
||||
async := Async2(seq, 5)
|
||||
result := toMap(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_EarlyTermination tests that Async2 properly handles early termination
|
||||
func TestAsync2_EarlyTermination(t *testing.T) {
|
||||
t.Run("stops producer when consumer breaks", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), From("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))
|
||||
async := Async2(seq, 5)
|
||||
|
||||
count := 0
|
||||
for range async {
|
||||
count++
|
||||
if count >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, count)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_WithChainedOperations tests Async2 with other operations
|
||||
func TestAsync2_WithChainedOperations(t *testing.T) {
|
||||
t.Run("async2 after map", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From(10, 20, 30))
|
||||
mapped := MonadMapWithKey(seq, func(k, v int) int { return k + v })
|
||||
async := Async2(mapped, 5)
|
||||
result := toMap(async)
|
||||
expected := map[int]int{1: 11, 2: 22, 3: 33}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair_Success tests basic ToSeqPair functionality
|
||||
func TestToSeqPair_Success(t *testing.T) {
|
||||
t.Run("converts Seq2 to Seq of Pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, 1, pair.Head(result[0]))
|
||||
assert.Equal(t, "a", pair.Tail(result[0]))
|
||||
assert.Equal(t, 2, pair.Head(result[1]))
|
||||
assert.Equal(t, "b", pair.Tail(result[1]))
|
||||
assert.Equal(t, 3, pair.Head(result[2]))
|
||||
assert.Equal(t, "c", pair.Tail(result[2]))
|
||||
})
|
||||
|
||||
t.Run("preserves order", func(t *testing.T) {
|
||||
seq2 := MonadZip(From("x", "y", "z"), From(10, 20, 30))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
for i, p := range result {
|
||||
expectedKey := string(rune('x' + i))
|
||||
expectedVal := (i + 1) * 10
|
||||
assert.Equal(t, expectedKey, pair.Head(p))
|
||||
assert.Equal(t, expectedVal, pair.Tail(p))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("works with single pair", func(t *testing.T) {
|
||||
seq2 := Of2("key", 42)
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "key", pair.Head(result[0]))
|
||||
assert.Equal(t, 42, pair.Tail(result[0]))
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair_Empty tests ToSeqPair with empty sequences
|
||||
func TestToSeqPair_Empty(t *testing.T) {
|
||||
t.Run("empty Seq2 produces empty Seq", func(t *testing.T) {
|
||||
seq2 := MonadZip(Empty[int](), Empty[string]())
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair_WithComplexTypes tests ToSeqPair with complex types
|
||||
func TestToSeqPair_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("works with struct values", func(t *testing.T) {
|
||||
seq2 := MonadZip(
|
||||
From(1, 2, 3),
|
||||
From(Person{"Alice", 30}, Person{"Bob", 25}, Person{"Charlie", 35}),
|
||||
)
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, 1, pair.Head(result[0]))
|
||||
assert.Equal(t, Person{"Alice", 30}, pair.Tail(result[0]))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSeqPair_Success tests basic FromSeqPair functionality
|
||||
func TestFromSeqPair_Success(t *testing.T) {
|
||||
t.Run("converts Seq of Pairs to Seq2", func(t *testing.T) {
|
||||
pairs := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("preserves order", func(t *testing.T) {
|
||||
pairs := From(
|
||||
pair.MakePair("x", 10),
|
||||
pair.MakePair("y", 20),
|
||||
pair.MakePair("z", 30),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
|
||||
keys := []string{}
|
||||
values := []int{}
|
||||
for k, v := range seq2 {
|
||||
keys = append(keys, k)
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"x", "y", "z"}, keys)
|
||||
assert.Equal(t, []int{10, 20, 30}, values)
|
||||
})
|
||||
|
||||
t.Run("works with single pair", func(t *testing.T) {
|
||||
pairs := From(pair.MakePair("key", 42))
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSeqPair_Empty tests FromSeqPair with empty sequences
|
||||
func TestFromSeqPair_Empty(t *testing.T) {
|
||||
t.Run("empty Seq produces empty Seq2", func(t *testing.T) {
|
||||
pairs := Empty[Pair[int, string]]()
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSeqPair_WithComplexTypes tests FromSeqPair with complex types
|
||||
func TestFromSeqPair_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("works with struct values", func(t *testing.T) {
|
||||
pairs := From(
|
||||
pair.MakePair(1, Person{"Alice", 30}),
|
||||
pair.MakePair(2, Person{"Bob", 25}),
|
||||
pair.MakePair(3, Person{"Charlie", 35}),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
expected := map[int]Person{
|
||||
1: {"Alice", 30},
|
||||
2: {"Bob", 25},
|
||||
3: {"Charlie", 35},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRoundTrip tests that ToSeqPair and FromSeqPair are inverses
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
t.Run("ToSeqPair then FromSeqPair", func(t *testing.T) {
|
||||
original := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(original)
|
||||
restored := FromSeqPair(pairs)
|
||||
result := toMap(restored)
|
||||
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("FromSeqPair then ToSeqPair", func(t *testing.T) {
|
||||
original := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
)
|
||||
seq2 := FromSeqPair(original)
|
||||
restored := ToSeqPair(seq2)
|
||||
result := toSlice(restored)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, 1, pair.Head(result[0]))
|
||||
assert.Equal(t, "a", pair.Tail(result[0]))
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for Async2
|
||||
func BenchmarkAsync2(b *testing.B) {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), From("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async2(seq, 5)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync2_LargeSequence(b *testing.B) {
|
||||
keys := make([]int, 1000)
|
||||
values := make([]string, 1000)
|
||||
for i := range keys {
|
||||
keys[i] = i
|
||||
values[i] = fmt.Sprintf("val%d", i)
|
||||
}
|
||||
seq := MonadZip(From(keys...), From(values...))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async2(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests for FromSeqPair
|
||||
func BenchmarkFromSeqPair(b *testing.B) {
|
||||
pairs := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
pair.MakePair(4, "d"),
|
||||
pair.MakePair(5, "e"),
|
||||
)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
seq2 := FromSeqPair(pairs)
|
||||
for range seq2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRoundTrip(b *testing.B) {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
pairs := ToSeqPair(seq)
|
||||
restored := FromSeqPair(pairs)
|
||||
for range restored {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for Async2
|
||||
func ExampleAsync2() {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2(seq, 10)
|
||||
|
||||
for k, v := range async {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
}
|
||||
// Output:
|
||||
// 1: a
|
||||
// 2: b
|
||||
// 3: c
|
||||
}
|
||||
|
||||
func ExampleAsync2_earlyTermination() {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
async := Async2(seq, 5)
|
||||
|
||||
count := 0
|
||||
for k, v := range async {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
count++
|
||||
if count >= 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// 1: a
|
||||
// 2: b
|
||||
}
|
||||
|
||||
// Example tests for FromSeqPair
|
||||
func ExampleFromSeqPair() {
|
||||
pairs := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
|
||||
for k, v := range seq2 {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
}
|
||||
// Output:
|
||||
// 1: a
|
||||
// 2: b
|
||||
// 3: c
|
||||
}
|
||||
|
||||
|
||||
@@ -1002,3 +1002,80 @@ func ToSeqPair[A, B any](as Seq2[A, B]) Seq[Pair[A, B]] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromSeqPair converts a sequence of Pairs into a key-value sequence.
|
||||
//
|
||||
// This function transforms a Seq[Pair[A, B]] (which yields Pair objects when iterated)
|
||||
// into a Seq2[A, B] (which yields key-value pairs as separate arguments). This is the
|
||||
// inverse operation of ToSeqPair and is useful when you need to convert from working
|
||||
// with pairs as first-class values back to the key-value iteration pattern.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the first element (key) in each pair
|
||||
// - B: The type of the second element (value) in each pair
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - as: A Seq that yields Pair objects
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Seq2[A, B]: A key-value sequence that yields the unpacked pairs
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Create a sequence of pairs
|
||||
// pairs := From(
|
||||
// pair.MakePair("a", 1),
|
||||
// pair.MakePair("b", 2),
|
||||
// pair.MakePair("c", 3),
|
||||
// )
|
||||
// seq2 := FromSeqPair(pairs)
|
||||
//
|
||||
// // Iterate as key-value pairs
|
||||
// for k, v := range seq2 {
|
||||
// fmt.Printf("%s: %d\n", k, v)
|
||||
// }
|
||||
// // Output:
|
||||
// // a: 1
|
||||
// // b: 2
|
||||
// // c: 3
|
||||
//
|
||||
// # Example with Map
|
||||
//
|
||||
// pairs := From(
|
||||
// pair.MakePair(1, 10),
|
||||
// pair.MakePair(2, 20),
|
||||
// pair.MakePair(3, 30),
|
||||
// )
|
||||
// seq2 := FromSeqPair(pairs)
|
||||
//
|
||||
// // Use with Seq2 operations
|
||||
// mapped := MonadMapWithKey(seq2, func(k, v int) int {
|
||||
// return k + v
|
||||
// })
|
||||
// // yields: 11, 22, 33
|
||||
//
|
||||
// # Example - Round-trip conversion
|
||||
//
|
||||
// original := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
// pairs := ToSeqPair(original)
|
||||
// restored := FromSeqPair(pairs)
|
||||
// // restored is equivalent to original
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ToSeqPair: Converts Seq2 to Seq of Pairs (inverse operation)
|
||||
// - MonadZip: Creates key-value sequences from two sequences
|
||||
// - pair.MakePair: Creates a Pair from two values
|
||||
// - pair.Unpack: Unpacks a Pair into two values
|
||||
func FromSeqPair[A, B any](as Seq[Pair[A, B]]) Seq2[A, B] {
|
||||
return func(yield func(A, B) bool) {
|
||||
for p := range as {
|
||||
if !yield(pair.Unpack(p)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +115,7 @@ func Inc[T Number](value T) T {
|
||||
// result := Min(5, 10) // returns 5
|
||||
// result := Min(3.14, 2.71) // returns 2.71
|
||||
func Min[A C.Ordered](a, b A) A {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
return min(a, b)
|
||||
}
|
||||
|
||||
// Max returns the maximum of two ordered values.
|
||||
@@ -132,10 +129,7 @@ func Min[A C.Ordered](a, b A) A {
|
||||
// result := Max(5, 10) // returns 10
|
||||
// result := Max(3.14, 2.71) // returns 3.14
|
||||
func Max[A C.Ordered](a, b A) A {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
return max(a, b)
|
||||
}
|
||||
|
||||
// MoreThan is a curried comparison function that checks if a value is more than (greater than) another.
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
|
||||
@@ -909,6 +910,83 @@ func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyF transforms a value through a lens using a function that returns a value in a functor context.
|
||||
//
|
||||
// This is the functorial version of Modify, allowing transformations that produce effects
|
||||
// (like Option, Either, IO, etc.) while updating the focused value. The functor's map operation
|
||||
// is used to apply the lens's setter to the transformed value, preserving the computational context.
|
||||
//
|
||||
// This function corresponds to modifyF from monocle-ts, enabling effectful updates through lenses.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - S: Structure type
|
||||
// - A: Focus type (the value being transformed)
|
||||
// - HKTA: Higher-kinded type containing the transformed value (e.g., Option[A], Either[E, A])
|
||||
// - HKTS: Higher-kinded type containing the updated structure (e.g., Option[S], Either[E, S])
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fmap: A functor map operation that transforms A to S within the functor context
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A curried function that takes:
|
||||
// 1. A transformation function (A → HKTA)
|
||||
// 2. A Lens[S, A]
|
||||
// 3. A structure S
|
||||
// And returns the updated structure in the functor context (HKTS)
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, age int) Person { p.Age = age; return p },
|
||||
// )
|
||||
//
|
||||
// // Validate age is positive, returning Option
|
||||
// validateAge := func(age int) option.Option[int] {
|
||||
// if age > 0 {
|
||||
// return option.Some(age)
|
||||
// }
|
||||
// return option.None[int]()
|
||||
// }
|
||||
//
|
||||
// // Create a modifier that validates while updating
|
||||
// modifyAge := lens.ModifyF[Person, int](option.Functor[int, Person]().Map)
|
||||
//
|
||||
// person := Person{Name: "Alice", Age: 30}
|
||||
// result := modifyAge(validateAge)(ageLens)(person)
|
||||
// // result is Some(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
// invalidResult := modifyAge(func(age int) option.Option[int] {
|
||||
// return option.None[int]()
|
||||
// })(ageLens)(person)
|
||||
// // invalidResult is None[Person]()
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Modify: Non-functorial version for simple transformations
|
||||
// - functor.Functor: The functor interface used for mapping
|
||||
func ModifyF[S, A, HKTA, HKTS any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
|
||||
return func(sa Lens[S, A]) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
})(f(sa.Get(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IMap transforms the focus type of a lens using an isomorphism.
|
||||
//
|
||||
// An isomorphism is a pair of functions (A → B, B → A) that are inverses of each other.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package lens
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
@@ -937,3 +938,367 @@ func TestMakeLensWithEq_WithNilState_MultipleOperations(t *testing.T) {
|
||||
assert.NotNil(t, street4)
|
||||
assert.Equal(t, "", street4.name)
|
||||
}
|
||||
|
||||
// TestModifyF_Success tests ModifyF with a simple Maybe-like functor for successful transformations
|
||||
func TestModifyF_Success(t *testing.T) {
|
||||
// Define a simple Maybe type for testing
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
none := func() Maybe[int] {
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
// Functor map for Maybe
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("transforms value with successful result", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
// Function that returns Some for positive values
|
||||
validatePositive := func(n int) Maybe[int] {
|
||||
if n > 0 {
|
||||
return some(n * 2)
|
||||
}
|
||||
return none()
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: 5, Foo: "test"}
|
||||
result := modifyAge(validatePositive)(ageLens)(person)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
updated := *result.value
|
||||
assert.Equal(t, 10, updated.Value)
|
||||
assert.Equal(t, "test", updated.Foo)
|
||||
})
|
||||
|
||||
t.Run("preserves structure with identity transformation", func(t *testing.T) {
|
||||
type MaybeStr struct {
|
||||
value *string
|
||||
}
|
||||
|
||||
someStr := func(s string) MaybeStr {
|
||||
return MaybeStr{value: &s}
|
||||
}
|
||||
|
||||
maybeStrMap := func(f func(string) Street) func(MaybeStr) struct{ value *Street } {
|
||||
return func(ma MaybeStr) struct{ value *Street } {
|
||||
if ma.value == nil {
|
||||
return struct{ value *Street }{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return struct{ value *Street }{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
nameLens := MakeLens(
|
||||
func(s Street) string { return s.name },
|
||||
func(s Street, name string) Street { s.name = name; return s },
|
||||
)
|
||||
|
||||
identity := func(s string) MaybeStr {
|
||||
return someStr(s)
|
||||
}
|
||||
|
||||
modifyName := ModifyF[Street, string](maybeStrMap)
|
||||
|
||||
street := Street{num: 1, name: "Main"}
|
||||
result := modifyName(identity)(nameLens)(street)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, street, *result.value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_Failure tests ModifyF with failures
|
||||
func TestModifyF_Failure(t *testing.T) {
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
none := func() Maybe[int] {
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("returns None when transformation fails", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
validatePositive := func(n int) Maybe[int] {
|
||||
if n > 0 {
|
||||
return some(n)
|
||||
}
|
||||
return none()
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: -5, Foo: "test"}
|
||||
result := modifyAge(validatePositive)(ageLens)(person)
|
||||
|
||||
assert.Nil(t, result.value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_WithResult tests ModifyF with Result/Either-like functor
|
||||
func TestModifyF_WithResult(t *testing.T) {
|
||||
type Result[A any] struct {
|
||||
value *A
|
||||
err error
|
||||
}
|
||||
|
||||
ok := func(a int) Result[int] {
|
||||
return Result[int]{value: &a, err: nil}
|
||||
}
|
||||
|
||||
fail := func(e error) Result[int] {
|
||||
return Result[int]{value: nil, err: e}
|
||||
}
|
||||
|
||||
resultMap := func(f func(int) Inner) func(Result[int]) Result[Inner] {
|
||||
return func(r Result[int]) Result[Inner] {
|
||||
if r.err != nil {
|
||||
return Result[Inner]{value: nil, err: r.err}
|
||||
}
|
||||
result := f(*r.value)
|
||||
return Result[Inner]{value: &result, err: nil}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("returns success for valid transformation", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
validateAge := func(n int) Result[int] {
|
||||
if n >= 0 && n <= 150 {
|
||||
return ok(n + 1)
|
||||
}
|
||||
return fail(errors.New("age out of range"))
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](resultMap)
|
||||
|
||||
person := Inner{Value: 30, Foo: "test"}
|
||||
result := modifyAge(validateAge)(ageLens)(person)
|
||||
|
||||
assert.Nil(t, result.err)
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, 31, result.value.Value)
|
||||
assert.Equal(t, "test", result.value.Foo)
|
||||
})
|
||||
|
||||
t.Run("returns error for failed validation", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
validateAge := func(n int) Result[int] {
|
||||
if n >= 0 && n <= 150 {
|
||||
return ok(n)
|
||||
}
|
||||
return fail(errors.New("age out of range"))
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](resultMap)
|
||||
|
||||
person := Inner{Value: 200, Foo: "test"}
|
||||
result := modifyAge(validateAge)(ageLens)(person)
|
||||
|
||||
assert.NotNil(t, result.err)
|
||||
assert.Equal(t, "age out of range", result.err.Error())
|
||||
assert.Nil(t, result.value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_EdgeCases tests edge cases for ModifyF
|
||||
func TestModifyF_EdgeCases(t *testing.T) {
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("handles zero values", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
identity := func(n int) Maybe[int] {
|
||||
return some(n)
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: 0, Foo: ""}
|
||||
result := modifyAge(identity)(ageLens)(person)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, person, *result.value)
|
||||
})
|
||||
|
||||
t.Run("works with composed lenses", func(t *testing.T) {
|
||||
innerLens := MakeLens(
|
||||
Outer.GetInner,
|
||||
Outer.SetInner,
|
||||
)
|
||||
valueLens := MakeLensRef(
|
||||
(*Inner).GetValue,
|
||||
(*Inner).SetValue,
|
||||
)
|
||||
|
||||
composedLens := Compose[Outer](valueLens)(innerLens)
|
||||
|
||||
maybeMapOuter := func(f func(int) Outer) func(Maybe[int]) Maybe[Outer] {
|
||||
return func(ma Maybe[int]) Maybe[Outer] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Outer]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Outer]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
validatePositive := func(n int) Maybe[int] {
|
||||
if n > 0 {
|
||||
return some(n * 2)
|
||||
}
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
modifyValue := ModifyF[Outer, int](maybeMapOuter)
|
||||
|
||||
outer := Outer{inner: &Inner{Value: 5, Foo: "test"}}
|
||||
result := modifyValue(validatePositive)(composedLens)(outer)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, 10, result.value.inner.Value)
|
||||
assert.Equal(t, "test", result.value.inner.Foo)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_Integration tests integration scenarios
|
||||
func TestModifyF_Integration(t *testing.T) {
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("chains multiple ModifyF operations", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
increment := func(n int) Maybe[int] {
|
||||
return some(n + 1)
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: 5, Foo: "test"}
|
||||
|
||||
// Apply transformation twice
|
||||
result1 := modifyAge(increment)(ageLens)(person)
|
||||
assert.NotNil(t, result1.value)
|
||||
|
||||
result2 := modifyAge(increment)(ageLens)(*result1.value)
|
||||
assert.NotNil(t, result2.value)
|
||||
|
||||
assert.Equal(t, 7, result2.value.Value)
|
||||
})
|
||||
|
||||
t.Run("combines with regular Modify", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
// First use regular Modify
|
||||
person := Inner{Value: 5, Foo: "test"}
|
||||
modified := F.Pipe2(
|
||||
ageLens,
|
||||
Modify[Inner](func(n int) int { return n * 2 }),
|
||||
func(endoFn func(Inner) Inner) Inner {
|
||||
return endoFn(person)
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, 10, modified.Value)
|
||||
|
||||
// Then use ModifyF with validation
|
||||
validateRange := func(n int) Maybe[int] {
|
||||
if n >= 0 && n <= 100 {
|
||||
return some(n)
|
||||
}
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
result := modifyAge(validateRange)(ageLens)(modified)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user