1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-03-14 13:42:48 +02:00

Compare commits

...

1 Commits

Author SHA1 Message Date
Dr. Carsten Leue
5d0f27ad10 fix: add SequenceSeq and TraverseSeq
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-12 20:38:52 +01:00
5 changed files with 718 additions and 69 deletions

View File

@@ -2,6 +2,20 @@
This document provides guidelines for AI agents working on the fp-go/v2 project.
## Table of Contents
- [Documentation Standards](#documentation-standards)
- [Go Doc Comments](#go-doc-comments)
- [File Headers](#file-headers)
- [Testing Standards](#testing-standards)
- [Test Structure](#test-structure)
- [Test Coverage](#test-coverage)
- [Example Test Pattern](#example-test-pattern)
- [Code Style](#code-style)
- [Functional Patterns](#functional-patterns)
- [Error Handling](#error-handling)
- [Checklist for New Code](#checklist-for-new-code)
## Documentation Standards
### Go Doc Comments
@@ -102,6 +116,50 @@ Always include the Apache 2.0 license header:
- Use `result.Of` for success values
- Use `result.Left` for error values
4. **Folding Either/Result Values in Tests**
- Use `F.Pipe1(result, Fold(onLeft, onRight))` — avoid the `_ = Fold(...)(result)` discard pattern
- Use `slices.Collect[T]` instead of a manual `for n := range seq { collected = append(...) }` loop
- Use `t.Fatal` in the unexpected branch to combine the `IsLeft`/`IsRight` check with value extraction:
```go
// Good: single fold combines assertion and extraction
collected := F.Pipe1(result, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
// Avoid: separate IsRight check + manual loop
assert.True(t, IsRight(result))
var collected []int
_ = MonadFold(result,
func(e error) []int { return nil },
func(seq iter.Seq[int]) []int {
for n := range seq { collected = append(collected, n) }
return collected
},
)
```
- Use `F.Identity[error]` as the Left branch when extracting an error value:
```go
err := F.Pipe1(result, Fold(
F.Identity[error],
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
))
```
- Extract repeated fold patterns as local helper closures within the test function:
```go
collectInts := func(r Result[iter.Seq[int]]) []int {
return F.Pipe1(r, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
}
```
5. **Other Test Style Details**
- Use `for i := range 10` instead of `for i := 0; i < 10; i++`
- Chain curried calls directly: `TraverseSeq(parse)(input)` — no need for an intermediate `traverseFn` variable
- Use direct slice literals (`[]string{"a", "b"}`) rather than `A.From("a", "b")` in tests
### Test Coverage
Include tests for:
@@ -168,56 +226,6 @@ func TestFromReaderResult_Success(t *testing.T) {
- Check error context is preserved
- Test error accumulation when applicable
## Common Patterns
### Converting Error-Based Functions
```go
// Good: Use Eitherize1
parseIntRR := result.Eitherize1(strconv.Atoi)
// Avoid: Manual error handling
parseIntRR := func(input string) result.Result[int] {
val, err := strconv.Atoi(input)
if err != nil {
return result.Left[int](err)
}
return result.Of(val)
}
```
### Testing Validation Results
```go
// Good: Direct comparison
assert.Equal(t, validation.Success(42), result)
// Avoid: Verbose extraction (unless you need to verify specific fields)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
```
### Documentation Examples
```go
// Good: Concise and idiomatic
// parseIntRR := result.Eitherize1(strconv.Atoi)
// validator := FromReaderResult[string, int](parseIntRR)
// Avoid: Verbose manual patterns
// parseIntRR := func(input string) result.Result[int] {
// val, err := strconv.Atoi(input)
// if err != nil {
// return result.Left[int](err)
// }
// return result.Of(val)
// }
```
## Checklist for New Code
- [ ] Apache 2.0 license header included

View File

@@ -16,6 +16,9 @@
package either
import (
"iter"
"slices"
F "github.com/IBM/fp-go/v2/function"
RA "github.com/IBM/fp-go/v2/internal/array"
)
@@ -178,3 +181,92 @@ func CompactArrayG[A1 ~[]Either[E, A], A2 ~[]A, E, A any](fa A1) A2 {
func CompactArray[E, A any](fa []Either[E, A]) []A {
return CompactArrayG[[]Either[E, A], []A](fa)
}
// TraverseSeq transforms an iterator by applying a function that returns an Either to each element.
// If any element produces a Left, the entire result is that Left (short-circuits).
// Otherwise, returns Right containing an iterator of all Right values.
//
// The function eagerly evaluates all elements in the input iterator to detect any Left values,
// then returns an iterator over the collected Right values. This is necessary because Either
// represents computations that can fail, and we need to know if any element failed before
// producing the result iterator.
//
// # Type Parameters
//
// - E: The error type for Left values
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - f: A function that transforms each element into an Either
//
// # Returns
//
// - A function that takes an iterator of A and returns Either containing an iterator of B
//
// # Example Usage
//
// parse := func(s string) either.Either[error, int] {
// v, err := strconv.Atoi(s)
// return either.FromError(v, err)
// }
// input := slices.Values([]string{"1", "2", "3"})
// result := either.TraverseSeq(parse)(input)
// // result is Right(iterator over [1, 2, 3])
//
// # See Also
//
// - TraverseArray: For slice-based traversal
// - SequenceSeq: For sequencing iterators of Either values
func TraverseSeq[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, iter.Seq[A], iter.Seq[B]] {
return func(ga iter.Seq[A]) Either[E, iter.Seq[B]] {
var bs []B
for a := range ga {
b := f(a)
if b.isLeft {
return Left[iter.Seq[B]](b.l)
}
bs = append(bs, b.r)
}
return Of[E](slices.Values(bs))
}
}
// SequenceSeq converts an iterator of Either into an Either of iterator.
// If any element is Left, returns that Left (short-circuits).
// Otherwise, returns Right containing an iterator of all the Right values.
//
// This function eagerly evaluates all Either values in the input iterator to detect
// any Left values, then returns an iterator over the collected Right values.
//
// # Type Parameters
//
// - E: The error type for Left values
// - A: The value type for Right values
//
// # Parameters
//
// - ma: An iterator of Either values
//
// # Returns
//
// - Either containing an iterator of Right values, or the first Left encountered
//
// # Example Usage
//
// eithers := slices.Values([]either.Either[error, int]{
// either.Right[error](1),
// either.Right[error](2),
// either.Right[error](3),
// })
// result := either.SequenceSeq(eithers)
// // result is Right(iterator over [1, 2, 3])
//
// # See Also
//
// - SequenceArray: For slice-based sequencing
// - TraverseSeq: For transforming and sequencing in one step
func SequenceSeq[E, A any](ma iter.Seq[Either[E, A]]) Either[E, iter.Seq[A]] {
return TraverseSeq(F.Identity[Either[E, A]])(ma)
}

View File

@@ -1,27 +1,28 @@
package either
import (
"errors"
"fmt"
"iter"
"slices"
"strconv"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
TST "github.com/IBM/fp-go/v2/internal/testing"
"github.com/stretchr/testify/assert"
)
func TestCompactArray(t *testing.T) {
ar := A.From(
ar := []Either[string, string]{
Of[string]("ok"),
Left[string]("err"),
Of[string]("ok"),
)
res := CompactArray(ar)
assert.Equal(t, 2, len(res))
}
assert.Equal(t, 2, len(CompactArray(ar)))
}
func TestSequenceArray(t *testing.T) {
s := TST.SequenceArrayTest(
FromStrictEquals[error, bool](),
Pointed[error, string](),
@@ -29,14 +30,12 @@ func TestSequenceArray(t *testing.T) {
Functor[error, []string, bool](),
SequenceArray[error, string],
)
for i := 0; i < 10; i++ {
for i := range 10 {
t.Run(fmt.Sprintf("TestSequenceArray %d", i), s(i))
}
}
func TestSequenceArrayError(t *testing.T) {
s := TST.SequenceArrayErrorTest(
FromStrictEquals[error, bool](),
Left[string, error],
@@ -46,6 +45,243 @@ func TestSequenceArrayError(t *testing.T) {
Functor[error, []string, bool](),
SequenceArray[error, string],
)
// run across four bits
s(4)(t)
}
func TestTraverseSeq_Success(t *testing.T) {
parse := func(s string) Either[error, int] {
v, err := strconv.Atoi(s)
return TryCatchError(v, err)
}
collectInts := func(result Either[error, iter.Seq[int]]) []int {
return F.Pipe1(result, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
}
t.Run("transforms all elements successfully", func(t *testing.T) {
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
assert.Equal(t, []int{1, 2, 3}, collectInts(result))
})
t.Run("works with empty iterator", func(t *testing.T) {
result := TraverseSeq(parse)(slices.Values([]string{}))
assert.Empty(t, collectInts(result))
})
t.Run("works with single element", func(t *testing.T) {
result := TraverseSeq(parse)(slices.Values([]string{"42"}))
assert.Equal(t, []int{42}, collectInts(result))
})
t.Run("preserves order of elements", func(t *testing.T) {
result := TraverseSeq(parse)(slices.Values([]string{"10", "20", "30", "40", "50"}))
assert.Equal(t, []int{10, 20, 30, 40, 50}, collectInts(result))
})
}
func TestTraverseSeq_Failure(t *testing.T) {
parse := func(s string) Either[error, int] {
v, err := strconv.Atoi(s)
return TryCatchError(v, err)
}
extractErr := func(result Either[error, iter.Seq[int]]) error {
return F.Pipe1(result, Fold(
F.Identity[error],
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
))
}
t.Run("short-circuits on first Left", func(t *testing.T) {
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "invalid", "3"})))
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})
t.Run("returns first error when multiple failures exist", func(t *testing.T) {
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "bad1", "bad2"})))
assert.Error(t, err)
assert.Contains(t, err.Error(), "bad1")
})
t.Run("handles custom error types", func(t *testing.T) {
customErr := errors.New("custom validation error")
validate := func(n int) Either[error, int] {
if n == 2 {
return Left[int](customErr)
}
return Right[error](n * 10)
}
err := extractErr(TraverseSeq(validate)(slices.Values([]int{1, 2, 3})))
assert.Equal(t, customErr, err)
})
}
func TestTraverseSeq_EdgeCases(t *testing.T) {
t.Run("handles complex transformations", func(t *testing.T) {
type User struct {
ID int
Name string
}
transform := func(id int) Either[error, User] {
return Right[error](User{ID: id, Name: fmt.Sprintf("User%d", id)})
}
result := TraverseSeq(transform)(slices.Values([]int{1, 2, 3}))
collected := F.Pipe1(result, Fold(
func(e error) []User { t.Fatal(e); return nil },
slices.Collect[User],
))
assert.Equal(t, []User{
{ID: 1, Name: "User1"},
{ID: 2, Name: "User2"},
{ID: 3, Name: "User3"},
}, collected)
})
t.Run("works with identity transformation", func(t *testing.T) {
input := slices.Values([]Either[error, int]{
Right[error](1),
Right[error](2),
Right[error](3),
})
result := TraverseSeq(F.Identity[Either[error, int]])(input)
collected := F.Pipe1(result, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
assert.Equal(t, []int{1, 2, 3}, collected)
})
}
func TestSequenceSeq_Success(t *testing.T) {
collectInts := func(result Either[error, iter.Seq[int]]) []int {
return F.Pipe1(result, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
}
t.Run("sequences multiple Right values", func(t *testing.T) {
input := slices.Values([]Either[error, int]{Right[error](1), Right[error](2), Right[error](3)})
assert.Equal(t, []int{1, 2, 3}, collectInts(SequenceSeq(input)))
})
t.Run("works with empty iterator", func(t *testing.T) {
input := slices.Values([]Either[error, string]{})
result := F.Pipe1(SequenceSeq(input), Fold(
func(e error) []string { t.Fatal(e); return nil },
slices.Collect[string],
))
assert.Empty(t, result)
})
t.Run("works with single Right value", func(t *testing.T) {
input := slices.Values([]Either[error, string]{Right[error]("hello")})
result := F.Pipe1(SequenceSeq(input), Fold(
func(e error) []string { t.Fatal(e); return nil },
slices.Collect[string],
))
assert.Equal(t, []string{"hello"}, result)
})
t.Run("preserves order of results", func(t *testing.T) {
input := slices.Values([]Either[error, int]{
Right[error](5), Right[error](4), Right[error](3), Right[error](2), Right[error](1),
})
assert.Equal(t, []int{5, 4, 3, 2, 1}, collectInts(SequenceSeq(input)))
})
t.Run("works with complex types", func(t *testing.T) {
type Item struct {
Value int
Label string
}
input := slices.Values([]Either[error, Item]{
Right[error](Item{Value: 1, Label: "first"}),
Right[error](Item{Value: 2, Label: "second"}),
Right[error](Item{Value: 3, Label: "third"}),
})
collected := F.Pipe1(SequenceSeq(input), Fold(
func(e error) []Item { t.Fatal(e); return nil },
slices.Collect[Item],
))
assert.Equal(t, []Item{
{Value: 1, Label: "first"},
{Value: 2, Label: "second"},
{Value: 3, Label: "third"},
}, collected)
})
}
func TestSequenceSeq_Failure(t *testing.T) {
extractErr := func(result Either[error, iter.Seq[int]]) error {
return F.Pipe1(result, Fold(
F.Identity[error],
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
))
}
t.Run("short-circuits on first Left", func(t *testing.T) {
testErr := errors.New("test error")
input := slices.Values([]Either[error, int]{Right[error](1), Left[int](testErr), Right[error](3)})
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
})
t.Run("returns first error when multiple Left values exist", func(t *testing.T) {
err1 := errors.New("error 1")
err2 := errors.New("error 2")
input := slices.Values([]Either[error, int]{Right[error](1), Left[int](err1), Left[int](err2)})
assert.Equal(t, err1, extractErr(SequenceSeq(input)))
})
t.Run("handles Left at the beginning", func(t *testing.T) {
testErr := errors.New("first error")
input := slices.Values([]Either[error, int]{Left[int](testErr), Right[error](2), Right[error](3)})
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
})
t.Run("handles Left at the end", func(t *testing.T) {
testErr := errors.New("last error")
input := slices.Values([]Either[error, int]{Right[error](1), Right[error](2), Left[int](testErr)})
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
})
}
func TestSequenceSeq_Integration(t *testing.T) {
t.Run("integrates with TraverseSeq", func(t *testing.T) {
parse := func(s string) Either[error, int] {
v, err := strconv.Atoi(s)
return TryCatchError(v, err)
}
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
assert.True(t, IsRight(result))
})
t.Run("SequenceSeq is equivalent to TraverseSeq with Identity", func(t *testing.T) {
mkInput := func() []Either[error, int] {
return []Either[error, int]{Right[error](10), Right[error](20), Right[error](30)}
}
collected1 := F.Pipe1(SequenceSeq(slices.Values(mkInput())), Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
collected2 := F.Pipe1(TraverseSeq(F.Identity[Either[error, int]])(slices.Values(mkInput())), Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
assert.Equal(t, collected1, collected2)
})
}

View File

@@ -16,6 +16,8 @@
package result
import (
"iter"
"github.com/IBM/fp-go/v2/either"
)
@@ -155,3 +157,84 @@ func CompactArrayG[A1 ~[]Result[A], A2 ~[]A, A any](fa A1) A2 {
func CompactArray[A any](fa []Result[A]) []A {
return either.CompactArray(fa)
}
// TraverseSeq transforms an iterator by applying a function that returns a Result to each element.
// If any element produces a Left, the entire result is that Left (short-circuits).
// Otherwise, returns Right containing an iterator of all Right values.
//
// The function eagerly evaluates all elements in the input iterator to detect any Left values,
// then returns an iterator over the collected Right values. This is necessary because Result
// represents computations that can fail, and we need to know if any element failed before
// producing the result iterator.
//
// # Type Parameters
//
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - f: A function that transforms each element into a Result
//
// # Returns
//
// - A function that takes an iterator of A and returns Result containing an iterator of B
//
// # Example Usage
//
// parse := func(s string) result.Result[int] {
// v, err := strconv.Atoi(s)
// return result.TryCatchError(v, err)
// }
// input := slices.Values([]string{"1", "2", "3"})
// result := result.TraverseSeq(parse)(input)
// // result is Right(iterator over [1, 2, 3])
//
// # See Also
//
// - TraverseArray: For slice-based traversal
// - SequenceSeq: For sequencing iterators of Result values
//
//go:inline
func TraverseSeq[A, B any](f Kleisli[A, B]) Kleisli[iter.Seq[A], iter.Seq[B]] {
return either.TraverseSeq(f)
}
// SequenceSeq converts an iterator of Result into a Result of iterator.
// If any element is Left, returns that Left (short-circuits).
// Otherwise, returns Right containing an iterator of all the Right values.
//
// This function eagerly evaluates all Result values in the input iterator to detect
// any Left values, then returns an iterator over the collected Right values.
//
// # Type Parameters
//
// - A: The value type for Right values
//
// # Parameters
//
// - ma: An iterator of Result values
//
// # Returns
//
// - Result containing an iterator of Right values, or the first Left encountered
//
// # Example Usage
//
// results := slices.Values([]result.Result[int]{
// result.Of(1),
// result.Of(2),
// result.Of(3),
// })
// result := result.SequenceSeq(results)
// // result is Right(iterator over [1, 2, 3])
//
// # See Also
//
// - SequenceArray: For slice-based sequencing
// - TraverseSeq: For transforming and sequencing in one step
//
//go:inline
func SequenceSeq[A any](ma iter.Seq[Result[A]]) Result[iter.Seq[A]] {
return either.SequenceSeq(ma)
}

View File

@@ -3,8 +3,12 @@ package result
import (
"errors"
"fmt"
"iter"
"slices"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
TST "github.com/IBM/fp-go/v2/internal/testing"
"github.com/stretchr/testify/assert"
)
@@ -15,13 +19,10 @@ func TestCompactArray(t *testing.T) {
Left[string](errors.New("err")),
Of("ok"),
}
res := CompactArray(ar)
assert.Equal(t, 2, len(res))
assert.Equal(t, 2, len(CompactArray(ar)))
}
func TestSequenceArray(t *testing.T) {
s := TST.SequenceArrayTest(
FromStrictEquals[bool](),
Pointed[string](),
@@ -29,14 +30,12 @@ func TestSequenceArray(t *testing.T) {
Functor[[]string, bool](),
SequenceArray[string],
)
for i := 0; i < 10; i++ {
for i := range 10 {
t.Run(fmt.Sprintf("TestSequenceArray %d", i), s(i))
}
}
func TestSequenceArrayError(t *testing.T) {
s := TST.SequenceArrayErrorTest(
FromStrictEquals[bool](),
Left[string],
@@ -46,6 +45,237 @@ func TestSequenceArrayError(t *testing.T) {
Functor[[]string, bool](),
SequenceArray[string],
)
// run across four bits
s(4)(t)
}
func TestTraverseSeq_Success(t *testing.T) {
parse := func(s string) Result[int] {
v, err := strconv.Atoi(s)
return TryCatchError(v, err)
}
collectInts := func(result Result[iter.Seq[int]]) []int {
return F.Pipe1(result, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
}
t.Run("transforms all elements successfully", func(t *testing.T) {
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
assert.Equal(t, []int{1, 2, 3}, collectInts(result))
})
t.Run("works with empty iterator", func(t *testing.T) {
result := TraverseSeq(parse)(slices.Values([]string{}))
assert.Empty(t, collectInts(result))
})
t.Run("works with single element", func(t *testing.T) {
result := TraverseSeq(parse)(slices.Values([]string{"42"}))
assert.Equal(t, []int{42}, collectInts(result))
})
t.Run("preserves order of elements", func(t *testing.T) {
result := TraverseSeq(parse)(slices.Values([]string{"10", "20", "30", "40", "50"}))
assert.Equal(t, []int{10, 20, 30, 40, 50}, collectInts(result))
})
}
func TestTraverseSeq_Failure(t *testing.T) {
parse := func(s string) Result[int] {
v, err := strconv.Atoi(s)
return TryCatchError(v, err)
}
extractErr := func(result Result[iter.Seq[int]]) error {
return F.Pipe1(result, Fold(
F.Identity[error],
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
))
}
t.Run("short-circuits on first Left", func(t *testing.T) {
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "invalid", "3"})))
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})
t.Run("returns first error when multiple failures exist", func(t *testing.T) {
err := extractErr(TraverseSeq(parse)(slices.Values([]string{"1", "bad1", "bad2"})))
assert.Error(t, err)
assert.Contains(t, err.Error(), "bad1")
})
t.Run("handles custom error types", func(t *testing.T) {
customErr := errors.New("custom validation error")
validate := func(n int) Result[int] {
if n == 2 {
return Left[int](customErr)
}
return Of(n * 10)
}
err := extractErr(TraverseSeq(validate)(slices.Values([]int{1, 2, 3})))
assert.Equal(t, customErr, err)
})
}
func TestTraverseSeq_EdgeCases(t *testing.T) {
t.Run("handles complex transformations", func(t *testing.T) {
type User struct {
ID int
Name string
}
transform := func(id int) Result[User] {
return Of(User{ID: id, Name: fmt.Sprintf("User%d", id)})
}
result := TraverseSeq(transform)(slices.Values([]int{1, 2, 3}))
collected := F.Pipe1(result, Fold(
func(e error) []User { t.Fatal(e); return nil },
slices.Collect[User],
))
assert.Equal(t, []User{
{ID: 1, Name: "User1"},
{ID: 2, Name: "User2"},
{ID: 3, Name: "User3"},
}, collected)
})
t.Run("works with identity transformation", func(t *testing.T) {
input := slices.Values([]Result[int]{Of(1), Of(2), Of(3)})
result := TraverseSeq(F.Identity[Result[int]])(input)
collected := F.Pipe1(result, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
assert.Equal(t, []int{1, 2, 3}, collected)
})
}
func TestSequenceSeq_Success(t *testing.T) {
collectInts := func(result Result[iter.Seq[int]]) []int {
return F.Pipe1(result, Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
}
t.Run("sequences multiple Right values", func(t *testing.T) {
input := slices.Values([]Result[int]{Of(1), Of(2), Of(3)})
assert.Equal(t, []int{1, 2, 3}, collectInts(SequenceSeq(input)))
})
t.Run("works with empty iterator", func(t *testing.T) {
input := slices.Values([]Result[string]{})
result := F.Pipe1(SequenceSeq(input), Fold(
func(e error) []string { t.Fatal(e); return nil },
slices.Collect[string],
))
assert.Empty(t, result)
})
t.Run("works with single Right value", func(t *testing.T) {
input := slices.Values([]Result[string]{Of("hello")})
result := F.Pipe1(SequenceSeq(input), Fold(
func(e error) []string { t.Fatal(e); return nil },
slices.Collect[string],
))
assert.Equal(t, []string{"hello"}, result)
})
t.Run("preserves order of results", func(t *testing.T) {
input := slices.Values([]Result[int]{Of(5), Of(4), Of(3), Of(2), Of(1)})
assert.Equal(t, []int{5, 4, 3, 2, 1}, collectInts(SequenceSeq(input)))
})
t.Run("works with complex types", func(t *testing.T) {
type Item struct {
Value int
Label string
}
input := slices.Values([]Result[Item]{
Of(Item{Value: 1, Label: "first"}),
Of(Item{Value: 2, Label: "second"}),
Of(Item{Value: 3, Label: "third"}),
})
collected := F.Pipe1(SequenceSeq(input), Fold(
func(e error) []Item { t.Fatal(e); return nil },
slices.Collect[Item],
))
assert.Equal(t, []Item{
{Value: 1, Label: "first"},
{Value: 2, Label: "second"},
{Value: 3, Label: "third"},
}, collected)
})
}
func TestSequenceSeq_Failure(t *testing.T) {
extractErr := func(result Result[iter.Seq[int]]) error {
return F.Pipe1(result, Fold(
F.Identity[error],
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
))
}
t.Run("short-circuits on first Left", func(t *testing.T) {
testErr := errors.New("test error")
input := slices.Values([]Result[int]{Of(1), Left[int](testErr), Of(3)})
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
})
t.Run("returns first error when multiple Left values exist", func(t *testing.T) {
err1 := errors.New("error 1")
err2 := errors.New("error 2")
input := slices.Values([]Result[int]{Of(1), Left[int](err1), Left[int](err2)})
assert.Equal(t, err1, extractErr(SequenceSeq(input)))
})
t.Run("handles Left at the beginning", func(t *testing.T) {
testErr := errors.New("first error")
input := slices.Values([]Result[int]{Left[int](testErr), Of(2), Of(3)})
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
})
t.Run("handles Left at the end", func(t *testing.T) {
testErr := errors.New("last error")
input := slices.Values([]Result[int]{Of(1), Of(2), Left[int](testErr)})
assert.Equal(t, testErr, extractErr(SequenceSeq(input)))
})
}
func TestSequenceSeq_Integration(t *testing.T) {
t.Run("integrates with TraverseSeq", func(t *testing.T) {
parse := func(s string) Result[int] {
v, err := strconv.Atoi(s)
return TryCatchError(v, err)
}
result := TraverseSeq(parse)(slices.Values([]string{"1", "2", "3"}))
assert.True(t, IsRight(result))
})
t.Run("SequenceSeq is equivalent to TraverseSeq with Identity", func(t *testing.T) {
mkInput := func() []Result[int] {
return []Result[int]{Of(10), Of(20), Of(30)}
}
collected1 := F.Pipe1(SequenceSeq(slices.Values(mkInput())), Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
collected2 := F.Pipe1(TraverseSeq(F.Identity[Result[int]])(slices.Values(mkInput())), Fold(
func(e error) []int { t.Fatal(e); return nil },
slices.Collect[int],
))
assert.Equal(t, collected1, collected2)
})
}