diff --git a/v2/internal/array/array.go b/v2/internal/array/array.go index d68da66..a454560 100644 --- a/v2/internal/array/array.go +++ b/v2/internal/array/array.go @@ -77,8 +77,7 @@ func IsNonNil[GA ~[]A, A any](as GA) bool { func Reduce[GA ~[]A, A, B any](fa GA, f func(B, A) B, initial B) B { current := initial - count := len(fa) - for i := 0; i < count; i++ { + for i := range len(fa) { current = f(current, fa[i]) } return current @@ -86,8 +85,7 @@ func Reduce[GA ~[]A, A, B any](fa GA, f func(B, A) B, initial B) B { func ReduceWithIndex[GA ~[]A, A, B any](fa GA, f func(int, B, A) B, initial B) B { current := initial - count := len(fa) - for i := 0; i < count; i++ { + for i := range len(fa) { current = f(i, current, fa[i]) } return current diff --git a/v2/internal/iter/iter.go b/v2/internal/iter/iter.go new file mode 100644 index 0000000..3993770 --- /dev/null +++ b/v2/internal/iter/iter.go @@ -0,0 +1,61 @@ +package iter + +import ( + F "github.com/IBM/fp-go/v2/function" +) + +func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(int, B, A) B, initial B) B { + current := initial + var i int + for a := range fa { + current = f(i, current, a) + i += 1 + } + return current +} + +func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B, initial B) B { + current := initial + for a := range fa { + current = f(current, a) + } + return current +} + +// Concat concatenates two sequences, yielding all elements from left followed by all elements from right. +func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT { + return func(yield func(T) bool) { + for t := range left { + if !yield(t) { + return + } + } + for t := range right { + if !yield(t) { + return + } + } + } +} + +func Of[GA ~func(yield func(A) bool), A any](a A) GA { + return func(yield func(A) bool) { + yield(a) + } +} + +func MonadAppend[GA ~func(yield func(A) bool), A any](f GA, tail A) GA { + return Concat(f, Of[GA](tail)) +} + +func Append[GA ~func(yield func(A) bool), A any](tail A) func(GA) GA { + return F.Bind2nd(Concat[GA], Of[GA](tail)) +} + +func Prepend[GA ~func(yield func(A) bool), A any](head A) func(GA) GA { + return F.Bind1st(Concat[GA], Of[GA](head)) +} + +func Empty[GA ~func(yield func(A) bool), A any]() GA { + return func(_ func(A) bool) {} +} diff --git a/v2/internal/iter/traverse.go b/v2/internal/iter/traverse.go new file mode 100644 index 0000000..a8b1333 --- /dev/null +++ b/v2/internal/iter/traverse.go @@ -0,0 +1,152 @@ +// 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" +) + +/* +* +We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces + +HKTRB = HKT +HKTB = HKT +HKTAB = HKT +*/ +func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKTB, HKTAB, HKTRB any]( + fof func(GB) HKTRB, + fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB, + fap func(HKTB) func(HKTAB) HKTRB, + + ta GA, + f func(A) HKTB) HKTRB { + return MonadTraverseReduce(fof, fmap, fap, ta, f, MonadAppend[GB, B], Empty[GB]()) +} + +/* +* +We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces + +HKTRB = HKT +HKTB = HKT +HKTAB = HKT +*/ +func MonadTraverseWithIndex[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKTB, HKTAB, HKTRB any]( + fof func(GB) HKTRB, + fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB, + fap func(HKTB) func(HKTAB) HKTRB, + + ta GA, + f func(int, A) HKTB) HKTRB { + return MonadTraverseReduceWithIndex(fof, fmap, fap, ta, f, MonadAppend[GB, B], Empty[GB]()) +} + +func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKTB, HKTAB, HKTRB any]( + fof func(GB) HKTRB, + fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB, + fap func(HKTB) func(HKTAB) HKTRB, + + f func(A) HKTB) func(GA) HKTRB { + + return func(ma GA) HKTRB { + return MonadTraverse(fof, fmap, fap, ma, f) + } +} + +func TraverseWithIndex[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKTB, HKTAB, HKTRB any]( + fof func(GB) HKTRB, + fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB, + fap func(HKTB) func(HKTAB) HKTRB, + + f func(int, A) HKTB) func(GA) HKTRB { + + return func(ma GA) HKTRB { + return MonadTraverseWithIndex(fof, fmap, fap, ma, f) + } +} + +func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any]( + fof func(GB) HKTRB, + fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB, + fap func(HKTB) func(HKTAB) HKTRB, + + ta GA, + + transform func(A) HKTB, + reduce func(GB, B) GB, + initial GB, +) HKTRB { + mmap := fmap(F.Curry2(reduce)) + + return MonadReduce(ta, func(r HKTRB, a A) HKTRB { + return F.Pipe2( + r, + mmap, + fap(transform(a)), + ) + }, fof(initial)) +} + +func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any]( + fof func(GB) HKTRB, + fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB, + fap func(HKTB) func(HKTAB) HKTRB, + + ta GA, + + transform func(int, A) HKTB, + reduce func(GB, B) GB, + initial GB, +) HKTRB { + mmap := fmap(F.Curry2(reduce)) + + return MonadReduceWithIndex(ta, func(idx int, r HKTRB, a A) HKTRB { + return F.Pipe2( + r, + mmap, + fap(transform(idx, a)), + ) + }, fof(initial)) +} + +func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any]( + fof func(GB) HKTRB, + fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB, + fap func(HKTB) func(HKTAB) HKTRB, + + transform func(A) HKTB, + reduce func(GB, B) GB, + initial GB, +) func(GA) HKTRB { + return func(ta GA) HKTRB { + return MonadTraverseReduce(fof, fmap, fap, ta, transform, reduce, initial) + } +} + +func TraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any]( + fof func(GB) HKTRB, + fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB, + fap func(HKTB) func(HKTAB) HKTRB, + + transform func(int, A) HKTB, + reduce func(GB, B) GB, + initial GB, +) func(GA) HKTRB { + return func(ta GA) HKTRB { + return MonadTraverseReduceWithIndex(fof, fmap, fap, ta, transform, reduce, initial) + } +} diff --git a/v2/internal/iter/types.go b/v2/internal/iter/types.go new file mode 100644 index 0000000..b393323 --- /dev/null +++ b/v2/internal/iter/types.go @@ -0,0 +1,9 @@ +package iter + +import ( + I "iter" +) + +type ( + Seq[A any] = I.Seq[A] +) diff --git a/v2/io/traverse.go b/v2/io/traverse.go index 2bff192..a4c49ba 100644 --- a/v2/io/traverse.go +++ b/v2/io/traverse.go @@ -18,6 +18,7 @@ package io import ( F "github.com/IBM/fp-go/v2/function" INTA "github.com/IBM/fp-go/v2/internal/array" + INTI "github.com/IBM/fp-go/v2/internal/iter" INTR "github.com/IBM/fp-go/v2/internal/record" ) @@ -60,6 +61,16 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] { ) } +func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] { + return INTI.Traverse[Seq[A]]( + Of[Seq[B]], + Map[Seq[B], func(B) Seq[B]], + Ap[Seq[B], B], + + f, + ) +} + // TraverseArrayWithIndex is like TraverseArray but the function also receives the index. // Executes in parallel by default. // diff --git a/v2/io/types.go b/v2/io/types.go new file mode 100644 index 0000000..47bbffb --- /dev/null +++ b/v2/io/types.go @@ -0,0 +1,7 @@ +package io + +import "iter" + +type ( + Seq[T any] = iter.Seq[T] +) diff --git a/v2/iterator/iter/iter.go b/v2/iterator/iter/iter.go index a2e602e..422d4ff 100644 --- a/v2/iterator/iter/iter.go +++ b/v2/iterator/iter/iter.go @@ -48,6 +48,7 @@ import ( F "github.com/IBM/fp-go/v2/function" "github.com/IBM/fp-go/v2/internal/functor" + G "github.com/IBM/fp-go/v2/internal/iter" M "github.com/IBM/fp-go/v2/monoid" "github.com/IBM/fp-go/v2/option" ) @@ -58,10 +59,10 @@ import ( // // seq := Of(42) // // yields: 42 +// +//go:inline func Of[A any](a A) Seq[A] { - return func(yield Predicate[A]) { - yield(a) - } + return G.Of[Seq[A]](a) } // Of2 creates a key-value sequence containing a single key-value pair. @@ -521,7 +522,7 @@ func From[A any](data ...A) Seq[A] { // //go:inline func Empty[A any]() Seq[A] { - return func(_ Predicate[A]) {} + return G.Empty[Seq[A]]() } // MakeBy creates a sequence of n elements by applying a function to each index. @@ -566,12 +567,10 @@ func Replicate[A any](n int, a A) Seq[A] { // seq := From(1, 2, 3, 4, 5) // sum := MonadReduce(seq, func(acc, x int) int { return acc + x }, 0) // // returns: 15 +// +//go:inline func MonadReduce[A, B any](fa Seq[A], f func(B, A) B, initial B) B { - current := initial - for a := range fa { - current = f(current, a) - } - return current + return G.MonadReduce(fa, f, initial) } // Reduce returns a function that reduces a sequence to a single value. @@ -598,14 +597,10 @@ func Reduce[A, B any](f func(B, A) B, initial B) func(Seq[A]) B { // return acc + (i * x) // }, 0) // // returns: 0*10 + 1*20 + 2*30 = 80 +// +//go:inline func MonadReduceWithIndex[A, B any](fa Seq[A], f func(int, B, A) B, initial B) B { - current := initial - var i int - for a := range fa { - current = f(i, current, a) - i += 1 - } - return current + return G.MonadReduceWithIndex(fa, f, initial) } // ReduceWithIndex returns a function that reduces with index. @@ -831,7 +826,7 @@ func Flap[B, A any](a A) Operator[func(A) B, B] { // //go:inline func Prepend[A any](head A) Operator[A, A] { - return F.Bind1st(concat[A], Of(head)) + return G.Prepend[Seq[A]](head) } // Append returns a function that adds an element to the end of a sequence. @@ -844,7 +839,7 @@ func Prepend[A any](head A) Operator[A, A] { // //go:inline func Append[A any](tail A) Operator[A, A] { - return F.Bind2nd(concat[A], Of(tail)) + return G.Append[Seq[A]](tail) } // MonadZip combines two sequences into a sequence of pairs. diff --git a/v2/iterator/iter/monid.go b/v2/iterator/iter/monid.go index 7a0a88e..1dbc1a1 100644 --- a/v2/iterator/iter/monid.go +++ b/v2/iterator/iter/monid.go @@ -16,25 +16,10 @@ package iter import ( + G "github.com/IBM/fp-go/v2/internal/iter" M "github.com/IBM/fp-go/v2/monoid" ) -// concat concatenates two sequences, yielding all elements from left followed by all elements from right. -func concat[T any](left, right Seq[T]) Seq[T] { - return func(yield Predicate[T]) { - for t := range left { - if !yield(t) { - return - } - } - for t := range right { - if !yield(t) { - return - } - } - } -} - // Monoid returns a Monoid instance for Seq[T]. // The monoid's concat operation concatenates sequences, and the empty value is an empty sequence. // @@ -48,5 +33,5 @@ func concat[T any](left, right Seq[T]) Seq[T] { // //go:inline func Monoid[T any]() M.Monoid[Seq[T]] { - return M.MakeMonoid(concat[T], Empty[T]()) + return M.MakeMonoid(G.Concat[Seq[T]], Empty[T]()) } diff --git a/v2/optics/traversal/either/traversal.go b/v2/optics/traversal/either/traversal.go index 631f688..58c2efa 100644 --- a/v2/optics/traversal/either/traversal.go +++ b/v2/optics/traversal/either/traversal.go @@ -21,7 +21,7 @@ import ( ) type ( - Traversal[E, S, A any] T.Traversal[S, A, ET.Either[E, S], ET.Either[E, A]] + Traversal[E, S, A any] = T.Traversal[S, A, ET.Either[E, S], ET.Either[E, A]] ) func Compose[ diff --git a/v2/optics/traversal/generic/traversal.go b/v2/optics/traversal/generic/traversal.go index 97f6635..57f5591 100644 --- a/v2/optics/traversal/generic/traversal.go +++ b/v2/optics/traversal/generic/traversal.go @@ -22,7 +22,7 @@ import ( ) type ( - Traversal[S, A, HKTS, HKTA any] func(func(A) HKTA) func(S) HKTS + Traversal[S, A, HKTS, HKTA any] = func(func(A) HKTA) func(S) HKTS ) func Compose[ diff --git a/v2/optics/traversal/result/traversal.go b/v2/optics/traversal/result/traversal.go new file mode 100644 index 0000000..9f7843f --- /dev/null +++ b/v2/optics/traversal/result/traversal.go @@ -0,0 +1,29 @@ +// 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 result + +import ( + T "github.com/IBM/fp-go/v2/optics/traversal/generic" +) + +func Compose[ + S, A, B any](ab Traversal[A, B]) Operator[S, A, B] { + return T.Compose[ + Traversal[A, B], + Traversal[S, A], + Traversal[S, B], + ](ab) +} diff --git a/v2/optics/traversal/result/types.go b/v2/optics/traversal/result/types.go new file mode 100644 index 0000000..2047179 --- /dev/null +++ b/v2/optics/traversal/result/types.go @@ -0,0 +1,12 @@ +package result + +import ( + T "github.com/IBM/fp-go/v2/optics/traversal/generic" + "github.com/IBM/fp-go/v2/result" +) + +type ( + Traversal[S, A any] = T.Traversal[S, A, Result[S], Result[A]] + Result[T any] = result.Result[T] + Operator[S, A, B any] = func(Traversal[S, A]) Traversal[S, B] +) diff --git a/v2/option/iter.go b/v2/option/iter.go new file mode 100644 index 0000000..409dd9d --- /dev/null +++ b/v2/option/iter.go @@ -0,0 +1,65 @@ +// 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 option + +import ( + INTI "github.com/IBM/fp-go/v2/internal/iter" +) + +// TraverseIter transforms a sequence by applying a function that returns an Option to each element. +// Returns Some containing a sequence of results if all operations succeed, None if any fails. +// This function is useful for processing sequences where each element may fail validation or transformation. +// +// The traversal short-circuits on the first None encountered, making it efficient for validation pipelines. +// The resulting sequence is lazy and will only be evaluated when iterated. +// +// Example: +// +// // Parse a sequence of strings to integers +// parse := func(s string) Option[int] { +// n, err := strconv.Atoi(s) +// if err != nil { return None[int]() } +// return Some(n) +// } +// +// // Create a sequence of strings +// strings := func(yield func(string) bool) { +// for _, s := range []string{"1", "2", "3"} { +// if !yield(s) { return } +// } +// } +// +// result := TraverseIter(parse)(strings) +// // result is Some(sequence of [1, 2, 3]) +// +// // With invalid input +// invalidStrings := func(yield func(string) bool) { +// for _, s := range []string{"1", "invalid", "3"} { +// if !yield(s) { return } +// } +// } +// +// result := TraverseIter(parse)(invalidStrings) +// // result is None because "invalid" cannot be parsed +func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] { + return INTI.Traverse[Seq[A]]( + Of[Seq[B]], + Map[Seq[B], func(B) Seq[B]], + Ap[Seq[B]], + + f, + ) +} diff --git a/v2/option/iter_test.go b/v2/option/iter_test.go new file mode 100644 index 0000000..056aae8 --- /dev/null +++ b/v2/option/iter_test.go @@ -0,0 +1,329 @@ +// 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 option + +import ( + "fmt" + "slices" + "strconv" + "testing" + + F "github.com/IBM/fp-go/v2/function" + "github.com/stretchr/testify/assert" +) + +// Helper function to create a sequence from a slice +func seqFromSlice[T any](items []T) Seq[T] { + return slices.Values(items) +} + +// Helper function to collect a sequence into a slice +func collectSeq[T any](seq Seq[T]) []T { + return slices.Collect(seq) +} + +func TestTraverseIter_AllSome(t *testing.T) { + // Test case where all transformations succeed + parse := func(s string) Option[int] { + n, err := strconv.Atoi(s) + if err != nil { + return None[int]() + } + return Some(n) + } + + input := seqFromSlice([]string{"1", "2", "3", "4", "5"}) + result := TraverseIter(parse)(input) + + assert.True(t, IsSome(result), "Expected Some result when all transformations succeed") + + collected := MonadFold(result, func() []int { return nil }, collectSeq[int]) + expected := []int{1, 2, 3, 4, 5} + assert.Equal(t, expected, collected) +} + +func TestTraverseIter_ContainsNone(t *testing.T) { + // Test case where one transformation fails + parse := func(s string) Option[int] { + n, err := strconv.Atoi(s) + if err != nil { + return None[int]() + } + return Some(n) + } + + input := seqFromSlice([]string{"1", "invalid", "3"}) + result := TraverseIter(parse)(input) + + assert.True(t, IsNone(result), "Expected None when any transformation fails") +} + +func TestTraverseIter_EmptySequence(t *testing.T) { + // Test with empty sequence + double := func(x int) Option[int] { + return Some(x * 2) + } + + input := seqFromSlice([]int{}) + result := TraverseIter(double)(input) + + assert.True(t, IsSome(result), "Expected Some for empty sequence") + + collected := MonadFold(result, func() []int { return nil }, collectSeq[int]) + assert.Empty(t, collected) +} + +func TestTraverseIter_SingleElement(t *testing.T) { + // Test with single element - success case + validate := func(x int) Option[int] { + if x > 0 { + return Some(x * 2) + } + return None[int]() + } + + input := seqFromSlice([]int{5}) + result := TraverseIter(validate)(input) + + assert.True(t, IsSome(result)) + collected := MonadFold(result, func() []int { return nil }, collectSeq[int]) + assert.Equal(t, []int{10}, collected) +} + +func TestTraverseIter_SingleElementFails(t *testing.T) { + // Test with single element - failure case + validate := func(x int) Option[int] { + if x > 0 { + return Some(x * 2) + } + return None[int]() + } + + input := seqFromSlice([]int{-5}) + result := TraverseIter(validate)(input) + + assert.True(t, IsNone(result)) +} + +func TestTraverseIter_Validation(t *testing.T) { + // Test validation use case + validatePositive := func(x int) Option[int] { + if x > 0 { + return Some(x) + } + return None[int]() + } + + // All positive + input1 := seqFromSlice([]int{1, 2, 3, 4}) + result1 := TraverseIter(validatePositive)(input1) + assert.True(t, IsSome(result1)) + + // Contains negative + input2 := seqFromSlice([]int{1, -2, 3}) + result2 := TraverseIter(validatePositive)(input2) + assert.True(t, IsNone(result2)) + + // Contains zero + input3 := seqFromSlice([]int{1, 0, 3}) + result3 := TraverseIter(validatePositive)(input3) + assert.True(t, IsNone(result3)) +} + +func TestTraverseIter_Transformation(t *testing.T) { + // Test transformation use case + safeDivide := func(x int) Option[float64] { + if x != 0 { + return Some(100.0 / float64(x)) + } + return None[float64]() + } + + // All non-zero + input1 := seqFromSlice([]int{1, 2, 4, 5}) + result1 := TraverseIter(safeDivide)(input1) + assert.True(t, IsSome(result1)) + + collected := MonadFold(result1, func() []float64 { return nil }, collectSeq[float64]) + expected := []float64{100.0, 50.0, 25.0, 20.0} + assert.Equal(t, expected, collected) + + // Contains zero + input2 := seqFromSlice([]int{1, 0, 4}) + result2 := TraverseIter(safeDivide)(input2) + assert.True(t, IsNone(result2)) +} + +func TestTraverseIter_ShortCircuit(t *testing.T) { + // Test that traversal short-circuits on first None + callCount := 0 + countingFunc := func(x int) Option[int] { + callCount++ + if x < 0 { + return None[int]() + } + return Some(x * 2) + } + + // First element fails + input := seqFromSlice([]int{-1, 2, 3, 4, 5}) + result := TraverseIter(countingFunc)(input) + + assert.True(t, IsNone(result)) + // Should have called the function for elements until the first failure + // Note: The exact count depends on implementation details of the traverse function + assert.Greater(t, callCount, 0, "Function should be called at least once") +} + +func TestTraverseIter_LazyEvaluation(t *testing.T) { + // Test that the result sequence is lazy + transform := func(x int) Option[int] { + return Some(x * 2) + } + + input := seqFromSlice([]int{1, 2, 3, 4, 5}) + result := TraverseIter(transform)(input) + + assert.True(t, IsSome(result)) + + // Partially consume the sequence + callCount := 0 + MonadFold(result, func() int { return 0 }, func(seq Seq[int]) int { + for val := range seq { + callCount++ + _ = val + if callCount == 2 { + break + } + } + return callCount + }) + + assert.Equal(t, 2, callCount, "Should only evaluate consumed elements") +} + +func TestTraverseIter_ComplexTransformation(t *testing.T) { + // Test with more complex transformation + type Person struct { + Name string + Age int + } + + validatePerson := func(name string) Option[Person] { + if name == "" { + return None[Person]() + } + return Some(Person{Name: name, Age: len(name)}) + } + + input := seqFromSlice([]string{"Alice", "Bob", "Charlie"}) + result := TraverseIter(validatePerson)(input) + + assert.True(t, IsSome(result)) + + collected := MonadFold(result, func() []Person { return nil }, collectSeq[Person]) + expected := []Person{ + {Name: "Alice", Age: 5}, + {Name: "Bob", Age: 3}, + {Name: "Charlie", Age: 7}, + } + assert.Equal(t, expected, collected) +} + +func TestTraverseIter_WithPipeline(t *testing.T) { + // Test TraverseIter in a functional pipeline + parse := func(s string) Option[int] { + n, err := strconv.Atoi(s) + if err != nil { + return None[int]() + } + return Some(n) + } + + input := seqFromSlice([]string{"1", "2", "3", "4", "5"}) + + result := F.Pipe2( + input, + TraverseIter(parse), + Map(collectSeq[int]), + ) + + collected := MonadFold(result, func() []int { return nil }, F.Identity[[]int]) + expected := []int{1, 2, 3, 4, 5} + assert.Equal(t, expected, collected) +} + +func TestTraverseIter_ChainedTransformations(t *testing.T) { + // Test chaining multiple transformations + parseAndValidate := func(s string) Option[int] { + n, err := strconv.Atoi(s) + if err != nil { + return None[int]() + } + if n > 0 { + return Some(n) + } + return None[int]() + } + + // All valid + input1 := seqFromSlice([]string{"1", "2", "3"}) + result1 := TraverseIter(parseAndValidate)(input1) + assert.True(t, IsSome(result1)) + + // Contains invalid number + input2 := seqFromSlice([]string{"1", "invalid", "3"}) + result2 := TraverseIter(parseAndValidate)(input2) + assert.True(t, IsNone(result2)) + + // Contains non-positive number + input3 := seqFromSlice([]string{"1", "0", "3"}) + result3 := TraverseIter(parseAndValidate)(input3) + assert.True(t, IsNone(result3)) +} + +// Example test demonstrating usage +func ExampleTraverseIter() { + // Parse a sequence of strings to integers + parse := func(s string) Option[int] { + n, err := strconv.Atoi(s) + if err != nil { + return None[int]() + } + return Some(n) + } + + // Create a sequence of valid strings + validStrings := seqFromSlice([]string{"1", "2", "3"}) + result := TraverseIter(parse)(validStrings) + + if IsSome(result) { + numbers := MonadFold(result, func() []int { return nil }, collectSeq[int]) + fmt.Println(numbers) + } + + // Create a sequence with invalid string + invalidStrings := seqFromSlice([]string{"1", "invalid", "3"}) + result2 := TraverseIter(parse)(invalidStrings) + + if IsNone(result2) { + fmt.Println("Parsing failed") + } + + // Output: + // [1 2 3] + // Parsing failed +} diff --git a/v2/option/types.go b/v2/option/types.go new file mode 100644 index 0000000..75f9c9f --- /dev/null +++ b/v2/option/types.go @@ -0,0 +1,7 @@ +package option + +import "iter" + +type ( + Seq[T any] = iter.Seq[T] +)