mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-23 23:51:14 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49227551b6 | ||
|
|
69691e9e70 |
@@ -98,7 +98,7 @@ func Example_resultAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Assert success
|
||||
successResult := result.Of[int](42)
|
||||
successResult := result.Of(42)
|
||||
assert.Success(successResult)(t)
|
||||
|
||||
// Assert failure
|
||||
|
||||
120
v2/idiomatic/context/readerresult/rec.go
Normal file
120
v2/idiomatic/context/readerresult/rec.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// 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 readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
)
|
||||
|
||||
// TailRec implements tail-recursive computation for ReaderResult with context cancellation support.
|
||||
//
|
||||
// TailRec takes a Kleisli function that returns Trampoline[A, B] and converts it into a stack-safe,
|
||||
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Land value.
|
||||
//
|
||||
// The implementation includes a short-circuit mechanism that checks for context cancellation on each
|
||||
// iteration. If the context is canceled (ctx.Err() != nil), the computation immediately returns an
|
||||
// error result containing the context's cause error, preventing unnecessary computation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input type for the recursive step
|
||||
// - B: The final result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Trampoline[A, B].
|
||||
// When the result is Bounce(a), recursion continues with the new value 'a'.
|
||||
// When the result is Land(b), recursion terminates with the final value 'b'.
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
|
||||
//
|
||||
// Behavior:
|
||||
// - On each iteration, checks if the context has been canceled (short circuit)
|
||||
// - If canceled, returns (zero value, context.Cause(ctx))
|
||||
// - If the step returns an error, propagates the error as (zero value, error)
|
||||
// - If the step returns Bounce(a), continues recursion with new value 'a'
|
||||
// - If the step returns Land(b), terminates with success value (b, nil)
|
||||
//
|
||||
// Example - Factorial computation with context:
|
||||
//
|
||||
// type State struct {
|
||||
// n int
|
||||
// acc int
|
||||
// }
|
||||
//
|
||||
// factorialStep := func(state State) ReaderResult[tailrec.Trampoline[State, int]] {
|
||||
// return func(ctx context.Context) (tailrec.Trampoline[State, int], error) {
|
||||
// if state.n <= 0 {
|
||||
// return tailrec.Land[State](state.acc), nil
|
||||
// }
|
||||
// return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}), nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// factorial := TailRec(factorialStep)
|
||||
// result, err := factorial(State{5, 1})(ctx) // Returns (120, nil)
|
||||
//
|
||||
// Example - Context cancellation:
|
||||
//
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// cancel() // Cancel immediately
|
||||
//
|
||||
// computation := TailRec(someStep)
|
||||
// result, err := computation(initialValue)(ctx)
|
||||
// // Returns (zero value, context.Cause(ctx)) without executing any steps
|
||||
//
|
||||
// Example - Error handling:
|
||||
//
|
||||
// errorStep := func(n int) ReaderResult[tailrec.Trampoline[int, int]] {
|
||||
// return func(ctx context.Context) (tailrec.Trampoline[int, int], error) {
|
||||
// if n == 5 {
|
||||
// return tailrec.Trampoline[int, int]{}, errors.New("computation error")
|
||||
// }
|
||||
// if n <= 0 {
|
||||
// return tailrec.Land[int](n), nil
|
||||
// }
|
||||
// return tailrec.Bounce[int](n - 1), nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// computation := TailRec(errorStep)
|
||||
// result, err := computation(10)(ctx) // Returns (0, errors.New("computation error"))
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return func(a A) ReaderResult[B] {
|
||||
initialReader := f(a)
|
||||
return func(ctx context.Context) (B, error) {
|
||||
rdr := initialReader
|
||||
for {
|
||||
// short circuit
|
||||
if ctx.Err() != nil {
|
||||
return result.Left[B](context.Cause(ctx))
|
||||
}
|
||||
rec, e := rdr(ctx)
|
||||
if e != nil {
|
||||
return result.Left[B](e)
|
||||
}
|
||||
if rec.Landed {
|
||||
return result.Of(rec.Land)
|
||||
}
|
||||
rdr = f(rec.Bounce)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
597
v2/idiomatic/context/readerresult/rec_test.go
Normal file
597
v2/idiomatic/context/readerresult/rec_test.go
Normal file
@@ -0,0 +1,597 @@
|
||||
// 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 readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTailRecFactorial tests factorial computation with context
|
||||
func TestTailRecFactorial(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.n <= 0 {
|
||||
return TR.Land[State](state.acc), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.n - 1, state.acc * state.n}), nil
|
||||
}
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result, err := factorial(State{5, 1})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 120, result)
|
||||
}
|
||||
|
||||
// TestTailRecFibonacci tests Fibonacci computation
|
||||
func TestTailRecFibonacci(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
prev int
|
||||
curr int
|
||||
}
|
||||
|
||||
fibStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.n <= 0 {
|
||||
return TR.Land[State](state.curr), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}), nil
|
||||
}
|
||||
}
|
||||
|
||||
fib := TailRec(fibStep)
|
||||
result, err := fib(State{10, 0, 1})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 89, result) // 10th Fibonacci number
|
||||
}
|
||||
|
||||
// TestTailRecCountdown tests countdown computation
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecImmediateTermination tests immediate termination (Land on first call)
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
immediateStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
return TR.Land[int](n * 2), nil
|
||||
}
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result, err := immediate(42)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 84, result)
|
||||
}
|
||||
|
||||
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10000)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecSumList tests summing a list
|
||||
func TestTailRecSumList(t *testing.T) {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
sumStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if A.IsEmpty(state.list) {
|
||||
return TR.Land[State](state.sum), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}), nil
|
||||
}
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
result, err := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 15, result)
|
||||
}
|
||||
|
||||
// TestTailRecCollatzConjecture tests the Collatz conjecture
|
||||
func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
collatzStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 1 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return TR.Bounce[int](n / 2), nil
|
||||
}
|
||||
return TR.Bounce[int](3*n + 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
collatz := TailRec(collatzStep)
|
||||
result, err := collatz(10)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, result)
|
||||
}
|
||||
|
||||
// TestTailRecGCD tests greatest common divisor
|
||||
func TestTailRecGCD(t *testing.T) {
|
||||
type State struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
|
||||
gcdStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.b == 0 {
|
||||
return TR.Land[State](state.a), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.b, state.a % state.b}), nil
|
||||
}
|
||||
}
|
||||
|
||||
gcd := TailRec(gcdStep)
|
||||
result, err := gcd(State{48, 18})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, result)
|
||||
}
|
||||
|
||||
// TestTailRecErrorPropagation tests that errors are properly propagated
|
||||
func TestTailRecErrorPropagation(t *testing.T) {
|
||||
expectedErr := errors.New("computation error")
|
||||
|
||||
errorStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n == 5 {
|
||||
return TR.Trampoline[int, int]{}, expectedErr
|
||||
}
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(errorStep)
|
||||
result, err := computation(10)(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
|
||||
func TestTailRecContextCancellationImmediate(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately before execution
|
||||
|
||||
stepExecuted := false
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
stepExecuted = true
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10)(ctx)
|
||||
|
||||
// Should short circuit without executing any steps
|
||||
assert.False(t, stepExecuted, "Step should not be executed when context is already canceled")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
|
||||
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
executionCount++
|
||||
// Cancel after 3 iterations
|
||||
if executionCount == 3 {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(100)(ctx)
|
||||
|
||||
// Should stop after cancellation
|
||||
assert.Error(t, err)
|
||||
assert.LessOrEqual(t, executionCount, 4, "Should stop shortly after cancellation")
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextWithTimeout tests behavior with timeout context
|
||||
func TestTailRecContextWithTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
executionCount := 0
|
||||
slowStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
executionCount++
|
||||
// Simulate slow computation
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(slowStep)
|
||||
result, err := computation(100)(ctx)
|
||||
|
||||
// Should timeout and return error
|
||||
assert.Error(t, err)
|
||||
assert.Less(t, executionCount, 100, "Should not complete all iterations due to timeout")
|
||||
assert.Equal(t, context.DeadlineExceeded, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextWithCause tests that context.Cause is properly returned
|
||||
func TestTailRecContextWithCause(t *testing.T) {
|
||||
customErr := errors.New("custom cancellation reason")
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
cancel(customErr)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10)(ctx)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, customErr, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
|
||||
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executionCount := 0
|
||||
maxExecutions := 5
|
||||
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
executionCount++
|
||||
if executionCount == maxExecutions {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(1000)(ctx)
|
||||
|
||||
// Should detect cancellation on next iteration check
|
||||
assert.Error(t, err)
|
||||
// Should stop within 1-2 iterations after cancellation
|
||||
assert.LessOrEqual(t, executionCount, maxExecutions+2)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
|
||||
func TestTailRecContextNotCanceled(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
executionCount++
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10)(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 11, executionCount) // 10, 9, 8, ..., 1, 0
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecPowerOfTwo tests computing power of 2
|
||||
func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
type State struct {
|
||||
exponent int
|
||||
result int
|
||||
target int
|
||||
}
|
||||
|
||||
powerStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.exponent >= state.target {
|
||||
return TR.Land[State](state.result), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}), nil
|
||||
}
|
||||
}
|
||||
|
||||
power := TailRec(powerStep)
|
||||
result, err := power(State{0, 1, 10})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1024, result) // 2^10
|
||||
}
|
||||
|
||||
// TestTailRecFindInRange tests finding a value in a range
|
||||
func TestTailRecFindInRange(t *testing.T) {
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.current >= state.max {
|
||||
return TR.Land[State](-1), nil // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return TR.Land[State](state.current), nil // Found
|
||||
}
|
||||
return TR.Bounce[int](State{state.current + 1, state.max, state.target}), nil
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result, err := find(State{0, 100, 42})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
// TestTailRecFindNotInRange tests finding a value not in range
|
||||
func TestTailRecFindNotInRange(t *testing.T) {
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.current >= state.max {
|
||||
return TR.Land[State](-1), nil // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return TR.Land[State](state.current), nil // Found
|
||||
}
|
||||
return TR.Bounce[int](State{state.current + 1, state.max, state.target}), nil
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result, err := find(State{0, 100, 200})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, -1, result)
|
||||
}
|
||||
|
||||
// TestTailRecWithContextValue tests that context values are accessible
|
||||
func TestTailRecWithContextValue(t *testing.T) {
|
||||
type contextKey string
|
||||
const multiplierKey contextKey = "multiplier"
|
||||
|
||||
ctx := context.WithValue(context.Background(), multiplierKey, 3)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 0 {
|
||||
multiplier := ctx.Value(multiplierKey).(int)
|
||||
return TR.Land[int](n * multiplier), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(5)(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result) // 0 * 3 = 0
|
||||
}
|
||||
|
||||
// TestTailRecComplexState tests with complex state structure
|
||||
func TestTailRecComplexState(t *testing.T) {
|
||||
type ComplexState struct {
|
||||
counter int
|
||||
sum int
|
||||
product int
|
||||
completed bool
|
||||
}
|
||||
|
||||
complexStep := func(state ComplexState) ReaderResult[TR.Trampoline[ComplexState, string]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[ComplexState, string], error) {
|
||||
if state.counter <= 0 || state.completed {
|
||||
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
|
||||
return TR.Land[ComplexState](result), nil
|
||||
}
|
||||
newState := ComplexState{
|
||||
counter: state.counter - 1,
|
||||
sum: state.sum + state.counter,
|
||||
product: state.product * state.counter,
|
||||
completed: state.counter == 1,
|
||||
}
|
||||
return TR.Bounce[string](newState), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(complexStep)
|
||||
result, err := computation(ComplexState{5, 0, 1, false})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "sum=15, product=120", result)
|
||||
}
|
||||
|
||||
// TestTailRecZeroIterations tests when computation terminates immediately
|
||||
func TestTailRecZeroIterations(t *testing.T) {
|
||||
step := func(n int) ReaderResult[TR.Trampoline[int, string]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, string], error) {
|
||||
return TR.Land[int]("immediate"), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(step)
|
||||
result, err := computation(0)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "immediate", result)
|
||||
}
|
||||
|
||||
// TestTailRecErrorInFirstIteration tests error on first iteration
|
||||
func TestTailRecErrorInFirstIteration(t *testing.T) {
|
||||
expectedErr := errors.New("first iteration error")
|
||||
|
||||
step := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
return TR.Trampoline[int, int]{}, expectedErr
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(step)
|
||||
result, err := computation(10)(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecAlternatingBounce tests alternating between different values
|
||||
func TestTailRecAlternatingBounce(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
alternate bool
|
||||
count int
|
||||
}
|
||||
|
||||
step := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.count >= 10 {
|
||||
return TR.Land[State](state.value), nil
|
||||
}
|
||||
newValue := state.value
|
||||
if state.alternate {
|
||||
newValue += 1
|
||||
} else {
|
||||
newValue -= 1
|
||||
}
|
||||
return TR.Bounce[int](State{newValue, !state.alternate, state.count + 1}), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(step)
|
||||
result, err := computation(State{0, true, 0})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result) // Should alternate +1, -1 and end at 0
|
||||
}
|
||||
|
||||
// TestTailRecLargeAccumulation tests accumulating large values
|
||||
func TestTailRecLargeAccumulation(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
sum int64
|
||||
}
|
||||
|
||||
step := func(state State) ReaderResult[TR.Trampoline[State, int64]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int64], error) {
|
||||
if state.n <= 0 {
|
||||
return TR.Land[State](state.sum), nil
|
||||
}
|
||||
return TR.Bounce[int64](State{state.n - 1, state.sum + int64(state.n)}), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(step)
|
||||
result, err := computation(State{1000, 0})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(500500), result) // Sum of 1 to 1000
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -68,4 +69,6 @@ type (
|
||||
|
||||
// Prism represents an optic that focuses on a case of type A within a sum type S.
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
Trampoline[A, B any] = tailrec.Trampoline[A, B]
|
||||
)
|
||||
|
||||
@@ -247,7 +247,7 @@ func TestOrLeft(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
orLeft := OrLeft[int, MyContext](enrichErr)
|
||||
orLeft := OrLeft[int](enrichErr)
|
||||
|
||||
v, err := F.Pipe1(Of[MyContext](42), orLeft)(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
488
v2/iterator/iter/bind.go
Normal file
488
v2/iterator/iter/bind.go
Normal file
@@ -0,0 +1,488 @@
|
||||
// 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 (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// Do creates a sequence containing a single element, typically used to start a do-notation chain.
|
||||
// This is the entry point for monadic composition using do-notation style.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The type of the state/structure being built
|
||||
//
|
||||
// Parameters:
|
||||
// - empty: The initial value to wrap in a sequence
|
||||
//
|
||||
// Returns:
|
||||
// - A sequence containing the single element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// // Start a do-notation chain
|
||||
// result := Do(User{})
|
||||
// // yields: User{Name: "", Age: 0}
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) Seq[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind performs a monadic bind operation in do-notation style, chaining a computation
|
||||
// that produces a sequence and updating the state with the result.
|
||||
//
|
||||
// This function is the core of do-notation for sequences. It takes a Kleisli arrow
|
||||
// (a function that returns a sequence) and a setter function that updates the state
|
||||
// with the result. The setter is curried to allow partial application.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of value produced by the Kleisli arrow
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: A curried function that takes a value T and returns a function that updates S1 to S2
|
||||
// - f: A Kleisli arrow that takes S1 and produces a sequence of T
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[S1] to Seq[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// Double int
|
||||
// }
|
||||
//
|
||||
// setValue := func(v int) func(State) State {
|
||||
// return func(s State) State { s.Value = v; return s }
|
||||
// }
|
||||
//
|
||||
// getValues := func(s State) Seq[int] {
|
||||
// return From(1, 2, 3)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{}),
|
||||
// Bind(setValue, getValues),
|
||||
// )
|
||||
// // yields: State{Value: 1}, State{Value: 2}, State{Value: 3}
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let performs a pure computation in do-notation style, updating the state with a computed value.
|
||||
//
|
||||
// Unlike Bind, Let doesn't perform a monadic operation - it simply computes a value from
|
||||
// the current state and updates the state with that value. This is useful for intermediate
|
||||
// calculations that don't require sequencing.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of the computed value
|
||||
//
|
||||
// Parameters:
|
||||
// - key: A curried function that takes a value T and returns a function that updates S1 to S2
|
||||
// - f: A function that computes T from S1
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[S1] to Seq[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// Double int
|
||||
// }
|
||||
//
|
||||
// setDouble := func(d int) func(State) State {
|
||||
// return func(s State) State { s.Double = d; return s }
|
||||
// }
|
||||
//
|
||||
// computeDouble := func(s State) int {
|
||||
// return s.Value * 2
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Value: 5}),
|
||||
// Let(setDouble, computeDouble),
|
||||
// )
|
||||
// // yields: State{Value: 5, Double: 10}
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
key func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[S1, S2] {
|
||||
return F.Let(
|
||||
Map[S1, S2],
|
||||
key,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo sets a field in the state to a constant value in do-notation style.
|
||||
//
|
||||
// This is a specialized version of Let that doesn't compute the value from the state,
|
||||
// but instead uses a fixed value. It's useful for setting constants or default values.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of the value to set
|
||||
//
|
||||
// Parameters:
|
||||
// - key: A curried function that takes a value T and returns a function that updates S1 to S2
|
||||
// - b: The constant value to set
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[S1] to Seq[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Status string
|
||||
// }
|
||||
//
|
||||
// setStatus := func(s string) func(State) State {
|
||||
// return func(st State) State { st.Status = s; return st }
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Name: "Alice"}),
|
||||
// LetTo(setStatus, "active"),
|
||||
// )
|
||||
// // yields: State{Name: "Alice", Status: "active"}
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
key func(T) func(S1) S2,
|
||||
b T,
|
||||
) Operator[S1, S2] {
|
||||
return F.LetTo(
|
||||
Map[S1, S2],
|
||||
key,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo wraps a value into a structure using a setter function.
|
||||
//
|
||||
// This is typically used at the beginning of a do-notation chain to convert a simple
|
||||
// value into a structured state. It's the inverse of extracting a value from a structure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The structure type to create
|
||||
// - T: The value type to wrap
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: A function that takes a value T and creates a structure S1
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[T] to Seq[S1]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// createState := func(v int) State {
|
||||
// return State{Value: v}
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// From(1, 2, 3),
|
||||
// BindTo(createState),
|
||||
// )
|
||||
// // yields: State{Value: 1}, State{Value: 2}, State{Value: 3}
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
return C.BindTo(
|
||||
Map[T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// BindToP wraps a value into a structure using a Prism's ReverseGet function.
|
||||
//
|
||||
// This is a specialized version of BindTo that works with Prisms (optics that focus
|
||||
// on a case of a sum type). It uses the Prism's ReverseGet to construct the structure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The structure type to create
|
||||
// - T: The value type to wrap
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: A Prism that can construct S1 from T
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[T] to Seq[S1]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Assuming a Prism for wrapping int into a Result type
|
||||
// result := F.Pipe2(
|
||||
// From(1, 2, 3),
|
||||
// BindToP(successPrism),
|
||||
// )
|
||||
// // yields: Success(1), Success(2), Success(3)
|
||||
//
|
||||
//go:inline
|
||||
func BindToP[S1, T any](
|
||||
setter Prism[S1, T],
|
||||
) Operator[T, S1] {
|
||||
return BindTo(setter.ReverseGet)
|
||||
}
|
||||
|
||||
// ApS applies a sequence of values to update a state using applicative style.
|
||||
//
|
||||
// This function combines applicative application with state updates. It takes a sequence
|
||||
// of values and a setter function, and produces an operator that applies each value
|
||||
// to update the state. This is useful for parallel composition of independent computations.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of values in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: A curried function that takes a value T and returns a function that updates S1 to S2
|
||||
// - fa: A sequence of values to apply
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[S1] to Seq[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// setY := func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// }
|
||||
//
|
||||
// yValues := From(10, 20, 30)
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{X: 5}),
|
||||
// ApS(setY, yValues),
|
||||
// )
|
||||
// // yields: State{X: 5, Y: 10}, State{X: 5, Y: 20}, State{X: 5, Y: 30}
|
||||
//
|
||||
//go:inline
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Seq[T],
|
||||
) Operator[S1, S2] {
|
||||
return A.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL applies a sequence of values to update a state field using a Lens.
|
||||
//
|
||||
// This is a specialized version of ApS that works with Lenses (optics that focus on
|
||||
// a field of a structure). It uses the Lens's Set function to update the field.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - T: The type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens focusing on the field to update
|
||||
// - fa: A sequence of values to set
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism on Seq[S] (transforms Seq[S] to Seq[S])
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.Prop[State, int]("Age")
|
||||
// ages := From(25, 30, 35)
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Name: "Alice"}),
|
||||
// ApSL(ageLens, ages),
|
||||
// )
|
||||
// // yields: State{Name: "Alice", Age: 25}, State{Name: "Alice", Age: 30}, State{Name: "Alice", Age: 35}
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Seq[T],
|
||||
) Endomorphism[Seq[S]] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL performs a monadic bind on a field of a structure using a Lens.
|
||||
//
|
||||
// This function combines Lens-based field access with monadic binding. It extracts
|
||||
// a field value using the Lens's Get, applies a Kleisli arrow to produce a sequence,
|
||||
// and updates the field with each result using the Lens's Set.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - T: The type of the field being accessed and updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens focusing on the field to bind
|
||||
// - f: A Kleisli arrow that takes the field value and produces a sequence
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism on Seq[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.Prop[State, int]("Value")
|
||||
//
|
||||
// multiplyValues := func(v int) Seq[int] {
|
||||
// return From(v, v*2, v*3)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Value: 5}),
|
||||
// BindL(valueLens, multiplyValues),
|
||||
// )
|
||||
// // yields: State{Value: 5}, State{Value: 10}, State{Value: 15}
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Endomorphism[Seq[S]] {
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL performs a pure computation on a field of a structure using a Lens.
|
||||
//
|
||||
// This function extracts a field value using the Lens's Get, applies a pure function
|
||||
// to compute a new value, and updates the field using the Lens's Set. It's useful
|
||||
// for transforming fields without monadic effects.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - T: The type of the field being transformed
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens focusing on the field to transform
|
||||
// - f: An Endomorphism that transforms the field value
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism on Seq[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Count int
|
||||
// }
|
||||
//
|
||||
// countLens := lens.Prop[State, int]("Count")
|
||||
//
|
||||
// increment := func(n int) int { return n + 1 }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Count: 5}),
|
||||
// LetL(countLens, increment),
|
||||
// )
|
||||
// // yields: State{Count: 6}
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Endomorphism[Seq[S]] {
|
||||
return Let(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL sets a field of a structure to a constant value using a Lens.
|
||||
//
|
||||
// This is a specialized version of LetL that sets a field to a fixed value rather
|
||||
// than computing it from the current value. It's useful for setting defaults or
|
||||
// resetting fields.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - T: The type of the field being set
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens focusing on the field to set
|
||||
// - b: The constant value to set
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism on Seq[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Status string
|
||||
// }
|
||||
//
|
||||
// statusLens := lens.Prop[State, string]("Status")
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Status: "pending"}),
|
||||
// LetToL(statusLens, "active"),
|
||||
// )
|
||||
// // yields: State{Status: "active"}
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Endomorphism[Seq[S]] {
|
||||
return LetTo(lens.Set, b)
|
||||
}
|
||||
741
v2/iterator/iter/bind_test.go
Normal file
741
v2/iterator/iter/bind_test.go
Normal file
@@ -0,0 +1,741 @@
|
||||
// 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"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Value int
|
||||
Double int
|
||||
Status string
|
||||
}
|
||||
|
||||
// TestDo tests the Do function
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates sequence with single element", func(t *testing.T) {
|
||||
result := Do(42)
|
||||
values := slices.Collect(result)
|
||||
assert.Equal(t, A.Of(42), values)
|
||||
})
|
||||
|
||||
t.Run("creates sequence with struct", func(t *testing.T) {
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := Do(user)
|
||||
values := slices.Collect(result)
|
||||
assert.Equal(t, A.Of(user), values)
|
||||
})
|
||||
|
||||
t.Run("creates sequence with zero value", func(t *testing.T) {
|
||||
result := Do(State{})
|
||||
values := slices.Collect(result)
|
||||
assert.Equal(t, []State{{Value: 0, Double: 0, Status: ""}}, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind tests the Bind function
|
||||
func TestBind(t *testing.T) {
|
||||
t.Run("binds simple value", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(1, 2, 3)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, getValues)
|
||||
result := bindOp(Do(State{}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 1},
|
||||
{Value: 2},
|
||||
{Value: 3},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("chains multiple binds", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(5)
|
||||
}
|
||||
|
||||
computeDouble := func(s State) Seq[int] {
|
||||
return From(s.Value * 2)
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
Bind(setValue, getValues),
|
||||
Bind(setDouble, computeDouble),
|
||||
)(Do(State{}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 5, Double: 10}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("binds with multiple results", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
multiplyValues := func(s State) Seq[int] {
|
||||
return From(s.Value, s.Value*2, s.Value*3)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, multiplyValues)
|
||||
result := bindOp(Do(State{Value: 2}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 2},
|
||||
{Value: 4},
|
||||
{Value: 6},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("binds with empty sequence", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
emptySeq := func(s State) Seq[int] {
|
||||
return Empty[int]()
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, emptySeq)
|
||||
result := bindOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
assert.Empty(t, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLet tests the Let function
|
||||
func TestLet(t *testing.T) {
|
||||
t.Run("computes value from state", func(t *testing.T) {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
letOp := Let(setDouble, computeDouble)
|
||||
result := letOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 5, Double: 10}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("chains multiple lets", func(t *testing.T) {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
computeValue := func(s State) int {
|
||||
return s.Double / 2
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
Let(setDouble, computeDouble),
|
||||
Let(setValue, computeValue),
|
||||
)(Do(State{Value: 7}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 7, Double: 14}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("computes complex transformation", func(t *testing.T) {
|
||||
setStatus := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Status = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
computeStatus := func(s State) string {
|
||||
if s.Value > 10 {
|
||||
return "high"
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
letOp := Let(setStatus, computeStatus)
|
||||
result := letOp(Do(State{Value: 15}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 15, Status: "high"}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLetTo tests the LetTo function
|
||||
func TestLetTo(t *testing.T) {
|
||||
t.Run("sets constant value", func(t *testing.T) {
|
||||
setStatus := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Status = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
letToOp := LetTo(setStatus, "active")
|
||||
result := letToOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 5, Status: "active"}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("chains multiple LetTo calls", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setStatus := func(st string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Status = st
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
LetTo(setValue, 42),
|
||||
LetTo(setStatus, "ready"),
|
||||
)(Do(State{}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 42, Status: "ready"}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("sets zero value", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
letToOp := LetTo(setValue, 0)
|
||||
result := letToOp(Do(State{Value: 100}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 0}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestBindTo tests the BindTo function
|
||||
func TestBindTo(t *testing.T) {
|
||||
t.Run("wraps values into structure", func(t *testing.T) {
|
||||
createState := func(v int) State {
|
||||
return State{Value: v}
|
||||
}
|
||||
|
||||
bindToOp := BindTo(createState)
|
||||
result := bindToOp(From(1, 2, 3))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 1},
|
||||
{Value: 2},
|
||||
{Value: 3},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("wraps into complex structure", func(t *testing.T) {
|
||||
createUser := func(name string) User {
|
||||
return User{Name: name, Age: 0}
|
||||
}
|
||||
|
||||
bindToOp := BindTo(createUser)
|
||||
result := bindToOp(From("Alice", "Bob", "Charlie"))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []User{
|
||||
{Name: "Alice", Age: 0},
|
||||
{Name: "Bob", Age: 0},
|
||||
{Name: "Charlie", Age: 0},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("wraps empty sequence", func(t *testing.T) {
|
||||
createState := func(v int) State {
|
||||
return State{Value: v}
|
||||
}
|
||||
|
||||
bindToOp := BindTo(createState)
|
||||
result := bindToOp(Empty[int]())
|
||||
|
||||
values := slices.Collect(result)
|
||||
assert.Empty(t, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApS tests the ApS function
|
||||
func TestApS(t *testing.T) {
|
||||
t.Run("applies sequence of values", func(t *testing.T) {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
doubles := From(10, 20, 30)
|
||||
|
||||
apOp := ApS(setDouble, doubles)
|
||||
result := apOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 5, Double: 10},
|
||||
{Value: 5, Double: 20},
|
||||
{Value: 5, Double: 30},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("applies with empty sequence", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
apOp := ApS(setValue, Empty[int]())
|
||||
result := apOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
assert.Empty(t, values)
|
||||
})
|
||||
|
||||
t.Run("chains multiple ApS calls", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
values := From(1, 2)
|
||||
doubles := From(10, 20)
|
||||
|
||||
result := F.Flow2(
|
||||
ApS(setValue, values),
|
||||
ApS(setDouble, doubles),
|
||||
)(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
// Cartesian product: 2 values × 2 doubles = 4 results
|
||||
assert.Len(t, results, 4)
|
||||
})
|
||||
}
|
||||
|
||||
// TestDoNotationChain tests a complete do-notation chain
|
||||
func TestDoNotationChain(t *testing.T) {
|
||||
t.Run("complex do-notation chain", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setStatus := func(st string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Status = st
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(5, 10)
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
result := F.Flow3(
|
||||
Bind(setValue, getValues),
|
||||
Let(setDouble, computeDouble),
|
||||
LetTo(setStatus, "computed"),
|
||||
)(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 5, Double: 10, Status: "computed"},
|
||||
{Value: 10, Double: 20, Status: "computed"},
|
||||
}
|
||||
assert.Equal(t, expected, results)
|
||||
})
|
||||
|
||||
t.Run("mixed bind and let operations", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getInitial := func(s State) Seq[int] {
|
||||
return From(3)
|
||||
}
|
||||
|
||||
multiplyValue := func(s State) Seq[int] {
|
||||
return From(s.Value*2, s.Value*3)
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
Bind(setValue, getInitial),
|
||||
Bind(setDouble, multiplyValue),
|
||||
)(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 3, Double: 6},
|
||||
{Value: 3, Double: 9},
|
||||
}
|
||||
assert.Equal(t, expected, results)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEdgeCases tests edge cases
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
t.Run("bind with single element", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getSingle := func(s State) Seq[int] {
|
||||
return From(42)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, getSingle)
|
||||
result := bindOp(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
expected := []State{{Value: 42}}
|
||||
assert.Equal(t, expected, results)
|
||||
})
|
||||
|
||||
t.Run("multiple binds with cartesian product", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(1, 2)
|
||||
}
|
||||
|
||||
getDoubles := func(s State) Seq[int] {
|
||||
return From(10, 20)
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
Bind(setValue, getValues),
|
||||
Bind(setDouble, getDoubles),
|
||||
)(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
// Should produce cartesian product: 2 × 2 = 4 results
|
||||
assert.Len(t, results, 4)
|
||||
})
|
||||
|
||||
t.Run("let with identity function", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
identity := func(s State) int {
|
||||
return s.Value
|
||||
}
|
||||
|
||||
letOp := Let(setValue, identity)
|
||||
result := letOp(Do(State{Value: 99}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
expected := []State{{Value: 99}}
|
||||
assert.Equal(t, expected, results)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkBind(b *testing.B) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(1, 2, 3, 4, 5)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, getValues)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := bindOp(Do(State{}))
|
||||
// Consume the sequence
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLet(b *testing.B) {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
letOp := Let(setDouble, computeDouble)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := letOp(Do(State{Value: 5}))
|
||||
// Consume the sequence
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDoNotationChain(b *testing.B) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setStatus := func(st string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Status = st
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(5, 10, 15)
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
chain := F.Flow3(
|
||||
Bind(setValue, getValues),
|
||||
Let(setDouble, computeDouble),
|
||||
LetTo(setStatus, "computed"),
|
||||
)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := chain(Do(State{}))
|
||||
// Consume the sequence
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleDo() {
|
||||
result := Do(42)
|
||||
for v := range result {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output: 42
|
||||
}
|
||||
|
||||
func ExampleBind() {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(1, 2, 3)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, getValues)
|
||||
result := bindOp(Do(State{}))
|
||||
|
||||
for s := range result {
|
||||
fmt.Printf("Value: %d\n", s.Value)
|
||||
}
|
||||
// Output:
|
||||
// Value: 1
|
||||
// Value: 2
|
||||
// Value: 3
|
||||
}
|
||||
|
||||
func ExampleLet() {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
letOp := Let(setDouble, computeDouble)
|
||||
result := letOp(Do(State{Value: 5}))
|
||||
|
||||
for s := range result {
|
||||
fmt.Printf("Value: %d, Double: %d\n", s.Value, s.Double)
|
||||
}
|
||||
// Output: Value: 5, Double: 10
|
||||
}
|
||||
|
||||
func ExampleLetTo() {
|
||||
setStatus := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Status = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
letToOp := LetTo(setStatus, "active")
|
||||
result := letToOp(Do(State{Value: 5}))
|
||||
|
||||
for s := range result {
|
||||
fmt.Printf("Value: %d, Status: %s\n", s.Value, s.Status)
|
||||
}
|
||||
// Output: Value: 5, Status: active
|
||||
}
|
||||
84
v2/iterator/iter/compress.go
Normal file
84
v2/iterator/iter/compress.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Compress filters elements from a sequence based on a corresponding sequence of boolean selectors.
|
||||
//
|
||||
// This function takes a sequence of boolean values and returns an operator that filters
|
||||
// elements from the input sequence. An element is included in the output if and only if
|
||||
// the corresponding boolean selector is true. The filtering stops when either sequence
|
||||
// is exhausted.
|
||||
//
|
||||
// The implementation works by:
|
||||
// 1. Zipping the input sequence with the selector sequence
|
||||
// 2. Converting the Seq2 to a sequence of Pairs
|
||||
// 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
|
||||
//
|
||||
// Parameters:
|
||||
// - sel: A sequence of boolean values used as selectors
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters elements based on the selector sequence
|
||||
//
|
||||
// Example - Basic filtering:
|
||||
//
|
||||
// data := iter.From(1, 2, 3, 4, 5)
|
||||
// selectors := iter.From(true, false, true, false, true)
|
||||
// filtered := iter.Compress(selectors)(data)
|
||||
// // yields: 1, 3, 5
|
||||
//
|
||||
// Example - Shorter selector sequence:
|
||||
//
|
||||
// data := iter.From("a", "b", "c", "d", "e")
|
||||
// selectors := iter.From(true, true, false)
|
||||
// filtered := iter.Compress(selectors)(data)
|
||||
// // yields: "a", "b" (stops when selectors are exhausted)
|
||||
//
|
||||
// Example - All false selectors:
|
||||
//
|
||||
// data := iter.From(1, 2, 3)
|
||||
// selectors := iter.From(false, false, false)
|
||||
// filtered := iter.Compress(selectors)(data)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - All true selectors:
|
||||
//
|
||||
// data := iter.From(10, 20, 30)
|
||||
// selectors := iter.From(true, true, true)
|
||||
// filtered := iter.Compress(selectors)(data)
|
||||
// // yields: 10, 20, 30 (all elements pass through)
|
||||
func Compress[U any](sel Seq[bool]) Operator[U, U] {
|
||||
return F.Flow3(
|
||||
Zip[U](sel),
|
||||
ToSeqPair[U, bool],
|
||||
FilterMap(F.Flow2(
|
||||
O.FromPredicate(P.Tail[U, bool]),
|
||||
O.Map(P.Head[U, bool]),
|
||||
)),
|
||||
)
|
||||
}
|
||||
365
v2/iterator/iter/compress_test.go
Normal file
365
v2/iterator/iter/compress_test.go
Normal file
@@ -0,0 +1,365 @@
|
||||
// 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"
|
||||
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCompress tests the Compress function
|
||||
func TestCompress(t *testing.T) {
|
||||
t.Run("filters with alternating selectors", func(t *testing.T) {
|
||||
data := From(1, 2, 3, 4, 5)
|
||||
selectors := From(true, false, true, false, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{1, 3, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("filters strings with boolean selectors", func(t *testing.T) {
|
||||
data := From("a", "b", "c", "d", "e")
|
||||
selectors := From(true, true, false, false, true)
|
||||
filtered := Compress[string](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []string{"a", "b", "e"}, result)
|
||||
})
|
||||
|
||||
t.Run("all true selectors pass all elements", func(t *testing.T) {
|
||||
data := From(10, 20, 30)
|
||||
selectors := From(true, true, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{10, 20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("all false selectors produce empty sequence", func(t *testing.T) {
|
||||
data := From(1, 2, 3)
|
||||
selectors := From(false, false, false)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("shorter selector sequence stops early", func(t *testing.T) {
|
||||
data := From("a", "b", "c", "d", "e")
|
||||
selectors := From(true, true, false)
|
||||
filtered := Compress[string](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []string{"a", "b"}, result)
|
||||
})
|
||||
|
||||
t.Run("shorter data sequence stops early", func(t *testing.T) {
|
||||
data := From(1, 2, 3)
|
||||
selectors := From(true, false, true, true, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{1, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("empty data sequence produces empty result", func(t *testing.T) {
|
||||
data := Empty[int]()
|
||||
selectors := From(true, true, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty selector sequence produces empty result", func(t *testing.T) {
|
||||
data := From(1, 2, 3)
|
||||
selectors := Empty[bool]()
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("both empty sequences produce empty result", func(t *testing.T) {
|
||||
data := Empty[int]()
|
||||
selectors := Empty[bool]()
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("single element with true selector", func(t *testing.T) {
|
||||
data := From(42)
|
||||
selectors := From(true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("single element with false selector", func(t *testing.T) {
|
||||
data := From(42)
|
||||
selectors := From(false)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCompressWithComplexTypes tests Compress with complex data types
|
||||
func TestCompressWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("filters struct values", func(t *testing.T) {
|
||||
data := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
selectors := From(true, false, true, false)
|
||||
filtered := Compress[Person](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Charlie", 35},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("filters pointer values", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
data := From(p1, p2, p3)
|
||||
selectors := From(false, true, true)
|
||||
filtered := Compress[*Person](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []*Person{p2, p3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCompressWithChainedOperations tests Compress with other operations
|
||||
func TestCompressWithChainedOperations(t *testing.T) {
|
||||
t.Run("compress then map", func(t *testing.T) {
|
||||
data := From(1, 2, 3, 4, 5)
|
||||
selectors := From(true, false, true, false, true)
|
||||
result := toSlice(
|
||||
MonadMap(
|
||||
Compress[int](selectors)(data),
|
||||
func(x int) int { return x * 10 },
|
||||
),
|
||||
)
|
||||
assert.Equal(t, []int{10, 30, 50}, result)
|
||||
})
|
||||
|
||||
t.Run("map then compress", func(t *testing.T) {
|
||||
data := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(data, func(x int) int { return x * 2 })
|
||||
selectors := From(true, true, false, false, true)
|
||||
result := toSlice(Compress[int](selectors)(mapped))
|
||||
assert.Equal(t, []int{2, 4, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("compress with filtered data", func(t *testing.T) {
|
||||
data := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
evens := MonadFilter(data, func(x int) bool { return x%2 == 0 })
|
||||
selectors := From(true, false, true, false, true)
|
||||
result := toSlice(Compress[int](selectors)(evens))
|
||||
assert.Equal(t, []int{2, 6, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair tests the ToSeqPair function
|
||||
func TestToSeqPair(t *testing.T) {
|
||||
t.Run("converts Seq2 to sequence of pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From("a", "b", "c"), From(1, 2, 3))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
expected := []Pair[string, int]{
|
||||
P.MakePair("a", 1),
|
||||
P.MakePair("b", 2),
|
||||
P.MakePair("c", 3),
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("converts empty Seq2", func(t *testing.T) {
|
||||
seq2 := MonadZip(Empty[int](), Empty[string]())
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("converts single pair", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(42), From("answer"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
expected := []Pair[int, string]{
|
||||
P.MakePair(42, "answer"),
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("stops at shorter sequence", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3, 4, 5), From("a", "b"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
expected := []Pair[int, string]{
|
||||
P.MakePair(1, "a"),
|
||||
P.MakePair(2, "b"),
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPairWithOperations tests ToSeqPair with other operations
|
||||
func TestToSeqPairWithOperations(t *testing.T) {
|
||||
t.Run("map over pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3), From(10, 20, 30))
|
||||
pairs := ToSeqPair(seq2)
|
||||
sums := MonadMap(pairs, func(p Pair[int, int]) int {
|
||||
return P.Head(p) + P.Tail(p)
|
||||
})
|
||||
result := toSlice(sums)
|
||||
assert.Equal(t, []int{11, 22, 33}, result)
|
||||
})
|
||||
|
||||
t.Run("filter pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3, 4, 5), From(10, 20, 30, 40, 50))
|
||||
pairs := ToSeqPair(seq2)
|
||||
filtered := MonadFilter(pairs, func(p Pair[int, int]) bool {
|
||||
return P.Head(p)%2 == 0
|
||||
})
|
||||
result := toSlice(filtered)
|
||||
expected := []Pair[int, int]{
|
||||
P.MakePair(2, 20),
|
||||
P.MakePair(4, 40),
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("extract first elements from pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3), From("x", "y", "z"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
firsts := MonadMap(pairs, func(p Pair[int, string]) int {
|
||||
return P.Head(p)
|
||||
})
|
||||
result := toSlice(firsts)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("extract second elements from pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
seconds := MonadMap(pairs, func(p Pair[int, string]) string {
|
||||
return P.Tail(p)
|
||||
})
|
||||
result := toSlice(seconds)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCompressAndToSeqPairTogether tests using both functions together
|
||||
func TestCompressAndToSeqPairTogether(t *testing.T) {
|
||||
t.Run("compress uses ToSeqPair internally", func(t *testing.T) {
|
||||
// This test verifies the integration works correctly
|
||||
data := From(10, 20, 30, 40, 50)
|
||||
selectors := From(true, false, true, true, false)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{10, 30, 40}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkCompress(b *testing.B) {
|
||||
data := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
selectors := From(true, false, true, false, true, false, true, false, true, false)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filtered := Compress[int](selectors)(data)
|
||||
for range filtered {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToSeqPair(b *testing.B) {
|
||||
seq2 := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pairs := ToSeqPair(seq2)
|
||||
for range pairs {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleCompress() {
|
||||
data := From(1, 2, 3, 4, 5)
|
||||
selectors := From(true, false, true, false, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
|
||||
for v := range filtered {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 3 5
|
||||
}
|
||||
|
||||
func ExampleCompress_allTrue() {
|
||||
data := From(10, 20, 30)
|
||||
selectors := From(true, true, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
|
||||
for v := range filtered {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 10 20 30
|
||||
}
|
||||
|
||||
func ExampleCompress_allFalse() {
|
||||
data := From(1, 2, 3)
|
||||
selectors := From(false, false, false)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
|
||||
count := 0
|
||||
for range filtered {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleToSeqPair() {
|
||||
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
|
||||
for p := range pairs {
|
||||
fmt.Printf("(%d, %s) ", P.Head(p), P.Tail(p))
|
||||
}
|
||||
// Output: (1, a) (2, b) (3, c)
|
||||
}
|
||||
|
||||
func ExampleToSeqPair_withMap() {
|
||||
seq2 := MonadZip(From(1, 2, 3), From(10, 20, 30))
|
||||
pairs := ToSeqPair(seq2)
|
||||
sums := MonadMap(pairs, func(p Pair[int, int]) int {
|
||||
return P.Head(p) + P.Tail(p)
|
||||
})
|
||||
|
||||
for sum := range sums {
|
||||
fmt.Printf("%d ", sum)
|
||||
}
|
||||
// Output: 11 22 33
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
60
v2/iterator/iter/first.go
Normal file
60
v2/iterator/iter/first.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 "github.com/IBM/fp-go/v2/option"
|
||||
|
||||
// First returns the first element from an [Iterator] wrapped in an [Option].
|
||||
//
|
||||
// This function attempts to retrieve the first element from the iterator. If the iterator
|
||||
// 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
|
||||
//
|
||||
// Parameters:
|
||||
// - mu: The input iterator to get the first element from
|
||||
//
|
||||
// Returns:
|
||||
// - Option[U]: Some(first element) if the iterator is non-empty, None otherwise
|
||||
//
|
||||
// Example with non-empty sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// first := iter.First(seq)
|
||||
// // Returns: Some(1)
|
||||
//
|
||||
// Example with empty sequence:
|
||||
//
|
||||
// seq := iter.Empty[int]()
|
||||
// first := iter.First(seq)
|
||||
// // Returns: None
|
||||
//
|
||||
// Example with filtered sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// filtered := iter.Filter(func(x int) bool { return x > 3 })(seq)
|
||||
// first := iter.First(filtered)
|
||||
// // Returns: Some(4)
|
||||
func First[U any](mu Seq[U]) Option[U] {
|
||||
for u := range mu {
|
||||
return option.Some(u)
|
||||
}
|
||||
return option.None[U]()
|
||||
}
|
||||
346
v2/iterator/iter/first_test.go
Normal file
346
v2/iterator/iter/first_test.go
Normal file
@@ -0,0 +1,346 @@
|
||||
// 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"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestFirst tests getting the first element from a non-empty sequence
|
||||
func TestFirst(t *testing.T) {
|
||||
t.Run("returns first element from integer sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first element from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of("a"), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first element from single element sequence", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first element from large sequence", func(t *testing.T) {
|
||||
seq := From(100, 200, 300, 400, 500)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(100), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstEmpty tests getting the first element from an empty sequence
|
||||
func TestFirstEmpty(t *testing.T) {
|
||||
t.Run("returns None for empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[string](), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty struct sequence", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Value int
|
||||
}
|
||||
seq := Empty[TestStruct]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[TestStruct](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithFiltered tests First with filtered sequences
|
||||
func TestFirstWithFiltered(t *testing.T) {
|
||||
t.Run("returns first element matching filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
filtered := MonadFilter(seq, N.MoreThan(3))
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.Of(4), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None when no elements match filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
filtered := MonadFilter(seq, N.MoreThan(10))
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first even number", func(t *testing.T) {
|
||||
seq := From(1, 3, 5, 6, 7, 8)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.Of(6), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithMapped tests First with mapped sequences
|
||||
func TestFirstWithMapped(t *testing.T) {
|
||||
t.Run("returns first element after mapping", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
fst := First(mapped)
|
||||
assert.Equal(t, O.Of(2), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first string after mapping", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := MonadMap(seq, S.Format[int]("num-%d"))
|
||||
fst := First(mapped)
|
||||
assert.Equal(t, O.Of("num-1"), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithComplex tests First with complex types
|
||||
func TestFirstWithComplex(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("returns first person", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
fst := First(seq)
|
||||
expected := O.Of(Person{"Alice", 30})
|
||||
assert.Equal(t, expected, fst)
|
||||
})
|
||||
|
||||
t.Run("returns first pointer", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
seq := From(p1, p2)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(p1), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstDoesNotConsumeEntireSequence tests that First only consumes the first element
|
||||
func TestFirstDoesNotConsumeEntireSequence(t *testing.T) {
|
||||
t.Run("only consumes first element", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := MonadMap(From(1, 2, 3, 4, 5), func(x int) int {
|
||||
callCount++
|
||||
return x * 2
|
||||
})
|
||||
|
||||
fst := First(seq)
|
||||
|
||||
assert.Equal(t, O.Of(2), fst)
|
||||
// Should only have called the map function once for the first element
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithChainedOperations tests First with multiple chained operations
|
||||
func TestFirstWithChainedOperations(t *testing.T) {
|
||||
t.Run("chains filter, map, and first", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, N.MoreThan(5))
|
||||
mapped := MonadMap(filtered, N.Mul(10))
|
||||
result := First(mapped)
|
||||
assert.Equal(t, O.Of(60), result)
|
||||
})
|
||||
|
||||
t.Run("chains map and filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
filtered := MonadFilter(mapped, N.MoreThan(5))
|
||||
result := First(filtered)
|
||||
assert.Equal(t, O.Of(6), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithReplicate tests First with replicated values
|
||||
func TestFirstWithReplicate(t *testing.T) {
|
||||
t.Run("returns first from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(5, 42)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None from zero replications", func(t *testing.T) {
|
||||
seq := Replicate(0, 42)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithMakeBy tests First with MakeBy
|
||||
func TestFirstWithMakeBy(t *testing.T) {
|
||||
t.Run("returns first generated element", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i * i })
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(0), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None for zero elements", func(t *testing.T) {
|
||||
seq := MakeBy(0, F.Identity[int])
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithPrepend tests First with Prepend
|
||||
func TestFirstWithPrepend(t *testing.T) {
|
||||
t.Run("returns prepended element", func(t *testing.T) {
|
||||
seq := From(2, 3, 4)
|
||||
prepended := Prepend(1)(seq)
|
||||
fst := First(prepended)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
prepended := Prepend(42)(seq)
|
||||
fst := First(prepended)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithAppend tests First with Append
|
||||
func TestFirstWithAppend(t *testing.T) {
|
||||
t.Run("returns first element, not appended", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
fst := First(appended)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns appended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
appended := Append(42)(seq)
|
||||
fst := First(appended)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithChain tests First with Chain (flatMap)
|
||||
func TestFirstWithChain(t *testing.T) {
|
||||
t.Run("returns first from chained sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
fst := First(chained)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None when chain produces empty", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return Empty[int]()
|
||||
})
|
||||
fst := First(chained)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithFlatten tests First with Flatten
|
||||
func TestFirstWithFlatten(t *testing.T) {
|
||||
t.Run("returns first from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5))
|
||||
flattened := Flatten(nested)
|
||||
fst := First(flattened)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None from empty nested sequence", func(t *testing.T) {
|
||||
nested := Empty[Seq[int]]()
|
||||
flattened := Flatten(nested)
|
||||
fst := First(flattened)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkFirst(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
First(seq)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFirstLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
First(seq)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFirstFiltered(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
filtered := MonadFilter(seq, N.MoreThan(5))
|
||||
First(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleFirst() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
first := First(seq)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First element: %d\n", value)
|
||||
}
|
||||
// Output: First element: 1
|
||||
}
|
||||
|
||||
func ExampleFirst_empty() {
|
||||
seq := Empty[int]()
|
||||
first := First(seq)
|
||||
|
||||
if _, ok := O.Unwrap(first); !ok {
|
||||
fmt.Println("Sequence is empty")
|
||||
}
|
||||
// Output: Sequence is empty
|
||||
}
|
||||
|
||||
func ExampleFirst_filtered() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
filtered := MonadFilter(seq, N.MoreThan(3))
|
||||
first := First(filtered)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First element > 3: %d\n", value)
|
||||
}
|
||||
// Output: First element > 3: 4
|
||||
}
|
||||
@@ -51,6 +51,7 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/internal/iter"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Of creates a sequence containing a single element.
|
||||
@@ -80,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)
|
||||
@@ -182,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)
|
||||
@@ -424,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)
|
||||
@@ -460,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))
|
||||
@@ -562,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)
|
||||
@@ -818,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)
|
||||
@@ -831,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)
|
||||
@@ -845,13 +860,15 @@ 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)
|
||||
// seqB := From("a", "b")
|
||||
// result := MonadZip(seqB, seqA)
|
||||
// result := MonadZip(seqA, seqB)
|
||||
// // yields: (1, "a"), (2, "b")
|
||||
func MonadZip[A, B any](fb Seq[B], fa Seq[A]) Seq2[A, B] {
|
||||
func MonadZip[A, B any](fa Seq[A], fb Seq[B]) Seq2[A, B] {
|
||||
|
||||
return func(yield func(A, B) bool) {
|
||||
na, sa := I.Pull(fa)
|
||||
@@ -882,8 +899,8 @@ func MonadZip[A, B any](fb Seq[B], fa Seq[A]) Seq2[A, B] {
|
||||
// // yields: (1, "a"), (2, "b"), (3, "c")
|
||||
//
|
||||
//go:inline
|
||||
func Zip[A, B any](fa Seq[A]) func(Seq[B]) Seq2[A, B] {
|
||||
return F.Bind2nd(MonadZip[A, B], fa)
|
||||
func Zip[A, B any](fb Seq[B]) func(Seq[A]) Seq2[A, B] {
|
||||
return F.Bind2nd(MonadZip[A, B], fb)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -895,3 +912,50 @@ func MonadMapToArray[A, B any](fa Seq[A], f func(A) B) []B {
|
||||
func MapToArray[A, B any](f func(A) B) func(Seq[A]) []B {
|
||||
return G.MapToArray[Seq[A], []B](f)
|
||||
}
|
||||
|
||||
// ToSeqPair converts a key-value sequence (Seq2) into a sequence of Pairs.
|
||||
//
|
||||
// This function transforms a Seq2[A, B] (which yields key-value pairs when iterated)
|
||||
// into a Seq[Pair[A, B]] (which yields Pair objects). This is useful when you need
|
||||
// to work with pairs as first-class values rather than as separate key-value arguments.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the first element (key) in each pair
|
||||
// - B: The type of the second element (value) in each pair
|
||||
//
|
||||
// Parameters:
|
||||
// - as: A Seq2 that yields key-value pairs
|
||||
//
|
||||
// Returns:
|
||||
// - A Seq that yields Pair objects containing the key-value pairs
|
||||
//
|
||||
// Example - Basic conversion:
|
||||
//
|
||||
// seq2 := iter.MonadZip(iter.From("a", "b", "c"), iter.From(1, 2, 3))
|
||||
// pairs := iter.ToSeqPair(seq2)
|
||||
// // yields: Pair("a", 1), Pair("b", 2), Pair("c", 3)
|
||||
//
|
||||
// Example - Using with Map:
|
||||
//
|
||||
// seq2 := iter.MonadZip(iter.From(1, 2, 3), iter.From(10, 20, 30))
|
||||
// pairs := iter.ToSeqPair(seq2)
|
||||
// sums := iter.MonadMap(pairs, func(p Pair[int, int]) int {
|
||||
// return p.Fst + p.Snd
|
||||
// })
|
||||
// // yields: 11, 22, 33
|
||||
//
|
||||
// Example - Empty sequence:
|
||||
//
|
||||
// seq2 := iter.Empty[int]()
|
||||
// zipped := iter.MonadZip(seq2, iter.Empty[string]())
|
||||
// pairs := iter.ToSeqPair(zipped)
|
||||
// // yields: nothing (empty sequence)
|
||||
func ToSeqPair[A, B any](as Seq2[A, B]) Seq[Pair[A, B]] {
|
||||
return func(yield Predicate[Pair[A, B]]) {
|
||||
for a, b := range as {
|
||||
if !yield(pair.MakePair(a, b)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ func TestMonadZip(t *testing.T) {
|
||||
|
||||
var pairs []string
|
||||
for a, b := range result {
|
||||
pairs = append(pairs, fmt.Sprintf("%d:%s", a, b))
|
||||
pairs = append(pairs, fmt.Sprintf("%d:%s", b, a))
|
||||
}
|
||||
assert.Equal(t, []string{"1:a", "2:b"}, pairs)
|
||||
}
|
||||
@@ -470,8 +470,8 @@ func TestMonadZip(t *testing.T) {
|
||||
func TestZip(t *testing.T) {
|
||||
seqA := From(1, 2, 3)
|
||||
seqB := From("a", "b", "c")
|
||||
zipWithA := Zip[int, string](seqA)
|
||||
result := zipWithA(seqB)
|
||||
zipWithA := Zip[int](seqB)
|
||||
result := zipWithA(seqA)
|
||||
|
||||
var pairs []string
|
||||
for a, b := range result {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -18,8 +18,12 @@ package iter
|
||||
import (
|
||||
I "iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/iterator/stateless"
|
||||
"github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
@@ -54,4 +58,12 @@ type (
|
||||
|
||||
// Operator2 represents a transformation from one key-value sequence to another.
|
||||
Operator2[K, A, B any] = Kleisli2[K, Seq2[K, A], B]
|
||||
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Pair[A, B any] = pair.Pair[A, B]
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -19,8 +19,28 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
|
||||
)
|
||||
|
||||
// DropWhile creates an [Iterator] that drops elements from the [Iterator] as long as the predicate is true; afterwards, returns every element.
|
||||
// Note, the [Iterator] does not produce any output until the predicate first becomes false
|
||||
// Cycle creates an [Iterator] that repeats the elements of the input [Iterator] indefinitely.
|
||||
// The iterator cycles through all elements of the input, and when it reaches the end, it starts over from the beginning.
|
||||
// This creates an infinite iterator, so it should be used with caution and typically combined with operations that limit the output.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the iterator
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The input iterator to cycle through
|
||||
//
|
||||
// Returns:
|
||||
// - An iterator that infinitely repeats the elements of the input iterator
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := stateless.FromArray([]int{1, 2, 3})
|
||||
// cycled := stateless.Cycle(iter)
|
||||
// // Produces: 1, 2, 3, 1, 2, 3, 1, 2, 3, ... (infinitely)
|
||||
//
|
||||
// // Typically used with Take to limit output:
|
||||
// limited := stateless.Take(7)(cycled)
|
||||
// // Produces: 1, 2, 3, 1, 2, 3, 1
|
||||
func Cycle[U any](ma Iterator[U]) Iterator[U] {
|
||||
return G.Cycle(ma)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,39 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
|
||||
)
|
||||
|
||||
// First returns the first item in an iterator if such an item exists
|
||||
// First returns the first element from an [Iterator] wrapped in an [Option].
|
||||
//
|
||||
// This function attempts to retrieve the first element from the iterator. If the iterator
|
||||
// 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.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the iterator
|
||||
//
|
||||
// Parameters:
|
||||
// - mu: The input iterator to get the first element from
|
||||
//
|
||||
// Returns:
|
||||
// - Option[U]: Some(first element) if the iterator is non-empty, None otherwise
|
||||
//
|
||||
// Example with non-empty iterator:
|
||||
//
|
||||
// iter := stateless.From(1, 2, 3, 4, 5)
|
||||
// first := stateless.First(iter)
|
||||
// // Returns: Some(1)
|
||||
//
|
||||
// Example with empty iterator:
|
||||
//
|
||||
// iter := stateless.Empty[int]()
|
||||
// first := stateless.First(iter)
|
||||
// // Returns: None
|
||||
//
|
||||
// Example with filtered iterator:
|
||||
//
|
||||
// iter := stateless.From(1, 2, 3, 4, 5)
|
||||
// filtered := stateless.Filter(func(x int) bool { return x > 3 })(iter)
|
||||
// first := stateless.First(filtered)
|
||||
// // Returns: Some(4)
|
||||
func First[U any](mu Iterator[U]) Option[U] {
|
||||
return G.First(mu)
|
||||
}
|
||||
|
||||
@@ -16,26 +16,240 @@
|
||||
package stateless
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestFirst tests getting the first element from a non-empty iterator
|
||||
func TestFirst(t *testing.T) {
|
||||
t.Run("returns first element from integer iterator", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
seq := From(1, 2, 3)
|
||||
t.Run("returns first element from string iterator", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of("a"), fst)
|
||||
})
|
||||
|
||||
fst := First(seq)
|
||||
t.Run("returns first element from single element iterator", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
t.Run("returns first element from large iterator", func(t *testing.T) {
|
||||
seq := From(100, 200, 300, 400, 500)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(100), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestNoFirst tests getting the first element from an empty iterator
|
||||
func TestNoFirst(t *testing.T) {
|
||||
t.Run("returns None for empty integer iterator", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
|
||||
seq := Empty[int]()
|
||||
t.Run("returns None for empty string iterator", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[string](), fst)
|
||||
})
|
||||
|
||||
fst := First(seq)
|
||||
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
t.Run("returns None for empty struct iterator", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Value int
|
||||
}
|
||||
seq := Empty[TestStruct]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[TestStruct](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithFiltered tests First with filtered iterators
|
||||
func TestFirstWithFiltered(t *testing.T) {
|
||||
t.Run("returns first element matching filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
filtered := Filter(N.MoreThan(3))(seq)
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.Of(4), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None when no elements match filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
filtered := Filter(N.MoreThan(10))(seq)
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first even number", func(t *testing.T) {
|
||||
seq := From(1, 3, 5, 6, 7, 8)
|
||||
filtered := Filter(func(x int) bool { return x%2 == 0 })(seq)
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.Of(6), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithMapped tests First with mapped iterators
|
||||
func TestFirstWithMapped(t *testing.T) {
|
||||
t.Run("returns first element after mapping", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := Map(N.Mul(2))(seq)
|
||||
fst := First(mapped)
|
||||
assert.Equal(t, O.Of(2), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first string after mapping", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := Map(S.Format[int]("num-%d"))(seq)
|
||||
fst := First(mapped)
|
||||
assert.Equal(t, O.Of("num-1"), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithTake tests First with Take
|
||||
func TestFirstWithTake(t *testing.T) {
|
||||
t.Run("returns first element from taken subset", func(t *testing.T) {
|
||||
seq := From(10, 20, 30, 40, 50)
|
||||
taken := Take[int](3)(seq)
|
||||
fst := First(taken)
|
||||
assert.Equal(t, O.Of(10), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None when taking zero elements", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
taken := Take[int](0)(seq)
|
||||
fst := First(taken)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithComplex tests First with complex types
|
||||
func TestFirstWithComplex(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("returns first person", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
fst := First(seq)
|
||||
expected := O.Of(Person{"Alice", 30})
|
||||
assert.Equal(t, expected, fst)
|
||||
})
|
||||
|
||||
t.Run("returns first pointer", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
seq := From(p1, p2)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(p1), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstDoesNotConsumeEntireIterator tests that First only consumes the first element
|
||||
func TestFirstDoesNotConsumeEntireIterator(t *testing.T) {
|
||||
t.Run("only consumes first element", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := Map(func(x int) int {
|
||||
callCount++
|
||||
return x * 2
|
||||
})(From(1, 2, 3, 4, 5))
|
||||
|
||||
fst := First(seq)
|
||||
|
||||
assert.Equal(t, O.Of(2), fst)
|
||||
// Should only have called the map function once for the first element
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithChainedOperations tests First with multiple chained operations
|
||||
func TestFirstWithChainedOperations(t *testing.T) {
|
||||
t.Run("chains filter, map, and first", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := First(
|
||||
Map(N.Mul(10))(
|
||||
Filter(N.MoreThan(5))(seq),
|
||||
),
|
||||
)
|
||||
assert.Equal(t, O.Of(60), result)
|
||||
})
|
||||
|
||||
t.Run("chains take, filter, and first", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := First(
|
||||
Filter(N.MoreThan(3))(
|
||||
Take[int](7)(seq),
|
||||
),
|
||||
)
|
||||
assert.Equal(t, O.Of(4), result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkFirst(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
First(seq)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFirstLargeIterator(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := FromArray(data)
|
||||
|
||||
for b.Loop() {
|
||||
First(seq)
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleFirst() {
|
||||
iter := From(1, 2, 3, 4, 5)
|
||||
first := First(iter)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First element: %d\n", value)
|
||||
}
|
||||
// Output: First element: 1
|
||||
}
|
||||
|
||||
func ExampleFirst_empty() {
|
||||
iter := Empty[int]()
|
||||
first := First(iter)
|
||||
|
||||
if _, ok := O.Unwrap(first); !ok {
|
||||
fmt.Println("Iterator is empty")
|
||||
}
|
||||
// Output: Iterator is empty
|
||||
}
|
||||
|
||||
func ExampleFirst_filtered() {
|
||||
iter := From(1, 2, 3, 4, 5)
|
||||
filtered := Filter(N.MoreThan(3))(iter)
|
||||
first := First(filtered)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First element > 3: %d\n", value)
|
||||
}
|
||||
// Output: First element > 3: 4
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func ChainReaderK[
|
||||
GEB ~func(E) ET.Either[L, B],
|
||||
GB ~func(E) B,
|
||||
L, E, A, B any](f func(A) GB) func(GEA) GEB {
|
||||
return Chain[GEA, GEB, L, E, A, B](F.Flow2(f, FromReader[GB, GEB, L, E, B]))
|
||||
return Chain[GEA](F.Flow2(f, FromReader[GB, GEB, L, E, B]))
|
||||
}
|
||||
|
||||
func Of[GEA ~func(E) ET.Either[L, A], L, E, A any](a A) GEA {
|
||||
|
||||
@@ -57,7 +57,7 @@ func TestLet(t *testing.T) {
|
||||
}
|
||||
result := F.Pipe1(
|
||||
input,
|
||||
Let[TestState, int, string, TestState](
|
||||
Let[TestState, int, string](
|
||||
func(length int) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Count = length
|
||||
@@ -83,7 +83,7 @@ func TestLetTo(t *testing.T) {
|
||||
}
|
||||
result := F.Pipe1(
|
||||
input,
|
||||
LetTo[TestState, int, string, TestState](
|
||||
LetTo[TestState, int, string](
|
||||
func(version int) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Version = version
|
||||
@@ -166,7 +166,7 @@ func TestBindChain(t *testing.T) {
|
||||
BindTo[TestState, string, string](func(name string) TestState {
|
||||
return TestState{Name: name}
|
||||
}),
|
||||
Let[TestState, int, string, TestState](
|
||||
Let[TestState, int, string](
|
||||
func(count int) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Count = count
|
||||
@@ -177,7 +177,7 @@ func TestBindChain(t *testing.T) {
|
||||
return len(s.Name)
|
||||
},
|
||||
),
|
||||
LetTo[TestState, int, string, TestState](
|
||||
LetTo[TestState, int, string](
|
||||
func(version int) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Version = version
|
||||
@@ -223,5 +223,3 @@ func TestBindWithDependentComputation(t *testing.T) {
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -205,7 +205,7 @@ func TestCollect(t *testing.T) {
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
}
|
||||
collector := Collect[string, int, string](func(k string, v int) string {
|
||||
collector := Collect(func(k string, v int) string {
|
||||
return fmt.Sprintf("%s=%d", k, v)
|
||||
})
|
||||
result := collector(data)
|
||||
@@ -232,7 +232,7 @@ func TestReduce(t *testing.T) {
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
}
|
||||
sum := Reduce[string, int, int](func(acc, v int) int {
|
||||
sum := Reduce[string](func(acc, v int) int {
|
||||
return acc + v
|
||||
}, 0)
|
||||
result := sum(data)
|
||||
@@ -245,7 +245,7 @@ func TestReduceWithIndex(t *testing.T) {
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
}
|
||||
concat := ReduceWithIndex[string, int, string](func(k string, acc string, v int) string {
|
||||
concat := ReduceWithIndex(func(k string, acc string, v int) string {
|
||||
if acc == "" {
|
||||
return fmt.Sprintf("%s:%d", k, v)
|
||||
}
|
||||
@@ -284,7 +284,7 @@ func TestMapWithIndex(t *testing.T) {
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
}
|
||||
mapper := MapWithIndex[string, int, string](func(k string, v int) string {
|
||||
mapper := MapWithIndex(func(k string, v int) string {
|
||||
return fmt.Sprintf("%s=%d", k, v)
|
||||
})
|
||||
result := mapper(data)
|
||||
@@ -367,7 +367,7 @@ func TestSingleton(t *testing.T) {
|
||||
|
||||
func TestFilterMapWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
filter := FilterMapWithIndex[string, int, int](func(k string, v int) O.Option[int] {
|
||||
filter := FilterMapWithIndex(func(k string, v int) O.Option[int] {
|
||||
if v%2 == 0 {
|
||||
return O.Some(v * 10)
|
||||
}
|
||||
@@ -379,7 +379,7 @@ func TestFilterMapWithIndex(t *testing.T) {
|
||||
|
||||
func TestFilterMap(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
filter := FilterMap[string, int, int](func(v int) O.Option[int] {
|
||||
filter := FilterMap[string](func(v int) O.Option[int] {
|
||||
if v%2 == 0 {
|
||||
return O.Some(v * 10)
|
||||
}
|
||||
@@ -400,7 +400,7 @@ func TestFilter(t *testing.T) {
|
||||
|
||||
func TestFilterWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
filter := FilterWithIndex[string, int](func(k string, v int) bool {
|
||||
filter := FilterWithIndex(func(k string, v int) bool {
|
||||
return v%2 == 0
|
||||
})
|
||||
result := filter(data)
|
||||
@@ -443,7 +443,7 @@ func TestMonadChain(t *testing.T) {
|
||||
func TestChain(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
monoid := MergeMonoid[string, int]()
|
||||
chain := Chain[int, string, int](monoid)(func(v int) map[string]int {
|
||||
chain := Chain[int](monoid)(func(v int) map[string]int {
|
||||
return map[string]int{
|
||||
fmt.Sprintf("x%d", v): v * 10,
|
||||
}
|
||||
@@ -466,7 +466,7 @@ func TestFlatten(t *testing.T) {
|
||||
func TestFoldMap(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
// Use string monoid for simplicity
|
||||
fold := FoldMap[string, int, string](S.Monoid)(func(v int) string {
|
||||
fold := FoldMap[string, int](S.Monoid)(func(v int) string {
|
||||
return fmt.Sprintf("%d", v)
|
||||
})
|
||||
result := fold(data)
|
||||
@@ -506,7 +506,7 @@ func TestFlap(t *testing.T) {
|
||||
"double": func(x int) int { return x * 2 },
|
||||
"triple": func(x int) int { return x * 3 },
|
||||
}
|
||||
flap := Flap[int, string, int](5)
|
||||
flap := Flap[int, string](5)
|
||||
result := flap(fns)
|
||||
assert.Equal(t, map[string]int{"double": 10, "triple": 15}, result)
|
||||
}
|
||||
@@ -518,7 +518,7 @@ func TestFromArray(t *testing.T) {
|
||||
P.MakePair("a", 3), // Duplicate key
|
||||
}
|
||||
// Use Second magma to keep last value
|
||||
from := FromArray[string, int](Mg.Second[int]())
|
||||
from := FromArray[string](Mg.Second[int]())
|
||||
result := from(entries)
|
||||
assert.Equal(t, map[string]int{"a": 3, "b": 2}, result)
|
||||
}
|
||||
@@ -543,7 +543,7 @@ func TestAp(t *testing.T) {
|
||||
"double": 5,
|
||||
}
|
||||
monoid := MergeMonoid[string, int]()
|
||||
ap := Ap[int, string, int](monoid)(vals)
|
||||
ap := Ap[int](monoid)(vals)
|
||||
result := ap(fns)
|
||||
assert.Equal(t, map[string]int{"double": 10}, result)
|
||||
}
|
||||
@@ -555,7 +555,7 @@ func TestOf(t *testing.T) {
|
||||
|
||||
func TestReduceRef(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
sum := ReduceRef[string, int, int](func(acc int, v *int) int {
|
||||
sum := ReduceRef[string](func(acc int, v *int) int {
|
||||
return acc + *v
|
||||
}, 0)
|
||||
result := sum(data)
|
||||
@@ -564,7 +564,7 @@ func TestReduceRef(t *testing.T) {
|
||||
|
||||
func TestReduceRefWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
concat := ReduceRefWithIndex[string, int, string](func(k string, acc string, v *int) string {
|
||||
concat := ReduceRefWithIndex(func(k string, acc string, v *int) string {
|
||||
if acc == "" {
|
||||
return fmt.Sprintf("%s:%d", k, *v)
|
||||
}
|
||||
@@ -583,7 +583,7 @@ func TestMonadMapRef(t *testing.T) {
|
||||
|
||||
func TestMapRef(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
mapper := MapRef[string, int, int](func(v *int) int { return *v * 2 })
|
||||
mapper := MapRef[string](func(v *int) int { return *v * 2 })
|
||||
result := mapper(data)
|
||||
assert.Equal(t, map[string]int{"a": 2, "b": 4}, result)
|
||||
}
|
||||
@@ -598,7 +598,7 @@ func TestMonadMapRefWithIndex(t *testing.T) {
|
||||
|
||||
func TestMapRefWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
mapper := MapRefWithIndex[string, int, string](func(k string, v *int) string {
|
||||
mapper := MapRefWithIndex(func(k string, v *int) string {
|
||||
return fmt.Sprintf("%s=%d", k, *v)
|
||||
})
|
||||
result := mapper(data)
|
||||
@@ -611,12 +611,12 @@ func TestUnion(t *testing.T) {
|
||||
// Union combines maps, with the magma resolving conflicts
|
||||
// The order is union(left)(right), which means right is merged into left
|
||||
// First magma keeps the first value (from right in this case)
|
||||
union := Union[string, int](Mg.First[int]())
|
||||
union := Union[string](Mg.First[int]())
|
||||
result := union(left)(right)
|
||||
assert.Equal(t, map[string]int{"a": 1, "b": 3, "c": 4}, result)
|
||||
|
||||
// Second magma keeps the second value (from left in this case)
|
||||
union2 := Union[string, int](Mg.Second[int]())
|
||||
union2 := Union[string](Mg.Second[int]())
|
||||
result2 := union2(left)(right)
|
||||
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 4}, result2)
|
||||
}
|
||||
@@ -635,7 +635,7 @@ func TestMonadChainWithIndex(t *testing.T) {
|
||||
func TestChainWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
monoid := MergeMonoid[string, string]()
|
||||
chain := ChainWithIndex[int, string, string](monoid)(func(k string, v int) map[string]string {
|
||||
chain := ChainWithIndex[int](monoid)(func(k string, v int) map[string]string {
|
||||
return map[string]string{
|
||||
fmt.Sprintf("%s%d", k, v): fmt.Sprintf("val%d", v),
|
||||
}
|
||||
@@ -671,7 +671,7 @@ func TestFilterChainWithIndex(t *testing.T) {
|
||||
|
||||
func TestFoldMapWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
fold := FoldMapWithIndex[string, int, string](S.Monoid)(func(k string, v int) string {
|
||||
fold := FoldMapWithIndex[string, int](S.Monoid)(func(k string, v int) string {
|
||||
return fmt.Sprintf("%s:%d", k, v)
|
||||
})
|
||||
result := fold(data)
|
||||
@@ -723,7 +723,7 @@ func TestFromFoldableMap(t *testing.T) {
|
||||
src := A.From("a", "b", "c", "a")
|
||||
// Create a reducer function
|
||||
reducer := A.Reduce[string, map[string]string]
|
||||
from := FromFoldableMap[func(func(map[string]string, string) map[string]string, map[string]string) func([]string) map[string]string, string, []string, string, string](
|
||||
from := FromFoldableMap(
|
||||
Mg.Second[string](),
|
||||
reducer,
|
||||
)
|
||||
@@ -743,7 +743,7 @@ func TestFromFoldable(t *testing.T) {
|
||||
P.MakePair("a", 3), // Duplicate key
|
||||
}
|
||||
reducer := A.Reduce[Entry[string, int], map[string]int]
|
||||
from := FromFoldable[[]Entry[string, int], func(func(map[string]int, Entry[string, int]) map[string]int, map[string]int) func([]Entry[string, int]) map[string]int, string, int](
|
||||
from := FromFoldable(
|
||||
Mg.Second[int](),
|
||||
reducer,
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
func TestUnionSemigroup(t *testing.T) {
|
||||
// Test with sum semigroup - values should be added for duplicate keys
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
mapSemigroup := UnionSemigroup[string, int](sumSemigroup)
|
||||
mapSemigroup := UnionSemigroup[string](sumSemigroup)
|
||||
|
||||
map1 := map[string]int{"a": 1, "b": 2}
|
||||
map2 := map[string]int{"b": 3, "c": 4}
|
||||
@@ -39,7 +39,7 @@ func TestUnionSemigroup(t *testing.T) {
|
||||
func TestUnionSemigroupString(t *testing.T) {
|
||||
// Test with string semigroup - strings should be concatenated
|
||||
stringSemigroup := S.Semigroup
|
||||
mapSemigroup := UnionSemigroup[string, string](stringSemigroup)
|
||||
mapSemigroup := UnionSemigroup[string](stringSemigroup)
|
||||
|
||||
map1 := map[string]string{"a": "Hello", "b": "World"}
|
||||
map2 := map[string]string{"b": "!", "c": "Goodbye"}
|
||||
@@ -52,7 +52,7 @@ func TestUnionSemigroupString(t *testing.T) {
|
||||
func TestUnionSemigroupProduct(t *testing.T) {
|
||||
// Test with product semigroup - values should be multiplied
|
||||
prodSemigroup := N.SemigroupProduct[int]()
|
||||
mapSemigroup := UnionSemigroup[string, int](prodSemigroup)
|
||||
mapSemigroup := UnionSemigroup[string](prodSemigroup)
|
||||
|
||||
map1 := map[string]int{"a": 2, "b": 3}
|
||||
map2 := map[string]int{"b": 4, "c": 5}
|
||||
@@ -65,7 +65,7 @@ func TestUnionSemigroupProduct(t *testing.T) {
|
||||
func TestUnionSemigroupEmpty(t *testing.T) {
|
||||
// Test with empty maps
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
mapSemigroup := UnionSemigroup[string, int](sumSemigroup)
|
||||
mapSemigroup := UnionSemigroup[string](sumSemigroup)
|
||||
|
||||
map1 := map[string]int{"a": 1}
|
||||
empty := map[string]int{}
|
||||
@@ -180,7 +180,7 @@ func TestUnionFirstSemigroupEmpty(t *testing.T) {
|
||||
// Test associativity law for UnionSemigroup
|
||||
func TestUnionSemigroupAssociativity(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
mapSemigroup := UnionSemigroup[string, int](sumSemigroup)
|
||||
mapSemigroup := UnionSemigroup[string](sumSemigroup)
|
||||
|
||||
map1 := map[string]int{"a": 1}
|
||||
map2 := map[string]int{"a": 2, "b": 3}
|
||||
@@ -225,5 +225,3 @@ func TestUnionFirstSemigroupAssociativity(t *testing.T) {
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
Reference in New Issue
Block a user