mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-30 00:09:39 +02:00
Compare commits
2 Commits
v2.0.3
...
cleue-or-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
451cbc8bf6 | ||
|
|
49227551b6 |
@@ -73,6 +73,28 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A
|
||||
return readereither.FromPredicate[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation with access to context.Context.
|
||||
// If the ReaderResult is Right, it returns the value unchanged.
|
||||
// If the ReaderResult is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderResult that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations
|
||||
// that need access to the context (for cancellation, deadlines, or values).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover with context-aware fallback
|
||||
// recover := readerresult.OrElse(func(err error) readerresult.ReaderResult[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return func(ctx context.Context) result.Result[int] {
|
||||
// // Could check ctx.Err() here
|
||||
// return result.Of(42)
|
||||
// }
|
||||
// }
|
||||
// return readerresult.Left[int](err)
|
||||
// })
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] {
|
||||
return readereither.OrElse(F.Flow2(onLeft, WithContext))
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
@@ -313,3 +314,69 @@ func TestMonadChainTo(t *testing.T) {
|
||||
assert.False(t, secondExecuted, "second reader should not be executed on error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Test OrElse with Right - should pass through unchanged
|
||||
t.Run("Right value unchanged", func(t *testing.T) {
|
||||
rightValue := Of(42)
|
||||
recover := OrElse(func(err error) ReaderResult[int] {
|
||||
return Left[int](errors.New("should not be called"))
|
||||
})
|
||||
res := recover(rightValue)(ctx)
|
||||
assert.Equal(t, E.Of[error](42), res)
|
||||
})
|
||||
|
||||
// Test OrElse with Left - should recover with fallback
|
||||
t.Run("Left value recovered", func(t *testing.T) {
|
||||
leftValue := Left[int](errors.New("not found"))
|
||||
recoverWithFallback := OrElse(func(err error) ReaderResult[int] {
|
||||
if err.Error() == "not found" {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
return E.Of[error](99)
|
||||
}
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
res := recoverWithFallback(leftValue)(ctx)
|
||||
assert.Equal(t, E.Of[error](99), res)
|
||||
})
|
||||
|
||||
// Test OrElse with Left - should propagate other errors
|
||||
t.Run("Left value propagated", func(t *testing.T) {
|
||||
leftValue := Left[int](errors.New("fatal error"))
|
||||
recoverWithFallback := OrElse(func(err error) ReaderResult[int] {
|
||||
if err.Error() == "not found" {
|
||||
return Of(99)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
res := recoverWithFallback(leftValue)(ctx)
|
||||
assert.True(t, E.IsLeft(res))
|
||||
val, err := E.UnwrapError(res)
|
||||
assert.Equal(t, 0, val)
|
||||
assert.Equal(t, "fatal error", err.Error())
|
||||
})
|
||||
|
||||
// Test OrElse with context-aware recovery
|
||||
t.Run("Context-aware recovery", func(t *testing.T) {
|
||||
type ctxKey string
|
||||
ctxWithValue := context.WithValue(ctx, ctxKey("fallback"), 123)
|
||||
|
||||
leftValue := Left[int](errors.New("use fallback"))
|
||||
ctxRecover := OrElse(func(err error) ReaderResult[int] {
|
||||
if err.Error() == "use fallback" {
|
||||
return func(ctx context.Context) E.Either[error, int] {
|
||||
if val := ctx.Value(ctxKey("fallback")); val != nil {
|
||||
return E.Of[error](val.(int))
|
||||
}
|
||||
return E.Left[int](errors.New("no fallback"))
|
||||
}
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
res := ctxRecover(leftValue)(ctxWithValue)
|
||||
assert.Equal(t, E.Of[error](123), res)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -466,16 +466,29 @@ func Alt[E, A any](that Lazy[Either[E, A]]) Operator[E, A, A] {
|
||||
return AltW[E](that)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left by providing an alternative computation.
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the Either is Right, it returns the value unchanged.
|
||||
// If the Either is Left, it applies the provided function to the error value,
|
||||
// which returns a new Either that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback values
|
||||
// recover := either.OrElse(func(err error) either.Either[error, int] {
|
||||
// return either.Right[error](0) // default value
|
||||
// if err.Error() == "not found" {
|
||||
// return either.Right[error](0) // default value
|
||||
// }
|
||||
// return either.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(either.Left[int](errors.New("fail"))) // Right(0)
|
||||
func OrElse[E, A any](onLeft Kleisli[E, E, A]) Operator[E, A, A] {
|
||||
return Fold(onLeft, Of[E, A])
|
||||
// result := recover(either.Left[int](errors.New("not found"))) // Right(0)
|
||||
// result := recover(either.Right[error](42)) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[E1, E2, A any](onLeft Kleisli[E2, E1, A]) Kleisli[E2, Either[E1, A], A] {
|
||||
return Fold(onLeft, Of[E2, A])
|
||||
}
|
||||
|
||||
// ToType attempts to convert an any value to a specific type, returning Either.
|
||||
|
||||
@@ -160,6 +160,7 @@ func TestToError(t *testing.T) {
|
||||
|
||||
// Test OrElse
|
||||
func TestOrElse(t *testing.T) {
|
||||
// Test basic recovery from Left
|
||||
recover := OrElse(func(e error) Either[error, int] {
|
||||
return Right[error](0)
|
||||
})
|
||||
@@ -167,8 +168,85 @@ func TestOrElse(t *testing.T) {
|
||||
result := recover(Left[int](errors.New("error")))
|
||||
assert.Equal(t, Right[error](0), result)
|
||||
|
||||
// Test Right value passes through unchanged
|
||||
result = recover(Right[error](42))
|
||||
assert.Equal(t, Right[error](42), result)
|
||||
|
||||
// Test selective recovery - recover some errors, propagate others
|
||||
selectiveRecover := OrElse(func(err error) Either[error, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[error](0) // default value for "not found"
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
assert.Equal(t, Right[error](0), selectiveRecover(Left[int](errors.New("not found"))))
|
||||
permissionErr := errors.New("permission denied")
|
||||
assert.Equal(t, Left[int](permissionErr), selectiveRecover(Left[int](permissionErr)))
|
||||
|
||||
// Test chaining multiple OrElse operations
|
||||
firstRecover := OrElse(func(err error) Either[error, int] {
|
||||
if err.Error() == "error1" {
|
||||
return Right[error](1)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
secondRecover := OrElse(func(err error) Either[error, int] {
|
||||
if err.Error() == "error2" {
|
||||
return Right[error](2)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
assert.Equal(t, Right[error](1), F.Pipe1(Left[int](errors.New("error1")), firstRecover))
|
||||
assert.Equal(t, Right[error](2), F.Pipe1(Left[int](errors.New("error2")), F.Flow2(firstRecover, secondRecover)))
|
||||
}
|
||||
|
||||
// Test OrElseW
|
||||
func TestOrElseW(t *testing.T) {
|
||||
type ValidationError string
|
||||
type AppError int
|
||||
|
||||
// Test with Right value - should return Right with widened error type
|
||||
rightValue := Right[ValidationError]("success")
|
||||
recoverValidation := OrElse(func(ve ValidationError) Either[AppError, string] {
|
||||
return Left[string](AppError(400))
|
||||
})
|
||||
result := recoverValidation(rightValue)
|
||||
assert.True(t, IsRight(result))
|
||||
assert.Equal(t, "success", F.Pipe1(result, GetOrElse(F.Constant1[AppError](""))))
|
||||
|
||||
// Test with Left value - should apply recovery with new error type
|
||||
leftValue := Left[string](ValidationError("invalid input"))
|
||||
result = recoverValidation(leftValue)
|
||||
assert.True(t, IsLeft(result))
|
||||
_, leftVal := Unwrap(result)
|
||||
assert.Equal(t, AppError(400), leftVal)
|
||||
|
||||
// Test error type conversion - ValidationError to AppError
|
||||
convertError := OrElse(func(ve ValidationError) Either[AppError, int] {
|
||||
return Left[int](AppError(len(ve)))
|
||||
})
|
||||
converted := convertError(Left[int](ValidationError("short")))
|
||||
assert.True(t, IsLeft(converted))
|
||||
_, leftConv := Unwrap(converted)
|
||||
assert.Equal(t, AppError(5), leftConv)
|
||||
|
||||
// Test recovery to Right with widened error type
|
||||
recoverToRight := OrElse(func(ve ValidationError) Either[AppError, int] {
|
||||
if ve == "recoverable" {
|
||||
return Right[AppError](99)
|
||||
}
|
||||
return Left[int](AppError(500))
|
||||
})
|
||||
assert.Equal(t, Right[AppError](99), recoverToRight(Left[int](ValidationError("recoverable"))))
|
||||
assert.True(t, IsLeft(recoverToRight(Left[int](ValidationError("fatal")))))
|
||||
|
||||
// Test that Right values are preserved with widened error type
|
||||
preservedRight := Right[ValidationError](42)
|
||||
preserveRecover := OrElse(func(ve ValidationError) Either[AppError, int] {
|
||||
return Left[int](AppError(999))
|
||||
})
|
||||
preserved := preserveRecover(preservedRight)
|
||||
assert.Equal(t, Right[AppError](42), preserved)
|
||||
}
|
||||
|
||||
// Test ToType
|
||||
|
||||
@@ -599,3 +599,28 @@ func ChainFirstLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
|
||||
func TapLeft[A, EA, EB, B any](f Kleisli[EB, EA, B]) Operator[EA, A, A] {
|
||||
return ChainFirstLeft[A](f)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the IOEither is Right, it returns the value unchanged.
|
||||
// If the IOEither is Left, it applies the provided function to the error value,
|
||||
// which returns a new IOEither that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative IO computations.
|
||||
// The error type can be widened from E1 to E2, allowing transformation of error types.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback IO operations
|
||||
// recover := ioeither.OrElse(func(err error) ioeither.IOEither[error, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return ioeither.Right[error](0) // default value
|
||||
// }
|
||||
// return ioeither.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(ioeither.Left[int](errors.New("not found"))) // Right(0)
|
||||
// result := recover(ioeither.Right[error](42)) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[E1, E2, A any](onLeft Kleisli[E2, E1, A]) Kleisli[E2, IOEither[E1, A], A] {
|
||||
return Fold(onLeft, Of[E2, A])
|
||||
}
|
||||
|
||||
@@ -401,3 +401,92 @@ func TestChainFirstLeft(t *testing.T) {
|
||||
assert.Equal(t, E.Left[string]("step2"), actualResult)
|
||||
})
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
// Test basic recovery from Left
|
||||
recover := OrElse(func(e error) IOEither[error, int] {
|
||||
return Right[error](0)
|
||||
})
|
||||
|
||||
result := recover(Left[int](fmt.Errorf("error")))()
|
||||
assert.Equal(t, E.Right[error](0), result)
|
||||
|
||||
// Test Right value passes through unchanged
|
||||
result = recover(Right[error](42))()
|
||||
assert.Equal(t, E.Right[error](42), result)
|
||||
|
||||
// Test selective recovery - recover some errors, propagate others
|
||||
selectiveRecover := OrElse(func(err error) IOEither[error, int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[error](0) // default value for "not found"
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
assert.Equal(t, E.Right[error](0), selectiveRecover(Left[int](fmt.Errorf("not found")))())
|
||||
permissionErr := fmt.Errorf("permission denied")
|
||||
assert.Equal(t, E.Left[int](permissionErr), selectiveRecover(Left[int](permissionErr))())
|
||||
|
||||
// Test chaining multiple OrElse operations
|
||||
firstRecover := OrElse(func(err error) IOEither[error, int] {
|
||||
if err.Error() == "error1" {
|
||||
return Right[error](1)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
secondRecover := OrElse(func(err error) IOEither[error, int] {
|
||||
if err.Error() == "error2" {
|
||||
return Right[error](2)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
assert.Equal(t, E.Right[error](1), F.Pipe1(Left[int](fmt.Errorf("error1")), firstRecover)())
|
||||
assert.Equal(t, E.Right[error](2), F.Pipe1(Left[int](fmt.Errorf("error2")), F.Flow2(firstRecover, secondRecover))())
|
||||
}
|
||||
|
||||
func TestOrElseW(t *testing.T) {
|
||||
type ValidationError string
|
||||
type AppError int
|
||||
|
||||
// Test with Right value - should return Right with widened error type
|
||||
rightValue := Right[ValidationError]("success")
|
||||
recoverValidation := OrElse(func(ve ValidationError) IOEither[AppError, string] {
|
||||
return Left[string](AppError(400))
|
||||
})
|
||||
result := recoverValidation(rightValue)()
|
||||
assert.True(t, E.IsRight(result))
|
||||
assert.Equal(t, "success", F.Pipe1(result, E.GetOrElse(F.Constant1[AppError](""))))
|
||||
|
||||
// Test with Left value - should apply recovery with new error type
|
||||
leftValue := Left[string](ValidationError("invalid input"))
|
||||
result = recoverValidation(leftValue)()
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, leftVal := E.Unwrap(result)
|
||||
assert.Equal(t, AppError(400), leftVal)
|
||||
|
||||
// Test error type conversion - ValidationError to AppError
|
||||
convertError := OrElse(func(ve ValidationError) IOEither[AppError, int] {
|
||||
return Left[int](AppError(len(ve)))
|
||||
})
|
||||
converted := convertError(Left[int](ValidationError("short")))()
|
||||
assert.True(t, E.IsLeft(converted))
|
||||
_, leftConv := E.Unwrap(converted)
|
||||
assert.Equal(t, AppError(5), leftConv)
|
||||
|
||||
// Test recovery to Right with widened error type
|
||||
recoverToRight := OrElse(func(ve ValidationError) IOEither[AppError, int] {
|
||||
if ve == "recoverable" {
|
||||
return Right[AppError](99)
|
||||
}
|
||||
return Left[int](AppError(500))
|
||||
})
|
||||
assert.Equal(t, E.Right[AppError](99), recoverToRight(Left[int](ValidationError("recoverable")))())
|
||||
assert.True(t, E.IsLeft(recoverToRight(Left[int](ValidationError("fatal")))()))
|
||||
|
||||
// Test that Right values are preserved with widened error type
|
||||
preservedRight := Right[ValidationError](42)
|
||||
preserveRecover := OrElse(func(ve ValidationError) IOEither[AppError, int] {
|
||||
return Left[int](AppError(999))
|
||||
})
|
||||
preserved := preserveRecover(preservedRight)()
|
||||
assert.Equal(t, E.Right[AppError](42), preserved)
|
||||
}
|
||||
|
||||
@@ -489,3 +489,28 @@ func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
|
||||
return ioeither.TapLeft[A](f)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation.
|
||||
// If the IOResult is Right, it returns the value unchanged.
|
||||
// If the IOResult is Left, it applies the provided function to the error value,
|
||||
// which returns a new IOResult that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations
|
||||
// in IO contexts. Since IOResult is specialized for error type, the error type remains error.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Recover from specific errors with fallback IO operations
|
||||
// recover := ioresult.OrElse(func(err error) ioresult.IOResult[int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return ioresult.Right[int](0) // default value
|
||||
// }
|
||||
// return ioresult.Left[int](err) // propagate other errors
|
||||
// })
|
||||
// result := recover(ioresult.Left[int](errors.New("not found"))) // Right(0)
|
||||
// result := recover(ioresult.Right[int](42)) // Right(42) - unchanged
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
|
||||
return ioeither.OrElse(onLeft)
|
||||
}
|
||||
|
||||
@@ -150,3 +150,51 @@ func TestApSecond(t *testing.T) {
|
||||
|
||||
assert.Equal(t, result.Of("b"), x())
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
// Test basic recovery from Left
|
||||
recover := OrElse(func(e error) IOResult[int] {
|
||||
return Right[int](0)
|
||||
})
|
||||
|
||||
res := recover(Left[int](fmt.Errorf("error")))()
|
||||
assert.Equal(t, result.Of(0), res)
|
||||
|
||||
// Test Right value passes through unchanged
|
||||
res = recover(Right[int](42))()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
|
||||
// Test selective recovery - recover some errors, propagate others
|
||||
selectiveRecover := OrElse(func(err error) IOResult[int] {
|
||||
if err.Error() == "not found" {
|
||||
return Right[int](0) // default value for "not found"
|
||||
}
|
||||
return Left[int](err) // propagate other errors
|
||||
})
|
||||
notFoundResult := selectiveRecover(Left[int](fmt.Errorf("not found")))()
|
||||
assert.Equal(t, result.Of(0), notFoundResult)
|
||||
|
||||
permissionErr := fmt.Errorf("permission denied")
|
||||
permissionResult := selectiveRecover(Left[int](permissionErr))()
|
||||
assert.Equal(t, result.Left[int](permissionErr), permissionResult)
|
||||
|
||||
// Test chaining multiple OrElse operations
|
||||
firstRecover := OrElse(func(err error) IOResult[int] {
|
||||
if err.Error() == "error1" {
|
||||
return Right[int](1)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
secondRecover := OrElse(func(err error) IOResult[int] {
|
||||
if err.Error() == "error2" {
|
||||
return Right[int](2)
|
||||
}
|
||||
return Left[int](err)
|
||||
})
|
||||
|
||||
result1 := F.Pipe1(Left[int](fmt.Errorf("error1")), firstRecover)()
|
||||
assert.Equal(t, result.Of(1), result1)
|
||||
|
||||
result2 := F.Pipe1(Left[int](fmt.Errorf("error2")), F.Flow2(firstRecover, secondRecover))()
|
||||
assert.Equal(t, result.Of(2), result2)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ import (
|
||||
// 3. Filtering to keep only pairs where the boolean (tail) is true
|
||||
// 4. Extracting the original values (head) from the filtered pairs
|
||||
//
|
||||
// RxJS Equivalent: Similar to combining [zip] with [filter] - https://rxjs.dev/api/operators/zip
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence to be filtered
|
||||
//
|
||||
|
||||
96
v2/iterator/iter/cycle.go
Normal file
96
v2/iterator/iter/cycle.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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
|
||||
|
||||
// Cycle creates a sequence that repeats the elements of the input sequence indefinitely.
|
||||
//
|
||||
// This function takes a finite sequence and creates an infinite sequence by cycling through
|
||||
// all elements repeatedly. When the end of the input sequence is reached, it starts over
|
||||
// from the beginning, continuing this pattern forever.
|
||||
//
|
||||
// RxJS Equivalent: [repeat] - https://rxjs.dev/api/operators/repeat
|
||||
//
|
||||
// WARNING: This creates an INFINITE sequence for non-empty inputs. It must be used with
|
||||
// operations that limit the output (such as Take, First, or early termination in iteration)
|
||||
// to avoid infinite loops.
|
||||
//
|
||||
// If the input sequence is empty, Cycle returns an empty sequence immediately. It does NOT
|
||||
// loop indefinitely - the result is simply an empty sequence.
|
||||
//
|
||||
// The operation is lazy - elements are only generated as they are consumed. The input sequence
|
||||
// is re-iterated each time the cycle completes, so any side effects in the source sequence
|
||||
// will be repeated.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The input sequence to cycle through. Should be finite.
|
||||
//
|
||||
// Returns:
|
||||
// - An infinite sequence that repeats the elements of the input sequence
|
||||
//
|
||||
// Example - Basic cycling with Take:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
// cycled := Cycle(seq)
|
||||
// result := Take[int](7)(cycled)
|
||||
// // yields: 1, 2, 3, 1, 2, 3, 1
|
||||
//
|
||||
// Example - Cycling strings:
|
||||
//
|
||||
// seq := From("A", "B", "C")
|
||||
// cycled := Cycle(seq)
|
||||
// result := Take[string](5)(cycled)
|
||||
// // yields: "A", "B", "C", "A", "B"
|
||||
//
|
||||
// Example - Using with First:
|
||||
//
|
||||
// seq := From(10, 20, 30)
|
||||
// cycled := Cycle(seq)
|
||||
// first := First(cycled)
|
||||
// // returns: Some(10)
|
||||
//
|
||||
// Example - Combining with filter and take:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// cycled := Cycle(seq)
|
||||
// evens := MonadFilter(cycled, func(x int) bool { return x%2 == 0 })
|
||||
// result := Take[int](5)(evens)
|
||||
// // yields: 2, 4, 2, 4, 2 (cycles through even numbers)
|
||||
//
|
||||
// Example - Empty sequence (returns empty, does not loop):
|
||||
//
|
||||
// seq := Empty[int]()
|
||||
// cycled := Cycle(seq)
|
||||
// result := Take[int](10)(cycled)
|
||||
// // yields: nothing (empty sequence, terminates immediately)
|
||||
func Cycle[U any](ma Seq[U]) Seq[U] {
|
||||
return func(yield func(U) bool) {
|
||||
for {
|
||||
isEmpty := true
|
||||
for u := range ma {
|
||||
if !yield(u) {
|
||||
return
|
||||
}
|
||||
isEmpty = false
|
||||
}
|
||||
if isEmpty {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
611
v2/iterator/iter/cycle_test.go
Normal file
611
v2/iterator/iter/cycle_test.go
Normal file
@@ -0,0 +1,611 @@
|
||||
// Copyright (c) 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"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCycleBasic tests basic Cycle functionality with Take
|
||||
func TestCycleBasic(t *testing.T) {
|
||||
t.Run("cycles through integer sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles through string sequence", func(t *testing.T) {
|
||||
seq := From("A", "B", "C")
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[string](8)(cycled))
|
||||
assert.Equal(t, []string{"A", "B", "C", "A", "B", "C", "A", "B"}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles through single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](5)(cycled))
|
||||
assert.Equal(t, []int{42, 42, 42, 42, 42}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles through two elements", func(t *testing.T) {
|
||||
seq := From(true, false)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[bool](6)(cycled))
|
||||
assert.Equal(t, []bool{true, false, true, false, true, false}, result)
|
||||
})
|
||||
|
||||
t.Run("takes exact multiple of cycle length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](9)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes less than one cycle", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](3)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleEmpty tests Cycle with empty sequences
|
||||
func TestCycleEmpty(t *testing.T) {
|
||||
t.Run("cycles empty sequence produces nothing", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](10)(cycled))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("cycles empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[string](5)(cycled))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("take zero from cycled sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](0)(cycled))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithComplexTypes tests Cycle with complex data types
|
||||
func TestCycleWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("cycles structs", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[Person](5)(cycled))
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Alice", 30},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("cycles pointers", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
seq := From(p1, p2)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[*Person](4)(cycled))
|
||||
assert.Equal(t, []*Person{p1, p2, p1, p2}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles slices", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3, 4})
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[[]int](5)(cycled))
|
||||
expected := [][]int{{1, 2}, {3, 4}, {1, 2}, {3, 4}, {1, 2}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithFirst tests Cycle with First operation
|
||||
func TestCycleWithFirst(t *testing.T) {
|
||||
t.Run("gets first element from cycled sequence", func(t *testing.T) {
|
||||
seq := From(10, 20, 30)
|
||||
cycled := Cycle(seq)
|
||||
first := First(cycled)
|
||||
assert.Equal(t, O.Of(10), first)
|
||||
})
|
||||
|
||||
t.Run("gets first from single element cycle", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
cycled := Cycle(seq)
|
||||
first := First(cycled)
|
||||
assert.Equal(t, O.Of(42), first)
|
||||
})
|
||||
|
||||
t.Run("gets none from empty cycle", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
cycled := Cycle(seq)
|
||||
first := First(cycled)
|
||||
assert.Equal(t, O.None[int](), first)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithChainedOperations tests Cycle with other operations
|
||||
func TestCycleWithChainedOperations(t *testing.T) {
|
||||
t.Run("cycle then map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
mapped := MonadMap(cycled, N.Mul(10))
|
||||
result := toSlice(Take[int](7)(mapped))
|
||||
assert.Equal(t, []int{10, 20, 30, 10, 20, 30, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("cycle then filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
filtered := MonadFilter(cycled, func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(Take[int](6)(filtered))
|
||||
assert.Equal(t, []int{2, 4, 2, 4, 2, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("map then cycle", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
cycled := Cycle(mapped)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{2, 4, 6, 2, 4, 6, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("filter then cycle", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
cycled := Cycle(filtered)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{2, 4, 6, 2, 4, 6, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("cycle with multiple takes", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
taken1 := Take[int](10)(cycled)
|
||||
taken2 := Take[int](5)(taken1)
|
||||
result := toSlice(taken2)
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithReplicate tests Cycle with Replicate
|
||||
func TestCycleWithReplicate(t *testing.T) {
|
||||
t.Run("cycles replicated values", func(t *testing.T) {
|
||||
seq := Replicate(3, "X")
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[string](7)(cycled))
|
||||
assert.Equal(t, []string{"X", "X", "X", "X", "X", "X", "X"}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles single replicated value", func(t *testing.T) {
|
||||
seq := Replicate(1, 99)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](5)(cycled))
|
||||
assert.Equal(t, []int{99, 99, 99, 99, 99}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithMakeBy tests Cycle with MakeBy
|
||||
func TestCycleWithMakeBy(t *testing.T) {
|
||||
t.Run("cycles generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(3, func(i int) int { return i * i })
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](8)(cycled))
|
||||
assert.Equal(t, []int{0, 1, 4, 0, 1, 4, 0, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles single generated element", func(t *testing.T) {
|
||||
seq := MakeBy(1, func(i int) int { return i + 10 })
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](4)(cycled))
|
||||
assert.Equal(t, []int{10, 10, 10, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithPrependAppend tests Cycle with Prepend and Append
|
||||
func TestCycleWithPrependAppend(t *testing.T) {
|
||||
t.Run("cycle prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3)
|
||||
prepended := Prepend(1)(seq)
|
||||
cycled := Cycle(prepended)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("cycle appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
appended := Append(3)(seq)
|
||||
cycled := Cycle(appended)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithFlatten tests Cycle with Flatten
|
||||
func TestCycleWithFlatten(t *testing.T) {
|
||||
t.Run("cycles flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3))
|
||||
flattened := Flatten(nested)
|
||||
cycled := Cycle(flattened)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithChain tests Cycle with Chain
|
||||
func TestCycleWithChain(t *testing.T) {
|
||||
t.Run("cycles chained sequence", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
cycled := Cycle(chained)
|
||||
result := toSlice(Take[int](10)(cycled))
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 1, 10, 2, 20, 1, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleEarlyTermination tests that Cycle respects early termination
|
||||
func TestCycleEarlyTermination(t *testing.T) {
|
||||
t.Run("terminates when yield returns false", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
|
||||
count := 0
|
||||
for v := range cycled {
|
||||
count++
|
||||
if v == 2 && count > 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Should have stopped at the second occurrence of 2
|
||||
assert.Equal(t, 5, count) // 1, 2, 3, 1, 2
|
||||
})
|
||||
|
||||
t.Run("take limits infinite cycle", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
taken := Take[int](100)(cycled)
|
||||
|
||||
result := toSlice(taken)
|
||||
assert.Len(t, result, 100)
|
||||
|
||||
// Verify pattern repeats correctly
|
||||
for i := 0; i < 100; i++ {
|
||||
expected := (i % 3) + 1
|
||||
assert.Equal(t, expected, result[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleLargeSequence tests Cycle with larger sequences
|
||||
func TestCycleLargeSequence(t *testing.T) {
|
||||
t.Run("cycles large sequence", func(t *testing.T) {
|
||||
data := make([]int, 10)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](25)(cycled))
|
||||
|
||||
assert.Len(t, result, 25)
|
||||
// Verify first cycle
|
||||
for i := 0; i < 10; i++ {
|
||||
assert.Equal(t, i, result[i])
|
||||
}
|
||||
// Verify second cycle
|
||||
for i := 10; i < 20; i++ {
|
||||
assert.Equal(t, i-10, result[i])
|
||||
}
|
||||
// Verify partial third cycle
|
||||
for i := 20; i < 25; i++ {
|
||||
assert.Equal(t, i-20, result[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithReduce tests Cycle with Reduce (limited by Take)
|
||||
func TestCycleWithReduce(t *testing.T) {
|
||||
t.Run("reduces limited cycled sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
limited := Take[int](10)(cycled)
|
||||
sum := MonadReduce(limited, func(acc, x int) int { return acc + x }, 0)
|
||||
// 1+2+3+1+2+3+1+2+3+1 = 19
|
||||
assert.Equal(t, 19, sum)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleEdgeCases tests edge cases
|
||||
func TestCycleEdgeCases(t *testing.T) {
|
||||
t.Run("cycle with very long take", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](1000)(cycled))
|
||||
assert.Len(t, result, 1000)
|
||||
|
||||
// Verify pattern
|
||||
for i := 0; i < 1000; i++ {
|
||||
expected := (i % 2) + 1
|
||||
assert.Equal(t, expected, result[i])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cycle single element many times", func(t *testing.T) {
|
||||
seq := From(7)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](100)(cycled))
|
||||
assert.Len(t, result, 100)
|
||||
for _, v := range result {
|
||||
assert.Equal(t, 7, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkCycle(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
taken := Take[int](100)(cycled)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCycleSingleElement(b *testing.B) {
|
||||
seq := From(42)
|
||||
cycled := Cycle(seq)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
taken := Take[int](100)(cycled)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCycleWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mapped := MonadMap(cycled, N.Mul(2))
|
||||
taken := Take[int](100)(mapped)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCycleWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
cycled := Cycle(seq)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filtered := MonadFilter(cycled, func(x int) bool { return x%2 == 0 })
|
||||
taken := Take[int](50)(filtered)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleCycle() {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
result := Take[int](7)(cycled)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 1 2 3 1
|
||||
}
|
||||
|
||||
func ExampleCycle_singleElement() {
|
||||
seq := From("X")
|
||||
cycled := Cycle(seq)
|
||||
result := Take[string](5)(cycled)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: X X X X X
|
||||
}
|
||||
|
||||
func ExampleCycle_withFirst() {
|
||||
seq := From(10, 20, 30)
|
||||
cycled := Cycle(seq)
|
||||
first := First(cycled)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First: %d\n", value)
|
||||
}
|
||||
// Output: First: 10
|
||||
}
|
||||
|
||||
func ExampleCycle_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
evens := MonadFilter(cycled, func(x int) bool { return x%2 == 0 })
|
||||
result := Take[int](6)(evens)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 2 4 2 4
|
||||
}
|
||||
|
||||
func ExampleCycle_withMap() {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
doubled := MonadMap(cycled, N.Mul(2))
|
||||
result := Take[int](7)(doubled)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 2 4 6 2
|
||||
}
|
||||
|
||||
func ExampleCycle_empty() {
|
||||
seq := Empty[int]()
|
||||
cycled := Cycle(seq)
|
||||
result := Take[int](5)(cycled)
|
||||
|
||||
count := 0
|
||||
for range result {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleCycle_exactMultiple() {
|
||||
seq := From("A", "B", "C")
|
||||
cycled := Cycle(seq)
|
||||
result := Take[string](9)(cycled)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: A B C A B C A B C
|
||||
}
|
||||
|
||||
// TestCycleWithZip tests Cycle combined with Zip operator
|
||||
func TestCycleWithZip(t *testing.T) {
|
||||
t.Run("zip infinite cycled sequence with finite sequence", func(t *testing.T) {
|
||||
// Create an infinite sequence by cycling
|
||||
infinite := Cycle(From(1, 2, 3))
|
||||
// Create a finite sequence
|
||||
finite := From("a", "b", "c", "d", "e")
|
||||
|
||||
// Zip them together - should stop when finite sequence ends
|
||||
zipped := MonadZip(infinite, finite)
|
||||
|
||||
// Convert to slice for verification
|
||||
result := make([]struct {
|
||||
num int
|
||||
str string
|
||||
}, 0)
|
||||
for num, str := range zipped {
|
||||
result = append(result, struct {
|
||||
num int
|
||||
str string
|
||||
}{num, str})
|
||||
}
|
||||
|
||||
// Should have 5 pairs (limited by finite sequence)
|
||||
assert.Len(t, result, 5)
|
||||
assert.Equal(t, 1, result[0].num)
|
||||
assert.Equal(t, "a", result[0].str)
|
||||
assert.Equal(t, 2, result[1].num)
|
||||
assert.Equal(t, "b", result[1].str)
|
||||
assert.Equal(t, 3, result[2].num)
|
||||
assert.Equal(t, "c", result[2].str)
|
||||
assert.Equal(t, 1, result[3].num) // Cycle repeats
|
||||
assert.Equal(t, "d", result[3].str)
|
||||
assert.Equal(t, 2, result[4].num)
|
||||
assert.Equal(t, "e", result[4].str)
|
||||
})
|
||||
|
||||
t.Run("zip finite sequence with infinite cycled sequence", func(t *testing.T) {
|
||||
// Reverse order: finite first, infinite second
|
||||
finite := From(10, 20, 30)
|
||||
infinite := Cycle(From("X", "Y"))
|
||||
|
||||
zipped := MonadZip(finite, infinite)
|
||||
|
||||
result := make([]struct {
|
||||
num int
|
||||
str string
|
||||
}, 0)
|
||||
for num, str := range zipped {
|
||||
result = append(result, struct {
|
||||
num int
|
||||
str string
|
||||
}{num, str})
|
||||
}
|
||||
|
||||
// Should have 3 pairs (limited by finite sequence)
|
||||
assert.Len(t, result, 3)
|
||||
assert.Equal(t, 10, result[0].num)
|
||||
assert.Equal(t, "X", result[0].str)
|
||||
assert.Equal(t, 20, result[1].num)
|
||||
assert.Equal(t, "Y", result[1].str)
|
||||
assert.Equal(t, 30, result[2].num)
|
||||
assert.Equal(t, "X", result[2].str) // Cycle repeats
|
||||
})
|
||||
|
||||
t.Run("zip two cycled sequences with take", func(t *testing.T) {
|
||||
// Both sequences are infinite, so we need Take to limit
|
||||
cycle1 := Cycle(From(1, 2))
|
||||
cycle2 := Cycle(From("a", "b", "c"))
|
||||
|
||||
zipped := MonadZip(cycle1, cycle2)
|
||||
|
||||
// Use Take to limit the infinite result
|
||||
count := 0
|
||||
result := make([]struct {
|
||||
num int
|
||||
str string
|
||||
}, 0)
|
||||
for num, str := range zipped {
|
||||
result = append(result, struct {
|
||||
num int
|
||||
str string
|
||||
}{num, str})
|
||||
count++
|
||||
if count >= 7 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, result, 7)
|
||||
// Verify the pattern
|
||||
assert.Equal(t, 1, result[0].num)
|
||||
assert.Equal(t, "a", result[0].str)
|
||||
assert.Equal(t, 2, result[1].num)
|
||||
assert.Equal(t, "b", result[1].str)
|
||||
assert.Equal(t, 1, result[2].num) // cycle1 repeats
|
||||
assert.Equal(t, "c", result[2].str)
|
||||
assert.Equal(t, 2, result[3].num)
|
||||
assert.Equal(t, "a", result[3].str) // cycle2 repeats
|
||||
})
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import "github.com/IBM/fp-go/v2/option"
|
||||
// contains at least one element, it returns Some(element). If the iterator is empty,
|
||||
// it returns None. The function consumes only the first element of the iterator.
|
||||
//
|
||||
// RxJS Equivalent: [first] - https://rxjs.dev/api/operators/first
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the iterator
|
||||
//
|
||||
|
||||
@@ -81,6 +81,8 @@ func Of2[K, A any](k K, a A) Seq2[K, A] {
|
||||
// MonadMap transforms each element in a sequence using the provided function.
|
||||
// This is the monadic version that takes the sequence as the first parameter.
|
||||
//
|
||||
// RxJS Equivalent: [map] - https://rxjs.dev/api/operators/map
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
@@ -183,6 +185,8 @@ func MapWithKey[K, A, B any](f func(K, A) B) Operator2[K, A, B] {
|
||||
|
||||
// MonadFilter returns a sequence containing only elements that satisfy the predicate.
|
||||
//
|
||||
// RxJS Equivalent: [filter] - https://rxjs.dev/api/operators/filter
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
@@ -425,6 +429,8 @@ func FilterMapWithKey[K, A, B any](f func(K, A) Option[B]) Operator2[K, A, B] {
|
||||
// MonadChain applies a function that returns a sequence to each element and flattens the results.
|
||||
// This is the monadic bind operation (flatMap).
|
||||
//
|
||||
// RxJS Equivalent: [mergeMap/flatMap] - https://rxjs.dev/api/operators/mergeMap
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
@@ -461,6 +467,8 @@ func Chain[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
|
||||
// Flatten flattens a sequence of sequences into a single sequence.
|
||||
//
|
||||
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nested := From(From(1, 2), From(3, 4), From(5))
|
||||
@@ -563,6 +571,8 @@ func Replicate[A any](n int, a A) Seq[A] {
|
||||
// MonadReduce reduces a sequence to a single value by applying a function to each element
|
||||
// and an accumulator, starting with an initial value.
|
||||
//
|
||||
// RxJS Equivalent: [reduce] - https://rxjs.dev/api/operators/reduce
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
@@ -819,6 +829,8 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
|
||||
// Prepend returns a function that adds an element to the beginning of a sequence.
|
||||
//
|
||||
// RxJS Equivalent: [startWith] - https://rxjs.dev/api/operators/startWith
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(2, 3, 4)
|
||||
@@ -832,6 +844,8 @@ func Prepend[A any](head A) Operator[A, A] {
|
||||
|
||||
// Append returns a function that adds an element to the end of a sequence.
|
||||
//
|
||||
// RxJS Equivalent: [endWith] - https://rxjs.dev/api/operators/endWith
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
@@ -846,6 +860,8 @@ func Append[A any](tail A) Operator[A, A] {
|
||||
// MonadZip combines two sequences into a sequence of pairs.
|
||||
// The resulting sequence stops when either input sequence is exhausted.
|
||||
//
|
||||
// RxJS Equivalent: [zip] - https://rxjs.dev/api/operators/zip
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seqA := From(1, 2, 3)
|
||||
|
||||
105
v2/iterator/iter/scan.go
Normal file
105
v2/iterator/iter/scan.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// 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
|
||||
|
||||
// Scan applies an accumulator function over a sequence, emitting each intermediate result.
|
||||
//
|
||||
// This function is similar to Reduce, but instead of returning only the final accumulated value,
|
||||
// it returns a sequence containing all intermediate accumulated values. Each element in the
|
||||
// output sequence is the result of applying the accumulator function to the previous accumulated
|
||||
// value and the current input element.
|
||||
//
|
||||
// The operation is lazy - intermediate values are computed only as they are consumed.
|
||||
//
|
||||
// RxJS Equivalent: [scan] - https://rxjs.dev/api/operators/scan
|
||||
//
|
||||
// Scan is useful for:
|
||||
// - Computing running totals or cumulative sums
|
||||
// - Tracking state changes over a sequence
|
||||
// - Building up complex values incrementally
|
||||
// - Generating sequences based on previous values
|
||||
//
|
||||
// Type Parameters:
|
||||
// - FCT: The accumulator function type, must be ~func(V, U) V
|
||||
// - U: The type of elements in the input sequence
|
||||
// - V: The type of the accumulated value and elements in the output sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The accumulator function that takes the current accumulated value and the next
|
||||
// input element, returning the new accumulated value
|
||||
// - initial: The initial accumulated value (not included in the output sequence)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] into a Seq[V] containing all intermediate
|
||||
// accumulated values
|
||||
//
|
||||
// Example - Running sum:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// runningSum := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
// result := runningSum(seq)
|
||||
// // yields: 1, 3, 6, 10, 15
|
||||
//
|
||||
// Example - Running product:
|
||||
//
|
||||
// seq := From(2, 3, 4)
|
||||
// runningProduct := Scan(func(acc, x int) int { return acc * x }, 1)
|
||||
// result := runningProduct(seq)
|
||||
// // yields: 2, 6, 24
|
||||
//
|
||||
// Example - Building strings:
|
||||
//
|
||||
// seq := From("a", "b", "c")
|
||||
// concat := Scan(func(acc, x string) string { return acc + x }, "")
|
||||
// result := concat(seq)
|
||||
// // yields: "a", "ab", "abc"
|
||||
//
|
||||
// Example - Tracking maximum:
|
||||
//
|
||||
// seq := From(3, 1, 4, 1, 5, 9, 2)
|
||||
// maxSoFar := Scan(func(acc, x int) int {
|
||||
// if x > acc { return x }
|
||||
// return acc
|
||||
// }, 0)
|
||||
// result := maxSoFar(seq)
|
||||
// // yields: 3, 3, 4, 4, 5, 9, 9
|
||||
//
|
||||
// Example - Empty sequence:
|
||||
//
|
||||
// seq := Empty[int]()
|
||||
// runningSum := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
// result := runningSum(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - Single element:
|
||||
//
|
||||
// seq := From(42)
|
||||
// runningSum := Scan(func(acc, x int) int { return acc + x }, 10)
|
||||
// result := runningSum(seq)
|
||||
// // yields: 52
|
||||
func Scan[FCT ~func(V, U) V, U, V any](f FCT, initial V) Operator[U, V] {
|
||||
return func(s Seq[U]) Seq[V] {
|
||||
return func(yield func(V) bool) {
|
||||
current := initial
|
||||
for u := range s {
|
||||
current = f(current, u)
|
||||
if !yield(current) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
407
v2/iterator/iter/scan_test.go
Normal file
407
v2/iterator/iter/scan_test.go
Normal file
@@ -0,0 +1,407 @@
|
||||
// Copyright (c) 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"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestScanBasic tests basic Scan functionality
|
||||
func TestScanBasic(t *testing.T) {
|
||||
t.Run("running sum of integers", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{1, 3, 6, 10, 15}, result)
|
||||
})
|
||||
|
||||
t.Run("running product", func(t *testing.T) {
|
||||
seq := From(2, 3, 4)
|
||||
scanned := Scan(func(acc, x int) int { return acc * x }, 1)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{2, 6, 24}, result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
scanned := Scan(func(acc, x string) string { return acc + x }, "")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []string{"a", "ab", "abc"}, result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation with separator", func(t *testing.T) {
|
||||
seq := From("hello", "world", "test")
|
||||
scanned := Scan(func(acc, x string) string {
|
||||
if acc == "" {
|
||||
return x
|
||||
}
|
||||
return acc + "-" + x
|
||||
}, "")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []string{"hello", "hello-world", "hello-world-test"}, result)
|
||||
})
|
||||
|
||||
t.Run("single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 10)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{52}, result)
|
||||
})
|
||||
|
||||
t.Run("two elements", func(t *testing.T) {
|
||||
seq := From(5, 10)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{5, 15}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanEmpty tests Scan with empty sequences
|
||||
func TestScanEmpty(t *testing.T) {
|
||||
t.Run("empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
scanned := Scan(func(acc, x string) string { return acc + x }, "start")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithDifferentTypes tests Scan with different input/output types
|
||||
func TestScanWithDifferentTypes(t *testing.T) {
|
||||
t.Run("int to string accumulation", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
scanned := Scan(func(acc string, x int) string {
|
||||
return fmt.Sprintf("%s%d", acc, x)
|
||||
}, "")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []string{"1", "12", "123"}, result)
|
||||
})
|
||||
|
||||
t.Run("string to int length accumulation", func(t *testing.T) {
|
||||
seq := From("a", "bb", "ccc")
|
||||
scanned := Scan(func(acc int, x string) int {
|
||||
return acc + len(x)
|
||||
}, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{1, 3, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("accumulate into slice", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
scanned := Scan(func(acc []int, x int) []int {
|
||||
return append(acc, x)
|
||||
}, []int{})
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, [][]int{
|
||||
{1},
|
||||
{1, 2},
|
||||
{1, 2, 3},
|
||||
}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanStateful tests Scan with stateful operations
|
||||
func TestScanStateful(t *testing.T) {
|
||||
t.Run("tracking maximum", func(t *testing.T) {
|
||||
seq := From(3, 1, 4, 1, 5, 9, 2)
|
||||
scanned := Scan(func(acc, x int) int {
|
||||
if x > acc {
|
||||
return x
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{3, 3, 4, 4, 5, 9, 9}, result)
|
||||
})
|
||||
|
||||
t.Run("tracking minimum", func(t *testing.T) {
|
||||
seq := From(5, 3, 8, 1, 4, 2)
|
||||
scanned := Scan(func(acc, x int) int {
|
||||
if acc == 0 || x < acc {
|
||||
return x
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{5, 3, 3, 1, 1, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("counting occurrences", func(t *testing.T) {
|
||||
seq := From(1, 2, 1, 3, 1, 2)
|
||||
scanned := Scan(func(acc map[int]int, x int) map[int]int {
|
||||
newMap := make(map[int]int)
|
||||
for k, v := range acc {
|
||||
newMap[k] = v
|
||||
}
|
||||
newMap[x]++
|
||||
return newMap
|
||||
}, map[int]int{})
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Len(t, result, 6)
|
||||
assert.Equal(t, 1, result[0][1])
|
||||
assert.Equal(t, 1, result[1][2])
|
||||
assert.Equal(t, 2, result[2][1])
|
||||
assert.Equal(t, 3, result[4][1])
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithComplexTypes tests Scan with complex data types
|
||||
func TestScanWithComplexTypes(t *testing.T) {
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
t.Run("accumulate points", func(t *testing.T) {
|
||||
seq := From(Point{1, 0}, Point{0, 1}, Point{2, 2})
|
||||
scanned := Scan(func(acc, p Point) Point {
|
||||
return Point{acc.X + p.X, acc.Y + p.Y}
|
||||
}, Point{0, 0})
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []Point{
|
||||
{1, 0},
|
||||
{1, 1},
|
||||
{3, 3},
|
||||
}, result)
|
||||
})
|
||||
|
||||
t.Run("accumulate struct fields", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value int
|
||||
Count int
|
||||
}
|
||||
seq := From(5, 10, 15)
|
||||
scanned := Scan(func(acc Data, x int) Data {
|
||||
return Data{
|
||||
Value: acc.Value + x,
|
||||
Count: acc.Count + 1,
|
||||
}
|
||||
}, Data{0, 0})
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []Data{
|
||||
{5, 1},
|
||||
{15, 2},
|
||||
{30, 3},
|
||||
}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithChainedOperations tests Scan combined with other operations
|
||||
func TestScanWithChainedOperations(t *testing.T) {
|
||||
t.Run("scan then map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
mapped := MonadMap(scanned(seq), func(x int) int { return x * 2 })
|
||||
result := toSlice(mapped)
|
||||
assert.Equal(t, []int{2, 6, 12, 20}, result)
|
||||
})
|
||||
|
||||
t.Run("map then scan", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4)
|
||||
mapped := MonadMap(seq, func(x int) int { return x * 2 })
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(mapped))
|
||||
assert.Equal(t, []int{2, 6, 12, 20}, result)
|
||||
})
|
||||
|
||||
t.Run("scan then filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
filtered := MonadFilter(scanned(seq), func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{6, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("scan then take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
taken := Take[int](3)(scanned(seq))
|
||||
result := toSlice(taken)
|
||||
assert.Equal(t, []int{1, 3, 6}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithCycle tests Scan with infinite sequences
|
||||
func TestScanWithCycle(t *testing.T) {
|
||||
t.Run("scan cycled sequence with take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
taken := Take[int](10)(scanned(cycled))
|
||||
result := toSlice(taken)
|
||||
// 1, 3, 6, 7, 9, 12, 13, 15, 18, 19
|
||||
assert.Equal(t, []int{1, 3, 6, 7, 9, 12, 13, 15, 18, 19}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanEarlyTermination tests that Scan respects early termination
|
||||
func TestScanEarlyTermination(t *testing.T) {
|
||||
t.Run("terminates when yield returns false", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
|
||||
count := 0
|
||||
for v := range scanned(seq) {
|
||||
count++
|
||||
if v >= 6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, count) // Should stop at 6 (1+2+3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithInitialValue tests different initial values
|
||||
func TestScanWithInitialValue(t *testing.T) {
|
||||
t.Run("non-zero initial value", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 10)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{11, 13, 16}, result)
|
||||
})
|
||||
|
||||
t.Run("negative initial value", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, -10)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{-9, -7, -4}, result)
|
||||
})
|
||||
|
||||
t.Run("string initial value", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
scanned := Scan(func(acc, x string) string { return acc + x }, "start:")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []string{"start:a", "start:ab", "start:abc"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanLargeSequence tests Scan with larger sequences
|
||||
func TestScanLargeSequence(t *testing.T) {
|
||||
t.Run("scan large sequence", func(t *testing.T) {
|
||||
data := make([]int, 100)
|
||||
for i := range data {
|
||||
data[i] = i + 1
|
||||
}
|
||||
seq := From(data...)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
|
||||
assert.Len(t, result, 100)
|
||||
// Sum of 1 to n is n*(n+1)/2
|
||||
assert.Equal(t, 5050, result[99]) // Sum of 1 to 100
|
||||
assert.Equal(t, 1, result[0])
|
||||
assert.Equal(t, 3, result[1])
|
||||
assert.Equal(t, 6, result[2])
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkScan(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range scanned(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkScanLarge(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i + 1
|
||||
}
|
||||
seq := From(data...)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range scanned(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleScan() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
runningSum := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := runningSum(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 3 6 10 15
|
||||
}
|
||||
|
||||
func ExampleScan_runningProduct() {
|
||||
seq := From(2, 3, 4)
|
||||
runningProduct := Scan(func(acc, x int) int { return acc * x }, 1)
|
||||
result := runningProduct(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 6 24
|
||||
}
|
||||
|
||||
func ExampleScan_stringConcatenation() {
|
||||
seq := From("a", "b", "c")
|
||||
concat := Scan(func(acc, x string) string { return acc + x }, "")
|
||||
result := concat(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: a ab abc
|
||||
}
|
||||
|
||||
func ExampleScan_trackingMaximum() {
|
||||
seq := From(3, 1, 4, 1, 5, 9, 2)
|
||||
maxSoFar := Scan(func(acc, x int) int {
|
||||
if x > acc {
|
||||
return x
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
result := maxSoFar(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 3 3 4 4 5 9 9
|
||||
}
|
||||
|
||||
func ExampleScan_empty() {
|
||||
seq := Empty[int]()
|
||||
runningSum := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := runningSum(seq)
|
||||
|
||||
count := 0
|
||||
for range result {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
80
v2/iterator/iter/take.go
Normal file
80
v2/iterator/iter/take.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// 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 F "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// Take returns an operator that limits the number of elements in a sequence to at most n elements.
|
||||
//
|
||||
// This function creates a transformation that takes the first n elements from a sequence
|
||||
// and discards the rest. If n is less than or equal to 0, it returns an empty sequence.
|
||||
// If the input sequence has fewer than n elements, all elements are returned.
|
||||
//
|
||||
// The operation is lazy and only consumes elements from the source sequence as needed.
|
||||
// Once n elements have been yielded, iteration stops immediately without consuming
|
||||
// the remaining elements from the source.
|
||||
//
|
||||
// RxJS Equivalent: [take] - https://rxjs.dev/api/operators/take
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - n: The maximum number of elements to take from the sequence
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] by taking at most n elements
|
||||
//
|
||||
// Example - Take first 3 elements:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// result := Take[int](3)(seq)
|
||||
// // yields: 1, 2, 3
|
||||
//
|
||||
// Example - Take more than available:
|
||||
//
|
||||
// seq := From(1, 2)
|
||||
// result := Take[int](5)(seq)
|
||||
// // yields: 1, 2 (all available elements)
|
||||
//
|
||||
// Example - Take zero or negative:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
// result := Take[int](0)(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - Chaining with other operations:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// evens := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
// result := Take[int](3)(evens)
|
||||
// // yields: 2, 4, 6 (first 3 even numbers)
|
||||
func Take[U any](n int) Operator[U, U] {
|
||||
if n <= 0 {
|
||||
return F.Constant1[Seq[U]](Empty[U]())
|
||||
}
|
||||
return func(s Seq[U]) Seq[U] {
|
||||
return func(yield Predicate[U]) {
|
||||
i := 0
|
||||
for u := range s {
|
||||
if i >= n || !yield(u) {
|
||||
return
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
463
v2/iterator/iter/take_test.go
Normal file
463
v2/iterator/iter/take_test.go
Normal file
@@ -0,0 +1,463 @@
|
||||
// Copyright (c) 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"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTake tests basic Take functionality
|
||||
func TestTake(t *testing.T) {
|
||||
t.Run("takes first n elements from sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Take[int](3)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes first element", func(t *testing.T) {
|
||||
seq := From(10, 20, 30)
|
||||
result := toSlice(Take[int](1)(seq))
|
||||
assert.Equal(t, []int{10}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all elements when n equals length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Take[int](3)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all elements when n exceeds length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Take[int](10)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "d", "e")
|
||||
result := toSlice(Take[string](3)(seq))
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
|
||||
t.Run("takes from single element sequence", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Take[int](1)(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("takes from large sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := toSlice(Take[int](5)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeZeroOrNegative tests Take with zero or negative values
|
||||
func TestTakeZeroOrNegative(t *testing.T) {
|
||||
t.Run("returns empty sequence when n is zero", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Take[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty sequence when n is negative", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Take[int](-1)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty sequence when n is large negative", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
result := toSlice(Take[string](-100)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeEmpty tests Take with empty sequences
|
||||
func TestTakeEmpty(t *testing.T) {
|
||||
t.Run("returns empty from empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(Take[int](5)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty from empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
result := toSlice(Take[string](3)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when taking zero from empty", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(Take[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithComplexTypes tests Take with complex data types
|
||||
func TestTakeWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("takes structs", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
result := toSlice(Take[Person](2)(seq))
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("takes pointers", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
result := toSlice(Take[*Person](2)(seq))
|
||||
assert.Equal(t, []*Person{p1, p2}, result)
|
||||
})
|
||||
|
||||
t.Run("takes slices", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3, 4}, []int{5, 6}, []int{7, 8})
|
||||
result := toSlice(Take[[]int](2)(seq))
|
||||
expected := [][]int{{1, 2}, {3, 4}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithChainedOperations tests Take with other sequence operations
|
||||
func TestTakeWithChainedOperations(t *testing.T) {
|
||||
t.Run("take after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
result := toSlice(Take[int](3)(mapped))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("take 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 })
|
||||
result := toSlice(Take[int](3)(filtered))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("map after take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
taken := Take[int](3)(seq)
|
||||
result := toSlice(MonadMap(taken, N.Mul(10)))
|
||||
assert.Equal(t, []int{10, 20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
taken := Take[int](6)(seq)
|
||||
result := toSlice(MonadFilter(taken, func(x int) bool { return x%2 == 0 }))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("take after chain", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
result := toSlice(Take[int](4)(chained))
|
||||
assert.Equal(t, []int{1, 10, 2, 20}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple takes", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken1 := Take[int](7)(seq)
|
||||
taken2 := Take[int](3)(taken1)
|
||||
result := toSlice(taken2)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithReplicate tests Take with Replicate
|
||||
func TestTakeWithReplicate(t *testing.T) {
|
||||
t.Run("takes from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(10, 42)
|
||||
result := toSlice(Take[int](3)(seq))
|
||||
assert.Equal(t, []int{42, 42, 42}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all from short replicate", func(t *testing.T) {
|
||||
seq := Replicate(2, "hello")
|
||||
result := toSlice(Take[string](5)(seq))
|
||||
assert.Equal(t, []string{"hello", "hello"}, result)
|
||||
})
|
||||
|
||||
t.Run("takes zero from replicate", func(t *testing.T) {
|
||||
seq := Replicate(5, 100)
|
||||
result := toSlice(Take[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithMakeBy tests Take with MakeBy
|
||||
func TestTakeWithMakeBy(t *testing.T) {
|
||||
t.Run("takes from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(10, func(i int) int { return i * i })
|
||||
result := toSlice(Take[int](5)(seq))
|
||||
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
|
||||
})
|
||||
|
||||
t.Run("takes more than generated", func(t *testing.T) {
|
||||
seq := MakeBy(3, func(i int) int { return i + 1 })
|
||||
result := toSlice(Take[int](10)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithPrependAppend tests Take with Prepend and Append
|
||||
func TestTakeWithPrependAppend(t *testing.T) {
|
||||
t.Run("take from prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3, 4, 5)
|
||||
prepended := Prepend(1)(seq)
|
||||
result := toSlice(Take[int](3)(prepended))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("take from appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(Take[int](2)(appended))
|
||||
assert.Equal(t, []int{1, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("take includes appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(Take[int](4)(appended))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithFlatten tests Take with Flatten
|
||||
func TestTakeWithFlatten(t *testing.T) {
|
||||
t.Run("takes from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5, 6))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(Take[int](4)(flattened))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("takes from flattened with empty inner sequences", func(t *testing.T) {
|
||||
nested := From(From(1, 2), Empty[int](), From(3, 4))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(Take[int](3)(flattened))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeDoesNotConsumeEntireSequence tests that Take is lazy
|
||||
func TestTakeDoesNotConsumeEntireSequence(t *testing.T) {
|
||||
t.Run("only consumes needed elements", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := MonadMap(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), func(x int) int {
|
||||
callCount++
|
||||
return x * 2
|
||||
})
|
||||
|
||||
taken := Take[int](3)(seq)
|
||||
|
||||
// Manually iterate to verify lazy evaluation
|
||||
result := []int{}
|
||||
for v := range taken {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
// The map function may be called one extra time to check if there are more elements
|
||||
// This is expected behavior with Go's range over iterators
|
||||
assert.LessOrEqual(t, callCount, 4, "should not consume significantly more than needed")
|
||||
assert.GreaterOrEqual(t, callCount, 3, "should consume at least the needed elements")
|
||||
})
|
||||
|
||||
t.Run("stops early with filter", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool {
|
||||
callCount++
|
||||
return x%2 == 0
|
||||
})
|
||||
|
||||
taken := Take[int](2)(filtered)
|
||||
|
||||
// Manually iterate to verify lazy evaluation
|
||||
result := []int{}
|
||||
for v := range taken {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{2, 4}, result)
|
||||
// Should stop after finding 2 even numbers, may check a few more elements
|
||||
assert.LessOrEqual(t, callCount, 7, "should not consume significantly more than needed")
|
||||
assert.GreaterOrEqual(t, callCount, 4, "should consume at least enough to find 2 evens")
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeEdgeCases tests edge cases
|
||||
func TestTakeEdgeCases(t *testing.T) {
|
||||
t.Run("take 1 from single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Take[int](1)(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("take 0 from single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Take[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("take large number from small sequence", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
result := toSlice(Take[int](1000000)(seq))
|
||||
assert.Equal(t, []int{1, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("take with very large n", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Take[int](int(^uint(0) >> 1))(seq)) // max int
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkTake(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
taken := Take[int](5)(seq)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
taken := Take[int](100)(seq)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
taken := Take[int](5)(mapped)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
taken := Take[int](3)(filtered)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleTake() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
taken := Take[int](3)(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleTake_moreThanAvailable() {
|
||||
seq := From(1, 2, 3)
|
||||
taken := Take[int](10)(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleTake_zero() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
taken := Take[int](0)(seq)
|
||||
|
||||
count := 0
|
||||
for range taken {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleTake_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
evens := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
taken := Take[int](3)(evens)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6
|
||||
}
|
||||
|
||||
func ExampleTake_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
doubled := MonadMap(seq, N.Mul(2))
|
||||
taken := Take[int](3)(doubled)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6
|
||||
}
|
||||
|
||||
func ExampleTake_chained() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := Take[int](5)(
|
||||
MonadFilter(seq, func(x int) bool { return x > 3 }),
|
||||
)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 4 5 6 7 8
|
||||
}
|
||||
167
v2/iterator/iter/uniq.go
Normal file
167
v2/iterator/iter/uniq.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// 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 F "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// Uniq returns an operator that filters a sequence to contain only unique elements,
|
||||
// where uniqueness is determined by a key extraction function.
|
||||
//
|
||||
// This function takes a key extraction function and returns an operator that removes
|
||||
// duplicate elements from a sequence. Two elements are considered duplicates if the
|
||||
// key extraction function returns the same key for both. Only the first occurrence
|
||||
// of each unique key is kept in the output sequence.
|
||||
//
|
||||
// The operation maintains a map of seen keys internally, so memory usage grows with
|
||||
// the number of unique keys encountered. The operation is lazy - elements are processed
|
||||
// and filtered as they are consumed.
|
||||
//
|
||||
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the sequence
|
||||
// - K: The type of the key used for uniqueness comparison (must be comparable)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that extracts a comparable key from each element
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters the sequence to contain only unique elements based on the key
|
||||
//
|
||||
// Example - Remove duplicate integers:
|
||||
//
|
||||
// seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
// unique := Uniq(func(x int) int { return x })
|
||||
// result := unique(seq)
|
||||
// // yields: 1, 2, 3, 4, 5
|
||||
//
|
||||
// Example - Unique by string length:
|
||||
//
|
||||
// seq := From("a", "bb", "c", "dd", "eee")
|
||||
// uniqueByLength := Uniq(func(s string) int { return len(s) })
|
||||
// result := uniqueByLength(seq)
|
||||
// // yields: "a", "bb", "eee" (first occurrence of each length)
|
||||
//
|
||||
// Example - Unique structs by field:
|
||||
//
|
||||
// type Person struct { ID int; Name string }
|
||||
// seq := From(
|
||||
// Person{1, "Alice"},
|
||||
// Person{2, "Bob"},
|
||||
// Person{1, "Alice2"}, // duplicate ID
|
||||
// )
|
||||
// uniqueByID := Uniq(func(p Person) int { return p.ID })
|
||||
// result := uniqueByID(seq)
|
||||
// // yields: Person{1, "Alice"}, Person{2, "Bob"}
|
||||
//
|
||||
// Example - Case-insensitive unique strings:
|
||||
//
|
||||
// seq := From("Hello", "world", "HELLO", "World", "test")
|
||||
// uniqueCaseInsensitive := Uniq(func(s string) string {
|
||||
// return strings.ToLower(s)
|
||||
// })
|
||||
// result := uniqueCaseInsensitive(seq)
|
||||
// // yields: "Hello", "world", "test"
|
||||
//
|
||||
// Example - Empty sequence:
|
||||
//
|
||||
// seq := Empty[int]()
|
||||
// unique := Uniq(func(x int) int { return x })
|
||||
// result := unique(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - All duplicates:
|
||||
//
|
||||
// seq := From(1, 1, 1, 1)
|
||||
// unique := Uniq(func(x int) int { return x })
|
||||
// result := unique(seq)
|
||||
// // yields: 1 (only first occurrence)
|
||||
func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
|
||||
return func(s Seq[A]) Seq[A] {
|
||||
return func(yield func(A) bool) {
|
||||
items := make(map[K]struct{})
|
||||
for a := range s {
|
||||
k := f(a)
|
||||
if _, ok := items[k]; !ok {
|
||||
items[k] = struct{}{}
|
||||
if !yield(a) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StrictUniq filters a sequence to contain only unique elements using direct comparison.
|
||||
//
|
||||
// This is a convenience function that uses the identity function as the key extractor,
|
||||
// meaning elements are compared directly for uniqueness. It's equivalent to calling
|
||||
// Uniq with the identity function, but provides a simpler API when the elements
|
||||
// themselves are comparable.
|
||||
//
|
||||
// The operation maintains a map of seen elements internally, so memory usage grows with
|
||||
// the number of unique elements. Only the first occurrence of each unique element is kept.
|
||||
//
|
||||
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the sequence (must be comparable)
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input sequence to filter for unique elements
|
||||
//
|
||||
// Returns:
|
||||
// - A sequence containing only the first occurrence of each unique element
|
||||
//
|
||||
// Example - Remove duplicate integers:
|
||||
//
|
||||
// seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: 1, 2, 3, 4, 5
|
||||
//
|
||||
// Example - Remove duplicate strings:
|
||||
//
|
||||
// seq := From("apple", "banana", "apple", "cherry", "banana")
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: "apple", "banana", "cherry"
|
||||
//
|
||||
// Example - Single element:
|
||||
//
|
||||
// seq := From(42)
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: 42
|
||||
//
|
||||
// Example - All duplicates:
|
||||
//
|
||||
// seq := From("x", "x", "x")
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: "x" (only first occurrence)
|
||||
//
|
||||
// Example - Empty sequence:
|
||||
//
|
||||
// seq := Empty[int]()
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - Already unique:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: 1, 2, 3, 4, 5 (no changes)
|
||||
func StrictUniq[A comparable](as Seq[A]) Seq[A] {
|
||||
return Uniq(F.Identity[A])(as)
|
||||
}
|
||||
433
v2/iterator/iter/uniq_test.go
Normal file
433
v2/iterator/iter/uniq_test.go
Normal file
@@ -0,0 +1,433 @@
|
||||
// Copyright (c) 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"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestUniqBasic tests basic Uniq functionality
|
||||
func TestUniqBasic(t *testing.T) {
|
||||
t.Run("removes duplicate integers", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("removes duplicate strings", func(t *testing.T) {
|
||||
seq := From("apple", "banana", "apple", "cherry", "banana")
|
||||
unique := Uniq(F.Identity[string])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []string{"apple", "banana", "cherry"}, result)
|
||||
})
|
||||
|
||||
t.Run("keeps first occurrence", func(t *testing.T) {
|
||||
seq := From(1, 2, 1, 3, 2, 4)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("all duplicates", func(t *testing.T) {
|
||||
seq := From(5, 5, 5, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{5}, result)
|
||||
})
|
||||
|
||||
t.Run("already unique", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqEmpty tests Uniq with empty sequences
|
||||
func TestUniqEmpty(t *testing.T) {
|
||||
t.Run("empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
unique := Uniq(F.Identity[string])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqWithKeyExtractor tests Uniq with custom key extraction
|
||||
func TestUniqWithKeyExtractor(t *testing.T) {
|
||||
t.Run("unique by string length", func(t *testing.T) {
|
||||
seq := From("a", "bb", "c", "dd", "eee", "f")
|
||||
uniqueByLength := Uniq(S.Size)
|
||||
result := toSlice(uniqueByLength(seq))
|
||||
assert.Equal(t, []string{"a", "bb", "eee"}, result)
|
||||
})
|
||||
|
||||
t.Run("unique by absolute value", func(t *testing.T) {
|
||||
seq := From(1, -1, 2, -2, 3, 1, -3)
|
||||
uniqueByAbs := Uniq(func(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
})
|
||||
result := toSlice(uniqueByAbs(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("case-insensitive unique strings", func(t *testing.T) {
|
||||
seq := From("Hello", "world", "HELLO", "World", "test")
|
||||
uniqueCaseInsensitive := Uniq(strings.ToLower)
|
||||
result := toSlice(uniqueCaseInsensitive(seq))
|
||||
assert.Equal(t, []string{"Hello", "world", "test"}, result)
|
||||
})
|
||||
|
||||
t.Run("unique by modulo", func(t *testing.T) {
|
||||
seq := From(1, 4, 7, 2, 5, 8, 3)
|
||||
uniqueByMod3 := Uniq(func(x int) int { return x % 3 })
|
||||
result := toSlice(uniqueByMod3(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result) // 1%3=1, 4%3=1 (dup), 7%3=1 (dup), 2%3=2, 5%3=2 (dup), 8%3=2 (dup), 3%3=0
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqWithComplexTypes tests Uniq with structs and complex types
|
||||
func TestUniqWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
t.Run("unique structs by ID", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{1, "Alice"},
|
||||
Person{2, "Bob"},
|
||||
Person{1, "Alice2"}, // duplicate ID
|
||||
Person{3, "Charlie"},
|
||||
Person{2, "Bob2"}, // duplicate ID
|
||||
)
|
||||
uniqueByID := Uniq(func(p Person) int { return p.ID })
|
||||
result := toSlice(uniqueByID(seq))
|
||||
assert.Equal(t, []Person{
|
||||
{1, "Alice"},
|
||||
{2, "Bob"},
|
||||
{3, "Charlie"},
|
||||
}, result)
|
||||
})
|
||||
|
||||
t.Run("unique structs by name", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{1, "Alice"},
|
||||
Person{2, "Bob"},
|
||||
Person{3, "Alice"}, // duplicate name
|
||||
)
|
||||
uniqueByName := Uniq(func(p Person) string { return p.Name })
|
||||
result := toSlice(uniqueByName(seq))
|
||||
assert.Equal(t, []Person{
|
||||
{1, "Alice"},
|
||||
{2, "Bob"},
|
||||
}, result)
|
||||
})
|
||||
|
||||
t.Run("unique slices by length", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3}, []int{4, 5}, []int{6})
|
||||
uniqueByLength := Uniq(func(s []int) int { return len(s) })
|
||||
result := toSlice(uniqueByLength(seq))
|
||||
assert.Len(t, result, 2)
|
||||
assert.Equal(t, 2, len(result[0]))
|
||||
assert.Equal(t, 1, len(result[1]))
|
||||
})
|
||||
}
|
||||
|
||||
// TestStrictUniq tests StrictUniq functionality
|
||||
func TestStrictUniq(t *testing.T) {
|
||||
t.Run("removes duplicate integers", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("removes duplicate strings", func(t *testing.T) {
|
||||
seq := From("apple", "banana", "apple", "cherry", "banana")
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []string{"apple", "banana", "cherry"}, result)
|
||||
})
|
||||
|
||||
t.Run("single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("all duplicates", func(t *testing.T) {
|
||||
seq := From("x", "x", "x")
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []string{"x"}, result)
|
||||
})
|
||||
|
||||
t.Run("empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("already unique", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("boolean values", func(t *testing.T) {
|
||||
seq := From(true, false, true, false, true)
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []bool{true, false}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqWithChainedOperations tests Uniq combined with other operations
|
||||
func TestUniqWithChainedOperations(t *testing.T) {
|
||||
t.Run("uniq then map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1)
|
||||
unique := Uniq(F.Identity[int])
|
||||
mapped := MonadMap(unique(seq), func(x int) int { return x * 2 })
|
||||
result := toSlice(mapped)
|
||||
assert.Equal(t, []int{2, 4, 6, 8}, result)
|
||||
})
|
||||
|
||||
t.Run("map then uniq", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, func(x int) int { return x % 3 })
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(mapped))
|
||||
assert.Equal(t, []int{1, 2, 0}, result)
|
||||
})
|
||||
|
||||
t.Run("filter then uniq", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 2, 4, 6)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(filtered))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("uniq then filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5, 6)
|
||||
unique := Uniq(F.Identity[int])
|
||||
filtered := MonadFilter(unique(seq), func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("uniq then take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
taken := Take[int](3)(unique(seq))
|
||||
result := toSlice(taken)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("take then uniq", func(t *testing.T) {
|
||||
seq := From(1, 2, 1, 3, 2, 4, 5)
|
||||
taken := Take[int](5)(seq)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(taken))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqEarlyTermination tests that Uniq respects early termination
|
||||
func TestUniqEarlyTermination(t *testing.T) {
|
||||
t.Run("terminates when yield returns false", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 6, 7)
|
||||
unique := Uniq(F.Identity[int])
|
||||
|
||||
count := 0
|
||||
for v := range unique(seq) {
|
||||
count++
|
||||
if v >= 4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 4, count) // Should stop at 4
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqLargeSequence tests Uniq with larger sequences
|
||||
func TestUniqLargeSequence(t *testing.T) {
|
||||
t.Run("uniq large sequence with many duplicates", func(t *testing.T) {
|
||||
// Create sequence with repeating pattern
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i % 10 // Only 10 unique values
|
||||
}
|
||||
seq := From(data...)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
|
||||
assert.Len(t, result, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
assert.Equal(t, i, result[i])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uniq large sequence all unique", func(t *testing.T) {
|
||||
data := make([]int, 100)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
|
||||
assert.Len(t, result, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
assert.Equal(t, i, result[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqPreservesOrder tests that Uniq maintains element order
|
||||
func TestUniqPreservesOrder(t *testing.T) {
|
||||
t.Run("maintains order of first occurrences", func(t *testing.T) {
|
||||
seq := From(5, 3, 5, 1, 3, 2, 1, 4)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{5, 3, 1, 2, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkUniq(b *testing.B) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5, 3, 6, 4, 7, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range unique(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStrictUniq(b *testing.B) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5, 3, 6, 4, 7, 5)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range StrictUniq(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUniqLarge(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i % 100
|
||||
}
|
||||
seq := From(data...)
|
||||
unique := Uniq(F.Identity[int])
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range unique(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleUniq() {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := unique(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 4 5
|
||||
}
|
||||
|
||||
func ExampleUniq_byLength() {
|
||||
seq := From("a", "bb", "c", "dd", "eee")
|
||||
uniqueByLength := Uniq(func(s string) int { return len(s) })
|
||||
result := uniqueByLength(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: a bb eee
|
||||
}
|
||||
|
||||
func ExampleUniq_caseInsensitive() {
|
||||
seq := From("Hello", "world", "HELLO", "World", "test")
|
||||
uniqueCaseInsensitive := Uniq(func(s string) string {
|
||||
return strings.ToLower(s)
|
||||
})
|
||||
result := uniqueCaseInsensitive(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: Hello world test
|
||||
}
|
||||
|
||||
func ExampleStrictUniq() {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
result := StrictUniq(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 4 5
|
||||
}
|
||||
|
||||
func ExampleStrictUniq_strings() {
|
||||
seq := From("apple", "banana", "apple", "cherry", "banana")
|
||||
result := StrictUniq(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: apple banana cherry
|
||||
}
|
||||
|
||||
func ExampleUniq_empty() {
|
||||
seq := Empty[int]()
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := unique(seq)
|
||||
|
||||
count := 0
|
||||
for range result {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
@@ -98,10 +98,6 @@ func GetOrElse[E, L, A any](onLeft func(L) Reader[E, A]) func(ReaderEither[E, L,
|
||||
return eithert.GetOrElse(reader.MonadChain[E, Either[L, A], A], reader.Of[E, A], onLeft)
|
||||
}
|
||||
|
||||
func OrElse[E, L1, A, L2 any](onLeft func(L1) ReaderEither[E, L2, A]) func(ReaderEither[E, L1, A]) ReaderEither[E, L2, A] {
|
||||
return eithert.OrElse(reader.MonadChain[E, Either[L1, A], Either[L2, A]], reader.Of[E, Either[L2, A]], onLeft)
|
||||
}
|
||||
|
||||
func OrLeft[A, L1, E, L2 any](onLeft func(L1) Reader[E, L2]) func(ReaderEither[E, L1, A]) ReaderEither[E, L2, A] {
|
||||
return eithert.OrLeft(
|
||||
reader.MonadChain[E, Either[L1, A], Either[L2, A]],
|
||||
@@ -180,3 +176,220 @@ func MonadMapLeft[C, E1, E2, A any](fa ReaderEither[C, E1, A], f func(E1) E2) Re
|
||||
func MapLeft[C, E1, E2, A any](f func(E1) E2) func(ReaderEither[C, E1, A]) ReaderEither[C, E2, A] {
|
||||
return eithert.MapLeft(reader.Map[C, Either[E1, A], Either[E2, A]], f)
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative computation with access to the reader context.
|
||||
// If the ReaderEither is Right, it returns the value unchanged.
|
||||
// If the ReaderEither is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderEither that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative computations
|
||||
// that need access to configuration or dependencies. The error type can be widened from E1 to E2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ fallbackValue int }
|
||||
//
|
||||
// // Recover using config-dependent fallback
|
||||
// recover := readereither.OrElse(func(err error) readereither.ReaderEither[Config, error, int] {
|
||||
// if err.Error() == "not found" {
|
||||
// return readereither.Asks[error](func(cfg Config) either.Either[error, int] {
|
||||
// return either.Right[error](cfg.fallbackValue)
|
||||
// })
|
||||
// }
|
||||
// return readereither.Left[Config, int](err)
|
||||
// })
|
||||
// result := recover(readereither.Left[Config, int](errors.New("not found")))(Config{fallbackValue: 42}) // Right(42)
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[R, E1, E2, A any](onLeft Kleisli[R, E2, E1, A]) Kleisli[R, E2, ReaderEither[R, E1, A], A] {
|
||||
return Fold(onLeft, Of[R, E2, A])
|
||||
}
|
||||
|
||||
// MonadChainLeft chains a computation on the left (error) side of a ReaderEither.
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
|
||||
//
|
||||
// This is useful for error recovery or error transformation scenarios where you want to handle
|
||||
// errors by performing another computation that may also fail, with access to configuration context.
|
||||
//
|
||||
// Note: This is functionally identical to the uncurried form of [OrElseW]. Use [ChainLeft] when
|
||||
// emphasizing the monadic chaining perspective, and [OrElseW] for error recovery semantics.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The input ReaderEither that may contain an error of type EA
|
||||
// - f: A Kleisli function that takes an error of type EA and returns a ReaderEither with error type EB
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderEither with the potentially transformed error type EB
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ fallbackValue int }
|
||||
// type ValidationError struct{ field string }
|
||||
// type SystemError struct{ code int }
|
||||
//
|
||||
// // Recover from validation errors using config
|
||||
// result := MonadChainLeft(
|
||||
// Left[Config, int](ValidationError{"username"}),
|
||||
// func(ve ValidationError) readereither.ReaderEither[Config, SystemError, int] {
|
||||
// if ve.field == "username" {
|
||||
// return Asks[SystemError](func(cfg Config) either.Either[SystemError, int] {
|
||||
// return either.Right[SystemError](cfg.fallbackValue)
|
||||
// })
|
||||
// }
|
||||
// return Left[Config, int](SystemError{400})
|
||||
// },
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[R, EA, EB, A any](fa ReaderEither[R, EA, A], f Kleisli[R, EB, EA, A]) ReaderEither[R, EB, A] {
|
||||
return func(r R) Either[EB, A] {
|
||||
return ET.Fold(
|
||||
func(ea EA) Either[EB, A] { return f(ea)(r) },
|
||||
ET.Right[EB, A],
|
||||
)(fa(r))
|
||||
}
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// It returns a function that chains a computation on the left (error) side of a ReaderEither.
|
||||
//
|
||||
// This is particularly useful in functional composition pipelines where you want to handle
|
||||
// errors by performing another computation that may also fail, with access to configuration context.
|
||||
//
|
||||
// Note: This is functionally identical to [OrElseW]. They are different names for the same operation.
|
||||
// Use [ChainLeft] when emphasizing the monadic chaining perspective on the error channel,
|
||||
// and [OrElseW] when emphasizing error recovery/fallback semantics.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that takes an error of type EA and returns a ReaderEither with error type EB
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a ReaderEither with error type EA to one with error type EB
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ retryLimit int }
|
||||
//
|
||||
// // Create a reusable error handler with config access
|
||||
// recoverFromError := ChainLeft(func(err string) readereither.ReaderEither[Config, int, string] {
|
||||
// if strings.Contains(err, "retryable") {
|
||||
// return Asks[int](func(cfg Config) either.Either[int, string] {
|
||||
// if cfg.retryLimit > 0 {
|
||||
// return either.Right[int]("recovered")
|
||||
// }
|
||||
// return either.Left[string](500)
|
||||
// })
|
||||
// }
|
||||
// return Left[Config, string](404)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Left[Config, string]("retryable error"),
|
||||
// recoverFromError,
|
||||
// )(Config{retryLimit: 3})
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[R, EA, EB, A any](f Kleisli[R, EB, EA, A]) func(ReaderEither[R, EA, A]) ReaderEither[R, EB, A] {
|
||||
return func(fa ReaderEither[R, EA, A]) ReaderEither[R, EB, A] {
|
||||
return MonadChainLeft(fa, f)
|
||||
}
|
||||
}
|
||||
|
||||
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
|
||||
// If the input is a Left value, it applies the function f to the error and executes the resulting computation,
|
||||
// but always returns the original Left error regardless of what f returns (Left or Right).
|
||||
// If the input is a Right value, it passes through unchanged without calling f.
|
||||
//
|
||||
// This is useful for side effects on errors (like logging or metrics) where you want to perform an action
|
||||
// when an error occurs but always propagate the original error, ensuring the error path is preserved.
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The input ReaderEither that may contain an error of type EA
|
||||
// - f: A function that takes an error of type EA and returns a ReaderEither (typically for side effects)
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderEither with the original error preserved if input was Left, or the original Right value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ loggingEnabled bool }
|
||||
//
|
||||
// // Log errors but preserve the original error
|
||||
// result := MonadChainFirstLeft(
|
||||
// Left[Config, int]("database error"),
|
||||
// func(err string) readereither.ReaderEither[Config, string, int] {
|
||||
// return Asks[string](func(cfg Config) either.Either[string, int] {
|
||||
// if cfg.loggingEnabled {
|
||||
// log.Printf("Error: %s", err)
|
||||
// }
|
||||
// return either.Right[string](0)
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
// // result will always be Left("database error")
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainFirstLeft[A, R, EA, EB, B any](ma ReaderEither[R, EA, A], f Kleisli[R, EB, EA, B]) ReaderEither[R, EA, A] {
|
||||
return MonadChainLeft(ma, function.Flow2(f, Fold(function.Constant1[EB](ma), function.Constant1[B](ma))))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadTapLeft[A, R, EA, EB, B any](ma ReaderEither[R, EA, A], f Kleisli[R, EB, EA, B]) ReaderEither[R, EA, A] {
|
||||
return MonadChainFirstLeft(ma, f)
|
||||
}
|
||||
|
||||
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
|
||||
// It returns a function that chains a computation on the left (error) side while always preserving the original error.
|
||||
//
|
||||
// This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
|
||||
// in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
|
||||
// ensuring the error path is preserved.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes an error of type EA and returns a ReaderEither (typically for side effects)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that performs the side effect but always returns the original error if input was Left
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ metricsEnabled bool }
|
||||
//
|
||||
// // Create a reusable error logger
|
||||
// logError := ChainFirstLeft(func(err string) readereither.ReaderEither[Config, any, int] {
|
||||
// return Asks[any](func(cfg Config) either.Either[any, int] {
|
||||
// if cfg.metricsEnabled {
|
||||
// metrics.RecordError(err)
|
||||
// }
|
||||
// return either.Right[any](0)
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// Left[Config, int]("validation failed"),
|
||||
// logError, // records the error in metrics
|
||||
// )
|
||||
// // result is always Left("validation failed")
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) func(ReaderEither[R, EA, A]) ReaderEither[R, EA, A] {
|
||||
return ChainLeft(func(e EA) ReaderEither[R, EA, A] {
|
||||
ma := Left[R, A](e)
|
||||
return MonadFold(f(e), function.Constant1[EB](ma), function.Constant1[B](ma))
|
||||
})
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapLeft[A, R, EA, EB, B any](f Kleisli[R, EB, EA, B]) func(ReaderEither[R, EA, A]) ReaderEither[R, EA, A] {
|
||||
return ChainFirstLeft[A](f)
|
||||
}
|
||||
|
||||
// MonadFold applies one of two functions depending on the Either value.
|
||||
// If Left, applies onLeft function. If Right, applies onRight function.
|
||||
// Both functions return a Reader[E, B].
|
||||
//
|
||||
//go:inline
|
||||
func MonadFold[E, L, A, B any](ma ReaderEither[E, L, A], onLeft func(L) Reader[E, B], onRight func(A) Reader[E, B]) Reader[E, B] {
|
||||
return Fold[E, L, A, B](onLeft, onRight)(ma)
|
||||
}
|
||||
|
||||
@@ -57,3 +57,169 @@ func TestFlatten(t *testing.T) {
|
||||
|
||||
assert.Equal(t, ET.Of[string]("a"), g(defaultContext))
|
||||
}
|
||||
|
||||
func TestChainLeftFunc(t *testing.T) {
|
||||
type Config struct {
|
||||
errorCode int
|
||||
}
|
||||
|
||||
// Test with Right - should pass through unchanged
|
||||
t.Run("Right passes through", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Right[Config, string](42),
|
||||
ChainLeft(func(err string) ReaderEither[Config, int, int] {
|
||||
return Left[Config, int](999)
|
||||
}),
|
||||
)
|
||||
result := g(Config{errorCode: 500})
|
||||
assert.Equal(t, ET.Right[int](42), result)
|
||||
})
|
||||
|
||||
// Test with Left - error transformation with config
|
||||
t.Run("Left transforms error with config", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("error"),
|
||||
ChainLeft(func(err string) ReaderEither[Config, int, int] {
|
||||
return func(cfg Config) Either[int, int] {
|
||||
return ET.Left[int](cfg.errorCode)
|
||||
}
|
||||
}),
|
||||
)
|
||||
result := g(Config{errorCode: 500})
|
||||
assert.Equal(t, ET.Left[int](500), result)
|
||||
})
|
||||
|
||||
// Test with Left - successful recovery
|
||||
t.Run("Left recovers successfully", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("recoverable"),
|
||||
ChainLeft(func(err string) ReaderEither[Config, int, int] {
|
||||
if err == "recoverable" {
|
||||
return Right[Config, int](999)
|
||||
}
|
||||
return Left[Config, int](0)
|
||||
}),
|
||||
)
|
||||
result := g(Config{errorCode: 500})
|
||||
assert.Equal(t, ET.Right[int](999), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstLeftFunc(t *testing.T) {
|
||||
type Config struct {
|
||||
logEnabled bool
|
||||
}
|
||||
|
||||
logged := false
|
||||
|
||||
// Test with Right - should not call function
|
||||
t.Run("Right does not call function", func(t *testing.T) {
|
||||
logged = false
|
||||
g := F.Pipe1(
|
||||
Right[Config, string](42),
|
||||
ChainFirstLeft[int](func(err string) ReaderEither[Config, int, string] {
|
||||
logged = true
|
||||
return Right[Config, int]("logged")
|
||||
}),
|
||||
)
|
||||
result := g(Config{logEnabled: true})
|
||||
assert.Equal(t, ET.Right[string](42), result)
|
||||
assert.False(t, logged)
|
||||
})
|
||||
|
||||
// Test with Left - calls function but preserves original error
|
||||
t.Run("Left calls function but preserves error", func(t *testing.T) {
|
||||
logged = false
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("original error"),
|
||||
ChainFirstLeft[int](func(err string) ReaderEither[Config, int, string] {
|
||||
return func(cfg Config) Either[int, string] {
|
||||
if cfg.logEnabled {
|
||||
logged = true
|
||||
}
|
||||
return ET.Right[int]("side effect done")
|
||||
}
|
||||
}),
|
||||
)
|
||||
result := g(Config{logEnabled: true})
|
||||
assert.Equal(t, ET.Left[int]("original error"), result)
|
||||
assert.True(t, logged)
|
||||
})
|
||||
|
||||
// Test with Left - preserves original error even if side effect fails
|
||||
t.Run("Left preserves error even if side effect fails", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("original error"),
|
||||
ChainFirstLeft[int](func(err string) ReaderEither[Config, int, string] {
|
||||
return Left[Config, string](999) // Side effect fails
|
||||
}),
|
||||
)
|
||||
result := g(Config{logEnabled: true})
|
||||
assert.Equal(t, ET.Left[int]("original error"), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapLeftFunc(t *testing.T) {
|
||||
// TapLeft is an alias for ChainFirstLeft, so just a basic sanity test
|
||||
type Config struct{}
|
||||
|
||||
sideEffectRan := false
|
||||
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("error"),
|
||||
TapLeft[int](func(err string) ReaderEither[Config, string, int] {
|
||||
sideEffectRan = true
|
||||
return Right[Config, string](0)
|
||||
}),
|
||||
)
|
||||
|
||||
result := g(Config{})
|
||||
assert.Equal(t, ET.Left[int]("error"), result)
|
||||
assert.True(t, sideEffectRan)
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
type Config struct {
|
||||
fallbackValue int
|
||||
}
|
||||
|
||||
// Test OrElse with Right - should pass through unchanged
|
||||
rightValue := Of[Config, string, int](42)
|
||||
recover := OrElse[Config, string, string, int](func(err string) ReaderEither[Config, string, int] {
|
||||
return Left[Config, int]("should not be called")
|
||||
})
|
||||
result := recover(rightValue)(Config{fallbackValue: 0})
|
||||
assert.Equal(t, ET.Right[string](42), result)
|
||||
|
||||
// Test OrElse with Left - should recover with fallback
|
||||
leftValue := Left[Config, int]("not found")
|
||||
recoverWithFallback := OrElse[Config, string, string, int](func(err string) ReaderEither[Config, string, int] {
|
||||
if err == "not found" {
|
||||
return func(cfg Config) ET.Either[string, int] {
|
||||
return ET.Right[string](cfg.fallbackValue)
|
||||
}
|
||||
}
|
||||
return Left[Config, int](err)
|
||||
})
|
||||
result = recoverWithFallback(leftValue)(Config{fallbackValue: 99})
|
||||
assert.Equal(t, ET.Right[string](99), result)
|
||||
|
||||
// Test OrElse with Left - should propagate other errors
|
||||
leftValue = Left[Config, int]("fatal error")
|
||||
result = recoverWithFallback(leftValue)(Config{fallbackValue: 99})
|
||||
assert.Equal(t, ET.Left[int]("fatal error"), result)
|
||||
|
||||
// Test error type widening
|
||||
type ValidationError struct{ field string }
|
||||
type AppError struct{ code int }
|
||||
|
||||
validationErr := Left[Config, int](ValidationError{field: "username"})
|
||||
wideningRecover := OrElse[Config, ValidationError, AppError, int](func(ve ValidationError) ReaderEither[Config, AppError, int] {
|
||||
if ve.field == "username" {
|
||||
return Right[Config, AppError](100)
|
||||
}
|
||||
return Left[Config, int](AppError{code: 400})
|
||||
})
|
||||
appResult := wideningRecover(validationErr)(Config{})
|
||||
assert.Equal(t, ET.Right[AppError](100), appResult)
|
||||
}
|
||||
|
||||
@@ -684,14 +684,6 @@ func GetOrElse[R, E, A any](onLeft func(E) ReaderIO[R, A]) func(ReaderIOEither[R
|
||||
return eithert.GetOrElse(readerio.MonadChain[R, either.Either[E, A], A], readerio.Of[R, A], onLeft)
|
||||
}
|
||||
|
||||
// OrElse tries an alternative computation if the first one fails.
|
||||
// The alternative can produce a different error type.
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[R, E1, A, E2 any](onLeft func(E1) ReaderIOEither[R, E2, A]) func(ReaderIOEither[R, E1, A]) ReaderIOEither[R, E2, A] {
|
||||
return eithert.OrElse(readerio.MonadChain[R, either.Either[E1, A], either.Either[E2, A]], readerio.Of[R, either.Either[E2, A]], onLeft)
|
||||
}
|
||||
|
||||
// OrLeft transforms the error using a ReaderIO if the computation fails.
|
||||
// The success value is preserved unchanged.
|
||||
//
|
||||
@@ -829,6 +821,42 @@ func Read[E, A, R any](r R) func(ReaderIOEither[R, E, A]) IOEither[E, A] {
|
||||
return reader.Read[IOEither[E, A]](r)
|
||||
}
|
||||
|
||||
// MonadChainLeft chains a computation on the left (error) side of a ReaderIOEither.
|
||||
// If the input is a Left value, it applies the function f to transform the error and potentially
|
||||
// change the error type from EA to EB. If the input is a Right value, it passes through unchanged.
|
||||
//
|
||||
// This is useful for error recovery or error transformation scenarios where you want to handle
|
||||
// errors by performing another computation that may also fail, with access to configuration context.
|
||||
//
|
||||
// Note: This is functionally identical to the uncurried form of [OrElse]. Use [ChainLeft] when
|
||||
// emphasizing the monadic chaining perspective, and [OrElse] for error recovery semantics.
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The input ReaderIOEither that may contain an error of type EA
|
||||
// - f: A Kleisli function that takes an error of type EA and returns a ReaderIOEither with error type EB
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderIOEither with the potentially transformed error type EB
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ retryCount int }
|
||||
// type NetworkError struct{ msg string }
|
||||
// type SystemError struct{ code int }
|
||||
//
|
||||
// // Recover from network errors by retrying with config
|
||||
// result := MonadChainLeft(
|
||||
// Left[Config, string](NetworkError{"connection failed"}),
|
||||
// func(ne NetworkError) readerioeither.ReaderIOEither[Config, SystemError, string] {
|
||||
// return readerioeither.Asks[SystemError](func(cfg Config) ioeither.IOEither[SystemError, string] {
|
||||
// if cfg.retryCount > 0 {
|
||||
// return ioeither.Right[SystemError]("recovered")
|
||||
// }
|
||||
// return ioeither.Left[string](SystemError{500})
|
||||
// })
|
||||
// },
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainLeft[R, EA, EB, A any](fa ReaderIOEither[R, EA, A], f Kleisli[R, EB, EA, A]) ReaderIOEither[R, EB, A] {
|
||||
return readert.MonadChain(
|
||||
@@ -838,6 +866,44 @@ func MonadChainLeft[R, EA, EB, A any](fa ReaderIOEither[R, EA, A], f Kleisli[R,
|
||||
)
|
||||
}
|
||||
|
||||
// ChainLeft is the curried version of [MonadChainLeft].
|
||||
// It returns a function that chains a computation on the left (error) side of a ReaderIOEither.
|
||||
//
|
||||
// This is particularly useful in functional composition pipelines where you want to handle
|
||||
// errors by performing another computation that may also fail, with access to configuration context.
|
||||
//
|
||||
// Note: This is functionally identical to [OrElse]. They are different names for the same operation.
|
||||
// Use [ChainLeft] when emphasizing the monadic chaining perspective on the error channel,
|
||||
// and [OrElse] when emphasizing error recovery/fallback semantics.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that takes an error of type EA and returns a ReaderIOEither with error type EB
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a ReaderIOEither with error type EA to one with error type EB
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ fallbackService string }
|
||||
//
|
||||
// // Create a reusable error handler with config access
|
||||
// recoverFromNetworkError := ChainLeft(func(err string) readerioeither.ReaderIOEither[Config, string, int] {
|
||||
// if strings.Contains(err, "network") {
|
||||
// return readerioeither.Asks[string](func(cfg Config) ioeither.IOEither[string, int] {
|
||||
// return ioeither.TryCatch(
|
||||
// func() (int, error) { return callService(cfg.fallbackService) },
|
||||
// func(e error) string { return e.Error() },
|
||||
// )
|
||||
// })
|
||||
// }
|
||||
// return readerioeither.Left[Config, int](err)
|
||||
// })
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// readerioeither.Left[Config, int]("network timeout"),
|
||||
// recoverFromNetworkError,
|
||||
// )(Config{fallbackService: "backup"})()
|
||||
//
|
||||
//go:inline
|
||||
func ChainLeft[R, EA, EB, A any](f Kleisli[R, EB, EA, A]) func(ReaderIOEither[R, EA, A]) ReaderIOEither[R, EB, A] {
|
||||
return readert.Chain[ReaderIOEither[R, EA, A]](
|
||||
@@ -910,3 +976,33 @@ func Delay[R, E, A any](delay time.Duration) Operator[R, E, A, A] {
|
||||
func After[R, E, A any](timestamp time.Time) Operator[R, E, A, A] {
|
||||
return function.Bind2nd(function.Flow2[ReaderIOEither[R, E, A]], io.After[Either[E, A]](timestamp))
|
||||
}
|
||||
|
||||
// OrElse recovers from a Left (error) by providing an alternative IO computation with access to the reader context.
|
||||
// If the ReaderIOEither is Right, it returns the value unchanged.
|
||||
// If the ReaderIOEither is Left, it applies the provided function to the error value,
|
||||
// which returns a new ReaderIOEither that replaces the original.
|
||||
//
|
||||
// This is useful for error recovery, fallback logic, or chaining alternative IO computations
|
||||
// that need access to configuration or dependencies. The error type can be widened from E1 to E2.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct{ retryLimit int }
|
||||
//
|
||||
// // Recover with IO operation using config
|
||||
// recover := readerioeither.OrElse(func(err error) readerioeither.ReaderIOEither[Config, error, int] {
|
||||
// if err.Error() == "retryable" {
|
||||
// return readerioeither.Asks[error](func(cfg Config) ioeither.IOEither[error, int] {
|
||||
// if cfg.retryLimit > 0 {
|
||||
// return ioeither.Right[error](42)
|
||||
// }
|
||||
// return ioeither.Left[int](err)
|
||||
// })
|
||||
// }
|
||||
// return readerioeither.Left[Config, int](err)
|
||||
// })
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[R, E1, E2, A any](onLeft Kleisli[R, E2, E1, A]) Kleisli[R, E2, ReaderIOEither[R, E1, A], A] {
|
||||
return Fold(onLeft, Of[R, E2, A])
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
R "github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -77,3 +78,154 @@ func TestChainReaderK(t *testing.T) {
|
||||
|
||||
assert.Equal(t, E.Right[error]("1"), g(context.Background())())
|
||||
}
|
||||
|
||||
func TestOrElseWFunc(t *testing.T) {
|
||||
type Config struct {
|
||||
retryEnabled bool
|
||||
}
|
||||
|
||||
// Test with Right - should pass through unchanged
|
||||
t.Run("Right passes through", func(t *testing.T) {
|
||||
rioe := Right[Config, string](42)
|
||||
handler := OrElse(func(err string) ReaderIOEither[Config, int, int] {
|
||||
return Left[Config, int](999)
|
||||
})
|
||||
result := handler(rioe)(Config{retryEnabled: true})()
|
||||
assert.Equal(t, E.Right[int](42), result)
|
||||
})
|
||||
|
||||
// Test with Left - error type widening
|
||||
t.Run("Left with error type widening", func(t *testing.T) {
|
||||
rioe := Left[Config, int]("network error")
|
||||
handler := OrElse(func(err string) ReaderIOEither[Config, int, int] {
|
||||
return func(cfg Config) IOEither[int, int] {
|
||||
if cfg.retryEnabled {
|
||||
return IOE.Right[int](100)
|
||||
}
|
||||
return IOE.Left[int](404)
|
||||
}
|
||||
})
|
||||
result := handler(rioe)(Config{retryEnabled: true})()
|
||||
assert.Equal(t, E.Right[int](100), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainLeftFunc(t *testing.T) {
|
||||
type Config struct {
|
||||
errorCode int
|
||||
}
|
||||
|
||||
// Test with Right - should pass through unchanged
|
||||
t.Run("Right passes through", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Right[Config, string](42),
|
||||
ChainLeft(func(err string) ReaderIOEither[Config, int, int] {
|
||||
return Left[Config, int](999)
|
||||
}),
|
||||
)
|
||||
result := g(Config{errorCode: 500})()
|
||||
assert.Equal(t, E.Right[int](42), result)
|
||||
})
|
||||
|
||||
// Test with Left - error transformation with config
|
||||
t.Run("Left transforms error with config", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("error"),
|
||||
ChainLeft(func(err string) ReaderIOEither[Config, int, int] {
|
||||
return func(cfg Config) IOEither[int, int] {
|
||||
return IOE.Left[int](cfg.errorCode)
|
||||
}
|
||||
}),
|
||||
)
|
||||
result := g(Config{errorCode: 500})()
|
||||
assert.Equal(t, E.Left[int](500), result)
|
||||
})
|
||||
|
||||
// Test with Left - successful recovery
|
||||
t.Run("Left recovers successfully", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("recoverable"),
|
||||
ChainLeft(func(err string) ReaderIOEither[Config, int, int] {
|
||||
if err == "recoverable" {
|
||||
return Right[Config, int](999)
|
||||
}
|
||||
return Left[Config, int](0)
|
||||
}),
|
||||
)
|
||||
result := g(Config{errorCode: 500})()
|
||||
assert.Equal(t, E.Right[int](999), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstLeftFunc(t *testing.T) {
|
||||
type Config struct {
|
||||
logEnabled bool
|
||||
}
|
||||
|
||||
logged := false
|
||||
|
||||
// Test with Right - should not call function
|
||||
t.Run("Right does not call function", func(t *testing.T) {
|
||||
logged = false
|
||||
g := F.Pipe1(
|
||||
Right[Config, string](42),
|
||||
ChainFirstLeft[int](func(err string) ReaderIOEither[Config, int, string] {
|
||||
logged = true
|
||||
return Right[Config, int]("logged")
|
||||
}),
|
||||
)
|
||||
result := g(Config{logEnabled: true})()
|
||||
assert.Equal(t, E.Right[string](42), result)
|
||||
assert.False(t, logged)
|
||||
})
|
||||
|
||||
// Test with Left - calls function but preserves original error
|
||||
t.Run("Left calls function but preserves error", func(t *testing.T) {
|
||||
logged = false
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("original error"),
|
||||
ChainFirstLeft[int](func(err string) ReaderIOEither[Config, int, string] {
|
||||
return func(cfg Config) IOEither[int, string] {
|
||||
if cfg.logEnabled {
|
||||
logged = true
|
||||
}
|
||||
return IOE.Right[int]("side effect done")
|
||||
}
|
||||
}),
|
||||
)
|
||||
result := g(Config{logEnabled: true})()
|
||||
assert.Equal(t, E.Left[int]("original error"), result)
|
||||
assert.True(t, logged)
|
||||
})
|
||||
|
||||
// Test with Left - preserves original error even if side effect fails
|
||||
t.Run("Left preserves error even if side effect fails", func(t *testing.T) {
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("original error"),
|
||||
ChainFirstLeft[int](func(err string) ReaderIOEither[Config, int, string] {
|
||||
return Left[Config, string](999) // Side effect fails
|
||||
}),
|
||||
)
|
||||
result := g(Config{logEnabled: true})()
|
||||
assert.Equal(t, E.Left[int]("original error"), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapLeft(t *testing.T) {
|
||||
// TapLeft is an alias for ChainFirstLeft, so just a basic sanity test
|
||||
type Config struct{}
|
||||
|
||||
sideEffectRan := false
|
||||
|
||||
g := F.Pipe1(
|
||||
Left[Config, int]("error"),
|
||||
TapLeft[int](func(err string) ReaderIOEither[Config, string, int] {
|
||||
sideEffectRan = true
|
||||
return Right[Config, string](0)
|
||||
}),
|
||||
)
|
||||
|
||||
result := g(Config{})()
|
||||
assert.Equal(t, E.Left[int]("error"), result)
|
||||
assert.True(t, sideEffectRan)
|
||||
}
|
||||
|
||||
@@ -397,22 +397,6 @@ func TestGetOrElse(t *testing.T) {
|
||||
assert.Equal(t, 0, resultLeft(ctx)())
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
ctx := testContext{value: 10}
|
||||
|
||||
// Test Right case
|
||||
resultRight := OrElse(func(e error) ReaderIOEither[testContext, string, int] {
|
||||
return Left[testContext, int]("alternative")
|
||||
})(Of[testContext, error](42))
|
||||
assert.Equal(t, E.Right[string](42), resultRight(ctx)())
|
||||
|
||||
// Test Left case
|
||||
resultLeft := OrElse(func(e error) ReaderIOEither[testContext, string, int] {
|
||||
return Of[testContext, string](99)
|
||||
})(Left[testContext, int](errors.New("test")))
|
||||
assert.Equal(t, E.Right[string](99), resultLeft(ctx)())
|
||||
}
|
||||
|
||||
func TestMonadBiMap(t *testing.T) {
|
||||
ctx := testContext{value: 10}
|
||||
|
||||
|
||||
@@ -648,10 +648,9 @@ func GetOrElse[R, A any](onLeft func(error) ReaderIO[R, A]) func(ReaderIOResult[
|
||||
}
|
||||
|
||||
// OrElse tries an alternative computation if the first one fails.
|
||||
// The alternative can produce a different error type.
|
||||
//
|
||||
//go:inline
|
||||
func OrElse[R, A, E any](onLeft func(error) RIOE.ReaderIOEither[R, E, A]) func(ReaderIOResult[R, A]) RIOE.ReaderIOEither[R, E, A] {
|
||||
func OrElse[R, A any](onLeft Kleisli[R, error, A]) Operator[R, A, A] {
|
||||
return RIOE.OrElse(onLeft)
|
||||
}
|
||||
|
||||
|
||||
@@ -768,3 +768,41 @@ func TestWithLock(t *testing.T) {
|
||||
assert.Equal(t, result.Of(42), res(ctx)())
|
||||
assert.True(t, unlocked)
|
||||
}
|
||||
|
||||
func TestOrElse(t *testing.T) {
|
||||
type Config struct {
|
||||
fallbackValue int
|
||||
}
|
||||
ctx := Config{fallbackValue: 99}
|
||||
|
||||
// Test OrElse with Right - should pass through unchanged
|
||||
rightValue := Of[Config](42)
|
||||
recover := OrElse[Config, int](func(err error) ReaderIOResult[Config, int] {
|
||||
return Left[Config, int](errors.New("should not be called"))
|
||||
})
|
||||
res := recover(rightValue)(ctx)()
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
|
||||
// Test OrElse with Left - should recover with fallback
|
||||
leftValue := Left[Config, int](errors.New("not found"))
|
||||
recoverWithFallback := OrElse[Config, int](func(err error) ReaderIOResult[Config, int] {
|
||||
if err.Error() == "not found" {
|
||||
return func(cfg Config) IOResult[int] {
|
||||
return func() result.Result[int] {
|
||||
return result.Of(cfg.fallbackValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Left[Config, int](err)
|
||||
})
|
||||
res = recoverWithFallback(leftValue)(ctx)()
|
||||
assert.Equal(t, result.Of(99), res)
|
||||
|
||||
// Test OrElse with Left - should propagate other errors
|
||||
leftValue = Left[Config, int](errors.New("fatal error"))
|
||||
res = recoverWithFallback(leftValue)(ctx)()
|
||||
assert.True(t, result.IsLeft(res))
|
||||
val, err := result.UnwrapError(res)
|
||||
assert.Equal(t, 0, val)
|
||||
assert.Equal(t, "fatal error", err.Error())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user