mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-14 13:42:48 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb27ecdc01 | ||
|
|
e5eb7d343c | ||
|
|
d5a3217251 | ||
|
|
c5cbdaad68 | ||
|
|
5d0f27ad10 | ||
|
|
3a954e0d1f | ||
|
|
cb2e0b23e8 |
108
v2/AGENTS.md
108
v2/AGENTS.md
@@ -2,6 +2,20 @@
|
||||
|
||||
This document provides guidelines for AI agents working on the fp-go/v2 project.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Documentation Standards](#documentation-standards)
|
||||
- [Go Doc Comments](#go-doc-comments)
|
||||
- [File Headers](#file-headers)
|
||||
- [Testing Standards](#testing-standards)
|
||||
- [Test Structure](#test-structure)
|
||||
- [Test Coverage](#test-coverage)
|
||||
- [Example Test Pattern](#example-test-pattern)
|
||||
- [Code Style](#code-style)
|
||||
- [Functional Patterns](#functional-patterns)
|
||||
- [Error Handling](#error-handling)
|
||||
- [Checklist for New Code](#checklist-for-new-code)
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
### Go Doc Comments
|
||||
@@ -102,6 +116,50 @@ Always include the Apache 2.0 license header:
|
||||
- Use `result.Of` for success values
|
||||
- Use `result.Left` for error values
|
||||
|
||||
4. **Folding Either/Result Values in Tests**
|
||||
- Use `F.Pipe1(result, Fold(onLeft, onRight))` — avoid the `_ = Fold(...)(result)` discard pattern
|
||||
- Use `slices.Collect[T]` instead of a manual `for n := range seq { collected = append(...) }` loop
|
||||
- Use `t.Fatal` in the unexpected branch to combine the `IsLeft`/`IsRight` check with value extraction:
|
||||
```go
|
||||
// Good: single fold combines assertion and extraction
|
||||
collected := F.Pipe1(result, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
|
||||
// Avoid: separate IsRight check + manual loop
|
||||
assert.True(t, IsRight(result))
|
||||
var collected []int
|
||||
_ = MonadFold(result,
|
||||
func(e error) []int { return nil },
|
||||
func(seq iter.Seq[int]) []int {
|
||||
for n := range seq { collected = append(collected, n) }
|
||||
return collected
|
||||
},
|
||||
)
|
||||
```
|
||||
- Use `F.Identity[error]` as the Left branch when extracting an error value:
|
||||
```go
|
||||
err := F.Pipe1(result, Fold(
|
||||
F.Identity[error],
|
||||
func(_ iter.Seq[int]) error { t.Fatal("expected Left but got Right"); return nil },
|
||||
))
|
||||
```
|
||||
- Extract repeated fold patterns as local helper closures within the test function:
|
||||
```go
|
||||
collectInts := func(r Result[iter.Seq[int]]) []int {
|
||||
return F.Pipe1(r, Fold(
|
||||
func(e error) []int { t.Fatal(e); return nil },
|
||||
slices.Collect[int],
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
5. **Other Test Style Details**
|
||||
- Use `for i := range 10` instead of `for i := 0; i < 10; i++`
|
||||
- Chain curried calls directly: `TraverseSeq(parse)(input)` — no need for an intermediate `traverseFn` variable
|
||||
- Use direct slice literals (`[]string{"a", "b"}`) rather than `A.From("a", "b")` in tests
|
||||
|
||||
### Test Coverage
|
||||
|
||||
Include tests for:
|
||||
@@ -168,56 +226,6 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
- Check error context is preserved
|
||||
- Test error accumulation when applicable
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Converting Error-Based Functions
|
||||
|
||||
```go
|
||||
// Good: Use Eitherize1
|
||||
parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
|
||||
// Avoid: Manual error handling
|
||||
parseIntRR := func(input string) result.Result[int] {
|
||||
val, err := strconv.Atoi(input)
|
||||
if err != nil {
|
||||
return result.Left[int](err)
|
||||
}
|
||||
return result.Of(val)
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Validation Results
|
||||
|
||||
```go
|
||||
// Good: Direct comparison
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Avoid: Verbose extraction (unless you need to verify specific fields)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
```
|
||||
|
||||
### Documentation Examples
|
||||
|
||||
```go
|
||||
// Good: Concise and idiomatic
|
||||
// parseIntRR := result.Eitherize1(strconv.Atoi)
|
||||
// validator := FromReaderResult[string, int](parseIntRR)
|
||||
|
||||
// Avoid: Verbose manual patterns
|
||||
// parseIntRR := func(input string) result.Result[int] {
|
||||
// val, err := strconv.Atoi(input)
|
||||
// if err != nil {
|
||||
// return result.Left[int](err)
|
||||
// }
|
||||
// return result.Of(val)
|
||||
// }
|
||||
```
|
||||
|
||||
## Checklist for New Code
|
||||
|
||||
- [ ] Apache 2.0 license header included
|
||||
|
||||
@@ -529,71 +529,116 @@ func Push[A any](a A) Operator[A, A] {
|
||||
return G.Push[Operator[A, A]](a)
|
||||
}
|
||||
|
||||
// Concat concatenates two arrays, appending the provided array to the end of the input array.
|
||||
// This is a curried function that takes an array to append and returns a function that
|
||||
// takes the base array and returns the concatenated result.
|
||||
// Concat concatenates two arrays by appending a suffix array to a base array.
|
||||
//
|
||||
// The function creates a new array containing all elements from the base array followed
|
||||
// by all elements from the appended array. Neither input array is modified.
|
||||
// This is a curried function that takes a suffix array and returns a function
|
||||
// that takes a base array and produces a new array with the suffix appended.
|
||||
// It follows the "data last" pattern, where the data to be operated on (base array)
|
||||
// is provided last, making it ideal for use in functional pipelines.
|
||||
//
|
||||
// Semantic: Concat(suffix)(base) produces [base... suffix...]
|
||||
//
|
||||
// The function creates a new array containing all elements from the base array
|
||||
// followed by all elements from the suffix array. Neither input array is modified.
|
||||
//
|
||||
// Type Parameters:
|
||||
//
|
||||
// - A: The type of elements in the arrays
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The array to append to the end of the base array
|
||||
//
|
||||
// - suffix: The array to append to the end of the base array
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a base array and returns a new array with `as` appended to its end
|
||||
//
|
||||
// - A function that takes a base array and returns [base... suffix...]
|
||||
//
|
||||
// Behavior:
|
||||
// - Creates a new array with length equal to the sum of both input arrays
|
||||
// - Copies all elements from the base array first
|
||||
// - Appends all elements from the `as` array at the end
|
||||
// - Returns the base array unchanged if `as` is empty
|
||||
// - Returns `as` unchanged if the base array is empty
|
||||
// - Does not modify either input array
|
||||
//
|
||||
// Example:
|
||||
// - Creates a new array with length equal to len(base) + len(suffix)
|
||||
// - Copies all elements from the base array first
|
||||
// - Appends all elements from the suffix array at the end
|
||||
// - Returns the base array unchanged if suffix is empty
|
||||
// - Returns suffix unchanged if the base array is empty
|
||||
// - Does not modify either input array
|
||||
// - Preserves element order within each array
|
||||
//
|
||||
// Example - Basic concatenation:
|
||||
//
|
||||
// base := []int{1, 2, 3}
|
||||
// toAppend := []int{4, 5, 6}
|
||||
// result := array.Concat(toAppend)(base)
|
||||
// suffix := []int{4, 5, 6}
|
||||
// concat := array.Concat(suffix)
|
||||
// result := concat(base)
|
||||
// // result: []int{1, 2, 3, 4, 5, 6}
|
||||
// // base: []int{1, 2, 3} (unchanged)
|
||||
// // toAppend: []int{4, 5, 6} (unchanged)
|
||||
// // suffix: []int{4, 5, 6} (unchanged)
|
||||
//
|
||||
// Example with empty arrays:
|
||||
// Example - Direct application:
|
||||
//
|
||||
// result := array.Concat([]int{4, 5, 6})([]int{1, 2, 3})
|
||||
// // result: []int{1, 2, 3, 4, 5, 6}
|
||||
// // Demonstrates: Concat(b)(a) = [a... b...]
|
||||
//
|
||||
// Example - Empty arrays:
|
||||
//
|
||||
// base := []int{1, 2, 3}
|
||||
// empty := []int{}
|
||||
// result := array.Concat(empty)(base)
|
||||
// // result: []int{1, 2, 3}
|
||||
//
|
||||
// Example with strings:
|
||||
// Example - Strings:
|
||||
//
|
||||
// words1 := []string{"hello", "world"}
|
||||
// words2 := []string{"foo", "bar"}
|
||||
// result := array.Concat(words2)(words1)
|
||||
// // result: []string{"hello", "world", "foo", "bar"}
|
||||
//
|
||||
// Example with functional composition:
|
||||
// Example - Functional composition:
|
||||
//
|
||||
// numbers := []int{1, 2, 3}
|
||||
// result := F.Pipe2(
|
||||
// numbers,
|
||||
// array.Map(N.Mul(2)),
|
||||
// array.Concat([]int{10, 20}),
|
||||
// array.Map(N.Mul(2)), // [2, 4, 6]
|
||||
// array.Concat([]int{10, 20}), // [2, 4, 6, 10, 20]
|
||||
// )
|
||||
// // result: []int{2, 4, 6, 10, 20}
|
||||
//
|
||||
// Example - Multiple concatenations:
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// []int{1},
|
||||
// array.Concat([]int{2, 3}), // [1, 2, 3]
|
||||
// array.Concat([]int{4, 5}), // [1, 2, 3, 4, 5]
|
||||
// )
|
||||
//
|
||||
// Example - Building arrays incrementally:
|
||||
//
|
||||
// header := []string{"Name", "Age"}
|
||||
// data := []string{"Alice", "30"}
|
||||
// footer := []string{"Total: 1"}
|
||||
// result := F.Pipe2(
|
||||
// header,
|
||||
// array.Concat(data),
|
||||
// array.Concat(footer),
|
||||
// )
|
||||
// // result: []string{"Name", "Age", "Alice", "30", "Total: 1"}
|
||||
//
|
||||
// Use cases:
|
||||
//
|
||||
// - Combining multiple arrays into one
|
||||
// - Building arrays incrementally
|
||||
// - Building arrays incrementally in pipelines
|
||||
// - Implementing array-based data structures (queues, buffers)
|
||||
// - Merging results from multiple operations
|
||||
// - Creating array pipelines with functional composition
|
||||
// - Creating array transformation pipelines
|
||||
// - Appending batches of elements
|
||||
//
|
||||
// Mathematical properties:
|
||||
//
|
||||
// - Associativity: Concat(c)(Concat(b)(a)) == Concat(Concat(c)(b))(a)
|
||||
// - Identity: Concat([])(a) == a and Concat(a)([]) == a
|
||||
// - Length: len(Concat(b)(a)) == len(a) + len(b)
|
||||
//
|
||||
// Performance:
|
||||
//
|
||||
// - Time complexity: O(n + m) where n and m are the lengths of the arrays
|
||||
// - Space complexity: O(n + m) for the new array
|
||||
// - Optimized to avoid allocation when one array is empty
|
||||
@@ -601,9 +646,15 @@ func Push[A any](a A) Operator[A, A] {
|
||||
// Note: This function is immutable - it creates a new array rather than modifying
|
||||
// the input arrays. For appending a single element, consider using Append or Push.
|
||||
//
|
||||
// See Also:
|
||||
//
|
||||
// - Append: For appending a single element
|
||||
// - Push: Curried version of Append
|
||||
// - Flatten: For flattening nested arrays
|
||||
//
|
||||
//go:inline
|
||||
func Concat[A any](as []A) Operator[A, A] {
|
||||
return F.Bind2nd(array.Concat[[]A, A], as)
|
||||
func Concat[A any](suffix []A) Operator[A, A] {
|
||||
return F.Bind2nd(array.Concat[[]A, A], suffix)
|
||||
}
|
||||
|
||||
// MonadFlap applies a value to an array of functions, producing an array of results.
|
||||
|
||||
@@ -767,6 +767,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 +889,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
|
||||
|
||||
@@ -52,7 +52,7 @@ import (
|
||||
//
|
||||
// - f: A Kleisli arrow (A => ReaderIOResult[Trampoline[A, B]]) that:
|
||||
// - Takes the current state A
|
||||
// - Returns a ReaderIOResult that depends on [context.Context]
|
||||
// - Returns a ReaderIOResult that depends on context.Context
|
||||
// - Can fail with error (Left in the outer Either)
|
||||
// - Produces Trampoline[A, B] to control recursion flow (Right in the outer Either)
|
||||
//
|
||||
@@ -60,13 +60,13 @@ import (
|
||||
//
|
||||
// A Kleisli arrow (A => ReaderIOResult[B]) that:
|
||||
// - Takes an initial state A
|
||||
// - Returns a ReaderIOResult that requires [context.Context]
|
||||
// - Returns a ReaderIOResult that requires context.Context
|
||||
// - Can fail with error or context cancellation
|
||||
// - Produces the final result B after recursion completes
|
||||
//
|
||||
// # Context Cancellation
|
||||
//
|
||||
// Unlike the base [readerioresult.TailRec], this version automatically integrates
|
||||
// Unlike the base readerioresult.TailRec, this version automatically integrates
|
||||
// context cancellation checking:
|
||||
// - Each recursive iteration checks if the context is cancelled
|
||||
// - If cancelled, recursion terminates immediately with a cancellation error
|
||||
@@ -92,9 +92,9 @@ import (
|
||||
//
|
||||
// # Example: Cancellable Countdown
|
||||
//
|
||||
// countdownStep := func(n int) readerioresult.ReaderIOResult[tailrec.Trampoline[int, string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[int, string]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[int, string]] {
|
||||
// countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
// return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
// return func() Either[Trampoline[int, string]] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[error](tailrec.Land[int]("Done!"))
|
||||
// }
|
||||
@@ -105,7 +105,7 @@ import (
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// countdown := readerioresult.TailRec(countdownStep)
|
||||
// countdown := TailRec(countdownStep)
|
||||
//
|
||||
// // With cancellation
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
|
||||
@@ -119,9 +119,9 @@ import (
|
||||
// processed []string
|
||||
// }
|
||||
//
|
||||
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// processStep := func(state ProcessState) ReaderIOResult[Trampoline[ProcessState, []string]] {
|
||||
// return func(ctx context.Context) IOEither[Trampoline[ProcessState, []string]] {
|
||||
// return func() Either[Trampoline[ProcessState, []string]] {
|
||||
// if len(state.files) == 0 {
|
||||
// return either.Right[error](tailrec.Land[ProcessState](state.processed))
|
||||
// }
|
||||
@@ -140,7 +140,7 @@ import (
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// processFiles := readerioresult.TailRec(processStep)
|
||||
// processFiles := TailRec(processStep)
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
//
|
||||
// // Can be cancelled at any point during processing
|
||||
@@ -158,7 +158,7 @@ import (
|
||||
// still respecting context cancellation:
|
||||
//
|
||||
// // Safe for very large inputs with cancellation support
|
||||
// largeCountdown := readerioresult.TailRec(countdownStep)
|
||||
// largeCountdown := TailRec(countdownStep)
|
||||
// ctx := t.Context()
|
||||
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
|
||||
//
|
||||
@@ -171,11 +171,11 @@ import (
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - [readerioresult.TailRec]: Base tail recursion without automatic context checking
|
||||
// - [WithContext]: Context cancellation wrapper used internally
|
||||
// - [Chain]: For sequencing ReaderIOResult computations
|
||||
// - [Ask]: For accessing the context
|
||||
// - [Left]/[Right]: For creating error/success values
|
||||
// - readerioresult.TailRec: Base tail recursion without automatic context checking
|
||||
// - WithContext: Context cancellation wrapper used internally
|
||||
// - Chain: For sequencing ReaderIOResult computations
|
||||
// - Ask: For accessing the context
|
||||
// - Left/Right: For creating error/success values
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
|
||||
@@ -30,6 +30,16 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// CustomError is a test error type
|
||||
type CustomError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *CustomError) Error() string {
|
||||
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func TestTailRec_BasicRecursion(t *testing.T) {
|
||||
// Test basic countdown recursion
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
@@ -432,3 +442,237 @@ func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
|
||||
func TestTailRec_MultipleErrorTypes(t *testing.T) {
|
||||
// Test that different error types are properly handled
|
||||
errorStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
if n == 5 {
|
||||
customErr := &CustomError{Code: 500, Message: "custom error"}
|
||||
return E.Left[Trampoline[int, string]](error(customErr))
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorRecursion := TailRec(errorStep)
|
||||
result := errorRecursion(10)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
customErr, ok := err.(*CustomError)
|
||||
require.True(t, ok, "Expected CustomError type")
|
||||
assert.Equal(t, 500, customErr.Code)
|
||||
assert.Equal(t, "custom error", customErr.Message)
|
||||
}
|
||||
|
||||
func TestTailRec_ContextCancelDuringBounce(t *testing.T) {
|
||||
// Test cancellation happens between bounces, not during computation
|
||||
var iterationCount int32
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
count := atomic.AddInt32(&iterationCount, 1)
|
||||
|
||||
// Cancel after 3 iterations
|
||||
if count == 3 {
|
||||
cancel()
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slowRecursion := TailRec(slowStep)
|
||||
result := slowRecursion(10)(ctx)()
|
||||
|
||||
// Should be cancelled after a few iterations
|
||||
assert.True(t, E.IsLeft(result))
|
||||
iterations := atomic.LoadInt32(&iterationCount)
|
||||
assert.Greater(t, iterations, int32(2))
|
||||
assert.Less(t, iterations, int32(10))
|
||||
}
|
||||
|
||||
func TestTailRec_EmptyState(t *testing.T) {
|
||||
// Test with empty/zero-value state
|
||||
type EmptyState struct{}
|
||||
|
||||
emptyStep := func(state EmptyState) ReaderIOResult[Trampoline[EmptyState, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[EmptyState, int]] {
|
||||
return func() Either[Trampoline[EmptyState, int]] {
|
||||
return E.Right[error](tailrec.Land[EmptyState](42))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emptyRecursion := TailRec(emptyStep)
|
||||
result := emptyRecursion(EmptyState{})(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
}
|
||||
|
||||
func TestTailRec_PointerState(t *testing.T) {
|
||||
// Test with pointer state to ensure proper handling
|
||||
type Node struct {
|
||||
Value int
|
||||
Next *Node
|
||||
}
|
||||
|
||||
// Create a linked list: 1 -> 2 -> 3 -> nil
|
||||
list := &Node{Value: 1, Next: &Node{Value: 2, Next: &Node{Value: 3, Next: nil}}}
|
||||
|
||||
sumStep := func(node *Node) ReaderIOResult[Trampoline[*Node, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[*Node, int]] {
|
||||
return func() Either[Trampoline[*Node, int]] {
|
||||
if node == nil {
|
||||
return E.Right[error](tailrec.Land[*Node](0))
|
||||
}
|
||||
if node.Next == nil {
|
||||
return E.Right[error](tailrec.Land[*Node](node.Value))
|
||||
}
|
||||
// Accumulate value and continue
|
||||
node.Next.Value += node.Value
|
||||
return E.Right[error](tailrec.Bounce[int](node.Next))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
result := sumList(list)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](6), result) // 1 + 2 + 3 = 6
|
||||
}
|
||||
|
||||
func TestTailRec_ConcurrentCancellation(t *testing.T) {
|
||||
// Test that cancellation works correctly with concurrent operations
|
||||
var iterationCount int32
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
atomic.AddInt32(&iterationCount, 1)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Cancel from another goroutine after 50ms
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
result := slowRecursion(20)(ctx)()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should be cancelled
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
// Should complete quickly due to cancellation
|
||||
assert.Less(t, elapsed, 100*time.Millisecond)
|
||||
|
||||
// Should have executed some but not all iterations
|
||||
iterations := atomic.LoadInt32(&iterationCount)
|
||||
assert.Greater(t, iterations, int32(0))
|
||||
assert.Less(t, iterations, int32(20))
|
||||
}
|
||||
|
||||
func TestTailRec_NestedContextValues(t *testing.T) {
|
||||
// Test that nested context values are preserved
|
||||
type contextKey string
|
||||
const (
|
||||
key1 contextKey = "key1"
|
||||
key2 contextKey = "key2"
|
||||
)
|
||||
|
||||
nestedStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
val1 := ctx.Value(key1)
|
||||
val2 := ctx.Value(key2)
|
||||
|
||||
require.NotNil(t, val1)
|
||||
require.NotNil(t, val2)
|
||||
assert.Equal(t, "value1", val1.(string))
|
||||
assert.Equal(t, "value2", val2.(string))
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nestedRecursion := TailRec(nestedStep)
|
||||
|
||||
ctx := context.WithValue(t.Context(), key1, "value1")
|
||||
ctx = context.WithValue(ctx, key2, "value2")
|
||||
|
||||
result := nestedRecursion(3)(ctx)()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
|
||||
func BenchmarkTailRec_SimpleCountdown(b *testing.B) {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
|
||||
return func() Either[Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = countdown(1000)(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTailRec_WithCancellation(b *testing.B) {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
|
||||
return func() Either[Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = countdown(1000)(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -612,3 +612,50 @@ 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
|
||||
//
|
||||
// - 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,411 @@ func TestChainThunkK_Integration(t *testing.T) {
|
||||
assert.Equal(t, result.Of("Value: 100"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
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[Config](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[Config](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[TestConfig](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[Config](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[TestConfig](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[Config](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[Config](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[Config](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[Config](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[Config](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[Config](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[TestConfig](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[TestConfig](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[Config](func(cfg Config) string {
|
||||
return cfg.First
|
||||
}),
|
||||
Chain(func(_ string) Effect[Config, string] {
|
||||
return Asks[Config](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[Config](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[Config](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[Config](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[DatabaseConfig](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[APIConfig](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[Config](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)
|
||||
})
|
||||
}
|
||||
|
||||
86
v2/effect/profunctor.go
Normal file
86
v2/effect/profunctor.go
Normal file
@@ -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),
|
||||
)
|
||||
}
|
||||
373
v2/effect/profunctor_test.go
Normal file
373
v2/effect/profunctor_test.go
Normal file
@@ -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)()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
169
v2/optics/codec/iso.go
Normal file
169
v2/optics/codec/iso.go
Normal file
@@ -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,
|
||||
)
|
||||
}
|
||||
504
v2/optics/codec/iso_test.go
Normal file
504
v2/optics/codec/iso_test.go
Normal file
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,6 +69,14 @@ func TestFormatterInterface(t *testing.T) {
|
||||
result := fmt.Sprintf("%q", tramp)
|
||||
assert.Equal(t, "\"Bounce(42)\"", result)
|
||||
})
|
||||
|
||||
t.Run("unknown verb format", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
result := fmt.Sprintf("%x", tramp)
|
||||
assert.Contains(t, result, "%!x")
|
||||
assert.Contains(t, result, "Trampoline[B, L]")
|
||||
assert.Contains(t, result, "Bounce(42)")
|
||||
})
|
||||
}
|
||||
|
||||
// TestGoStringerInterface verifies fmt.GoStringer implementation
|
||||
|
||||
@@ -22,14 +22,18 @@ import (
|
||||
// LogValue implements the slog.LogValuer interface for Trampoline.
|
||||
//
|
||||
// This method allows Trampoline values to be logged using Go's structured logging
|
||||
// (log/slog) with proper representation of their state:
|
||||
// with proper representation of their state:
|
||||
// - When Landed is true: returns a group with a single "landed" attribute containing the Land value
|
||||
// - When Landed is false: returns a group with a single "bouncing" attribute containing the Bounce value
|
||||
//
|
||||
// The implementation ensures that Trampoline values are logged in a structured,
|
||||
// readable format that clearly shows the current state of the tail-recursive computation.
|
||||
//
|
||||
// Example usage:
|
||||
// # Returns
|
||||
//
|
||||
// - slog.Value: A structured log value representing the trampoline state
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// trampoline := tailrec.Bounce[int](42)
|
||||
// slog.Info("Processing", "state", trampoline)
|
||||
|
||||
@@ -8,17 +8,20 @@ import "fmt"
|
||||
// This represents a recursive call in the original algorithm. The computation
|
||||
// will continue by processing the provided state value in the next iteration.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - L: The final result type (land type)
|
||||
// - B: The intermediate state type (bounce type)
|
||||
// # Type Parameters
|
||||
//
|
||||
// - B: The intermediate state type (bounce type)
|
||||
// - L: The final result type (land type)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The new intermediate state to process in the next step
|
||||
//
|
||||
// Returns:
|
||||
// - A Trampoline in the "bounce" state containing the intermediate value
|
||||
// # Returns
|
||||
//
|
||||
// Example:
|
||||
// - Trampoline[B, L]: A Trampoline in the "bounce" state containing the intermediate value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Countdown that bounces until reaching zero
|
||||
// func countdownStep(n int) Trampoline[int, int] {
|
||||
@@ -40,17 +43,20 @@ func Bounce[L, B any](b B) Trampoline[B, L] {
|
||||
// a Land trampoline is encountered, the executor should stop iterating and
|
||||
// return the final result.
|
||||
//
|
||||
// Type Parameters:
|
||||
// # Type Parameters
|
||||
//
|
||||
// - B: The intermediate state type (bounce type)
|
||||
// - L: The final result type (land type)
|
||||
//
|
||||
// Parameters:
|
||||
// # Parameters
|
||||
//
|
||||
// - l: The final result value
|
||||
//
|
||||
// Returns:
|
||||
// - A Trampoline in the "land" state containing the final result
|
||||
// # Returns
|
||||
//
|
||||
// Example:
|
||||
// - Trampoline[B, L]: A Trampoline in the "land" state containing the final result
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Factorial base case
|
||||
// func factorialStep(state State) Trampoline[State, int] {
|
||||
@@ -66,7 +72,13 @@ func Land[B, L any](l L) Trampoline[B, L] {
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer for Trampoline.
|
||||
//
|
||||
// Returns a human-readable string representation of the trampoline state.
|
||||
// For bounce states, returns "Bounce(value)". For land states, returns "Land(value)".
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - string: A formatted string representation of the trampoline state
|
||||
func (t Trampoline[B, L]) String() string {
|
||||
if t.Landed {
|
||||
return fmt.Sprintf("Land(%v)", t.Land)
|
||||
@@ -75,7 +87,18 @@ func (t Trampoline[B, L]) String() string {
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Trampoline.
|
||||
// Supports various formatting verbs for detailed output.
|
||||
//
|
||||
// Supports various formatting verbs for detailed output:
|
||||
// - %v: Default format (delegates to String)
|
||||
// - %+v: Detailed format with type information
|
||||
// - %#v: Go-syntax representation (delegates to GoString)
|
||||
// - %s: String format
|
||||
// - %q: Quoted string format
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: The format state
|
||||
// - verb: The formatting verb
|
||||
func (t Trampoline[B, L]) Format(f fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
@@ -106,7 +129,13 @@ func (t Trampoline[B, L]) Format(f fmt.State, verb rune) {
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Trampoline.
|
||||
//
|
||||
// Returns a Go-syntax representation that could be used to recreate the value.
|
||||
// The output includes the package name, function name, type parameters, and value.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - string: A Go-syntax representation of the trampoline
|
||||
func (t Trampoline[B, L]) GoString() string {
|
||||
if t.Landed {
|
||||
return fmt.Sprintf("tailrec.Land[%T](%#v)", t.Bounce, t.Land)
|
||||
|
||||
@@ -7,14 +7,21 @@ type (
|
||||
// - Bounce: The computation should continue with a new intermediate state (type B)
|
||||
// - Land: The computation is complete with a final result (type L)
|
||||
//
|
||||
// Type Parameters:
|
||||
// # Type Parameters
|
||||
//
|
||||
// - B: The "bounce" type - intermediate state passed between recursive steps
|
||||
// - L: The "land" type - the final result type when computation completes
|
||||
//
|
||||
// The trampoline pattern allows converting recursive algorithms into iterative ones,
|
||||
// preventing stack overflow for deep recursion while maintaining code clarity.
|
||||
//
|
||||
// Example:
|
||||
// # Design Note
|
||||
//
|
||||
// This type uses a struct with a boolean flag rather than the Either type to avoid
|
||||
// a cyclic dependency. The either package depends on tailrec for its own tail-recursive
|
||||
// operations, so using Either here would create a circular import.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Factorial using trampolines
|
||||
// type State struct { n, acc int }
|
||||
|
||||
Reference in New Issue
Block a user