mirror of
https://github.com/IBM/fp-go.git
synced 2026-04-22 20:33:06 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b288003f93 | |||
| aed19f6edf | |||
| 45cc0a7fc1 | |||
| 21b517d388 | |||
| 0df62c0031 | |||
| 57318e2d1d | |||
| 2b937d3e93 | |||
| 747a1794e5 | |||
| c754cacf1f | |||
| d357b32847 | |||
| a3af003e74 | |||
| c81235827b | |||
| f35430cf18 | |||
| d3ffc71808 | |||
| 62844b7030 | |||
| 99a0ddd4b6 | |||
| 02acbae8f6 | |||
| eb27ecdc01 | |||
| e5eb7d343c | |||
| d5a3217251 | |||
| c5cbdaad68 | |||
| 5d0f27ad10 | |||
| 3a954e0d1f |
+66
-59
@@ -2,14 +2,30 @@
|
||||
|
||||
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
|
||||
|
||||
1. **Use Standard Go Doc Format**
|
||||
- Do NOT use markdown-style links like `[text](url)`
|
||||
- Do NOT use markdown-style headers like `# Section` or `## Subsection`
|
||||
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
|
||||
- Go's documentation system will automatically create links
|
||||
- Use plain text with blank lines to separate sections
|
||||
|
||||
2. **Structure**
|
||||
```go
|
||||
@@ -17,24 +33,20 @@ This document provides guidelines for AI agents working on the fp-go/v2 project.
|
||||
//
|
||||
// Longer description explaining the purpose and behavior.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: Description of type parameter
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// Parameters:
|
||||
// - param: Description of parameter
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// Returns:
|
||||
// - ReturnType: Description of return value
|
||||
//
|
||||
// # Example Usage
|
||||
// Example:
|
||||
//
|
||||
// code example here
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// See Also:
|
||||
// - RelatedFunction: Brief description
|
||||
func FunctionName[T any](param T) ReturnType {
|
||||
```
|
||||
@@ -43,6 +55,7 @@ This document provides guidelines for AI agents working on the fp-go/v2 project.
|
||||
- Use idiomatic Go patterns
|
||||
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
|
||||
- Show realistic, runnable examples
|
||||
- Indent code examples with spaces (not tabs) for proper godoc rendering
|
||||
|
||||
### File Headers
|
||||
|
||||
@@ -102,6 +115,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 +225,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
|
||||
|
||||
+725
-49
File diff suppressed because it is too large
Load Diff
+287
-3
@@ -198,11 +198,228 @@ func TestFilterMap(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFoldMap(t *testing.T) {
|
||||
src := From("a", "b", "c")
|
||||
t.Run("FoldMap with 0 items", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
|
||||
result := foldMap(empty)
|
||||
assert.Equal(t, 0, result, "FoldMap should return monoid empty for 0 items")
|
||||
})
|
||||
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
t.Run("FoldMap with 1 item", func(t *testing.T) {
|
||||
single := From(5)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
|
||||
result := foldMap(single)
|
||||
assert.Equal(t, 10, result, "FoldMap should map and return single item")
|
||||
})
|
||||
|
||||
assert.Equal(t, "ABC", fold(src))
|
||||
t.Run("FoldMap with 2 items", func(t *testing.T) {
|
||||
two := From(3, 4)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
|
||||
result := foldMap(two)
|
||||
assert.Equal(t, 14, result, "FoldMap should map and fold 2 items: (3*2) + (4*2) = 14")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with many items", func(t *testing.T) {
|
||||
many := From(1, 2, 3, 4, 5)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMap[int](sumMonoid)(N.Mul(2))
|
||||
result := foldMap(many)
|
||||
assert.Equal(t, 30, result, "FoldMap should map and fold many items: (1*2) + (2*2) + (3*2) + (4*2) + (5*2) = 30")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation - 0 items", func(t *testing.T) {
|
||||
empty := []string{}
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
result := fold(empty)
|
||||
assert.Equal(t, "", result, "FoldMap should return empty string for 0 items")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation - 1 item", func(t *testing.T) {
|
||||
single := From("a")
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
result := fold(single)
|
||||
assert.Equal(t, "A", result, "FoldMap should map single string")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation - 2 items", func(t *testing.T) {
|
||||
two := From("a", "b")
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
result := fold(two)
|
||||
assert.Equal(t, "AB", result, "FoldMap should map and concatenate 2 strings")
|
||||
})
|
||||
|
||||
t.Run("FoldMap with string concatenation - many items", func(t *testing.T) {
|
||||
many := From("a", "b", "c", "d", "e")
|
||||
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
|
||||
result := fold(many)
|
||||
assert.Equal(t, "ABCDE", result, "FoldMap should map and concatenate many strings")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
t.Run("Fold with 0 items", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
fold := Fold[int](sumMonoid)
|
||||
result := fold(empty)
|
||||
assert.Equal(t, 0, result, "Fold should return monoid empty for 0 items")
|
||||
})
|
||||
|
||||
t.Run("Fold with 1 item", func(t *testing.T) {
|
||||
single := From(42)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
fold := Fold[int](sumMonoid)
|
||||
result := fold(single)
|
||||
assert.Equal(t, 42, result, "Fold should return single item")
|
||||
})
|
||||
|
||||
t.Run("Fold with 2 items", func(t *testing.T) {
|
||||
two := From(10, 20)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
fold := Fold[int](sumMonoid)
|
||||
result := fold(two)
|
||||
assert.Equal(t, 30, result, "Fold should combine 2 items: 10 + 20 = 30")
|
||||
})
|
||||
|
||||
t.Run("Fold with many items", func(t *testing.T) {
|
||||
many := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
fold := Fold[int](sumMonoid)
|
||||
result := fold(many)
|
||||
assert.Equal(t, 55, result, "Fold should combine many items: 1+2+3+4+5+6+7+8+9+10 = 55")
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation - 0 items", func(t *testing.T) {
|
||||
empty := []string{}
|
||||
fold := Fold[string](S.Monoid)
|
||||
result := fold(empty)
|
||||
assert.Equal(t, "", result, "Fold should return empty string for 0 items")
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation - 1 item", func(t *testing.T) {
|
||||
single := From("hello")
|
||||
fold := Fold[string](S.Monoid)
|
||||
result := fold(single)
|
||||
assert.Equal(t, "hello", result, "Fold should return single string")
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation - 2 items", func(t *testing.T) {
|
||||
two := From("hello", "world")
|
||||
fold := Fold[string](S.Monoid)
|
||||
result := fold(two)
|
||||
assert.Equal(t, "helloworld", result, "Fold should concatenate 2 strings")
|
||||
})
|
||||
|
||||
t.Run("Fold with string concatenation - many items", func(t *testing.T) {
|
||||
many := From("a", "b", "c", "d", "e", "f")
|
||||
fold := Fold[string](S.Monoid)
|
||||
result := fold(many)
|
||||
assert.Equal(t, "abcdef", result, "Fold should concatenate many strings")
|
||||
})
|
||||
|
||||
t.Run("Fold with product monoid - 0 items", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
productMonoid := N.MonoidProduct[int]()
|
||||
fold := Fold[int](productMonoid)
|
||||
result := fold(empty)
|
||||
assert.Equal(t, 1, result, "Fold should return monoid empty (1) for product with 0 items")
|
||||
})
|
||||
|
||||
t.Run("Fold with product monoid - 1 item", func(t *testing.T) {
|
||||
single := From(7)
|
||||
productMonoid := N.MonoidProduct[int]()
|
||||
fold := Fold[int](productMonoid)
|
||||
result := fold(single)
|
||||
assert.Equal(t, 7, result, "Fold should return single item for product")
|
||||
})
|
||||
|
||||
t.Run("Fold with product monoid - 2 items", func(t *testing.T) {
|
||||
two := From(3, 4)
|
||||
productMonoid := N.MonoidProduct[int]()
|
||||
fold := Fold[int](productMonoid)
|
||||
result := fold(two)
|
||||
assert.Equal(t, 12, result, "Fold should multiply 2 items: 3 * 4 = 12")
|
||||
})
|
||||
|
||||
t.Run("Fold with product monoid - many items", func(t *testing.T) {
|
||||
many := From(2, 3, 4, 5)
|
||||
productMonoid := N.MonoidProduct[int]()
|
||||
fold := Fold[int](productMonoid)
|
||||
result := fold(many)
|
||||
assert.Equal(t, 120, result, "Fold should multiply many items: 2*3*4*5 = 120")
|
||||
})
|
||||
}
|
||||
func TestFoldMapWithIndex(t *testing.T) {
|
||||
t.Run("FoldMapWithIndex with 0 items", func(t *testing.T) {
|
||||
empty := []int{}
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
|
||||
result := foldMap(empty)
|
||||
assert.Equal(t, 0, result, "FoldMapWithIndex should return monoid empty for 0 items")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with 1 item", func(t *testing.T) {
|
||||
single := From(10)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
|
||||
result := foldMap(single)
|
||||
assert.Equal(t, 10, result, "FoldMapWithIndex should map with index: 0 + 10 = 10")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with 2 items", func(t *testing.T) {
|
||||
two := From(10, 20)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i + x })
|
||||
result := foldMap(two)
|
||||
assert.Equal(t, 31, result, "FoldMapWithIndex should map with indices: (0+10) + (1+20) = 31")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with many items", func(t *testing.T) {
|
||||
many := From(5, 10, 15, 20)
|
||||
sumMonoid := N.MonoidSum[int]()
|
||||
foldMap := FoldMapWithIndex[int](sumMonoid)(func(i, x int) int { return i * x })
|
||||
result := foldMap(many)
|
||||
assert.Equal(t, 100, result, "FoldMapWithIndex should map with indices: (0*5) + (1*10) + (2*15) + (3*20) = 100")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with string concatenation - 0 items", func(t *testing.T) {
|
||||
empty := []string{}
|
||||
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s", i, s)
|
||||
})
|
||||
result := foldMap(empty)
|
||||
assert.Equal(t, "", result, "FoldMapWithIndex should return empty string for 0 items")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with string concatenation - 1 item", func(t *testing.T) {
|
||||
single := From("a")
|
||||
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s", i, s)
|
||||
})
|
||||
result := foldMap(single)
|
||||
assert.Equal(t, "0:a", result, "FoldMapWithIndex should format single item with index")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with string concatenation - 2 items", func(t *testing.T) {
|
||||
two := From("a", "b")
|
||||
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("%d:%s,", i, s)
|
||||
})
|
||||
result := foldMap(two)
|
||||
assert.Equal(t, "0:a,1:b,", result, "FoldMapWithIndex should format 2 items with indices")
|
||||
})
|
||||
|
||||
t.Run("FoldMapWithIndex with string concatenation - many items", func(t *testing.T) {
|
||||
many := From("a", "b", "c", "d")
|
||||
foldMap := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
|
||||
return fmt.Sprintf("[%d]%s", i, s)
|
||||
})
|
||||
result := foldMap(many)
|
||||
assert.Equal(t, "[0]a[1]b[2]c[3]d", result, "FoldMapWithIndex should format many items with indices")
|
||||
})
|
||||
}
|
||||
|
||||
func ExampleFoldMap() {
|
||||
@@ -767,6 +984,25 @@ func TestExtendUseCases(t *testing.T) {
|
||||
|
||||
// TestConcat tests the Concat function
|
||||
func TestConcat(t *testing.T) {
|
||||
t.Run("Semantic: Concat(b)(a) produces [a... b...]", func(t *testing.T) {
|
||||
a := []int{1, 2, 3}
|
||||
b := []int{4, 5, 6}
|
||||
|
||||
// Concat(b)(a) should produce [a... b...]
|
||||
result := Concat(b)(a)
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
|
||||
assert.Equal(t, expected, result, "Concat(b)(a) should produce [a... b...]")
|
||||
|
||||
// Verify order: a's elements come first, then b's elements
|
||||
assert.Equal(t, a[0], result[0], "First element should be from a")
|
||||
assert.Equal(t, a[1], result[1], "Second element should be from a")
|
||||
assert.Equal(t, a[2], result[2], "Third element should be from a")
|
||||
assert.Equal(t, b[0], result[3], "Fourth element should be from b")
|
||||
assert.Equal(t, b[1], result[4], "Fifth element should be from b")
|
||||
assert.Equal(t, b[2], result[5], "Sixth element should be from b")
|
||||
})
|
||||
|
||||
t.Run("Concat two non-empty arrays", func(t *testing.T) {
|
||||
base := []int{1, 2, 3}
|
||||
toAppend := []int{4, 5, 6}
|
||||
@@ -870,6 +1106,54 @@ func TestConcat(t *testing.T) {
|
||||
expected := []int{1, 2, 3}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
t.Run("Explicit append semantic demonstration", func(t *testing.T) {
|
||||
// Given a base array
|
||||
base := []string{"A", "B", "C"}
|
||||
|
||||
// And a suffix to append
|
||||
suffix := []string{"D", "E", "F"}
|
||||
|
||||
// When we apply Concat(suffix) to base
|
||||
appendSuffix := Concat(suffix)
|
||||
result := appendSuffix(base)
|
||||
|
||||
// Then the result should be base followed by suffix
|
||||
expected := []string{"A", "B", "C", "D", "E", "F"}
|
||||
assert.Equal(t, expected, result)
|
||||
|
||||
// And the base should be unchanged
|
||||
assert.Equal(t, []string{"A", "B", "C"}, base)
|
||||
|
||||
// And the suffix should be unchanged
|
||||
assert.Equal(t, []string{"D", "E", "F"}, suffix)
|
||||
})
|
||||
|
||||
t.Run("Append semantic with different types", func(t *testing.T) {
|
||||
// Integers
|
||||
intResult := Concat([]int{4, 5})([]int{1, 2, 3})
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, intResult)
|
||||
|
||||
// Strings
|
||||
strResult := Concat([]string{"world"})([]string{"hello"})
|
||||
assert.Equal(t, []string{"hello", "world"}, strResult)
|
||||
|
||||
// Floats
|
||||
floatResult := Concat([]float64{3.3, 4.4})([]float64{1.1, 2.2})
|
||||
assert.Equal(t, []float64{1.1, 2.2, 3.3, 4.4}, floatResult)
|
||||
})
|
||||
|
||||
t.Run("Append semantic in pipeline", func(t *testing.T) {
|
||||
// Start with [1, 2, 3]
|
||||
// Append [4, 5] to get [1, 2, 3, 4, 5]
|
||||
// Append [6, 7] to get [1, 2, 3, 4, 5, 6, 7]
|
||||
result := F.Pipe2(
|
||||
[]int{1, 2, 3},
|
||||
Concat([]int{4, 5}),
|
||||
Concat([]int{6, 7}),
|
||||
)
|
||||
expected := []int{1, 2, 3, 4, 5, 6, 7}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestConcatComposition tests Concat with other array operations
|
||||
|
||||
@@ -323,34 +323,49 @@ func Clone[AS ~[]A, A any](f func(A) A) func(as AS) AS {
|
||||
}
|
||||
|
||||
func FoldMap[AS ~[]A, A, B any](m M.Monoid[B]) func(func(A) B) func(AS) B {
|
||||
empty := m.Empty()
|
||||
concat := m.Concat
|
||||
return func(f func(A) B) func(AS) B {
|
||||
return func(as AS) B {
|
||||
return array.Reduce(as, func(cur B, a A) B {
|
||||
return concat(cur, f(a))
|
||||
}, empty)
|
||||
switch len(as) {
|
||||
case 0:
|
||||
return m.Empty()
|
||||
case 1:
|
||||
return f(as[0])
|
||||
case 2:
|
||||
return concat(f(as[0]), f(as[1]))
|
||||
default:
|
||||
return array.Reduce(as[1:], func(cur B, a A) B {
|
||||
return concat(cur, f(a))
|
||||
}, f(as[0]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FoldMapWithIndex[AS ~[]A, A, B any](m M.Monoid[B]) func(func(int, A) B) func(AS) B {
|
||||
empty := m.Empty()
|
||||
concat := m.Concat
|
||||
return func(f func(int, A) B) func(AS) B {
|
||||
return func(as AS) B {
|
||||
return array.ReduceWithIndex(as, func(idx int, cur B, a A) B {
|
||||
return concat(cur, f(idx, a))
|
||||
}, empty)
|
||||
}, m.Empty())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A {
|
||||
empty := m.Empty()
|
||||
concat := m.Concat
|
||||
return func(as AS) A {
|
||||
return array.Reduce(as, concat, empty)
|
||||
switch len(as) {
|
||||
case 0:
|
||||
return m.Empty()
|
||||
case 1:
|
||||
return as[0]
|
||||
case 2:
|
||||
return concat(as[0], as[1])
|
||||
default:
|
||||
return array.Reduce(as[1:], concat, as[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,3 +489,16 @@ func Extend[GA ~[]A, GB ~[]B, A, B any](f func(GA) B) func(GA) GB {
|
||||
return MakeBy[GB](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateAt[GT ~[]T, T any](i int, v T) func(GT) O.Option[GT] {
|
||||
none := O.None[GT]()
|
||||
if i < 0 {
|
||||
return F.Constant1[GT](none)
|
||||
}
|
||||
return func(g GT) O.Option[GT] {
|
||||
if i >= len(g) {
|
||||
return none
|
||||
}
|
||||
return O.Of(array.UnsafeUpdateAt(g, i, v))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func MonadSequence[HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
m M.Monoid[HKTRA],
|
||||
ma []HKTA) HKTRA {
|
||||
return array.MonadSequence(fof, m.Empty(), m.Concat, ma)
|
||||
return array.MonadSequence(fof, m.Empty, m.Concat, ma)
|
||||
}
|
||||
|
||||
// Sequence takes an array where elements are HKT<A> (higher kinded type) and,
|
||||
@@ -67,7 +67,7 @@ func Sequence[HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
m M.Monoid[HKTRA],
|
||||
) func([]HKTA) HKTRA {
|
||||
return array.Sequence[[]HKTA](fof, m.Empty(), m.Concat)
|
||||
return array.Sequence[[]HKTA](fof, m.Empty, m.Concat)
|
||||
}
|
||||
|
||||
// ArrayOption returns a function to convert a sequence of options into an option of a sequence.
|
||||
|
||||
@@ -13,28 +13,218 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Package constraints defines a set of useful type constraints for generic programming in Go.
|
||||
|
||||
# Overview
|
||||
|
||||
This package provides type constraints that can be used with Go generics to restrict
|
||||
type parameters to specific categories of types. These constraints are similar to those
|
||||
in Go's standard constraints package but are defined here for consistency within the
|
||||
fp-go project.
|
||||
|
||||
# Type Constraints
|
||||
|
||||
Ordered - Types that support comparison operators:
|
||||
|
||||
type Ordered interface {
|
||||
Integer | Float | ~string
|
||||
}
|
||||
|
||||
Used for types that can be compared using <, <=, >, >= operators.
|
||||
|
||||
Integer - All integer types (signed and unsigned):
|
||||
|
||||
type Integer interface {
|
||||
Signed | Unsigned
|
||||
}
|
||||
|
||||
Signed - Signed integer types:
|
||||
|
||||
type Signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
Unsigned - Unsigned integer types:
|
||||
|
||||
type Unsigned interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
Float - Floating-point types:
|
||||
|
||||
type Float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
Complex - Complex number types:
|
||||
|
||||
type Complex interface {
|
||||
~complex64 | ~complex128
|
||||
}
|
||||
|
||||
# Usage Examples
|
||||
|
||||
Using Ordered constraint for comparison:
|
||||
|
||||
import C "github.com/IBM/fp-go/v2/constraints"
|
||||
|
||||
func Min[T C.Ordered](a, b T) T {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
result := Min(5, 3) // 3
|
||||
result := Min(3.14, 2.71) // 2.71
|
||||
result := Min("apple", "banana") // "apple"
|
||||
|
||||
Using Integer constraint:
|
||||
|
||||
func Abs[T C.Integer](n T) T {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
result := Abs(-42) // 42
|
||||
result := Abs(uint(10)) // 10
|
||||
|
||||
Using Float constraint:
|
||||
|
||||
func Average[T C.Float](a, b T) T {
|
||||
return (a + b) / 2
|
||||
}
|
||||
|
||||
result := Average(3.14, 2.86) // 3.0
|
||||
|
||||
Using Complex constraint:
|
||||
|
||||
func Magnitude[T C.Complex](c T) float64 {
|
||||
r, i := real(c), imag(c)
|
||||
return math.Sqrt(r*r + i*i)
|
||||
}
|
||||
|
||||
c := complex(3, 4)
|
||||
result := Magnitude(c) // 5.0
|
||||
|
||||
# Combining Constraints
|
||||
|
||||
Constraints can be combined to create more specific type restrictions:
|
||||
|
||||
type Number interface {
|
||||
C.Integer | C.Float | C.Complex
|
||||
}
|
||||
|
||||
func Add[T Number](a, b T) T {
|
||||
return a + b
|
||||
}
|
||||
|
||||
# Tilde Operator
|
||||
|
||||
The ~ operator in type constraints means "underlying type". For example, ~int
|
||||
matches not only int but also any type whose underlying type is int:
|
||||
|
||||
type MyInt int
|
||||
|
||||
func Double[T C.Integer](n T) T {
|
||||
return n * 2
|
||||
}
|
||||
|
||||
var x MyInt = 5
|
||||
result := Double(x) // Works because MyInt's underlying type is int
|
||||
|
||||
# Related Packages
|
||||
|
||||
- number: Provides algebraic structures and utilities for numeric types
|
||||
- ord: Provides ordering operations using these constraints
|
||||
- eq: Provides equality operations for comparable types
|
||||
*/
|
||||
package constraints
|
||||
|
||||
// Ordered is a constraint that permits any ordered type: any type that supports
|
||||
// the operators < <= >= >. Ordered types include integers, floats, and strings.
|
||||
//
|
||||
// This constraint is commonly used for comparison operations, sorting, and
|
||||
// finding minimum/maximum values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Max[T Ordered](a, b T) T {
|
||||
// if a > b {
|
||||
// return a
|
||||
// }
|
||||
// return b
|
||||
// }
|
||||
type Ordered interface {
|
||||
Integer | Float | ~string
|
||||
}
|
||||
|
||||
// Signed is a constraint that permits any signed integer type.
|
||||
// This includes int, int8, int16, int32, and int64, as well as any
|
||||
// types whose underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Negate[T Signed](n T) T {
|
||||
// return -n
|
||||
// }
|
||||
type Signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
// Unsigned is a constraint that permits any unsigned integer type.
|
||||
// This includes uint, uint8, uint16, uint32, uint64, and uintptr, as well
|
||||
// as any types whose underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func IsEven[T Unsigned](n T) bool {
|
||||
// return n%2 == 0
|
||||
// }
|
||||
type Unsigned interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
// Integer is a constraint that permits any integer type, both signed and unsigned.
|
||||
// This is a union of the Signed and Unsigned constraints.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Abs[T Integer](n T) T {
|
||||
// if n < 0 {
|
||||
// return -n
|
||||
// }
|
||||
// return n
|
||||
// }
|
||||
type Integer interface {
|
||||
Signed | Unsigned
|
||||
}
|
||||
|
||||
// Float is a constraint that permits any floating-point type.
|
||||
// This includes float32 and float64, as well as any types whose
|
||||
// underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Round[T Float](f T) T {
|
||||
// return T(math.Round(float64(f)))
|
||||
// }
|
||||
type Float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
// Complex is a constraint that permits any complex numeric type.
|
||||
// This includes complex64 and complex128, as well as any types whose
|
||||
// underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Conjugate[T Complex](c T) T {
|
||||
// return complex(real(c), -imag(c))
|
||||
// }
|
||||
type Complex interface {
|
||||
~complex64 | ~complex128
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ package readerioresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/internal/witherable"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
|
||||
@@ -49,3 +53,43 @@ import (
|
||||
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {
|
||||
return RIOR.FilterOrElse[context.Context](pred, onFalse)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func Filter[HKTA, A any](
|
||||
filter func(Predicate[A]) Endomorphism[HKTA],
|
||||
) func(Predicate[A]) Operator[HKTA, HKTA] {
|
||||
return witherable.Filter(
|
||||
Map,
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterArray[A any](p Predicate[A]) Operator[[]A, []A] {
|
||||
return Filter(array.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterIter[A any](p Predicate[A]) Operator[Seq[A], Seq[A]] {
|
||||
return Filter(iter.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMap[HKTA, HKTB, A, B any](
|
||||
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
|
||||
) func(option.Kleisli[A, B]) Operator[HKTA, HKTB] {
|
||||
return witherable.FilterMap(
|
||||
Map,
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapArray[A, B any](p option.Kleisli[A, B]) Operator[[]A, []B] {
|
||||
return FilterMap(array.FilterMap[A, B])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapIter[A, B any](p option.Kleisli[A, B]) Operator[Seq[A], Seq[B]] {
|
||||
return FilterMap(iter.FilterMap[A, B])(p)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ package readerioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/consumer"
|
||||
"github.com/IBM/fp-go/v2/context/ioresult"
|
||||
@@ -220,4 +221,10 @@ type (
|
||||
// The first element is the CancelFunc that should be called to release resources.
|
||||
// The second element is the new Context that was created.
|
||||
ContextCancel = Pair[context.CancelFunc, context.Context]
|
||||
|
||||
// Seq is an iterator over sequences of individual values.
|
||||
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
|
||||
// stopping early if yield returns false.
|
||||
// See the [iter] package documentation for more details.
|
||||
Seq[A any] = iter.Seq[A]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package readerreaderioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/array"
|
||||
"github.com/IBM/fp-go/v2/internal/witherable"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func Filter[C, HKTA, A any](
|
||||
filter func(Predicate[A]) Endomorphism[HKTA],
|
||||
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
|
||||
return witherable.Filter(
|
||||
Map[C],
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
|
||||
return Filter[C](array.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
|
||||
return Filter[C](iter.Filter[A])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMap[C, HKTA, HKTB, A, B any](
|
||||
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
|
||||
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
|
||||
return witherable.FilterMap(
|
||||
Map[C],
|
||||
filter,
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
|
||||
return FilterMap[C](array.FilterMap[A, B])(p)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
|
||||
return FilterMap[C](iter.FilterMap[A, B])(p)
|
||||
}
|
||||
@@ -834,7 +834,7 @@ func Flap[R, B, A any](a A) Operator[R, func(A) B, B] {
|
||||
// This is the monadic version that takes the computation as the first parameter.
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error]) ReaderReaderIOResult[R, A] {
|
||||
func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endomorphism[error]) ReaderReaderIOResult[R, A] {
|
||||
return RRIOE.MonadMapLeft(fa, f)
|
||||
}
|
||||
|
||||
@@ -843,7 +843,7 @@ func MonadMapLeft[R, A any](fa ReaderReaderIOResult[R, A], f Endmorphism[error])
|
||||
// This is the curried version that returns an operator.
|
||||
//
|
||||
//go:inline
|
||||
func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
|
||||
func MapLeft[R, A any](f Endomorphism[error]) Operator[R, A, A] {
|
||||
return RRIOE.MapLeft[R, context.Context, A](f)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/traversal/result"
|
||||
@@ -146,9 +147,15 @@ type (
|
||||
// It's an alias for predicate.Predicate[A].
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Endmorphism represents a function from type A to type A.
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
// It's an alias for endomorphism.Endomorphism[A].
|
||||
Endmorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Seq is an iterator over sequences of individual values.
|
||||
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
|
||||
// stopping early if yield returns false.
|
||||
// See the [iter] package documentation for more details.
|
||||
Seq[A any] = iter.Seq[A]
|
||||
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
# fp-go/v2 Reference for Claude Code
|
||||
|
||||
fp-go/v2 (`github.com/IBM/fp-go/v2`) is a typed functional programming library for Go 1.24+. It provides Option, Either/Result, IO, and Effect monads with data-last, curried APIs designed for pipeline composition via `Pipe` and `Flow`. The library follows Haskell/fp-ts conventions adapted for Go generics with explicit arity-numbered functions (e.g., `Pipe3`, `Flow2`). Two module families exist: **standard** (struct-based monads, full FP toolkit) and **idiomatic** (Go-native `(value, error)` tuples, zero-alloc, 2-32x faster).
|
||||
|
||||
## Import Conventions
|
||||
|
||||
| Alias | Package |
|
||||
|-------|---------|
|
||||
| `F` | `github.com/IBM/fp-go/v2/function` |
|
||||
| `O` | `github.com/IBM/fp-go/v2/option` |
|
||||
| `E` | `github.com/IBM/fp-go/v2/either` |
|
||||
| `R` | `github.com/IBM/fp-go/v2/result` |
|
||||
| `A` | `github.com/IBM/fp-go/v2/array` |
|
||||
| `IO` | `github.com/IBM/fp-go/v2/io` |
|
||||
| `IOR` | `github.com/IBM/fp-go/v2/ioresult` |
|
||||
| `IOE` | `github.com/IBM/fp-go/v2/ioeither` |
|
||||
| `RIO` | `github.com/IBM/fp-go/v2/context/readerioresult` |
|
||||
| `EFF` | `github.com/IBM/fp-go/v2/effect` |
|
||||
| `P` | `github.com/IBM/fp-go/v2/pair` |
|
||||
| `T` | `github.com/IBM/fp-go/v2/tuple` |
|
||||
| `N` | `github.com/IBM/fp-go/v2/number` |
|
||||
| `S` | `github.com/IBM/fp-go/v2/string` |
|
||||
| `B` | `github.com/IBM/fp-go/v2/boolean` |
|
||||
| `L` | `github.com/IBM/fp-go/v2/optics/lens` |
|
||||
| `PR` | `github.com/IBM/fp-go/v2/optics/prism` |
|
||||
|
||||
**Idiomatic variants** (tuple-based, zero-alloc):
|
||||
|
||||
| Alias | Package |
|
||||
|-------|---------|
|
||||
| `IR` | `github.com/IBM/fp-go/v2/idiomatic/result` |
|
||||
| `IO_` | `github.com/IBM/fp-go/v2/idiomatic/option` |
|
||||
| `IIR` | `github.com/IBM/fp-go/v2/idiomatic/ioresult` |
|
||||
| `IRR` | `github.com/IBM/fp-go/v2/idiomatic/context/readerresult` |
|
||||
| `IRO` | `github.com/IBM/fp-go/v2/idiomatic/readerioresult` |
|
||||
|
||||
## Monad Selection
|
||||
|
||||
- **Pure value** -- use the value directly, no wrapper needed
|
||||
- **May be absent** -- `Option[A]` (struct-based) or `(A, bool)` (idiomatic)
|
||||
- **Can fail with `error`** -- `Result[A]` = `Either[error, A]`
|
||||
- Need custom error type E -- use `Either[E, A]` instead
|
||||
- **Lazy + can fail** -- `IOResult[A]` = `func() Either[error, A]`
|
||||
- Idiomatic: `func() (A, error)`
|
||||
- **Needs `context.Context` + lazy + can fail** -- `ReaderIOResult[A]` via `context/readerioresult`
|
||||
- Type: `func(context.Context) func() Either[error, A]`
|
||||
- Idiomatic: `func(context.Context) (A, error)` via `idiomatic/context/readerresult`
|
||||
- **Typed DI + context + lazy + can fail** -- `Effect[C, A]` via `effect` package
|
||||
- Type: `func(C) func(context.Context) func() Either[error, A]`
|
||||
- C is your dependency/config struct; context.Context is handled internally
|
||||
- **Performance-critical** -- prefer `idiomatic/` variants throughout
|
||||
|
||||
## Standard vs Idiomatic
|
||||
|
||||
| Aspect | Standard | Idiomatic |
|
||||
|--------|----------|-----------|
|
||||
| Representation | `Either[error, A]` struct | `(A, error)` tuple |
|
||||
| Performance | Baseline | 2-32x faster, zero allocs |
|
||||
| Custom error types | `Either[E, A]` for any E | error only |
|
||||
| Do-notation | Full support | Full support |
|
||||
| FP toolkit | Complete | Complete |
|
||||
| Go interop | Requires `Unwrap`/`Eitherize` | Native `(val, err)` |
|
||||
|
||||
**Rule of thumb**: Use idiomatic for production code and hot paths. Use standard when you need custom error types (`Either[E, A]`) or when composing with packages that use the standard types.
|
||||
|
||||
## Core Types
|
||||
|
||||
```go
|
||||
// function package
|
||||
type Void = struct{}
|
||||
var VOID Void = struct{}{}
|
||||
|
||||
// option
|
||||
type Option[A any] struct { /* Some/None */ }
|
||||
|
||||
// either
|
||||
type Either[E, A any] struct { /* Left/Right */ }
|
||||
|
||||
// result (specialized Either)
|
||||
type Result[A any] = Either[error, A]
|
||||
|
||||
// io
|
||||
type IO[A any] = func() A
|
||||
|
||||
// ioresult
|
||||
type IOResult[A any] = IO[Result[A]] // = func() Either[error, A]
|
||||
|
||||
// context/readerioresult
|
||||
type ReaderIOResult[A any] = func(context.Context) func() Either[error, A]
|
||||
|
||||
// effect
|
||||
type Effect[C, A any] = func(C) func(context.Context) func() Either[error, A]
|
||||
type Kleisli[C, A, B any] = func(A) Effect[C, B]
|
||||
|
||||
// idiomatic equivalents
|
||||
type IOResult[A any] = func() (A, error)
|
||||
type ReaderResult[A any] = func(context.Context) (A, error)
|
||||
```
|
||||
|
||||
## Key Rules
|
||||
|
||||
1. **Data-last**: Configuration/behavior params come first, data comes last. This enables partial application and pipeline composition.
|
||||
|
||||
2. **Type parameter ordering**: Non-inferrable type params come first. Example: `Ap[B, E, A]` -- B cannot be inferred, so it leads. `Map[A, B]` -- both usually inferred.
|
||||
|
||||
3. **Composition direction**:
|
||||
- `F.Flow1/2/3/.../N` -- left-to-right (use this for pipelines)
|
||||
- `Compose` -- right-to-left (mathematical convention; avoid in pipelines)
|
||||
|
||||
4. **Pipe vs Flow**:
|
||||
- `F.Pipe3(value, f1, f2, f3)` -- apply data to a pipeline immediately
|
||||
- `F.Flow3(f1, f2, f3)` -- create a reusable pipeline (returns a function)
|
||||
|
||||
5. **Arity-numbered functions**: `Pipe1` through `Pipe20`, `Flow1` through `Flow20`. Choose the number matching your operation count.
|
||||
|
||||
6. **Naming conventions**:
|
||||
- `Chain` = flatMap/bind (`A -> F[B]`, flattens)
|
||||
- `Map` = fmap (`A -> B`, lifts into context)
|
||||
- `Ap` = applicative apply (apply wrapped function to wrapped value)
|
||||
- `ChainFirst` / `Tap` = execute for side effects, keep original value
|
||||
- `ChainEitherK` = lift pure `func(A) Either[E, B]` into monadic chain
|
||||
- `Of` = pure/return (lift value into monad)
|
||||
- `Fold` = catamorphism (handle both cases)
|
||||
- `Left` / `Right` = Either constructors
|
||||
- `Some` / `None` = Option constructors
|
||||
|
||||
7. **Prefer `result` over `either`** unless you need a custom error type E. `Result[A]` = `Either[error, A]`.
|
||||
|
||||
8. **Wrapping Go functions**:
|
||||
- `result.Eitherize1(fn)` wraps `func(X) (Y, error)` into `func(X) Result[Y]`
|
||||
- `result.Eitherize2(fn)` wraps `func(X, Y) (Z, error)` into `func(X, Y) Result[Z]`
|
||||
- Variants up to `Eitherize15`
|
||||
|
||||
9. **Use `function.Void` / `function.VOID`** instead of `struct{}` / `struct{}{}`.
|
||||
|
||||
10. **Go 1.24+ required** (generic type aliases).
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pipeline with Pipe
|
||||
```go
|
||||
result := F.Pipe3(
|
||||
inputValue,
|
||||
R.Map(transform),
|
||||
R.Chain(validate),
|
||||
R.Fold(onError, onSuccess),
|
||||
)
|
||||
```
|
||||
|
||||
### Reusable pipeline with Flow
|
||||
```go
|
||||
pipeline := F.Flow3(
|
||||
R.Map(normalize),
|
||||
R.Chain(validate),
|
||||
R.Map(format),
|
||||
)
|
||||
output := pipeline(R.Of(input))
|
||||
```
|
||||
|
||||
### Wrapping Go error functions
|
||||
```go
|
||||
safeParseInt := R.Eitherize1(strconv.Atoi)
|
||||
// safeParseInt: func(string) Result[int]
|
||||
result := safeParseInt("42") // Right(42)
|
||||
```
|
||||
|
||||
### Effect with DI
|
||||
```go
|
||||
type Deps struct { DB *sql.DB }
|
||||
|
||||
fetchUser := EFF.Eitherize(func(deps Deps, ctx context.Context) (*User, error) {
|
||||
return deps.DB.QueryRowContext(ctx, "SELECT ...").Scan(...)
|
||||
})
|
||||
// fetchUser: Effect[Deps, *User]
|
||||
|
||||
// Execute:
|
||||
val, err := EFF.RunSync(EFF.Provide[*User](myDeps)(fetchUser))(ctx)
|
||||
```
|
||||
|
||||
### Effect composition
|
||||
```go
|
||||
pipeline := F.Pipe1(
|
||||
fetchUser,
|
||||
EFF.Map[Deps](func(u *User) string { return u.Name }),
|
||||
)
|
||||
```
|
||||
|
||||
### Do-notation (building up state)
|
||||
```go
|
||||
type State struct { X int; Y string }
|
||||
|
||||
result := F.Pipe3(
|
||||
R.Do(State{}),
|
||||
R.Bind(
|
||||
func(x int) func(State) State {
|
||||
return func(s State) State { s.X = x; return s }
|
||||
},
|
||||
func(s State) Result[int] { return R.Of(42) },
|
||||
),
|
||||
R.Let(
|
||||
func(y string) func(State) State {
|
||||
return func(s State) State { s.Y = y; return s }
|
||||
},
|
||||
func(s State) string { return fmt.Sprintf("val=%d", s.X) },
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
### Optics (Lens)
|
||||
```go
|
||||
type Person struct { Name string; Age int }
|
||||
|
||||
nameLens := L.MakeLens(
|
||||
func(p Person) string { return p.Name },
|
||||
func(p Person, name string) Person { p.Name = name; return p },
|
||||
)
|
||||
|
||||
name := nameLens.Get(person) // get
|
||||
updated := nameLens.Set("Bob")(person) // set (returns new Person)
|
||||
modified := L.Modify(strings.ToUpper)(nameLens)(person) // modify
|
||||
```
|
||||
|
||||
### Option handling
|
||||
```go
|
||||
result := F.Pipe3(
|
||||
O.Some(42),
|
||||
O.Map(func(x int) int { return x * 2 }),
|
||||
O.GetOrElse(F.Constant(0)),
|
||||
)
|
||||
```
|
||||
|
||||
### Idiomatic IOResult
|
||||
```go
|
||||
readFile := func() ([]byte, error) { return os.ReadFile("config.json") }
|
||||
// This IS an idiomatic IOResult[[]byte] -- just a func() ([]byte, error)
|
||||
|
||||
parsed := IIR.Map(parseConfig)(readFile)
|
||||
config, err := parsed()
|
||||
```
|
||||
|
||||
### ReaderIOResult (context-dependent IO)
|
||||
```go
|
||||
// Eitherize1 wraps func(context.Context, T0) (R, error) -> func(T0) ReaderIOResult[R]
|
||||
fetchURL := RIO.Eitherize1(func(ctx context.Context, url string) ([]byte, error) {
|
||||
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil { return nil, err }
|
||||
defer resp.Body.Close()
|
||||
return iolib.ReadAll(resp.Body)
|
||||
})
|
||||
// fetchURL: func(string) ReaderIOResult[[]byte]
|
||||
result := fetchURL("https://example.com")(ctx)() // execute
|
||||
```
|
||||
|
||||
## Deeper Documentation
|
||||
|
||||
- `fp-go-cookbook.md` -- migration recipes and "how do I X in fp-go?"
|
||||
- `fp-go-core-patterns.md` -- core types, operations, and composition details
|
||||
- `fp-go-mastery.md` -- advanced FP techniques, architecture, and Effect system
|
||||
- `fp-go-full-reference.md` -- complete API inventory across all packages
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -354,3 +354,20 @@ func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effe
|
||||
func LocalReaderK[A, C1, C2 any](f reader.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalReaderK[A](f)
|
||||
}
|
||||
|
||||
// Ask returns an Effect that produces the context C as its success value.
|
||||
// This is the fundamental operation of the reader/environment monad,
|
||||
// allowing effects to access their own context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type (also the produced value type)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, C]: An effect that succeeds with its own context value
|
||||
//
|
||||
//go:inline
|
||||
func Ask[C any]() Effect[C, C] {
|
||||
return readerreaderioresult.Ask[C]()
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/reader"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -922,45 +920,77 @@ func TestLocalReaderK(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("runtime context deadline awareness", func(t *testing.T) {
|
||||
type Config struct {
|
||||
HasDeadline bool
|
||||
}
|
||||
|
||||
// Reader that checks runtime context for deadline
|
||||
checkContext := func(path string) reader.Reader[Config] {
|
||||
return func(ctx context.Context) Config {
|
||||
_, hasDeadline := ctx.Deadline()
|
||||
return Config{HasDeadline: hasDeadline}
|
||||
}
|
||||
}
|
||||
|
||||
// Effect that uses the config
|
||||
configEffect := Chain(func(cfg Config) Effect[Config, string] {
|
||||
return Of[Config](fmt.Sprintf("Has deadline: %v", cfg.HasDeadline))
|
||||
})(readerreaderioresult.Ask[Config]())
|
||||
|
||||
transform := LocalReaderK[string](checkContext)
|
||||
pathEffect := transform(configEffect)
|
||||
|
||||
// Without deadline
|
||||
ioResult := Provide[string]("config.json")(pathEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
func TestAsk(t *testing.T) {
|
||||
t.Run("returns context as value", func(t *testing.T) {
|
||||
ctx := "my-context"
|
||||
result, err := runEffect(Ask[string](), ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Has deadline: false", result)
|
||||
assert.Equal(t, ctx, result)
|
||||
})
|
||||
|
||||
// With deadline
|
||||
ctxWithDeadline, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
t.Run("works with struct context", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
ioResult2 := Provide[string]("config.json")(pathEffect)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result2, err2 := readerResult2(ctxWithDeadline)
|
||||
cfg := Config{Host: "localhost", Port: 8080}
|
||||
result, err := runEffect(Ask[Config](), cfg)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cfg, result)
|
||||
})
|
||||
|
||||
t.Run("can be chained with Map to extract a field", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
hostEffect := Map[Config](func(cfg Config) string {
|
||||
return cfg.Host
|
||||
})(Ask[Config]())
|
||||
|
||||
result, err := runEffect(hostEffect, Config{Host: "example.com", Port: 443})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "example.com", result)
|
||||
})
|
||||
|
||||
t.Run("can be chained with Chain to produce a derived effect", func(t *testing.T) {
|
||||
type Config struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
derived := Chain(func(cfg Config) Effect[Config, string] {
|
||||
if cfg.APIKey == "" {
|
||||
return Fail[Config, string](assert.AnError)
|
||||
}
|
||||
return Of[Config]("authenticated: " + cfg.APIKey)
|
||||
})(Ask[Config]())
|
||||
|
||||
// Valid key
|
||||
result, err := runEffect(derived, Config{APIKey: "secret"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "authenticated: secret", result)
|
||||
|
||||
// Empty key
|
||||
_, err = runEffect(derived, Config{APIKey: ""})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("is idempotent - multiple calls return same context", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "shared"}
|
||||
|
||||
r1, err1 := runEffect(Ask[TestContext](), ctx)
|
||||
r2, err2 := runEffect(Ask[TestContext](), ctx)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "Has deadline: true", result2)
|
||||
assert.Equal(t, r1, r2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,6 +204,102 @@ func ChainFirst[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, A] {
|
||||
return readerreaderioresult.ChainFirst(f)
|
||||
}
|
||||
|
||||
// ChainFirstThunkK chains an effect with a function that returns a Thunk,
|
||||
// but discards the result and returns the original value.
|
||||
// This is useful for performing side effects (like logging or IO operations) that don't
|
||||
// need the effect's context, without changing the value flowing through the computation.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The value type (preserved)
|
||||
// - B: The type produced by the Thunk (discarded)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns Thunk[B] for side effects
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, A]: A function that executes the Thunk but preserves the original value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// logToFile := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
// return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
// return func() result.Result[any] {
|
||||
// // Perform IO operation that doesn't need effect context
|
||||
// fmt.Printf("Logging: %d\n", n)
|
||||
// return result.Of[any](nil)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// logged := effect.ChainFirstThunkK[MyContext](logToFile)(eff)
|
||||
// // Prints "Logging: 42" but still produces 42
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ChainThunkK: Chains with a Thunk and uses its result
|
||||
// - TapThunkK: Alias for ChainFirstThunkK
|
||||
// - ChainFirstIOK: Similar but for IO operations
|
||||
//
|
||||
//go:inline
|
||||
func ChainFirstThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, A] {
|
||||
return fromreader.ChainFirstReaderK(
|
||||
ChainFirst[C, A, B],
|
||||
FromThunk[C, B],
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// TapThunkK is an alias for ChainFirstThunkK.
|
||||
// It chains an effect with a function that returns a Thunk for side effects,
|
||||
// but preserves the original value. This is useful for logging, debugging, or
|
||||
// performing IO operations that don't need the effect's context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The value type (preserved)
|
||||
// - B: The type produced by the Thunk (discarded)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes A and returns Thunk[B] for side effects
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, A, A]: A function that executes the Thunk but preserves the original value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// performSideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
// return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
// return func() result.Result[any] {
|
||||
// // Perform context-independent IO operation
|
||||
// log.Printf("Processing value: %d", n)
|
||||
// return result.Of[any](nil)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// eff := effect.Of[MyContext](42)
|
||||
// tapped := effect.TapThunkK[MyContext](performSideEffect)(eff)
|
||||
// // Logs "Processing value: 42" but still produces 42
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ChainFirstThunkK: The underlying implementation
|
||||
// - TapIOK: Similar but for IO operations
|
||||
// - Tap: Similar but for full effects
|
||||
//
|
||||
//go:inline
|
||||
func TapThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, A] {
|
||||
return ChainFirstThunkK[C](f)
|
||||
}
|
||||
|
||||
// ChainIOK chains an effect with a function that returns an IO action.
|
||||
// This is useful for integrating IO-based computations (synchronous side effects)
|
||||
// into effect chains. The IO action is automatically lifted into the Effect context.
|
||||
@@ -612,3 +708,52 @@ func ChainReaderIOK[C, A, B any](f readerio.Kleisli[C, A, B]) Operator[C, A, B]
|
||||
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
|
||||
return readerreaderioresult.Read[A](c)
|
||||
}
|
||||
|
||||
// Asks creates an Effect that projects a value from the context using a Reader function.
|
||||
// This is useful for extracting specific fields or computing derived values from the context.
|
||||
// It's essentially a lifted version of the Reader pattern into the Effect context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type
|
||||
// - A: The type of the projected value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - r: A Reader function that extracts or computes a value from the context
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, A]: An effect that succeeds with the projected value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Extract a specific field
|
||||
// getHost := effect.Asks[Config](func(cfg Config) string {
|
||||
// return cfg.Host
|
||||
// })
|
||||
//
|
||||
// // Compute a derived value
|
||||
// getURL := effect.Asks[Config](func(cfg Config) string {
|
||||
// return fmt.Sprintf("http://%s:%d", cfg.Host, cfg.Port)
|
||||
// })
|
||||
//
|
||||
// result, err := runEffect(getHost, Config{Host: "localhost", Port: 8080})
|
||||
// // result == "localhost", err == nil
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// See Also:
|
||||
//
|
||||
// - Ask: Returns the entire context as the value
|
||||
// - Map: Transforms the value after extraction
|
||||
//
|
||||
//go:inline
|
||||
func Asks[C, A any](r Reader[C, A]) Effect[C, A] {
|
||||
return readerreaderioresult.Asks(r)
|
||||
}
|
||||
|
||||
@@ -677,3 +677,992 @@ func TestChainThunkK_Integration(t *testing.T) {
|
||||
assert.Equal(t, result.Of("Value: 100"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstThunkK_Success(t *testing.T) {
|
||||
t.Run("executes thunk but preserves original value", func(t *testing.T) {
|
||||
sideEffectExecuted := false
|
||||
|
||||
sideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
sideEffectExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](sideEffect),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.True(t, sideEffectExecuted)
|
||||
})
|
||||
|
||||
t.Run("chains multiple side effects", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("log: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe2(
|
||||
Of[TestConfig](10),
|
||||
ChainFirstThunkK[TestConfig](logValue),
|
||||
ChainFirstThunkK[TestConfig](logValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(10), outcome)
|
||||
assert.Equal(t, 2, len(log))
|
||||
assert.Equal(t, "log: 10", log[0])
|
||||
assert.Equal(t, "log: 10", log[1])
|
||||
})
|
||||
|
||||
t.Run("side effect can access runtime context", func(t *testing.T) {
|
||||
var capturedCtx context.Context
|
||||
|
||||
captureContext := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
capturedCtx = ctx
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](captureContext),
|
||||
)
|
||||
outcome := computation(testConfig)(ctx)()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, ctx, capturedCtx)
|
||||
})
|
||||
|
||||
t.Run("side effect result is discarded", func(t *testing.T) {
|
||||
returnDifferentValue := func(n int) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) io.IO[result.Result[string]] {
|
||||
return func() result.Result[string] {
|
||||
return result.Of("different value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](returnDifferentValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstThunkK_Failure(t *testing.T) {
|
||||
t.Run("propagates error from previous effect", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("previous error")
|
||||
sideEffectExecuted := false
|
||||
|
||||
sideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
sideEffectExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
ChainFirstThunkK[TestConfig](sideEffect),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
assert.False(t, sideEffectExecuted)
|
||||
})
|
||||
|
||||
t.Run("propagates error from thunk side effect", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("side effect error")
|
||||
|
||||
failingSideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
return result.Left[any](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](failingSideEffect),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("stops execution on first error", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("first error")
|
||||
secondEffectExecuted := false
|
||||
|
||||
failingEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
return result.Left[any](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
secondEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
secondEffectExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe2(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](failingEffect),
|
||||
ChainFirstThunkK[TestConfig](secondEffect),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
assert.False(t, secondEffectExecuted)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstThunkK_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero value", func(t *testing.T) {
|
||||
callCount := 0
|
||||
|
||||
countCalls := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
callCount++
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](0),
|
||||
ChainFirstThunkK[TestConfig](countCalls),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
|
||||
t.Run("handles empty string", func(t *testing.T) {
|
||||
var capturedValue string
|
||||
|
||||
captureValue := func(s string) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
capturedValue = s
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](""),
|
||||
ChainFirstThunkK[TestConfig](captureValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(""), outcome)
|
||||
assert.Equal(t, "", capturedValue)
|
||||
})
|
||||
|
||||
t.Run("handles nil pointer", func(t *testing.T) {
|
||||
var capturedPtr *int
|
||||
|
||||
capturePtr := func(ptr *int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
capturedPtr = ptr
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig]((*int)(nil)),
|
||||
ChainFirstThunkK[TestConfig](capturePtr),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of((*int)(nil)), outcome)
|
||||
assert.Nil(t, capturedPtr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainFirstThunkK_Integration(t *testing.T) {
|
||||
t.Run("composes with Map and Chain", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("value: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe3(
|
||||
Of[TestConfig](5),
|
||||
Map[TestConfig](func(x int) int { return x * 2 }),
|
||||
ChainFirstThunkK[TestConfig](logValue),
|
||||
Map[TestConfig](func(x int) int { return x + 3 }),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(13), outcome) // (5 * 2) + 3
|
||||
assert.Equal(t, 1, len(log))
|
||||
assert.Equal(t, "value: 10", log[0])
|
||||
})
|
||||
|
||||
t.Run("composes with ChainThunkK", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logSideEffect := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("side-effect: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
transformValue := func(n int) readerioresult.ReaderIOResult[string] {
|
||||
return func(ctx context.Context) io.IO[result.Result[string]] {
|
||||
return func() result.Result[string] {
|
||||
log = append(log, fmt.Sprintf("transform: %d", n))
|
||||
return result.Of(fmt.Sprintf("Result: %d", n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe2(
|
||||
Of[TestConfig](42),
|
||||
ChainFirstThunkK[TestConfig](logSideEffect),
|
||||
ChainThunkK[TestConfig](transformValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of("Result: 42"), outcome)
|
||||
assert.Equal(t, 2, len(log))
|
||||
assert.Equal(t, "side-effect: 42", log[0])
|
||||
assert.Equal(t, "transform: 42", log[1])
|
||||
})
|
||||
|
||||
t.Run("composes with ChainReaderK and ChainReaderIOK", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
addMultiplier := func(n int) reader.Reader[TestConfig, int] {
|
||||
return func(cfg TestConfig) int {
|
||||
return n + cfg.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
logReaderIO := func(n int) readerio.ReaderIO[TestConfig, int] {
|
||||
return func(cfg TestConfig) io.IO[int] {
|
||||
return func() int {
|
||||
log = append(log, fmt.Sprintf("reader-io: %d", n))
|
||||
return n * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logThunk := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("thunk: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe3(
|
||||
Of[TestConfig](5),
|
||||
ChainReaderK(addMultiplier),
|
||||
ChainReaderIOK(logReaderIO),
|
||||
ChainFirstThunkK[TestConfig](logThunk),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(16), outcome) // (5 + 3) * 2
|
||||
assert.Equal(t, 2, len(log))
|
||||
assert.Equal(t, "reader-io: 8", log[0])
|
||||
assert.Equal(t, "thunk: 16", log[1])
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapThunkK_Success(t *testing.T) {
|
||||
t.Run("is alias for ChainFirstThunkK", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("tapped: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
TapThunkK[TestConfig](logValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, 1, len(log))
|
||||
assert.Equal(t, "tapped: 42", log[0])
|
||||
})
|
||||
|
||||
t.Run("useful for logging without changing value", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logStep := func(step string) func(int) readerioresult.ReaderIOResult[any] {
|
||||
return func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("%s: %d", step, n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe4(
|
||||
Of[TestConfig](10),
|
||||
TapThunkK[TestConfig](logStep("start")),
|
||||
Map[TestConfig](func(x int) int { return x * 2 }),
|
||||
TapThunkK[TestConfig](logStep("after-map")),
|
||||
Map[TestConfig](func(x int) int { return x + 5 }),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(25), outcome) // (10 * 2) + 5
|
||||
assert.Equal(t, 2, len(log))
|
||||
assert.Equal(t, "start: 10", log[0])
|
||||
assert.Equal(t, "after-map: 20", log[1])
|
||||
})
|
||||
|
||||
t.Run("can perform IO operations", func(t *testing.T) {
|
||||
var ioExecuted bool
|
||||
|
||||
performIO := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
// Simulate IO operation
|
||||
ioExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
TapThunkK[TestConfig](performIO),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.True(t, ioExecuted)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapThunkK_Failure(t *testing.T) {
|
||||
t.Run("propagates error from previous effect", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("previous error")
|
||||
tapExecuted := false
|
||||
|
||||
tapValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
tapExecuted = true
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Fail[TestConfig, int](testErr),
|
||||
TapThunkK[TestConfig](tapValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
assert.False(t, tapExecuted)
|
||||
})
|
||||
|
||||
t.Run("propagates error from tap operation", func(t *testing.T) {
|
||||
testErr := fmt.Errorf("tap error")
|
||||
|
||||
failingTap := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
return result.Left[any](testErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Of[TestConfig](42),
|
||||
TapThunkK[TestConfig](failingTap),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Left[int](testErr), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapThunkK_EdgeCases(t *testing.T) {
|
||||
t.Run("handles multiple taps in sequence", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
tap1 := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, "tap1")
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tap2 := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, "tap2")
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tap3 := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, "tap3")
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe3(
|
||||
Of[TestConfig](42),
|
||||
TapThunkK[TestConfig](tap1),
|
||||
TapThunkK[TestConfig](tap2),
|
||||
TapThunkK[TestConfig](tap3),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
assert.Equal(t, []string{"tap1", "tap2", "tap3"}, log)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTapThunkK_Integration(t *testing.T) {
|
||||
t.Run("real-world logging scenario", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
logStart := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("Starting computation with: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logIntermediate := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("Intermediate result: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logFinal := func(s string) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("Final result: %s", s))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe5(
|
||||
Of[TestConfig](10),
|
||||
TapThunkK[TestConfig](logStart),
|
||||
Map[TestConfig](func(x int) int { return x * 3 }),
|
||||
TapThunkK[TestConfig](logIntermediate),
|
||||
Map[TestConfig](func(x int) string { return fmt.Sprintf("Value: %d", x) }),
|
||||
TapThunkK[TestConfig](logFinal),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of("Value: 30"), outcome)
|
||||
assert.Equal(t, 3, len(log))
|
||||
assert.Equal(t, "Starting computation with: 10", log[0])
|
||||
assert.Equal(t, "Intermediate result: 30", log[1])
|
||||
assert.Equal(t, "Final result: Value: 30", log[2])
|
||||
})
|
||||
|
||||
t.Run("composes with FromThunk", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
thunk := func(ctx context.Context) io.IO[result.Result[int]] {
|
||||
return func() result.Result[int] {
|
||||
return result.Of(100)
|
||||
}
|
||||
}
|
||||
|
||||
logValue := func(n int) readerioresult.ReaderIOResult[any] {
|
||||
return func(ctx context.Context) io.IO[result.Result[any]] {
|
||||
return func() result.Result[any] {
|
||||
log = append(log, fmt.Sprintf("value: %d", n))
|
||||
return result.Of[any](nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
FromThunk[TestConfig](thunk),
|
||||
TapThunkK[TestConfig](logValue),
|
||||
)
|
||||
outcome := computation(testConfig)(context.Background())()
|
||||
|
||||
assert.Equal(t, result.Of(100), outcome)
|
||||
assert.Equal(t, 1, len(log))
|
||||
assert.Equal(t, "value: 100", log[0])
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_Success(t *testing.T) {
|
||||
t.Run("extracts a field from context", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
getHost := Asks(func(cfg Config) string {
|
||||
return cfg.Host
|
||||
})
|
||||
|
||||
result, err := runEffect(getHost, Config{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "localhost", result)
|
||||
})
|
||||
|
||||
t.Run("extracts multiple fields and computes derived value", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
getURL := Asks(func(cfg Config) string {
|
||||
return fmt.Sprintf("http://%s:%d", cfg.Host, cfg.Port)
|
||||
})
|
||||
|
||||
result, err := runEffect(getURL, Config{Host: "example.com", Port: 443})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "http://example.com:443", result)
|
||||
})
|
||||
|
||||
t.Run("extracts numeric field", func(t *testing.T) {
|
||||
getPort := Asks(func(cfg TestConfig) int {
|
||||
return cfg.Multiplier
|
||||
})
|
||||
|
||||
result, err := runEffect(getPort, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, result)
|
||||
})
|
||||
|
||||
t.Run("computes value from context", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
getArea := Asks(func(cfg Config) int {
|
||||
return cfg.Width * cfg.Height
|
||||
})
|
||||
|
||||
result, err := runEffect(getArea, Config{Width: 10, Height: 20})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, result)
|
||||
})
|
||||
|
||||
t.Run("transforms string field", func(t *testing.T) {
|
||||
getUpperPrefix := Asks(func(cfg TestConfig) string {
|
||||
return fmt.Sprintf("[%s]", cfg.Prefix)
|
||||
})
|
||||
|
||||
result, err := runEffect(getUpperPrefix, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[LOG]", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero values", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
getValue := Asks(func(cfg Config) int {
|
||||
return cfg.Value
|
||||
})
|
||||
|
||||
result, err := runEffect(getValue, Config{Value: 0})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("handles empty string", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
getName := Asks(func(cfg Config) string {
|
||||
return cfg.Name
|
||||
})
|
||||
|
||||
result, err := runEffect(getName, Config{Name: ""})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("handles nil pointer fields", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Data *string
|
||||
}
|
||||
|
||||
hasData := Asks(func(cfg Config) bool {
|
||||
return cfg.Data != nil
|
||||
})
|
||||
|
||||
result, err := runEffect(hasData, Config{Data: nil})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("handles complex nested structures", func(t *testing.T) {
|
||||
type Database struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
type Config struct {
|
||||
DB Database
|
||||
}
|
||||
|
||||
getDBHost := Asks(func(cfg Config) string {
|
||||
return cfg.DB.Host
|
||||
})
|
||||
|
||||
result, err := runEffect(getDBHost, Config{
|
||||
DB: Database{Host: "db.example.com", Port: 5432},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "db.example.com", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_Integration(t *testing.T) {
|
||||
t.Run("composes with Map", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks(func(cfg Config) int {
|
||||
return cfg.Value
|
||||
}),
|
||||
Map[Config](func(x int) int { return x * 2 }),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{Value: 21})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks(func(cfg Config) int {
|
||||
return cfg.Multiplier
|
||||
}),
|
||||
Chain(func(mult int) Effect[Config, int] {
|
||||
return Of[Config](mult * 10)
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{Multiplier: 5})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 50, result)
|
||||
})
|
||||
|
||||
t.Run("composes with ChainReaderK", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Asks(func(cfg TestConfig) int {
|
||||
return cfg.Multiplier
|
||||
}),
|
||||
ChainReaderK(func(mult int) reader.Reader[TestConfig, int] {
|
||||
return func(cfg TestConfig) int {
|
||||
return mult + len(cfg.Prefix)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, result) // 3 + len("LOG")
|
||||
})
|
||||
|
||||
t.Run("composes with ChainReaderIOK", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks(func(cfg TestConfig) string {
|
||||
return cfg.Prefix
|
||||
}),
|
||||
ChainReaderIOK(func(prefix string) readerio.ReaderIO[TestConfig, string] {
|
||||
return func(cfg TestConfig) io.IO[string] {
|
||||
return func() string {
|
||||
log = append(log, "executed")
|
||||
return fmt.Sprintf("%s:%d", prefix, cfg.Multiplier)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG:3", result)
|
||||
assert.Equal(t, 1, len(log))
|
||||
})
|
||||
|
||||
t.Run("multiple Asks in sequence", func(t *testing.T) {
|
||||
type Config struct {
|
||||
First string
|
||||
Second string
|
||||
}
|
||||
|
||||
computation := F.Pipe2(
|
||||
Asks(func(cfg Config) string {
|
||||
return cfg.First
|
||||
}),
|
||||
Chain(func(_ string) Effect[Config, string] {
|
||||
return Asks(func(cfg Config) string {
|
||||
return cfg.Second
|
||||
})
|
||||
}),
|
||||
Map[Config](func(s string) string {
|
||||
return "Result: " + s
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{First: "A", Second: "B"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Result: B", result)
|
||||
})
|
||||
|
||||
t.Run("Asks combined with Ask", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Ask[Config](),
|
||||
Chain(func(cfg Config) Effect[Config, int] {
|
||||
return Asks(func(c Config) int {
|
||||
return c.Value * 2
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{Value: 15})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_Comparison(t *testing.T) {
|
||||
t.Run("Asks vs Ask with Map", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
// Using Asks
|
||||
asksVersion := Asks(func(cfg Config) int {
|
||||
return cfg.Port
|
||||
})
|
||||
|
||||
// Using Ask + Map
|
||||
askMapVersion := F.Pipe1(
|
||||
Ask[Config](),
|
||||
Map[Config](func(cfg Config) int {
|
||||
return cfg.Port
|
||||
}),
|
||||
)
|
||||
|
||||
cfg := Config{Port: 8080}
|
||||
|
||||
result1, err1 := runEffect(asksVersion, cfg)
|
||||
result2, err2 := runEffect(askMapVersion, cfg)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, 8080, result1)
|
||||
})
|
||||
|
||||
t.Run("Asks is more concise than Ask + Map", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// Asks is more direct for field extraction
|
||||
getHost := Asks(func(cfg Config) string {
|
||||
return cfg.Host
|
||||
})
|
||||
|
||||
result, err := runEffect(getHost, Config{Host: "api.example.com", Port: 443})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "api.example.com", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_RealWorldScenarios(t *testing.T) {
|
||||
t.Run("extract database connection string", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Database string
|
||||
User string
|
||||
}
|
||||
|
||||
getConnectionString := Asks(func(cfg DatabaseConfig) string {
|
||||
return fmt.Sprintf("postgres://%s@%s:%d/%s",
|
||||
cfg.User, cfg.Host, cfg.Port, cfg.Database)
|
||||
})
|
||||
|
||||
result, err := runEffect(getConnectionString, DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Database: "myapp",
|
||||
User: "admin",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://admin@localhost:5432/myapp", result)
|
||||
})
|
||||
|
||||
t.Run("compute API endpoint from config", func(t *testing.T) {
|
||||
type APIConfig struct {
|
||||
Protocol string
|
||||
Host string
|
||||
Port int
|
||||
BasePath string
|
||||
}
|
||||
|
||||
getEndpoint := Asks(func(cfg APIConfig) string {
|
||||
return fmt.Sprintf("%s://%s:%d%s",
|
||||
cfg.Protocol, cfg.Host, cfg.Port, cfg.BasePath)
|
||||
})
|
||||
|
||||
result, err := runEffect(getEndpoint, APIConfig{
|
||||
Protocol: "https",
|
||||
Host: "api.example.com",
|
||||
Port: 443,
|
||||
BasePath: "/v1",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://api.example.com:443/v1", result)
|
||||
})
|
||||
|
||||
t.Run("validate configuration", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Timeout int
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
isValid := Asks(func(cfg Config) bool {
|
||||
return cfg.Timeout > 0 && cfg.MaxRetries >= 0
|
||||
})
|
||||
|
||||
// Valid config
|
||||
result1, err1 := runEffect(isValid, Config{Timeout: 30, MaxRetries: 3})
|
||||
assert.NoError(t, err1)
|
||||
assert.True(t, result1)
|
||||
|
||||
// Invalid config
|
||||
result2, err2 := runEffect(isValid, Config{Timeout: 0, MaxRetries: 3})
|
||||
assert.NoError(t, err2)
|
||||
assert.False(t, result2)
|
||||
})
|
||||
|
||||
t.Run("extract feature flags", func(t *testing.T) {
|
||||
type FeatureFlags struct {
|
||||
EnableNewUI bool
|
||||
EnableBetaAPI bool
|
||||
EnableAnalytics bool
|
||||
}
|
||||
|
||||
hasNewUI := Asks[FeatureFlags](func(flags FeatureFlags) bool {
|
||||
return flags.EnableNewUI
|
||||
})
|
||||
|
||||
result, err := runEffect(hasNewUI, FeatureFlags{
|
||||
EnableNewUI: true,
|
||||
EnableBetaAPI: false,
|
||||
EnableAnalytics: true,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// Filter lifts a filtering operation on a higher-kinded type into an Effect operator.
|
||||
// This is a generic function that works with any filterable data structure by taking
|
||||
// a filter function and returning an operator that can be used in effect chains.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - HKTA: The higher-kinded type being filtered (e.g., []A, Seq[A])
|
||||
// - A: The element type being filtered
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - filter: A function that takes a predicate and returns an endomorphism on HKTA
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(Predicate[A]) Operator[C, HKTA, HKTA]: A function that takes a predicate
|
||||
// and returns an operator that filters effects containing HKTA values
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import A "github.com/IBM/fp-go/v2/array"
|
||||
//
|
||||
// // Create a custom filter operator for arrays
|
||||
// filterOp := Filter[MyContext](A.Filter[int])
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
//
|
||||
// pipeline := F.Pipe2(
|
||||
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
|
||||
// filterOp(isEven),
|
||||
// Map[MyContext](func(arr []int) int { return len(arr) }),
|
||||
// )
|
||||
// // Result: Effect that produces 2 (count of even numbers)
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterArray: Specialized version for array filtering
|
||||
// - FilterIter: Specialized version for iterator filtering
|
||||
// - FilterMap: For filtering and mapping simultaneously
|
||||
//
|
||||
//go:inline
|
||||
func Filter[C, HKTA, A any](
|
||||
filter func(Predicate[A]) Endomorphism[HKTA],
|
||||
) func(Predicate[A]) Operator[C, HKTA, HKTA] {
|
||||
return readerreaderioresult.Filter[C](filter)
|
||||
}
|
||||
|
||||
// FilterArray creates an operator that filters array elements within an Effect based on a predicate.
|
||||
// Elements that satisfy the predicate are kept, while others are removed.
|
||||
// This is a specialized version of Filter for arrays.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The element type in the array
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A predicate function that tests each element
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, []A, []A]: An operator that filters array elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// isPositive := func(n int) bool { return n > 0 }
|
||||
// filterPositive := FilterArray[MyContext](isPositive)
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext]([]int{-2, -1, 0, 1, 2, 3}),
|
||||
// filterPositive,
|
||||
// )
|
||||
// // Result: Effect that produces []int{1, 2, 3}
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Filter: Generic version for any filterable type
|
||||
// - FilterIter: For filtering iterators
|
||||
// - FilterMapArray: For filtering and mapping arrays simultaneously
|
||||
//
|
||||
//go:inline
|
||||
func FilterArray[C, A any](p Predicate[A]) Operator[C, []A, []A] {
|
||||
return readerreaderioresult.FilterArray[C](p)
|
||||
}
|
||||
|
||||
// FilterIter creates an operator that filters iterator elements within an Effect based on a predicate.
|
||||
// Elements that satisfy the predicate are kept in the resulting iterator, while others are removed.
|
||||
// This is a specialized version of Filter for iterators (Seq).
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The element type in the iterator
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A predicate function that tests each element
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, Seq[A], Seq[A]]: An operator that filters iterator elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// isEven := func(n int) bool { return n%2 == 0 }
|
||||
// filterEven := FilterIter[MyContext](isEven)
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext](slices.Values([]int{1, 2, 3, 4, 5, 6})),
|
||||
// filterEven,
|
||||
// )
|
||||
// // Result: Effect that produces an iterator over [2, 4, 6]
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Filter: Generic version for any filterable type
|
||||
// - FilterArray: For filtering arrays
|
||||
// - FilterMapIter: For filtering and mapping iterators simultaneously
|
||||
//
|
||||
//go:inline
|
||||
func FilterIter[C, A any](p Predicate[A]) Operator[C, Seq[A], Seq[A]] {
|
||||
return readerreaderioresult.FilterIter[C](p)
|
||||
}
|
||||
|
||||
// FilterMap lifts a filter-map operation on a higher-kinded type into an Effect operator.
|
||||
// This combines filtering and mapping in a single operation: elements are transformed
|
||||
// using a function that returns Option, and only Some values are kept in the result.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - HKTA: The input higher-kinded type (e.g., []A, Seq[A])
|
||||
// - HKTB: The output higher-kinded type (e.g., []B, Seq[B])
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - filter: A function that takes an option.Kleisli and returns a transformation from HKTA to HKTB
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB]: A function that takes a Kleisli arrow
|
||||
// and returns an operator that filter-maps effects
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import A "github.com/IBM/fp-go/v2/array"
|
||||
// import O "github.com/IBM/fp-go/v2/option"
|
||||
//
|
||||
// // Parse and filter positive integers
|
||||
// parsePositive := func(s string) O.Option[int] {
|
||||
// var n int
|
||||
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n > 0 {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// filterMapOp := FilterMap[MyContext](A.FilterMap[string, int])
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext]([]string{"1", "-2", "3", "invalid", "5"}),
|
||||
// filterMapOp(parsePositive),
|
||||
// )
|
||||
// // Result: Effect that produces []int{1, 3, 5}
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterMapArray: Specialized version for arrays
|
||||
// - FilterMapIter: Specialized version for iterators
|
||||
// - Filter: For filtering without transformation
|
||||
//
|
||||
//go:inline
|
||||
func FilterMap[C, HKTA, HKTB, A, B any](
|
||||
filter func(option.Kleisli[A, B]) Reader[HKTA, HKTB],
|
||||
) func(option.Kleisli[A, B]) Operator[C, HKTA, HKTB] {
|
||||
return readerreaderioresult.FilterMap[C](filter)
|
||||
}
|
||||
|
||||
// FilterMapArray creates an operator that filters and maps array elements within an Effect.
|
||||
// Each element is transformed using a function that returns Option[B]. Elements that
|
||||
// produce Some(b) are kept in the result array, while None values are filtered out.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, []A, []B]: An operator that filter-maps array elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import O "github.com/IBM/fp-go/v2/option"
|
||||
//
|
||||
// // Double even numbers, filter out odd numbers
|
||||
// doubleEven := func(n int) O.Option[int] {
|
||||
// if n%2 == 0 {
|
||||
// return O.Some(n * 2)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext]([]int{1, 2, 3, 4, 5}),
|
||||
// FilterMapArray[MyContext](doubleEven),
|
||||
// )
|
||||
// // Result: Effect that produces []int{4, 8}
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterMap: Generic version for any filterable type
|
||||
// - FilterMapIter: For filter-mapping iterators
|
||||
// - FilterArray: For filtering without transformation
|
||||
//
|
||||
//go:inline
|
||||
func FilterMapArray[C, A, B any](p option.Kleisli[A, B]) Operator[C, []A, []B] {
|
||||
return readerreaderioresult.FilterMapArray[C](p)
|
||||
}
|
||||
|
||||
// FilterMapIter creates an operator that filters and maps iterator elements within an Effect.
|
||||
// Each element is transformed using a function that returns Option[B]. Elements that
|
||||
// produce Some(b) are kept in the resulting iterator, while None values are filtered out.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input element type
|
||||
// - B: The output element type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - p: A Kleisli arrow from A to Option[B] that transforms and filters elements
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Operator[C, Seq[A], Seq[B]]: An operator that filter-maps iterator elements in an effect
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// import O "github.com/IBM/fp-go/v2/option"
|
||||
//
|
||||
// // Parse strings to integers, keeping only valid ones
|
||||
// parseInt := func(s string) O.Option[int] {
|
||||
// var n int
|
||||
// if _, err := fmt.Sscanf(s, "%d", &n); err == nil {
|
||||
// return O.Some(n)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// Succeed[MyContext](slices.Values([]string{"1", "2", "invalid", "3"})),
|
||||
// FilterMapIter[MyContext](parseInt),
|
||||
// )
|
||||
// // Result: Effect that produces an iterator over [1, 2, 3]
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - FilterMap: Generic version for any filterable type
|
||||
// - FilterMapArray: For filter-mapping arrays
|
||||
// - FilterIter: For filtering without transformation
|
||||
//
|
||||
//go:inline
|
||||
func FilterMapIter[C, A, B any](p option.Kleisli[A, B]) Operator[C, Seq[A], Seq[B]] {
|
||||
return readerreaderioresult.FilterMapIter[C](p)
|
||||
}
|
||||
@@ -0,0 +1,653 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type FilterTestConfig struct {
|
||||
MaxValue int
|
||||
MinValue int
|
||||
}
|
||||
|
||||
// Helper to collect iterator results from an effect
|
||||
func collectSeqEffect[C, A any](eff Effect[C, Seq[A]], cfg C) []A {
|
||||
result, err := runEffect(eff, cfg)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return slices.Collect(result)
|
||||
}
|
||||
|
||||
func TestFilterArray_Success(t *testing.T) {
|
||||
t.Run("filters array keeping matching elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
input := Succeed[FilterTestConfig]([]int{1, -2, 3, -4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 3, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty array when no elements match", func(t *testing.T) {
|
||||
// Arrange
|
||||
isNegative := N.LessThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isNegative)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("returns all elements when all match", func(t *testing.T) {
|
||||
// Arrange
|
||||
alwaysTrue := func(n int) bool { return true }
|
||||
filterOp := FilterArray[FilterTestConfig](alwaysTrue)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterIter_Success(t *testing.T) {
|
||||
t.Run("filters iterator keeping matching elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterOp := FilterIter[FilterTestConfig](isEven)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5, 6}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{2, 4, 6}, collected)
|
||||
})
|
||||
|
||||
t.Run("returns empty iterator when no elements match", func(t *testing.T) {
|
||||
// Arrange
|
||||
isNegative := N.LessThan(0)
|
||||
filterOp := FilterIter[FilterTestConfig](isNegative)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterArray_WithContext(t *testing.T) {
|
||||
t.Run("uses context for filtering", func(t *testing.T) {
|
||||
// Arrange
|
||||
cfg := FilterTestConfig{MaxValue: 100, MinValue: 0}
|
||||
inRange := func(n int) bool { return n >= cfg.MinValue && n <= cfg.MaxValue }
|
||||
filterOp := FilterArray[FilterTestConfig](inRange)
|
||||
input := Succeed[FilterTestConfig]([]int{-10, 50, 150, 75})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), cfg)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{50, 75}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterArray_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
input := Succeed[FilterTestConfig]([]int{})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves error from input", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
inputErr := errors.New("input error")
|
||||
input := Fail[FilterTestConfig, []int](inputErr)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, inputErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterIter_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty iterator", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterIter[FilterTestConfig](isPositive)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("preserves error from input", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterIter[FilterTestConfig](isPositive)
|
||||
inputErr := errors.New("input error")
|
||||
input := Fail[FilterTestConfig, Seq[int]](inputErr)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, inputErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_GenericFilter(t *testing.T) {
|
||||
t.Run("works with custom filter function", func(t *testing.T) {
|
||||
// Arrange
|
||||
customFilter := func(p Predicate[int]) Endomorphism[[]int] {
|
||||
return A.Filter(p)
|
||||
}
|
||||
filterOp := Filter[FilterTestConfig](customFilter)
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3, 4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(isEven)(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapArray_Success(t *testing.T) {
|
||||
t.Run("filters and maps array elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
parsePositive := func(n int) O.Option[string] {
|
||||
if n > 0 {
|
||||
return O.Some(fmt.Sprintf("positive:%d", n))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](parsePositive)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"positive:2", "positive:4", "positive:5"}, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when no elements match", func(t *testing.T) {
|
||||
// Arrange
|
||||
neverMatch := func(n int) O.Option[int] {
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](neverMatch)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("maps all elements when all match", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
input := Succeed[FilterTestConfig]([]int{1, 2, 3})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapIter_Success(t *testing.T) {
|
||||
t.Run("filters and maps iterator elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
doubleEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapIter[FilterTestConfig](doubleEven)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{4, 8}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapArray_TypeConversion(t *testing.T) {
|
||||
t.Run("converts int to string", func(t *testing.T) {
|
||||
// Arrange
|
||||
intToString := func(n int) O.Option[string] {
|
||||
if n > 0 {
|
||||
return O.Some(fmt.Sprintf("%d", n))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](intToString)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"2", "4"}, result)
|
||||
})
|
||||
|
||||
t.Run("converts string to int", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseEven := func(s string) O.Option[int] {
|
||||
var n int
|
||||
if _, err := fmt.Sscanf(s, "%d", &n); err == nil && n%2 == 0 {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](parseEven)
|
||||
input := Succeed[FilterTestConfig]([]string{"1", "2", "3", "4", "invalid"})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapArray_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
input := Succeed[FilterTestConfig]([]int{})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves error from input", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
inputErr := errors.New("input error")
|
||||
input := Fail[FilterTestConfig, []int](inputErr)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, inputErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMapIter_EdgeCases(t *testing.T) {
|
||||
t.Run("handles empty iterator", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapIter[FilterTestConfig](double)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilterMap_GenericFilterMap(t *testing.T) {
|
||||
t.Run("works with custom filterMap function", func(t *testing.T) {
|
||||
// Arrange
|
||||
customFilterMap := func(f O.Kleisli[int, string]) Reader[[]int, []string] {
|
||||
return A.FilterMap(f)
|
||||
}
|
||||
filterMapOp := FilterMap[FilterTestConfig](customFilterMap)
|
||||
intToString := func(n int) O.Option[string] {
|
||||
if n > 0 {
|
||||
return O.Some(fmt.Sprintf("%d", n))
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 2, -3, 4})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(intToString)(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"2", "4"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_Composition(t *testing.T) {
|
||||
t.Run("chains multiple filters", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterPositive := FilterArray[FilterTestConfig](isPositive)
|
||||
filterEven := FilterArray[FilterTestConfig](isEven)
|
||||
input := Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterEven(filterPositive(input)), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("chains filter and filterMap", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
doubleEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](doubleEven)
|
||||
input := Succeed[FilterTestConfig]([]int{-2, 1, 2, 3, 4, 5})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(filterOp(input)), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{4, 8}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_WithComplexTypes(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("filters structs", func(t *testing.T) {
|
||||
// Arrange
|
||||
isAdult := func(u User) bool { return u.Age >= 18 }
|
||||
filterOp := FilterArray[FilterTestConfig](isAdult)
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 25},
|
||||
{Name: "Bob", Age: 16},
|
||||
{Name: "Charlie", Age: 30},
|
||||
}
|
||||
input := Succeed[FilterTestConfig](users)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
expected := []User{
|
||||
{Name: "Alice", Age: 25},
|
||||
{Name: "Charlie", Age: 30},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("filterMaps structs to different type", func(t *testing.T) {
|
||||
// Arrange
|
||||
extractAdultName := func(u User) O.Option[string] {
|
||||
if u.Age >= 18 {
|
||||
return O.Some(u.Name)
|
||||
}
|
||||
return O.None[string]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](extractAdultName)
|
||||
users := []User{
|
||||
{Name: "Alice", Age: 25},
|
||||
{Name: "Bob", Age: 16},
|
||||
{Name: "Charlie", Age: 30},
|
||||
}
|
||||
input := Succeed[FilterTestConfig](users)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"Alice", "Charlie"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_BoundaryConditions(t *testing.T) {
|
||||
t.Run("filters with boundary predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
inRange := func(n int) bool { return n >= 0 && n <= 100 }
|
||||
filterOp := FilterArray[FilterTestConfig](inRange)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{0, 50, 100}, result)
|
||||
})
|
||||
|
||||
t.Run("filterMap with boundary conditions", func(t *testing.T) {
|
||||
// Arrange
|
||||
clampToRange := func(n int) O.Option[int] {
|
||||
if n >= 0 && n <= 100 {
|
||||
return O.Some(n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](clampToRange)
|
||||
input := Succeed[FilterTestConfig]([]int{-1, 0, 50, 100, 101})
|
||||
|
||||
// Act
|
||||
result, err := runEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{0, 50, 100}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_WithIterators(t *testing.T) {
|
||||
t.Run("filters large iterator efficiently", func(t *testing.T) {
|
||||
// Arrange
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filterOp := FilterIter[FilterTestConfig](isEven)
|
||||
|
||||
// Create iterator for range 0-99
|
||||
makeSeq := func(yield func(int) bool) {
|
||||
for i := range 100 {
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
input := Succeed[FilterTestConfig](Seq[int](makeSeq))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, 50, len(collected))
|
||||
assert.Equal(t, 0, collected[0])
|
||||
assert.Equal(t, 98, collected[49])
|
||||
})
|
||||
|
||||
t.Run("filterMap with iterator", func(t *testing.T) {
|
||||
// Arrange
|
||||
squareEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * n)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
filterMapOp := FilterMapIter[FilterTestConfig](squareEven)
|
||||
input := Succeed[FilterTestConfig](slices.Values([]int{1, 2, 3, 4, 5}))
|
||||
|
||||
// Act
|
||||
collected := collectSeqEffect(filterMapOp(input), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{4, 16}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_ErrorPropagation(t *testing.T) {
|
||||
t.Run("filter propagates Left through chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
isPositive := N.MoreThan(0)
|
||||
filterOp := FilterArray[FilterTestConfig](isPositive)
|
||||
originalErr := errors.New("original error")
|
||||
|
||||
// Create an effect that fails
|
||||
failedEffect := F.Pipe1(
|
||||
Succeed[FilterTestConfig]([]int{1, 2, 3}),
|
||||
Chain(func([]int) Effect[FilterTestConfig, []int] {
|
||||
return Fail[FilterTestConfig, []int](originalErr)
|
||||
}),
|
||||
)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterOp(failedEffect), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
})
|
||||
|
||||
t.Run("filterMap propagates Left through chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
double := func(n int) O.Option[int] {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
filterMapOp := FilterMapArray[FilterTestConfig](double)
|
||||
originalErr := errors.New("original error")
|
||||
|
||||
// Create an effect that fails
|
||||
failedEffect := F.Pipe1(
|
||||
Succeed[FilterTestConfig]([]int{1, 2, 3}),
|
||||
Chain(func([]int) Effect[FilterTestConfig, []int] {
|
||||
return Fail[FilterTestConfig, []int](originalErr)
|
||||
}),
|
||||
)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(filterMapOp(failedEffect), FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, originalErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_Integration(t *testing.T) {
|
||||
t.Run("complex filtering pipeline", func(t *testing.T) {
|
||||
// Arrange: Filter positive numbers, then double evens, then filter > 5
|
||||
isPositive := N.MoreThan(0)
|
||||
doubleEven := func(n int) O.Option[int] {
|
||||
if n%2 == 0 {
|
||||
return O.Some(n * 2)
|
||||
}
|
||||
return O.None[int]()
|
||||
}
|
||||
isGreaterThan5 := N.MoreThan(5)
|
||||
|
||||
pipeline := F.Pipe3(
|
||||
Succeed[FilterTestConfig]([]int{-2, -1, 0, 1, 2, 3, 4, 5, 6}),
|
||||
FilterArray[FilterTestConfig](isPositive),
|
||||
FilterMapArray[FilterTestConfig](doubleEven),
|
||||
FilterArray[FilterTestConfig](isGreaterThan5),
|
||||
)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(pipeline, FilterTestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
// Positive: [1,2,3,4,5,6] -> DoubleEven: [4,8,12] -> >5: [8,12]
|
||||
assert.Equal(t, []int{8, 12}, result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of an Effect.
|
||||
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Modify the context before passing it to the Effect (via f)
|
||||
// - Transform the success value after the computation completes (via g)
|
||||
//
|
||||
// Promap is particularly useful for adapting effects to work with different context types
|
||||
// while simultaneously transforming their output values.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - E: The original context type expected by the Effect
|
||||
// - A: The original success type produced by the Effect
|
||||
// - D: The new input context type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: Function to transform the input context from D to E (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Kleisli arrow that takes an Effect[E, A] and returns a function from D to B
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// APIKey string
|
||||
// }
|
||||
//
|
||||
// type DBConfig struct {
|
||||
// URL string
|
||||
// }
|
||||
//
|
||||
// // Effect that uses DBConfig and returns an int
|
||||
// getUserCount := func(cfg DBConfig) effect.Effect[context.Context, int] {
|
||||
// return effect.Succeed[context.Context](42)
|
||||
// }
|
||||
//
|
||||
// // Transform AppConfig to DBConfig
|
||||
// extractDBConfig := func(app AppConfig) DBConfig {
|
||||
// return DBConfig{URL: app.DatabaseURL}
|
||||
// }
|
||||
//
|
||||
// // Transform int to string
|
||||
// formatCount := func(count int) string {
|
||||
// return fmt.Sprintf("Users: %d", count)
|
||||
// }
|
||||
//
|
||||
// // Adapt the effect to work with AppConfig and return string
|
||||
// adapted := effect.Promap(extractDBConfig, formatCount)(getUserCount)
|
||||
// result := adapted(AppConfig{DatabaseURL: "localhost:5432", APIKey: "secret"})
|
||||
//
|
||||
//go:inline
|
||||
func Promap[E, A, D, B any](f Reader[D, E], g Reader[A, B]) Kleisli[D, Effect[E, A], B] {
|
||||
return F.Flow2(
|
||||
Local[A](f),
|
||||
Map[D](g),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for profunctor tests
|
||||
type AppConfig struct {
|
||||
DatabaseURL string
|
||||
APIKey string
|
||||
Port int
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both context and output", func(t *testing.T) {
|
||||
// Effect that uses DBConfig and returns an int
|
||||
getUserCount := Succeed[DBConfig](42)
|
||||
|
||||
// Transform AppConfig to DBConfig
|
||||
extractDBConfig := func(app AppConfig) DBConfig {
|
||||
return DBConfig{URL: app.DatabaseURL}
|
||||
}
|
||||
|
||||
// Transform int to string
|
||||
formatCount := func(count int) string {
|
||||
return fmt.Sprintf("Users: %d", count)
|
||||
}
|
||||
|
||||
// Adapt the effect to work with AppConfig and return string
|
||||
adapted := Promap(extractDBConfig, formatCount)(getUserCount)
|
||||
result := adapted(AppConfig{
|
||||
DatabaseURL: "localhost:5432",
|
||||
APIKey: "secret",
|
||||
Port: 8080,
|
||||
})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Users: 42"), result)
|
||||
})
|
||||
|
||||
t.Run("identity transformations", func(t *testing.T) {
|
||||
// Effect that returns a value
|
||||
getValue := Succeed[DBConfig](100)
|
||||
|
||||
// Identity transformations
|
||||
identity := func(x DBConfig) DBConfig { return x }
|
||||
identityInt := func(x int) int { return x }
|
||||
|
||||
// Apply identity transformations
|
||||
adapted := Promap(identity, identityInt)(getValue)
|
||||
result := adapted(DBConfig{URL: "localhost"})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(100), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapComposition tests that Promap composes correctly
|
||||
func TestPromapComposition(t *testing.T) {
|
||||
t.Run("compose multiple transformations", func(t *testing.T) {
|
||||
// Effect that uses ServerConfig and returns the port
|
||||
getPort := Map[ServerConfig](func(cfg ServerConfig) int {
|
||||
return cfg.Port
|
||||
})(Ask[ServerConfig]())
|
||||
|
||||
// First transformation: AppConfig -> ServerConfig
|
||||
extractServerConfig := func(app AppConfig) ServerConfig {
|
||||
return ServerConfig{Host: "localhost", Port: app.Port}
|
||||
}
|
||||
|
||||
// Second transformation: int -> string
|
||||
formatPort := func(port int) string {
|
||||
return fmt.Sprintf(":%d", port)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(extractServerConfig, formatPort)(getPort)
|
||||
result := adapted(AppConfig{
|
||||
DatabaseURL: "db.example.com",
|
||||
APIKey: "key123",
|
||||
Port: 9000,
|
||||
})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(":9000"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithErrors tests Promap with effects that can fail
|
||||
func TestPromapWithErrors(t *testing.T) {
|
||||
t.Run("propagates errors correctly", func(t *testing.T) {
|
||||
// Effect that fails
|
||||
failingEffect := Fail[DBConfig, int](fmt.Errorf("database connection failed"))
|
||||
|
||||
// Transformations
|
||||
extractDBConfig := func(app AppConfig) DBConfig {
|
||||
return DBConfig{URL: app.DatabaseURL}
|
||||
}
|
||||
formatCount := func(count int) string {
|
||||
return fmt.Sprintf("Count: %d", count)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(extractDBConfig, formatCount)(failingEffect)
|
||||
result := adapted(AppConfig{DatabaseURL: "localhost"})(context.Background())()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
err := R.MonadFold(result,
|
||||
func(e error) error { return e },
|
||||
func(string) error { return nil },
|
||||
)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "database connection failed")
|
||||
})
|
||||
|
||||
t.Run("output transformation not applied on error", func(t *testing.T) {
|
||||
callCount := 0
|
||||
|
||||
// Effect that fails
|
||||
failingEffect := Fail[DBConfig, int](fmt.Errorf("error"))
|
||||
|
||||
// Transformation that counts calls
|
||||
countingTransform := func(x int) string {
|
||||
callCount++
|
||||
return strconv.Itoa(x)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(
|
||||
func(app AppConfig) DBConfig { return DBConfig{URL: app.DatabaseURL} },
|
||||
countingTransform,
|
||||
)(failingEffect)
|
||||
result := adapted(AppConfig{DatabaseURL: "localhost"})(context.Background())()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
assert.Equal(t, 0, callCount, "output transformation should not be called on error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithComplexTypes tests Promap with more complex type transformations
|
||||
func TestPromapWithComplexTypes(t *testing.T) {
|
||||
t.Run("transform struct to different struct", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
type UserDTO struct {
|
||||
UserID int
|
||||
FullName string
|
||||
}
|
||||
|
||||
// Effect that uses User and returns a string
|
||||
getUserInfo := Map[User](func(user User) string {
|
||||
return fmt.Sprintf("User %s (ID: %d)", user.Name, user.ID)
|
||||
})(Ask[User]())
|
||||
|
||||
// Transform UserDTO to User
|
||||
dtoToUser := func(dto UserDTO) User {
|
||||
return User{ID: dto.UserID, Name: dto.FullName}
|
||||
}
|
||||
|
||||
// Transform string to uppercase
|
||||
toUpper := func(s string) string {
|
||||
return fmt.Sprintf("INFO: %s", s)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(dtoToUser, toUpper)(getUserInfo)
|
||||
result := adapted(UserDTO{UserID: 123, FullName: "Alice"})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("INFO: User Alice (ID: 123)"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapChaining tests chaining multiple Promap operations
|
||||
func TestPromapChaining(t *testing.T) {
|
||||
t.Run("chain multiple Promap operations", func(t *testing.T) {
|
||||
// Base effect that doubles the input
|
||||
baseEffect := Map[int](func(x int) int {
|
||||
return x * 2
|
||||
})(Ask[int]())
|
||||
|
||||
// First Promap: string -> int, int -> string
|
||||
step1 := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
strconv.Itoa,
|
||||
)(baseEffect)
|
||||
|
||||
// Second Promap: float64 -> string, string -> float64
|
||||
step2 := Promap(
|
||||
func(f float64) string {
|
||||
return fmt.Sprintf("%.0f", f)
|
||||
},
|
||||
func(s string) float64 {
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
return f
|
||||
},
|
||||
)(step1)
|
||||
|
||||
result := step2(21.0)(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(42.0), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapEdgeCases tests edge cases
|
||||
func TestPromapEdgeCases(t *testing.T) {
|
||||
t.Run("zero values", func(t *testing.T) {
|
||||
effect := Map[int](func(x int) int {
|
||||
return x
|
||||
})(Ask[int]())
|
||||
|
||||
adapted := Promap(
|
||||
func(s string) int { return 0 },
|
||||
func(x int) string { return "" },
|
||||
)(effect)
|
||||
|
||||
result := adapted("anything")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(""), result)
|
||||
})
|
||||
|
||||
t.Run("nil context handling", func(t *testing.T) {
|
||||
effect := Succeed[int]("success")
|
||||
|
||||
adapted := Promap(
|
||||
func(s string) int { return 42 },
|
||||
func(s string) string { return s + "!" },
|
||||
)(effect)
|
||||
|
||||
// Using background context instead of nil
|
||||
result := adapted("test")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("success!"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapIntegration tests integration with other effect operations
|
||||
func TestPromapIntegration(t *testing.T) {
|
||||
t.Run("Promap with Map", func(t *testing.T) {
|
||||
// Base effect that adds 10
|
||||
baseEffect := Map[int](func(x int) int {
|
||||
return x + 10
|
||||
})(Ask[int]())
|
||||
|
||||
// Apply Promap
|
||||
promapped := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
func(x int) int { return x * 2 },
|
||||
)(baseEffect)
|
||||
|
||||
// Apply Map on top
|
||||
mapped := Map[string](func(x int) string {
|
||||
return fmt.Sprintf("Result: %d", x)
|
||||
})(promapped)
|
||||
|
||||
result := mapped("5")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Result: 30"), result)
|
||||
})
|
||||
|
||||
t.Run("Promap with Chain", func(t *testing.T) {
|
||||
// Base effect
|
||||
baseEffect := Ask[int]()
|
||||
|
||||
// Apply Promap
|
||||
promapped := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
func(x int) int { return x * 2 },
|
||||
)(baseEffect)
|
||||
|
||||
// Chain with another effect
|
||||
chained := Chain(func(x int) Effect[string, string] {
|
||||
return Succeed[string](fmt.Sprintf("Value: %d", x))
|
||||
})(promapped)
|
||||
|
||||
result := chained("10")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Value: 20"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkPromap benchmarks the Promap operation
|
||||
func BenchmarkPromap(b *testing.B) {
|
||||
effect := Map[int](func(x int) int {
|
||||
return x * 2
|
||||
})(Ask[int]())
|
||||
|
||||
adapted := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
strconv.Itoa,
|
||||
)(effect)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = adapted("42")(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPromapChained benchmarks chained Promap operations
|
||||
func BenchmarkPromapChained(b *testing.B) {
|
||||
baseEffect := Map[int](func(x int) int {
|
||||
return x * 2
|
||||
})(Ask[int]())
|
||||
|
||||
step1 := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
strconv.Itoa,
|
||||
)(baseEffect)
|
||||
|
||||
step2 := Promap(
|
||||
func(f float64) string {
|
||||
return fmt.Sprintf("%.0f", f)
|
||||
},
|
||||
func(s string) float64 {
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
return f
|
||||
},
|
||||
)(step1)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = step2(21.0)(ctx)()
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,11 @@ import (
|
||||
"github.com/IBM/fp-go/v2/context/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/iterator/iter"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
@@ -89,4 +91,14 @@ type (
|
||||
// Operator represents a function that transforms Effect[C, A] to Effect[C, B].
|
||||
// It's used for lifting operations over effects.
|
||||
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
|
||||
|
||||
// Endomorphism represents a function from type A to type A.
|
||||
// It's an alias for endomorphism.Endomorphism[A].
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
// Seq is an iterator over sequences of individual values.
|
||||
// When called as seq(yield), seq calls yield(v) for each value v in the sequence,
|
||||
// stopping early if yield returns false.
|
||||
// See the [iter] package documentation for more details.
|
||||
Seq[A any] = iter.Seq[A]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+247
-11
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -236,6 +236,7 @@ func Pipe4[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, T
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow4[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, T0, T1, T2, T3, T4 any](f1 F1, f2 F2, f3 F3, f4 F4) func(T0) T4 {
|
||||
//go:inline
|
||||
return func(t0 T0) T4 {
|
||||
return Pipe4(t0, f1, f2, f3, f4)
|
||||
}
|
||||
@@ -302,6 +303,7 @@ func Pipe5[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow5[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, T0, T1, T2, T3, T4, T5 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) func(T0) T5 {
|
||||
//go:inline
|
||||
return func(t0 T0) T5 {
|
||||
return Pipe5(t0, f1, f2, f3, f4, f5)
|
||||
}
|
||||
@@ -370,6 +372,7 @@ func Pipe6[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow6[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, T0, T1, T2, T3, T4, T5, T6 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6) func(T0) T6 {
|
||||
//go:inline
|
||||
return func(t0 T0) T6 {
|
||||
return Pipe6(t0, f1, f2, f3, f4, f5, f6)
|
||||
}
|
||||
@@ -440,6 +443,7 @@ func Pipe7[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow7[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, T0, T1, T2, T3, T4, T5, T6, T7 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7) func(T0) T7 {
|
||||
//go:inline
|
||||
return func(t0 T0) T7 {
|
||||
return Pipe7(t0, f1, f2, f3, f4, f5, f6, f7)
|
||||
}
|
||||
@@ -512,6 +516,7 @@ func Pipe8[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow8[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, T0, T1, T2, T3, T4, T5, T6, T7, T8 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8) func(T0) T8 {
|
||||
//go:inline
|
||||
return func(t0 T0) T8 {
|
||||
return Pipe8(t0, f1, f2, f3, f4, f5, f6, f7, f8)
|
||||
}
|
||||
@@ -586,6 +591,7 @@ func Pipe9[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow9[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9) func(T0) T9 {
|
||||
//go:inline
|
||||
return func(t0 T0) T9 {
|
||||
return Pipe9(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9)
|
||||
}
|
||||
@@ -662,6 +668,7 @@ func Pipe10[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow10[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10) func(T0) T10 {
|
||||
//go:inline
|
||||
return func(t0 T0) T10 {
|
||||
return Pipe10(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10)
|
||||
}
|
||||
@@ -740,6 +747,7 @@ func Pipe11[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow11[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11) func(T0) T11 {
|
||||
//go:inline
|
||||
return func(t0 T0) T11 {
|
||||
return Pipe11(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11)
|
||||
}
|
||||
@@ -820,6 +828,7 @@ func Pipe12[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow12[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12) func(T0) T12 {
|
||||
//go:inline
|
||||
return func(t0 T0) T12 {
|
||||
return Pipe12(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12)
|
||||
}
|
||||
@@ -902,6 +911,7 @@ func Pipe13[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow13[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13) func(T0) T13 {
|
||||
//go:inline
|
||||
return func(t0 T0) T13 {
|
||||
return Pipe13(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13)
|
||||
}
|
||||
@@ -986,6 +996,7 @@ func Pipe14[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow14[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14) func(T0) T14 {
|
||||
//go:inline
|
||||
return func(t0 T0) T14 {
|
||||
return Pipe14(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14)
|
||||
}
|
||||
@@ -1072,6 +1083,7 @@ func Pipe15[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow15[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15) func(T0) T15 {
|
||||
//go:inline
|
||||
return func(t0 T0) T15 {
|
||||
return Pipe15(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15)
|
||||
}
|
||||
@@ -1160,6 +1172,7 @@ func Pipe16[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow16[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16) func(T0) T16 {
|
||||
//go:inline
|
||||
return func(t0 T0) T16 {
|
||||
return Pipe16(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16)
|
||||
}
|
||||
@@ -1250,6 +1263,7 @@ func Pipe17[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow17[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17) func(T0) T17 {
|
||||
//go:inline
|
||||
return func(t0 T0) T17 {
|
||||
return Pipe17(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17)
|
||||
}
|
||||
@@ -1342,6 +1356,7 @@ func Pipe18[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow18[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, F18 ~func(T17) T18, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17, f18 F18) func(T0) T18 {
|
||||
//go:inline
|
||||
return func(t0 T0) T18 {
|
||||
return Pipe18(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18)
|
||||
}
|
||||
@@ -1436,6 +1451,7 @@ func Pipe19[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow19[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, F18 ~func(T17) T18, F19 ~func(T18) T19, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17, f18 F18, f19 F19) func(T0) T19 {
|
||||
//go:inline
|
||||
return func(t0 T0) T19 {
|
||||
return Pipe19(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19)
|
||||
}
|
||||
@@ -1532,6 +1548,7 @@ func Pipe20[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4,
|
||||
// The final return value is the result of the last function application
|
||||
//go:inline
|
||||
func Flow20[F1 ~func(T0) T1, F2 ~func(T1) T2, F3 ~func(T2) T3, F4 ~func(T3) T4, F5 ~func(T4) T5, F6 ~func(T5) T6, F7 ~func(T6) T7, F8 ~func(T7) T8, F9 ~func(T8) T9, F10 ~func(T9) T10, F11 ~func(T10) T11, F12 ~func(T11) T12, F13 ~func(T12) T13, F14 ~func(T13) T14, F15 ~func(T14) T15, F16 ~func(T15) T16, F17 ~func(T16) T17, F18 ~func(T17) T18, F19 ~func(T18) T19, F20 ~func(T19) T20, T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10, f11 F11, f12 F12, f13 F13, f14 F14, f15 F15, f16 F16, f17 F17, f18 F18, f19 F19, f20 F20) func(T0) T20 {
|
||||
//go:inline
|
||||
return func(t0 T0) T20 {
|
||||
return Pipe20(t0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16, f17, f18, f19, f20)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ go 1.24
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/urfave/cli/v3 v3.7.0
|
||||
github.com/urfave/cli/v3 v3.8.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -4,10 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
|
||||
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
|
||||
github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
github.com/urfave/cli/v3 v3.8.0 h1:XqKPrm0q4P0q5JpoclYoCAv0/MIvH/jZ2umzuf8pNTI=
|
||||
github.com/urfave/cli/v3 v3.8.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
package array
|
||||
|
||||
import "slices"
|
||||
|
||||
func Of[GA ~[]A, A any](a A) GA {
|
||||
return GA{a}
|
||||
}
|
||||
@@ -197,3 +199,9 @@ func Reverse[GT ~[]T, T any](as GT) GT {
|
||||
}
|
||||
return ras
|
||||
}
|
||||
|
||||
func UnsafeUpdateAt[GT ~[]T, T any](as GT, i int, v T) GT {
|
||||
c := slices.Clone(as)
|
||||
c[i] = v
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ import (
|
||||
// - Multiple elements: recursively divides and conquers
|
||||
func MonadSequenceSegment[HKTB, HKTRB any](
|
||||
fof func(HKTB) HKTRB,
|
||||
empty HKTRB,
|
||||
empty func() HKTRB,
|
||||
concat func(HKTRB, HKTRB) HKTRB,
|
||||
fbs []HKTB,
|
||||
start, end int,
|
||||
@@ -54,7 +54,7 @@ func MonadSequenceSegment[HKTB, HKTRB any](
|
||||
|
||||
switch end - start {
|
||||
case 0:
|
||||
return empty
|
||||
return empty()
|
||||
case 1:
|
||||
return fof(fbs[start])
|
||||
default:
|
||||
@@ -254,7 +254,7 @@ HKTAB = HKT<func(A)B>
|
||||
*/
|
||||
func MonadSequence[GA ~[]HKTA, HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
empty HKTRA,
|
||||
empty func() HKTRA,
|
||||
concat func(HKTRA, HKTRA) HKTRA,
|
||||
|
||||
ta GA) HKTRA {
|
||||
@@ -263,7 +263,7 @@ func MonadSequence[GA ~[]HKTA, HKTA, HKTRA any](
|
||||
|
||||
func Sequence[GA ~[]HKTA, HKTA, HKTRA any](
|
||||
fof func(HKTA) HKTRA,
|
||||
empty HKTRA,
|
||||
empty func() HKTRA,
|
||||
concat func(HKTRA, HKTRA) HKTRA,
|
||||
) func(GA) HKTRA {
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package filterable
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
Separated[A, B any] = pair.Pair[A, B]
|
||||
|
||||
FilterType[A, HKTA any] = func(func(A) bool) func(HKTA) HKTA
|
||||
|
||||
FilterMapType[A, B, HKTA, HKTB any] = func(func(A) Option[B]) func(HKTA) HKTB
|
||||
)
|
||||
@@ -73,7 +73,7 @@ func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A
|
||||
|
||||
fof := F.Bind2nd(fmap_b, Of[GB])
|
||||
|
||||
empty := fof_gb(Empty[GB]())
|
||||
empty := F.Nullary2(Empty[GB], fof_gb)
|
||||
|
||||
cb := F.Curry2(Concat[GB])
|
||||
concat_gb := F.Bind2nd(fmap_gb, cb)
|
||||
@@ -180,7 +180,7 @@ func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
|
||||
|
||||
// convert to an array
|
||||
hktb := ToArray[GA, []HKTA](ta)
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty, m.Concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
// MonadTraverseWithIndex traverses an iterator sequence with index tracking, applying an effectful
|
||||
@@ -223,7 +223,7 @@ func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
|
||||
|
||||
// convert to an array
|
||||
hktb := MonadMapToArrayWithIndex[GA, []HKTB](ta, f)
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
|
||||
return INTA.MonadSequenceSegment(fof, m.Empty, m.Concat, hktb, 0, len(hktb))
|
||||
}
|
||||
|
||||
// Sequence is the curried version of MonadSequence, returning a function that sequences an iterator of effects.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package witherable
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/filterable"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
func Filter[A, HKT_G_A, HKT_F_HKT_G_A any](
|
||||
fmap functor.MapType[HKT_G_A, HKT_G_A, HKT_F_HKT_G_A, HKT_F_HKT_G_A],
|
||||
ffilter filterable.FilterType[A, HKT_G_A],
|
||||
) func(func(A) bool) func(HKT_F_HKT_G_A) HKT_F_HKT_G_A {
|
||||
return function.Flow2(
|
||||
ffilter,
|
||||
fmap,
|
||||
)
|
||||
}
|
||||
|
||||
func FilterMap[A, B, HKT_G_A, HKT_G_B, HKT_F_HKT_G_A, HKT_F_HKT_G_B any](
|
||||
fmap functor.MapType[HKT_G_A, HKT_G_B, HKT_F_HKT_G_A, HKT_F_HKT_G_B],
|
||||
ffilter filterable.FilterMapType[A, B, HKT_G_A, HKT_G_B],
|
||||
) func(func(A) Option[B]) func(HKT_F_HKT_G_A) HKT_F_HKT_G_B {
|
||||
return function.Flow2(
|
||||
ffilter,
|
||||
fmap,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package witherable
|
||||
@@ -0,0 +1,7 @@
|
||||
package witherable
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
|
||||
type (
|
||||
Option[A any] = option.Option[A]
|
||||
)
|
||||
@@ -0,0 +1,254 @@
|
||||
// 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 (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
)
|
||||
|
||||
// AsyncBuf converts a synchronous sequence into an asynchronous buffered sequence.
|
||||
// It spawns a goroutine to consume the input sequence and sends values through
|
||||
// a buffered channel, allowing concurrent production and consumption of elements.
|
||||
//
|
||||
// The function provides backpressure control through the buffer size and properly
|
||||
// handles early termination when the consumer stops iterating. This is useful for
|
||||
// decoupling producers and consumers, enabling pipeline parallelism, or when you
|
||||
// need to process sequences concurrently.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - T: The type of elements in the sequence
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - input: The source sequence to be consumed asynchronously
|
||||
// - bufSize: The buffer size for the channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// but uses more memory. A buffer of 0 creates an unbuffered channel requiring
|
||||
// synchronization between producer and consumer.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Seq[T]: A new sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - Spawns a goroutine that consumes the input sequence
|
||||
// - Elements are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
|
||||
// the producer goroutine is signaled to stop via a done channel
|
||||
// - Both the producer goroutine and the done channel are properly cleaned up
|
||||
// - The channel is closed when the input sequence is exhausted or early termination occurs
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Create an async sequence with a buffer of 10
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// async := AsyncBuf(seq, 10)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for v := range async {
|
||||
// fmt.Println(v) // Prints: 1, 2, 3, 4, 5
|
||||
// }
|
||||
//
|
||||
// # Example with Early Termination
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// async := AsyncBuf(seq, 5)
|
||||
//
|
||||
// // Stop after 3 elements - producer goroutine will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range async {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 3 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # Example with Unbuffered Channel
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// seq := From(1, 2, 3)
|
||||
// async := AsyncBuf(seq, 0)
|
||||
//
|
||||
// // Producer and consumer are synchronized
|
||||
// for v := range async {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - From: Creates a sequence from values
|
||||
// - Map: Transforms sequence elements
|
||||
// - Filter: Filters sequence elements
|
||||
func AsyncBuf[T any](input Seq[T], bufSize int) Seq[T] {
|
||||
return MergeBuf(A.Of(input), bufSize)
|
||||
}
|
||||
|
||||
// Async converts a synchronous sequence into an asynchronous sequence using a default buffer size.
|
||||
// This is a convenience wrapper around AsyncBuf that uses a default buffer size of 8.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - input: The source sequence to be consumed asynchronously
|
||||
//
|
||||
// Returns:
|
||||
// - Seq[T]: A new sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Spawns a goroutine that consumes the input sequence
|
||||
// - Elements are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - The channel is closed when the input sequence is exhausted
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// async := Async(seq)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for v := range async {
|
||||
// fmt.Println(v) // Prints: 1, 2, 3, 4, 5
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - AsyncBuf: Async with custom buffer size
|
||||
// - Async2: Asynchronous sequence for key-value sequences
|
||||
// - Merge: Merges multiple sequences concurrently
|
||||
func Async[T any](input Seq[T]) Seq[T] {
|
||||
return AsyncBuf(input, defaultBufferSize)
|
||||
}
|
||||
|
||||
// Async2Buf converts a synchronous key-value sequence into an asynchronous buffered sequence.
|
||||
// It spawns a goroutine to consume the input sequence and sends key-value pairs through
|
||||
// a buffered channel, allowing concurrent production and consumption of elements.
|
||||
//
|
||||
// This function is the Seq2 variant of Async, providing the same asynchronous behavior
|
||||
// for key-value sequences. It internally converts the Seq2 to a sequence of Pairs,
|
||||
// applies Async, and converts back to Seq2.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - K: The type of keys in the sequence
|
||||
// - V: The type of values in the sequence
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - input: The source key-value sequence to be consumed asynchronously
|
||||
// - bufSize: The buffer size for the channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// but uses more memory. A buffer of 0 creates an unbuffered channel requiring
|
||||
// synchronization between producer and consumer.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Seq2[K, V]: A new key-value sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// - Spawns a goroutine that consumes the input key-value sequence
|
||||
// - Key-value pairs are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
|
||||
// the producer goroutine is signaled to stop via a done channel
|
||||
// - Both the producer goroutine and the done channel are properly cleaned up
|
||||
// - The channel is closed when the input sequence is exhausted or early termination occurs
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Create an async key-value sequence with a buffer of 10
|
||||
// seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
// async := Async2Buf(seq, 10)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for k, v := range async {
|
||||
// fmt.Printf("%d: %s\n", k, v)
|
||||
// }
|
||||
// // Output:
|
||||
// // 1: a
|
||||
// // 2: b
|
||||
// // 3: c
|
||||
//
|
||||
// # Example with Early Termination
|
||||
//
|
||||
// seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
// async := Async2Buf(seq, 5)
|
||||
//
|
||||
// // Stop after 2 pairs - producer goroutine will be properly cleaned up
|
||||
// count := 0
|
||||
// for k, v := range async {
|
||||
// fmt.Printf("%d: %s\n", k, v)
|
||||
// count++
|
||||
// if count >= 2 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Async: Asynchronous sequence for single-value sequences
|
||||
// - ToSeqPair: Converts Seq2 to Seq of Pairs
|
||||
// - FromSeqPair: Converts Seq of Pairs to Seq2
|
||||
// - MonadZip: Creates key-value sequences from two sequences
|
||||
func Async2Buf[K, V any](input Seq2[K, V], bufSize int) Seq2[K, V] {
|
||||
return FromSeqPair(AsyncBuf(ToSeqPair(input), bufSize))
|
||||
}
|
||||
|
||||
// Async2 converts a synchronous key-value sequence into an asynchronous sequence using a default buffer size.
|
||||
// This is a convenience wrapper around Async2Buf that uses a default buffer size of 8.
|
||||
// It's the Seq2 variant of Async, providing the same asynchronous behavior for key-value sequences.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - K: The type of keys in the sequence
|
||||
// - V: The type of values in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - input: The source key-value sequence to be consumed asynchronously
|
||||
//
|
||||
// Returns:
|
||||
// - Seq2[K, V]: A new key-value sequence that yields elements from the input sequence asynchronously
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Spawns a goroutine that consumes the input key-value sequence
|
||||
// - Key-value pairs are sent through a buffered channel to the output sequence
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - The channel is closed when the input sequence is exhausted
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
// async := Async2(seq)
|
||||
//
|
||||
// // Elements are produced concurrently
|
||||
// for k, v := range async {
|
||||
// fmt.Printf("%d: %s\n", k, v)
|
||||
// }
|
||||
// // Output:
|
||||
// // 1: a
|
||||
// // 2: b
|
||||
// // 3: c
|
||||
//
|
||||
// See Also:
|
||||
// - Async2Buf: Async2 with custom buffer size
|
||||
// - Async: Asynchronous sequence for single-value sequences
|
||||
// - MonadZip: Creates key-value sequences from two sequences
|
||||
func Async2[K, V any](input Seq2[K, V]) Seq2[K, V] {
|
||||
return Async2Buf(input, defaultBufferSize)
|
||||
}
|
||||
@@ -0,0 +1,903 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestAsync_Success tests basic Async functionality
|
||||
func TestAsync_Success(t *testing.T) {
|
||||
t.Run("converts sequence to async with buffer", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("preserves element order", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "d", "e")
|
||||
async := AsyncBuf(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []string{"a", "b", "c", "d", "e"}, result)
|
||||
})
|
||||
|
||||
t.Run("works with single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
async := AsyncBuf(seq, 1)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("works with large sequence", func(t *testing.T) {
|
||||
data := make([]int, 100)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
async := AsyncBuf(seq, 20)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, data, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_BufferSizes tests different buffer sizes
|
||||
func TestAsync_BufferSizes(t *testing.T) {
|
||||
t.Run("unbuffered channel (bufSize 0)", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, 0)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("small buffer", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 2)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("large buffer", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 100)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("negative buffer size treated as 0", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, -5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("buffer size equals sequence length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("buffer size larger than sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_Empty tests Async with empty sequences
|
||||
func TestAsync_Empty(t *testing.T) {
|
||||
t.Run("empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
async := AsyncBuf(seq, 5)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
async := AsyncBuf(seq, 10)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty with zero buffer", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
async := AsyncBuf(seq, 0)
|
||||
result := toSlice(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_EarlyTermination tests that Async properly handles early termination
|
||||
func TestAsync_EarlyTermination(t *testing.T) {
|
||||
t.Run("stops producer when consumer breaks", func(t *testing.T) {
|
||||
var producerCount atomic.Int32
|
||||
|
||||
// Create a sequence that tracks how many elements were produced
|
||||
seq := func(yield func(int) bool) {
|
||||
for i := range 100 {
|
||||
producerCount.Add(1)
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async := AsyncBuf(seq, 10)
|
||||
|
||||
// Consume only 5 elements
|
||||
count := 0
|
||||
for range async {
|
||||
count++
|
||||
if count >= 5 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Give goroutine time to clean up
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// Producer should have stopped shortly after consumer stopped
|
||||
// It may produce a few extra due to buffering, but not all 100
|
||||
produced := producerCount.Load()
|
||||
assert.LessOrEqual(t, produced, int32(20), "producer should stop after consumer breaks")
|
||||
assert.GreaterOrEqual(t, produced, int32(5), "producer should produce at least what was consumed")
|
||||
})
|
||||
|
||||
t.Run("handles yield returning false", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
|
||||
collected := []int{}
|
||||
for v := range async {
|
||||
collected = append(collected, v)
|
||||
if v == 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
|
||||
t.Run("early termination with unbuffered channel", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 0)
|
||||
|
||||
collected := []int{}
|
||||
for v := range async {
|
||||
collected = append(collected, v)
|
||||
if v == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{1, 2}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_WithComplexTypes tests Async with complex data types
|
||||
func TestAsync_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("works with structs", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
async := AsyncBuf(seq, 5)
|
||||
result := toSlice(async)
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Charlie", 35},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("works with pointers", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []*Person{p1, p2, p3}, result)
|
||||
})
|
||||
|
||||
t.Run("works with slices", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3, 4}, []int{5, 6})
|
||||
async := AsyncBuf(seq, 2)
|
||||
result := toSlice(async)
|
||||
expected := [][]int{{1, 2}, {3, 4}, {5, 6}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("works with maps", func(t *testing.T) {
|
||||
m1 := map[string]int{"a": 1}
|
||||
m2 := map[string]int{"b": 2}
|
||||
m3 := map[string]int{"c": 3}
|
||||
seq := From(m1, m2, m3)
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []map[string]int{m1, m2, m3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_WithChainedOperations tests Async with other sequence operations
|
||||
func TestAsync_WithChainedOperations(t *testing.T) {
|
||||
t.Run("async after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
async := AsyncBuf(mapped, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("map after async", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
mapped := MonadMap(async, N.Mul(2))
|
||||
result := toSlice(mapped)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("async after filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
async := AsyncBuf(filtered, 5)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after async", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
filtered := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("async after chain", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
async := AsyncBuf(chained, 10)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple async operations", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async1 := AsyncBuf(seq, 3)
|
||||
async2 := AsyncBuf(async1, 2)
|
||||
result := toSlice(async2)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_Concurrency tests concurrent behavior
|
||||
func TestAsync_Concurrency(t *testing.T) {
|
||||
t.Run("allows concurrent production and consumption", func(t *testing.T) {
|
||||
// Create a slow producer
|
||||
seq := func(yield func(int) bool) {
|
||||
for i := range 5 {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
if !yield(i) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async := AsyncBuf(seq, 10)
|
||||
|
||||
result := toSlice(async)
|
||||
|
||||
// Verify all elements are produced correctly
|
||||
assert.Equal(t, []int{0, 1, 2, 3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("handles concurrent consumption safely", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
|
||||
// Consume with some processing time
|
||||
var sum atomic.Int32
|
||||
for v := range async {
|
||||
sum.Add(int32(v))
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
assert.Equal(t, int32(55), sum.Load())
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync_EdgeCases tests edge cases
|
||||
func TestAsync_EdgeCases(t *testing.T) {
|
||||
t.Run("very large buffer size", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, 1000000)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("buffer size of 1", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 1)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("works with replicate", func(t *testing.T) {
|
||||
seq := Replicate(5, 42)
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{42, 42, 42, 42, 42}, result)
|
||||
})
|
||||
|
||||
t.Run("works with makeBy", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i * i })
|
||||
async := AsyncBuf(seq, 3)
|
||||
result := toSlice(async)
|
||||
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkAsync(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 5)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_LargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_SmallBuffer(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 1)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_LargeBuffer(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_Unbuffered(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 0)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_WithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 5)
|
||||
mapped := MonadMap(async, N.Mul(2))
|
||||
for range mapped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync_WithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := AsyncBuf(seq, 5)
|
||||
filtered := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
for range filtered {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleAsync() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 10)
|
||||
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 4 5
|
||||
}
|
||||
|
||||
func ExampleAsync_unbuffered() {
|
||||
seq := From(1, 2, 3)
|
||||
async := AsyncBuf(seq, 0)
|
||||
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleAsync_earlyTermination() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
|
||||
count := 0
|
||||
for v := range async {
|
||||
fmt.Printf("%d ", v)
|
||||
count++
|
||||
if count >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleAsync_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
async := AsyncBuf(seq, 5)
|
||||
doubled := MonadMap(async, N.Mul(2))
|
||||
|
||||
for v := range doubled {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 8 10
|
||||
}
|
||||
|
||||
func ExampleAsync_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
async := AsyncBuf(seq, 5)
|
||||
evens := MonadFilter(async, func(x int) bool { return x%2 == 0 })
|
||||
|
||||
for v := range evens {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 8 10
|
||||
}
|
||||
|
||||
// TestAsync2_Success tests basic Async2 functionality
|
||||
func TestAsync2_Success(t *testing.T) {
|
||||
t.Run("converts Seq2 to async with buffer", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, 10)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("preserves key-value pairs order", func(t *testing.T) {
|
||||
seq := MonadZip(From("x", "y", "z"), From(10, 20, 30))
|
||||
async := Async2Buf(seq, 5)
|
||||
|
||||
keys := []string{}
|
||||
values := []int{}
|
||||
for k, v := range async {
|
||||
keys = append(keys, k)
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"x", "y", "z"}, keys)
|
||||
assert.Equal(t, []int{10, 20, 30}, values)
|
||||
})
|
||||
|
||||
t.Run("works with single pair", func(t *testing.T) {
|
||||
seq := Of2("key", 42)
|
||||
async := Async2Buf(seq, 1)
|
||||
result := toMap(async)
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
})
|
||||
|
||||
t.Run("works with large Seq2", func(t *testing.T) {
|
||||
keys := make([]int, 100)
|
||||
values := make([]string, 100)
|
||||
for i := range keys {
|
||||
keys[i] = i
|
||||
values[i] = fmt.Sprintf("val%d", i)
|
||||
}
|
||||
seq := MonadZip(From(keys...), From(values...))
|
||||
async := Async2Buf(seq, 20)
|
||||
result := toMap(async)
|
||||
assert.Equal(t, 100, len(result))
|
||||
for i := range 100 {
|
||||
assert.Equal(t, fmt.Sprintf("val%d", i), result[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_BufferSizes tests different buffer sizes
|
||||
func TestAsync2_BufferSizes(t *testing.T) {
|
||||
t.Run("unbuffered channel (bufSize 0)", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, 0)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("negative buffer size treated as 0", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, -5)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("large buffer", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, 100)
|
||||
result := toMap(async)
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_Empty tests Async2 with empty sequences
|
||||
func TestAsync2_Empty(t *testing.T) {
|
||||
t.Run("empty Seq2", func(t *testing.T) {
|
||||
seq := MonadZip(Empty[int](), Empty[string]())
|
||||
async := Async2Buf(seq, 5)
|
||||
result := toMap(async)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_EarlyTermination tests that Async2 properly handles early termination
|
||||
func TestAsync2_EarlyTermination(t *testing.T) {
|
||||
t.Run("stops producer when consumer breaks", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), From("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))
|
||||
async := Async2Buf(seq, 5)
|
||||
|
||||
count := 0
|
||||
for range async {
|
||||
count++
|
||||
if count >= 3 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, count)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAsync2_WithChainedOperations tests Async2 with other operations
|
||||
func TestAsync2_WithChainedOperations(t *testing.T) {
|
||||
t.Run("async2 after map", func(t *testing.T) {
|
||||
seq := MonadZip(From(1, 2, 3), From(10, 20, 30))
|
||||
mapped := MonadMapWithKey(seq, func(k, v int) int { return k + v })
|
||||
async := Async2Buf(mapped, 5)
|
||||
result := toMap(async)
|
||||
expected := map[int]int{1: 11, 2: 22, 3: 33}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair_Success tests basic ToSeqPair functionality
|
||||
func TestToSeqPair_Success(t *testing.T) {
|
||||
t.Run("converts Seq2 to Seq of Pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, 1, pair.Head(result[0]))
|
||||
assert.Equal(t, "a", pair.Tail(result[0]))
|
||||
assert.Equal(t, 2, pair.Head(result[1]))
|
||||
assert.Equal(t, "b", pair.Tail(result[1]))
|
||||
assert.Equal(t, 3, pair.Head(result[2]))
|
||||
assert.Equal(t, "c", pair.Tail(result[2]))
|
||||
})
|
||||
|
||||
t.Run("preserves order", func(t *testing.T) {
|
||||
seq2 := MonadZip(From("x", "y", "z"), From(10, 20, 30))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
for i, p := range result {
|
||||
expectedKey := string(rune('x' + i))
|
||||
expectedVal := (i + 1) * 10
|
||||
assert.Equal(t, expectedKey, pair.Head(p))
|
||||
assert.Equal(t, expectedVal, pair.Tail(p))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("works with single pair", func(t *testing.T) {
|
||||
seq2 := Of2("key", 42)
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "key", pair.Head(result[0]))
|
||||
assert.Equal(t, 42, pair.Tail(result[0]))
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair_Empty tests ToSeqPair with empty sequences
|
||||
func TestToSeqPair_Empty(t *testing.T) {
|
||||
t.Run("empty Seq2 produces empty Seq", func(t *testing.T) {
|
||||
seq2 := MonadZip(Empty[int](), Empty[string]())
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair_WithComplexTypes tests ToSeqPair with complex types
|
||||
func TestToSeqPair_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("works with struct values", func(t *testing.T) {
|
||||
seq2 := MonadZip(
|
||||
From(1, 2, 3),
|
||||
From(Person{"Alice", 30}, Person{"Bob", 25}, Person{"Charlie", 35}),
|
||||
)
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, 1, pair.Head(result[0]))
|
||||
assert.Equal(t, Person{"Alice", 30}, pair.Tail(result[0]))
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSeqPair_Success tests basic FromSeqPair functionality
|
||||
func TestFromSeqPair_Success(t *testing.T) {
|
||||
t.Run("converts Seq of Pairs to Seq2", func(t *testing.T) {
|
||||
pairs := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("preserves order", func(t *testing.T) {
|
||||
pairs := From(
|
||||
pair.MakePair("x", 10),
|
||||
pair.MakePair("y", 20),
|
||||
pair.MakePair("z", 30),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
|
||||
keys := []string{}
|
||||
values := []int{}
|
||||
for k, v := range seq2 {
|
||||
keys = append(keys, k)
|
||||
values = append(values, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"x", "y", "z"}, keys)
|
||||
assert.Equal(t, []int{10, 20, 30}, values)
|
||||
})
|
||||
|
||||
t.Run("works with single pair", func(t *testing.T) {
|
||||
pairs := From(pair.MakePair("key", 42))
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSeqPair_Empty tests FromSeqPair with empty sequences
|
||||
func TestFromSeqPair_Empty(t *testing.T) {
|
||||
t.Run("empty Seq produces empty Seq2", func(t *testing.T) {
|
||||
pairs := Empty[Pair[int, string]]()
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFromSeqPair_WithComplexTypes tests FromSeqPair with complex types
|
||||
func TestFromSeqPair_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("works with struct values", func(t *testing.T) {
|
||||
pairs := From(
|
||||
pair.MakePair(1, Person{"Alice", 30}),
|
||||
pair.MakePair(2, Person{"Bob", 25}),
|
||||
pair.MakePair(3, Person{"Charlie", 35}),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
result := toMap(seq2)
|
||||
|
||||
expected := map[int]Person{
|
||||
1: {"Alice", 30},
|
||||
2: {"Bob", 25},
|
||||
3: {"Charlie", 35},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestRoundTrip tests that ToSeqPair and FromSeqPair are inverses
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
t.Run("ToSeqPair then FromSeqPair", func(t *testing.T) {
|
||||
original := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(original)
|
||||
restored := FromSeqPair(pairs)
|
||||
result := toMap(restored)
|
||||
|
||||
expected := map[int]string{1: "a", 2: "b", 3: "c"}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("FromSeqPair then ToSeqPair", func(t *testing.T) {
|
||||
original := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
)
|
||||
seq2 := FromSeqPair(original)
|
||||
restored := ToSeqPair(seq2)
|
||||
result := toSlice(restored)
|
||||
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, 1, pair.Head(result[0]))
|
||||
assert.Equal(t, "a", pair.Tail(result[0]))
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for Async2
|
||||
func BenchmarkAsync2(b *testing.B) {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), From("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async2Buf(seq, 5)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsync2_LargeSequence(b *testing.B) {
|
||||
keys := make([]int, 1000)
|
||||
values := make([]string, 1000)
|
||||
for i := range keys {
|
||||
keys[i] = i
|
||||
values[i] = fmt.Sprintf("val%d", i)
|
||||
}
|
||||
seq := MonadZip(From(keys...), From(values...))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
async := Async2Buf(seq, 100)
|
||||
for range async {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests for FromSeqPair
|
||||
func BenchmarkFromSeqPair(b *testing.B) {
|
||||
pairs := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
pair.MakePair(4, "d"),
|
||||
pair.MakePair(5, "e"),
|
||||
)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
seq2 := FromSeqPair(pairs)
|
||||
for range seq2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRoundTrip(b *testing.B) {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
pairs := ToSeqPair(seq)
|
||||
restored := FromSeqPair(pairs)
|
||||
for range restored {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for Async2
|
||||
func ExampleAsync2() {
|
||||
seq := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
async := Async2Buf(seq, 10)
|
||||
|
||||
for k, v := range async {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
}
|
||||
// Output:
|
||||
// 1: a
|
||||
// 2: b
|
||||
// 3: c
|
||||
}
|
||||
|
||||
func ExampleAsync2_earlyTermination() {
|
||||
seq := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
async := Async2Buf(seq, 5)
|
||||
|
||||
count := 0
|
||||
for k, v := range async {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
count++
|
||||
if count >= 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Output:
|
||||
// 1: a
|
||||
// 2: b
|
||||
}
|
||||
|
||||
// Example tests for FromSeqPair
|
||||
func ExampleFromSeqPair() {
|
||||
pairs := From(
|
||||
pair.MakePair(1, "a"),
|
||||
pair.MakePair(2, "b"),
|
||||
pair.MakePair(3, "c"),
|
||||
)
|
||||
seq2 := FromSeqPair(pairs)
|
||||
|
||||
for k, v := range seq2 {
|
||||
fmt.Printf("%d: %s\n", k, v)
|
||||
}
|
||||
// Output:
|
||||
// 1: a
|
||||
// 2: b
|
||||
// 3: c
|
||||
}
|
||||
@@ -34,6 +34,13 @@ import (
|
||||
// 3. Filtering to keep only pairs where the boolean (tail) is true
|
||||
// 4. Extracting the original values (head) from the filtered pairs
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Data: --1--2--3--4--5-->
|
||||
// Selectors: --T--F--T--F--T-->
|
||||
// Compress
|
||||
// Output: --1-----3-----5-->
|
||||
//
|
||||
// RxJS Equivalent: Similar to combining [zip] with [filter] - https://rxjs.dev/api/operators/zip
|
||||
//
|
||||
// Type Parameters:
|
||||
|
||||
@@ -21,6 +21,12 @@ package iter
|
||||
// all elements repeatedly. When the end of the input sequence is reached, it starts over
|
||||
// from the beginning, continuing this pattern forever.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3|
|
||||
// Cycle
|
||||
// Output: --1--2--3--1--2--3--1--2--3--> (infinite)
|
||||
//
|
||||
// RxJS Equivalent: [repeat] - https://rxjs.dev/api/operators/repeat
|
||||
//
|
||||
// WARNING: This creates an INFINITE sequence for non-empty inputs. It must be used with
|
||||
|
||||
@@ -23,6 +23,16 @@ import "github.com/IBM/fp-go/v2/option"
|
||||
// contains at least one element, it returns Some(element). If the iterator is empty,
|
||||
// it returns None. The function consumes only the first element of the iterator.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// First
|
||||
// Output: --Some(1)|
|
||||
//
|
||||
// Input: --|
|
||||
// First
|
||||
// Output: --None|
|
||||
//
|
||||
// RxJS Equivalent: [first] - https://rxjs.dev/api/operators/first
|
||||
//
|
||||
// Type Parameters:
|
||||
|
||||
@@ -82,6 +82,12 @@ 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.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3-->
|
||||
// Map(x => x * 2)
|
||||
// Output: --2--4--6-->
|
||||
//
|
||||
// RxJS Equivalent: [map] - https://rxjs.dev/api/operators/map
|
||||
//
|
||||
// Example:
|
||||
@@ -186,6 +192,12 @@ 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.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// Filter(x => x % 2 == 0)
|
||||
// Output: -----2-----4----->
|
||||
//
|
||||
// RxJS Equivalent: [filter] - https://rxjs.dev/api/operators/filter
|
||||
//
|
||||
// Example:
|
||||
@@ -293,6 +305,12 @@ func FilterWithKey[K, A any](pred func(K, A) bool) Operator2[K, A, A] {
|
||||
// MonadFilterMap applies a function that returns an Option to each element,
|
||||
// keeping only the Some values and unwrapping them.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// FilterMap(x => x % 2 == 0 ? Some(x * 10) : None)
|
||||
// Output: -----20----40---->
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
@@ -430,6 +448,12 @@ 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).
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1-----2-----3---->
|
||||
// Chain(x => [x, x*10])
|
||||
// Output: --1-10--2-20--3-30->
|
||||
//
|
||||
// RxJS Equivalent: [mergeMap/flatMap] - https://rxjs.dev/api/operators/mergeMap
|
||||
//
|
||||
// Example:
|
||||
@@ -471,8 +495,34 @@ func FlatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
// ConcatMap is an alias for Chain that emphasizes sequential concatenation.
|
||||
// It maps each element to a sequence and concatenates the results in order.
|
||||
//
|
||||
// Unlike concurrent operations, ConcatMap preserves the order of elements:
|
||||
// it fully processes each input element (yielding all elements from f(a))
|
||||
// before moving to the next input element.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
// result := ConcatMap(func(x int) Seq[int] {
|
||||
// return From(x, x*10)
|
||||
// })(seq)
|
||||
// // yields: 1, 10, 2, 20, 3, 30 (order preserved)
|
||||
//
|
||||
//go:inline
|
||||
func ConcatMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return Chain(f)
|
||||
}
|
||||
|
||||
// Flatten flattens a sequence of sequences into a single sequence.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --[1,2]--[3,4]--[5]-->
|
||||
// Flatten
|
||||
// Output: --1-2----3-4----5---->
|
||||
//
|
||||
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll
|
||||
//
|
||||
// Example:
|
||||
@@ -486,9 +536,22 @@ func Flatten[A any](mma Seq[Seq[A]]) Seq[A] {
|
||||
return MonadChain(mma, F.Identity[Seq[A]])
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ConcatAll[A any](mma Seq[Seq[A]]) Seq[A] {
|
||||
return Flatten(mma)
|
||||
}
|
||||
|
||||
// MonadAp applies a sequence of functions to a sequence of values.
|
||||
// This is the applicative apply operation.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Functions: --(*2)---(+10)-->
|
||||
// Values: --5------3------>
|
||||
// Ap
|
||||
// Output: --10-6---15-13-->
|
||||
// (each function applied to each value)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fns := From(N.Mul(2), N.Add(10))
|
||||
@@ -577,6 +640,13 @@ 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.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--|
|
||||
// Reduce((acc, x) => acc + x, 0)
|
||||
// Output: ------------------15|
|
||||
// (emits final result only)
|
||||
//
|
||||
// RxJS Equivalent: [reduce] - https://rxjs.dev/api/operators/reduce
|
||||
//
|
||||
// Example:
|
||||
@@ -811,6 +881,13 @@ func FoldMapWithKey[K, A, B any](m M.Monoid[B]) func(func(K, A) B) func(Seq2[K,
|
||||
// MonadFlap applies a fixed value to a sequence of functions.
|
||||
// This is the dual of MonadAp.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Functions: --(*2)---(+10)-->
|
||||
// Value: 5 (fixed)
|
||||
// Flap
|
||||
// Output: --10-----15----->
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fns := From(N.Mul(2), N.Add(10))
|
||||
@@ -832,6 +909,12 @@ 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.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: -----2--3--4-->
|
||||
// Prepend(1)
|
||||
// Output: --1--2--3--4-->
|
||||
//
|
||||
// RxJS Equivalent: [startWith] - https://rxjs.dev/api/operators/startWith
|
||||
//
|
||||
// Example:
|
||||
@@ -847,6 +930,12 @@ func Prepend[A any](head A) Operator[A, A] {
|
||||
|
||||
// Append returns a function that adds an element to the end of a sequence.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3-----|
|
||||
// Append(4)
|
||||
// Output: --1--2--3--4--|
|
||||
//
|
||||
// RxJS Equivalent: [endWith] - https://rxjs.dev/api/operators/endWith
|
||||
//
|
||||
// Example:
|
||||
@@ -863,6 +952,14 @@ 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.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// SeqA: --1--2--3---->
|
||||
// SeqB: --a--b------->
|
||||
// Zip
|
||||
// Output: --(1,a)-(2,b)|
|
||||
// (stops when shorter sequence ends)
|
||||
//
|
||||
// RxJS Equivalent: [zip] - https://rxjs.dev/api/operators/zip
|
||||
//
|
||||
// Example:
|
||||
@@ -1002,3 +1099,138 @@ func ToSeqPair[A, B any](as Seq2[A, B]) Seq[Pair[A, B]] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FromSeqPair converts a sequence of Pairs into a key-value sequence.
|
||||
//
|
||||
// This function transforms a Seq[Pair[A, B]] (which yields Pair objects when iterated)
|
||||
// into a Seq2[A, B] (which yields key-value pairs as separate arguments). This is the
|
||||
// inverse operation of ToSeqPair and is useful when you need to convert from working
|
||||
// with pairs as first-class values back to the key-value iteration pattern.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the first element (key) in each pair
|
||||
// - B: The type of the second element (value) in each pair
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - as: A Seq that yields Pair objects
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Seq2[A, B]: A key-value sequence that yields the unpacked pairs
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// // Create a sequence of pairs
|
||||
// pairs := From(
|
||||
// pair.MakePair("a", 1),
|
||||
// pair.MakePair("b", 2),
|
||||
// pair.MakePair("c", 3),
|
||||
// )
|
||||
// seq2 := FromSeqPair(pairs)
|
||||
//
|
||||
// // Iterate as key-value pairs
|
||||
// for k, v := range seq2 {
|
||||
// fmt.Printf("%s: %d\n", k, v)
|
||||
// }
|
||||
// // Output:
|
||||
// // a: 1
|
||||
// // b: 2
|
||||
// // c: 3
|
||||
//
|
||||
// # Example with Map
|
||||
//
|
||||
// pairs := From(
|
||||
// pair.MakePair(1, 10),
|
||||
// pair.MakePair(2, 20),
|
||||
// pair.MakePair(3, 30),
|
||||
// )
|
||||
// seq2 := FromSeqPair(pairs)
|
||||
//
|
||||
// // Use with Seq2 operations
|
||||
// mapped := MonadMapWithKey(seq2, func(k, v int) int {
|
||||
// return k + v
|
||||
// })
|
||||
// // yields: 11, 22, 33
|
||||
//
|
||||
// # Example - Round-trip conversion
|
||||
//
|
||||
// original := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
// pairs := ToSeqPair(original)
|
||||
// restored := FromSeqPair(pairs)
|
||||
// // restored is equivalent to original
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - ToSeqPair: Converts Seq2 to Seq of Pairs (inverse operation)
|
||||
// - MonadZip: Creates key-value sequences from two sequences
|
||||
// - pair.MakePair: Creates a Pair from two values
|
||||
// - pair.Unpack: Unpacks a Pair into two values
|
||||
func FromSeqPair[A, B any](as Seq[Pair[A, B]]) Seq2[A, B] {
|
||||
return func(yield func(A, B) bool) {
|
||||
for p := range as {
|
||||
if !yield(pair.Unpack(p)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip returns an operator that skips the first n elements of a sequence.
|
||||
//
|
||||
// This function creates a transformation that discards the first n elements from
|
||||
// the source sequence and yields all remaining elements. If n is less than or equal
|
||||
// to 0, all elements are yielded. If n is greater than or equal to the sequence length,
|
||||
// an empty sequence is returned.
|
||||
//
|
||||
// The operation is lazy and only consumes elements from the source sequence as needed.
|
||||
// The first n elements are consumed and discarded, then subsequent elements are yielded.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--6--7--8-->
|
||||
// Skip(3)
|
||||
// Output: -----------4--5--6--7--8-->
|
||||
//
|
||||
// RxJS Equivalent: [skip] - https://rxjs.dev/api/operators/skip
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - count: The number of elements to skip from the beginning of the sequence
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] by skipping the first count elements
|
||||
//
|
||||
// Example - Skip first 3 elements:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// result := Skip[int](3)(seq)
|
||||
// // yields: 4, 5
|
||||
//
|
||||
// Example - Skip more than available:
|
||||
//
|
||||
// seq := From(1, 2)
|
||||
// result := Skip[int](5)(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - Skip zero or negative:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
// result := Skip[int](0)(seq)
|
||||
// // yields: 1, 2, 3 (all elements)
|
||||
//
|
||||
// Example - Chaining with other operations:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// result := F.Pipe2(
|
||||
// seq,
|
||||
// Skip[int](3),
|
||||
// MonadFilter(seq, func(x int) bool { return x%2 == 0 }),
|
||||
// )
|
||||
// // yields: 4, 6, 8, 10 (skip first 3, then filter evens)
|
||||
func Skip[U any](count int) Operator[U, U] {
|
||||
return FilterWithIndex(func(idx int, _ U) bool { return idx >= count })
|
||||
}
|
||||
|
||||
@@ -612,3 +612,440 @@ func TestMapToArrayIdentity(t *testing.T) {
|
||||
result := mapper(seq)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
}
|
||||
|
||||
// TestSkip tests basic Skip functionality
|
||||
func TestSkip(t *testing.T) {
|
||||
t.Run("skips first n elements from sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Skip[int](3)(seq))
|
||||
assert.Equal(t, []int{4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("skips first element", func(t *testing.T) {
|
||||
seq := From(10, 20, 30)
|
||||
result := toSlice(Skip[int](1)(seq))
|
||||
assert.Equal(t, []int{20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("skips all elements when n equals length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Skip[int](3)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips all elements when n exceeds length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Skip[int](10)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "d", "e")
|
||||
result := toSlice(Skip[string](2)(seq))
|
||||
assert.Equal(t, []string{"c", "d", "e"}, result)
|
||||
})
|
||||
|
||||
t.Run("skips from single element sequence", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Skip[int](1)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips from large sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := toSlice(Skip[int](7)(seq))
|
||||
assert.Equal(t, []int{8, 9, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipZeroOrNegative tests Skip with zero or negative values
|
||||
func TestSkipZeroOrNegative(t *testing.T) {
|
||||
t.Run("returns all elements when n is zero", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Skip[int](0)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("returns all elements when n is negative", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Skip[int](-1)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("returns all elements when n is large negative", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
result := toSlice(Skip[string](-100)(seq))
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipEmpty tests Skip with empty sequences
|
||||
func TestSkipEmpty(t *testing.T) {
|
||||
t.Run("returns empty from empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(Skip[int](5)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty from empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
result := toSlice(Skip[string](3)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when skipping zero from empty", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(Skip[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithComplexTypes tests Skip with complex data types
|
||||
func TestSkipWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("skips structs", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
result := toSlice(Skip[Person](2)(seq))
|
||||
expected := []Person{
|
||||
{"Charlie", 35},
|
||||
{"David", 28},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("skips pointers", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
result := toSlice(Skip[*Person](1)(seq))
|
||||
assert.Equal(t, []*Person{p2, p3}, result)
|
||||
})
|
||||
|
||||
t.Run("skips slices", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3, 4}, []int{5, 6}, []int{7, 8})
|
||||
result := toSlice(Skip[[]int](2)(seq))
|
||||
expected := [][]int{{5, 6}, {7, 8}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithChainedOperations tests Skip with other sequence operations
|
||||
func TestSkipWithChainedOperations(t *testing.T) {
|
||||
t.Run("skip after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
result := toSlice(Skip[int](2)(mapped))
|
||||
assert.Equal(t, []int{6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skip 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(Skip[int](2)(filtered))
|
||||
assert.Equal(t, []int{6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("map after skip", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
skipped := Skip[int](2)(seq)
|
||||
result := toSlice(MonadMap(skipped, N.Mul(10)))
|
||||
assert.Equal(t, []int{30, 40, 50}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after skip", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
skipped := Skip[int](2)(seq)
|
||||
result := toSlice(MonadFilter(skipped, func(x int) bool { return x%2 == 0 }))
|
||||
assert.Equal(t, []int{4, 6, 8}, result)
|
||||
})
|
||||
|
||||
t.Run("skip 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(Skip[int](3)(chained))
|
||||
assert.Equal(t, []int{20, 3, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple skips", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped1 := Skip[int](2)(seq)
|
||||
skipped2 := Skip[int](3)(skipped1)
|
||||
result := toSlice(skipped2)
|
||||
assert.Equal(t, []int{6, 7, 8, 9, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skip and take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped := Skip[int](3)(seq)
|
||||
taken := Take[int](3)(skipped)
|
||||
result := toSlice(taken)
|
||||
assert.Equal(t, []int{4, 5, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("take and skip", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken := Take[int](7)(seq)
|
||||
skipped := Skip[int](2)(taken)
|
||||
result := toSlice(skipped)
|
||||
assert.Equal(t, []int{3, 4, 5, 6, 7}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithReplicate tests Skip with Replicate
|
||||
func TestSkipWithReplicate(t *testing.T) {
|
||||
t.Run("skips from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(10, 42)
|
||||
result := toSlice(Skip[int](7)(seq))
|
||||
assert.Equal(t, []int{42, 42, 42}, result)
|
||||
})
|
||||
|
||||
t.Run("skips all from short replicate", func(t *testing.T) {
|
||||
seq := Replicate(2, "hello")
|
||||
result := toSlice(Skip[string](5)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips zero from replicate", func(t *testing.T) {
|
||||
seq := Replicate(3, 100)
|
||||
result := toSlice(Skip[int](0)(seq))
|
||||
assert.Equal(t, []int{100, 100, 100}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithMakeBy tests Skip with MakeBy
|
||||
func TestSkipWithMakeBy(t *testing.T) {
|
||||
t.Run("skips from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(10, func(i int) int { return i * i })
|
||||
result := toSlice(Skip[int](5)(seq))
|
||||
assert.Equal(t, []int{25, 36, 49, 64, 81}, result)
|
||||
})
|
||||
|
||||
t.Run("skips more than generated", func(t *testing.T) {
|
||||
seq := MakeBy(3, func(i int) int { return i + 1 })
|
||||
result := toSlice(Skip[int](10)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithPrependAppend tests Skip with Prepend and Append
|
||||
func TestSkipWithPrependAppend(t *testing.T) {
|
||||
t.Run("skip from prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3, 4, 5)
|
||||
prepended := Prepend(1)(seq)
|
||||
result := toSlice(Skip[int](2)(prepended))
|
||||
assert.Equal(t, []int{3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("skip from appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(Skip[int](2)(appended))
|
||||
assert.Equal(t, []int{3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("skip includes appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(Skip[int](3)(appended))
|
||||
assert.Equal(t, []int{4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWithFlatten tests Skip with Flatten
|
||||
func TestSkipWithFlatten(t *testing.T) {
|
||||
t.Run("skips from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5, 6))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(Skip[int](3)(flattened))
|
||||
assert.Equal(t, []int{4, 5, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("skips 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(Skip[int](2)(flattened))
|
||||
assert.Equal(t, []int{3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipDoesNotConsumeSkippedElements tests that Skip is efficient
|
||||
func TestSkipDoesNotConsumeSkippedElements(t *testing.T) {
|
||||
t.Run("processes all elements including skipped", 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
|
||||
})
|
||||
|
||||
skipped := Skip[int](7)(seq)
|
||||
|
||||
result := []int{}
|
||||
for v := range skipped {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{16, 18, 20}, result)
|
||||
// Skip still needs to iterate through skipped elements to count them
|
||||
assert.Equal(t, 10, callCount, "should process all elements")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipEdgeCases tests edge cases
|
||||
func TestSkipEdgeCases(t *testing.T) {
|
||||
t.Run("skip 0 from single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Skip[int](0)(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("skip 1 from single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Skip[int](1)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skip large number from small sequence", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
result := toSlice(Skip[int](1000000)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skip with very large n", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Skip[int](int(^uint(0) >> 1))(seq)) // max int
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skip all but one", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Skip[int](4)(seq))
|
||||
assert.Equal(t, []int{5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for Skip
|
||||
func BenchmarkSkip(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
skipped := Skip[int](5)(seq)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
skipped := Skip[int](900)(seq)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
skipped := Skip[int](5)(mapped)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
skipped := Skip[int](2)(filtered)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleSkip() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
skipped := Skip[int](3)(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 4 5
|
||||
}
|
||||
|
||||
func ExampleSkip_moreThanAvailable() {
|
||||
seq := From(1, 2, 3)
|
||||
skipped := Skip[int](10)(seq)
|
||||
|
||||
count := 0
|
||||
for range skipped {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleSkip_zero() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
skipped := Skip[int](0)(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 4 5
|
||||
}
|
||||
|
||||
func ExampleSkip_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
evens := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
skipped := Skip[int](2)(evens)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 6 8 10
|
||||
}
|
||||
|
||||
func ExampleSkip_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
doubled := MonadMap(seq, N.Mul(2))
|
||||
skipped := Skip[int](2)(doubled)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 6 8 10
|
||||
}
|
||||
|
||||
func ExampleSkip_chained() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := F.Pipe3(
|
||||
seq,
|
||||
Skip[int](3),
|
||||
Filter(func(x int) bool { return x%2 == 0 }),
|
||||
toSlice[int],
|
||||
)
|
||||
|
||||
fmt.Println(result)
|
||||
// Output: [4 6 8 10]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,16 @@ import (
|
||||
// sequence. If the iterator contains at least one element, it returns Some(element).
|
||||
// If the iterator is empty, it returns None.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--|
|
||||
// Last
|
||||
// Output: -----------------Some(5)|
|
||||
//
|
||||
// Input: --|
|
||||
// Last
|
||||
// Output: --None|
|
||||
//
|
||||
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
|
||||
//
|
||||
// Type Parameters:
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
// 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 (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBufferSize = 8
|
||||
)
|
||||
|
||||
// MergeBuf merges multiple sequences concurrently into a single sequence.
|
||||
// It spawns a goroutine for each input sequence and merges their elements through
|
||||
// a buffered channel, allowing concurrent production from all sources. The output
|
||||
// order is non-deterministic and depends on the timing of concurrent producers.
|
||||
//
|
||||
// This function is useful for combining results from multiple concurrent operations,
|
||||
// processing data from multiple sources in parallel, or implementing fan-in patterns
|
||||
// where multiple producers feed into a single consumer.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - iterables: A slice of sequences to merge. If empty, returns an empty sequence.
|
||||
// - bufSize: The buffer size for the internal channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// reducing contention between producers but using more memory.
|
||||
// A buffer of 0 creates an unbuffered channel requiring synchronization.
|
||||
//
|
||||
// Returns:
|
||||
// - Seq[T]: A new sequence that yields elements from all input sequences in non-deterministic order
|
||||
//
|
||||
// Behavior:
|
||||
// - Spawns one goroutine per input sequence to produce elements concurrently
|
||||
// - Elements from different sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination: if the consumer stops iterating (yield returns false),
|
||||
// all producer goroutines are signaled to stop and cleaned up
|
||||
// - The output channel is closed when all input sequences are exhausted
|
||||
// - No goroutines leak even with early termination
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // MergeBuf three sequences concurrently
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
// seq3 := From(7, 8, 9)
|
||||
// merged := MergeBuf([]Seq[int]{seq1, seq2, seq3}, 10)
|
||||
//
|
||||
// // Elements appear in non-deterministic order
|
||||
// for v := range merged {
|
||||
// fmt.Println(v) // May print: 1, 4, 7, 2, 5, 8, 3, 6, 9 (order varies)
|
||||
// }
|
||||
//
|
||||
// Example with Early Termination:
|
||||
//
|
||||
// seq1 := From(1, 2, 3, 4, 5)
|
||||
// seq2 := From(6, 7, 8, 9, 10)
|
||||
// merged := MergeBuf([]Seq[int]{seq1, seq2}, 5)
|
||||
//
|
||||
// // Stop after 3 elements - all producer goroutines will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 3 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example with Unbuffered Channel:
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
// merged := MergeBuf([]Seq[int]{seq1, seq2}, 0)
|
||||
//
|
||||
// // Producers and consumer are synchronized
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
// - From: Creates a sequence from values
|
||||
// - MonadChain: Sequentially chains sequences (deterministic order)
|
||||
func MergeBuf[T any](iterables []Seq[T], bufSize int) Seq[T] {
|
||||
return F.Pipe2(
|
||||
iterables,
|
||||
slices.Values,
|
||||
MergeAll[T](bufSize),
|
||||
)
|
||||
}
|
||||
|
||||
// Merge merges multiple sequences concurrently into a single sequence using a default buffer size.
|
||||
// This is a convenience wrapper around MergeBuf that uses a default buffer size of 8.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - iterables: A slice of sequences to merge. If empty, returns an empty sequence.
|
||||
//
|
||||
// Returns:
|
||||
// - Seq[T]: A new sequence that yields elements from all input sequences in non-deterministic order
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Spawns one goroutine per input sequence to produce elements concurrently
|
||||
// - Elements from different sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
// seq3 := From(7, 8, 9)
|
||||
// merged := Merge([]Seq[int]{seq1, seq2, seq3})
|
||||
//
|
||||
// // Elements appear in non-deterministic order
|
||||
// for v := range merged {
|
||||
// fmt.Println(v) // May print: 1, 4, 7, 2, 5, 8, 3, 6, 9 (order varies)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - MergeBuf: Merge with custom buffer size
|
||||
// - MergeAll: Merges a sequence of sequences
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
func Merge[T any](iterables []Seq[T]) Seq[T] {
|
||||
return MergeBuf(iterables, defaultBufferSize)
|
||||
}
|
||||
|
||||
// MergeMonoid creates a Monoid for merging sequences concurrently.
|
||||
// The monoid combines two sequences by merging them concurrently with the specified
|
||||
// buffer size, and uses an empty sequence as the identity element.
|
||||
//
|
||||
// A Monoid is an algebraic structure with an associative binary operation (concat)
|
||||
// and an identity element (empty). For sequences, the concat operation merges two
|
||||
// sequences concurrently, and the identity is an empty sequence.
|
||||
//
|
||||
// This is useful for functional composition patterns where you need to combine
|
||||
// multiple sequences using monoid operations like Reduce, FoldMap, or when working
|
||||
// with monadic operations that require a monoid instance.
|
||||
//
|
||||
// Marble Diagram (Concurrent Merging):
|
||||
//
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Merge: --1-4-2-5-3-6--|
|
||||
// (non-deterministic order)
|
||||
//
|
||||
// Marble Diagram (vs ConcatMonoid):
|
||||
//
|
||||
// MergeMonoid (concurrent):
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1-4-2-5-3-6--|
|
||||
// (elements interleaved)
|
||||
//
|
||||
// ConcatMonoid (sequential):
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1--2--3--4--5--6--|
|
||||
// (deterministic order)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - bufSize: The buffer size for the internal channel used during merging.
|
||||
// This buffer size will be used for all merge operations performed by the monoid.
|
||||
// Negative values are treated as 0 (unbuffered).
|
||||
//
|
||||
// Returns:
|
||||
// - Monoid[Seq[T]]: A monoid instance with:
|
||||
// - Concat: Merges two sequences concurrently using Merge
|
||||
// - Empty: Returns an empty sequence
|
||||
//
|
||||
// Properties:
|
||||
// - Identity: concat(empty, x) = concat(x, empty) = x
|
||||
// - Associativity: concat(concat(a, b), c) = concat(a, concat(b, c))
|
||||
// Note: Due to concurrent execution, element order may vary between equivalent expressions
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Create a monoid for merging integer sequences
|
||||
// monoid := MergeMonoid[int](10)
|
||||
//
|
||||
// // Use with Reduce to merge multiple sequences
|
||||
// sequences := []Seq[int]{
|
||||
// From(1, 2, 3),
|
||||
// From(4, 5, 6),
|
||||
// From(7, 8, 9),
|
||||
// }
|
||||
// merged := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty)
|
||||
// // merged contains all elements from all sequences (order non-deterministic)
|
||||
//
|
||||
// Example with Empty Identity:
|
||||
//
|
||||
// monoid := MergeMonoid[int](5)
|
||||
// seq := From(1, 2, 3)
|
||||
//
|
||||
// // Merging with empty is identity
|
||||
// result1 := monoid.Concat(monoid.Empty, seq) // same as seq
|
||||
// result2 := monoid.Concat(seq, monoid.Empty) // same as seq
|
||||
//
|
||||
// Example with FoldMap:
|
||||
//
|
||||
// // Convert each number to a sequence and merge all results
|
||||
// monoid := MergeMonoid[int](10)
|
||||
// numbers := From(1, 2, 3)
|
||||
// result := MonadFoldMap(numbers, func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, monoid)
|
||||
// // result contains: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
//
|
||||
// See Also:
|
||||
// - Merge: The underlying merge function
|
||||
// - MergeAll: Merges multiple sequences at once
|
||||
// - Empty: Creates an empty sequence
|
||||
func MergeMonoid[T any](bufSize int) M.Monoid[Seq[T]] {
|
||||
return M.MakeMonoid(
|
||||
func(l, r Seq[T]) Seq[T] {
|
||||
return MergeBuf(A.From(l, r), bufSize)
|
||||
},
|
||||
Empty[T](),
|
||||
)
|
||||
}
|
||||
|
||||
// MergeAll creates an operator that flattens and merges a sequence of sequences concurrently.
|
||||
// It takes a sequence of sequences (Seq[Seq[T]]) and produces a single flat sequence (Seq[T])
|
||||
// by spawning a goroutine for each inner sequence as it arrives, merging all their elements
|
||||
// through a buffered channel. This enables dynamic concurrent processing where inner sequences
|
||||
// can be produced and consumed concurrently.
|
||||
//
|
||||
// Unlike Merge which takes a pre-defined slice of sequences, MergeAll processes sequences
|
||||
// dynamically as they are produced by the outer sequence. This makes it ideal for scenarios
|
||||
// where the number of sequences isn't known upfront or where sequences are generated on-the-fly.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the inner sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - bufSize: The buffer size for the internal channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// reducing contention between producers but using more memory.
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[Seq[T], T]: A function that takes a sequence of sequences and returns a flat sequence
|
||||
//
|
||||
// Behavior:
|
||||
// - Spawns one goroutine for the outer sequence to iterate and spawn inner producers
|
||||
// - Spawns one goroutine per inner sequence as it arrives from the outer sequence
|
||||
// - Elements from different inner sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination: if the consumer stops iterating, all goroutines are cleaned up
|
||||
// - The output channel is closed when both the outer sequence and all inner sequences are exhausted
|
||||
// - No goroutines leak even with early termination
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Create a sequence of sequences dynamically
|
||||
// outer := From(
|
||||
// From(1, 2, 3),
|
||||
// From(4, 5, 6),
|
||||
// From(7, 8, 9),
|
||||
// )
|
||||
// mergeAll := MergeAll[int](10)
|
||||
// merged := mergeAll(outer)
|
||||
//
|
||||
// // Elements appear in non-deterministic order
|
||||
// for v := range merged {
|
||||
// fmt.Println(v) // May print: 1, 4, 7, 2, 5, 8, 3, 6, 9 (order varies)
|
||||
// }
|
||||
//
|
||||
// Example with Dynamic Generation:
|
||||
//
|
||||
// // Generate sequences on-the-fly
|
||||
// outer := Map(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// })(From(1, 2, 3))
|
||||
// mergeAll := MergeAll[int](10)
|
||||
// merged := mergeAll(outer)
|
||||
//
|
||||
// // Yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// Example with Early Termination:
|
||||
//
|
||||
// outer := From(
|
||||
// From(1, 2, 3, 4, 5),
|
||||
// From(6, 7, 8, 9, 10),
|
||||
// From(11, 12, 13, 14, 15),
|
||||
// )
|
||||
// mergeAll := MergeAll[int](5)
|
||||
// merged := mergeAll(outer)
|
||||
//
|
||||
// // Stop after 5 elements - all goroutines will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range merged {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 5 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example with Chain:
|
||||
//
|
||||
// // Use with Chain to flatten nested sequences
|
||||
// numbers := From(1, 2, 3)
|
||||
// result := Chain(func(n int) Seq[int] {
|
||||
// return From(n, n*10)
|
||||
// })(numbers)
|
||||
// // This is equivalent to: MergeAll[int](0)(Map(...)(numbers))
|
||||
//
|
||||
// See Also:
|
||||
// - Merge: Merges a pre-defined slice of sequences
|
||||
// - Chain: Sequentially flattens sequences (deterministic order)
|
||||
// - Flatten: Flattens nested sequences sequentially
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
func MergeAll[T any](bufSize int) Operator[Seq[T], T] {
|
||||
buf := N.Max(bufSize, 0)
|
||||
|
||||
return func(s Seq[Seq[T]]) Seq[T] {
|
||||
|
||||
return func(yield func(T) bool) {
|
||||
|
||||
ch := make(chan T, buf)
|
||||
done := make(chan Void)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Outer producer: iterates the outer Seq and spawns an inner
|
||||
// goroutine for each inner Seq it emits.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s(func(inner Seq[T]) bool {
|
||||
select {
|
||||
case <-done:
|
||||
return false
|
||||
default:
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(seq Seq[T]) {
|
||||
defer wg.Done()
|
||||
seq(func(v T) bool {
|
||||
select {
|
||||
case ch <- v:
|
||||
return true
|
||||
case <-done:
|
||||
return false
|
||||
}
|
||||
})
|
||||
}(inner)
|
||||
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
// Close ch once the outer producer and all inner producers finish.
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
// On exit, signal cancellation and drain so no producer blocks
|
||||
// forever on `ch <- v`.
|
||||
defer func() {
|
||||
close(done)
|
||||
for range ch {
|
||||
}
|
||||
}()
|
||||
|
||||
for v := range ch {
|
||||
if !yield(v) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MergeMapBuf applies a function that returns a sequence to each element and merges the results concurrently.
|
||||
// This is the concurrent version of Chain (flatMap), where each mapped sequence is processed in parallel
|
||||
// rather than sequentially. It combines Map and MergeAll into a single operation.
|
||||
//
|
||||
// Unlike Chain which processes sequences sequentially (deterministic order), MergeMapBuf spawns a goroutine
|
||||
// for each mapped sequence and merges their elements concurrently through a buffered channel. This makes
|
||||
// it ideal for I/O-bound operations, parallel data processing, or when the order of results doesn't matter.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the input sequence
|
||||
// - B: The type of elements in the output sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms each input element into a sequence of output elements
|
||||
// - bufSize: The buffer size for the internal channel. Negative values are treated as 0 (unbuffered).
|
||||
// A larger buffer allows more elements to be produced ahead of consumption,
|
||||
// reducing contention between producers but using more memory.
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that takes a sequence of A and returns a flat sequence of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Applies f to each element in the input sequence to produce inner sequences
|
||||
// - Spawns one goroutine per inner sequence to produce elements concurrently
|
||||
// - Elements from different inner sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination: if the consumer stops iterating, all goroutines are cleaned up
|
||||
// - No goroutines leak even with early termination
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Comparison with Chain:
|
||||
// - Chain: Sequential processing, deterministic order, no concurrency overhead
|
||||
// - MergeMapBuf: Concurrent processing, non-deterministic order, better for I/O-bound tasks
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Expand each number into a sequence concurrently
|
||||
// expand := MergeMapBuf(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, 10)
|
||||
// seq := From(1, 2, 3)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// Example with I/O Operations:
|
||||
//
|
||||
// // Fetch data concurrently for each ID
|
||||
// fetchData := MergeMapBuf(func(id int) Seq[string] {
|
||||
// // Simulate I/O operation
|
||||
// data := fetchFromAPI(id)
|
||||
// return From(data...)
|
||||
// }, 20)
|
||||
// ids := From(1, 2, 3, 4, 5)
|
||||
// results := fetchData(ids)
|
||||
//
|
||||
// // All fetches happen concurrently
|
||||
// for data := range results {
|
||||
// fmt.Println(data)
|
||||
// }
|
||||
//
|
||||
// Example with Early Termination:
|
||||
//
|
||||
// expand := MergeMapBuf(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, 5)
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Stop after 5 elements - all goroutines will be properly cleaned up
|
||||
// count := 0
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// count++
|
||||
// if count >= 5 {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example with Unbuffered Channel:
|
||||
//
|
||||
// // bufSize of 0 creates an unbuffered channel
|
||||
// expand := MergeMapBuf(func(n int) Seq[int] {
|
||||
// return From(n, n*2)
|
||||
// }, 0)
|
||||
// seq := From(1, 2, 3)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Producers and consumer are synchronized
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - Chain: Sequential version (deterministic order)
|
||||
// - MergeAll: Merges pre-existing sequences concurrently
|
||||
// - Map: Transforms elements without flattening
|
||||
// - Async: Converts a single sequence to asynchronous
|
||||
func MergeMapBuf[A, B any](f func(A) Seq[B], bufSize int) Operator[A, B] {
|
||||
return F.Flow2(
|
||||
Map(f),
|
||||
MergeAll[B](bufSize),
|
||||
)
|
||||
}
|
||||
|
||||
// MergeMap applies a function that returns a sequence to each element and merges the results concurrently using a default buffer size.
|
||||
// This is a convenience wrapper around MergeMapBuf that uses a default buffer size of 8.
|
||||
// It's the concurrent version of Chain (flatMap), where each mapped sequence is processed in parallel.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the input sequence
|
||||
// - B: The type of elements in the output sequences
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that transforms each input element into a sequence of output elements
|
||||
//
|
||||
// Returns:
|
||||
// - Operator[A, B]: A function that takes a sequence of A and returns a flat sequence of B
|
||||
//
|
||||
// Behavior:
|
||||
// - Uses a default buffer size of 8 for the internal channel
|
||||
// - Applies f to each element in the input sequence to produce inner sequences
|
||||
// - Spawns one goroutine per inner sequence to produce elements concurrently
|
||||
// - Elements from different inner sequences are interleaved non-deterministically
|
||||
// - Properly handles early termination with goroutine cleanup
|
||||
// - Thread-safe: multiple producers can safely send to the shared channel
|
||||
//
|
||||
// Comparison with Chain:
|
||||
// - Chain: Sequential processing, deterministic order, no concurrency overhead
|
||||
// - MergeMap: Concurrent processing, non-deterministic order, better for I/O-bound tasks
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Expand each number into a sequence concurrently
|
||||
// expand := MergeMap(func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// })
|
||||
// seq := From(1, 2, 3)
|
||||
// result := expand(seq)
|
||||
//
|
||||
// // Yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (order varies)
|
||||
// for v := range result {
|
||||
// fmt.Println(v)
|
||||
// }
|
||||
//
|
||||
// See Also:
|
||||
// - MergeMapBuf: MergeMap with custom buffer size
|
||||
// - Chain: Sequential version (deterministic order)
|
||||
// - MergeAll: Merges pre-existing sequences concurrently
|
||||
// - Map: Transforms elements without flattening
|
||||
func MergeMap[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
return MergeMapBuf(f, defaultBufferSize)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+134
-1
@@ -21,7 +21,13 @@ import (
|
||||
)
|
||||
|
||||
// Monoid returns a Monoid instance for Seq[T].
|
||||
// The monoid's concat operation concatenates sequences, and the empty value is an empty sequence.
|
||||
// The monoid's concat operation concatenates sequences sequentially, and the empty value is an empty sequence.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Concat: --1--2--3--4--5--6--|
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -35,3 +41,130 @@ import (
|
||||
func Monoid[T any]() M.Monoid[Seq[T]] {
|
||||
return G.Monoid[Seq[T]]()
|
||||
}
|
||||
|
||||
// ConcatMonoid returns a Monoid instance for Seq[T] that concatenates sequences sequentially.
|
||||
// This is an alias for Monoid that makes the sequential concatenation behavior explicit.
|
||||
//
|
||||
// A Monoid is an algebraic structure with an associative binary operation (concat)
|
||||
// and an identity element (empty). For sequences, the concat operation appends one
|
||||
// sequence after another in deterministic order, and the identity is an empty sequence.
|
||||
//
|
||||
// This monoid is useful for functional composition patterns where you need to combine
|
||||
// multiple sequences sequentially using monoid operations like Reduce, FoldMap, or when
|
||||
// working with monadic operations that require a monoid instance.
|
||||
//
|
||||
// Marble Diagram (Sequential Concatenation):
|
||||
//
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Concat: --1--2--3--4--5--6--|
|
||||
// (deterministic order)
|
||||
//
|
||||
// Marble Diagram (vs MergeMonoid):
|
||||
//
|
||||
// ConcatMonoid:
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1--2--3--4--5--6--|
|
||||
//
|
||||
// MergeMonoid:
|
||||
// Seq1: --1--2--3--|
|
||||
// Seq2: --4--5--6--|
|
||||
// Result: --1-4-2-5-3-6--|
|
||||
// (non-deterministic)
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: The type of elements in the sequences
|
||||
//
|
||||
// Returns:
|
||||
// - Monoid[Seq[T]]: A monoid instance with:
|
||||
// - Concat: Appends sequences sequentially (deterministic order)
|
||||
// - Empty: Returns an empty sequence
|
||||
//
|
||||
// Properties:
|
||||
// - Identity: concat(empty, x) = concat(x, empty) = x
|
||||
// - Associativity: concat(concat(a, b), c) = concat(a, concat(b, c))
|
||||
// - Deterministic: Elements always appear in the order of the input sequences
|
||||
//
|
||||
// Comparison with MergeMonoid:
|
||||
//
|
||||
// ConcatMonoid and MergeMonoid serve different purposes:
|
||||
//
|
||||
// - ConcatMonoid: Sequential concatenation
|
||||
//
|
||||
// - Order: Deterministic - elements from first sequence, then second, etc.
|
||||
//
|
||||
// - Concurrency: No concurrency - sequences are processed one after another
|
||||
//
|
||||
// - Performance: Lower overhead, no goroutines or channels
|
||||
//
|
||||
// - Use when: Order matters, no I/O operations, or simplicity is preferred
|
||||
//
|
||||
// - MergeMonoid: Concurrent merging
|
||||
//
|
||||
// - Order: Non-deterministic - elements interleaved based on timing
|
||||
//
|
||||
// - Concurrency: Spawns goroutines for each sequence
|
||||
//
|
||||
// - Performance: Better for I/O-bound operations, higher overhead for CPU-bound
|
||||
//
|
||||
// - Use when: Order doesn't matter, parallel I/O, or concurrent processing needed
|
||||
//
|
||||
// Example Usage:
|
||||
//
|
||||
// // Create a monoid for concatenating integer sequences
|
||||
// monoid := ConcatMonoid[int]()
|
||||
//
|
||||
// // Use with Reduce to concatenate multiple sequences
|
||||
// sequences := []Seq[int]{
|
||||
// From(1, 2, 3),
|
||||
// From(4, 5, 6),
|
||||
// From(7, 8, 9),
|
||||
// }
|
||||
// concatenated := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty)
|
||||
// // yields: 1, 2, 3, 4, 5, 6, 7, 8, 9 (deterministic order)
|
||||
//
|
||||
// Example with Empty Identity:
|
||||
//
|
||||
// monoid := ConcatMonoid[int]()
|
||||
// seq := From(1, 2, 3)
|
||||
//
|
||||
// // Concatenating with empty is identity
|
||||
// result1 := monoid.Concat(monoid.Empty, seq) // same as seq
|
||||
// result2 := monoid.Concat(seq, monoid.Empty) // same as seq
|
||||
//
|
||||
// Example with FoldMap:
|
||||
//
|
||||
// // Convert each number to a sequence and concatenate all results
|
||||
// monoid := ConcatMonoid[int]()
|
||||
// numbers := From(1, 2, 3)
|
||||
// result := MonadFoldMap(numbers, func(n int) Seq[int] {
|
||||
// return From(n, n*10, n*100)
|
||||
// }, monoid)
|
||||
// // yields: 1, 10, 100, 2, 20, 200, 3, 30, 300 (deterministic order)
|
||||
//
|
||||
// Example Comparing ConcatMonoid vs MergeMonoid:
|
||||
//
|
||||
// seq1 := From(1, 2, 3)
|
||||
// seq2 := From(4, 5, 6)
|
||||
//
|
||||
// // ConcatMonoid: Sequential, deterministic
|
||||
// concatMonoid := ConcatMonoid[int]()
|
||||
// concat := concatMonoid.Concat(seq1, seq2)
|
||||
// // Always yields: 1, 2, 3, 4, 5, 6
|
||||
//
|
||||
// // MergeMonoid: Concurrent, non-deterministic
|
||||
// mergeMonoid := MergeMonoid[int](10)
|
||||
// merged := mergeMonoid.Concat(seq1, seq2)
|
||||
// // May yield: 1, 4, 2, 5, 3, 6 (order varies)
|
||||
//
|
||||
// See Also:
|
||||
// - Monoid: The base monoid function (alias)
|
||||
// - MergeMonoid: Concurrent merging monoid
|
||||
// - MonadChain: Sequential flattening of sequences
|
||||
// - Empty: Creates an empty sequence
|
||||
//
|
||||
//go:inline
|
||||
func ConcatMonoid[T any]() M.Monoid[Seq[T]] {
|
||||
return Monoid[T]()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConcatMonoid_Identity(t *testing.T) {
|
||||
t.Run("left identity", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq := From(1, 2, 3)
|
||||
|
||||
result := monoid.Concat(monoid.Empty(), seq)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
|
||||
t.Run("right identity", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq := From(1, 2, 3)
|
||||
|
||||
result := monoid.Concat(seq, monoid.Empty())
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_Associativity(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2)
|
||||
seq2 := From(3, 4)
|
||||
seq3 := From(5, 6)
|
||||
|
||||
// (a + b) + c
|
||||
left := monoid.Concat(monoid.Concat(seq1, seq2), seq3)
|
||||
leftResult := slices.Collect(left)
|
||||
|
||||
// a + (b + c)
|
||||
right := monoid.Concat(seq1, monoid.Concat(seq2, seq3))
|
||||
rightResult := slices.Collect(right)
|
||||
|
||||
assert.Equal(t, leftResult, rightResult)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, leftResult)
|
||||
}
|
||||
|
||||
func TestConcatMonoid_DeterministicOrder(t *testing.T) {
|
||||
t.Run("concatenates in deterministic order", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
seq3 := From(7, 8, 9)
|
||||
|
||||
result := monoid.Concat(monoid.Concat(seq1, seq2), seq3)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Order is always deterministic
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, collected)
|
||||
})
|
||||
|
||||
t.Run("multiple runs produce same order", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
// Run multiple times
|
||||
results := make([][]int, 5)
|
||||
for i := range 5 {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
results[i] = slices.Collect(result)
|
||||
}
|
||||
|
||||
// All results should be identical
|
||||
expected := []int{1, 2, 3, 4, 5, 6}
|
||||
for i, result := range results {
|
||||
assert.Equal(t, expected, result, "run %d should match", i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_WithReduce(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
sequences := []Seq[int]{
|
||||
From(1, 2, 3),
|
||||
From(4, 5, 6),
|
||||
From(7, 8, 9),
|
||||
}
|
||||
|
||||
result := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty())
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, collected)
|
||||
}
|
||||
|
||||
func TestConcatMonoid_WithFoldMap(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
numbers := From(1, 2, 3)
|
||||
|
||||
result := MonadFoldMap(numbers, func(n int) Seq[int] {
|
||||
return From(n, n*10, n*100)
|
||||
}, monoid)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Deterministic order: each number's expansion in sequence
|
||||
assert.Equal(t, []int{1, 10, 100, 2, 20, 200, 3, 30, 300}, collected)
|
||||
}
|
||||
|
||||
func TestConcatMonoid_ComparisonWithMergeMonoid(t *testing.T) {
|
||||
t.Run("ConcatMonoid is deterministic", func(t *testing.T) {
|
||||
concatMonoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
result := concatMonoid.Concat(seq1, seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Always the same order
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, collected)
|
||||
})
|
||||
|
||||
t.Run("MergeMonoid may be non-deterministic", func(t *testing.T) {
|
||||
mergeMonoid := MergeMonoid[int](10)
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
result := mergeMonoid.Concat(seq1, seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
// Contains all elements but order may vary
|
||||
assert.ElementsMatch(t, []int{1, 2, 3, 4, 5, 6}, collected)
|
||||
// Note: We can't assert exact order as it's non-deterministic
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_EmptySequences(t *testing.T) {
|
||||
t.Run("concatenating empty sequences", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
empty1 := Empty[int]()
|
||||
empty2 := Empty[int]()
|
||||
|
||||
result := monoid.Concat(empty1, empty2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Empty(t, collected)
|
||||
})
|
||||
|
||||
t.Run("concatenating with empty in middle", func(t *testing.T) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2)
|
||||
empty := Empty[int]()
|
||||
seq2 := From(3, 4)
|
||||
|
||||
result := monoid.Concat(monoid.Concat(seq1, empty), seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, collected)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConcatMonoid_WithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
monoid := ConcatMonoid[Person]()
|
||||
seq1 := From(Person{"Alice", 30}, Person{"Bob", 25})
|
||||
seq2 := From(Person{"Charlie", 35}, Person{"Diana", 28})
|
||||
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
collected := slices.Collect(result)
|
||||
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Charlie", 35},
|
||||
{"Diana", 28},
|
||||
}
|
||||
assert.Equal(t, expected, collected)
|
||||
}
|
||||
|
||||
func BenchmarkConcatMonoid_TwoSequences(b *testing.B) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3, 4, 5)
|
||||
seq2 := From(6, 7, 8, 9, 10)
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConcatMonoid_Reduce(b *testing.B) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
sequences := []Seq[int]{
|
||||
From(1, 2, 3),
|
||||
From(4, 5, 6),
|
||||
From(7, 8, 9),
|
||||
From(10, 11, 12),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty())
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConcatMonoid_VsMergeMonoid(b *testing.B) {
|
||||
seq1 := From(1, 2, 3, 4, 5)
|
||||
seq2 := From(6, 7, 8, 9, 10)
|
||||
|
||||
b.Run("ConcatMonoid", func(b *testing.B) {
|
||||
monoid := ConcatMonoid[int]()
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("MergeMonoid", func(b *testing.B) {
|
||||
monoid := MergeMonoid[int](10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid() {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
result := monoid.Concat(seq1, seq2)
|
||||
for v := range result {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid_identity() {
|
||||
monoid := ConcatMonoid[int]()
|
||||
seq := From(1, 2, 3)
|
||||
|
||||
// Left identity
|
||||
result1 := monoid.Concat(monoid.Empty(), seq)
|
||||
for v := range result1 {
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
// Right identity
|
||||
result2 := monoid.Concat(seq, monoid.Empty())
|
||||
for v := range result2 {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid_reduce() {
|
||||
monoid := ConcatMonoid[int]()
|
||||
sequences := []Seq[int]{
|
||||
From(1, 2, 3),
|
||||
From(4, 5, 6),
|
||||
From(7, 8, 9),
|
||||
}
|
||||
|
||||
result := MonadReduce(From(sequences...), monoid.Concat, monoid.Empty())
|
||||
for v := range result {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
// 7
|
||||
// 8
|
||||
// 9
|
||||
}
|
||||
|
||||
func ExampleConcatMonoid_comparison() {
|
||||
seq1 := From(1, 2, 3)
|
||||
seq2 := From(4, 5, 6)
|
||||
|
||||
// ConcatMonoid: Sequential, deterministic
|
||||
concatMonoid := ConcatMonoid[int]()
|
||||
concat := concatMonoid.Concat(seq1, seq2)
|
||||
fmt.Println("ConcatMonoid (always same order):")
|
||||
for v := range concat {
|
||||
fmt.Println(v)
|
||||
}
|
||||
|
||||
// MergeMonoid: Concurrent, non-deterministic
|
||||
// Note: Output order may vary in actual runs
|
||||
mergeMonoid := MergeMonoid[int](10)
|
||||
merged := mergeMonoid.Concat(seq1, seq2)
|
||||
fmt.Println("\nMergeMonoid (order may vary):")
|
||||
collected := slices.Collect(merged)
|
||||
// Sort for consistent test output
|
||||
slices.Sort(collected)
|
||||
for _, v := range collected {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output:
|
||||
// ConcatMonoid (always same order):
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
//
|
||||
// MergeMonoid (order may vary):
|
||||
// 1
|
||||
// 2
|
||||
// 3
|
||||
// 4
|
||||
// 5
|
||||
// 6
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
@@ -28,6 +28,13 @@ import (
|
||||
//
|
||||
// This is the monadic form that takes the sequence as the first parameter.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// ChainOptionK(x => x % 2 == 0 ? Some(x * 10) : None)
|
||||
// Output: -----20----40---->
|
||||
// (filters and transforms)
|
||||
//
|
||||
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
|
||||
//
|
||||
// Type parameters:
|
||||
@@ -72,6 +79,13 @@ func MonadChainOptionK[A, B any](as Seq[A], f option.Kleisli[A, B]) Seq[B] {
|
||||
// This is the curried version of [MonadChainOptionK], useful for function composition
|
||||
// and creating reusable transformations.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// ChainOptionK(x => x > 2 ? Some(x) : None)
|
||||
// Output: --------3--4--5-->
|
||||
// (filters out values <= 2)
|
||||
//
|
||||
// RxJS Equivalent: [concatMap] combined with [filter] - https://rxjs.dev/api/operators/concatMap
|
||||
//
|
||||
// Type parameters:
|
||||
|
||||
@@ -24,6 +24,13 @@ package iter
|
||||
//
|
||||
// The operation is lazy - intermediate values are computed only as they are consumed.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5-->
|
||||
// Scan((acc, x) => acc + x, 0)
|
||||
// Output: --1--3--6--10-15->
|
||||
// (running sum)
|
||||
//
|
||||
// RxJS Equivalent: [scan] - https://rxjs.dev/api/operators/scan
|
||||
//
|
||||
// Scan is useful for:
|
||||
|
||||
@@ -27,6 +27,12 @@ import F "github.com/IBM/fp-go/v2/function"
|
||||
// Once n elements have been yielded, iteration stops immediately without consuming
|
||||
// the remaining elements from the source.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--6--7--8-->
|
||||
// Take(3)
|
||||
// Output: --1--2--3|
|
||||
//
|
||||
// RxJS Equivalent: [take] - https://rxjs.dev/api/operators/take
|
||||
//
|
||||
// Type Parameters:
|
||||
@@ -78,3 +84,158 @@ func Take[U any](n int) Operator[U, U] {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TakeWhile returns an operator that emits elements from a sequence while a predicate is satisfied.
|
||||
//
|
||||
// This function creates a transformation that yields elements from the source sequence
|
||||
// as long as each element satisfies the provided predicate. Once an element fails the
|
||||
// predicate test, the sequence terminates immediately, and no further elements are
|
||||
// emitted, even if subsequent elements would satisfy the predicate.
|
||||
//
|
||||
// The operation is lazy and only consumes elements from the source sequence as needed.
|
||||
// Once the predicate returns false, iteration stops immediately without consuming
|
||||
// the remaining elements from the source.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--2--1-->
|
||||
// TakeWhile(x < 4)
|
||||
// Output: --1--2--3|
|
||||
// (stops at 4)
|
||||
//
|
||||
// RxJS Equivalent: [takeWhile] - https://rxjs.dev/api/operators/takeWhile
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests each element. Returns true to continue, false to stop
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] by taking elements while the predicate is satisfied
|
||||
//
|
||||
// Example - Take while less than threshold:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
// result := TakeWhile(func(x int) bool { return x < 4 })(seq)
|
||||
// // yields: 1, 2, 3 (stops at 4, doesn't continue to 2, 1)
|
||||
//
|
||||
// Example - Take while condition is met:
|
||||
//
|
||||
// seq := From("a", "b", "c", "1", "d", "e")
|
||||
// isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
// result := TakeWhile(isLetter)(seq)
|
||||
// // yields: "a", "b", "c" (stops at "1")
|
||||
//
|
||||
// Example - Take all when predicate always true:
|
||||
//
|
||||
// seq := From(2, 4, 6, 8)
|
||||
// result := TakeWhile(func(x int) bool { return x%2 == 0 })(seq)
|
||||
// // yields: 2, 4, 6, 8 (all elements satisfy predicate)
|
||||
//
|
||||
// Example - Take none when first element fails:
|
||||
//
|
||||
// seq := From(5, 1, 2, 3)
|
||||
// result := TakeWhile(func(x int) bool { return x < 5 })(seq)
|
||||
// // yields: nothing (first element fails predicate)
|
||||
//
|
||||
// Example - Chaining with other operations:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// result := F.Pipe2(
|
||||
// seq,
|
||||
// MonadMap(seq, func(x int) int { return x * 2 }),
|
||||
// TakeWhile(func(x int) bool { return x < 10 }),
|
||||
// )
|
||||
// // yields: 2, 4, 6, 8 (stops when doubled value reaches 10)
|
||||
func TakeWhile[U any](p Predicate[U]) Operator[U, U] {
|
||||
return func(s Seq[U]) Seq[U] {
|
||||
return func(yield func(U) bool) {
|
||||
for u := range s {
|
||||
if !p(u) || !yield(u) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SkipWhile returns an operator that skips elements from a sequence while a predicate is satisfied.
|
||||
//
|
||||
// This function creates a transformation that discards elements from the source sequence
|
||||
// as long as each element satisfies the provided predicate. Once an element fails the
|
||||
// predicate test, that element and all subsequent elements are yielded, regardless of
|
||||
// whether they satisfy the predicate.
|
||||
//
|
||||
// The operation is lazy and only consumes elements from the source sequence as needed.
|
||||
// Once the predicate returns false, all remaining elements are yielded without further
|
||||
// predicate evaluation.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--4--5--2--1-->
|
||||
// SkipWhile(x < 4)
|
||||
// Output: -----------4--5--2--1-->
|
||||
// (starts at 4, continues with all)
|
||||
//
|
||||
// RxJS Equivalent: [skipWhile] - https://rxjs.dev/api/operators/skipWhile
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - p: A predicate function that tests each element. Returns true to skip, false to start yielding
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] by skipping elements while the predicate is satisfied
|
||||
//
|
||||
// Example - Skip while less than threshold:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
// result := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
// // yields: 4, 5, 2, 1 (starts at 4, continues with all remaining)
|
||||
//
|
||||
// Example - Skip while condition is met:
|
||||
//
|
||||
// seq := From("a", "b", "c", "1", "d", "e")
|
||||
// isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
// result := SkipWhile(isLetter)(seq)
|
||||
// // yields: "1", "d", "e" (starts at "1", continues with all remaining)
|
||||
//
|
||||
// Example - Skip none when first element fails:
|
||||
//
|
||||
// seq := From(5, 1, 2, 3)
|
||||
// result := SkipWhile(func(x int) bool { return x < 5 })(seq)
|
||||
// // yields: 5, 1, 2, 3 (first element fails predicate, all yielded)
|
||||
//
|
||||
// Example - Skip all when predicate always true:
|
||||
//
|
||||
// seq := From(2, 4, 6, 8)
|
||||
// result := SkipWhile(func(x int) bool { return x%2 == 0 })(seq)
|
||||
// // yields: nothing (all elements satisfy predicate)
|
||||
//
|
||||
// Example - Chaining with other operations:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// result := F.Pipe2(
|
||||
// seq,
|
||||
// SkipWhile(func(x int) bool { return x < 5 }),
|
||||
// MonadMap(seq, func(x int) int { return x * 2 }),
|
||||
// )
|
||||
// // yields: 10, 12, 14, 16, 18, 20 (skip until 5, then double remaining)
|
||||
func SkipWhile[U any](p Predicate[U]) Operator[U, U] {
|
||||
return func(s Seq[U]) Seq[U] {
|
||||
return func(yield func(U) bool) {
|
||||
skipping := true
|
||||
for u := range s {
|
||||
if skipping && p(u) {
|
||||
continue
|
||||
}
|
||||
skipping = false
|
||||
if !yield(u) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,3 +461,831 @@ func ExampleTake_chained() {
|
||||
}
|
||||
// Output: 4 5 6 7 8
|
||||
}
|
||||
|
||||
// TestSkipWhile tests basic SkipWhile functionality
|
||||
func TestSkipWhile(t *testing.T) {
|
||||
t.Run("skips while predicate is true", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(seq))
|
||||
assert.Equal(t, []int{4, 5, 2, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("skips none when first element fails", func(t *testing.T) {
|
||||
seq := From(5, 1, 2, 3)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 5 })(seq))
|
||||
assert.Equal(t, []int{5, 1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("skips all when predicate always true", func(t *testing.T) {
|
||||
seq := From(2, 4, 6, 8)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x%2 == 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "1", "d", "e")
|
||||
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
result := toSlice(SkipWhile(isLetter)(seq))
|
||||
assert.Equal(t, []string{"1", "d", "e"}, result)
|
||||
})
|
||||
|
||||
t.Run("continues after predicate fails", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 1, 2, 3)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(seq))
|
||||
assert.Equal(t, []int{4, 1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("skips single element", func(t *testing.T) {
|
||||
seq := From(1, 10, 2, 3)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Equal(t, []int{10, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileEmpty tests SkipWhile with empty sequences
|
||||
func TestSkipWhileEmpty(t *testing.T) {
|
||||
t.Run("returns empty from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x > 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when predicate always satisfied", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithComplexTypes tests SkipWhile with complex data types
|
||||
func TestSkipWhileWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("skips structs while condition met", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 25},
|
||||
Person{"Bob", 30},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
result := toSlice(SkipWhile(func(p Person) bool { return p.Age < 35 })(seq))
|
||||
expected := []Person{
|
||||
{"Charlie", 35},
|
||||
{"David", 28},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("skips pointers while condition met", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 25}
|
||||
p2 := &Person{"Bob", 30}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
p4 := &Person{"David", 28}
|
||||
seq := From(p1, p2, p3, p4)
|
||||
result := toSlice(SkipWhile(func(p *Person) bool { return p.Age < 35 })(seq))
|
||||
assert.Equal(t, []*Person{p3, p4}, result)
|
||||
})
|
||||
|
||||
t.Run("skips slices while condition met", func(t *testing.T) {
|
||||
seq := From([]int{1}, []int{1, 2}, []int{1, 2, 3}, []int{1})
|
||||
result := toSlice(SkipWhile(func(s []int) bool { return len(s) < 3 })(seq))
|
||||
expected := [][]int{{1, 2, 3}, {1}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithChainedOperations tests SkipWhile with other sequence operations
|
||||
func TestSkipWhileWithChainedOperations(t *testing.T) {
|
||||
t.Run("skipWhile after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 8 })(mapped))
|
||||
assert.Equal(t, []int{8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile 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(SkipWhile(func(x int) bool { return x < 6 })(filtered))
|
||||
assert.Equal(t, []int{6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("map after skipWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
result := toSlice(MonadMap(skipped, N.Mul(10)))
|
||||
assert.Equal(t, []int{40, 50}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after skipWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
result := toSlice(MonadFilter(skipped, func(x int) bool { return x%2 == 0 }))
|
||||
assert.Equal(t, []int{4, 6, 8}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile 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(SkipWhile(func(x int) bool { return x < 20 })(chained))
|
||||
assert.Equal(t, []int{20, 3, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("skip after skipWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped1 := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
skipped2 := Skip[int](2)(skipped1)
|
||||
result := toSlice(skipped2)
|
||||
assert.Equal(t, []int{6, 7, 8, 9, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile after skip", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped := Skip[int](3)(seq)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 7 })(skipped))
|
||||
assert.Equal(t, []int{7, 8, 9, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile after skipWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
taken := TakeWhile(func(x int) bool { return x < 8 })(skipped)
|
||||
result := toSlice(taken)
|
||||
assert.Equal(t, []int{4, 5, 6, 7}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile after takeWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken := TakeWhile(func(x int) bool { return x < 8 })(seq)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(taken)
|
||||
result := toSlice(skipped)
|
||||
assert.Equal(t, []int{4, 5, 6, 7}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithReplicate tests SkipWhile with Replicate
|
||||
func TestSkipWhileWithReplicate(t *testing.T) {
|
||||
t.Run("skips all from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(10, 5)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x == 5 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skips none when predicate fails on replicate", func(t *testing.T) {
|
||||
seq := Replicate(5, 10)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Equal(t, []int{10, 10, 10, 10, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithMakeBy tests SkipWhile with MakeBy
|
||||
func TestSkipWhileWithMakeBy(t *testing.T) {
|
||||
t.Run("skips from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(10, func(i int) int { return i * i })
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 25 })(seq))
|
||||
assert.Equal(t, []int{25, 36, 49, 64, 81}, result)
|
||||
})
|
||||
|
||||
t.Run("skips all from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i + 1 })
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 100 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithPrependAppend tests SkipWhile with Prepend and Append
|
||||
func TestSkipWhileWithPrependAppend(t *testing.T) {
|
||||
t.Run("skipWhile from prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3, 4, 5)
|
||||
prepended := Prepend(1)(seq)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(prepended))
|
||||
assert.Equal(t, []int{4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile from appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(10)(seq)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(appended))
|
||||
assert.Equal(t, []int{10}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile includes appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 3 })(appended))
|
||||
assert.Equal(t, []int{3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileWithFlatten tests SkipWhile with Flatten
|
||||
func TestSkipWhileWithFlatten(t *testing.T) {
|
||||
t.Run("skips from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5, 6))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 4 })(flattened))
|
||||
assert.Equal(t, []int{4, 5, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("skips 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(SkipWhile(func(x int) bool { return x < 3 })(flattened))
|
||||
assert.Equal(t, []int{3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileDoesNotConsumeEntireSequence tests that SkipWhile is lazy
|
||||
func TestSkipWhileDoesNotConsumeEntireSequence(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
|
||||
})
|
||||
|
||||
skipped := SkipWhile(func(x int) bool { return x < 8 })(seq)
|
||||
|
||||
result := []int{}
|
||||
for v := range skipped {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{8, 10, 12, 14, 16, 18, 20}, result)
|
||||
// Should process all elements since we iterate through all remaining
|
||||
assert.Equal(t, 10, callCount, "should process all elements")
|
||||
})
|
||||
|
||||
t.Run("stops early when consumer stops", 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
|
||||
})
|
||||
|
||||
skipped := SkipWhile(func(x int) bool { return x < 6 })(filtered)
|
||||
|
||||
result := []int{}
|
||||
count := 0
|
||||
for v := range skipped {
|
||||
result = append(result, v)
|
||||
count++
|
||||
if count == 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{6, 8}, result)
|
||||
// Should stop after getting 2 elements
|
||||
assert.LessOrEqual(t, callCount, 9, "should not consume all elements")
|
||||
})
|
||||
}
|
||||
|
||||
// TestSkipWhileEdgeCases tests edge cases
|
||||
func TestSkipWhileEdgeCases(t *testing.T) {
|
||||
t.Run("skipWhile with always false predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return false })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile with always true predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return true })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile from single element that passes", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x > 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile from single element that fails", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 0 })(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile with complex predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := toSlice(SkipWhile(func(x int) bool {
|
||||
return x%2 == 1 || x < 5
|
||||
})(seq))
|
||||
assert.Equal(t, []int{6, 7, 8, 9, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("skipWhile yields elements that satisfy predicate after first failure", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 10, 1, 2, 3)
|
||||
result := toSlice(SkipWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Equal(t, []int{10, 1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for SkipWhile
|
||||
func BenchmarkSkipWhile(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
skipped := SkipWhile(func(x int) bool { return x < 6 })(seq)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWhileLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
skipped := SkipWhile(func(x int) bool { return x < 100 })(seq)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWhileWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
skipped := SkipWhile(func(x int) bool { return x < 12 })(mapped)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSkipWhileWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
skipped := SkipWhile(func(x int) bool { return x < 6 })(filtered)
|
||||
for range skipped {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleSkipWhile() {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 4 })(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 4 5 2 1
|
||||
}
|
||||
|
||||
func ExampleSkipWhile_allSatisfy() {
|
||||
seq := From(2, 4, 6, 8)
|
||||
skipped := SkipWhile(func(x int) bool { return x%2 == 0 })(seq)
|
||||
|
||||
count := 0
|
||||
for range skipped {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleSkipWhile_firstFails() {
|
||||
seq := From(5, 1, 2, 3)
|
||||
skipped := SkipWhile(func(x int) bool { return x < 5 })(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 5 1 2 3
|
||||
}
|
||||
|
||||
func ExampleSkipWhile_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
doubled := MonadMap(seq, N.Mul(2))
|
||||
skipped := SkipWhile(func(x int) bool { return x < 8 })(doubled)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 8 10
|
||||
}
|
||||
|
||||
func ExampleSkipWhile_strings() {
|
||||
seq := From("a", "b", "c", "1", "d", "e")
|
||||
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
skipped := SkipWhile(isLetter)(seq)
|
||||
|
||||
for v := range skipped {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: 1 d e
|
||||
}
|
||||
|
||||
// TestTakeWhile tests basic TakeWhile functionality
|
||||
func TestTakeWhile(t *testing.T) {
|
||||
t.Run("takes while predicate is true", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all when predicate always true", func(t *testing.T) {
|
||||
seq := From(2, 4, 6, 8)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x%2 == 0 })(seq))
|
||||
assert.Equal(t, []int{2, 4, 6, 8}, result)
|
||||
})
|
||||
|
||||
t.Run("takes none when first element fails", func(t *testing.T) {
|
||||
seq := From(5, 1, 2, 3)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("takes from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "1", "d", "e")
|
||||
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
result := toSlice(TakeWhile(isLetter)(seq))
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
|
||||
t.Run("takes single element", func(t *testing.T) {
|
||||
seq := From(1, 10, 2, 3)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Equal(t, []int{1}, result)
|
||||
})
|
||||
|
||||
t.Run("stops at first false predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 1, 2, 3)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileEmpty tests TakeWhile with empty sequences
|
||||
func TestTakeWhileEmpty(t *testing.T) {
|
||||
t.Run("returns empty from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x > 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when predicate never satisfied", func(t *testing.T) {
|
||||
seq := From(10, 20, 30)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithComplexTypes tests TakeWhile with complex data types
|
||||
func TestTakeWhileWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("takes structs while condition met", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 25},
|
||||
Person{"Bob", 30},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
result := toSlice(TakeWhile(func(p Person) bool { return p.Age < 35 })(seq))
|
||||
expected := []Person{
|
||||
{"Alice", 25},
|
||||
{"Bob", 30},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("takes pointers while condition met", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 25}
|
||||
p2 := &Person{"Bob", 30}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
result := toSlice(TakeWhile(func(p *Person) bool { return p.Age < 35 })(seq))
|
||||
assert.Equal(t, []*Person{p1, p2}, result)
|
||||
})
|
||||
|
||||
t.Run("takes slices while condition met", func(t *testing.T) {
|
||||
seq := From([]int{1}, []int{1, 2}, []int{1, 2, 3}, []int{1})
|
||||
result := toSlice(TakeWhile(func(s []int) bool { return len(s) < 3 })(seq))
|
||||
expected := [][]int{{1}, {1, 2}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithChainedOperations tests TakeWhile with other sequence operations
|
||||
func TestTakeWhileWithChainedOperations(t *testing.T) {
|
||||
t.Run("takeWhile after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 8 })(mapped))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile 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(TakeWhile(func(x int) bool { return x < 7 })(filtered))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("map after takeWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
taken := TakeWhile(func(x int) bool { return x < 4 })(seq)
|
||||
result := toSlice(MonadMap(taken, N.Mul(10)))
|
||||
assert.Equal(t, []int{10, 20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after takeWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
taken := TakeWhile(func(x int) bool { return x < 7 })(seq)
|
||||
result := toSlice(MonadFilter(taken, func(x int) bool { return x%2 == 0 }))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile 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(TakeWhile(func(x int) bool { return x < 20 })(chained))
|
||||
assert.Equal(t, []int{1, 10, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("take after takeWhile", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken1 := TakeWhile(func(x int) bool { return x < 8 })(seq)
|
||||
taken2 := Take[int](3)(taken1)
|
||||
result := toSlice(taken2)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile after take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken := Take[int](7)(seq)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(taken))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithReplicate tests TakeWhile with Replicate
|
||||
func TestTakeWhileWithReplicate(t *testing.T) {
|
||||
t.Run("takes from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(10, 5)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x == 5 })(seq))
|
||||
assert.Equal(t, []int{5, 5, 5, 5, 5, 5, 5, 5, 5, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("takes none when predicate fails on replicate", func(t *testing.T) {
|
||||
seq := Replicate(5, 10)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 10 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithMakeBy tests TakeWhile with MakeBy
|
||||
func TestTakeWhileWithMakeBy(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(TakeWhile(func(x int) bool { return x < 25 })(seq))
|
||||
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i + 1 })
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 100 })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithPrependAppend tests TakeWhile with Prepend and Append
|
||||
func TestTakeWhileWithPrependAppend(t *testing.T) {
|
||||
t.Run("takeWhile from prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3, 4, 5)
|
||||
prepended := Prepend(1)(seq)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 4 })(prepended))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile from appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(10)(seq)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 10 })(appended))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile includes appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 5 })(appended))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileWithFlatten tests TakeWhile with Flatten
|
||||
func TestTakeWhileWithFlatten(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(TakeWhile(func(x int) bool { return x < 5 })(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(TakeWhile(func(x int) bool { return x < 4 })(flattened))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileDoesNotConsumeEntireSequence tests that TakeWhile is lazy
|
||||
func TestTakeWhileDoesNotConsumeEntireSequence(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 := TakeWhile(func(x int) bool { return x < 8 })(seq)
|
||||
|
||||
result := []int{}
|
||||
for v := range taken {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
// Should stop after finding element that fails predicate
|
||||
assert.LessOrEqual(t, callCount, 5, "should not consume significantly more than needed")
|
||||
assert.GreaterOrEqual(t, callCount, 4, "should consume at least enough to find failure")
|
||||
})
|
||||
|
||||
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 := TakeWhile(func(x int) bool { return x < 7 })(filtered)
|
||||
|
||||
result := []int{}
|
||||
for v := range taken {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
// Should stop after finding even number >= 7
|
||||
assert.LessOrEqual(t, callCount, 9, "should not consume significantly more than needed")
|
||||
assert.GreaterOrEqual(t, callCount, 7, "should consume at least enough to find 8")
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWhileEdgeCases tests edge cases
|
||||
func TestTakeWhileEdgeCases(t *testing.T) {
|
||||
t.Run("takeWhile with always false predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return false })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile with always true predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return true })(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile from single element that passes", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x > 0 })(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile from single element that fails", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(TakeWhile(func(x int) bool { return x < 0 })(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("takeWhile with complex predicate", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := toSlice(TakeWhile(func(x int) bool {
|
||||
return x%2 == 1 || x < 5
|
||||
})(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests for TakeWhile
|
||||
func BenchmarkTakeWhile(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
taken := TakeWhile(func(x int) bool { return x < 6 })(seq)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWhileLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
taken := TakeWhile(func(x int) bool { return x < 100 })(seq)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWhileWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
taken := TakeWhile(func(x int) bool { return x < 12 })(mapped)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWhileWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for range b.N {
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
taken := TakeWhile(func(x int) bool { return x < 7 })(filtered)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleTakeWhile() {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 1)
|
||||
taken := TakeWhile(func(x int) bool { return x < 4 })(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleTakeWhile_allSatisfy() {
|
||||
seq := From(2, 4, 6, 8)
|
||||
taken := TakeWhile(func(x int) bool { return x%2 == 0 })(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 8
|
||||
}
|
||||
|
||||
func ExampleTakeWhile_firstFails() {
|
||||
seq := From(5, 1, 2, 3)
|
||||
taken := TakeWhile(func(x int) bool { return x < 5 })(seq)
|
||||
|
||||
count := 0
|
||||
for range taken {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleTakeWhile_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
doubled := MonadMap(seq, N.Mul(2))
|
||||
taken := TakeWhile(func(x int) bool { return x < 8 })(doubled)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6
|
||||
}
|
||||
|
||||
func ExampleTakeWhile_strings() {
|
||||
seq := From("a", "b", "c", "1", "d", "e")
|
||||
isLetter := func(s string) bool { return s >= "a" && s <= "z" }
|
||||
taken := TakeWhile(isLetter)(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: a b c
|
||||
}
|
||||
|
||||
@@ -32,6 +32,13 @@ import (
|
||||
// the number of unique keys encountered. The operation is lazy - elements are processed
|
||||
// and filtered as they are consumed.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--2--4--1--5-->
|
||||
// Uniq(identity)
|
||||
// Output: --1--2--3-----4-----5-->
|
||||
// (first occurrence only)
|
||||
//
|
||||
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
|
||||
//
|
||||
// Type Parameters:
|
||||
@@ -119,6 +126,13 @@ func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
|
||||
// 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.
|
||||
//
|
||||
// Marble Diagram:
|
||||
//
|
||||
// Input: --1--2--3--2--4--1--5-->
|
||||
// StrictUniq
|
||||
// Output: --1--2--3-----4-----5-->
|
||||
// (first occurrence only)
|
||||
//
|
||||
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
|
||||
//
|
||||
// Type Parameters:
|
||||
|
||||
+2
-8
@@ -115,10 +115,7 @@ func Inc[T Number](value T) T {
|
||||
// result := Min(5, 10) // returns 5
|
||||
// result := Min(3.14, 2.71) // returns 2.71
|
||||
func Min[A C.Ordered](a, b A) A {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
return min(a, b)
|
||||
}
|
||||
|
||||
// Max returns the maximum of two ordered values.
|
||||
@@ -132,10 +129,7 @@ func Min[A C.Ordered](a, b A) A {
|
||||
// result := Max(5, 10) // returns 10
|
||||
// result := Max(3.14, 2.71) // returns 3.14
|
||||
func Max[A C.Ordered](a, b A) A {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
return max(a, b)
|
||||
}
|
||||
|
||||
// MoreThan is a curried comparison function that checks if a value is more than (greater than) another.
|
||||
|
||||
@@ -522,3 +522,199 @@ func MarshalJSON[T any](
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// FromNonZero creates a bidirectional codec for non-zero values of comparable types.
|
||||
// This codec validates that values are not equal to their zero value (e.g., 0 for int,
|
||||
// "" for string, false for bool, nil for pointers).
|
||||
//
|
||||
// The codec uses a refinement (prism) that:
|
||||
// - Decodes: Validates that the input is not the zero value of type T
|
||||
// - Encodes: Returns the value unchanged (identity function)
|
||||
// - Validates: Ensures the value is non-zero/non-default
|
||||
//
|
||||
// This is useful for enforcing that required fields have meaningful values rather than
|
||||
// their default zero values, which often represent "not set" or "missing" states.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: A comparable type (must support == and != operators)
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[T, T, T] codec that validates non-zero values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for non-zero integers
|
||||
// nonZeroInt := FromNonZero[int]()
|
||||
//
|
||||
// // Decode non-zero value succeeds
|
||||
// result := nonZeroInt.Decode(42)
|
||||
// // result is Right(42)
|
||||
//
|
||||
// // Decode zero value fails
|
||||
// result := nonZeroInt.Decode(0)
|
||||
// // result is Left(ValidationError{...})
|
||||
//
|
||||
// // Encode is identity
|
||||
// encoded := nonZeroInt.Encode(42)
|
||||
// // encoded is 42
|
||||
//
|
||||
// // Works with strings
|
||||
// nonEmptyStr := FromNonZero[string]()
|
||||
// result := nonEmptyStr.Decode("hello") // Right("hello")
|
||||
// result = nonEmptyStr.Decode("") // Left(ValidationError{...})
|
||||
//
|
||||
// // Works with pointers
|
||||
// nonNilPtr := FromNonZero[*int]()
|
||||
// value := 42
|
||||
// result := nonNilPtr.Decode(&value) // Right(&value)
|
||||
// result = nonNilPtr.Decode(nil) // Left(ValidationError{...})
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating required numeric fields are not zero
|
||||
// - Ensuring string fields are not empty
|
||||
// - Checking pointers are not nil
|
||||
// - Validating boolean flags are explicitly set to true
|
||||
// - Composing with other codecs for multi-stage validation
|
||||
//
|
||||
// See Also:
|
||||
// - NonEmptyString: Specialized version for strings with clearer intent
|
||||
// - FromRefinement: General function for creating codecs from prisms
|
||||
func FromNonZero[T comparable]() Type[T, T, T] {
|
||||
return FromRefinement(prism.FromNonZero[T]())
|
||||
}
|
||||
|
||||
// NonEmptyString creates a bidirectional codec for non-empty strings.
|
||||
// This codec validates that string values are not empty, providing a type-safe
|
||||
// way to work with strings that must contain at least one character.
|
||||
//
|
||||
// This is a specialized version of FromNonZero[string]() that makes the intent
|
||||
// clearer when working specifically with strings that must not be empty.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Validates that the input string is not empty ("")
|
||||
// - Encodes: Returns the string unchanged (identity function)
|
||||
// - Validates: Ensures the string has length > 0
|
||||
//
|
||||
// Note: This codec only checks for empty strings, not whitespace-only strings.
|
||||
// A string containing only spaces, tabs, or newlines will pass validation.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[string, string, string] codec that validates non-empty strings
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nonEmpty := NonEmptyString()
|
||||
//
|
||||
// // Decode non-empty string succeeds
|
||||
// result := nonEmpty.Decode("hello")
|
||||
// // result is Right("hello")
|
||||
//
|
||||
// // Decode empty string fails
|
||||
// result := nonEmpty.Decode("")
|
||||
// // result is Left(ValidationError{...})
|
||||
//
|
||||
// // Whitespace-only strings pass validation
|
||||
// result := nonEmpty.Decode(" ")
|
||||
// // result is Right(" ")
|
||||
//
|
||||
// // Encode is identity
|
||||
// encoded := nonEmpty.Encode("world")
|
||||
// // encoded is "world"
|
||||
//
|
||||
// // Compose with other codecs for validation pipelines
|
||||
// intFromNonEmptyString := Pipe(IntFromString())(nonEmpty)
|
||||
// result := intFromNonEmptyString.Decode("42") // Right(42)
|
||||
// result = intFromNonEmptyString.Decode("") // Left(ValidationError{...})
|
||||
// result = intFromNonEmptyString.Decode("abc") // Left(ValidationError{...})
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating required string fields (usernames, names, IDs)
|
||||
// - Ensuring configuration values are provided
|
||||
// - Validating user input before processing
|
||||
// - Composing with parsing codecs to validate before parsing
|
||||
// - Building validation pipelines for string data
|
||||
//
|
||||
// See Also:
|
||||
// - FromNonZero: General version for any comparable type
|
||||
// - String: Basic string codec without validation
|
||||
// - IntFromString: Codec for parsing integers from strings
|
||||
func NonEmptyString() Type[string, string, string] {
|
||||
return F.Pipe1(
|
||||
FromRefinement(prism.NonEmptyString()),
|
||||
WithName[string, string, string]("NonEmptyString"),
|
||||
)
|
||||
}
|
||||
|
||||
// WithName creates an endomorphism that renames a codec without changing its behavior.
|
||||
// This function returns a higher-order function that takes a codec and returns a new codec
|
||||
// with the specified name, while preserving all validation, encoding, and type-checking logic.
|
||||
//
|
||||
// This is useful for:
|
||||
// - Providing more descriptive names for composed codecs
|
||||
// - Creating domain-specific codec names for better error messages
|
||||
// - Documenting the purpose of complex codec pipelines
|
||||
// - Improving debugging and logging output
|
||||
//
|
||||
// The renamed codec maintains the same:
|
||||
// - Type checking behavior (Is function)
|
||||
// - Validation logic (Validate function)
|
||||
// - Encoding behavior (Encode function)
|
||||
//
|
||||
// Only the name returned by the Name() method changes.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type (what we decode to and encode from)
|
||||
// - O: The output type (what we encode to)
|
||||
// - I: The input type (what we decode from)
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The new name for the codec
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism[Type[A, O, I]] that renames the codec
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec with a generic name
|
||||
// positiveInt := Pipe[int, int, string, int](
|
||||
// FromRefinement(prism.FromPredicate(func(n int) bool { return n > 0 })),
|
||||
// )(IntFromString())
|
||||
// // positiveInt.Name() returns something like "Pipe(FromRefinement(...), IntFromString)"
|
||||
//
|
||||
// // Rename it for clarity
|
||||
// namedCodec := WithName[int, string, string]("PositiveIntFromString")(positiveInt)
|
||||
// // namedCodec.Name() returns "PositiveIntFromString"
|
||||
//
|
||||
// // Use in a pipeline with F.Pipe
|
||||
// userAgeCodec := F.Pipe1(
|
||||
// IntFromString(),
|
||||
// WithName[int, string, string]("UserAge"),
|
||||
// )
|
||||
//
|
||||
// // Validation errors will show the custom name
|
||||
// result := userAgeCodec.Decode("invalid")
|
||||
// // Error context will reference "UserAge" instead of "IntFromString"
|
||||
//
|
||||
// Common use cases:
|
||||
// - Naming composed codecs for better error messages
|
||||
// - Creating domain-specific codec names (e.g., "EmailAddress", "PhoneNumber")
|
||||
// - Documenting complex validation pipelines
|
||||
// - Improving debugging output in logs
|
||||
// - Making codec composition more readable
|
||||
//
|
||||
// Note: This function creates a new codec instance with the same behavior but a different
|
||||
// name. The original codec is not modified.
|
||||
//
|
||||
// See Also:
|
||||
// - MakeType: For creating codecs with custom names from scratch
|
||||
// - Pipe: For composing codecs (which generates automatic names)
|
||||
func WithName[A, O, I any](name string) Endomorphism[Type[A, O, I]] {
|
||||
return func(codec Type[A, O, I]) Type[A, O, I] {
|
||||
return MakeType(
|
||||
name,
|
||||
codec.Is,
|
||||
codec.Validate,
|
||||
codec.Encode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
@@ -688,6 +689,596 @@ func TestBoolFromString_Integration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FromNonZero
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestFromNonZero_Decode_Success(t *testing.T) {
|
||||
t.Run("int - decodes non-zero value", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(42)
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("int - decodes negative value", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(-5)
|
||||
assert.Equal(t, validation.Success(-5), result)
|
||||
})
|
||||
|
||||
t.Run("string - decodes non-empty string", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
result := c.Decode("hello")
|
||||
assert.Equal(t, validation.Success("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("string - decodes whitespace string", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
result := c.Decode(" ")
|
||||
assert.Equal(t, validation.Success(" "), result)
|
||||
})
|
||||
|
||||
t.Run("bool - decodes true", func(t *testing.T) {
|
||||
c := FromNonZero[bool]()
|
||||
result := c.Decode(true)
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("float64 - decodes non-zero value", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
result := c.Decode(3.14)
|
||||
assert.Equal(t, validation.Success(3.14), result)
|
||||
})
|
||||
|
||||
t.Run("float64 - decodes negative value", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
result := c.Decode(-2.5)
|
||||
assert.Equal(t, validation.Success(-2.5), result)
|
||||
})
|
||||
|
||||
t.Run("pointer - decodes non-nil pointer", func(t *testing.T) {
|
||||
c := FromNonZero[*int]()
|
||||
value := 42
|
||||
result := c.Decode(&value)
|
||||
assert.True(t, either.IsRight(result))
|
||||
ptr := either.MonadFold(result, func(validation.Errors) *int { return nil }, func(p *int) *int { return p })
|
||||
require.NotNil(t, ptr)
|
||||
assert.Equal(t, 42, *ptr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Decode_Failure(t *testing.T) {
|
||||
t.Run("int - fails on zero", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(0)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("string - fails on empty string", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("bool - fails on false", func(t *testing.T) {
|
||||
c := FromNonZero[bool]()
|
||||
result := c.Decode(false)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("float64 - fails on zero", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
result := c.Decode(0.0)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("pointer - fails on nil", func(t *testing.T) {
|
||||
c := FromNonZero[*int]()
|
||||
result := c.Decode(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Encode(t *testing.T) {
|
||||
t.Run("int - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
assert.Equal(t, 42, c.Encode(42))
|
||||
})
|
||||
|
||||
t.Run("string - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
assert.Equal(t, "hello", c.Encode("hello"))
|
||||
})
|
||||
|
||||
t.Run("bool - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[bool]()
|
||||
assert.Equal(t, true, c.Encode(true))
|
||||
})
|
||||
|
||||
t.Run("float64 - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
assert.Equal(t, 3.14, c.Encode(3.14))
|
||||
})
|
||||
|
||||
t.Run("pointer - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[*int]()
|
||||
value := 42
|
||||
ptr := &value
|
||||
assert.Equal(t, ptr, c.Encode(ptr))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode then encode", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
original := 42
|
||||
result := c.Decode(original)
|
||||
require.True(t, either.IsRight(result))
|
||||
decoded := either.MonadFold(result, func(validation.Errors) int { return 0 }, func(n int) int { return n })
|
||||
assert.Equal(t, original, c.Encode(decoded))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Name(t *testing.T) {
|
||||
t.Run("int codec name", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
assert.Contains(t, c.Name(), "FromRefinement")
|
||||
assert.Contains(t, c.Name(), "PrismFromNonZero")
|
||||
})
|
||||
|
||||
t.Run("string codec name", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
assert.Contains(t, c.Name(), "FromRefinement")
|
||||
assert.Contains(t, c.Name(), "PrismFromNonZero")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Integration(t *testing.T) {
|
||||
t.Run("validates multiple non-zero integers", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
values := []int{1, -1, 42, -100, 999}
|
||||
for _, v := range values {
|
||||
result := c.Decode(v)
|
||||
require.True(t, either.IsRight(result), "expected success for %d", v)
|
||||
decoded := either.MonadFold(result, func(validation.Errors) int { return 0 }, func(n int) int { return n })
|
||||
assert.Equal(t, v, decoded)
|
||||
assert.Equal(t, v, c.Encode(decoded))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects zero values", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(0)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("works with custom comparable types", func(t *testing.T) {
|
||||
type UserID string
|
||||
c := FromNonZero[UserID]()
|
||||
|
||||
result := c.Decode(UserID("user123"))
|
||||
assert.Equal(t, validation.Success(UserID("user123")), result)
|
||||
|
||||
result = c.Decode(UserID(""))
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NonEmptyString
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestNonEmptyString_Decode_Success(t *testing.T) {
|
||||
t.Run("decodes non-empty string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("hello")
|
||||
assert.Equal(t, validation.Success("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes single character", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("a")
|
||||
assert.Equal(t, validation.Success("a"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes whitespace string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode(" ")
|
||||
assert.Equal(t, validation.Success(" "), result)
|
||||
})
|
||||
|
||||
t.Run("decodes string with newlines", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("\n\t")
|
||||
assert.Equal(t, validation.Success("\n\t"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes unicode string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("你好")
|
||||
assert.Equal(t, validation.Success("你好"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes emoji string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("🎉")
|
||||
assert.Equal(t, validation.Success("🎉"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes multiline string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
multiline := "line1\nline2\nline3"
|
||||
result := c.Decode(multiline)
|
||||
assert.Equal(t, validation.Success(multiline), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Decode_Failure(t *testing.T) {
|
||||
t.Run("fails on empty string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("error contains context", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("")
|
||||
require.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result, func(e validation.Errors) validation.Errors { return e }, func(string) validation.Errors { return nil })
|
||||
require.NotEmpty(t, errors)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Encode(t *testing.T) {
|
||||
t.Run("encodes string unchanged", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, "hello", c.Encode("hello"))
|
||||
})
|
||||
|
||||
t.Run("encodes unicode string unchanged", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, "你好", c.Encode("你好"))
|
||||
})
|
||||
|
||||
t.Run("encodes whitespace string unchanged", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, " ", c.Encode(" "))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode then encode", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
original := "test string"
|
||||
result := c.Decode(original)
|
||||
require.True(t, either.IsRight(result))
|
||||
decoded := either.MonadFold(result, func(validation.Errors) string { return "" }, func(s string) string { return s })
|
||||
assert.Equal(t, original, c.Encode(decoded))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Name(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, c.Name(), "NonEmptyString")
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Integration(t *testing.T) {
|
||||
t.Run("validates multiple non-empty strings", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
strings := []string{"a", "hello", "world", "test123", " spaces ", "🎉"}
|
||||
for _, s := range strings {
|
||||
result := c.Decode(s)
|
||||
require.True(t, either.IsRight(result), "expected success for %q", s)
|
||||
decoded := either.MonadFold(result, func(validation.Errors) string { return "" }, func(str string) string { return str })
|
||||
assert.Equal(t, s, decoded)
|
||||
assert.Equal(t, s, c.Encode(decoded))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects empty string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("compose with IntFromString", func(t *testing.T) {
|
||||
// Create a codec that only parses non-empty strings to integers
|
||||
nonEmptyThenInt := Pipe[string, string](IntFromString())(NonEmptyString())
|
||||
|
||||
// Valid non-empty string with integer
|
||||
result := nonEmptyThenInt.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Empty string fails at NonEmptyString stage
|
||||
result = nonEmptyThenInt.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
// Non-empty but invalid integer fails at IntFromString stage
|
||||
result = nonEmptyThenInt.Decode("abc")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("use in validation pipeline", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
|
||||
// Simulate validating user input
|
||||
inputs := []struct {
|
||||
value string
|
||||
expected bool
|
||||
}{
|
||||
{"john_doe", true},
|
||||
{"", false},
|
||||
{"a", true},
|
||||
{"user@example.com", true},
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
result := c.Decode(input.value)
|
||||
if input.expected {
|
||||
assert.True(t, either.IsRight(result), "expected success for %q", input.value)
|
||||
} else {
|
||||
assert.True(t, either.IsLeft(result), "expected failure for %q", input.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WithName
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestWithName_BasicFunctionality(t *testing.T) {
|
||||
t.Run("renames codec without changing behavior", func(t *testing.T) {
|
||||
original := IntFromString()
|
||||
renamed := WithName[int, string, string]("CustomIntCodec")(original)
|
||||
|
||||
// Name should be changed
|
||||
assert.Equal(t, "CustomIntCodec", renamed.Name())
|
||||
assert.NotEqual(t, original.Name(), renamed.Name())
|
||||
|
||||
// Behavior should be unchanged
|
||||
result := renamed.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
encoded := renamed.Encode(42)
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
|
||||
t.Run("preserves validation logic", func(t *testing.T) {
|
||||
original := IntFromString()
|
||||
renamed := WithName[int, string, string]("MyInt")(original)
|
||||
|
||||
// Valid input should succeed
|
||||
result := renamed.Decode("123")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Invalid input should fail
|
||||
result = renamed.Decode("not a number")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("preserves encoding logic", func(t *testing.T) {
|
||||
original := BoolFromString()
|
||||
renamed := WithName[bool, string, string]("CustomBool")(original)
|
||||
|
||||
assert.Equal(t, "true", renamed.Encode(true))
|
||||
assert.Equal(t, "false", renamed.Encode(false))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_WithComposedCodecs(t *testing.T) {
|
||||
t.Run("renames composed codec", func(t *testing.T) {
|
||||
// Create a composed codec
|
||||
composed := Pipe[string, string](IntFromString())(NonEmptyString())
|
||||
|
||||
// Rename it
|
||||
renamed := WithName[int, string, string]("NonEmptyIntString")(composed)
|
||||
|
||||
assert.Equal(t, "NonEmptyIntString", renamed.Name())
|
||||
|
||||
// Behavior should be preserved
|
||||
result := renamed.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Empty string should fail
|
||||
result = renamed.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
// Non-numeric should fail
|
||||
result = renamed.Decode("abc")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("works in pipeline with F.Pipe", func(t *testing.T) {
|
||||
codec := F.Pipe1(
|
||||
IntFromString(),
|
||||
WithName[int, string, string]("UserAge"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "UserAge", codec.Name())
|
||||
|
||||
result := codec.Decode("25")
|
||||
assert.Equal(t, validation.Success(25), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_PreservesTypeChecking(t *testing.T) {
|
||||
t.Run("preserves Is function", func(t *testing.T) {
|
||||
original := String()
|
||||
renamed := WithName[string, string, any]("CustomString")(original)
|
||||
|
||||
// Should accept string
|
||||
result := renamed.Is("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should reject non-string
|
||||
result = renamed.Is(42)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("preserves complex type checking", func(t *testing.T) {
|
||||
original := Array(Int())
|
||||
renamed := WithName[[]int, []int, any]("IntArray")(original)
|
||||
|
||||
// Should accept []int
|
||||
result := renamed.Is([]int{1, 2, 3})
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should reject []string
|
||||
result = renamed.Is([]string{"a", "b"})
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_RoundTrip(t *testing.T) {
|
||||
t.Run("maintains round-trip property", func(t *testing.T) {
|
||||
original := Int64FromString()
|
||||
renamed := WithName[int64, string, string]("CustomInt64")(original)
|
||||
|
||||
testValues := []string{"0", "42", "-100", "9223372036854775807"}
|
||||
for _, input := range testValues {
|
||||
result := renamed.Decode(input)
|
||||
require.True(t, either.IsRight(result), "expected success for %s", input)
|
||||
|
||||
decoded := either.MonadFold(result, func(validation.Errors) int64 { return 0 }, func(n int64) int64 { return n })
|
||||
encoded := renamed.Encode(decoded)
|
||||
assert.Equal(t, input, encoded)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_ErrorMessages(t *testing.T) {
|
||||
t.Run("custom name appears in validation context", func(t *testing.T) {
|
||||
codec := WithName[int, string, string]("PositiveInteger")(IntFromString())
|
||||
|
||||
result := codec.Decode("not a number")
|
||||
require.True(t, either.IsLeft(result))
|
||||
|
||||
// The error context should reference the custom name
|
||||
errors := either.MonadFold(result, func(e validation.Errors) validation.Errors { return e }, func(int) validation.Errors { return nil })
|
||||
require.NotEmpty(t, errors)
|
||||
|
||||
// Check that at least one error references our custom name
|
||||
found := false
|
||||
for _, err := range errors {
|
||||
if len(err.Context) > 0 {
|
||||
for _, ctx := range err.Context {
|
||||
if ctx.Type == "PositiveInteger" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "expected custom name 'PositiveInteger' in error context")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_MultipleRenames(t *testing.T) {
|
||||
t.Run("can rename multiple times", func(t *testing.T) {
|
||||
codec := IntFromString()
|
||||
|
||||
renamed1 := WithName[int, string, string]("FirstName")(codec)
|
||||
assert.Equal(t, "FirstName", renamed1.Name())
|
||||
|
||||
renamed2 := WithName[int, string, string]("SecondName")(renamed1)
|
||||
assert.Equal(t, "SecondName", renamed2.Name())
|
||||
|
||||
// Behavior should still work
|
||||
result := renamed2.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_WithDifferentTypes(t *testing.T) {
|
||||
t.Run("works with string codec", func(t *testing.T) {
|
||||
codec := WithName[string, string, string]("Username")(NonEmptyString())
|
||||
assert.Equal(t, "Username", codec.Name())
|
||||
|
||||
result := codec.Decode("john_doe")
|
||||
assert.Equal(t, validation.Success("john_doe"), result)
|
||||
})
|
||||
|
||||
t.Run("works with bool codec", func(t *testing.T) {
|
||||
codec := WithName[bool, string, string]("IsActive")(BoolFromString())
|
||||
assert.Equal(t, "IsActive", codec.Name())
|
||||
|
||||
result := codec.Decode("true")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("works with URL codec", func(t *testing.T) {
|
||||
codec := WithName[*url.URL, string, string]("WebsiteURL")(URL())
|
||||
assert.Equal(t, "WebsiteURL", codec.Name())
|
||||
|
||||
result := codec.Decode("https://example.com")
|
||||
assert.True(t, either.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("works with array codec", func(t *testing.T) {
|
||||
codec := WithName[[]int, []int, any]("Numbers")(Array(Int()))
|
||||
assert.Equal(t, "Numbers", codec.Name())
|
||||
|
||||
result := codec.Decode([]int{1, 2, 3})
|
||||
assert.Equal(t, validation.Success([]int{1, 2, 3}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_AsDecoderEncoder(t *testing.T) {
|
||||
t.Run("AsDecoder returns decoder interface", func(t *testing.T) {
|
||||
codec := WithName[int, string, string]("MyInt")(IntFromString())
|
||||
decoder := codec.AsDecoder()
|
||||
|
||||
result := decoder.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("AsEncoder returns encoder interface", func(t *testing.T) {
|
||||
codec := WithName[int, string, string]("MyInt")(IntFromString())
|
||||
encoder := codec.AsEncoder()
|
||||
|
||||
encoded := encoder.Encode(42)
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_Integration(t *testing.T) {
|
||||
t.Run("domain-specific codec names", func(t *testing.T) {
|
||||
// Create domain-specific codecs with meaningful names
|
||||
emailCodec := WithName[string, string, string]("EmailAddress")(NonEmptyString())
|
||||
phoneCodec := WithName[string, string, string]("PhoneNumber")(NonEmptyString())
|
||||
ageCodec := WithName[int, string, string]("Age")(IntFromString())
|
||||
|
||||
// Test email
|
||||
result := emailCodec.Decode("user@example.com")
|
||||
assert.True(t, either.IsRight(result))
|
||||
assert.Equal(t, "EmailAddress", emailCodec.Name())
|
||||
|
||||
// Test phone
|
||||
result = phoneCodec.Decode("+1234567890")
|
||||
assert.True(t, either.IsRight(result))
|
||||
assert.Equal(t, "PhoneNumber", phoneCodec.Name())
|
||||
|
||||
// Test age
|
||||
ageResult := ageCodec.Decode("25")
|
||||
assert.True(t, either.IsRight(ageResult))
|
||||
assert.Equal(t, "Age", ageCodec.Name())
|
||||
})
|
||||
|
||||
t.Run("naming complex validation pipelines", func(t *testing.T) {
|
||||
// Create a complex codec and give it a clear name
|
||||
positiveIntCodec := F.Pipe2(
|
||||
NonEmptyString(),
|
||||
Pipe[string, string](IntFromString()),
|
||||
WithName[int, string, string]("PositiveIntegerFromString"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "PositiveIntegerFromString", positiveIntCodec.Name())
|
||||
|
||||
result := positiveIntCodec.Decode("42")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
result = positiveIntCodec.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MarshalJSON
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -773,7 +1364,7 @@ func TestIntFromString_PipeComposition(t *testing.T) {
|
||||
func(n int) int { return n },
|
||||
"PositiveInt",
|
||||
)
|
||||
positiveIntCodec := Pipe[string, string, int, int](
|
||||
positiveIntCodec := Pipe[string, string](
|
||||
FromRefinement(positiveIntPrism),
|
||||
)(IntFromString())
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
// 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 codec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/decode"
|
||||
)
|
||||
|
||||
// FromIso creates a Type codec from an Iso (isomorphism).
|
||||
//
|
||||
// An isomorphism represents a bidirectional transformation between types I and A
|
||||
// without any loss of information. This function converts an Iso[I, A] into a
|
||||
// Type[A, I, I] codec that can validate, decode, and encode values using the
|
||||
// isomorphism's transformations.
|
||||
//
|
||||
// The resulting codec:
|
||||
// - Decode: Uses iso.Get to transform I → A, always succeeds (no validation)
|
||||
// - Encode: Uses iso.ReverseGet to transform A → I
|
||||
// - Validation: Always succeeds since isomorphisms are lossless transformations
|
||||
// - Type checking: Uses standard type checking for type A
|
||||
//
|
||||
// This is particularly useful for:
|
||||
// - Creating codecs for newtype patterns (wrapping/unwrapping types)
|
||||
// - Building codecs for types with lossless conversions
|
||||
// - Composing with other codecs using Pipe or other operators
|
||||
// - Implementing bidirectional transformations in codec pipelines
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type (what we decode to and encode from)
|
||||
// - I: The input/output type (what we decode from and encode to)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - iso: An Iso[I, A] that defines the bidirectional transformation:
|
||||
// - Get: I → A (converts input to target type)
|
||||
// - ReverseGet: A → I (converts target back to input type)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Type[A, I, I] codec where:
|
||||
// - Decode: I → Validation[A] - transforms using iso.Get, always succeeds
|
||||
// - Encode: A → I - transforms using iso.ReverseGet
|
||||
// - Is: Checks if a value is of type A
|
||||
// - Name: Returns "FromIso[iso_string_representation]"
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// Decoding:
|
||||
// - Applies iso.Get to transform the input value
|
||||
// - Wraps the result in decode.Of (always successful validation)
|
||||
// - No validation errors can occur since isomorphisms are lossless
|
||||
//
|
||||
// Encoding:
|
||||
// - Applies iso.ReverseGet to transform back to the input type
|
||||
// - Always succeeds as isomorphisms guarantee reversibility
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Creating a codec for a newtype pattern:
|
||||
//
|
||||
// type UserId int
|
||||
//
|
||||
// // Define an isomorphism between int and UserId
|
||||
// userIdIso := iso.MakeIso(
|
||||
// func(id UserId) int { return int(id) },
|
||||
// func(i int) UserId { return UserId(i) },
|
||||
// )
|
||||
//
|
||||
// // Create a codec from the isomorphism
|
||||
// userIdCodec := codec.FromIso[int, UserId](userIdIso)
|
||||
//
|
||||
// // Decode: UserId → int
|
||||
// result := userIdCodec.Decode(UserId(42)) // Success: Right(42)
|
||||
//
|
||||
// // Encode: int → UserId
|
||||
// encoded := userIdCodec.Encode(42) // Returns: UserId(42)
|
||||
//
|
||||
// Using with temperature conversions:
|
||||
//
|
||||
// type Celsius float64
|
||||
// type Fahrenheit float64
|
||||
//
|
||||
// celsiusToFahrenheit := iso.MakeIso(
|
||||
// func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
// func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
// )
|
||||
//
|
||||
// tempCodec := codec.FromIso[Fahrenheit, Celsius](celsiusToFahrenheit)
|
||||
//
|
||||
// // Decode: Celsius → Fahrenheit
|
||||
// result := tempCodec.Decode(Celsius(20)) // Success: Right(68°F)
|
||||
//
|
||||
// // Encode: Fahrenheit → Celsius
|
||||
// encoded := tempCodec.Encode(Fahrenheit(68)) // Returns: 20°C
|
||||
//
|
||||
// Composing with other codecs:
|
||||
//
|
||||
// type Email string
|
||||
// type ValidatedEmail struct{ value Email }
|
||||
//
|
||||
// emailIso := iso.MakeIso(
|
||||
// func(ve ValidatedEmail) Email { return ve.value },
|
||||
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
|
||||
// )
|
||||
//
|
||||
// // Compose with string codec for validation
|
||||
// emailCodec := F.Pipe2(
|
||||
// codec.String(), // Type[string, string, any]
|
||||
// codec.Pipe(codec.FromIso[Email, string]( // Add string → Email iso
|
||||
// iso.MakeIso(
|
||||
// func(s string) Email { return Email(s) },
|
||||
// func(e Email) string { return string(e) },
|
||||
// ),
|
||||
// )),
|
||||
// codec.Pipe(codec.FromIso[ValidatedEmail, Email](emailIso)), // Add Email → ValidatedEmail iso
|
||||
// )
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Newtype patterns: Wrapping primitive types for type safety
|
||||
// - Unit conversions: Temperature, distance, time, etc.
|
||||
// - Format transformations: Between equivalent representations
|
||||
// - Type aliasing: Creating semantic types from base types
|
||||
// - Codec composition: Building complex codecs from simple isomorphisms
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Isomorphisms must satisfy the round-trip laws:
|
||||
// - iso.ReverseGet(iso.Get(i)) == i
|
||||
// - iso.Get(iso.ReverseGet(a)) == a
|
||||
// - Validation always succeeds since isomorphisms are lossless
|
||||
// - The codec name includes the isomorphism's string representation
|
||||
// - Type checking is performed using the standard Is[A]() function
|
||||
// - This codec is ideal for lossless transformations without validation logic
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - iso.Iso: The isomorphism type used by this function
|
||||
// - iso.MakeIso: Constructor for creating isomorphisms
|
||||
// - Pipe: For composing this codec with other codecs
|
||||
// - MakeType: For creating codecs with custom validation logic
|
||||
func FromIso[A, I any](iso Iso[I, A]) Type[A, I, I] {
|
||||
return MakeType(
|
||||
fmt.Sprintf("FromIso[%s]", iso),
|
||||
Is[A](),
|
||||
F.Flow2(
|
||||
iso.Get,
|
||||
decode.Of[Context],
|
||||
),
|
||||
iso.ReverseGet,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,504 @@
|
||||
// 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 codec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for newtype pattern
|
||||
type UserId int
|
||||
type Email string
|
||||
type Celsius float64
|
||||
type Fahrenheit float64
|
||||
|
||||
func TestFromIso_Success(t *testing.T) {
|
||||
t.Run("decodes using iso.Get", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(42))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("encodes using iso.ReverseGet", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(42)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, UserId(42), encoded)
|
||||
})
|
||||
|
||||
t.Run("round-trip preserves value", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
original := UserId(123)
|
||||
|
||||
// Act
|
||||
decoded := codec.Decode(original)
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(decoded))
|
||||
roundTrip := either.Fold[validation.Errors, int, UserId](
|
||||
func(validation.Errors) UserId { return UserId(0) },
|
||||
codec.Encode,
|
||||
)(decoded)
|
||||
assert.Equal(t, original, roundTrip)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_StringTypes(t *testing.T) {
|
||||
t.Run("handles string newtype", func(t *testing.T) {
|
||||
// Arrange
|
||||
emailIso := iso.MakeIso(
|
||||
func(e Email) string { return string(e) },
|
||||
func(s string) Email { return Email(s) },
|
||||
)
|
||||
codec := FromIso[string, Email](emailIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Email("user@example.com"))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success("user@example.com"), result)
|
||||
})
|
||||
|
||||
t.Run("encodes string newtype", func(t *testing.T) {
|
||||
// Arrange
|
||||
emailIso := iso.MakeIso(
|
||||
func(e Email) string { return string(e) },
|
||||
func(s string) Email { return Email(s) },
|
||||
)
|
||||
codec := FromIso[string, Email](emailIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode("admin@example.com")
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, Email("admin@example.com"), encoded)
|
||||
})
|
||||
|
||||
t.Run("handles empty string", func(t *testing.T) {
|
||||
// Arrange
|
||||
emailIso := iso.MakeIso(
|
||||
func(e Email) string { return string(e) },
|
||||
func(s string) Email { return Email(s) },
|
||||
)
|
||||
codec := FromIso[string, Email](emailIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Email(""))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_NumericConversions(t *testing.T) {
|
||||
t.Run("converts Celsius to Fahrenheit", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Celsius(0))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(Fahrenheit(32)), result)
|
||||
})
|
||||
|
||||
t.Run("converts Fahrenheit to Celsius", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(Fahrenheit(68))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, Celsius(20), encoded)
|
||||
})
|
||||
|
||||
t.Run("handles negative temperatures", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Celsius(-40))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(Fahrenheit(-40)), result)
|
||||
})
|
||||
|
||||
t.Run("temperature round-trip", func(t *testing.T) {
|
||||
// Arrange
|
||||
tempIso := iso.MakeIso(
|
||||
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
|
||||
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
|
||||
)
|
||||
codec := FromIso[Fahrenheit, Celsius](tempIso)
|
||||
original := Celsius(25)
|
||||
|
||||
// Act
|
||||
decoded := codec.Decode(original)
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(decoded))
|
||||
roundTrip := either.Fold[validation.Errors, Fahrenheit, Celsius](
|
||||
func(validation.Errors) Celsius { return Celsius(0) },
|
||||
codec.Encode,
|
||||
)(decoded)
|
||||
// Allow small floating point error
|
||||
diff := float64(original - roundTrip)
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
assert.True(t, diff < 0.0001)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero values", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(0))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(0), result)
|
||||
})
|
||||
|
||||
t.Run("handles negative values", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(-1))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(-1), result)
|
||||
})
|
||||
|
||||
t.Run("handles large values", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(999999999))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(999999999), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_TypeChecking(t *testing.T) {
|
||||
t.Run("Is checks target type", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
isResult := codec.Is(42)
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(isResult))
|
||||
})
|
||||
|
||||
t.Run("Is rejects wrong type", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
isResult := codec.Is("not an int")
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsLeft(isResult))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Name(t *testing.T) {
|
||||
t.Run("includes iso in name", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
name := codec.Name()
|
||||
|
||||
// Assert
|
||||
assert.True(t, len(name) > 0)
|
||||
assert.True(t, name[:7] == "FromIso")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Composition(t *testing.T) {
|
||||
t.Run("composes with Pipe", func(t *testing.T) {
|
||||
// Arrange
|
||||
type PositiveInt int
|
||||
|
||||
// First iso: UserId -> int
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
|
||||
// Second iso: int -> PositiveInt (no validation, just type conversion)
|
||||
positiveIso := iso.MakeIso(
|
||||
func(i int) PositiveInt { return PositiveInt(i) },
|
||||
func(p PositiveInt) int { return int(p) },
|
||||
)
|
||||
|
||||
// Compose codecs
|
||||
codec := F.Pipe1(
|
||||
FromIso[int, UserId](userIdIso),
|
||||
Pipe[UserId, UserId](FromIso[PositiveInt, int](positiveIso)),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(UserId(42))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Of(PositiveInt(42)), result)
|
||||
})
|
||||
|
||||
t.Run("composed codec encodes correctly", func(t *testing.T) {
|
||||
// Arrange
|
||||
type PositiveInt int
|
||||
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
|
||||
positiveIso := iso.MakeIso(
|
||||
func(i int) PositiveInt { return PositiveInt(i) },
|
||||
func(p PositiveInt) int { return int(p) },
|
||||
)
|
||||
|
||||
codec := F.Pipe1(
|
||||
FromIso[int, UserId](userIdIso),
|
||||
Pipe[UserId, UserId](FromIso[PositiveInt, int](positiveIso)),
|
||||
)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(PositiveInt(42))
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, UserId(42), encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Integration(t *testing.T) {
|
||||
t.Run("works with Array codec", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
userIdCodec := FromIso[int, UserId](userIdIso)
|
||||
arrayCodec := TranscodeArray(userIdCodec)
|
||||
|
||||
// Act
|
||||
result := arrayCodec.Decode([]UserId{UserId(1), UserId(2), UserId(3)})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
t.Run("encodes array correctly", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
userIdCodec := FromIso[int, UserId](userIdIso)
|
||||
arrayCodec := TranscodeArray(userIdCodec)
|
||||
|
||||
// Act
|
||||
encoded := arrayCodec.Encode([]int{1, 2, 3})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []UserId{UserId(1), UserId(2), UserId(3)}, encoded)
|
||||
})
|
||||
|
||||
t.Run("handles empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
userIdCodec := FromIso[int, UserId](userIdIso)
|
||||
arrayCodec := TranscodeArray(userIdCodec)
|
||||
|
||||
// Act
|
||||
result := arrayCodec.Decode([]UserId{})
|
||||
|
||||
// Assert
|
||||
assert.True(t, either.IsRight(result))
|
||||
decoded := either.Fold[validation.Errors, []int, []int](
|
||||
func(validation.Errors) []int { return nil },
|
||||
func(arr []int) []int { return arr },
|
||||
)(result)
|
||||
assert.Equal(t, 0, len(decoded))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_ComplexTypes(t *testing.T) {
|
||||
t.Run("handles struct wrapping", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Wrapper struct{ Value int }
|
||||
|
||||
wrapperIso := iso.MakeIso(
|
||||
func(w Wrapper) int { return w.Value },
|
||||
func(i int) Wrapper { return Wrapper{Value: i} },
|
||||
)
|
||||
codec := FromIso[int, Wrapper](wrapperIso)
|
||||
|
||||
// Act
|
||||
result := codec.Decode(Wrapper{Value: 42})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("encodes struct wrapping", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Wrapper struct{ Value int }
|
||||
|
||||
wrapperIso := iso.MakeIso(
|
||||
func(w Wrapper) int { return w.Value },
|
||||
func(i int) Wrapper { return Wrapper{Value: i} },
|
||||
)
|
||||
codec := FromIso[int, Wrapper](wrapperIso)
|
||||
|
||||
// Act
|
||||
encoded := codec.Encode(42)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, Wrapper{Value: 42}, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_AsDecoder(t *testing.T) {
|
||||
t.Run("returns decoder interface", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
decoder := codec.AsDecoder()
|
||||
|
||||
// Assert
|
||||
result := decoder.Decode(UserId(42))
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_AsEncoder(t *testing.T) {
|
||||
t.Run("returns encoder interface", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
encoder := codec.AsEncoder()
|
||||
|
||||
// Assert
|
||||
encoded := encoder.Encode(42)
|
||||
assert.Equal(t, UserId(42), encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromIso_Validate(t *testing.T) {
|
||||
t.Run("validate method works correctly", func(t *testing.T) {
|
||||
// Arrange
|
||||
userIdIso := iso.MakeIso(
|
||||
func(id UserId) int { return int(id) },
|
||||
func(i int) UserId { return UserId(i) },
|
||||
)
|
||||
codec := FromIso[int, UserId](userIdIso)
|
||||
|
||||
// Act
|
||||
validateFn := codec.Validate(UserId(42))
|
||||
result := validateFn([]validation.ContextEntry{})
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/decoder"
|
||||
"github.com/IBM/fp-go/v2/optics/encoder"
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
@@ -494,4 +495,7 @@ type (
|
||||
// - function.VOID: The single value of type Void
|
||||
// - Empty: Codec function that uses Void for unit types
|
||||
Void = function.Void
|
||||
|
||||
// Iso represents an isomorphism - a bidirectional transformation between two types.
|
||||
Iso[S, A any] = iso.Iso[S, A]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
I "github.com/IBM/fp-go/v2/optics/iso"
|
||||
)
|
||||
|
||||
// AsTraversal converts a iso to a traversal
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(I.Iso[S, A]) R {
|
||||
return func(sa I.Iso[S, A]) R {
|
||||
saSet := fmap(sa.ReverseGet)
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return F.Flow3(
|
||||
sa.Get,
|
||||
f,
|
||||
saSet,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,5 +23,5 @@ import (
|
||||
)
|
||||
|
||||
func AsTraversal[E, S, A any]() func(L.Lens[S, A]) T.Traversal[E, S, A] {
|
||||
return LG.AsTraversal[T.Traversal[E, S, A]](ET.MonadMap[E, A, S])
|
||||
return LG.AsTraversal[T.Traversal[E, S, A]](ET.Map[E, A, S])
|
||||
}
|
||||
|
||||
@@ -16,19 +16,24 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// AsTraversal converts a lens to a traversal
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fmap func(HKTA, func(A) S) HKTS,
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(L.Lens[S, A]) R {
|
||||
return func(sa L.Lens[S, A]) R {
|
||||
return func(f func(a A) HKTA) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return fmap(f(sa.Get(s)), func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
})
|
||||
return F.Pipe1(
|
||||
f(sa.Get(s)),
|
||||
fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
|
||||
@@ -909,6 +910,83 @@ func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S
|
||||
}
|
||||
}
|
||||
|
||||
// ModifyF transforms a value through a lens using a function that returns a value in a functor context.
|
||||
//
|
||||
// This is the functorial version of Modify, allowing transformations that produce effects
|
||||
// (like Option, Either, IO, etc.) while updating the focused value. The functor's map operation
|
||||
// is used to apply the lens's setter to the transformed value, preserving the computational context.
|
||||
//
|
||||
// This function corresponds to modifyF from monocle-ts, enabling effectful updates through lenses.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - S: Structure type
|
||||
// - A: Focus type (the value being transformed)
|
||||
// - HKTA: Higher-kinded type containing the transformed value (e.g., Option[A], Either[E, A])
|
||||
// - HKTS: Higher-kinded type containing the updated structure (e.g., Option[S], Either[E, S])
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fmap: A functor map operation that transforms A to S within the functor context
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A curried function that takes:
|
||||
// 1. A transformation function (A → HKTA)
|
||||
// 2. A Lens[S, A]
|
||||
// 3. A structure S
|
||||
// And returns the updated structure in the functor context (HKTS)
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.MakeLens(
|
||||
// func(p Person) int { return p.Age },
|
||||
// func(p Person, age int) Person { p.Age = age; return p },
|
||||
// )
|
||||
//
|
||||
// // Validate age is positive, returning Option
|
||||
// validateAge := func(age int) option.Option[int] {
|
||||
// if age > 0 {
|
||||
// return option.Some(age)
|
||||
// }
|
||||
// return option.None[int]()
|
||||
// }
|
||||
//
|
||||
// // Create a modifier that validates while updating
|
||||
// modifyAge := lens.ModifyF[Person, int](option.Functor[int, Person]().Map)
|
||||
//
|
||||
// person := Person{Name: "Alice", Age: 30}
|
||||
// result := modifyAge(validateAge)(ageLens)(person)
|
||||
// // result is Some(Person{Name: "Alice", Age: 30})
|
||||
//
|
||||
// invalidResult := modifyAge(func(age int) option.Option[int] {
|
||||
// return option.None[int]()
|
||||
// })(ageLens)(person)
|
||||
// // invalidResult is None[Person]()
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Modify: Non-functorial version for simple transformations
|
||||
// - functor.Functor: The functor interface used for mapping
|
||||
func ModifyF[S, A, HKTA, HKTS any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(Lens[S, A]) func(S) HKTS {
|
||||
return func(sa Lens[S, A]) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
})(f(sa.Get(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IMap transforms the focus type of a lens using an isomorphism.
|
||||
//
|
||||
// An isomorphism is a pair of functions (A → B, B → A) that are inverses of each other.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package lens
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
@@ -937,3 +938,367 @@ func TestMakeLensWithEq_WithNilState_MultipleOperations(t *testing.T) {
|
||||
assert.NotNil(t, street4)
|
||||
assert.Equal(t, "", street4.name)
|
||||
}
|
||||
|
||||
// TestModifyF_Success tests ModifyF with a simple Maybe-like functor for successful transformations
|
||||
func TestModifyF_Success(t *testing.T) {
|
||||
// Define a simple Maybe type for testing
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
none := func() Maybe[int] {
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
// Functor map for Maybe
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("transforms value with successful result", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
// Function that returns Some for positive values
|
||||
validatePositive := func(n int) Maybe[int] {
|
||||
if n > 0 {
|
||||
return some(n * 2)
|
||||
}
|
||||
return none()
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: 5, Foo: "test"}
|
||||
result := modifyAge(validatePositive)(ageLens)(person)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
updated := *result.value
|
||||
assert.Equal(t, 10, updated.Value)
|
||||
assert.Equal(t, "test", updated.Foo)
|
||||
})
|
||||
|
||||
t.Run("preserves structure with identity transformation", func(t *testing.T) {
|
||||
type MaybeStr struct {
|
||||
value *string
|
||||
}
|
||||
|
||||
someStr := func(s string) MaybeStr {
|
||||
return MaybeStr{value: &s}
|
||||
}
|
||||
|
||||
maybeStrMap := func(f func(string) Street) func(MaybeStr) struct{ value *Street } {
|
||||
return func(ma MaybeStr) struct{ value *Street } {
|
||||
if ma.value == nil {
|
||||
return struct{ value *Street }{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return struct{ value *Street }{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
nameLens := MakeLens(
|
||||
func(s Street) string { return s.name },
|
||||
func(s Street, name string) Street { s.name = name; return s },
|
||||
)
|
||||
|
||||
identity := func(s string) MaybeStr {
|
||||
return someStr(s)
|
||||
}
|
||||
|
||||
modifyName := ModifyF[Street, string](maybeStrMap)
|
||||
|
||||
street := Street{num: 1, name: "Main"}
|
||||
result := modifyName(identity)(nameLens)(street)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, street, *result.value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_Failure tests ModifyF with failures
|
||||
func TestModifyF_Failure(t *testing.T) {
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
none := func() Maybe[int] {
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("returns None when transformation fails", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
validatePositive := func(n int) Maybe[int] {
|
||||
if n > 0 {
|
||||
return some(n)
|
||||
}
|
||||
return none()
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: -5, Foo: "test"}
|
||||
result := modifyAge(validatePositive)(ageLens)(person)
|
||||
|
||||
assert.Nil(t, result.value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_WithResult tests ModifyF with Result/Either-like functor
|
||||
func TestModifyF_WithResult(t *testing.T) {
|
||||
type Result[A any] struct {
|
||||
value *A
|
||||
err error
|
||||
}
|
||||
|
||||
ok := func(a int) Result[int] {
|
||||
return Result[int]{value: &a, err: nil}
|
||||
}
|
||||
|
||||
fail := func(e error) Result[int] {
|
||||
return Result[int]{value: nil, err: e}
|
||||
}
|
||||
|
||||
resultMap := func(f func(int) Inner) func(Result[int]) Result[Inner] {
|
||||
return func(r Result[int]) Result[Inner] {
|
||||
if r.err != nil {
|
||||
return Result[Inner]{value: nil, err: r.err}
|
||||
}
|
||||
result := f(*r.value)
|
||||
return Result[Inner]{value: &result, err: nil}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("returns success for valid transformation", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
validateAge := func(n int) Result[int] {
|
||||
if n >= 0 && n <= 150 {
|
||||
return ok(n + 1)
|
||||
}
|
||||
return fail(errors.New("age out of range"))
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](resultMap)
|
||||
|
||||
person := Inner{Value: 30, Foo: "test"}
|
||||
result := modifyAge(validateAge)(ageLens)(person)
|
||||
|
||||
assert.Nil(t, result.err)
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, 31, result.value.Value)
|
||||
assert.Equal(t, "test", result.value.Foo)
|
||||
})
|
||||
|
||||
t.Run("returns error for failed validation", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
validateAge := func(n int) Result[int] {
|
||||
if n >= 0 && n <= 150 {
|
||||
return ok(n)
|
||||
}
|
||||
return fail(errors.New("age out of range"))
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](resultMap)
|
||||
|
||||
person := Inner{Value: 200, Foo: "test"}
|
||||
result := modifyAge(validateAge)(ageLens)(person)
|
||||
|
||||
assert.NotNil(t, result.err)
|
||||
assert.Equal(t, "age out of range", result.err.Error())
|
||||
assert.Nil(t, result.value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_EdgeCases tests edge cases for ModifyF
|
||||
func TestModifyF_EdgeCases(t *testing.T) {
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("handles zero values", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
identity := func(n int) Maybe[int] {
|
||||
return some(n)
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: 0, Foo: ""}
|
||||
result := modifyAge(identity)(ageLens)(person)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, person, *result.value)
|
||||
})
|
||||
|
||||
t.Run("works with composed lenses", func(t *testing.T) {
|
||||
innerLens := MakeLens(
|
||||
Outer.GetInner,
|
||||
Outer.SetInner,
|
||||
)
|
||||
valueLens := MakeLensRef(
|
||||
(*Inner).GetValue,
|
||||
(*Inner).SetValue,
|
||||
)
|
||||
|
||||
composedLens := Compose[Outer](valueLens)(innerLens)
|
||||
|
||||
maybeMapOuter := func(f func(int) Outer) func(Maybe[int]) Maybe[Outer] {
|
||||
return func(ma Maybe[int]) Maybe[Outer] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Outer]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Outer]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
validatePositive := func(n int) Maybe[int] {
|
||||
if n > 0 {
|
||||
return some(n * 2)
|
||||
}
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
modifyValue := ModifyF[Outer, int](maybeMapOuter)
|
||||
|
||||
outer := Outer{inner: &Inner{Value: 5, Foo: "test"}}
|
||||
result := modifyValue(validatePositive)(composedLens)(outer)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
assert.Equal(t, 10, result.value.inner.Value)
|
||||
assert.Equal(t, "test", result.value.inner.Foo)
|
||||
})
|
||||
}
|
||||
|
||||
// TestModifyF_Integration tests integration scenarios
|
||||
func TestModifyF_Integration(t *testing.T) {
|
||||
type Maybe[A any] struct {
|
||||
value *A
|
||||
}
|
||||
|
||||
some := func(a int) Maybe[int] {
|
||||
return Maybe[int]{value: &a}
|
||||
}
|
||||
|
||||
maybeMap := func(f func(int) Inner) func(Maybe[int]) Maybe[Inner] {
|
||||
return func(ma Maybe[int]) Maybe[Inner] {
|
||||
if ma.value == nil {
|
||||
return Maybe[Inner]{value: nil}
|
||||
}
|
||||
result := f(*ma.value)
|
||||
return Maybe[Inner]{value: &result}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("chains multiple ModifyF operations", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
increment := func(n int) Maybe[int] {
|
||||
return some(n + 1)
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
|
||||
person := Inner{Value: 5, Foo: "test"}
|
||||
|
||||
// Apply transformation twice
|
||||
result1 := modifyAge(increment)(ageLens)(person)
|
||||
assert.NotNil(t, result1.value)
|
||||
|
||||
result2 := modifyAge(increment)(ageLens)(*result1.value)
|
||||
assert.NotNil(t, result2.value)
|
||||
|
||||
assert.Equal(t, 7, result2.value.Value)
|
||||
})
|
||||
|
||||
t.Run("combines with regular Modify", func(t *testing.T) {
|
||||
ageLens := MakeLens(
|
||||
func(p Inner) int { return p.Value },
|
||||
func(p Inner, age int) Inner { p.Value = age; return p },
|
||||
)
|
||||
|
||||
// First use regular Modify
|
||||
person := Inner{Value: 5, Foo: "test"}
|
||||
modified := F.Pipe2(
|
||||
ageLens,
|
||||
Modify[Inner](func(n int) int { return n * 2 }),
|
||||
func(endoFn func(Inner) Inner) Inner {
|
||||
return endoFn(person)
|
||||
},
|
||||
)
|
||||
|
||||
assert.Equal(t, 10, modified.Value)
|
||||
|
||||
// Then use ModifyF with validation
|
||||
validateRange := func(n int) Maybe[int] {
|
||||
if n >= 0 && n <= 100 {
|
||||
return some(n)
|
||||
}
|
||||
return Maybe[int]{value: nil}
|
||||
}
|
||||
|
||||
modifyAge := ModifyF[Inner, int](maybeMap)
|
||||
result := modifyAge(validateRange)(ageLens)(modified)
|
||||
|
||||
assert.NotNil(t, result.value)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,5 +60,5 @@ import (
|
||||
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
|
||||
// // Apply operations across all configs using the traversal
|
||||
func AsTraversal[S, A any]() func(Lens[S, A]) T.Traversal[S, A] {
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.Map[A, S])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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 identity
|
||||
|
||||
import (
|
||||
I "github.com/IBM/fp-go/v2/identity"
|
||||
G "github.com/IBM/fp-go/v2/optics/lens/traversal/generic"
|
||||
)
|
||||
|
||||
// Compose composes a lens with a traversal to create a new traversal.
|
||||
//
|
||||
// This function allows you to focus deeper into a data structure by first using
|
||||
// a lens to access a field, then using a traversal to access multiple values within
|
||||
// that field. The result is a traversal that can operate on all the nested values.
|
||||
//
|
||||
// The composition follows the pattern: Lens[S, A] → Traversal[A, B] → Traversal[S, B]
|
||||
// where the lens focuses on field A within structure S, and the traversal focuses on
|
||||
// multiple B values within A.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The outer structure type
|
||||
// - A: The intermediate field type (target of the lens)
|
||||
// - B: The final focus type (targets of the traversal)
|
||||
//
|
||||
// Parameters:
|
||||
// - t: A traversal that focuses on B values within A
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns a Traversal[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/optics/lens"
|
||||
// LT "github.com/IBM/fp-go/v2/optics/lens/traversal"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// type Team struct {
|
||||
// Name string
|
||||
// Members []string
|
||||
// }
|
||||
//
|
||||
// // Lens to access the Members field
|
||||
// membersLens := lens.MakeLens(
|
||||
// func(t Team) []string { return t.Members },
|
||||
// func(t Team, m []string) Team { t.Members = m; return t },
|
||||
// )
|
||||
//
|
||||
// // Traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[string]()
|
||||
//
|
||||
// // Compose lens with traversal to access all member names
|
||||
// memberTraversal := F.Pipe1(
|
||||
// membersLens,
|
||||
// LT.Compose[Team, []string, string](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// team := Team{Name: "Engineering", Members: []string{"Alice", "Bob"}}
|
||||
// // Uppercase all member names
|
||||
// updated := memberTraversal(strings.ToUpper)(team)
|
||||
// // updated.Members: ["ALICE", "BOB"]
|
||||
//
|
||||
// See Also:
|
||||
// - Lens: A functional reference to a subpart of a data structure
|
||||
// - Traversal: A functional reference to multiple subparts
|
||||
// - traversal.Compose: Composes two traversals
|
||||
func Compose[S, A, B any](t Traversal[A, B, A, B]) func(Lens[S, A]) Traversal[S, B, S, B] {
|
||||
return G.Compose[S, A, B, S, A, B](
|
||||
I.Map,
|
||||
)(t)
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
// 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 identity
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
AR "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Team struct {
|
||||
Name string
|
||||
Members []string
|
||||
}
|
||||
|
||||
type Company struct {
|
||||
Name string
|
||||
Teams []Team
|
||||
}
|
||||
|
||||
func TestCompose_Success(t *testing.T) {
|
||||
t.Run("composes lens with array traversal to modify nested values", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice", "bob", "charlie"},
|
||||
}
|
||||
|
||||
// Act - uppercase all member names
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert
|
||||
expected := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"ALICE", "BOB", "CHARLIE"},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("composes lens with array traversal on empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{},
|
||||
}
|
||||
|
||||
// Act
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, team, result)
|
||||
})
|
||||
|
||||
t.Run("composes lens with array traversal to transform numbers", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Stats struct {
|
||||
Name string
|
||||
Scores []int
|
||||
}
|
||||
|
||||
scoresLens := lens.MakeLens(
|
||||
func(s Stats) []int { return s.Scores },
|
||||
func(s Stats, scores []int) Stats {
|
||||
s.Scores = scores
|
||||
return s
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
|
||||
scoreTraversal := F.Pipe1(
|
||||
scoresLens,
|
||||
Compose[Stats, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
stats := Stats{
|
||||
Name: "Player1",
|
||||
Scores: []int{10, 20, 30},
|
||||
}
|
||||
|
||||
// Act - double all scores
|
||||
result := scoreTraversal(func(n int) int { return n * 2 })(stats)
|
||||
|
||||
// Assert
|
||||
expected := Stats{
|
||||
Name: "Player1",
|
||||
Scores: []int{20, 40, 60},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose_Integration(t *testing.T) {
|
||||
t.Run("composes multiple lenses and traversals", func(t *testing.T) {
|
||||
// Arrange - nested structure with Company -> Teams -> Members
|
||||
teamsLens := lens.MakeLens(
|
||||
func(c Company) []Team { return c.Teams },
|
||||
func(c Company, teams []Team) Company {
|
||||
c.Teams = teams
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
// First compose: Company -> []Team -> Team
|
||||
teamArrayTraversal := AI.FromArray[Team]()
|
||||
companyToTeamTraversal := F.Pipe1(
|
||||
teamsLens,
|
||||
Compose[Company, []Team, Team](teamArrayTraversal),
|
||||
)
|
||||
|
||||
// Second compose: Team -> []string -> string
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
memberArrayTraversal := AI.FromArray[string]()
|
||||
teamToMemberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](memberArrayTraversal),
|
||||
)
|
||||
|
||||
company := Company{
|
||||
Name: "TechCorp",
|
||||
Teams: []Team{
|
||||
{Name: "Engineering", Members: []string{"alice", "bob"}},
|
||||
{Name: "Design", Members: []string{"charlie", "diana"}},
|
||||
},
|
||||
}
|
||||
|
||||
// Act - uppercase all members in all teams
|
||||
// First traverse to teams, then for each team traverse to members
|
||||
result := companyToTeamTraversal(func(team Team) Team {
|
||||
return teamToMemberTraversal(strings.ToUpper)(team)
|
||||
})(company)
|
||||
|
||||
// Assert
|
||||
expected := Company{
|
||||
Name: "TechCorp",
|
||||
Teams: []Team{
|
||||
{Name: "Engineering", Members: []string{"ALICE", "BOB"}},
|
||||
{Name: "Design", Members: []string{"CHARLIE", "DIANA"}},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose_EdgeCases(t *testing.T) {
|
||||
t.Run("preserves structure name when modifying members", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice"},
|
||||
}
|
||||
|
||||
// Act
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert - Name should be unchanged
|
||||
assert.Equal(t, "Engineering", result.Name)
|
||||
assert.Equal(t, AR.From("ALICE"), result.Members)
|
||||
})
|
||||
|
||||
t.Run("handles identity transformation", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice", "bob"},
|
||||
}
|
||||
|
||||
// Act - apply identity function
|
||||
result := memberTraversal(F.Identity[string])(team)
|
||||
|
||||
// Assert - should be unchanged
|
||||
assert.Equal(t, team, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
G "github.com/IBM/fp-go/v2/optics/lens/generic"
|
||||
TG "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
func Compose[S, A, B, HKTS, HKTA, HKTB any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
|
||||
lensTrav := G.AsTraversal[Traversal[S, A, HKTS, HKTA]](fmap)
|
||||
|
||||
return func(ab Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
|
||||
return F.Flow2(
|
||||
lensTrav,
|
||||
TG.Compose[
|
||||
Traversal[A, B, HKTA, HKTB],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, B, HKTS, HKTB],
|
||||
](ab),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
|
||||
)
|
||||
@@ -5,6 +5,7 @@ package lenses
|
||||
// 2026-01-27 16:08:47.5483589 +0100 CET m=+0.003380301
|
||||
|
||||
import (
|
||||
"net"
|
||||
url "net/url"
|
||||
|
||||
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
|
||||
@@ -119,6 +120,8 @@ type URLLenses struct {
|
||||
RawQuery __lens.Lens[url.URL, string]
|
||||
Fragment __lens.Lens[url.URL, string]
|
||||
RawFragment __lens.Lens[url.URL, string]
|
||||
Hostname __lens.Lens[url.URL, string]
|
||||
Port __lens.Lens[url.URL, string]
|
||||
// optional fields
|
||||
SchemeO __lens_option.LensO[url.URL, string]
|
||||
OpaqueO __lens_option.LensO[url.URL, string]
|
||||
@@ -131,6 +134,8 @@ type URLLenses struct {
|
||||
RawQueryO __lens_option.LensO[url.URL, string]
|
||||
FragmentO __lens_option.LensO[url.URL, string]
|
||||
RawFragmentO __lens_option.LensO[url.URL, string]
|
||||
HostnameO __lens_option.LensO[url.URL, string]
|
||||
PortO __lens_option.LensO[url.URL, string]
|
||||
}
|
||||
|
||||
// URLRefLenses provides lenses for accessing fields of url.URL via a reference to url.URL
|
||||
@@ -147,6 +152,8 @@ type URLRefLenses struct {
|
||||
RawQuery __lens.Lens[*url.URL, string]
|
||||
Fragment __lens.Lens[*url.URL, string]
|
||||
RawFragment __lens.Lens[*url.URL, string]
|
||||
Hostname __lens.Lens[*url.URL, string]
|
||||
Port __lens.Lens[*url.URL, string]
|
||||
// optional fields
|
||||
SchemeO __lens_option.LensO[*url.URL, string]
|
||||
OpaqueO __lens_option.LensO[*url.URL, string]
|
||||
@@ -159,6 +166,8 @@ type URLRefLenses struct {
|
||||
RawQueryO __lens_option.LensO[*url.URL, string]
|
||||
FragmentO __lens_option.LensO[*url.URL, string]
|
||||
RawFragmentO __lens_option.LensO[*url.URL, string]
|
||||
HostnameO __lens_option.LensO[*url.URL, string]
|
||||
PortO __lens_option.LensO[*url.URL, string]
|
||||
}
|
||||
|
||||
// MakeURLLenses creates a new URLLenses with lenses for all fields
|
||||
@@ -219,6 +228,38 @@ func MakeURLLenses() URLLenses {
|
||||
func(s url.URL, v string) url.URL { s.RawFragment = v; return s },
|
||||
"URL.RawFragment",
|
||||
)
|
||||
lensHostname := __lens.MakeLensWithName(
|
||||
func(s url.URL) string {
|
||||
host, _, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
return s.Host
|
||||
}
|
||||
return host
|
||||
},
|
||||
func(s url.URL, v string) url.URL {
|
||||
_, port, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
s.Host = v
|
||||
} else {
|
||||
s.Host = net.JoinHostPort(v, port)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"URL.Hostname",
|
||||
)
|
||||
lensPort := __lens.MakeLensWithName(
|
||||
func(s url.URL) string { return s.Port() },
|
||||
func(s url.URL, v string) url.URL {
|
||||
host, _, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
s.Host = net.JoinHostPort(s.Host, v)
|
||||
} else {
|
||||
s.Host = net.JoinHostPort(host, v)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"URL.Port",
|
||||
)
|
||||
// optional lenses
|
||||
lensSchemeO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensScheme)
|
||||
lensOpaqueO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensOpaque)
|
||||
@@ -231,6 +272,8 @@ func MakeURLLenses() URLLenses {
|
||||
lensRawQueryO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawQuery)
|
||||
lensFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensFragment)
|
||||
lensRawFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawFragment)
|
||||
lensHostnameO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensHostname)
|
||||
lensPortO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensPort)
|
||||
return URLLenses{
|
||||
// mandatory lenses
|
||||
Scheme: lensScheme,
|
||||
@@ -244,6 +287,8 @@ func MakeURLLenses() URLLenses {
|
||||
RawQuery: lensRawQuery,
|
||||
Fragment: lensFragment,
|
||||
RawFragment: lensRawFragment,
|
||||
Hostname: lensHostname,
|
||||
Port: lensPort,
|
||||
// optional lenses
|
||||
SchemeO: lensSchemeO,
|
||||
OpaqueO: lensOpaqueO,
|
||||
@@ -256,6 +301,8 @@ func MakeURLLenses() URLLenses {
|
||||
RawQueryO: lensRawQueryO,
|
||||
FragmentO: lensFragmentO,
|
||||
RawFragmentO: lensRawFragmentO,
|
||||
HostnameO: lensHostnameO,
|
||||
PortO: lensPortO,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +364,38 @@ func MakeURLRefLenses() URLRefLenses {
|
||||
func(s *url.URL, v string) *url.URL { s.RawFragment = v; return s },
|
||||
"(*url.URL).RawFragment",
|
||||
)
|
||||
lensHostname := __lens.MakeLensStrictWithName(
|
||||
func(s *url.URL) string {
|
||||
host, _, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
return s.Host
|
||||
}
|
||||
return host
|
||||
},
|
||||
func(s *url.URL, v string) *url.URL {
|
||||
_, port, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
s.Host = v
|
||||
} else {
|
||||
s.Host = net.JoinHostPort(v, port)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"URL.Hostname",
|
||||
)
|
||||
lensPort := __lens.MakeLensStrictWithName(
|
||||
(*url.URL).Port,
|
||||
func(s *url.URL, v string) *url.URL {
|
||||
host, _, err := net.SplitHostPort(s.Host)
|
||||
if err != nil {
|
||||
s.Host = net.JoinHostPort(s.Host, v)
|
||||
} else {
|
||||
s.Host = net.JoinHostPort(host, v)
|
||||
}
|
||||
return s
|
||||
},
|
||||
"URL.Port",
|
||||
)
|
||||
// optional lenses
|
||||
lensSchemeO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensScheme)
|
||||
lensOpaqueO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensOpaque)
|
||||
@@ -329,6 +408,8 @@ func MakeURLRefLenses() URLRefLenses {
|
||||
lensRawQueryO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawQuery)
|
||||
lensFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensFragment)
|
||||
lensRawFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawFragment)
|
||||
lensHostnameO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensHostname)
|
||||
lensPortO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensPort)
|
||||
return URLRefLenses{
|
||||
// mandatory lenses
|
||||
Scheme: lensScheme,
|
||||
@@ -342,6 +423,8 @@ func MakeURLRefLenses() URLRefLenses {
|
||||
RawQuery: lensRawQuery,
|
||||
Fragment: lensFragment,
|
||||
RawFragment: lensRawFragment,
|
||||
Hostname: lensHostname,
|
||||
Port: lensPort,
|
||||
// optional lenses
|
||||
SchemeO: lensSchemeO,
|
||||
OpaqueO: lensOpaqueO,
|
||||
@@ -354,6 +437,8 @@ func MakeURLRefLenses() URLRefLenses {
|
||||
RawQueryO: lensRawQueryO,
|
||||
FragmentO: lensFragmentO,
|
||||
RawFragmentO: lensRawFragmentO,
|
||||
HostnameO: lensHostnameO,
|
||||
PortO: lensPortO,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+501
-590
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
// 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 array
|
||||
|
||||
import (
|
||||
OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
G "github.com/IBM/fp-go/v2/optics/optional/array/generic"
|
||||
)
|
||||
|
||||
// At creates an Optional that focuses on the element at a specific index in an array.
|
||||
//
|
||||
// This function returns an Optional that can get and set the element at the given index.
|
||||
// If the index is out of bounds, GetOption returns None and Set operations are no-ops
|
||||
// (the array is returned unchanged). This follows the Optional laws where operations
|
||||
// on non-existent values have no effect.
|
||||
//
|
||||
// The Optional provides safe array access without panicking on invalid indices, making
|
||||
// it ideal for functional transformations where you want to modify array elements only
|
||||
// when they exist.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - idx: The zero-based index to focus on
|
||||
//
|
||||
// Returns:
|
||||
// - An Optional that focuses on the element at the specified index
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
// OA "github.com/IBM/fp-go/v2/optics/optional/array"
|
||||
// )
|
||||
//
|
||||
// numbers := []int{10, 20, 30, 40}
|
||||
//
|
||||
// // Create an optional focusing on index 1
|
||||
// second := OA.At[int](1)
|
||||
//
|
||||
// // Get the element at index 1
|
||||
// value := second.GetOption(numbers)
|
||||
// // value: option.Some(20)
|
||||
//
|
||||
// // Set the element at index 1
|
||||
// updated := second.Set(25)(numbers)
|
||||
// // updated: []int{10, 25, 30, 40}
|
||||
//
|
||||
// // Out of bounds access returns None
|
||||
// outOfBounds := OA.At[int](10)
|
||||
// value = outOfBounds.GetOption(numbers)
|
||||
// // value: option.None[int]()
|
||||
//
|
||||
// // Out of bounds set is a no-op
|
||||
// unchanged := outOfBounds.Set(99)(numbers)
|
||||
// // unchanged: []int{10, 20, 30, 40} (original array)
|
||||
//
|
||||
// See Also:
|
||||
// - AR.Lookup: Gets an element at an index, returning an Option
|
||||
// - AR.UpdateAt: Updates an element at an index, returning an Option
|
||||
// - OP.Optional: The Optional optic type
|
||||
func At[A any](idx int) OP.Optional[[]A, A] {
|
||||
return G.At[[]A](idx)
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
// 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 array
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestAt_GetOption tests the GetOption functionality
|
||||
func TestAt_GetOption(t *testing.T) {
|
||||
t.Run("returns Some for valid index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.Some(20), result)
|
||||
})
|
||||
|
||||
t.Run("returns Some for first element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("returns Some for last element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](2)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.Some(30), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for negative index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](-1)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for out of bounds index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty array", func(t *testing.T) {
|
||||
numbers := []int{}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for nil array", func(t *testing.T) {
|
||||
var numbers []int
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_Set tests the Set functionality
|
||||
func TestAt_Set(t *testing.T) {
|
||||
t.Run("updates element at valid index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
result := optional.Set(25)(numbers)
|
||||
|
||||
assert.Equal(t, []int{10, 25, 30, 40}, result)
|
||||
assert.Equal(t, []int{10, 20, 30, 40}, numbers) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("updates first element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.Set(5)(numbers)
|
||||
|
||||
assert.Equal(t, []int{5, 20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("updates last element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](2)
|
||||
|
||||
result := optional.Set(35)(numbers)
|
||||
|
||||
assert.Equal(t, []int{10, 20, 35}, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for negative index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](-1)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for out of bounds index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for empty array", func(t *testing.T) {
|
||||
numbers := []int{}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for nil array", func(t *testing.T) {
|
||||
var numbers []int
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_OptionalLaw1_GetSetNoOp tests Optional Law 1: GetSet Law (No-op on None)
|
||||
// If GetOption(s) returns None, then Set(a)(s) must return s unchanged (no-op).
|
||||
func TestAt_OptionalLaw1_GetSetNoOp(t *testing.T) {
|
||||
t.Run("out of bounds index - set is no-op", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("negative index - set is no-op", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](-1)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("empty array - set is no-op", func(t *testing.T) {
|
||||
numbers := []int{}
|
||||
optional := At[int](0)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("nil array - set is no-op", func(t *testing.T) {
|
||||
var numbers []int
|
||||
optional := At[int](0)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_OptionalLaw2_SetGet tests Optional Law 2: SetGet Law (Get what you Set)
|
||||
// If GetOption(s) returns Some(_), then GetOption(Set(a)(s)) must return Some(a).
|
||||
func TestAt_OptionalLaw2_SetGet(t *testing.T) {
|
||||
t.Run("set then get returns the set value", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
// Verify GetOption returns Some (precondition)
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
// Set a new value
|
||||
newValue := 25
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
// GetOption on updated should return Some(newValue)
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
})
|
||||
|
||||
t.Run("set first element then get", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
newValue := 5
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
})
|
||||
|
||||
t.Run("set last element then get", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](2)
|
||||
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
newValue := 35
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
})
|
||||
|
||||
t.Run("multiple indices satisfy law", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40, 50}
|
||||
|
||||
for i := range 5 {
|
||||
optional := At[int](i)
|
||||
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
newValue := i * 100
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_OptionalLaw3_SetSet tests Optional Law 3: SetSet Law (Last Set Wins)
|
||||
// Setting twice is the same as setting once with the final value.
|
||||
// Formally: Set(b)(Set(a)(s)) = Set(b)(s)
|
||||
func TestAt_OptionalLaw3_SetSet(t *testing.T) {
|
||||
eqSlice := EQ.FromEquals(func(a, b []int) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range len(a) {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
t.Run("setting twice equals setting once with final value", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
// Set twice: first to 25, then to 99
|
||||
setTwice := F.Pipe2(
|
||||
numbers,
|
||||
optional.Set(25),
|
||||
optional.Set(99),
|
||||
)
|
||||
|
||||
// Set once with final value
|
||||
setOnce := optional.Set(99)(numbers)
|
||||
|
||||
assert.True(t, eqSlice.Equals(setTwice, setOnce))
|
||||
})
|
||||
|
||||
t.Run("multiple sets - last one wins", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
// Set multiple times
|
||||
result := F.Pipe4(
|
||||
numbers,
|
||||
optional.Set(1),
|
||||
optional.Set(2),
|
||||
optional.Set(3),
|
||||
optional.Set(4),
|
||||
)
|
||||
|
||||
// Should equal setting once with final value
|
||||
expected := optional.Set(4)(numbers)
|
||||
|
||||
assert.True(t, eqSlice.Equals(result, expected))
|
||||
})
|
||||
|
||||
t.Run("set twice on out of bounds - both no-ops", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
// Set twice on out of bounds
|
||||
setTwice := F.Pipe2(
|
||||
numbers,
|
||||
optional.Set(25),
|
||||
optional.Set(99),
|
||||
)
|
||||
|
||||
// Set once on out of bounds
|
||||
setOnce := optional.Set(99)(numbers)
|
||||
|
||||
// Both should be no-ops, returning original
|
||||
assert.True(t, eqSlice.Equals(setTwice, numbers))
|
||||
assert.True(t, eqSlice.Equals(setOnce, numbers))
|
||||
assert.True(t, eqSlice.Equals(setTwice, setOnce))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_EdgeCases tests edge cases and boundary conditions
|
||||
func TestAt_EdgeCases(t *testing.T) {
|
||||
t.Run("single element array", func(t *testing.T) {
|
||||
numbers := []int{42}
|
||||
optional := At[int](0)
|
||||
|
||||
// Get
|
||||
assert.Equal(t, O.Some(42), optional.GetOption(numbers))
|
||||
|
||||
// Set
|
||||
updated := optional.Set(99)(numbers)
|
||||
assert.Equal(t, []int{99}, updated)
|
||||
|
||||
// Out of bounds
|
||||
outOfBounds := At[int](1)
|
||||
assert.Equal(t, O.None[int](), outOfBounds.GetOption(numbers))
|
||||
assert.Equal(t, numbers, outOfBounds.Set(99)(numbers))
|
||||
})
|
||||
|
||||
t.Run("large array", func(t *testing.T) {
|
||||
numbers := make([]int, 1000)
|
||||
for i := range 1000 {
|
||||
numbers[i] = i
|
||||
}
|
||||
|
||||
optional := At[int](500)
|
||||
|
||||
// Get
|
||||
assert.Equal(t, O.Some(500), optional.GetOption(numbers))
|
||||
|
||||
// Set
|
||||
updated := optional.Set(9999)(numbers)
|
||||
assert.Equal(t, 9999, updated[500])
|
||||
assert.Equal(t, 500, numbers[500]) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// String array
|
||||
strings := []string{"a", "b", "c"}
|
||||
strOptional := At[string](1)
|
||||
assert.Equal(t, O.Some("b"), strOptional.GetOption(strings))
|
||||
assert.Equal(t, []string{"a", "x", "c"}, strOptional.Set("x")(strings))
|
||||
|
||||
// Bool array
|
||||
bools := []bool{true, false, true}
|
||||
boolOptional := At[bool](1)
|
||||
assert.Equal(t, O.Some(false), boolOptional.GetOption(bools))
|
||||
assert.Equal(t, []bool{true, true, true}, boolOptional.Set(true)(bools))
|
||||
})
|
||||
|
||||
t.Run("preserves array capacity", func(t *testing.T) {
|
||||
numbers := make([]int, 3, 10)
|
||||
numbers[0], numbers[1], numbers[2] = 10, 20, 30
|
||||
|
||||
optional := At[int](1)
|
||||
updated := optional.Set(25)(numbers)
|
||||
|
||||
assert.Equal(t, []int{10, 25, 30}, updated)
|
||||
assert.Equal(t, 3, len(updated))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_Integration tests integration scenarios
|
||||
func TestAt_Integration(t *testing.T) {
|
||||
t.Run("multiple optionals on same array", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
|
||||
first := At[int](0)
|
||||
second := At[int](1)
|
||||
third := At[int](2)
|
||||
|
||||
// Update multiple indices
|
||||
result := F.Pipe3(
|
||||
numbers,
|
||||
first.Set(1),
|
||||
second.Set(2),
|
||||
third.Set(3),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 40}, result)
|
||||
assert.Equal(t, []int{10, 20, 30, 40}, numbers) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("chaining operations", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](1)
|
||||
|
||||
// Get, verify, set, get again
|
||||
original := optional.GetOption(numbers)
|
||||
assert.Equal(t, O.Some(20), original)
|
||||
|
||||
updated := optional.Set(25)(numbers)
|
||||
newValue := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(25), newValue)
|
||||
|
||||
// Original still unchanged
|
||||
assert.Equal(t, O.Some(20), optional.GetOption(numbers))
|
||||
})
|
||||
|
||||
t.Run("conditional update based on current value", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](1)
|
||||
|
||||
// Get current value and conditionally update
|
||||
result := F.Pipe1(
|
||||
optional.GetOption(numbers),
|
||||
O.Fold(
|
||||
func() []int { return numbers },
|
||||
func(current int) []int {
|
||||
if current > 15 {
|
||||
return optional.Set(current * 2)(numbers)
|
||||
}
|
||||
return numbers
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{10, 40, 30}, result)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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 generic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
AR "github.com/IBM/fp-go/v2/array/generic"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// At creates an Optional that focuses on the element at a specific index in an array.
|
||||
//
|
||||
// This function returns an Optional that can get and set the element at the given index.
|
||||
// If the index is out of bounds, GetOption returns None and Set operations are no-ops
|
||||
// (the array is returned unchanged). This follows the Optional laws where operations
|
||||
// on non-existent values have no effect.
|
||||
//
|
||||
// The Optional provides safe array access without panicking on invalid indices, making
|
||||
// it ideal for functional transformations where you want to modify array elements only
|
||||
// when they exist.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - idx: The zero-based index to focus on
|
||||
//
|
||||
// Returns:
|
||||
// - An Optional that focuses on the element at the specified index
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
// OA "github.com/IBM/fp-go/v2/optics/optional/array"
|
||||
// )
|
||||
//
|
||||
// numbers := []int{10, 20, 30, 40}
|
||||
//
|
||||
// // Create an optional focusing on index 1
|
||||
// second := OA.At[int](1)
|
||||
//
|
||||
// // Get the element at index 1
|
||||
// value := second.GetOption(numbers)
|
||||
// // value: option.Some(20)
|
||||
//
|
||||
// // Set the element at index 1
|
||||
// updated := second.Set(25)(numbers)
|
||||
// // updated: []int{10, 25, 30, 40}
|
||||
//
|
||||
// // Out of bounds access returns None
|
||||
// outOfBounds := OA.At[int](10)
|
||||
// value = outOfBounds.GetOption(numbers)
|
||||
// // value: option.None[int]()
|
||||
//
|
||||
// // Out of bounds set is a no-op
|
||||
// unchanged := outOfBounds.Set(99)(numbers)
|
||||
// // unchanged: []int{10, 20, 30, 40} (original array)
|
||||
//
|
||||
// See Also:
|
||||
// - AR.Lookup: Gets an element at an index, returning an Option
|
||||
// - AR.UpdateAt: Updates an element at an index, returning an Option
|
||||
// - OP.Optional: The Optional optic type
|
||||
func At[GA ~[]A, A any](idx int) OP.Optional[GA, A] {
|
||||
lookup := AR.Lookup[GA](idx)
|
||||
return OP.MakeOptionalCurriedWithName(
|
||||
lookup,
|
||||
func(a A) func(GA) GA {
|
||||
update := AR.UpdateAt[GA](idx, a)
|
||||
return func(as GA) GA {
|
||||
return F.Pipe2(
|
||||
as,
|
||||
update,
|
||||
O.GetOrElse(lazy.Of(as)),
|
||||
)
|
||||
}
|
||||
},
|
||||
fmt.Sprintf("At[%d]", idx),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package optional
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fof pointed.OfType[S, HKTS],
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Optional[S, A]) R {
|
||||
return func(sa Optional[S, A]) R {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.GetOption,
|
||||
O.Fold(
|
||||
lazy.Of(fof(s)),
|
||||
F.Flow2(
|
||||
f,
|
||||
fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,8 +310,10 @@ func TestAsTraversal(t *testing.T) {
|
||||
return Identity[Option[int]]{Value: s}
|
||||
}
|
||||
|
||||
fmap := func(ia Identity[int], f func(int) Option[int]) Identity[Option[int]] {
|
||||
return Identity[Option[int]]{Value: f(ia.Value)}
|
||||
fmap := func(f func(int) Option[int]) func(Identity[int]) Identity[Option[int]] {
|
||||
return func(ia Identity[int]) Identity[Option[int]] {
|
||||
return Identity[Option[int]]{Value: f(ia.Value)}
|
||||
}
|
||||
}
|
||||
|
||||
type TraversalFunc func(func(int) Identity[int]) func(Option[int]) Identity[Option[int]]
|
||||
|
||||
@@ -17,6 +17,9 @@ package prism
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
@@ -58,24 +61,23 @@ import (
|
||||
// higher-kinded types and applicative functors. Most users will work
|
||||
// directly with prisms rather than converting them to traversals.
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fof func(S) HKTS,
|
||||
fmap func(HKTA, func(A) S) HKTS,
|
||||
fof pointed.OfType[S, HKTS],
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Prism[S, A]) R {
|
||||
return func(sa Prism[S, A]) R {
|
||||
return func(f func(a A) HKTA) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.GetOption,
|
||||
O.Fold(
|
||||
// If prism doesn't match, return the original value lifted into HKTS
|
||||
F.Nullary2(F.Constant(s), fof),
|
||||
// If prism matches, apply f to the extracted value and map back
|
||||
func(a A) HKTS {
|
||||
return fmap(f(a), func(a A) S {
|
||||
return prismModify(F.Constant1[A](a), sa, s)
|
||||
})
|
||||
},
|
||||
lazy.Of(fof(s)),
|
||||
F.Flow2(
|
||||
f,
|
||||
fmap(func(a A) S {
|
||||
return Set[S](a)(sa)(s)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ import (
|
||||
)
|
||||
|
||||
// FromArray returns a traversal from an array for the identity [Monoid]
|
||||
func FromArray[E, A any](m M.Monoid[E]) G.Traversal[[]A, A, C.Const[E, []A], C.Const[E, A]] {
|
||||
func FromArray[A, E any](m M.Monoid[E]) G.Traversal[[]A, A, C.Const[E, []A], C.Const[E, A]] {
|
||||
return AR.FromArray[[]A](m)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,51 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
// FromArray returns a traversal from an array for the identity monad
|
||||
// FromArray creates a traversal for array elements using the Identity functor.
|
||||
//
|
||||
// This is a specialized version of the generic FromArray that uses the Identity
|
||||
// functor, which provides the simplest possible computational context (no context).
|
||||
// This makes it ideal for straightforward array transformations where you want to
|
||||
// modify elements directly without additional effects.
|
||||
//
|
||||
// The Identity functor means that operations are applied directly to values without
|
||||
// wrapping them in any additional structure. This results in clean, efficient
|
||||
// traversals that simply map functions over array elements.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Array type constraint (e.g., []A)
|
||||
// - A: The element type within the array
|
||||
//
|
||||
// Returns:
|
||||
// - A Traversal that can transform all elements in an array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
// TI "github.com/IBM/fp-go/v2/optics/traversal/array/generic/identity"
|
||||
// )
|
||||
//
|
||||
// // Create a traversal for integer arrays
|
||||
// arrayTraversal := TI.FromArray[[]int, int]()
|
||||
//
|
||||
// // Compose with identity traversal
|
||||
// traversal := F.Pipe1(
|
||||
// T.Id[[]int, []int](),
|
||||
// T.Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// // Double all numbers in the array
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// doubled := traversal(func(n int) int { return n * 2 })(numbers)
|
||||
// // doubled: []int{2, 4, 6, 8, 10}
|
||||
//
|
||||
// See Also:
|
||||
// - AR.FromArray: Generic version with configurable functor
|
||||
// - I.Of: Identity functor's pure/of operation
|
||||
// - I.Map: Identity functor's map operation
|
||||
// - I.Ap: Identity functor's applicative operation
|
||||
func FromArray[GA ~[]A, A any]() G.Traversal[GA, A, GA, A] {
|
||||
return AR.FromArray[GA](
|
||||
I.Of[GA],
|
||||
@@ -29,3 +73,75 @@ func FromArray[GA ~[]A, A any]() G.Traversal[GA, A, GA, A] {
|
||||
I.Ap[GA, A],
|
||||
)
|
||||
}
|
||||
|
||||
// At creates a function that focuses a traversal on a specific array index using the Identity functor.
|
||||
//
|
||||
// This is a specialized version of the generic At that uses the Identity functor,
|
||||
// providing the simplest computational context for array element access. It transforms
|
||||
// a traversal focusing on an array into a traversal focusing on the element at the
|
||||
// specified index.
|
||||
//
|
||||
// The Identity functor means operations are applied directly without additional wrapping,
|
||||
// making this ideal for straightforward element modifications. If the index is out of
|
||||
// bounds, the traversal focuses on zero elements (no-op).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Array type constraint (e.g., []A)
|
||||
// - S: The source type of the outer traversal
|
||||
// - A: The element type within the array
|
||||
//
|
||||
// Parameters:
|
||||
// - idx: The zero-based index to focus on
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a traversal on arrays into a traversal on a specific element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
// TI "github.com/IBM/fp-go/v2/optics/traversal/array/generic/identity"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Hobbies []string
|
||||
// }
|
||||
//
|
||||
// // Create a traversal focusing on hobbies
|
||||
// hobbiesTraversal := T.Id[Person, []string]()
|
||||
//
|
||||
// // Focus on the second hobby (index 1)
|
||||
// secondHobby := F.Pipe1(
|
||||
// hobbiesTraversal,
|
||||
// TI.At[[]string, Person, string](1),
|
||||
// )
|
||||
//
|
||||
// // Modify the second hobby
|
||||
// person := Person{Name: "Alice", Hobbies: []string{"reading", "coding", "gaming"}}
|
||||
// updated := secondHobby(func(s string) string {
|
||||
// return s + "!"
|
||||
// })(person)
|
||||
// // updated.Hobbies: []string{"reading", "coding!", "gaming"}
|
||||
//
|
||||
// // Out of bounds index is a no-op
|
||||
// outOfBounds := F.Pipe1(
|
||||
// hobbiesTraversal,
|
||||
// TI.At[[]string, Person, string](10),
|
||||
// )
|
||||
// unchanged := outOfBounds(func(s string) string {
|
||||
// return s + "!"
|
||||
// })(person)
|
||||
// // unchanged.Hobbies: []string{"reading", "coding", "gaming"} (no change)
|
||||
//
|
||||
// See Also:
|
||||
// - AR.At: Generic version with configurable functor
|
||||
// - I.Of: Identity functor's pure/of operation
|
||||
// - I.Map: Identity functor's map operation
|
||||
func At[GA ~[]A, S, A any](idx int) func(G.Traversal[S, GA, S, GA]) G.Traversal[S, A, S, A] {
|
||||
return AR.At[GA, S, A, S](
|
||||
I.Of[GA],
|
||||
I.Map[A, GA],
|
||||
)(idx)
|
||||
}
|
||||
|
||||
@@ -16,19 +16,105 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
AR "github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
OA "github.com/IBM/fp-go/v2/optics/optional/array/generic"
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
// FromArray returns a traversal from an array
|
||||
func FromArray[GA ~[]A, GB ~[]B, 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,
|
||||
fof pointed.OfType[GB, HKTRB],
|
||||
fmap functor.MapType[GB, func(B) GB, HKTRB, HKTAB],
|
||||
fap apply.ApType[HKTB, HKTRB, HKTAB],
|
||||
) G.Traversal[GA, A, HKTRB, HKTB] {
|
||||
return func(f func(A) HKTB) func(s GA) HKTRB {
|
||||
return func(s GA) HKTRB {
|
||||
return AR.MonadTraverse(fof, fmap, fap, s, f)
|
||||
}
|
||||
return func(f func(A) HKTB) func(GA) HKTRB {
|
||||
return AR.Traverse[GA](fof, fmap, fap, f)
|
||||
}
|
||||
}
|
||||
|
||||
// At creates a function that focuses a traversal on a specific array index.
|
||||
//
|
||||
// This function takes an index and returns a function that transforms a traversal
|
||||
// focusing on an array into a traversal focusing on the element at that index.
|
||||
// It works by:
|
||||
// 1. Creating an Optional that focuses on the array element at the given index
|
||||
// 2. Converting that Optional into a Traversal
|
||||
// 3. Composing it with the original traversal
|
||||
//
|
||||
// If the index is out of bounds, the traversal will focus on zero elements (no-op),
|
||||
// following the Optional laws where operations on non-existent values have no effect.
|
||||
//
|
||||
// This is particularly useful when you have a nested structure containing arrays
|
||||
// and want to traverse to a specific element within those arrays.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Array type constraint (e.g., []A)
|
||||
// - S: The source type of the outer traversal
|
||||
// - A: The element type within the array
|
||||
// - HKTS: Higher-kinded type for S (functor/applicative context)
|
||||
// - HKTGA: Higher-kinded type for GA (functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift GA into the higher-kinded type HKTGA (pure/of operation)
|
||||
// - fmap: Function to map over HKTA and produce HKTGA (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an index and returns a traversal transformer
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/identity"
|
||||
// T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
// TA "github.com/IBM/fp-go/v2/optics/traversal/array/generic"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Hobbies []string
|
||||
// }
|
||||
//
|
||||
// // Create a traversal focusing on the hobbies array
|
||||
// hobbiesTraversal := T.Id[Person, []string]()
|
||||
//
|
||||
// // Focus on the first hobby (index 0)
|
||||
// firstHobby := F.Pipe1(
|
||||
// hobbiesTraversal,
|
||||
// TA.At[[]string, Person, string](
|
||||
// identity.Of[[]string],
|
||||
// identity.Map[string, []string],
|
||||
// )(0),
|
||||
// )
|
||||
//
|
||||
// // Modify the first hobby
|
||||
// person := Person{Name: "Alice", Hobbies: []string{"reading", "coding"}}
|
||||
// updated := firstHobby(func(s string) string {
|
||||
// return s + "!"
|
||||
// })(person)
|
||||
// // updated.Hobbies: []string{"reading!", "coding"}
|
||||
//
|
||||
// See Also:
|
||||
// - OA.At: Creates an Optional focusing on an array element
|
||||
// - optional.AsTraversal: Converts an Optional to a Traversal
|
||||
// - G.Compose: Composes two traversals
|
||||
func At[GA ~[]A, S, A, HKTS, HKTGA, HKTA any](
|
||||
fof pointed.OfType[GA, HKTGA],
|
||||
fmap functor.MapType[A, GA, HKTA, HKTGA],
|
||||
) func(int) func(G.Traversal[S, GA, HKTS, HKTGA]) G.Traversal[S, A, HKTS, HKTA] {
|
||||
return F.Flow3(
|
||||
OA.At[GA],
|
||||
optional.AsTraversal[G.Traversal[GA, A, HKTGA, HKTA]](fof, fmap),
|
||||
G.Compose[
|
||||
G.Traversal[GA, A, HKTGA, HKTA],
|
||||
G.Traversal[S, GA, HKTS, HKTGA],
|
||||
G.Traversal[S, A, HKTS, HKTA],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@ package generic
|
||||
import (
|
||||
AR "github.com/IBM/fp-go/v2/array/generic"
|
||||
C "github.com/IBM/fp-go/v2/constant"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -47,7 +52,7 @@ func FromTraversable[
|
||||
}
|
||||
|
||||
// FoldMap maps each target to a `Monoid` and combines the result
|
||||
func FoldMap[M, S, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
func FoldMap[S, M, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return F.Flow2(
|
||||
F.Pipe1(
|
||||
@@ -61,13 +66,84 @@ func FoldMap[M, S, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.
|
||||
|
||||
// Fold maps each target to a `Monoid` and combines the result
|
||||
func Fold[S, A any](sa Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
|
||||
return FoldMap[A, S](F.Identity[A])(sa)
|
||||
return FoldMap[S](F.Identity[A])(sa)
|
||||
}
|
||||
|
||||
// GetAll gets all the targets of a traversal
|
||||
func GetAll[GA ~[]A, S, A any](s S) func(sa Traversal[S, A, C.Const[GA, S], C.Const[GA, A]]) GA {
|
||||
fmap := FoldMap[GA, S](AR.Of[GA, A])
|
||||
fmap := FoldMap[S](AR.Of[GA, A])
|
||||
return func(sa Traversal[S, A, C.Const[GA, S], C.Const[GA, A]]) GA {
|
||||
return fmap(sa)(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter creates a function that filters the targets of a traversal based on a predicate.
|
||||
//
|
||||
// This function allows you to refine a traversal to only focus on values that satisfy
|
||||
// a given predicate. It works by converting the predicate into a prism, then converting
|
||||
// that prism into a traversal, and finally composing it with the original traversal.
|
||||
//
|
||||
// The filtering is selective: when modifying values through the filtered traversal,
|
||||
// only values that satisfy the predicate will be transformed. Values that don't
|
||||
// satisfy the predicate remain unchanged.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The focus type (the values being filtered)
|
||||
// - HKTS: Higher-kinded type for S (functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift A into the higher-kinded type HKTA (pure/of operation)
|
||||
// - fmap: Function to map over HKTA (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a predicate and returns an endomorphism on traversals
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/identity"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// // Create a traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[int]()
|
||||
// baseTraversal := F.Pipe1(
|
||||
// Id[[]int, []int](),
|
||||
// Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// // Filter to only positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// filteredTraversal := F.Pipe1(
|
||||
// baseTraversal,
|
||||
// Filter[[]int, int](identity.Of[int], identity.Map[int, int])(isPositive),
|
||||
// )
|
||||
//
|
||||
// // Double only positive numbers
|
||||
// numbers := []int{-2, -1, 0, 1, 2, 3}
|
||||
// result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
|
||||
// // result: [-2, -1, 0, 2, 4, 6]
|
||||
//
|
||||
// See Also:
|
||||
// - prism.FromPredicate: Creates a prism from a predicate
|
||||
// - prism.AsTraversal: Converts a prism to a traversal
|
||||
// - Compose: Composes two traversals
|
||||
func Filter[
|
||||
S, HKTS, A, HKTA any](
|
||||
fof pointed.OfType[A, HKTA],
|
||||
fmap functor.MapType[A, A, HKTA, HKTA],
|
||||
) func(predicate.Predicate[A]) endomorphism.Endomorphism[Traversal[S, A, HKTS, HKTA]] {
|
||||
return F.Flow3(
|
||||
prism.FromPredicate,
|
||||
prism.AsTraversal[Traversal[A, A, HKTA, HKTA]](fof, fmap),
|
||||
Compose[
|
||||
Traversal[A, A, HKTA, HKTA],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, A, HKTS, HKTA]],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,46 +18,110 @@ package traversal
|
||||
import (
|
||||
C "github.com/IBM/fp-go/v2/constant"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/identity"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
// Id is the identity constructor of a traversal
|
||||
func Id[S, A any]() G.Traversal[S, S, A, A] {
|
||||
func Id[S, A any]() Traversal[S, S, A, A] {
|
||||
return F.Identity[func(S) A]
|
||||
}
|
||||
|
||||
// Modify applies a transformation function to a traversal
|
||||
func Modify[S, A any](f func(A) A) func(sa G.Traversal[S, A, S, A]) func(S) S {
|
||||
return func(sa G.Traversal[S, A, S, A]) func(S) S {
|
||||
return sa(f)
|
||||
}
|
||||
func Modify[S, A any](f Endomorphism[A]) func(Traversal[S, A, S, A]) Endomorphism[S] {
|
||||
return identity.Flap[Endomorphism[S]](f)
|
||||
}
|
||||
|
||||
// Set sets a constant value for all values of the traversal
|
||||
func Set[S, A any](a A) func(sa G.Traversal[S, A, S, A]) func(S) S {
|
||||
func Set[S, A any](a A) func(Traversal[S, A, S, A]) Endomorphism[S] {
|
||||
return Modify[S](F.Constant1[A](a))
|
||||
}
|
||||
|
||||
// FoldMap maps each target to a `Monoid` and combines the result
|
||||
func FoldMap[M, S, A any](f func(A) M) func(sa G.Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return G.FoldMap[M, S](f)
|
||||
func FoldMap[S, M, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return G.FoldMap[S](f)
|
||||
}
|
||||
|
||||
// Fold maps each target to a `Monoid` and combines the result
|
||||
func Fold[S, A any](sa G.Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
|
||||
func Fold[S, A any](sa Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
|
||||
return G.Fold(sa)
|
||||
}
|
||||
|
||||
// GetAll gets all the targets of a traversal
|
||||
func GetAll[S, A any](s S) func(sa G.Traversal[S, A, C.Const[[]A, S], C.Const[[]A, A]]) []A {
|
||||
func GetAll[A, S any](s S) func(sa Traversal[S, A, C.Const[[]A, S], C.Const[[]A, A]]) []A {
|
||||
return G.GetAll[[]A](s)
|
||||
}
|
||||
|
||||
// Compose composes two traversables
|
||||
func Compose[
|
||||
S, A, B, HKTS, HKTA, HKTB any](ab G.Traversal[A, B, HKTA, HKTB]) func(sa G.Traversal[S, A, HKTS, HKTA]) G.Traversal[S, B, HKTS, HKTB] {
|
||||
S, HKTS, A, B, HKTA, HKTB any](ab Traversal[A, B, HKTA, HKTB]) func(Traversal[S, A, HKTS, HKTA]) Traversal[S, B, HKTS, HKTB] {
|
||||
return G.Compose[
|
||||
G.Traversal[A, B, HKTA, HKTB],
|
||||
G.Traversal[S, A, HKTS, HKTA],
|
||||
G.Traversal[S, B, HKTS, HKTB]](ab)
|
||||
Traversal[A, B, HKTA, HKTB],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, B, HKTS, HKTB]](ab)
|
||||
}
|
||||
|
||||
// Filter creates a function that filters the targets of a traversal based on a predicate.
|
||||
//
|
||||
// This function allows you to refine a traversal to only focus on values that satisfy
|
||||
// a given predicate. It works by converting the predicate into a prism, then converting
|
||||
// that prism into a traversal, and finally composing it with the original traversal.
|
||||
//
|
||||
// The filtering is selective: when modifying values through the filtered traversal,
|
||||
// only values that satisfy the predicate will be transformed. Values that don't
|
||||
// satisfy the predicate remain unchanged.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The focus type (the values being filtered)
|
||||
// - HKTS: Higher-kinded type for S (functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift A into the higher-kinded type HKTA (pure/of operation)
|
||||
// - fmap: Function to map over HKTA (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a predicate and returns an endomorphism on traversals
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/identity"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// // Create a traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[int]()
|
||||
// baseTraversal := F.Pipe1(
|
||||
// Id[[]int, []int](),
|
||||
// Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// // Filter to only positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// filteredTraversal := F.Pipe1(
|
||||
// baseTraversal,
|
||||
// Filter[[]int, int](identity.Of[int], identity.Map[int, int])(isPositive),
|
||||
// )
|
||||
//
|
||||
// // Double only positive numbers
|
||||
// numbers := []int{-2, -1, 0, 1, 2, 3}
|
||||
// result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
|
||||
// // result: [-2, -1, 0, 2, 4, 6]
|
||||
//
|
||||
// See Also:
|
||||
// - prism.FromPredicate: Creates a prism from a predicate
|
||||
// - prism.AsTraversal: Converts a prism to a traversal
|
||||
// - Compose: Composes two traversals
|
||||
func Filter[S, HKTS, A, HKTA any](
|
||||
fof pointed.OfType[A, HKTA],
|
||||
fmap functor.MapType[A, A, HKTA, HKTA],
|
||||
) func(Predicate[A]) Endomorphism[Traversal[S, A, HKTS, HKTA]] {
|
||||
return G.Filter[S, HKTS](fof, fmap)
|
||||
}
|
||||
|
||||
@@ -32,14 +32,14 @@ func TestGetAll(t *testing.T) {
|
||||
|
||||
as := AR.From(1, 2, 3)
|
||||
|
||||
tr := AT.FromArray[[]int, int](AR.Monoid[int]())
|
||||
tr := AT.FromArray[int](AR.Monoid[int]())
|
||||
|
||||
sa := F.Pipe1(
|
||||
Id[[]int, C.Const[[]int, []int]](),
|
||||
Compose[[]int, []int, int, C.Const[[]int, []int]](tr),
|
||||
Compose[[]int, C.Const[[]int, []int], []int, int](tr),
|
||||
)
|
||||
|
||||
getall := GetAll[[]int, int](as)(sa)
|
||||
getall := GetAll[int](as)(sa)
|
||||
|
||||
assert.Equal(t, AR.From(1, 2, 3), getall)
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func TestFold(t *testing.T) {
|
||||
|
||||
sa := F.Pipe1(
|
||||
Id[[]int, C.Const[int, []int]](),
|
||||
Compose[[]int, []int, int, C.Const[int, []int]](tr),
|
||||
Compose[[]int, C.Const[int, []int], []int, int](tr),
|
||||
)
|
||||
|
||||
folded := Fold(sa)(as)
|
||||
@@ -70,10 +70,245 @@ func TestTraverse(t *testing.T) {
|
||||
|
||||
sa := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, int, []int](tr),
|
||||
Compose[[]int, []int](tr),
|
||||
)
|
||||
|
||||
res := sa(utils.Double)(as)
|
||||
|
||||
assert.Equal(t, AR.From(2, 4, 6), res)
|
||||
}
|
||||
|
||||
func TestFilter_Success(t *testing.T) {
|
||||
t.Run("filters and modifies only matching elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{-2, -1, 0, 1, 2, 3}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter to only positive numbers
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act - double only positive numbers
|
||||
result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{-2, -1, 0, 2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("filters even numbers and triples them", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5, 6}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter to only even numbers
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isEven),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(func(n int) int { return n * 3 })(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{1, 6, 3, 12, 5, 18}, result)
|
||||
})
|
||||
|
||||
t.Run("filters strings by length", func(t *testing.T) {
|
||||
// Arrange
|
||||
words := []string{"a", "ab", "abc", "abcd", "abcde"}
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]string, []string](),
|
||||
Compose[[]string, []string, []string, string](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter strings with length > 2
|
||||
longerThanTwo := func(s string) bool { return len(s) > 2 }
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]string, []string, string, string](F.Identity[string], F.Identity[func(string) string])(longerThanTwo),
|
||||
)
|
||||
|
||||
// Act - convert to uppercase
|
||||
result := filteredTraversal(func(s string) string {
|
||||
return s + "!"
|
||||
})(words)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []string{"a", "ab", "abc!", "abcd!", "abcde!"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_EdgeCases(t *testing.T) {
|
||||
t.Run("empty array returns empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("no elements match predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{-5, -4, -3, -2, -1}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert - all elements unchanged
|
||||
assert.Equal(t, []int{-5, -4, -3, -2, -1}, result)
|
||||
})
|
||||
|
||||
t.Run("all elements match predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert - all elements doubled
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("single element matching", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{42}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{84}, result)
|
||||
})
|
||||
|
||||
t.Run("single element not matching", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{-42}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{-42}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_Integration(t *testing.T) {
|
||||
t.Run("multiple filters composed", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter to only even numbers, then only those > 4
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
greaterThanFour := N.MoreThan(4)
|
||||
|
||||
filteredTraversal := F.Pipe2(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isEven),
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(greaterThanFour),
|
||||
)
|
||||
|
||||
// Act - add 100 to matching elements
|
||||
result := filteredTraversal(func(n int) int { return n + 100 })(numbers)
|
||||
|
||||
// Assert - only 6, 8, 10 should be modified
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 106, 7, 108, 9, 110}, result)
|
||||
})
|
||||
|
||||
t.Run("filter with identity transformation", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int](arrayTraversal),
|
||||
)
|
||||
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int](F.Identity[int], F.Identity[func(int) int])(isEven),
|
||||
)
|
||||
|
||||
// Act - identity transformation
|
||||
result := filteredTraversal(F.Identity[int])(numbers)
|
||||
|
||||
// Assert - array unchanged
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package traversal
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
type (
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = G.Traversal[S, A, HKTS, HKTA]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
@@ -16,6 +16,7 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"sort"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
@@ -301,13 +302,8 @@ func unionLast[M ~map[K]V, K comparable, V any](left, right M) M {
|
||||
|
||||
result := make(M, lenLeft+lenRight)
|
||||
|
||||
for k, v := range left {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
for k, v := range right {
|
||||
result[k] = v
|
||||
}
|
||||
maps.Copy(result, left)
|
||||
maps.Copy(result, right)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+238
-8
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user