mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-23 23:51:14 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49227551b6 | ||
|
|
69691e9e70 | ||
|
|
d3c466bfb7 | ||
|
|
a6c6ea804f | ||
|
|
31ff98901e | ||
|
|
255cf4353c |
16
v2/README.md
16
v2/README.md
@@ -452,17 +452,27 @@ func process() IOResult[string] {
|
||||
|
||||
### Core Modules
|
||||
|
||||
#### Standard Packages (Struct-based)
|
||||
- **Option** - Represent optional values without nil
|
||||
- **Either** - Type-safe error handling with left/right values
|
||||
- **Result** - Simplified Either with error as left type
|
||||
- **Result** - Simplified Either with error as left type (recommended for error handling)
|
||||
- **IO** - Lazy evaluation and side effect management
|
||||
- **IOEither** - Combine IO with error handling
|
||||
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
|
||||
- **Reader** - Dependency injection pattern
|
||||
- **ReaderIOEither** - Combine Reader, IO, and Either for complex workflows
|
||||
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
|
||||
- **Array** - Functional array operations
|
||||
- **Record** - Functional record/map operations
|
||||
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
|
||||
|
||||
#### Idiomatic Packages (Tuple-based, High Performance)
|
||||
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples
|
||||
- **idiomatic/result** - Result monad using native Go `(value, error)` tuples
|
||||
- **idiomatic/ioresult** - IOResult monad using `func() (value, error)` for IO operations
|
||||
- **idiomatic/readerresult** - Reader monad combined with Result pattern
|
||||
- **idiomatic/readerioresult** - Reader monad combined with IOResult pattern
|
||||
|
||||
The idiomatic packages offer 2-10x performance improvements and zero allocations by using Go's native tuple patterns instead of struct wrappers. Use them for performance-critical code or when you prefer Go's native error handling style.
|
||||
|
||||
## 🤔 Should I Migrate?
|
||||
|
||||
**Migrate to V2 if:**
|
||||
|
||||
@@ -98,7 +98,7 @@ func Example_resultAssertions() {
|
||||
var t *testing.T // placeholder for example
|
||||
|
||||
// Assert success
|
||||
successResult := result.Of[int](42)
|
||||
successResult := result.Of(42)
|
||||
assert.Success(successResult)(t)
|
||||
|
||||
// Assert failure
|
||||
|
||||
@@ -20,6 +20,6 @@ import (
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return readerio.TailRec(f)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerio"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -72,4 +73,6 @@ type (
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
)
|
||||
@@ -36,9 +35,9 @@ import (
|
||||
//
|
||||
// # How It Works
|
||||
//
|
||||
// TailRec takes a Kleisli arrow that returns Either[A, B]:
|
||||
// - Left(A): Continue recursion with the new state A
|
||||
// - Right(B): Terminate recursion successfully and return the final result B
|
||||
// TailRec takes a Kleisli arrow that returns Trampoline[A, B]:
|
||||
// - Bounce(A): Continue recursion with the new state A
|
||||
// - Land(B): Terminate recursion successfully and return the final result B
|
||||
//
|
||||
// The function wraps each iteration with [WithContext] to ensure context cancellation
|
||||
// is checked before each recursive step. If the context is cancelled, the recursion
|
||||
@@ -51,11 +50,11 @@ import (
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow (A => ReaderIOResult[Either[A, B]]) that:
|
||||
// - f: A Kleisli arrow (A => ReaderIOResult[Trampoline[A, B]]) that:
|
||||
// - Takes the current state A
|
||||
// - Returns a ReaderIOResult that depends on [context.Context]
|
||||
// - Can fail with error (Left in the outer Either)
|
||||
// - Produces Either[A, B] to control recursion flow (Right in the outer Either)
|
||||
// - Produces Trampoline[A, B] to control recursion flow (Right in the outer Either)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
@@ -93,15 +92,15 @@ import (
|
||||
//
|
||||
// # Example: Cancellable Countdown
|
||||
//
|
||||
// countdownStep := func(n int) readerioresult.ReaderIOResult[either.Either[int, string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[int, string]] {
|
||||
// return func() either.Either[error, either.Either[int, string]] {
|
||||
// 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]] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[error](either.Right[int]("Done!"))
|
||||
// return either.Right[error](tailrec.Land[int]("Done!"))
|
||||
// }
|
||||
// // Simulate some work
|
||||
// time.Sleep(100 * time.Millisecond)
|
||||
// return either.Right[error](either.Left[string](n - 1))
|
||||
// return either.Right[error](tailrec.Bounce[string](n - 1))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -120,20 +119,20 @@ import (
|
||||
// processed []string
|
||||
// }
|
||||
//
|
||||
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[either.Either[ProcessState, []string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[ProcessState, []string]] {
|
||||
// return func() either.Either[error, either.Either[ProcessState, []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]] {
|
||||
// if len(state.files) == 0 {
|
||||
// return either.Right[error](either.Right[ProcessState](state.processed))
|
||||
// return either.Right[error](tailrec.Land[ProcessState](state.processed))
|
||||
// }
|
||||
//
|
||||
// file := state.files[0]
|
||||
// // Process file (this could be cancelled via context)
|
||||
// if err := processFileWithContext(ctx, file); err != nil {
|
||||
// return either.Left[either.Either[ProcessState, []string]](err)
|
||||
// return either.Left[tailrec.Trampoline[ProcessState, []string]](err)
|
||||
// }
|
||||
//
|
||||
// return either.Right[error](either.Left[[]string](ProcessState{
|
||||
// return either.Right[error](tailrec.Bounce[[]string](ProcessState{
|
||||
// files: state.files[1:],
|
||||
// processed: append(state.processed, file),
|
||||
// }))
|
||||
@@ -179,6 +178,6 @@ import (
|
||||
// - [Left]/[Right]: For creating error/success values
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return RIOR.TailRec(F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
@@ -25,19 +25,20 @@ import (
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTailRec_BasicRecursion(t *testing.T) {
|
||||
// Test basic countdown recursion
|
||||
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[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 E.Right[error](E.Right[int]("Done!"))
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,13 +56,13 @@ func TestTailRec_FactorialRecursion(t *testing.T) {
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state FactorialState) ReaderIOResult[E.Either[FactorialState, int]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[FactorialState, int]] {
|
||||
return func() Either[E.Either[FactorialState, int]] {
|
||||
factorialStep := func(state FactorialState) ReaderIOResult[Trampoline[FactorialState, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[FactorialState, int]] {
|
||||
return func() Either[Trampoline[FactorialState, int]] {
|
||||
if state.n <= 1 {
|
||||
return E.Right[error](E.Right[FactorialState](state.acc))
|
||||
return E.Right[error](tailrec.Land[FactorialState](state.acc))
|
||||
}
|
||||
return E.Right[error](E.Left[int](FactorialState{
|
||||
return E.Right[error](tailrec.Bounce[int](FactorialState{
|
||||
n: state.n - 1,
|
||||
acc: state.acc * state.n,
|
||||
}))
|
||||
@@ -79,16 +80,16 @@ func TestTailRec_ErrorHandling(t *testing.T) {
|
||||
// Test that errors are properly propagated
|
||||
testErr := errors.New("computation error")
|
||||
|
||||
errorStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
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 == 3 {
|
||||
return E.Left[E.Either[int, string]](testErr)
|
||||
return E.Left[Trampoline[int, string]](testErr)
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,18 +106,18 @@ func TestTailRec_ContextCancellation(t *testing.T) {
|
||||
// Test that recursion gets cancelled early when context is canceled
|
||||
var iterationCount int32
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
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)
|
||||
|
||||
// Simulate some work
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,13 +145,13 @@ func TestTailRec_ContextCancellation(t *testing.T) {
|
||||
|
||||
func TestTailRec_ImmediateCancellation(t *testing.T) {
|
||||
// Test with an already cancelled context
|
||||
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[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 E.Right[error](E.Right[int]("Done!"))
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,13 +174,13 @@ func TestTailRec_StackSafety(t *testing.T) {
|
||||
// Test that deep recursion doesn't cause stack overflow
|
||||
const largeN = 10000
|
||||
|
||||
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, int]] {
|
||||
return func() Either[E.Either[int, int]] {
|
||||
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](E.Right[int](0))
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](E.Left[int](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,9 +196,9 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
|
||||
const largeN = 100000
|
||||
var iterationCount int32
|
||||
|
||||
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, int]] {
|
||||
return func() Either[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
|
||||
return func() Either[Trampoline[int, int]] {
|
||||
atomic.AddInt32(&iterationCount, 1)
|
||||
|
||||
// Add a small delay every 1000 iterations to make cancellation more likely
|
||||
@@ -206,9 +207,9 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int](0))
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](E.Left[int](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,22 +241,22 @@ func TestTailRec_ComplexState(t *testing.T) {
|
||||
errors []error
|
||||
}
|
||||
|
||||
processStep := func(state ProcessState) ReaderIOResult[E.Either[ProcessState, []string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[ProcessState, []string]] {
|
||||
return func() Either[E.Either[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 A.IsEmpty(state.items) {
|
||||
return E.Right[error](E.Right[ProcessState](state.processed))
|
||||
return E.Right[error](tailrec.Land[ProcessState](state.processed))
|
||||
}
|
||||
|
||||
item := state.items[0]
|
||||
|
||||
// Simulate processing that might fail for certain items
|
||||
if item == "error-item" {
|
||||
return E.Left[E.Either[ProcessState, []string]](
|
||||
return E.Left[Trampoline[ProcessState, []string]](
|
||||
fmt.Errorf("failed to process item: %s", item))
|
||||
}
|
||||
|
||||
return E.Right[error](E.Left[[]string](ProcessState{
|
||||
return E.Right[error](tailrec.Bounce[[]string](ProcessState{
|
||||
items: state.items[1:],
|
||||
processed: append(state.processed, item),
|
||||
errors: state.errors,
|
||||
@@ -302,18 +303,18 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
|
||||
|
||||
var processedCount int32
|
||||
|
||||
processFileStep := func(state FileProcessState) ReaderIOResult[E.Either[FileProcessState, int]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[FileProcessState, int]] {
|
||||
return func() Either[E.Either[FileProcessState, int]] {
|
||||
processFileStep := func(state FileProcessState) ReaderIOResult[Trampoline[FileProcessState, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[FileProcessState, int]] {
|
||||
return func() Either[Trampoline[FileProcessState, int]] {
|
||||
if A.IsEmpty(state.files) {
|
||||
return E.Right[error](E.Right[FileProcessState](state.processed))
|
||||
return E.Right[error](tailrec.Land[FileProcessState](state.processed))
|
||||
}
|
||||
|
||||
// Simulate file processing time
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
atomic.AddInt32(&processedCount, 1)
|
||||
|
||||
return E.Right[error](E.Left[int](FileProcessState{
|
||||
return E.Right[error](tailrec.Bounce[int](FileProcessState{
|
||||
files: state.files[1:],
|
||||
processed: state.processed + 1,
|
||||
}))
|
||||
@@ -356,10 +357,10 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
|
||||
|
||||
func TestTailRec_ZeroIterations(t *testing.T) {
|
||||
// Test case where recursion terminates immediately
|
||||
immediateStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
return E.Right[error](E.Right[int]("immediate"))
|
||||
immediateStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
return E.Right[error](tailrec.Land[int]("immediate"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,16 +375,16 @@ func TestTailRec_ContextWithDeadline(t *testing.T) {
|
||||
// Test with context deadline
|
||||
var iterationCount int32
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
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(30 * time.Millisecond)
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,17 +411,17 @@ func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
type contextKey string
|
||||
const testKey contextKey = "test"
|
||||
|
||||
valueStep := func(n int) ReaderIOResult[E.Either[int, string]] {
|
||||
return func(ctx context.Context) IOEither[E.Either[int, string]] {
|
||||
return func() Either[E.Either[int, string]] {
|
||||
valueStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
value := ctx.Value(testKey)
|
||||
require.NotNil(t, value)
|
||||
assert.Equal(t, "test-value", value.(string))
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](E.Right[int]("Done!"))
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](E.Left[string](n - 1))
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
RIOR "github.com/IBM/fp-go/v2/readerioresult"
|
||||
"github.com/IBM/fp-go/v2/readeroption"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -137,4 +138,6 @@ type (
|
||||
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
)
|
||||
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
|
||||
// TailRec implements tail-recursive computation for ReaderResult with context cancellation support.
|
||||
//
|
||||
// TailRec takes a Kleisli function that returns Either[A, B] and converts it into a stack-safe,
|
||||
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Right value.
|
||||
// TailRec takes a Kleisli function that returns Trampoline[A, B] and converts it into a stack-safe,
|
||||
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Land value.
|
||||
//
|
||||
// The implementation includes a short-circuit mechanism that checks for context cancellation on each
|
||||
// iteration. If the context is canceled (ctx.Err() != nil), the computation immediately returns a
|
||||
@@ -37,9 +37,9 @@ import (
|
||||
// - B: The final result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Either[A, B].
|
||||
// When the result is Left[B](a), recursion continues with the new value 'a'.
|
||||
// When the result is Right[A](b), recursion terminates with the final value 'b'.
|
||||
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Trampoline[A, B].
|
||||
// When the result is Bounce(a), recursion continues with the new value 'a'.
|
||||
// When the result is Land(b), recursion terminates with the final value 'b'.
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
|
||||
@@ -48,8 +48,8 @@ import (
|
||||
// - On each iteration, checks if the context has been canceled (short circuit)
|
||||
// - If canceled, returns result.Left[B](context.Cause(ctx))
|
||||
// - If the step returns Left[B](error), propagates the error
|
||||
// - If the step returns Right[A](Left[B](a)), continues recursion with new value 'a'
|
||||
// - If the step returns Right[A](Right[A](b)), terminates with success value 'b'
|
||||
// - If the step returns Right[A](Bounce(a)), continues recursion with new value 'a'
|
||||
// - If the step returns Right[A](Land(b)), terminates with success value 'b'
|
||||
//
|
||||
// Example - Factorial computation with context:
|
||||
//
|
||||
@@ -58,12 +58,12 @@ import (
|
||||
// acc int
|
||||
// }
|
||||
//
|
||||
// factorialStep := func(state State) ReaderResult[either.Either[State, int]] {
|
||||
// return func(ctx context.Context) result.Result[either.Either[State, int]] {
|
||||
// factorialStep := func(state State) ReaderResult[tailrec.Trampoline[State, int]] {
|
||||
// return func(ctx context.Context) result.Result[tailrec.Trampoline[State, int]] {
|
||||
// if state.n <= 0 {
|
||||
// return result.Of(either.Right[State](state.acc))
|
||||
// return result.Of(tailrec.Land[State](state.acc))
|
||||
// }
|
||||
// return result.Of(either.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
// return result.Of(tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
// }
|
||||
// }
|
||||
//
|
||||
@@ -80,10 +80,10 @@ import (
|
||||
// // Returns result.Left[B](context.Cause(ctx)) without executing any steps
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return func(a A) ReaderResult[B] {
|
||||
initialReader := f(a)
|
||||
return func(ctx context.Context) Result[B] {
|
||||
return func(ctx context.Context) result.Result[B] {
|
||||
rdr := initialReader
|
||||
for {
|
||||
// short circuit
|
||||
@@ -95,11 +95,10 @@ func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
|
||||
if either.IsLeft(current) {
|
||||
return result.Left[B](e)
|
||||
}
|
||||
b, a := either.Unwrap(rec)
|
||||
if either.IsRight(rec) {
|
||||
return result.Of(b)
|
||||
if rec.Landed {
|
||||
return result.Of(rec.Land)
|
||||
}
|
||||
rdr = f(a)
|
||||
rdr = f(rec.Bounce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -35,12 +35,12 @@ func TestTailRecFactorial(t *testing.T) {
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
factorialStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.n <= 0 {
|
||||
return R.Of(E.Right[State](state.acc))
|
||||
return R.Of(TR.Land[State](state.acc))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
return R.Of(TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,12 +58,12 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
curr int
|
||||
}
|
||||
|
||||
fibStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
fibStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.n <= 0 {
|
||||
return R.Of(E.Right[State](state.curr))
|
||||
return R.Of(TR.Land[State](state.curr))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
return R.Of(TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +75,12 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
|
||||
// TestTailRecCountdown tests countdown computation
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,9 @@ func TestTailRecCountdown(t *testing.T) {
|
||||
|
||||
// TestTailRecImmediateTermination tests immediate termination (Right on first call)
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
immediateStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
return R.Of(E.Right[int](n * 2))
|
||||
immediateStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
return R.Of(TR.Land[int](n * 2))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,12 +106,12 @@ func TestTailRecImmediateTermination(t *testing.T) {
|
||||
|
||||
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@ func TestTailRecSumList(t *testing.T) {
|
||||
sum int
|
||||
}
|
||||
|
||||
sumStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
sumStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if A.IsEmpty(state.list) {
|
||||
return R.Of(E.Right[State](state.sum))
|
||||
return R.Of(TR.Land[State](state.sum))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
return R.Of(TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,15 +145,15 @@ func TestTailRecSumList(t *testing.T) {
|
||||
|
||||
// TestTailRecCollatzConjecture tests the Collatz conjecture
|
||||
func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
collatzStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
collatzStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 1 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return R.Of(E.Left[int](n / 2))
|
||||
return R.Of(TR.Bounce[int](n / 2))
|
||||
}
|
||||
return R.Of(E.Left[int](3*n + 1))
|
||||
return R.Of(TR.Bounce[int](3*n + 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,12 +170,12 @@ func TestTailRecGCD(t *testing.T) {
|
||||
b int
|
||||
}
|
||||
|
||||
gcdStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
gcdStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.b == 0 {
|
||||
return R.Of(E.Right[State](state.a))
|
||||
return R.Of(TR.Land[State](state.a))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.b, state.a % state.b}))
|
||||
return R.Of(TR.Bounce[int](State{state.b, state.a % state.b}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,15 +189,15 @@ func TestTailRecGCD(t *testing.T) {
|
||||
func TestTailRecErrorPropagation(t *testing.T) {
|
||||
expectedErr := errors.New("computation error")
|
||||
|
||||
errorStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
errorStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n == 5 {
|
||||
return R.Left[E.Either[int, int]](expectedErr)
|
||||
return R.Left[TR.Trampoline[int, int]](expectedErr)
|
||||
}
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,13 +215,13 @@ func TestTailRecContextCancellationImmediate(t *testing.T) {
|
||||
cancel() // Cancel immediately before execution
|
||||
|
||||
stepExecuted := false
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
stepExecuted = true
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,17 +240,17 @@ func TestTailRecContextCancellationDuringExecution(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
executionCount++
|
||||
// Cancel after 3 iterations
|
||||
if executionCount == 3 {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,15 +270,15 @@ func TestTailRecContextWithTimeout(t *testing.T) {
|
||||
defer cancel()
|
||||
|
||||
executionCount := 0
|
||||
slowStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
slowStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
executionCount++
|
||||
// Simulate slow computation
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,12 +298,12 @@ func TestTailRecContextWithCause(t *testing.T) {
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
cancel(customErr)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,16 +322,16 @@ func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
|
||||
executionCount := 0
|
||||
maxExecutions := 5
|
||||
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
executionCount++
|
||||
if executionCount == maxExecutions {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,13 +351,13 @@ func TestTailRecContextNotCanceled(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
executionCount++
|
||||
if n <= 0 {
|
||||
return R.Of(E.Right[int](n))
|
||||
return R.Of(TR.Land[int](n))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,12 +376,12 @@ func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
target int
|
||||
}
|
||||
|
||||
powerStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
powerStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.exponent >= state.target {
|
||||
return R.Of(E.Right[State](state.result))
|
||||
return R.Of(TR.Land[State](state.result))
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.exponent + 1, state.result * 2, state.target}))
|
||||
return R.Of(TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,15 +399,15 @@ func TestTailRecFindInRange(t *testing.T) {
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.current >= state.max {
|
||||
return R.Of(E.Right[State](-1)) // Not found
|
||||
return R.Of(TR.Land[State](-1)) // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return R.Of(E.Right[State](state.current)) // Found
|
||||
return R.Of(TR.Land[State](state.current)) // Found
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
|
||||
return R.Of(TR.Bounce[int](State{state.current + 1, state.max, state.target}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,15 +425,15 @@ func TestTailRecFindNotInRange(t *testing.T) {
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[E.Either[State, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[State, int]] {
|
||||
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[State, int]] {
|
||||
if state.current >= state.max {
|
||||
return R.Of(E.Right[State](-1)) // Not found
|
||||
return R.Of(TR.Land[State](-1)) // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return R.Of(E.Right[State](state.current)) // Found
|
||||
return R.Of(TR.Land[State](state.current)) // Found
|
||||
}
|
||||
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
|
||||
return R.Of(TR.Bounce[int](State{state.current + 1, state.max, state.target}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,13 +450,13 @@ func TestTailRecWithContextValue(t *testing.T) {
|
||||
|
||||
ctx := context.WithValue(context.Background(), multiplierKey, 3)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
|
||||
return func(ctx context.Context) Result[E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
multiplier := ctx.Value(multiplierKey).(int)
|
||||
return R.Of(E.Right[int](n * multiplier))
|
||||
return R.Of(TR.Land[int](n * multiplier))
|
||||
}
|
||||
return R.Of(E.Left[int](n - 1))
|
||||
return R.Of(TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,11 +475,11 @@ func TestTailRecComplexState(t *testing.T) {
|
||||
completed bool
|
||||
}
|
||||
|
||||
complexStep := func(state ComplexState) ReaderResult[E.Either[ComplexState, string]] {
|
||||
return func(ctx context.Context) Result[E.Either[ComplexState, string]] {
|
||||
complexStep := func(state ComplexState) ReaderResult[TR.Trampoline[ComplexState, string]] {
|
||||
return func(ctx context.Context) Result[TR.Trampoline[ComplexState, string]] {
|
||||
if state.counter <= 0 || state.completed {
|
||||
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
|
||||
return R.Of(E.Right[ComplexState](result))
|
||||
return R.Of(TR.Land[ComplexState](result))
|
||||
}
|
||||
newState := ComplexState{
|
||||
counter: state.counter - 1,
|
||||
@@ -487,7 +487,7 @@ func TestTailRecComplexState(t *testing.T) {
|
||||
product: state.product * state.counter,
|
||||
completed: state.counter == 1,
|
||||
}
|
||||
return R.Of(E.Left[string](newState))
|
||||
return R.Of(TR.Bounce[string](newState))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -61,9 +62,10 @@ type (
|
||||
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
|
||||
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
|
||||
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
|
||||
Operator[A, B any] = Kleisli[ReaderResult[A], B]
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Prism[S, T any] = prism.Prism[S, T]
|
||||
Lens[S, T any] = lens.Lens[S, T]
|
||||
Trampoline[A, B any] = tailrec.Trampoline[A, B]
|
||||
)
|
||||
|
||||
@@ -23,14 +23,15 @@ import (
|
||||
IOR "github.com/IBM/fp-go/v2/ioresult"
|
||||
L "github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
R "github.com/IBM/fp-go/v2/record"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
|
||||
"sync"
|
||||
)
|
||||
|
||||
func providerToEntry(p Provider) T.Tuple2[string, ProviderFactory] {
|
||||
return T.MakeTuple2(p.Provides().Id(), p.Factory())
|
||||
func providerToEntry(p Provider) Entry[string, ProviderFactory] {
|
||||
return pair.MakePair(p.Provides().Id(), p.Factory())
|
||||
}
|
||||
|
||||
func itemProviderToMap(p Provider) map[string][]ProviderFactory {
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/record"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[T any] = option.Option[T]
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Option[T any] = option.Option[T]
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Entry[K comparable, V any] = record.Entry[K, V]
|
||||
)
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"github.com/IBM/fp-go/v2/context/ioresult"
|
||||
"github.com/IBM/fp-go/v2/iooption"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/record"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
type (
|
||||
Option[T any] = option.Option[T]
|
||||
Result[T any] = result.Result[T]
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Option[T any] = option.Option[T]
|
||||
Result[T any] = result.Result[T]
|
||||
IOResult[T any] = ioresult.IOResult[T]
|
||||
IOOption[T any] = iooption.IOOption[T]
|
||||
Entry[K comparable, V any] = record.Entry[K, V]
|
||||
)
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
|
||||
package either
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
// Either defines a data structure that logically holds either an E or an A. The flag discriminates the cases
|
||||
Either[E, A any] struct {
|
||||
@@ -28,28 +24,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
// String prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) String() string {
|
||||
if !s.isLeft {
|
||||
return fmt.Sprintf("Right[%T](%v)", s.r, s.r)
|
||||
}
|
||||
return fmt.Sprintf("Left[%T](%v)", s.l, s.l)
|
||||
}
|
||||
|
||||
// Format prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) Format(f fmt.State, c rune) {
|
||||
switch c {
|
||||
case 's':
|
||||
fmt.Fprint(f, s.String())
|
||||
default:
|
||||
fmt.Fprint(f, s.String())
|
||||
}
|
||||
}
|
||||
|
||||
// IsLeft tests if the Either is a Left value.
|
||||
// Rather use [Fold] or [MonadFold] if you need to access the values.
|
||||
// Inverse is [IsRight].
|
||||
|
||||
149
v2/either/examples_format_test.go
Normal file
149
v2/either/examples_format_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// 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 either_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
// ExampleEither_String demonstrates the fmt.Stringer interface implementation.
|
||||
func ExampleEither_String() {
|
||||
right := E.Right[error](42)
|
||||
left := E.Left[int](errors.New("something went wrong"))
|
||||
|
||||
fmt.Println(right.String())
|
||||
fmt.Println(left.String())
|
||||
|
||||
// Output:
|
||||
// Right[int](42)
|
||||
// Left[*errors.errorString](something went wrong)
|
||||
}
|
||||
|
||||
// ExampleEither_GoString demonstrates the fmt.GoStringer interface implementation.
|
||||
func ExampleEither_GoString() {
|
||||
right := E.Right[error](42)
|
||||
left := E.Left[int](errors.New("error"))
|
||||
|
||||
fmt.Printf("%#v\n", right)
|
||||
fmt.Printf("%#v\n", left)
|
||||
|
||||
// Output:
|
||||
// either.Right[error](42)
|
||||
// either.Left[int](&errors.errorString{s:"error"})
|
||||
}
|
||||
|
||||
// ExampleEither_Format demonstrates the fmt.Formatter interface implementation.
|
||||
func ExampleEither_Format() {
|
||||
result := E.Right[error](42)
|
||||
|
||||
// Different format verbs
|
||||
fmt.Printf("%%s: %s\n", result)
|
||||
fmt.Printf("%%v: %v\n", result)
|
||||
fmt.Printf("%%+v: %+v\n", result)
|
||||
fmt.Printf("%%#v: %#v\n", result)
|
||||
|
||||
// Output:
|
||||
// %s: Right[int](42)
|
||||
// %v: Right[int](42)
|
||||
// %+v: Right[int](42)
|
||||
// %#v: either.Right[error](42)
|
||||
}
|
||||
|
||||
// ExampleEither_LogValue demonstrates the slog.LogValuer interface implementation.
|
||||
func ExampleEither_LogValue() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
// Remove time for consistent output
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
|
||||
// Right value
|
||||
rightResult := E.Right[error](42)
|
||||
logger.Info("computation succeeded", "result", rightResult)
|
||||
|
||||
// Left value
|
||||
leftResult := E.Left[int](errors.New("computation failed"))
|
||||
logger.Error("computation failed", "result", leftResult)
|
||||
|
||||
// Output:
|
||||
// level=INFO msg="computation succeeded" result.right=42
|
||||
// level=ERROR msg="computation failed" result.left="computation failed"
|
||||
}
|
||||
|
||||
// ExampleEither_formatting_comparison demonstrates different formatting options.
|
||||
func ExampleEither_formatting_comparison() {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
result := E.Right[error](user)
|
||||
|
||||
fmt.Printf("String(): %s\n", result.String())
|
||||
fmt.Printf("GoString(): %s\n", result.GoString())
|
||||
fmt.Printf("%%v: %v\n", result)
|
||||
fmt.Printf("%%#v: %#v\n", result)
|
||||
|
||||
// Output:
|
||||
// String(): Right[either_test.User]({123 Alice})
|
||||
// GoString(): either.Right[error](either_test.User{ID:123, Name:"Alice"})
|
||||
// %v: Right[either_test.User]({123 Alice})
|
||||
// %#v: either.Right[error](either_test.User{ID:123, Name:"Alice"})
|
||||
}
|
||||
|
||||
// ExampleEither_LogValue_structured demonstrates structured logging with Either.
|
||||
func ExampleEither_LogValue_structured() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
|
||||
// Simulate a computation pipeline
|
||||
compute := func(x int) E.Either[error, int] {
|
||||
if x < 0 {
|
||||
return E.Left[int](errors.New("negative input"))
|
||||
}
|
||||
return E.Right[error](x * 2)
|
||||
}
|
||||
|
||||
// Log successful computation
|
||||
result1 := compute(21)
|
||||
logger.Info("computation", "input", 21, "output", result1)
|
||||
|
||||
// Log failed computation
|
||||
result2 := compute(-5)
|
||||
logger.Error("computation", "input", -5, "output", result2)
|
||||
|
||||
// Output:
|
||||
// level=INFO msg=computation input=21 output.right=42
|
||||
// level=ERROR msg=computation input=-5 output.left="negative input"
|
||||
}
|
||||
103
v2/either/format.go
Normal file
103
v2/either/format.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// 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 either
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
)
|
||||
|
||||
const (
|
||||
leftGoTemplate = "either.Left[%s](%#v)"
|
||||
rightGoTemplate = "either.Right[%s](%#v)"
|
||||
|
||||
leftFmtTemplate = "Left[%T](%v)"
|
||||
rightFmtTemplate = "Right[%T](%v)"
|
||||
)
|
||||
|
||||
func goString(template string, other, v any) string {
|
||||
return fmt.Sprintf(template, formatting.TypeInfo(other), v)
|
||||
}
|
||||
|
||||
// String prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) String() string {
|
||||
if !s.isLeft {
|
||||
return fmt.Sprintf(rightFmtTemplate, s.r, s.r)
|
||||
}
|
||||
return fmt.Sprintf(leftFmtTemplate, s.l, s.l)
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Either.
|
||||
// Supports all standard format verbs:
|
||||
// - %s, %v, %+v: uses String() representation
|
||||
// - %#v: uses GoString() representation
|
||||
// - %q: quoted String() representation
|
||||
// - other verbs: uses String() representation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// e := either.Right[error](42)
|
||||
// fmt.Printf("%s", e) // "Right[int](42)"
|
||||
// fmt.Printf("%v", e) // "Right[int](42)"
|
||||
// fmt.Printf("%#v", e) // "either.Right[error](42)"
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) Format(f fmt.State, c rune) {
|
||||
formatting.FmtString(s, f, c)
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Either.
|
||||
// Returns a Go-syntax representation of the Either value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// either.Right[error](42).GoString() // "either.Right[error](42)"
|
||||
// either.Left[int](errors.New("fail")).GoString() // "either.Left[int](error)"
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) GoString() string {
|
||||
if !s.isLeft {
|
||||
return goString(rightGoTemplate, new(E), s.r)
|
||||
}
|
||||
return goString(leftGoTemplate, new(A), s.l)
|
||||
}
|
||||
|
||||
// LogValue implements slog.LogValuer for Either.
|
||||
// Returns a slog.Value that represents the Either for structured logging.
|
||||
// Returns a group value with "right" key for Right values and "left" key for Left values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logger := slog.Default()
|
||||
// result := either.Right[error](42)
|
||||
// logger.Info("result", "value", result)
|
||||
// // Logs: {"msg":"result","value":{"right":42}}
|
||||
//
|
||||
// err := either.Left[int](errors.New("failed"))
|
||||
// logger.Error("error", "value", err)
|
||||
// // Logs: {"msg":"error","value":{"left":"failed"}}
|
||||
//
|
||||
//go:noinline
|
||||
func (s Either[E, A]) LogValue() slog.Value {
|
||||
if !s.isLeft {
|
||||
return slog.GroupValue(slog.Any("right", s.r))
|
||||
}
|
||||
return slog.GroupValue(slog.Any("left", s.l))
|
||||
}
|
||||
311
v2/either/format_test.go
Normal file
311
v2/either/format_test.go
Normal file
@@ -0,0 +1,311 @@
|
||||
// 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 either
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
t.Run("Right value", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := e.String()
|
||||
assert.Equal(t, "Right[int](42)", result)
|
||||
})
|
||||
|
||||
t.Run("Left value", func(t *testing.T) {
|
||||
e := Left[int](errors.New("test error"))
|
||||
result := e.String()
|
||||
assert.Contains(t, result, "Left[*errors.errorString]")
|
||||
assert.Contains(t, result, "test error")
|
||||
})
|
||||
|
||||
t.Run("Right with string", func(t *testing.T) {
|
||||
e := Right[error]("hello")
|
||||
result := e.String()
|
||||
assert.Equal(t, "Right[string](hello)", result)
|
||||
})
|
||||
|
||||
t.Run("Left with string", func(t *testing.T) {
|
||||
e := Left[int]("error message")
|
||||
result := e.String()
|
||||
assert.Equal(t, "Left[string](error message)", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoString(t *testing.T) {
|
||||
t.Run("Right value", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := e.GoString()
|
||||
assert.Contains(t, result, "either.Right")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Left value", func(t *testing.T) {
|
||||
e := Left[int](errors.New("test error"))
|
||||
result := e.GoString()
|
||||
assert.Contains(t, result, "either.Left")
|
||||
assert.Contains(t, result, "test error")
|
||||
})
|
||||
|
||||
t.Run("Right with struct", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
e := Right[error](TestStruct{Name: "Alice", Age: 30})
|
||||
result := e.GoString()
|
||||
assert.Contains(t, result, "either.Right")
|
||||
assert.Contains(t, result, "Alice")
|
||||
assert.Contains(t, result, "30")
|
||||
})
|
||||
|
||||
t.Run("Left with custom error", func(t *testing.T) {
|
||||
e := Left[string]("custom error")
|
||||
result := e.GoString()
|
||||
assert.Contains(t, result, "either.Left")
|
||||
assert.Contains(t, result, "custom error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatInterface(t *testing.T) {
|
||||
t.Run("Right value with %s", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%s", e)
|
||||
assert.Equal(t, "Right[int](42)", result)
|
||||
})
|
||||
|
||||
t.Run("Left value with %s", func(t *testing.T) {
|
||||
e := Left[int](errors.New("test error"))
|
||||
result := fmt.Sprintf("%s", e)
|
||||
assert.Contains(t, result, "Left")
|
||||
assert.Contains(t, result, "test error")
|
||||
})
|
||||
|
||||
t.Run("Right value with %v", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%v", e)
|
||||
assert.Equal(t, "Right[int](42)", result)
|
||||
})
|
||||
|
||||
t.Run("Left value with %v", func(t *testing.T) {
|
||||
e := Left[int]("error")
|
||||
result := fmt.Sprintf("%v", e)
|
||||
assert.Equal(t, "Left[string](error)", result)
|
||||
})
|
||||
|
||||
t.Run("Right value with %+v", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%+v", e)
|
||||
assert.Contains(t, result, "Right")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Right value with %#v (GoString)", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%#v", e)
|
||||
assert.Contains(t, result, "either.Right")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Left value with %#v (GoString)", func(t *testing.T) {
|
||||
e := Left[int]("error")
|
||||
result := fmt.Sprintf("%#v", e)
|
||||
assert.Contains(t, result, "either.Left")
|
||||
assert.Contains(t, result, "error")
|
||||
})
|
||||
|
||||
t.Run("Right value with %q", func(t *testing.T) {
|
||||
e := Right[error]("hello")
|
||||
result := fmt.Sprintf("%q", e)
|
||||
// Should use String() representation
|
||||
assert.Contains(t, result, "Right")
|
||||
})
|
||||
|
||||
t.Run("Right value with %T", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
result := fmt.Sprintf("%T", e)
|
||||
assert.Contains(t, result, "either.Either")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogValue(t *testing.T) {
|
||||
t.Run("Right value", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
logValue := e.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "right", attrs[0].Key)
|
||||
assert.Equal(t, int64(42), attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Left value", func(t *testing.T) {
|
||||
e := Left[int](errors.New("test error"))
|
||||
logValue := e.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "left", attrs[0].Key)
|
||||
assert.NotNil(t, attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Right with string", func(t *testing.T) {
|
||||
e := Right[error]("success")
|
||||
logValue := e.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "right", attrs[0].Key)
|
||||
assert.Equal(t, "success", attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Left with string", func(t *testing.T) {
|
||||
e := Left[int]("error message")
|
||||
logValue := e.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "left", attrs[0].Key)
|
||||
assert.Equal(t, "error message", attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Integration with slog - Right", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
e := Right[error](42)
|
||||
logger.Info("test message", "result", e)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "test message")
|
||||
assert.Contains(t, output, "result")
|
||||
assert.Contains(t, output, "right")
|
||||
assert.Contains(t, output, "42")
|
||||
})
|
||||
|
||||
t.Run("Integration with slog - Left", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
e := Left[int]("error occurred")
|
||||
logger.Info("test message", "result", e)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "test message")
|
||||
assert.Contains(t, output, "result")
|
||||
assert.Contains(t, output, "left")
|
||||
assert.Contains(t, output, "error occurred")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatComprehensive(t *testing.T) {
|
||||
t.Run("All format verbs for Right", func(t *testing.T) {
|
||||
e := Right[error](42)
|
||||
|
||||
tests := []struct {
|
||||
verb string
|
||||
contains []string
|
||||
}{
|
||||
{"%s", []string{"Right", "42"}},
|
||||
{"%v", []string{"Right", "42"}},
|
||||
{"%+v", []string{"Right", "42"}},
|
||||
{"%#v", []string{"either.Right", "42"}},
|
||||
{"%T", []string{"either.Either"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.verb, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.verb, e)
|
||||
for _, substr := range tt.contains {
|
||||
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("All format verbs for Left", func(t *testing.T) {
|
||||
e := Left[int]("error")
|
||||
|
||||
tests := []struct {
|
||||
verb string
|
||||
contains []string
|
||||
}{
|
||||
{"%s", []string{"Left", "error"}},
|
||||
{"%v", []string{"Left", "error"}},
|
||||
{"%+v", []string{"Left", "error"}},
|
||||
{"%#v", []string{"either.Left", "error"}},
|
||||
{"%T", []string{"either.Either"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.verb, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.verb, e)
|
||||
for _, substr := range tt.contains {
|
||||
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInterfaceImplementations(t *testing.T) {
|
||||
t.Run("fmt.Stringer interface", func(t *testing.T) {
|
||||
var _ fmt.Stringer = Right[error](42)
|
||||
var _ fmt.Stringer = Left[int](errors.New("error"))
|
||||
})
|
||||
|
||||
t.Run("fmt.GoStringer interface", func(t *testing.T) {
|
||||
var _ fmt.GoStringer = Right[error](42)
|
||||
var _ fmt.GoStringer = Left[int](errors.New("error"))
|
||||
})
|
||||
|
||||
t.Run("fmt.Formatter interface", func(t *testing.T) {
|
||||
var _ fmt.Formatter = Right[error](42)
|
||||
var _ fmt.Formatter = Left[int](errors.New("error"))
|
||||
})
|
||||
|
||||
t.Run("slog.LogValuer interface", func(t *testing.T) {
|
||||
var _ slog.LogValuer = Right[error](42)
|
||||
var _ slog.LogValuer = Left[int](errors.New("error"))
|
||||
})
|
||||
}
|
||||
@@ -15,8 +15,12 @@
|
||||
|
||||
package either
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, tailrec.Trampoline[A, B]]) Kleisli[E, A, B] {
|
||||
return func(a A) Either[E, B] {
|
||||
current := f(a)
|
||||
for {
|
||||
@@ -24,11 +28,10 @@ func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
|
||||
if IsLeft(current) {
|
||||
return Left[B](e)
|
||||
}
|
||||
b, a := Unwrap(rec)
|
||||
if IsRight(rec) {
|
||||
return Right[E](b)
|
||||
if rec.Landed {
|
||||
return Right[E](rec.Land)
|
||||
}
|
||||
current = f(a)
|
||||
current = f(rec.Bounce)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
120
v2/idiomatic/context/readerresult/rec.go
Normal file
120
v2/idiomatic/context/readerresult/rec.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/idiomatic/result"
|
||||
)
|
||||
|
||||
// TailRec implements tail-recursive computation for ReaderResult with context cancellation support.
|
||||
//
|
||||
// TailRec takes a Kleisli function that returns Trampoline[A, B] and converts it into a stack-safe,
|
||||
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Land value.
|
||||
//
|
||||
// The implementation includes a short-circuit mechanism that checks for context cancellation on each
|
||||
// iteration. If the context is canceled (ctx.Err() != nil), the computation immediately returns an
|
||||
// error result containing the context's cause error, preventing unnecessary computation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The input type for the recursive step
|
||||
// - B: The final result type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Trampoline[A, B].
|
||||
// When the result is Bounce(a), recursion continues with the new value 'a'.
|
||||
// When the result is Land(b), recursion terminates with the final value 'b'.
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
|
||||
//
|
||||
// Behavior:
|
||||
// - On each iteration, checks if the context has been canceled (short circuit)
|
||||
// - If canceled, returns (zero value, context.Cause(ctx))
|
||||
// - If the step returns an error, propagates the error as (zero value, error)
|
||||
// - If the step returns Bounce(a), continues recursion with new value 'a'
|
||||
// - If the step returns Land(b), terminates with success value (b, nil)
|
||||
//
|
||||
// Example - Factorial computation with context:
|
||||
//
|
||||
// type State struct {
|
||||
// n int
|
||||
// acc int
|
||||
// }
|
||||
//
|
||||
// factorialStep := func(state State) ReaderResult[tailrec.Trampoline[State, int]] {
|
||||
// return func(ctx context.Context) (tailrec.Trampoline[State, int], error) {
|
||||
// if state.n <= 0 {
|
||||
// return tailrec.Land[State](state.acc), nil
|
||||
// }
|
||||
// return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}), nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// factorial := TailRec(factorialStep)
|
||||
// result, err := factorial(State{5, 1})(ctx) // Returns (120, nil)
|
||||
//
|
||||
// Example - Context cancellation:
|
||||
//
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// cancel() // Cancel immediately
|
||||
//
|
||||
// computation := TailRec(someStep)
|
||||
// result, err := computation(initialValue)(ctx)
|
||||
// // Returns (zero value, context.Cause(ctx)) without executing any steps
|
||||
//
|
||||
// Example - Error handling:
|
||||
//
|
||||
// errorStep := func(n int) ReaderResult[tailrec.Trampoline[int, int]] {
|
||||
// return func(ctx context.Context) (tailrec.Trampoline[int, int], error) {
|
||||
// if n == 5 {
|
||||
// return tailrec.Trampoline[int, int]{}, errors.New("computation error")
|
||||
// }
|
||||
// if n <= 0 {
|
||||
// return tailrec.Land[int](n), nil
|
||||
// }
|
||||
// return tailrec.Bounce[int](n - 1), nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// computation := TailRec(errorStep)
|
||||
// result, err := computation(10)(ctx) // Returns (0, errors.New("computation error"))
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return func(a A) ReaderResult[B] {
|
||||
initialReader := f(a)
|
||||
return func(ctx context.Context) (B, error) {
|
||||
rdr := initialReader
|
||||
for {
|
||||
// short circuit
|
||||
if ctx.Err() != nil {
|
||||
return result.Left[B](context.Cause(ctx))
|
||||
}
|
||||
rec, e := rdr(ctx)
|
||||
if e != nil {
|
||||
return result.Left[B](e)
|
||||
}
|
||||
if rec.Landed {
|
||||
return result.Of(rec.Land)
|
||||
}
|
||||
rdr = f(rec.Bounce)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
597
v2/idiomatic/context/readerresult/rec_test.go
Normal file
597
v2/idiomatic/context/readerresult/rec_test.go
Normal file
@@ -0,0 +1,597 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTailRecFactorial tests factorial computation with context
|
||||
func TestTailRecFactorial(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.n <= 0 {
|
||||
return TR.Land[State](state.acc), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.n - 1, state.acc * state.n}), nil
|
||||
}
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result, err := factorial(State{5, 1})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 120, result)
|
||||
}
|
||||
|
||||
// TestTailRecFibonacci tests Fibonacci computation
|
||||
func TestTailRecFibonacci(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
prev int
|
||||
curr int
|
||||
}
|
||||
|
||||
fibStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.n <= 0 {
|
||||
return TR.Land[State](state.curr), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}), nil
|
||||
}
|
||||
}
|
||||
|
||||
fib := TailRec(fibStep)
|
||||
result, err := fib(State{10, 0, 1})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 89, result) // 10th Fibonacci number
|
||||
}
|
||||
|
||||
// TestTailRecCountdown tests countdown computation
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecImmediateTermination tests immediate termination (Land on first call)
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
immediateStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
return TR.Land[int](n * 2), nil
|
||||
}
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result, err := immediate(42)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 84, result)
|
||||
}
|
||||
|
||||
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10000)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecSumList tests summing a list
|
||||
func TestTailRecSumList(t *testing.T) {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
sumStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if A.IsEmpty(state.list) {
|
||||
return TR.Land[State](state.sum), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}), nil
|
||||
}
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
result, err := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 15, result)
|
||||
}
|
||||
|
||||
// TestTailRecCollatzConjecture tests the Collatz conjecture
|
||||
func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
collatzStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 1 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return TR.Bounce[int](n / 2), nil
|
||||
}
|
||||
return TR.Bounce[int](3*n + 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
collatz := TailRec(collatzStep)
|
||||
result, err := collatz(10)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, result)
|
||||
}
|
||||
|
||||
// TestTailRecGCD tests greatest common divisor
|
||||
func TestTailRecGCD(t *testing.T) {
|
||||
type State struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
|
||||
gcdStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.b == 0 {
|
||||
return TR.Land[State](state.a), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.b, state.a % state.b}), nil
|
||||
}
|
||||
}
|
||||
|
||||
gcd := TailRec(gcdStep)
|
||||
result, err := gcd(State{48, 18})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, result)
|
||||
}
|
||||
|
||||
// TestTailRecErrorPropagation tests that errors are properly propagated
|
||||
func TestTailRecErrorPropagation(t *testing.T) {
|
||||
expectedErr := errors.New("computation error")
|
||||
|
||||
errorStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n == 5 {
|
||||
return TR.Trampoline[int, int]{}, expectedErr
|
||||
}
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(errorStep)
|
||||
result, err := computation(10)(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
|
||||
func TestTailRecContextCancellationImmediate(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately before execution
|
||||
|
||||
stepExecuted := false
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
stepExecuted = true
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10)(ctx)
|
||||
|
||||
// Should short circuit without executing any steps
|
||||
assert.False(t, stepExecuted, "Step should not be executed when context is already canceled")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
|
||||
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
executionCount++
|
||||
// Cancel after 3 iterations
|
||||
if executionCount == 3 {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(100)(ctx)
|
||||
|
||||
// Should stop after cancellation
|
||||
assert.Error(t, err)
|
||||
assert.LessOrEqual(t, executionCount, 4, "Should stop shortly after cancellation")
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextWithTimeout tests behavior with timeout context
|
||||
func TestTailRecContextWithTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
executionCount := 0
|
||||
slowStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
executionCount++
|
||||
// Simulate slow computation
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(slowStep)
|
||||
result, err := computation(100)(ctx)
|
||||
|
||||
// Should timeout and return error
|
||||
assert.Error(t, err)
|
||||
assert.Less(t, executionCount, 100, "Should not complete all iterations due to timeout")
|
||||
assert.Equal(t, context.DeadlineExceeded, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextWithCause tests that context.Cause is properly returned
|
||||
func TestTailRecContextWithCause(t *testing.T) {
|
||||
customErr := errors.New("custom cancellation reason")
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
cancel(customErr)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10)(ctx)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, customErr, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
|
||||
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
executionCount := 0
|
||||
maxExecutions := 5
|
||||
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
executionCount++
|
||||
if executionCount == maxExecutions {
|
||||
cancel()
|
||||
}
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(1000)(ctx)
|
||||
|
||||
// Should detect cancellation on next iteration check
|
||||
assert.Error(t, err)
|
||||
// Should stop within 1-2 iterations after cancellation
|
||||
assert.LessOrEqual(t, executionCount, maxExecutions+2)
|
||||
assert.Equal(t, context.Canceled, err)
|
||||
assert.Equal(t, 0, result) // zero value
|
||||
}
|
||||
|
||||
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
|
||||
func TestTailRecContextNotCanceled(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
executionCount := 0
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
executionCount++
|
||||
if n <= 0 {
|
||||
return TR.Land[int](n), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(10)(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 11, executionCount) // 10, 9, 8, ..., 1, 0
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecPowerOfTwo tests computing power of 2
|
||||
func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
type State struct {
|
||||
exponent int
|
||||
result int
|
||||
target int
|
||||
}
|
||||
|
||||
powerStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.exponent >= state.target {
|
||||
return TR.Land[State](state.result), nil
|
||||
}
|
||||
return TR.Bounce[int](State{state.exponent + 1, state.result * 2, state.target}), nil
|
||||
}
|
||||
}
|
||||
|
||||
power := TailRec(powerStep)
|
||||
result, err := power(State{0, 1, 10})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1024, result) // 2^10
|
||||
}
|
||||
|
||||
// TestTailRecFindInRange tests finding a value in a range
|
||||
func TestTailRecFindInRange(t *testing.T) {
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.current >= state.max {
|
||||
return TR.Land[State](-1), nil // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return TR.Land[State](state.current), nil // Found
|
||||
}
|
||||
return TR.Bounce[int](State{state.current + 1, state.max, state.target}), nil
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result, err := find(State{0, 100, 42})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
// TestTailRecFindNotInRange tests finding a value not in range
|
||||
func TestTailRecFindNotInRange(t *testing.T) {
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
target int
|
||||
}
|
||||
|
||||
findStep := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.current >= state.max {
|
||||
return TR.Land[State](-1), nil // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return TR.Land[State](state.current), nil // Found
|
||||
}
|
||||
return TR.Bounce[int](State{state.current + 1, state.max, state.target}), nil
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result, err := find(State{0, 100, 200})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, -1, result)
|
||||
}
|
||||
|
||||
// TestTailRecWithContextValue tests that context values are accessible
|
||||
func TestTailRecWithContextValue(t *testing.T) {
|
||||
type contextKey string
|
||||
const multiplierKey contextKey = "multiplier"
|
||||
|
||||
ctx := context.WithValue(context.Background(), multiplierKey, 3)
|
||||
|
||||
countdownStep := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
if n <= 0 {
|
||||
multiplier := ctx.Value(multiplierKey).(int)
|
||||
return TR.Land[int](n * multiplier), nil
|
||||
}
|
||||
return TR.Bounce[int](n - 1), nil
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result, err := countdown(5)(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result) // 0 * 3 = 0
|
||||
}
|
||||
|
||||
// TestTailRecComplexState tests with complex state structure
|
||||
func TestTailRecComplexState(t *testing.T) {
|
||||
type ComplexState struct {
|
||||
counter int
|
||||
sum int
|
||||
product int
|
||||
completed bool
|
||||
}
|
||||
|
||||
complexStep := func(state ComplexState) ReaderResult[TR.Trampoline[ComplexState, string]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[ComplexState, string], error) {
|
||||
if state.counter <= 0 || state.completed {
|
||||
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
|
||||
return TR.Land[ComplexState](result), nil
|
||||
}
|
||||
newState := ComplexState{
|
||||
counter: state.counter - 1,
|
||||
sum: state.sum + state.counter,
|
||||
product: state.product * state.counter,
|
||||
completed: state.counter == 1,
|
||||
}
|
||||
return TR.Bounce[string](newState), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(complexStep)
|
||||
result, err := computation(ComplexState{5, 0, 1, false})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "sum=15, product=120", result)
|
||||
}
|
||||
|
||||
// TestTailRecZeroIterations tests when computation terminates immediately
|
||||
func TestTailRecZeroIterations(t *testing.T) {
|
||||
step := func(n int) ReaderResult[TR.Trampoline[int, string]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, string], error) {
|
||||
return TR.Land[int]("immediate"), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(step)
|
||||
result, err := computation(0)(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "immediate", result)
|
||||
}
|
||||
|
||||
// TestTailRecErrorInFirstIteration tests error on first iteration
|
||||
func TestTailRecErrorInFirstIteration(t *testing.T) {
|
||||
expectedErr := errors.New("first iteration error")
|
||||
|
||||
step := func(n int) ReaderResult[TR.Trampoline[int, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[int, int], error) {
|
||||
return TR.Trampoline[int, int]{}, expectedErr
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(step)
|
||||
result, err := computation(10)(context.Background())
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecAlternatingBounce tests alternating between different values
|
||||
func TestTailRecAlternatingBounce(t *testing.T) {
|
||||
type State struct {
|
||||
value int
|
||||
alternate bool
|
||||
count int
|
||||
}
|
||||
|
||||
step := func(state State) ReaderResult[TR.Trampoline[State, int]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int], error) {
|
||||
if state.count >= 10 {
|
||||
return TR.Land[State](state.value), nil
|
||||
}
|
||||
newValue := state.value
|
||||
if state.alternate {
|
||||
newValue += 1
|
||||
} else {
|
||||
newValue -= 1
|
||||
}
|
||||
return TR.Bounce[int](State{newValue, !state.alternate, state.count + 1}), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(step)
|
||||
result, err := computation(State{0, true, 0})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result) // Should alternate +1, -1 and end at 0
|
||||
}
|
||||
|
||||
// TestTailRecLargeAccumulation tests accumulating large values
|
||||
func TestTailRecLargeAccumulation(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
sum int64
|
||||
}
|
||||
|
||||
step := func(state State) ReaderResult[TR.Trampoline[State, int64]] {
|
||||
return func(ctx context.Context) (TR.Trampoline[State, int64], error) {
|
||||
if state.n <= 0 {
|
||||
return TR.Land[State](state.sum), nil
|
||||
}
|
||||
return TR.Bounce[int64](State{state.n - 1, state.sum + int64(state.n)}), nil
|
||||
}
|
||||
}
|
||||
|
||||
computation := TailRec(step)
|
||||
result, err := computation(State{1000, 0})(context.Background())
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(500500), result) // Sum of 1 to 1000
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -68,4 +69,6 @@ type (
|
||||
|
||||
// Prism represents an optic that focuses on a case of type A within a sum type S.
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
Trampoline[A, B any] = tailrec.Trampoline[A, B]
|
||||
)
|
||||
|
||||
@@ -247,7 +247,7 @@ func TestOrLeft(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
orLeft := OrLeft[int, MyContext](enrichErr)
|
||||
orLeft := OrLeft[int](enrichErr)
|
||||
|
||||
v, err := F.Pipe1(Of[MyContext](42), orLeft)(defaultContext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
78
v2/internal/formatting/type.go
Normal file
78
v2/internal/formatting/type.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package formatting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Formattable is a composite interface that combines multiple formatting capabilities
|
||||
// from the Go standard library. Types implementing this interface can be formatted
|
||||
// in various contexts including string conversion, custom formatting, Go syntax
|
||||
// representation, and structured logging.
|
||||
//
|
||||
// This interface is particularly useful for types that need to provide consistent
|
||||
// formatting across different output contexts, such as logging, debugging, and
|
||||
// user-facing displays.
|
||||
//
|
||||
// Embedded Interfaces:
|
||||
//
|
||||
// - fmt.Stringer: Provides String() string method for basic string representation
|
||||
// - fmt.Formatter: Provides Format(f fmt.State, verb rune) for custom formatting with verbs like %v, %s, %+v, etc.
|
||||
// - fmt.GoStringer: Provides GoString() string method for Go-syntax representation (used with %#v)
|
||||
// - slog.LogValuer: Provides LogValue() slog.Value for structured logging with the slog package
|
||||
//
|
||||
// Example Implementation:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// }
|
||||
//
|
||||
// // String provides a simple string representation
|
||||
// func (u User) String() string {
|
||||
// return fmt.Sprintf("User(%s)", u.Name)
|
||||
// }
|
||||
//
|
||||
// // Format provides custom formatting based on the verb
|
||||
// func (u User) Format(f fmt.State, verb rune) {
|
||||
// switch verb {
|
||||
// case 'v':
|
||||
// if f.Flag('+') {
|
||||
// fmt.Fprintf(f, "User{ID: %d, Name: %s}", u.ID, u.Name)
|
||||
// } else {
|
||||
// fmt.Fprint(f, u.String())
|
||||
// }
|
||||
// case 's':
|
||||
// fmt.Fprint(f, u.String())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // GoString provides Go-syntax representation
|
||||
// func (u User) GoString() string {
|
||||
// return fmt.Sprintf("User{ID: %d, Name: %q}", u.ID, u.Name)
|
||||
// }
|
||||
//
|
||||
// // LogValue provides structured logging representation
|
||||
// func (u User) LogValue() slog.Value {
|
||||
// return slog.GroupValue(
|
||||
// slog.Int("id", u.ID),
|
||||
// slog.String("name", u.Name),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// user := User{ID: 1, Name: "Alice"}
|
||||
// fmt.Println(user) // Output: User(Alice)
|
||||
// fmt.Printf("%+v\n", user) // Output: User{ID: 1, Name: Alice}
|
||||
// fmt.Printf("%#v\n", user) // Output: User{ID: 1, Name: "Alice"}
|
||||
// slog.Info("user", "user", user) // Structured log with id and name fields
|
||||
Formattable interface {
|
||||
fmt.Stringer
|
||||
fmt.Formatter
|
||||
fmt.GoStringer
|
||||
slog.LogValuer
|
||||
}
|
||||
)
|
||||
123
v2/internal/formatting/utils.go
Normal file
123
v2/internal/formatting/utils.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package formatting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FmtString implements the fmt.Formatter interface for Formattable types.
|
||||
// It handles various format verbs to provide consistent string formatting
|
||||
// across different output contexts.
|
||||
//
|
||||
// Supported format verbs:
|
||||
// - %v: Uses String() representation (default format)
|
||||
// - %+v: Uses String() representation (verbose format)
|
||||
// - %#v: Uses GoString() representation (Go-syntax format)
|
||||
// - %s: Uses String() representation (string format)
|
||||
// - %q: Uses quoted String() representation (quoted string format)
|
||||
// - default: Uses String() representation for any other verb
|
||||
//
|
||||
// The function delegates to the appropriate method of the Formattable interface
|
||||
// based on the format verb and flags provided by fmt.State.
|
||||
//
|
||||
// Parameters:
|
||||
// - stg: The Formattable value to format
|
||||
// - f: The fmt.State that provides formatting context and flags
|
||||
// - c: The format verb (rune) being used
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// type MyType struct {
|
||||
// value int
|
||||
// }
|
||||
//
|
||||
// func (m MyType) Format(f fmt.State, verb rune) {
|
||||
// formatting.FmtString(m, f, verb)
|
||||
// }
|
||||
//
|
||||
// func (m MyType) String() string {
|
||||
// return fmt.Sprintf("MyType(%d)", m.value)
|
||||
// }
|
||||
//
|
||||
// func (m MyType) GoString() string {
|
||||
// return fmt.Sprintf("MyType{value: %d}", m.value)
|
||||
// }
|
||||
//
|
||||
// // Usage:
|
||||
// mt := MyType{value: 42}
|
||||
// fmt.Printf("%v\n", mt) // Output: MyType(42)
|
||||
// fmt.Printf("%#v\n", mt) // Output: MyType{value: 42}
|
||||
// fmt.Printf("%s\n", mt) // Output: MyType(42)
|
||||
// fmt.Printf("%q\n", mt) // Output: "MyType(42)"
|
||||
func FmtString(stg Formattable, f fmt.State, c rune) {
|
||||
switch c {
|
||||
case 'v':
|
||||
if f.Flag('#') {
|
||||
// %#v uses GoString representation
|
||||
fmt.Fprint(f, stg.GoString())
|
||||
} else {
|
||||
// %v and %+v use String representation
|
||||
fmt.Fprint(f, stg.String())
|
||||
}
|
||||
case 's':
|
||||
fmt.Fprint(f, stg.String())
|
||||
case 'q':
|
||||
fmt.Fprintf(f, "%q", stg.String())
|
||||
default:
|
||||
fmt.Fprint(f, stg.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TypeInfo returns a string representation of the type of the given value.
|
||||
// It uses reflection to determine the type and removes the leading asterisk (*)
|
||||
// from pointer types to provide a cleaner type name.
|
||||
//
|
||||
// This function is useful for generating human-readable type information in
|
||||
// string representations, particularly for generic types where the concrete
|
||||
// type needs to be displayed.
|
||||
//
|
||||
// Parameters:
|
||||
// - v: The value whose type information should be extracted
|
||||
//
|
||||
// Returns:
|
||||
// - A string representing the type name, with pointer prefix removed
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// // For non-pointer types
|
||||
// TypeInfo(42) // Returns: "int"
|
||||
// TypeInfo("hello") // Returns: "string"
|
||||
// TypeInfo([]int{1, 2, 3}) // Returns: "[]int"
|
||||
//
|
||||
// // For pointer types (asterisk is removed)
|
||||
// var ptr *int
|
||||
// TypeInfo(ptr) // Returns: "int" (not "*int")
|
||||
//
|
||||
// // For custom types
|
||||
// type MyStruct struct{ Name string }
|
||||
// TypeInfo(MyStruct{}) // Returns: "formatting.MyStruct"
|
||||
// TypeInfo(&MyStruct{}) // Returns: "formatting.MyStruct" (not "*formatting.MyStruct")
|
||||
//
|
||||
// // For interface types
|
||||
// var err error = fmt.Errorf("test")
|
||||
// TypeInfo(err) // Returns: "errors.errorString"
|
||||
func TypeInfo(v any) string {
|
||||
// Remove the leading * from pointer type
|
||||
return strings.TrimPrefix(reflect.TypeOf(v).String(), "*")
|
||||
}
|
||||
369
v2/internal/formatting/utils_test.go
Normal file
369
v2/internal/formatting/utils_test.go
Normal file
@@ -0,0 +1,369 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package formatting
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// mockFormattable is a test implementation of the Formattable interface
|
||||
type mockFormattable struct {
|
||||
stringValue string
|
||||
goStringValue string
|
||||
}
|
||||
|
||||
func (m mockFormattable) String() string {
|
||||
return m.stringValue
|
||||
}
|
||||
|
||||
func (m mockFormattable) GoString() string {
|
||||
return m.goStringValue
|
||||
}
|
||||
|
||||
func (m mockFormattable) Format(f fmt.State, verb rune) {
|
||||
FmtString(m, f, verb)
|
||||
}
|
||||
|
||||
func (m mockFormattable) LogValue() slog.Value {
|
||||
return slog.StringValue(m.stringValue)
|
||||
}
|
||||
|
||||
func TestFmtString(t *testing.T) {
|
||||
t.Run("format with %v verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%v", mock)
|
||||
assert.Equal(t, "test value", result, "Should use String() for %v")
|
||||
})
|
||||
|
||||
t.Run("format with %+v verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%+v", mock)
|
||||
assert.Equal(t, "test value", result, "Should use String() for %+v")
|
||||
})
|
||||
|
||||
t.Run("format with %#v verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%#v", mock)
|
||||
assert.Equal(t, "test.GoString", result, "Should use GoString() for %#v")
|
||||
})
|
||||
|
||||
t.Run("format with %s verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%s", mock)
|
||||
assert.Equal(t, "test value", result, "Should use String() for %s")
|
||||
})
|
||||
|
||||
t.Run("format with %q verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%q", mock)
|
||||
assert.Equal(t, `"test value"`, result, "Should use quoted String() for %q")
|
||||
})
|
||||
|
||||
t.Run("format with unsupported verb", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test value",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
// Using %d which is not a typical string verb
|
||||
result := fmt.Sprintf("%d", mock)
|
||||
assert.Equal(t, "test value", result, "Should use String() for unsupported verbs")
|
||||
})
|
||||
|
||||
t.Run("format with special characters in string", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test\nvalue\twith\rspecial",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%s", mock)
|
||||
assert.Equal(t, "test\nvalue\twith\rspecial", result)
|
||||
})
|
||||
|
||||
t.Run("format with empty string", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "",
|
||||
goStringValue: "",
|
||||
}
|
||||
result := fmt.Sprintf("%s", mock)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("format with unicode characters", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "Hello 世界 🌍",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%s", mock)
|
||||
assert.Equal(t, "Hello 世界 🌍", result)
|
||||
})
|
||||
|
||||
t.Run("format with %q and special characters", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test\nvalue",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := fmt.Sprintf("%q", mock)
|
||||
assert.Equal(t, `"test\nvalue"`, result, "Should properly escape special characters in quoted format")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTypeInfo(t *testing.T) {
|
||||
t.Run("basic types", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
expected string
|
||||
}{
|
||||
{"int", 42, "int"},
|
||||
{"string", "hello", "string"},
|
||||
{"bool", true, "bool"},
|
||||
{"float64", 3.14, "float64"},
|
||||
{"float32", float32(3.14), "float32"},
|
||||
{"int64", int64(42), "int64"},
|
||||
{"int32", int32(42), "int32"},
|
||||
{"uint", uint(42), "uint"},
|
||||
{"byte", byte(42), "uint8"},
|
||||
{"rune", rune('a'), "int32"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := TypeInfo(tt.value)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("pointer types", func(t *testing.T) {
|
||||
var intPtr *int
|
||||
result := TypeInfo(intPtr)
|
||||
assert.Equal(t, "int", result, "Should remove leading * from pointer type")
|
||||
|
||||
var strPtr *string
|
||||
result = TypeInfo(strPtr)
|
||||
assert.Equal(t, "string", result, "Should remove leading * from pointer type")
|
||||
})
|
||||
|
||||
t.Run("slice types", func(t *testing.T) {
|
||||
result := TypeInfo([]int{1, 2, 3})
|
||||
assert.Equal(t, "[]int", result)
|
||||
|
||||
result = TypeInfo([]string{"a", "b"})
|
||||
assert.Equal(t, "[]string", result)
|
||||
|
||||
result = TypeInfo([][]int{{1, 2}, {3, 4}})
|
||||
assert.Equal(t, "[][]int", result)
|
||||
})
|
||||
|
||||
t.Run("map types", func(t *testing.T) {
|
||||
result := TypeInfo(map[string]int{"a": 1})
|
||||
assert.Equal(t, "map[string]int", result)
|
||||
|
||||
result = TypeInfo(map[int]string{1: "a"})
|
||||
assert.Equal(t, "map[int]string", result)
|
||||
})
|
||||
|
||||
t.Run("struct types", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
result := TypeInfo(TestStruct{})
|
||||
assert.Equal(t, "formatting.TestStruct", result)
|
||||
|
||||
result = TypeInfo(&TestStruct{})
|
||||
assert.Equal(t, "formatting.TestStruct", result, "Should remove leading * from pointer to struct")
|
||||
})
|
||||
|
||||
t.Run("interface types", func(t *testing.T) {
|
||||
var err error = fmt.Errorf("test error")
|
||||
result := TypeInfo(err)
|
||||
assert.Contains(t, result, "errors", "Should contain package name")
|
||||
assert.NotContains(t, result, "*", "Should not contain pointer prefix")
|
||||
})
|
||||
|
||||
t.Run("channel types", func(t *testing.T) {
|
||||
ch := make(chan int)
|
||||
result := TypeInfo(ch)
|
||||
assert.Equal(t, "chan int", result)
|
||||
|
||||
ch2 := make(chan string, 10)
|
||||
result = TypeInfo(ch2)
|
||||
assert.Equal(t, "chan string", result)
|
||||
})
|
||||
|
||||
t.Run("function types", func(t *testing.T) {
|
||||
fn := func(int) string { return "" }
|
||||
result := TypeInfo(fn)
|
||||
assert.Equal(t, "func(int) string", result)
|
||||
})
|
||||
|
||||
t.Run("array types", func(t *testing.T) {
|
||||
arr := [3]int{1, 2, 3}
|
||||
result := TypeInfo(arr)
|
||||
assert.Equal(t, "[3]int", result)
|
||||
})
|
||||
|
||||
t.Run("complex types", func(t *testing.T) {
|
||||
type ComplexStruct struct {
|
||||
Data map[string][]int
|
||||
}
|
||||
result := TypeInfo(ComplexStruct{})
|
||||
assert.Equal(t, "formatting.ComplexStruct", result)
|
||||
})
|
||||
|
||||
t.Run("nil pointer", func(t *testing.T) {
|
||||
var ptr *int
|
||||
result := TypeInfo(ptr)
|
||||
assert.Equal(t, "int", result, "Should handle nil pointer correctly")
|
||||
})
|
||||
}
|
||||
|
||||
func TestTypeInfoWithCustomTypes(t *testing.T) {
|
||||
t.Run("custom type with methods", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "test",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := TypeInfo(mock)
|
||||
assert.Equal(t, "formatting.mockFormattable", result)
|
||||
})
|
||||
|
||||
t.Run("pointer to custom type", func(t *testing.T) {
|
||||
mock := &mockFormattable{
|
||||
stringValue: "test",
|
||||
goStringValue: "test.GoString",
|
||||
}
|
||||
result := TypeInfo(mock)
|
||||
assert.Equal(t, "formatting.mockFormattable", result, "Should remove pointer prefix")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFmtStringIntegration(t *testing.T) {
|
||||
t.Run("integration with fmt.Printf", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "integration test",
|
||||
goStringValue: "mock.GoString",
|
||||
}
|
||||
|
||||
// Test various format combinations
|
||||
tests := []struct {
|
||||
format string
|
||||
expected string
|
||||
}{
|
||||
{"%v", "integration test"},
|
||||
{"%+v", "integration test"},
|
||||
{"%#v", "mock.GoString"},
|
||||
{"%s", "integration test"},
|
||||
{"%q", `"integration test"`},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.format, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.format, mock)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("integration with fmt.Fprintf", func(t *testing.T) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "buffer test",
|
||||
goStringValue: "mock.GoString",
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
n, err := fmt.Fprintf((*mockWriter)(&buf), "%s", mock)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, n, 0)
|
||||
assert.Equal(t, "buffer test", string(buf))
|
||||
})
|
||||
}
|
||||
|
||||
// mockWriter is a simple writer for testing fmt.Fprintf
|
||||
type mockWriter []byte
|
||||
|
||||
func (m *mockWriter) Write(p []byte) (n int, err error) {
|
||||
*m = append(*m, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func BenchmarkFmtString(b *testing.B) {
|
||||
mock := mockFormattable{
|
||||
stringValue: "benchmark test value",
|
||||
goStringValue: "mock.GoString",
|
||||
}
|
||||
|
||||
b.Run("format with %v", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%v", mock)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("format with %#v", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%#v", mock)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("format with %s", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%s", mock)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("format with %q", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = fmt.Sprintf("%q", mock)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkTypeInfo(b *testing.B) {
|
||||
values := []any{
|
||||
42,
|
||||
"string",
|
||||
[]int{1, 2, 3},
|
||||
map[string]int{"a": 1},
|
||||
mockFormattable{},
|
||||
}
|
||||
|
||||
for _, v := range values {
|
||||
b.Run(fmt.Sprintf("%T", v), func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = TypeInfo(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,16 @@ package ioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
// TailRec creates a tail-recursive computation in the IOEither monad.
|
||||
// It enables writing recursive algorithms that don't overflow the call stack by using
|
||||
// trampolining - a technique where recursive calls are converted into iterations.
|
||||
//
|
||||
// The function takes a step function that returns either:
|
||||
// - Left(A): Continue recursion with a new value of type A
|
||||
// - Right(B): Terminate recursion with a final result of type B
|
||||
// The function takes a step function that returns a Trampoline:
|
||||
// - Bounce(A): Continue recursion with a new value of type A
|
||||
// - Land(B): Terminate recursion with a final result of type B
|
||||
//
|
||||
// This is particularly useful for implementing recursive algorithms like:
|
||||
// - Iterative calculations (factorial, fibonacci, etc.)
|
||||
@@ -34,7 +35,7 @@ import (
|
||||
// - Processing collections with early termination
|
||||
//
|
||||
// The recursion is stack-safe because each step returns a value that indicates
|
||||
// whether to continue (Left) or stop (Right), rather than making direct recursive calls.
|
||||
// whether to continue (Bounce) or stop (Land), rather than making direct recursive calls.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The error type that may occur during computation
|
||||
@@ -43,7 +44,7 @@ import (
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A step function that takes the current state (A) and returns an IOEither
|
||||
// containing either Left(A) to continue with a new state, or Right(B) to
|
||||
// containing either Bounce(A) to continue with a new state, or Land(B) to
|
||||
// terminate with a final result
|
||||
//
|
||||
// Returns:
|
||||
@@ -57,13 +58,13 @@ import (
|
||||
// result int
|
||||
// }
|
||||
//
|
||||
// factorial := TailRec(func(state FactState) IOEither[error, Either[FactState, int]] {
|
||||
// factorial := TailRec(func(state FactState) IOEither[error, tailrec.Trampoline[FactState, int]] {
|
||||
// if state.n <= 1 {
|
||||
// // Terminate with final result
|
||||
// return Of[error](either.Right[FactState](state.result))
|
||||
// return Of[error](tailrec.Land[FactState](state.result))
|
||||
// }
|
||||
// // Continue with next iteration
|
||||
// return Of[error](either.Left[int](FactState{
|
||||
// return Of[error](tailrec.Bounce[int](FactState{
|
||||
// n: state.n - 1,
|
||||
// result: state.result * state.n,
|
||||
// }))
|
||||
@@ -78,36 +79,35 @@ import (
|
||||
// sum int
|
||||
// }
|
||||
//
|
||||
// processItems := TailRec(func(state ProcessState) IOEither[error, Either[ProcessState, int]] {
|
||||
// processItems := TailRec(func(state ProcessState) IOEither[error, tailrec.Trampoline[ProcessState, int]] {
|
||||
// if len(state.items) == 0 {
|
||||
// return Of[error](either.Right[ProcessState](state.sum))
|
||||
// return Of[error](tailrec.Land[ProcessState](state.sum))
|
||||
// }
|
||||
// val, err := strconv.Atoi(state.items[0])
|
||||
// if err != nil {
|
||||
// return Left[Either[ProcessState, int]](err)
|
||||
// return Left[tailrec.Trampoline[ProcessState, int]](err)
|
||||
// }
|
||||
// return Of[error](either.Left[int](ProcessState{
|
||||
// return Of[error](tailrec.Bounce[int](ProcessState{
|
||||
// items: state.items[1:],
|
||||
// sum: state.sum + val,
|
||||
// }))
|
||||
// })
|
||||
//
|
||||
// result := processItems(ProcessState{items: []string{"1", "2", "3"}, sum: 0})() // Right(6)
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
|
||||
func TailRec[E, A, B any](f Kleisli[E, A, tailrec.Trampoline[A, B]]) Kleisli[E, A, B] {
|
||||
return func(a A) IOEither[E, B] {
|
||||
initial := f(a)
|
||||
return func() Either[E, B] {
|
||||
return func() either.Either[E, B] {
|
||||
current := initial()
|
||||
for {
|
||||
r, e := either.Unwrap(current)
|
||||
if either.IsLeft(current) {
|
||||
return either.Left[B](e)
|
||||
}
|
||||
b, a := either.Unwrap(r)
|
||||
if either.IsRight(r) {
|
||||
return either.Right[E](b)
|
||||
if r.Landed {
|
||||
return either.Right[E](r.Land)
|
||||
}
|
||||
current = f(a)()
|
||||
current = f(r.Bounce)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -32,13 +33,13 @@ func TestTailRecFactorial(t *testing.T) {
|
||||
result int
|
||||
}
|
||||
|
||||
factorial := TailRec(func(state FactState) IOEither[error, E.Either[FactState, int]] {
|
||||
factorial := TailRec(func(state FactState) IOEither[error, TR.Trampoline[FactState, int]] {
|
||||
if state.n <= 1 {
|
||||
// Terminate with final result
|
||||
return Of[error](E.Right[FactState](state.result))
|
||||
return Of[error](TR.Land[FactState](state.result))
|
||||
}
|
||||
// Continue with next iteration
|
||||
return Of[error](E.Left[int](FactState{
|
||||
return Of[error](TR.Bounce[int](FactState{
|
||||
n: state.n - 1,
|
||||
result: state.result * state.n,
|
||||
}))
|
||||
@@ -73,11 +74,11 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
curr int
|
||||
}
|
||||
|
||||
fibonacci := TailRec(func(state FibState) IOEither[error, E.Either[FibState, int]] {
|
||||
fibonacci := TailRec(func(state FibState) IOEither[error, TR.Trampoline[FibState, int]] {
|
||||
if state.n == 0 {
|
||||
return Of[error](E.Right[FibState](state.curr))
|
||||
return Of[error](TR.Land[FibState](state.curr))
|
||||
}
|
||||
return Of[error](E.Left[int](FibState{
|
||||
return Of[error](TR.Bounce[int](FibState{
|
||||
n: state.n - 1,
|
||||
prev: state.curr,
|
||||
curr: state.prev + state.curr,
|
||||
@@ -107,11 +108,11 @@ func TestTailRecSumList(t *testing.T) {
|
||||
sum int
|
||||
}
|
||||
|
||||
sumList := TailRec(func(state SumState) IOEither[error, E.Either[SumState, int]] {
|
||||
sumList := TailRec(func(state SumState) IOEither[error, TR.Trampoline[SumState, int]] {
|
||||
if A.IsEmpty(state.items) {
|
||||
return Of[error](E.Right[SumState](state.sum))
|
||||
return Of[error](TR.Land[SumState](state.sum))
|
||||
}
|
||||
return Of[error](E.Left[int](SumState{
|
||||
return Of[error](TR.Bounce[int](SumState{
|
||||
items: state.items[1:],
|
||||
sum: state.sum + state.items[0],
|
||||
}))
|
||||
@@ -141,14 +142,14 @@ func TestTailRecWithError(t *testing.T) {
|
||||
}
|
||||
|
||||
// Divide n by 2 repeatedly until it reaches 1, fail if we encounter an odd number > 1
|
||||
divideByTwo := TailRec(func(state DivState) IOEither[error, E.Either[DivState, int]] {
|
||||
divideByTwo := TailRec(func(state DivState) IOEither[error, TR.Trampoline[DivState, int]] {
|
||||
if state.n == 1 {
|
||||
return Of[error](E.Right[DivState](state.result))
|
||||
return Of[error](TR.Land[DivState](state.result))
|
||||
}
|
||||
if state.n%2 != 0 {
|
||||
return Left[E.Either[DivState, int]](fmt.Errorf("cannot divide odd number %d", state.n))
|
||||
return Left[TR.Trampoline[DivState, int]](fmt.Errorf("cannot divide odd number %d", state.n))
|
||||
}
|
||||
return Of[error](E.Left[int](DivState{
|
||||
return Of[error](TR.Bounce[int](DivState{
|
||||
n: state.n / 2,
|
||||
result: state.result + 1,
|
||||
}))
|
||||
@@ -183,11 +184,11 @@ func TestTailRecWithError(t *testing.T) {
|
||||
|
||||
// TestTailRecCountdown tests a simple countdown
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdown := TailRec(func(n int) IOEither[error, E.Either[int, string]] {
|
||||
countdown := TailRec(func(n int) IOEither[error, TR.Trampoline[int, string]] {
|
||||
if n <= 0 {
|
||||
return Of[error](E.Right[int]("Done!"))
|
||||
return Of[error](TR.Land[int]("Done!"))
|
||||
}
|
||||
return Of[error](E.Left[string](n - 1))
|
||||
return Of[error](TR.Bounce[string](n - 1))
|
||||
})
|
||||
|
||||
t.Run("countdown from 5", func(t *testing.T) {
|
||||
@@ -209,11 +210,11 @@ func TestTailRecCountdown(t *testing.T) {
|
||||
// TestTailRecStackSafety tests that TailRec doesn't overflow the stack with large iterations
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
// Count down from a large number - this would overflow the stack with regular recursion
|
||||
largeCountdown := TailRec(func(n int) IOEither[error, E.Either[int, int]] {
|
||||
largeCountdown := TailRec(func(n int) IOEither[error, TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return Of[error](E.Right[int](0))
|
||||
return Of[error](TR.Land[int](0))
|
||||
}
|
||||
return Of[error](E.Left[int](n - 1))
|
||||
return Of[error](TR.Bounce[int](n - 1))
|
||||
})
|
||||
|
||||
t.Run("large iteration count", func(t *testing.T) {
|
||||
@@ -231,14 +232,14 @@ func TestTailRecFindInList(t *testing.T) {
|
||||
index int
|
||||
}
|
||||
|
||||
findInList := TailRec(func(state FindState) IOEither[error, E.Either[FindState, int]] {
|
||||
findInList := TailRec(func(state FindState) IOEither[error, TR.Trampoline[FindState, int]] {
|
||||
if A.IsEmpty(state.items) {
|
||||
return Left[E.Either[FindState, int]](errors.New("not found"))
|
||||
return Left[TR.Trampoline[FindState, int]](errors.New("not found"))
|
||||
}
|
||||
if state.items[0] == state.target {
|
||||
return Of[error](E.Right[FindState](state.index))
|
||||
return Of[error](TR.Land[FindState](state.index))
|
||||
}
|
||||
return Of[error](E.Left[int](FindState{
|
||||
return Of[error](TR.Bounce[int](FindState{
|
||||
items: state.items[1:],
|
||||
target: state.target,
|
||||
index: state.index + 1,
|
||||
|
||||
@@ -16,18 +16,18 @@
|
||||
package iooption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
// TailRec creates a tail-recursive computation in the IOOption monad.
|
||||
// It enables writing recursive algorithms that don't overflow the call stack by using
|
||||
// an iterative loop - a technique where recursive calls are converted into iterations.
|
||||
//
|
||||
// The function takes a step function that returns an IOOption containing either:
|
||||
// The function takes a step function that returns an IOOption containing a Trampoline:
|
||||
// - None: Terminate recursion with no result
|
||||
// - Some(Left(A)): Continue recursion with a new value of type A
|
||||
// - Some(Right(B)): Terminate recursion with a final result of type B
|
||||
// - Some(Bounce(A)): Continue recursion with a new value of type A
|
||||
// - Some(Land(B)): Terminate recursion with a final result of type B
|
||||
//
|
||||
// This is particularly useful for implementing recursive algorithms that may fail at any step:
|
||||
// - Iterative calculations that may not produce a result
|
||||
@@ -45,8 +45,8 @@ import (
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A step function that takes the current state (A) and returns an IOOption
|
||||
// containing either None (failure), Some(Left(A)) to continue with a new state,
|
||||
// or Some(Right(B)) to terminate with a final result
|
||||
// containing either None (failure), Some(Bounce(A)) to continue with a new state,
|
||||
// or Some(Land(B)) to terminate with a final result
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow (function from A to IOOption[B]) that executes the
|
||||
@@ -59,17 +59,17 @@ import (
|
||||
// result int
|
||||
// }
|
||||
//
|
||||
// factorial := TailRec[any](func(state FactState) IOOption[Either[FactState, int]] {
|
||||
// factorial := TailRec[any](func(state FactState) IOOption[tailrec.Trampoline[FactState, int]] {
|
||||
// if state.n < 0 {
|
||||
// // Negative numbers have no factorial
|
||||
// return None[Either[FactState, int]]()
|
||||
// return None[tailrec.Trampoline[FactState, int]]()
|
||||
// }
|
||||
// if state.n <= 1 {
|
||||
// // Terminate with final result
|
||||
// return Of(either.Right[FactState](state.result))
|
||||
// return Of(tailrec.Land[FactState](state.result))
|
||||
// }
|
||||
// // Continue with next iteration
|
||||
// return Of(either.Left[int](FactState{
|
||||
// return Of(tailrec.Bounce[int](FactState{
|
||||
// n: state.n - 1,
|
||||
// result: state.result * state.n,
|
||||
// }))
|
||||
@@ -86,14 +86,14 @@ import (
|
||||
// steps int
|
||||
// }
|
||||
//
|
||||
// safeDivide := TailRec[any](func(state DivState) IOOption[Either[DivState, int]] {
|
||||
// safeDivide := TailRec[any](func(state DivState) IOOption[tailrec.Trampoline[DivState, int]] {
|
||||
// if state.denominator == 0 {
|
||||
// return None[Either[DivState, int]]() // Division by zero
|
||||
// return None[tailrec.Trampoline[DivState, int]]() // Division by zero
|
||||
// }
|
||||
// if state.numerator < state.denominator {
|
||||
// return Of(either.Right[DivState](state.steps))
|
||||
// return Of(tailrec.Land[DivState](state.steps))
|
||||
// }
|
||||
// return Of(either.Left[int](DivState{
|
||||
// return Of(tailrec.Bounce[int](DivState{
|
||||
// numerator: state.numerator - state.denominator,
|
||||
// denominator: state.denominator,
|
||||
// steps: state.steps + 1,
|
||||
@@ -102,21 +102,20 @@ import (
|
||||
//
|
||||
// result := safeDivide(DivState{numerator: 10, denominator: 3, steps: 0})() // Some(3)
|
||||
// result := safeDivide(DivState{numerator: 10, denominator: 0, steps: 0})() // None
|
||||
func TailRec[E, A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
|
||||
func TailRec[E, A, B any](f Kleisli[A, tailrec.Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return func(a A) IOOption[B] {
|
||||
initial := f(a)
|
||||
return func() Option[B] {
|
||||
return func() option.Option[B] {
|
||||
current := initial()
|
||||
for {
|
||||
r, ok := option.Unwrap(current)
|
||||
if !ok {
|
||||
return option.None[B]()
|
||||
}
|
||||
b, a := either.Unwrap(r)
|
||||
if either.IsRight(r) {
|
||||
return option.Some(b)
|
||||
if r.Landed {
|
||||
return option.Some(r.Land)
|
||||
}
|
||||
current = f(a)()
|
||||
current = f(r.Bounce)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ package iooption
|
||||
import (
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -30,17 +30,17 @@ func TestTailRecFactorial(t *testing.T) {
|
||||
result int
|
||||
}
|
||||
|
||||
factorial := TailRec[any](func(state FactState) IOOption[E.Either[FactState, int]] {
|
||||
factorial := TailRec[any](func(state FactState) IOOption[TR.Trampoline[FactState, int]] {
|
||||
if state.n < 0 {
|
||||
// Negative numbers have no factorial
|
||||
return None[E.Either[FactState, int]]()
|
||||
return None[TR.Trampoline[FactState, int]]()
|
||||
}
|
||||
if state.n <= 1 {
|
||||
// Terminate with final result
|
||||
return Of(E.Right[FactState](state.result))
|
||||
return Of(TR.Land[FactState](state.result))
|
||||
}
|
||||
// Continue with next iteration
|
||||
return Of(E.Left[int](FactState{
|
||||
return Of(TR.Bounce[int](FactState{
|
||||
n: state.n - 1,
|
||||
result: state.result * state.n,
|
||||
}))
|
||||
@@ -80,14 +80,14 @@ func TestTailRecSafeDivision(t *testing.T) {
|
||||
steps int
|
||||
}
|
||||
|
||||
safeDivide := TailRec[any](func(state DivState) IOOption[E.Either[DivState, int]] {
|
||||
safeDivide := TailRec[any](func(state DivState) IOOption[TR.Trampoline[DivState, int]] {
|
||||
if state.denominator == 0 {
|
||||
return None[E.Either[DivState, int]]() // Division by zero
|
||||
return None[TR.Trampoline[DivState, int]]() // Division by zero
|
||||
}
|
||||
if state.numerator < state.denominator {
|
||||
return Of(E.Right[DivState](state.steps))
|
||||
return Of(TR.Land[DivState](state.steps))
|
||||
}
|
||||
return Of(E.Left[int](DivState{
|
||||
return Of(TR.Bounce[int](DivState{
|
||||
numerator: state.numerator - state.denominator,
|
||||
denominator: state.denominator,
|
||||
steps: state.steps + 1,
|
||||
@@ -123,14 +123,14 @@ func TestTailRecFindInRange(t *testing.T) {
|
||||
max int
|
||||
}
|
||||
|
||||
findInRange := TailRec[any](func(state FindState) IOOption[E.Either[FindState, int]] {
|
||||
findInRange := TailRec[any](func(state FindState) IOOption[TR.Trampoline[FindState, int]] {
|
||||
if state.current > state.max {
|
||||
return None[E.Either[FindState, int]]() // Not found
|
||||
return None[TR.Trampoline[FindState, int]]() // Not found
|
||||
}
|
||||
if state.current == state.target {
|
||||
return Of(E.Right[FindState](state.current))
|
||||
return Of(TR.Land[FindState](state.current))
|
||||
}
|
||||
return Of(E.Left[int](FindState{
|
||||
return Of(TR.Bounce[int](FindState{
|
||||
current: state.current + 1,
|
||||
target: state.target,
|
||||
max: state.max,
|
||||
@@ -166,14 +166,14 @@ func TestTailRecSumUntilLimit(t *testing.T) {
|
||||
limit int
|
||||
}
|
||||
|
||||
sumUntilLimit := TailRec[any](func(state SumState) IOOption[E.Either[SumState, int]] {
|
||||
sumUntilLimit := TailRec[any](func(state SumState) IOOption[TR.Trampoline[SumState, int]] {
|
||||
if state.sum > state.limit {
|
||||
return None[E.Either[SumState, int]]() // Exceeded limit
|
||||
return None[TR.Trampoline[SumState, int]]() // Exceeded limit
|
||||
}
|
||||
if state.current <= 0 {
|
||||
return Of(E.Right[SumState](state.sum))
|
||||
return Of(TR.Land[SumState](state.sum))
|
||||
}
|
||||
return Of(E.Left[int](SumState{
|
||||
return Of(TR.Bounce[int](SumState{
|
||||
current: state.current - 1,
|
||||
sum: state.sum + state.current,
|
||||
limit: state.limit,
|
||||
@@ -198,14 +198,14 @@ func TestTailRecSumUntilLimit(t *testing.T) {
|
||||
|
||||
// TestTailRecCountdown tests a simple countdown with optional result
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
countdown := TailRec[any](func(n int) IOOption[E.Either[int, string]] {
|
||||
countdown := TailRec[any](func(n int) IOOption[TR.Trampoline[int, string]] {
|
||||
if n < 0 {
|
||||
return None[E.Either[int, string]]() // Negative not allowed
|
||||
return None[TR.Trampoline[int, string]]() // Negative not allowed
|
||||
}
|
||||
if n == 0 {
|
||||
return Of(E.Right[int]("Done!"))
|
||||
return Of(TR.Land[int]("Done!"))
|
||||
}
|
||||
return Of(E.Left[string](n - 1))
|
||||
return Of(TR.Bounce[string](n - 1))
|
||||
})
|
||||
|
||||
t.Run("countdown from 5", func(t *testing.T) {
|
||||
@@ -227,14 +227,14 @@ func TestTailRecCountdown(t *testing.T) {
|
||||
// TestTailRecStackSafety tests that TailRec doesn't overflow the stack with large iterations
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
// Count down from a large number - this would overflow the stack with regular recursion
|
||||
largeCountdown := TailRec[any](func(n int) IOOption[E.Either[int, int]] {
|
||||
largeCountdown := TailRec[any](func(n int) IOOption[TR.Trampoline[int, int]] {
|
||||
if n < 0 {
|
||||
return None[E.Either[int, int]]()
|
||||
return None[TR.Trampoline[int, int]]()
|
||||
}
|
||||
if n == 0 {
|
||||
return Of(E.Right[int](0))
|
||||
return Of(TR.Land[int](0))
|
||||
}
|
||||
return Of(E.Left[int](n - 1))
|
||||
return Of(TR.Bounce[int](n - 1))
|
||||
})
|
||||
|
||||
t.Run("large iteration count", func(t *testing.T) {
|
||||
@@ -252,14 +252,14 @@ func TestTailRecValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
// Validate all items are positive, return count if valid
|
||||
validatePositive := TailRec[any](func(state ValidationState) IOOption[E.Either[ValidationState, int]] {
|
||||
validatePositive := TailRec[any](func(state ValidationState) IOOption[TR.Trampoline[ValidationState, int]] {
|
||||
if state.index >= len(state.items) {
|
||||
return Of(E.Right[ValidationState](state.index))
|
||||
return Of(TR.Land[ValidationState](state.index))
|
||||
}
|
||||
if state.items[state.index] <= 0 {
|
||||
return None[E.Either[ValidationState, int]]() // Invalid item
|
||||
return None[TR.Trampoline[ValidationState, int]]() // Invalid item
|
||||
}
|
||||
return Of(E.Left[int](ValidationState{
|
||||
return Of(TR.Bounce[int](ValidationState{
|
||||
items: state.items,
|
||||
index: state.index + 1,
|
||||
}))
|
||||
@@ -294,17 +294,17 @@ func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
}
|
||||
|
||||
// Count steps to reach 1 in Collatz sequence
|
||||
collatz := TailRec[any](func(state CollatzState) IOOption[E.Either[CollatzState, int]] {
|
||||
collatz := TailRec[any](func(state CollatzState) IOOption[TR.Trampoline[CollatzState, int]] {
|
||||
if state.n <= 0 {
|
||||
return None[E.Either[CollatzState, int]]() // Invalid input
|
||||
return None[TR.Trampoline[CollatzState, int]]() // Invalid input
|
||||
}
|
||||
if state.n == 1 {
|
||||
return Of(E.Right[CollatzState](state.steps))
|
||||
return Of(TR.Land[CollatzState](state.steps))
|
||||
}
|
||||
if state.n%2 == 0 {
|
||||
return Of(E.Left[int](CollatzState{n: state.n / 2, steps: state.steps + 1}))
|
||||
return Of(TR.Bounce[int](CollatzState{n: state.n / 2, steps: state.steps + 1}))
|
||||
}
|
||||
return Of(E.Left[int](CollatzState{n: 3*state.n + 1, steps: state.steps + 1}))
|
||||
return Of(TR.Bounce[int](CollatzState{n: 3*state.n + 1, steps: state.steps + 1}))
|
||||
})
|
||||
|
||||
t.Run("collatz for 1", func(t *testing.T) {
|
||||
|
||||
@@ -17,9 +17,10 @@ package ioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
|
||||
func TailRec[A, B any](f Kleisli[A, tailrec.Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return ioeither.TailRec(f)
|
||||
}
|
||||
|
||||
488
v2/iterator/iter/bind.go
Normal file
488
v2/iterator/iter/bind.go
Normal file
@@ -0,0 +1,488 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
A "github.com/IBM/fp-go/v2/internal/apply"
|
||||
C "github.com/IBM/fp-go/v2/internal/chain"
|
||||
F "github.com/IBM/fp-go/v2/internal/functor"
|
||||
)
|
||||
|
||||
// Do creates a sequence containing a single element, typically used to start a do-notation chain.
|
||||
// This is the entry point for monadic composition using do-notation style.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The type of the state/structure being built
|
||||
//
|
||||
// Parameters:
|
||||
// - empty: The initial value to wrap in a sequence
|
||||
//
|
||||
// Returns:
|
||||
// - A sequence containing the single element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// // Start a do-notation chain
|
||||
// result := Do(User{})
|
||||
// // yields: User{Name: "", Age: 0}
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) Seq[S] {
|
||||
return Of(empty)
|
||||
}
|
||||
|
||||
// Bind performs a monadic bind operation in do-notation style, chaining a computation
|
||||
// that produces a sequence and updating the state with the result.
|
||||
//
|
||||
// This function is the core of do-notation for sequences. It takes a Kleisli arrow
|
||||
// (a function that returns a sequence) and a setter function that updates the state
|
||||
// with the result. The setter is curried to allow partial application.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of value produced by the Kleisli arrow
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: A curried function that takes a value T and returns a function that updates S1 to S2
|
||||
// - f: A Kleisli arrow that takes S1 and produces a sequence of T
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[S1] to Seq[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// Double int
|
||||
// }
|
||||
//
|
||||
// setValue := func(v int) func(State) State {
|
||||
// return func(s State) State { s.Value = v; return s }
|
||||
// }
|
||||
//
|
||||
// getValues := func(s State) Seq[int] {
|
||||
// return From(1, 2, 3)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{}),
|
||||
// Bind(setValue, getValues),
|
||||
// )
|
||||
// // yields: State{Value: 1}, State{Value: 2}, State{Value: 3}
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Operator[S1, S2] {
|
||||
return C.Bind(
|
||||
Chain[S1, S2],
|
||||
Map[T, S2],
|
||||
setter,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// Let performs a pure computation in do-notation style, updating the state with a computed value.
|
||||
//
|
||||
// Unlike Bind, Let doesn't perform a monadic operation - it simply computes a value from
|
||||
// the current state and updates the state with that value. This is useful for intermediate
|
||||
// calculations that don't require sequencing.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of the computed value
|
||||
//
|
||||
// Parameters:
|
||||
// - key: A curried function that takes a value T and returns a function that updates S1 to S2
|
||||
// - f: A function that computes T from S1
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[S1] to Seq[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// Double int
|
||||
// }
|
||||
//
|
||||
// setDouble := func(d int) func(State) State {
|
||||
// return func(s State) State { s.Double = d; return s }
|
||||
// }
|
||||
//
|
||||
// computeDouble := func(s State) int {
|
||||
// return s.Value * 2
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Value: 5}),
|
||||
// Let(setDouble, computeDouble),
|
||||
// )
|
||||
// // yields: State{Value: 5, Double: 10}
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
key func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) Operator[S1, S2] {
|
||||
return F.Let(
|
||||
Map[S1, S2],
|
||||
key,
|
||||
f,
|
||||
)
|
||||
}
|
||||
|
||||
// LetTo sets a field in the state to a constant value in do-notation style.
|
||||
//
|
||||
// This is a specialized version of Let that doesn't compute the value from the state,
|
||||
// but instead uses a fixed value. It's useful for setting constants or default values.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of the value to set
|
||||
//
|
||||
// Parameters:
|
||||
// - key: A curried function that takes a value T and returns a function that updates S1 to S2
|
||||
// - b: The constant value to set
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[S1] to Seq[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Status string
|
||||
// }
|
||||
//
|
||||
// setStatus := func(s string) func(State) State {
|
||||
// return func(st State) State { st.Status = s; return st }
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Name: "Alice"}),
|
||||
// LetTo(setStatus, "active"),
|
||||
// )
|
||||
// // yields: State{Name: "Alice", Status: "active"}
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
key func(T) func(S1) S2,
|
||||
b T,
|
||||
) Operator[S1, S2] {
|
||||
return F.LetTo(
|
||||
Map[S1, S2],
|
||||
key,
|
||||
b,
|
||||
)
|
||||
}
|
||||
|
||||
// BindTo wraps a value into a structure using a setter function.
|
||||
//
|
||||
// This is typically used at the beginning of a do-notation chain to convert a simple
|
||||
// value into a structured state. It's the inverse of extracting a value from a structure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The structure type to create
|
||||
// - T: The value type to wrap
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: A function that takes a value T and creates a structure S1
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[T] to Seq[S1]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// createState := func(v int) State {
|
||||
// return State{Value: v}
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// From(1, 2, 3),
|
||||
// BindTo(createState),
|
||||
// )
|
||||
// // yields: State{Value: 1}, State{Value: 2}, State{Value: 3}
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Operator[T, S1] {
|
||||
return C.BindTo(
|
||||
Map[T, S1],
|
||||
setter,
|
||||
)
|
||||
}
|
||||
|
||||
// BindToP wraps a value into a structure using a Prism's ReverseGet function.
|
||||
//
|
||||
// This is a specialized version of BindTo that works with Prisms (optics that focus
|
||||
// on a case of a sum type). It uses the Prism's ReverseGet to construct the structure.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The structure type to create
|
||||
// - T: The value type to wrap
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: A Prism that can construct S1 from T
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[T] to Seq[S1]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Assuming a Prism for wrapping int into a Result type
|
||||
// result := F.Pipe2(
|
||||
// From(1, 2, 3),
|
||||
// BindToP(successPrism),
|
||||
// )
|
||||
// // yields: Success(1), Success(2), Success(3)
|
||||
//
|
||||
//go:inline
|
||||
func BindToP[S1, T any](
|
||||
setter Prism[S1, T],
|
||||
) Operator[T, S1] {
|
||||
return BindTo(setter.ReverseGet)
|
||||
}
|
||||
|
||||
// ApS applies a sequence of values to update a state using applicative style.
|
||||
//
|
||||
// This function combines applicative application with state updates. It takes a sequence
|
||||
// of values and a setter function, and produces an operator that applies each value
|
||||
// to update the state. This is useful for parallel composition of independent computations.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S1: The input state type
|
||||
// - S2: The output state type
|
||||
// - T: The type of values in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - setter: A curried function that takes a value T and returns a function that updates S1 to S2
|
||||
// - fa: A sequence of values to apply
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms Seq[S1] to Seq[S2]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// X int
|
||||
// Y int
|
||||
// }
|
||||
//
|
||||
// setY := func(y int) func(State) State {
|
||||
// return func(s State) State { s.Y = y; return s }
|
||||
// }
|
||||
//
|
||||
// yValues := From(10, 20, 30)
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{X: 5}),
|
||||
// ApS(setY, yValues),
|
||||
// )
|
||||
// // yields: State{X: 5, Y: 10}, State{X: 5, Y: 20}, State{X: 5, Y: 30}
|
||||
//
|
||||
//go:inline
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa Seq[T],
|
||||
) Operator[S1, S2] {
|
||||
return A.ApS(
|
||||
Ap[S2, T],
|
||||
Map[S1, func(T) S2],
|
||||
setter,
|
||||
fa,
|
||||
)
|
||||
}
|
||||
|
||||
// ApSL applies a sequence of values to update a state field using a Lens.
|
||||
//
|
||||
// This is a specialized version of ApS that works with Lenses (optics that focus on
|
||||
// a field of a structure). It uses the Lens's Set function to update the field.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - T: The type of the field being updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens focusing on the field to update
|
||||
// - fa: A sequence of values to set
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism on Seq[S] (transforms Seq[S] to Seq[S])
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Age int
|
||||
// }
|
||||
//
|
||||
// ageLens := lens.Prop[State, int]("Age")
|
||||
// ages := From(25, 30, 35)
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Name: "Alice"}),
|
||||
// ApSL(ageLens, ages),
|
||||
// )
|
||||
// // yields: State{Name: "Alice", Age: 25}, State{Name: "Alice", Age: 30}, State{Name: "Alice", Age: 35}
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens Lens[S, T],
|
||||
fa Seq[T],
|
||||
) Endomorphism[Seq[S]] {
|
||||
return ApS(lens.Set, fa)
|
||||
}
|
||||
|
||||
// BindL performs a monadic bind on a field of a structure using a Lens.
|
||||
//
|
||||
// This function combines Lens-based field access with monadic binding. It extracts
|
||||
// a field value using the Lens's Get, applies a Kleisli arrow to produce a sequence,
|
||||
// and updates the field with each result using the Lens's Set.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - T: The type of the field being accessed and updated
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens focusing on the field to bind
|
||||
// - f: A Kleisli arrow that takes the field value and produces a sequence
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism on Seq[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// valueLens := lens.Prop[State, int]("Value")
|
||||
//
|
||||
// multiplyValues := func(v int) Seq[int] {
|
||||
// return From(v, v*2, v*3)
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Value: 5}),
|
||||
// BindL(valueLens, multiplyValues),
|
||||
// )
|
||||
// // yields: State{Value: 5}, State{Value: 10}, State{Value: 15}
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Endomorphism[Seq[S]] {
|
||||
return Bind(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetL performs a pure computation on a field of a structure using a Lens.
|
||||
//
|
||||
// This function extracts a field value using the Lens's Get, applies a pure function
|
||||
// to compute a new value, and updates the field using the Lens's Set. It's useful
|
||||
// for transforming fields without monadic effects.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - T: The type of the field being transformed
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens focusing on the field to transform
|
||||
// - f: An Endomorphism that transforms the field value
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism on Seq[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Count int
|
||||
// }
|
||||
//
|
||||
// countLens := lens.Prop[State, int]("Count")
|
||||
//
|
||||
// increment := func(n int) int { return n + 1 }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Count: 5}),
|
||||
// LetL(countLens, increment),
|
||||
// )
|
||||
// // yields: State{Count: 6}
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens Lens[S, T],
|
||||
f Endomorphism[T],
|
||||
) Endomorphism[Seq[S]] {
|
||||
return Let(lens.Set, function.Flow2(lens.Get, f))
|
||||
}
|
||||
|
||||
// LetToL sets a field of a structure to a constant value using a Lens.
|
||||
//
|
||||
// This is a specialized version of LetL that sets a field to a fixed value rather
|
||||
// than computing it from the current value. It's useful for setting defaults or
|
||||
// resetting fields.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The state type
|
||||
// - T: The type of the field being set
|
||||
//
|
||||
// Parameters:
|
||||
// - lens: A Lens focusing on the field to set
|
||||
// - b: The constant value to set
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism on Seq[S]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Status string
|
||||
// }
|
||||
//
|
||||
// statusLens := lens.Prop[State, string]("Status")
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// Do(State{Status: "pending"}),
|
||||
// LetToL(statusLens, "active"),
|
||||
// )
|
||||
// // yields: State{Status: "active"}
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens Lens[S, T],
|
||||
b T,
|
||||
) Endomorphism[Seq[S]] {
|
||||
return LetTo(lens.Set, b)
|
||||
}
|
||||
741
v2/iterator/iter/bind_test.go
Normal file
741
v2/iterator/iter/bind_test.go
Normal file
@@ -0,0 +1,741 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Value int
|
||||
Double int
|
||||
Status string
|
||||
}
|
||||
|
||||
// TestDo tests the Do function
|
||||
func TestDo(t *testing.T) {
|
||||
t.Run("creates sequence with single element", func(t *testing.T) {
|
||||
result := Do(42)
|
||||
values := slices.Collect(result)
|
||||
assert.Equal(t, A.Of(42), values)
|
||||
})
|
||||
|
||||
t.Run("creates sequence with struct", func(t *testing.T) {
|
||||
user := User{Name: "Alice", Age: 30}
|
||||
result := Do(user)
|
||||
values := slices.Collect(result)
|
||||
assert.Equal(t, A.Of(user), values)
|
||||
})
|
||||
|
||||
t.Run("creates sequence with zero value", func(t *testing.T) {
|
||||
result := Do(State{})
|
||||
values := slices.Collect(result)
|
||||
assert.Equal(t, []State{{Value: 0, Double: 0, Status: ""}}, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestBind tests the Bind function
|
||||
func TestBind(t *testing.T) {
|
||||
t.Run("binds simple value", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(1, 2, 3)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, getValues)
|
||||
result := bindOp(Do(State{}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 1},
|
||||
{Value: 2},
|
||||
{Value: 3},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("chains multiple binds", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(5)
|
||||
}
|
||||
|
||||
computeDouble := func(s State) Seq[int] {
|
||||
return From(s.Value * 2)
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
Bind(setValue, getValues),
|
||||
Bind(setDouble, computeDouble),
|
||||
)(Do(State{}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 5, Double: 10}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("binds with multiple results", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
multiplyValues := func(s State) Seq[int] {
|
||||
return From(s.Value, s.Value*2, s.Value*3)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, multiplyValues)
|
||||
result := bindOp(Do(State{Value: 2}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 2},
|
||||
{Value: 4},
|
||||
{Value: 6},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("binds with empty sequence", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
emptySeq := func(s State) Seq[int] {
|
||||
return Empty[int]()
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, emptySeq)
|
||||
result := bindOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
assert.Empty(t, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLet tests the Let function
|
||||
func TestLet(t *testing.T) {
|
||||
t.Run("computes value from state", func(t *testing.T) {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
letOp := Let(setDouble, computeDouble)
|
||||
result := letOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 5, Double: 10}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("chains multiple lets", func(t *testing.T) {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
computeValue := func(s State) int {
|
||||
return s.Double / 2
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
Let(setDouble, computeDouble),
|
||||
Let(setValue, computeValue),
|
||||
)(Do(State{Value: 7}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 7, Double: 14}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("computes complex transformation", func(t *testing.T) {
|
||||
setStatus := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Status = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
computeStatus := func(s State) string {
|
||||
if s.Value > 10 {
|
||||
return "high"
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
letOp := Let(setStatus, computeStatus)
|
||||
result := letOp(Do(State{Value: 15}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 15, Status: "high"}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLetTo tests the LetTo function
|
||||
func TestLetTo(t *testing.T) {
|
||||
t.Run("sets constant value", func(t *testing.T) {
|
||||
setStatus := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Status = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
letToOp := LetTo(setStatus, "active")
|
||||
result := letToOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 5, Status: "active"}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("chains multiple LetTo calls", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setStatus := func(st string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Status = st
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
LetTo(setValue, 42),
|
||||
LetTo(setStatus, "ready"),
|
||||
)(Do(State{}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 42, Status: "ready"}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("sets zero value", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
letToOp := LetTo(setValue, 0)
|
||||
result := letToOp(Do(State{Value: 100}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{{Value: 0}}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestBindTo tests the BindTo function
|
||||
func TestBindTo(t *testing.T) {
|
||||
t.Run("wraps values into structure", func(t *testing.T) {
|
||||
createState := func(v int) State {
|
||||
return State{Value: v}
|
||||
}
|
||||
|
||||
bindToOp := BindTo(createState)
|
||||
result := bindToOp(From(1, 2, 3))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 1},
|
||||
{Value: 2},
|
||||
{Value: 3},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("wraps into complex structure", func(t *testing.T) {
|
||||
createUser := func(name string) User {
|
||||
return User{Name: name, Age: 0}
|
||||
}
|
||||
|
||||
bindToOp := BindTo(createUser)
|
||||
result := bindToOp(From("Alice", "Bob", "Charlie"))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []User{
|
||||
{Name: "Alice", Age: 0},
|
||||
{Name: "Bob", Age: 0},
|
||||
{Name: "Charlie", Age: 0},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("wraps empty sequence", func(t *testing.T) {
|
||||
createState := func(v int) State {
|
||||
return State{Value: v}
|
||||
}
|
||||
|
||||
bindToOp := BindTo(createState)
|
||||
result := bindToOp(Empty[int]())
|
||||
|
||||
values := slices.Collect(result)
|
||||
assert.Empty(t, values)
|
||||
})
|
||||
}
|
||||
|
||||
// TestApS tests the ApS function
|
||||
func TestApS(t *testing.T) {
|
||||
t.Run("applies sequence of values", func(t *testing.T) {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
doubles := From(10, 20, 30)
|
||||
|
||||
apOp := ApS(setDouble, doubles)
|
||||
result := apOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 5, Double: 10},
|
||||
{Value: 5, Double: 20},
|
||||
{Value: 5, Double: 30},
|
||||
}
|
||||
assert.Equal(t, expected, values)
|
||||
})
|
||||
|
||||
t.Run("applies with empty sequence", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
apOp := ApS(setValue, Empty[int]())
|
||||
result := apOp(Do(State{Value: 5}))
|
||||
|
||||
values := slices.Collect(result)
|
||||
assert.Empty(t, values)
|
||||
})
|
||||
|
||||
t.Run("chains multiple ApS calls", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
values := From(1, 2)
|
||||
doubles := From(10, 20)
|
||||
|
||||
result := F.Flow2(
|
||||
ApS(setValue, values),
|
||||
ApS(setDouble, doubles),
|
||||
)(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
// Cartesian product: 2 values × 2 doubles = 4 results
|
||||
assert.Len(t, results, 4)
|
||||
})
|
||||
}
|
||||
|
||||
// TestDoNotationChain tests a complete do-notation chain
|
||||
func TestDoNotationChain(t *testing.T) {
|
||||
t.Run("complex do-notation chain", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setStatus := func(st string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Status = st
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(5, 10)
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
result := F.Flow3(
|
||||
Bind(setValue, getValues),
|
||||
Let(setDouble, computeDouble),
|
||||
LetTo(setStatus, "computed"),
|
||||
)(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 5, Double: 10, Status: "computed"},
|
||||
{Value: 10, Double: 20, Status: "computed"},
|
||||
}
|
||||
assert.Equal(t, expected, results)
|
||||
})
|
||||
|
||||
t.Run("mixed bind and let operations", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getInitial := func(s State) Seq[int] {
|
||||
return From(3)
|
||||
}
|
||||
|
||||
multiplyValue := func(s State) Seq[int] {
|
||||
return From(s.Value*2, s.Value*3)
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
Bind(setValue, getInitial),
|
||||
Bind(setDouble, multiplyValue),
|
||||
)(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
expected := []State{
|
||||
{Value: 3, Double: 6},
|
||||
{Value: 3, Double: 9},
|
||||
}
|
||||
assert.Equal(t, expected, results)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEdgeCases tests edge cases
|
||||
func TestEdgeCases(t *testing.T) {
|
||||
t.Run("bind with single element", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getSingle := func(s State) Seq[int] {
|
||||
return From(42)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, getSingle)
|
||||
result := bindOp(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
expected := []State{{Value: 42}}
|
||||
assert.Equal(t, expected, results)
|
||||
})
|
||||
|
||||
t.Run("multiple binds with cartesian product", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(1, 2)
|
||||
}
|
||||
|
||||
getDoubles := func(s State) Seq[int] {
|
||||
return From(10, 20)
|
||||
}
|
||||
|
||||
result := F.Flow2(
|
||||
Bind(setValue, getValues),
|
||||
Bind(setDouble, getDoubles),
|
||||
)(Do(State{}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
// Should produce cartesian product: 2 × 2 = 4 results
|
||||
assert.Len(t, results, 4)
|
||||
})
|
||||
|
||||
t.Run("let with identity function", func(t *testing.T) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
identity := func(s State) int {
|
||||
return s.Value
|
||||
}
|
||||
|
||||
letOp := Let(setValue, identity)
|
||||
result := letOp(Do(State{Value: 99}))
|
||||
|
||||
results := slices.Collect(result)
|
||||
expected := []State{{Value: 99}}
|
||||
assert.Equal(t, expected, results)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkBind(b *testing.B) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(1, 2, 3, 4, 5)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, getValues)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := bindOp(Do(State{}))
|
||||
// Consume the sequence
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkLet(b *testing.B) {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
letOp := Let(setDouble, computeDouble)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := letOp(Do(State{Value: 5}))
|
||||
// Consume the sequence
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDoNotationChain(b *testing.B) {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
setStatus := func(st string) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Status = st
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(5, 10, 15)
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
chain := F.Flow3(
|
||||
Bind(setValue, getValues),
|
||||
Let(setDouble, computeDouble),
|
||||
LetTo(setStatus, "computed"),
|
||||
)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := chain(Do(State{}))
|
||||
// Consume the sequence
|
||||
for range result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleDo() {
|
||||
result := Do(42)
|
||||
for v := range result {
|
||||
fmt.Println(v)
|
||||
}
|
||||
// Output: 42
|
||||
}
|
||||
|
||||
func ExampleBind() {
|
||||
setValue := func(v int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Value = v
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
getValues := func(s State) Seq[int] {
|
||||
return From(1, 2, 3)
|
||||
}
|
||||
|
||||
bindOp := Bind(setValue, getValues)
|
||||
result := bindOp(Do(State{}))
|
||||
|
||||
for s := range result {
|
||||
fmt.Printf("Value: %d\n", s.Value)
|
||||
}
|
||||
// Output:
|
||||
// Value: 1
|
||||
// Value: 2
|
||||
// Value: 3
|
||||
}
|
||||
|
||||
func ExampleLet() {
|
||||
setDouble := func(d int) func(State) State {
|
||||
return func(s State) State {
|
||||
s.Double = d
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
computeDouble := func(s State) int {
|
||||
return s.Value * 2
|
||||
}
|
||||
|
||||
letOp := Let(setDouble, computeDouble)
|
||||
result := letOp(Do(State{Value: 5}))
|
||||
|
||||
for s := range result {
|
||||
fmt.Printf("Value: %d, Double: %d\n", s.Value, s.Double)
|
||||
}
|
||||
// Output: Value: 5, Double: 10
|
||||
}
|
||||
|
||||
func ExampleLetTo() {
|
||||
setStatus := func(s string) func(State) State {
|
||||
return func(st State) State {
|
||||
st.Status = s
|
||||
return st
|
||||
}
|
||||
}
|
||||
|
||||
letToOp := LetTo(setStatus, "active")
|
||||
result := letToOp(Do(State{Value: 5}))
|
||||
|
||||
for s := range result {
|
||||
fmt.Printf("Value: %d, Status: %s\n", s.Value, s.Status)
|
||||
}
|
||||
// Output: Value: 5, Status: active
|
||||
}
|
||||
84
v2/iterator/iter/compress.go
Normal file
84
v2/iterator/iter/compress.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Compress filters elements from a sequence based on a corresponding sequence of boolean selectors.
|
||||
//
|
||||
// This function takes a sequence of boolean values and returns an operator that filters
|
||||
// elements from the input sequence. An element is included in the output if and only if
|
||||
// the corresponding boolean selector is true. The filtering stops when either sequence
|
||||
// is exhausted.
|
||||
//
|
||||
// The implementation works by:
|
||||
// 1. Zipping the input sequence with the selector sequence
|
||||
// 2. Converting the Seq2 to a sequence of Pairs
|
||||
// 3. Filtering to keep only pairs where the boolean (tail) is true
|
||||
// 4. Extracting the original values (head) from the filtered pairs
|
||||
//
|
||||
// RxJS Equivalent: Similar to combining [zip] with [filter] - https://rxjs.dev/api/operators/zip
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence to be filtered
|
||||
//
|
||||
// Parameters:
|
||||
// - sel: A sequence of boolean values used as selectors
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters elements based on the selector sequence
|
||||
//
|
||||
// Example - Basic filtering:
|
||||
//
|
||||
// data := iter.From(1, 2, 3, 4, 5)
|
||||
// selectors := iter.From(true, false, true, false, true)
|
||||
// filtered := iter.Compress(selectors)(data)
|
||||
// // yields: 1, 3, 5
|
||||
//
|
||||
// Example - Shorter selector sequence:
|
||||
//
|
||||
// data := iter.From("a", "b", "c", "d", "e")
|
||||
// selectors := iter.From(true, true, false)
|
||||
// filtered := iter.Compress(selectors)(data)
|
||||
// // yields: "a", "b" (stops when selectors are exhausted)
|
||||
//
|
||||
// Example - All false selectors:
|
||||
//
|
||||
// data := iter.From(1, 2, 3)
|
||||
// selectors := iter.From(false, false, false)
|
||||
// filtered := iter.Compress(selectors)(data)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - All true selectors:
|
||||
//
|
||||
// data := iter.From(10, 20, 30)
|
||||
// selectors := iter.From(true, true, true)
|
||||
// filtered := iter.Compress(selectors)(data)
|
||||
// // yields: 10, 20, 30 (all elements pass through)
|
||||
func Compress[U any](sel Seq[bool]) Operator[U, U] {
|
||||
return F.Flow3(
|
||||
Zip[U](sel),
|
||||
ToSeqPair[U, bool],
|
||||
FilterMap(F.Flow2(
|
||||
O.FromPredicate(P.Tail[U, bool]),
|
||||
O.Map(P.Head[U, bool]),
|
||||
)),
|
||||
)
|
||||
}
|
||||
365
v2/iterator/iter/compress_test.go
Normal file
365
v2/iterator/iter/compress_test.go
Normal file
@@ -0,0 +1,365 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCompress tests the Compress function
|
||||
func TestCompress(t *testing.T) {
|
||||
t.Run("filters with alternating selectors", func(t *testing.T) {
|
||||
data := From(1, 2, 3, 4, 5)
|
||||
selectors := From(true, false, true, false, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{1, 3, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("filters strings with boolean selectors", func(t *testing.T) {
|
||||
data := From("a", "b", "c", "d", "e")
|
||||
selectors := From(true, true, false, false, true)
|
||||
filtered := Compress[string](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []string{"a", "b", "e"}, result)
|
||||
})
|
||||
|
||||
t.Run("all true selectors pass all elements", func(t *testing.T) {
|
||||
data := From(10, 20, 30)
|
||||
selectors := From(true, true, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{10, 20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("all false selectors produce empty sequence", func(t *testing.T) {
|
||||
data := From(1, 2, 3)
|
||||
selectors := From(false, false, false)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("shorter selector sequence stops early", func(t *testing.T) {
|
||||
data := From("a", "b", "c", "d", "e")
|
||||
selectors := From(true, true, false)
|
||||
filtered := Compress[string](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []string{"a", "b"}, result)
|
||||
})
|
||||
|
||||
t.Run("shorter data sequence stops early", func(t *testing.T) {
|
||||
data := From(1, 2, 3)
|
||||
selectors := From(true, false, true, true, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{1, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("empty data sequence produces empty result", func(t *testing.T) {
|
||||
data := Empty[int]()
|
||||
selectors := From(true, true, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty selector sequence produces empty result", func(t *testing.T) {
|
||||
data := From(1, 2, 3)
|
||||
selectors := Empty[bool]()
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("both empty sequences produce empty result", func(t *testing.T) {
|
||||
data := Empty[int]()
|
||||
selectors := Empty[bool]()
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("single element with true selector", func(t *testing.T) {
|
||||
data := From(42)
|
||||
selectors := From(true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("single element with false selector", func(t *testing.T) {
|
||||
data := From(42)
|
||||
selectors := From(false)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCompressWithComplexTypes tests Compress with complex data types
|
||||
func TestCompressWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("filters struct values", func(t *testing.T) {
|
||||
data := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
selectors := From(true, false, true, false)
|
||||
filtered := Compress[Person](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Charlie", 35},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("filters pointer values", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
data := From(p1, p2, p3)
|
||||
selectors := From(false, true, true)
|
||||
filtered := Compress[*Person](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []*Person{p2, p3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCompressWithChainedOperations tests Compress with other operations
|
||||
func TestCompressWithChainedOperations(t *testing.T) {
|
||||
t.Run("compress then map", func(t *testing.T) {
|
||||
data := From(1, 2, 3, 4, 5)
|
||||
selectors := From(true, false, true, false, true)
|
||||
result := toSlice(
|
||||
MonadMap(
|
||||
Compress[int](selectors)(data),
|
||||
func(x int) int { return x * 10 },
|
||||
),
|
||||
)
|
||||
assert.Equal(t, []int{10, 30, 50}, result)
|
||||
})
|
||||
|
||||
t.Run("map then compress", func(t *testing.T) {
|
||||
data := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(data, func(x int) int { return x * 2 })
|
||||
selectors := From(true, true, false, false, true)
|
||||
result := toSlice(Compress[int](selectors)(mapped))
|
||||
assert.Equal(t, []int{2, 4, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("compress with filtered data", func(t *testing.T) {
|
||||
data := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
evens := MonadFilter(data, func(x int) bool { return x%2 == 0 })
|
||||
selectors := From(true, false, true, false, true)
|
||||
result := toSlice(Compress[int](selectors)(evens))
|
||||
assert.Equal(t, []int{2, 6, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPair tests the ToSeqPair function
|
||||
func TestToSeqPair(t *testing.T) {
|
||||
t.Run("converts Seq2 to sequence of pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From("a", "b", "c"), From(1, 2, 3))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
expected := []Pair[string, int]{
|
||||
P.MakePair("a", 1),
|
||||
P.MakePair("b", 2),
|
||||
P.MakePair("c", 3),
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("converts empty Seq2", func(t *testing.T) {
|
||||
seq2 := MonadZip(Empty[int](), Empty[string]())
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("converts single pair", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(42), From("answer"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
expected := []Pair[int, string]{
|
||||
P.MakePair(42, "answer"),
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("stops at shorter sequence", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3, 4, 5), From("a", "b"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
result := toSlice(pairs)
|
||||
expected := []Pair[int, string]{
|
||||
P.MakePair(1, "a"),
|
||||
P.MakePair(2, "b"),
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestToSeqPairWithOperations tests ToSeqPair with other operations
|
||||
func TestToSeqPairWithOperations(t *testing.T) {
|
||||
t.Run("map over pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3), From(10, 20, 30))
|
||||
pairs := ToSeqPair(seq2)
|
||||
sums := MonadMap(pairs, func(p Pair[int, int]) int {
|
||||
return P.Head(p) + P.Tail(p)
|
||||
})
|
||||
result := toSlice(sums)
|
||||
assert.Equal(t, []int{11, 22, 33}, result)
|
||||
})
|
||||
|
||||
t.Run("filter pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3, 4, 5), From(10, 20, 30, 40, 50))
|
||||
pairs := ToSeqPair(seq2)
|
||||
filtered := MonadFilter(pairs, func(p Pair[int, int]) bool {
|
||||
return P.Head(p)%2 == 0
|
||||
})
|
||||
result := toSlice(filtered)
|
||||
expected := []Pair[int, int]{
|
||||
P.MakePair(2, 20),
|
||||
P.MakePair(4, 40),
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("extract first elements from pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3), From("x", "y", "z"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
firsts := MonadMap(pairs, func(p Pair[int, string]) int {
|
||||
return P.Head(p)
|
||||
})
|
||||
result := toSlice(firsts)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("extract second elements from pairs", func(t *testing.T) {
|
||||
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
seconds := MonadMap(pairs, func(p Pair[int, string]) string {
|
||||
return P.Tail(p)
|
||||
})
|
||||
result := toSlice(seconds)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCompressAndToSeqPairTogether tests using both functions together
|
||||
func TestCompressAndToSeqPairTogether(t *testing.T) {
|
||||
t.Run("compress uses ToSeqPair internally", func(t *testing.T) {
|
||||
// This test verifies the integration works correctly
|
||||
data := From(10, 20, 30, 40, 50)
|
||||
selectors := From(true, false, true, true, false)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{10, 30, 40}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkCompress(b *testing.B) {
|
||||
data := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
selectors := From(true, false, true, false, true, false, true, false, true, false)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filtered := Compress[int](selectors)(data)
|
||||
for range filtered {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToSeqPair(b *testing.B) {
|
||||
seq2 := MonadZip(From(1, 2, 3, 4, 5), From("a", "b", "c", "d", "e"))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
pairs := ToSeqPair(seq2)
|
||||
for range pairs {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleCompress() {
|
||||
data := From(1, 2, 3, 4, 5)
|
||||
selectors := From(true, false, true, false, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
|
||||
for v := range filtered {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 3 5
|
||||
}
|
||||
|
||||
func ExampleCompress_allTrue() {
|
||||
data := From(10, 20, 30)
|
||||
selectors := From(true, true, true)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
|
||||
for v := range filtered {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 10 20 30
|
||||
}
|
||||
|
||||
func ExampleCompress_allFalse() {
|
||||
data := From(1, 2, 3)
|
||||
selectors := From(false, false, false)
|
||||
filtered := Compress[int](selectors)(data)
|
||||
|
||||
count := 0
|
||||
for range filtered {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleToSeqPair() {
|
||||
seq2 := MonadZip(From(1, 2, 3), From("a", "b", "c"))
|
||||
pairs := ToSeqPair(seq2)
|
||||
|
||||
for p := range pairs {
|
||||
fmt.Printf("(%d, %s) ", P.Head(p), P.Tail(p))
|
||||
}
|
||||
// Output: (1, a) (2, b) (3, c)
|
||||
}
|
||||
|
||||
func ExampleToSeqPair_withMap() {
|
||||
seq2 := MonadZip(From(1, 2, 3), From(10, 20, 30))
|
||||
pairs := ToSeqPair(seq2)
|
||||
sums := MonadMap(pairs, func(p Pair[int, int]) int {
|
||||
return P.Head(p) + P.Tail(p)
|
||||
})
|
||||
|
||||
for sum := range sums {
|
||||
fmt.Printf("%d ", sum)
|
||||
}
|
||||
// Output: 11 22 33
|
||||
}
|
||||
96
v2/iterator/iter/cycle.go
Normal file
96
v2/iterator/iter/cycle.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
// Cycle creates a sequence that repeats the elements of the input sequence indefinitely.
|
||||
//
|
||||
// This function takes a finite sequence and creates an infinite sequence by cycling through
|
||||
// all elements repeatedly. When the end of the input sequence is reached, it starts over
|
||||
// from the beginning, continuing this pattern forever.
|
||||
//
|
||||
// RxJS Equivalent: [repeat] - https://rxjs.dev/api/operators/repeat
|
||||
//
|
||||
// WARNING: This creates an INFINITE sequence for non-empty inputs. It must be used with
|
||||
// operations that limit the output (such as Take, First, or early termination in iteration)
|
||||
// to avoid infinite loops.
|
||||
//
|
||||
// If the input sequence is empty, Cycle returns an empty sequence immediately. It does NOT
|
||||
// loop indefinitely - the result is simply an empty sequence.
|
||||
//
|
||||
// The operation is lazy - elements are only generated as they are consumed. The input sequence
|
||||
// is re-iterated each time the cycle completes, so any side effects in the source sequence
|
||||
// will be repeated.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The input sequence to cycle through. Should be finite.
|
||||
//
|
||||
// Returns:
|
||||
// - An infinite sequence that repeats the elements of the input sequence
|
||||
//
|
||||
// Example - Basic cycling with Take:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
// cycled := Cycle(seq)
|
||||
// result := Take[int](7)(cycled)
|
||||
// // yields: 1, 2, 3, 1, 2, 3, 1
|
||||
//
|
||||
// Example - Cycling strings:
|
||||
//
|
||||
// seq := From("A", "B", "C")
|
||||
// cycled := Cycle(seq)
|
||||
// result := Take[string](5)(cycled)
|
||||
// // yields: "A", "B", "C", "A", "B"
|
||||
//
|
||||
// Example - Using with First:
|
||||
//
|
||||
// seq := From(10, 20, 30)
|
||||
// cycled := Cycle(seq)
|
||||
// first := First(cycled)
|
||||
// // returns: Some(10)
|
||||
//
|
||||
// Example - Combining with filter and take:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// cycled := Cycle(seq)
|
||||
// evens := MonadFilter(cycled, func(x int) bool { return x%2 == 0 })
|
||||
// result := Take[int](5)(evens)
|
||||
// // yields: 2, 4, 2, 4, 2 (cycles through even numbers)
|
||||
//
|
||||
// Example - Empty sequence (returns empty, does not loop):
|
||||
//
|
||||
// seq := Empty[int]()
|
||||
// cycled := Cycle(seq)
|
||||
// result := Take[int](10)(cycled)
|
||||
// // yields: nothing (empty sequence, terminates immediately)
|
||||
func Cycle[U any](ma Seq[U]) Seq[U] {
|
||||
return func(yield func(U) bool) {
|
||||
for {
|
||||
isEmpty := true
|
||||
for u := range ma {
|
||||
if !yield(u) {
|
||||
return
|
||||
}
|
||||
isEmpty = false
|
||||
}
|
||||
if isEmpty {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
611
v2/iterator/iter/cycle_test.go
Normal file
611
v2/iterator/iter/cycle_test.go
Normal file
@@ -0,0 +1,611 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestCycleBasic tests basic Cycle functionality with Take
|
||||
func TestCycleBasic(t *testing.T) {
|
||||
t.Run("cycles through integer sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles through string sequence", func(t *testing.T) {
|
||||
seq := From("A", "B", "C")
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[string](8)(cycled))
|
||||
assert.Equal(t, []string{"A", "B", "C", "A", "B", "C", "A", "B"}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles through single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](5)(cycled))
|
||||
assert.Equal(t, []int{42, 42, 42, 42, 42}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles through two elements", func(t *testing.T) {
|
||||
seq := From(true, false)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[bool](6)(cycled))
|
||||
assert.Equal(t, []bool{true, false, true, false, true, false}, result)
|
||||
})
|
||||
|
||||
t.Run("takes exact multiple of cycle length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](9)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes less than one cycle", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](3)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleEmpty tests Cycle with empty sequences
|
||||
func TestCycleEmpty(t *testing.T) {
|
||||
t.Run("cycles empty sequence produces nothing", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](10)(cycled))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("cycles empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[string](5)(cycled))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("take zero from cycled sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](0)(cycled))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithComplexTypes tests Cycle with complex data types
|
||||
func TestCycleWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("cycles structs", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[Person](5)(cycled))
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
{"Alice", 30},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("cycles pointers", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
seq := From(p1, p2)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[*Person](4)(cycled))
|
||||
assert.Equal(t, []*Person{p1, p2, p1, p2}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles slices", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3, 4})
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[[]int](5)(cycled))
|
||||
expected := [][]int{{1, 2}, {3, 4}, {1, 2}, {3, 4}, {1, 2}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithFirst tests Cycle with First operation
|
||||
func TestCycleWithFirst(t *testing.T) {
|
||||
t.Run("gets first element from cycled sequence", func(t *testing.T) {
|
||||
seq := From(10, 20, 30)
|
||||
cycled := Cycle(seq)
|
||||
first := First(cycled)
|
||||
assert.Equal(t, O.Of(10), first)
|
||||
})
|
||||
|
||||
t.Run("gets first from single element cycle", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
cycled := Cycle(seq)
|
||||
first := First(cycled)
|
||||
assert.Equal(t, O.Of(42), first)
|
||||
})
|
||||
|
||||
t.Run("gets none from empty cycle", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
cycled := Cycle(seq)
|
||||
first := First(cycled)
|
||||
assert.Equal(t, O.None[int](), first)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithChainedOperations tests Cycle with other operations
|
||||
func TestCycleWithChainedOperations(t *testing.T) {
|
||||
t.Run("cycle then map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
mapped := MonadMap(cycled, N.Mul(10))
|
||||
result := toSlice(Take[int](7)(mapped))
|
||||
assert.Equal(t, []int{10, 20, 30, 10, 20, 30, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("cycle then filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
filtered := MonadFilter(cycled, func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(Take[int](6)(filtered))
|
||||
assert.Equal(t, []int{2, 4, 2, 4, 2, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("map then cycle", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
cycled := Cycle(mapped)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{2, 4, 6, 2, 4, 6, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("filter then cycle", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
cycled := Cycle(filtered)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{2, 4, 6, 2, 4, 6, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("cycle with multiple takes", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
taken1 := Take[int](10)(cycled)
|
||||
taken2 := Take[int](5)(taken1)
|
||||
result := toSlice(taken2)
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithReplicate tests Cycle with Replicate
|
||||
func TestCycleWithReplicate(t *testing.T) {
|
||||
t.Run("cycles replicated values", func(t *testing.T) {
|
||||
seq := Replicate(3, "X")
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[string](7)(cycled))
|
||||
assert.Equal(t, []string{"X", "X", "X", "X", "X", "X", "X"}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles single replicated value", func(t *testing.T) {
|
||||
seq := Replicate(1, 99)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](5)(cycled))
|
||||
assert.Equal(t, []int{99, 99, 99, 99, 99}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithMakeBy tests Cycle with MakeBy
|
||||
func TestCycleWithMakeBy(t *testing.T) {
|
||||
t.Run("cycles generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(3, func(i int) int { return i * i })
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](8)(cycled))
|
||||
assert.Equal(t, []int{0, 1, 4, 0, 1, 4, 0, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("cycles single generated element", func(t *testing.T) {
|
||||
seq := MakeBy(1, func(i int) int { return i + 10 })
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](4)(cycled))
|
||||
assert.Equal(t, []int{10, 10, 10, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithPrependAppend tests Cycle with Prepend and Append
|
||||
func TestCycleWithPrependAppend(t *testing.T) {
|
||||
t.Run("cycle prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3)
|
||||
prepended := Prepend(1)(seq)
|
||||
cycled := Cycle(prepended)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("cycle appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
appended := Append(3)(seq)
|
||||
cycled := Cycle(appended)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithFlatten tests Cycle with Flatten
|
||||
func TestCycleWithFlatten(t *testing.T) {
|
||||
t.Run("cycles flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3))
|
||||
flattened := Flatten(nested)
|
||||
cycled := Cycle(flattened)
|
||||
result := toSlice(Take[int](7)(cycled))
|
||||
assert.Equal(t, []int{1, 2, 3, 1, 2, 3, 1}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithChain tests Cycle with Chain
|
||||
func TestCycleWithChain(t *testing.T) {
|
||||
t.Run("cycles chained sequence", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
cycled := Cycle(chained)
|
||||
result := toSlice(Take[int](10)(cycled))
|
||||
assert.Equal(t, []int{1, 10, 2, 20, 1, 10, 2, 20, 1, 10}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleEarlyTermination tests that Cycle respects early termination
|
||||
func TestCycleEarlyTermination(t *testing.T) {
|
||||
t.Run("terminates when yield returns false", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
|
||||
count := 0
|
||||
for v := range cycled {
|
||||
count++
|
||||
if v == 2 && count > 2 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Should have stopped at the second occurrence of 2
|
||||
assert.Equal(t, 5, count) // 1, 2, 3, 1, 2
|
||||
})
|
||||
|
||||
t.Run("take limits infinite cycle", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
taken := Take[int](100)(cycled)
|
||||
|
||||
result := toSlice(taken)
|
||||
assert.Len(t, result, 100)
|
||||
|
||||
// Verify pattern repeats correctly
|
||||
for i := 0; i < 100; i++ {
|
||||
expected := (i % 3) + 1
|
||||
assert.Equal(t, expected, result[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleLargeSequence tests Cycle with larger sequences
|
||||
func TestCycleLargeSequence(t *testing.T) {
|
||||
t.Run("cycles large sequence", func(t *testing.T) {
|
||||
data := make([]int, 10)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](25)(cycled))
|
||||
|
||||
assert.Len(t, result, 25)
|
||||
// Verify first cycle
|
||||
for i := 0; i < 10; i++ {
|
||||
assert.Equal(t, i, result[i])
|
||||
}
|
||||
// Verify second cycle
|
||||
for i := 10; i < 20; i++ {
|
||||
assert.Equal(t, i-10, result[i])
|
||||
}
|
||||
// Verify partial third cycle
|
||||
for i := 20; i < 25; i++ {
|
||||
assert.Equal(t, i-20, result[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleWithReduce tests Cycle with Reduce (limited by Take)
|
||||
func TestCycleWithReduce(t *testing.T) {
|
||||
t.Run("reduces limited cycled sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
limited := Take[int](10)(cycled)
|
||||
sum := MonadReduce(limited, func(acc, x int) int { return acc + x }, 0)
|
||||
// 1+2+3+1+2+3+1+2+3+1 = 19
|
||||
assert.Equal(t, 19, sum)
|
||||
})
|
||||
}
|
||||
|
||||
// TestCycleEdgeCases tests edge cases
|
||||
func TestCycleEdgeCases(t *testing.T) {
|
||||
t.Run("cycle with very long take", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](1000)(cycled))
|
||||
assert.Len(t, result, 1000)
|
||||
|
||||
// Verify pattern
|
||||
for i := 0; i < 1000; i++ {
|
||||
expected := (i % 2) + 1
|
||||
assert.Equal(t, expected, result[i])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cycle single element many times", func(t *testing.T) {
|
||||
seq := From(7)
|
||||
cycled := Cycle(seq)
|
||||
result := toSlice(Take[int](100)(cycled))
|
||||
assert.Len(t, result, 100)
|
||||
for _, v := range result {
|
||||
assert.Equal(t, 7, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkCycle(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
taken := Take[int](100)(cycled)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCycleSingleElement(b *testing.B) {
|
||||
seq := From(42)
|
||||
cycled := Cycle(seq)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
taken := Take[int](100)(cycled)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCycleWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mapped := MonadMap(cycled, N.Mul(2))
|
||||
taken := Take[int](100)(mapped)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCycleWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
cycled := Cycle(seq)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filtered := MonadFilter(cycled, func(x int) bool { return x%2 == 0 })
|
||||
taken := Take[int](50)(filtered)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleCycle() {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
result := Take[int](7)(cycled)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 1 2 3 1
|
||||
}
|
||||
|
||||
func ExampleCycle_singleElement() {
|
||||
seq := From("X")
|
||||
cycled := Cycle(seq)
|
||||
result := Take[string](5)(cycled)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: X X X X X
|
||||
}
|
||||
|
||||
func ExampleCycle_withFirst() {
|
||||
seq := From(10, 20, 30)
|
||||
cycled := Cycle(seq)
|
||||
first := First(cycled)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First: %d\n", value)
|
||||
}
|
||||
// Output: First: 10
|
||||
}
|
||||
|
||||
func ExampleCycle_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
cycled := Cycle(seq)
|
||||
evens := MonadFilter(cycled, func(x int) bool { return x%2 == 0 })
|
||||
result := Take[int](6)(evens)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 2 4 2 4
|
||||
}
|
||||
|
||||
func ExampleCycle_withMap() {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
doubled := MonadMap(cycled, N.Mul(2))
|
||||
result := Take[int](7)(doubled)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6 2 4 6 2
|
||||
}
|
||||
|
||||
func ExampleCycle_empty() {
|
||||
seq := Empty[int]()
|
||||
cycled := Cycle(seq)
|
||||
result := Take[int](5)(cycled)
|
||||
|
||||
count := 0
|
||||
for range result {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleCycle_exactMultiple() {
|
||||
seq := From("A", "B", "C")
|
||||
cycled := Cycle(seq)
|
||||
result := Take[string](9)(cycled)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: A B C A B C A B C
|
||||
}
|
||||
|
||||
// TestCycleWithZip tests Cycle combined with Zip operator
|
||||
func TestCycleWithZip(t *testing.T) {
|
||||
t.Run("zip infinite cycled sequence with finite sequence", func(t *testing.T) {
|
||||
// Create an infinite sequence by cycling
|
||||
infinite := Cycle(From(1, 2, 3))
|
||||
// Create a finite sequence
|
||||
finite := From("a", "b", "c", "d", "e")
|
||||
|
||||
// Zip them together - should stop when finite sequence ends
|
||||
zipped := MonadZip(infinite, finite)
|
||||
|
||||
// Convert to slice for verification
|
||||
result := make([]struct {
|
||||
num int
|
||||
str string
|
||||
}, 0)
|
||||
for num, str := range zipped {
|
||||
result = append(result, struct {
|
||||
num int
|
||||
str string
|
||||
}{num, str})
|
||||
}
|
||||
|
||||
// Should have 5 pairs (limited by finite sequence)
|
||||
assert.Len(t, result, 5)
|
||||
assert.Equal(t, 1, result[0].num)
|
||||
assert.Equal(t, "a", result[0].str)
|
||||
assert.Equal(t, 2, result[1].num)
|
||||
assert.Equal(t, "b", result[1].str)
|
||||
assert.Equal(t, 3, result[2].num)
|
||||
assert.Equal(t, "c", result[2].str)
|
||||
assert.Equal(t, 1, result[3].num) // Cycle repeats
|
||||
assert.Equal(t, "d", result[3].str)
|
||||
assert.Equal(t, 2, result[4].num)
|
||||
assert.Equal(t, "e", result[4].str)
|
||||
})
|
||||
|
||||
t.Run("zip finite sequence with infinite cycled sequence", func(t *testing.T) {
|
||||
// Reverse order: finite first, infinite second
|
||||
finite := From(10, 20, 30)
|
||||
infinite := Cycle(From("X", "Y"))
|
||||
|
||||
zipped := MonadZip(finite, infinite)
|
||||
|
||||
result := make([]struct {
|
||||
num int
|
||||
str string
|
||||
}, 0)
|
||||
for num, str := range zipped {
|
||||
result = append(result, struct {
|
||||
num int
|
||||
str string
|
||||
}{num, str})
|
||||
}
|
||||
|
||||
// Should have 3 pairs (limited by finite sequence)
|
||||
assert.Len(t, result, 3)
|
||||
assert.Equal(t, 10, result[0].num)
|
||||
assert.Equal(t, "X", result[0].str)
|
||||
assert.Equal(t, 20, result[1].num)
|
||||
assert.Equal(t, "Y", result[1].str)
|
||||
assert.Equal(t, 30, result[2].num)
|
||||
assert.Equal(t, "X", result[2].str) // Cycle repeats
|
||||
})
|
||||
|
||||
t.Run("zip two cycled sequences with take", func(t *testing.T) {
|
||||
// Both sequences are infinite, so we need Take to limit
|
||||
cycle1 := Cycle(From(1, 2))
|
||||
cycle2 := Cycle(From("a", "b", "c"))
|
||||
|
||||
zipped := MonadZip(cycle1, cycle2)
|
||||
|
||||
// Use Take to limit the infinite result
|
||||
count := 0
|
||||
result := make([]struct {
|
||||
num int
|
||||
str string
|
||||
}, 0)
|
||||
for num, str := range zipped {
|
||||
result = append(result, struct {
|
||||
num int
|
||||
str string
|
||||
}{num, str})
|
||||
count++
|
||||
if count >= 7 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Len(t, result, 7)
|
||||
// Verify the pattern
|
||||
assert.Equal(t, 1, result[0].num)
|
||||
assert.Equal(t, "a", result[0].str)
|
||||
assert.Equal(t, 2, result[1].num)
|
||||
assert.Equal(t, "b", result[1].str)
|
||||
assert.Equal(t, 1, result[2].num) // cycle1 repeats
|
||||
assert.Equal(t, "c", result[2].str)
|
||||
assert.Equal(t, 2, result[3].num)
|
||||
assert.Equal(t, "a", result[3].str) // cycle2 repeats
|
||||
})
|
||||
}
|
||||
60
v2/iterator/iter/first.go
Normal file
60
v2/iterator/iter/first.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import "github.com/IBM/fp-go/v2/option"
|
||||
|
||||
// First returns the first element from an [Iterator] wrapped in an [Option].
|
||||
//
|
||||
// This function attempts to retrieve the first element from the iterator. If the iterator
|
||||
// contains at least one element, it returns Some(element). If the iterator is empty,
|
||||
// it returns None. The function consumes only the first element of the iterator.
|
||||
//
|
||||
// RxJS Equivalent: [first] - https://rxjs.dev/api/operators/first
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the iterator
|
||||
//
|
||||
// Parameters:
|
||||
// - mu: The input iterator to get the first element from
|
||||
//
|
||||
// Returns:
|
||||
// - Option[U]: Some(first element) if the iterator is non-empty, None otherwise
|
||||
//
|
||||
// Example with non-empty sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// first := iter.First(seq)
|
||||
// // Returns: Some(1)
|
||||
//
|
||||
// Example with empty sequence:
|
||||
//
|
||||
// seq := iter.Empty[int]()
|
||||
// first := iter.First(seq)
|
||||
// // Returns: None
|
||||
//
|
||||
// Example with filtered sequence:
|
||||
//
|
||||
// seq := iter.From(1, 2, 3, 4, 5)
|
||||
// filtered := iter.Filter(func(x int) bool { return x > 3 })(seq)
|
||||
// first := iter.First(filtered)
|
||||
// // Returns: Some(4)
|
||||
func First[U any](mu Seq[U]) Option[U] {
|
||||
for u := range mu {
|
||||
return option.Some(u)
|
||||
}
|
||||
return option.None[U]()
|
||||
}
|
||||
346
v2/iterator/iter/first_test.go
Normal file
346
v2/iterator/iter/first_test.go
Normal file
@@ -0,0 +1,346 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestFirst tests getting the first element from a non-empty sequence
|
||||
func TestFirst(t *testing.T) {
|
||||
t.Run("returns first element from integer sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first element from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of("a"), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first element from single element sequence", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first element from large sequence", func(t *testing.T) {
|
||||
seq := From(100, 200, 300, 400, 500)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(100), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstEmpty tests getting the first element from an empty sequence
|
||||
func TestFirstEmpty(t *testing.T) {
|
||||
t.Run("returns None for empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[string](), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty struct sequence", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Value int
|
||||
}
|
||||
seq := Empty[TestStruct]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[TestStruct](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithFiltered tests First with filtered sequences
|
||||
func TestFirstWithFiltered(t *testing.T) {
|
||||
t.Run("returns first element matching filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
filtered := MonadFilter(seq, N.MoreThan(3))
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.Of(4), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None when no elements match filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
filtered := MonadFilter(seq, N.MoreThan(10))
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first even number", func(t *testing.T) {
|
||||
seq := From(1, 3, 5, 6, 7, 8)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.Of(6), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithMapped tests First with mapped sequences
|
||||
func TestFirstWithMapped(t *testing.T) {
|
||||
t.Run("returns first element after mapping", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
fst := First(mapped)
|
||||
assert.Equal(t, O.Of(2), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first string after mapping", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := MonadMap(seq, S.Format[int]("num-%d"))
|
||||
fst := First(mapped)
|
||||
assert.Equal(t, O.Of("num-1"), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithComplex tests First with complex types
|
||||
func TestFirstWithComplex(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("returns first person", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
fst := First(seq)
|
||||
expected := O.Of(Person{"Alice", 30})
|
||||
assert.Equal(t, expected, fst)
|
||||
})
|
||||
|
||||
t.Run("returns first pointer", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
seq := From(p1, p2)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(p1), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstDoesNotConsumeEntireSequence tests that First only consumes the first element
|
||||
func TestFirstDoesNotConsumeEntireSequence(t *testing.T) {
|
||||
t.Run("only consumes first element", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := MonadMap(From(1, 2, 3, 4, 5), func(x int) int {
|
||||
callCount++
|
||||
return x * 2
|
||||
})
|
||||
|
||||
fst := First(seq)
|
||||
|
||||
assert.Equal(t, O.Of(2), fst)
|
||||
// Should only have called the map function once for the first element
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithChainedOperations tests First with multiple chained operations
|
||||
func TestFirstWithChainedOperations(t *testing.T) {
|
||||
t.Run("chains filter, map, and first", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, N.MoreThan(5))
|
||||
mapped := MonadMap(filtered, N.Mul(10))
|
||||
result := First(mapped)
|
||||
assert.Equal(t, O.Of(60), result)
|
||||
})
|
||||
|
||||
t.Run("chains map and filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
filtered := MonadFilter(mapped, N.MoreThan(5))
|
||||
result := First(filtered)
|
||||
assert.Equal(t, O.Of(6), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithReplicate tests First with replicated values
|
||||
func TestFirstWithReplicate(t *testing.T) {
|
||||
t.Run("returns first from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(5, 42)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None from zero replications", func(t *testing.T) {
|
||||
seq := Replicate(0, 42)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithMakeBy tests First with MakeBy
|
||||
func TestFirstWithMakeBy(t *testing.T) {
|
||||
t.Run("returns first generated element", func(t *testing.T) {
|
||||
seq := MakeBy(5, func(i int) int { return i * i })
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(0), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None for zero elements", func(t *testing.T) {
|
||||
seq := MakeBy(0, F.Identity[int])
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithPrepend tests First with Prepend
|
||||
func TestFirstWithPrepend(t *testing.T) {
|
||||
t.Run("returns prepended element", func(t *testing.T) {
|
||||
seq := From(2, 3, 4)
|
||||
prepended := Prepend(1)(seq)
|
||||
fst := First(prepended)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
prepended := Prepend(42)(seq)
|
||||
fst := First(prepended)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithAppend tests First with Append
|
||||
func TestFirstWithAppend(t *testing.T) {
|
||||
t.Run("returns first element, not appended", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
fst := First(appended)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns appended element from empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
appended := Append(42)(seq)
|
||||
fst := First(appended)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithChain tests First with Chain (flatMap)
|
||||
func TestFirstWithChain(t *testing.T) {
|
||||
t.Run("returns first from chained sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
fst := First(chained)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None when chain produces empty", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return Empty[int]()
|
||||
})
|
||||
fst := First(chained)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithFlatten tests First with Flatten
|
||||
func TestFirstWithFlatten(t *testing.T) {
|
||||
t.Run("returns first from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5))
|
||||
flattened := Flatten(nested)
|
||||
fst := First(flattened)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None from empty nested sequence", func(t *testing.T) {
|
||||
nested := Empty[Seq[int]]()
|
||||
flattened := Flatten(nested)
|
||||
fst := First(flattened)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkFirst(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
First(seq)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFirstLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
First(seq)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFirstFiltered(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
filtered := MonadFilter(seq, N.MoreThan(5))
|
||||
First(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleFirst() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
first := First(seq)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First element: %d\n", value)
|
||||
}
|
||||
// Output: First element: 1
|
||||
}
|
||||
|
||||
func ExampleFirst_empty() {
|
||||
seq := Empty[int]()
|
||||
first := First(seq)
|
||||
|
||||
if _, ok := O.Unwrap(first); !ok {
|
||||
fmt.Println("Sequence is empty")
|
||||
}
|
||||
// Output: Sequence is empty
|
||||
}
|
||||
|
||||
func ExampleFirst_filtered() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
filtered := MonadFilter(seq, N.MoreThan(3))
|
||||
first := First(filtered)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First element > 3: %d\n", value)
|
||||
}
|
||||
// Output: First element > 3: 4
|
||||
}
|
||||
@@ -51,6 +51,7 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/internal/iter"
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// Of creates a sequence containing a single element.
|
||||
@@ -80,6 +81,8 @@ func Of2[K, A any](k K, a A) Seq2[K, A] {
|
||||
// MonadMap transforms each element in a sequence using the provided function.
|
||||
// This is the monadic version that takes the sequence as the first parameter.
|
||||
//
|
||||
// RxJS Equivalent: [map] - https://rxjs.dev/api/operators/map
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
@@ -182,6 +185,8 @@ func MapWithKey[K, A, B any](f func(K, A) B) Operator2[K, A, B] {
|
||||
|
||||
// MonadFilter returns a sequence containing only elements that satisfy the predicate.
|
||||
//
|
||||
// RxJS Equivalent: [filter] - https://rxjs.dev/api/operators/filter
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
@@ -424,6 +429,8 @@ func FilterMapWithKey[K, A, B any](f func(K, A) Option[B]) Operator2[K, A, B] {
|
||||
// MonadChain applies a function that returns a sequence to each element and flattens the results.
|
||||
// This is the monadic bind operation (flatMap).
|
||||
//
|
||||
// RxJS Equivalent: [mergeMap/flatMap] - https://rxjs.dev/api/operators/mergeMap
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
@@ -460,6 +467,8 @@ func Chain[A, B any](f func(A) Seq[B]) Operator[A, B] {
|
||||
|
||||
// Flatten flattens a sequence of sequences into a single sequence.
|
||||
//
|
||||
// RxJS Equivalent: [mergeAll] - https://rxjs.dev/api/operators/mergeAll
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nested := From(From(1, 2), From(3, 4), From(5))
|
||||
@@ -562,6 +571,8 @@ func Replicate[A any](n int, a A) Seq[A] {
|
||||
// MonadReduce reduces a sequence to a single value by applying a function to each element
|
||||
// and an accumulator, starting with an initial value.
|
||||
//
|
||||
// RxJS Equivalent: [reduce] - https://rxjs.dev/api/operators/reduce
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
@@ -818,6 +829,8 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
|
||||
// Prepend returns a function that adds an element to the beginning of a sequence.
|
||||
//
|
||||
// RxJS Equivalent: [startWith] - https://rxjs.dev/api/operators/startWith
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(2, 3, 4)
|
||||
@@ -831,6 +844,8 @@ func Prepend[A any](head A) Operator[A, A] {
|
||||
|
||||
// Append returns a function that adds an element to the end of a sequence.
|
||||
//
|
||||
// RxJS Equivalent: [endWith] - https://rxjs.dev/api/operators/endWith
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
@@ -845,13 +860,15 @@ func Append[A any](tail A) Operator[A, A] {
|
||||
// MonadZip combines two sequences into a sequence of pairs.
|
||||
// The resulting sequence stops when either input sequence is exhausted.
|
||||
//
|
||||
// RxJS Equivalent: [zip] - https://rxjs.dev/api/operators/zip
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// seqA := From(1, 2, 3)
|
||||
// seqB := From("a", "b")
|
||||
// result := MonadZip(seqB, seqA)
|
||||
// result := MonadZip(seqA, seqB)
|
||||
// // yields: (1, "a"), (2, "b")
|
||||
func MonadZip[A, B any](fb Seq[B], fa Seq[A]) Seq2[A, B] {
|
||||
func MonadZip[A, B any](fa Seq[A], fb Seq[B]) Seq2[A, B] {
|
||||
|
||||
return func(yield func(A, B) bool) {
|
||||
na, sa := I.Pull(fa)
|
||||
@@ -882,8 +899,8 @@ func MonadZip[A, B any](fb Seq[B], fa Seq[A]) Seq2[A, B] {
|
||||
// // yields: (1, "a"), (2, "b"), (3, "c")
|
||||
//
|
||||
//go:inline
|
||||
func Zip[A, B any](fa Seq[A]) func(Seq[B]) Seq2[A, B] {
|
||||
return F.Bind2nd(MonadZip[A, B], fa)
|
||||
func Zip[A, B any](fb Seq[B]) func(Seq[A]) Seq2[A, B] {
|
||||
return F.Bind2nd(MonadZip[A, B], fb)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
@@ -895,3 +912,50 @@ func MonadMapToArray[A, B any](fa Seq[A], f func(A) B) []B {
|
||||
func MapToArray[A, B any](f func(A) B) func(Seq[A]) []B {
|
||||
return G.MapToArray[Seq[A], []B](f)
|
||||
}
|
||||
|
||||
// ToSeqPair converts a key-value sequence (Seq2) into a sequence of Pairs.
|
||||
//
|
||||
// This function transforms a Seq2[A, B] (which yields key-value pairs when iterated)
|
||||
// into a Seq[Pair[A, B]] (which yields Pair objects). This is useful when you need
|
||||
// to work with pairs as first-class values rather than as separate key-value arguments.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the first element (key) in each pair
|
||||
// - B: The type of the second element (value) in each pair
|
||||
//
|
||||
// Parameters:
|
||||
// - as: A Seq2 that yields key-value pairs
|
||||
//
|
||||
// Returns:
|
||||
// - A Seq that yields Pair objects containing the key-value pairs
|
||||
//
|
||||
// Example - Basic conversion:
|
||||
//
|
||||
// seq2 := iter.MonadZip(iter.From("a", "b", "c"), iter.From(1, 2, 3))
|
||||
// pairs := iter.ToSeqPair(seq2)
|
||||
// // yields: Pair("a", 1), Pair("b", 2), Pair("c", 3)
|
||||
//
|
||||
// Example - Using with Map:
|
||||
//
|
||||
// seq2 := iter.MonadZip(iter.From(1, 2, 3), iter.From(10, 20, 30))
|
||||
// pairs := iter.ToSeqPair(seq2)
|
||||
// sums := iter.MonadMap(pairs, func(p Pair[int, int]) int {
|
||||
// return p.Fst + p.Snd
|
||||
// })
|
||||
// // yields: 11, 22, 33
|
||||
//
|
||||
// Example - Empty sequence:
|
||||
//
|
||||
// seq2 := iter.Empty[int]()
|
||||
// zipped := iter.MonadZip(seq2, iter.Empty[string]())
|
||||
// pairs := iter.ToSeqPair(zipped)
|
||||
// // yields: nothing (empty sequence)
|
||||
func ToSeqPair[A, B any](as Seq2[A, B]) Seq[Pair[A, B]] {
|
||||
return func(yield Predicate[Pair[A, B]]) {
|
||||
for a, b := range as {
|
||||
if !yield(pair.MakePair(a, b)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,9 +22,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
R "github.com/IBM/fp-go/v2/record"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@@ -42,13 +44,13 @@ func toMap[K comparable, V any](seq Seq2[K, V]) map[K]V {
|
||||
func TestOf(t *testing.T) {
|
||||
seq := Of(42)
|
||||
result := toSlice(seq)
|
||||
assert.Equal(t, []int{42}, result)
|
||||
assert.Equal(t, A.Of(42), result)
|
||||
}
|
||||
|
||||
func TestOf2(t *testing.T) {
|
||||
seq := Of2("key", 100)
|
||||
result := toMap(seq)
|
||||
assert.Equal(t, map[string]int{"key": 100}, result)
|
||||
assert.Equal(t, R.Of("key", 100), result)
|
||||
}
|
||||
|
||||
func TestFrom(t *testing.T) {
|
||||
@@ -460,7 +462,7 @@ func TestMonadZip(t *testing.T) {
|
||||
|
||||
var pairs []string
|
||||
for a, b := range result {
|
||||
pairs = append(pairs, fmt.Sprintf("%d:%s", a, b))
|
||||
pairs = append(pairs, fmt.Sprintf("%d:%s", b, a))
|
||||
}
|
||||
assert.Equal(t, []string{"1:a", "2:b"}, pairs)
|
||||
}
|
||||
@@ -468,8 +470,8 @@ func TestMonadZip(t *testing.T) {
|
||||
func TestZip(t *testing.T) {
|
||||
seqA := From(1, 2, 3)
|
||||
seqB := From("a", "b", "c")
|
||||
zipWithA := Zip[int, string](seqA)
|
||||
result := zipWithA(seqB)
|
||||
zipWithA := Zip[int](seqB)
|
||||
result := zipWithA(seqA)
|
||||
|
||||
var pairs []string
|
||||
for a, b := range result {
|
||||
@@ -587,3 +589,29 @@ func ExampleMonoid() {
|
||||
fmt.Println(result)
|
||||
// Output: [1 2 3 4 5 6]
|
||||
}
|
||||
|
||||
func TestMonadMapToArray(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := MonadMapToArray(seq, N.Mul(2))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestMonadMapToArrayEmpty(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := MonadMapToArray(seq, N.Mul(2))
|
||||
assert.Empty(t, result)
|
||||
}
|
||||
|
||||
func TestMapToArray(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapper := MapToArray(N.Mul(2))
|
||||
result := mapper(seq)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
}
|
||||
|
||||
func TestMapToArrayIdentity(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
mapper := MapToArray(F.Identity[string])
|
||||
result := mapper(seq)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
}
|
||||
|
||||
105
v2/iterator/iter/scan.go
Normal file
105
v2/iterator/iter/scan.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
// Scan applies an accumulator function over a sequence, emitting each intermediate result.
|
||||
//
|
||||
// This function is similar to Reduce, but instead of returning only the final accumulated value,
|
||||
// it returns a sequence containing all intermediate accumulated values. Each element in the
|
||||
// output sequence is the result of applying the accumulator function to the previous accumulated
|
||||
// value and the current input element.
|
||||
//
|
||||
// The operation is lazy - intermediate values are computed only as they are consumed.
|
||||
//
|
||||
// RxJS Equivalent: [scan] - https://rxjs.dev/api/operators/scan
|
||||
//
|
||||
// Scan is useful for:
|
||||
// - Computing running totals or cumulative sums
|
||||
// - Tracking state changes over a sequence
|
||||
// - Building up complex values incrementally
|
||||
// - Generating sequences based on previous values
|
||||
//
|
||||
// Type Parameters:
|
||||
// - FCT: The accumulator function type, must be ~func(V, U) V
|
||||
// - U: The type of elements in the input sequence
|
||||
// - V: The type of the accumulated value and elements in the output sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The accumulator function that takes the current accumulated value and the next
|
||||
// input element, returning the new accumulated value
|
||||
// - initial: The initial accumulated value (not included in the output sequence)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] into a Seq[V] containing all intermediate
|
||||
// accumulated values
|
||||
//
|
||||
// Example - Running sum:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// runningSum := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
// result := runningSum(seq)
|
||||
// // yields: 1, 3, 6, 10, 15
|
||||
//
|
||||
// Example - Running product:
|
||||
//
|
||||
// seq := From(2, 3, 4)
|
||||
// runningProduct := Scan(func(acc, x int) int { return acc * x }, 1)
|
||||
// result := runningProduct(seq)
|
||||
// // yields: 2, 6, 24
|
||||
//
|
||||
// Example - Building strings:
|
||||
//
|
||||
// seq := From("a", "b", "c")
|
||||
// concat := Scan(func(acc, x string) string { return acc + x }, "")
|
||||
// result := concat(seq)
|
||||
// // yields: "a", "ab", "abc"
|
||||
//
|
||||
// Example - Tracking maximum:
|
||||
//
|
||||
// seq := From(3, 1, 4, 1, 5, 9, 2)
|
||||
// maxSoFar := Scan(func(acc, x int) int {
|
||||
// if x > acc { return x }
|
||||
// return acc
|
||||
// }, 0)
|
||||
// result := maxSoFar(seq)
|
||||
// // yields: 3, 3, 4, 4, 5, 9, 9
|
||||
//
|
||||
// Example - Empty sequence:
|
||||
//
|
||||
// seq := Empty[int]()
|
||||
// runningSum := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
// result := runningSum(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - Single element:
|
||||
//
|
||||
// seq := From(42)
|
||||
// runningSum := Scan(func(acc, x int) int { return acc + x }, 10)
|
||||
// result := runningSum(seq)
|
||||
// // yields: 52
|
||||
func Scan[FCT ~func(V, U) V, U, V any](f FCT, initial V) Operator[U, V] {
|
||||
return func(s Seq[U]) Seq[V] {
|
||||
return func(yield func(V) bool) {
|
||||
current := initial
|
||||
for u := range s {
|
||||
current = f(current, u)
|
||||
if !yield(current) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
407
v2/iterator/iter/scan_test.go
Normal file
407
v2/iterator/iter/scan_test.go
Normal file
@@ -0,0 +1,407 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestScanBasic tests basic Scan functionality
|
||||
func TestScanBasic(t *testing.T) {
|
||||
t.Run("running sum of integers", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{1, 3, 6, 10, 15}, result)
|
||||
})
|
||||
|
||||
t.Run("running product", func(t *testing.T) {
|
||||
seq := From(2, 3, 4)
|
||||
scanned := Scan(func(acc, x int) int { return acc * x }, 1)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{2, 6, 24}, result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
scanned := Scan(func(acc, x string) string { return acc + x }, "")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []string{"a", "ab", "abc"}, result)
|
||||
})
|
||||
|
||||
t.Run("string concatenation with separator", func(t *testing.T) {
|
||||
seq := From("hello", "world", "test")
|
||||
scanned := Scan(func(acc, x string) string {
|
||||
if acc == "" {
|
||||
return x
|
||||
}
|
||||
return acc + "-" + x
|
||||
}, "")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []string{"hello", "hello-world", "hello-world-test"}, result)
|
||||
})
|
||||
|
||||
t.Run("single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 10)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{52}, result)
|
||||
})
|
||||
|
||||
t.Run("two elements", func(t *testing.T) {
|
||||
seq := From(5, 10)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{5, 15}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanEmpty tests Scan with empty sequences
|
||||
func TestScanEmpty(t *testing.T) {
|
||||
t.Run("empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
scanned := Scan(func(acc, x string) string { return acc + x }, "start")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithDifferentTypes tests Scan with different input/output types
|
||||
func TestScanWithDifferentTypes(t *testing.T) {
|
||||
t.Run("int to string accumulation", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
scanned := Scan(func(acc string, x int) string {
|
||||
return fmt.Sprintf("%s%d", acc, x)
|
||||
}, "")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []string{"1", "12", "123"}, result)
|
||||
})
|
||||
|
||||
t.Run("string to int length accumulation", func(t *testing.T) {
|
||||
seq := From("a", "bb", "ccc")
|
||||
scanned := Scan(func(acc int, x string) int {
|
||||
return acc + len(x)
|
||||
}, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{1, 3, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("accumulate into slice", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
scanned := Scan(func(acc []int, x int) []int {
|
||||
return append(acc, x)
|
||||
}, []int{})
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, [][]int{
|
||||
{1},
|
||||
{1, 2},
|
||||
{1, 2, 3},
|
||||
}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanStateful tests Scan with stateful operations
|
||||
func TestScanStateful(t *testing.T) {
|
||||
t.Run("tracking maximum", func(t *testing.T) {
|
||||
seq := From(3, 1, 4, 1, 5, 9, 2)
|
||||
scanned := Scan(func(acc, x int) int {
|
||||
if x > acc {
|
||||
return x
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{3, 3, 4, 4, 5, 9, 9}, result)
|
||||
})
|
||||
|
||||
t.Run("tracking minimum", func(t *testing.T) {
|
||||
seq := From(5, 3, 8, 1, 4, 2)
|
||||
scanned := Scan(func(acc, x int) int {
|
||||
if acc == 0 || x < acc {
|
||||
return x
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{5, 3, 3, 1, 1, 1}, result)
|
||||
})
|
||||
|
||||
t.Run("counting occurrences", func(t *testing.T) {
|
||||
seq := From(1, 2, 1, 3, 1, 2)
|
||||
scanned := Scan(func(acc map[int]int, x int) map[int]int {
|
||||
newMap := make(map[int]int)
|
||||
for k, v := range acc {
|
||||
newMap[k] = v
|
||||
}
|
||||
newMap[x]++
|
||||
return newMap
|
||||
}, map[int]int{})
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Len(t, result, 6)
|
||||
assert.Equal(t, 1, result[0][1])
|
||||
assert.Equal(t, 1, result[1][2])
|
||||
assert.Equal(t, 2, result[2][1])
|
||||
assert.Equal(t, 3, result[4][1])
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithComplexTypes tests Scan with complex data types
|
||||
func TestScanWithComplexTypes(t *testing.T) {
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
t.Run("accumulate points", func(t *testing.T) {
|
||||
seq := From(Point{1, 0}, Point{0, 1}, Point{2, 2})
|
||||
scanned := Scan(func(acc, p Point) Point {
|
||||
return Point{acc.X + p.X, acc.Y + p.Y}
|
||||
}, Point{0, 0})
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []Point{
|
||||
{1, 0},
|
||||
{1, 1},
|
||||
{3, 3},
|
||||
}, result)
|
||||
})
|
||||
|
||||
t.Run("accumulate struct fields", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value int
|
||||
Count int
|
||||
}
|
||||
seq := From(5, 10, 15)
|
||||
scanned := Scan(func(acc Data, x int) Data {
|
||||
return Data{
|
||||
Value: acc.Value + x,
|
||||
Count: acc.Count + 1,
|
||||
}
|
||||
}, Data{0, 0})
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []Data{
|
||||
{5, 1},
|
||||
{15, 2},
|
||||
{30, 3},
|
||||
}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithChainedOperations tests Scan combined with other operations
|
||||
func TestScanWithChainedOperations(t *testing.T) {
|
||||
t.Run("scan then map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
mapped := MonadMap(scanned(seq), func(x int) int { return x * 2 })
|
||||
result := toSlice(mapped)
|
||||
assert.Equal(t, []int{2, 6, 12, 20}, result)
|
||||
})
|
||||
|
||||
t.Run("map then scan", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4)
|
||||
mapped := MonadMap(seq, func(x int) int { return x * 2 })
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(mapped))
|
||||
assert.Equal(t, []int{2, 6, 12, 20}, result)
|
||||
})
|
||||
|
||||
t.Run("scan then filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
filtered := MonadFilter(scanned(seq), func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{6, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("scan then take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
taken := Take[int](3)(scanned(seq))
|
||||
result := toSlice(taken)
|
||||
assert.Equal(t, []int{1, 3, 6}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithCycle tests Scan with infinite sequences
|
||||
func TestScanWithCycle(t *testing.T) {
|
||||
t.Run("scan cycled sequence with take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
cycled := Cycle(seq)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
taken := Take[int](10)(scanned(cycled))
|
||||
result := toSlice(taken)
|
||||
// 1, 3, 6, 7, 9, 12, 13, 15, 18, 19
|
||||
assert.Equal(t, []int{1, 3, 6, 7, 9, 12, 13, 15, 18, 19}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanEarlyTermination tests that Scan respects early termination
|
||||
func TestScanEarlyTermination(t *testing.T) {
|
||||
t.Run("terminates when yield returns false", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
|
||||
count := 0
|
||||
for v := range scanned(seq) {
|
||||
count++
|
||||
if v >= 6 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 3, count) // Should stop at 6 (1+2+3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanWithInitialValue tests different initial values
|
||||
func TestScanWithInitialValue(t *testing.T) {
|
||||
t.Run("non-zero initial value", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 10)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{11, 13, 16}, result)
|
||||
})
|
||||
|
||||
t.Run("negative initial value", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, -10)
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []int{-9, -7, -4}, result)
|
||||
})
|
||||
|
||||
t.Run("string initial value", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
scanned := Scan(func(acc, x string) string { return acc + x }, "start:")
|
||||
result := toSlice(scanned(seq))
|
||||
assert.Equal(t, []string{"start:a", "start:ab", "start:abc"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestScanLargeSequence tests Scan with larger sequences
|
||||
func TestScanLargeSequence(t *testing.T) {
|
||||
t.Run("scan large sequence", func(t *testing.T) {
|
||||
data := make([]int, 100)
|
||||
for i := range data {
|
||||
data[i] = i + 1
|
||||
}
|
||||
seq := From(data...)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := toSlice(scanned(seq))
|
||||
|
||||
assert.Len(t, result, 100)
|
||||
// Sum of 1 to n is n*(n+1)/2
|
||||
assert.Equal(t, 5050, result[99]) // Sum of 1 to 100
|
||||
assert.Equal(t, 1, result[0])
|
||||
assert.Equal(t, 3, result[1])
|
||||
assert.Equal(t, 6, result[2])
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkScan(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range scanned(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkScanLarge(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i + 1
|
||||
}
|
||||
seq := From(data...)
|
||||
scanned := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range scanned(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleScan() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
runningSum := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := runningSum(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 3 6 10 15
|
||||
}
|
||||
|
||||
func ExampleScan_runningProduct() {
|
||||
seq := From(2, 3, 4)
|
||||
runningProduct := Scan(func(acc, x int) int { return acc * x }, 1)
|
||||
result := runningProduct(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 6 24
|
||||
}
|
||||
|
||||
func ExampleScan_stringConcatenation() {
|
||||
seq := From("a", "b", "c")
|
||||
concat := Scan(func(acc, x string) string { return acc + x }, "")
|
||||
result := concat(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: a ab abc
|
||||
}
|
||||
|
||||
func ExampleScan_trackingMaximum() {
|
||||
seq := From(3, 1, 4, 1, 5, 9, 2)
|
||||
maxSoFar := Scan(func(acc, x int) int {
|
||||
if x > acc {
|
||||
return x
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
result := maxSoFar(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 3 3 4 4 5 9 9
|
||||
}
|
||||
|
||||
func ExampleScan_empty() {
|
||||
seq := Empty[int]()
|
||||
runningSum := Scan(func(acc, x int) int { return acc + x }, 0)
|
||||
result := runningSum(seq)
|
||||
|
||||
count := 0
|
||||
for range result {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
80
v2/iterator/iter/take.go
Normal file
80
v2/iterator/iter/take.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import F "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// Take returns an operator that limits the number of elements in a sequence to at most n elements.
|
||||
//
|
||||
// This function creates a transformation that takes the first n elements from a sequence
|
||||
// and discards the rest. If n is less than or equal to 0, it returns an empty sequence.
|
||||
// If the input sequence has fewer than n elements, all elements are returned.
|
||||
//
|
||||
// The operation is lazy and only consumes elements from the source sequence as needed.
|
||||
// Once n elements have been yielded, iteration stops immediately without consuming
|
||||
// the remaining elements from the source.
|
||||
//
|
||||
// RxJS Equivalent: [take] - https://rxjs.dev/api/operators/take
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the sequence
|
||||
//
|
||||
// Parameters:
|
||||
// - n: The maximum number of elements to take from the sequence
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that transforms a Seq[U] by taking at most n elements
|
||||
//
|
||||
// Example - Take first 3 elements:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// result := Take[int](3)(seq)
|
||||
// // yields: 1, 2, 3
|
||||
//
|
||||
// Example - Take more than available:
|
||||
//
|
||||
// seq := From(1, 2)
|
||||
// result := Take[int](5)(seq)
|
||||
// // yields: 1, 2 (all available elements)
|
||||
//
|
||||
// Example - Take zero or negative:
|
||||
//
|
||||
// seq := From(1, 2, 3)
|
||||
// result := Take[int](0)(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - Chaining with other operations:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
// evens := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
// result := Take[int](3)(evens)
|
||||
// // yields: 2, 4, 6 (first 3 even numbers)
|
||||
func Take[U any](n int) Operator[U, U] {
|
||||
if n <= 0 {
|
||||
return F.Constant1[Seq[U]](Empty[U]())
|
||||
}
|
||||
return func(s Seq[U]) Seq[U] {
|
||||
return func(yield Predicate[U]) {
|
||||
i := 0
|
||||
for u := range s {
|
||||
if i >= n || !yield(u) {
|
||||
return
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
463
v2/iterator/iter/take_test.go
Normal file
463
v2/iterator/iter/take_test.go
Normal file
@@ -0,0 +1,463 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestTake tests basic Take functionality
|
||||
func TestTake(t *testing.T) {
|
||||
t.Run("takes first n elements from sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Take[int](3)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes first element", func(t *testing.T) {
|
||||
seq := From(10, 20, 30)
|
||||
result := toSlice(Take[int](1)(seq))
|
||||
assert.Equal(t, []int{10}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all elements when n equals length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Take[int](3)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all elements when n exceeds length", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Take[int](10)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("takes from string sequence", func(t *testing.T) {
|
||||
seq := From("a", "b", "c", "d", "e")
|
||||
result := toSlice(Take[string](3)(seq))
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
})
|
||||
|
||||
t.Run("takes from single element sequence", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Take[int](1)(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("takes from large sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := toSlice(Take[int](5)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeZeroOrNegative tests Take with zero or negative values
|
||||
func TestTakeZeroOrNegative(t *testing.T) {
|
||||
t.Run("returns empty sequence when n is zero", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Take[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty sequence when n is negative", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(Take[int](-1)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty sequence when n is large negative", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
result := toSlice(Take[string](-100)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeEmpty tests Take with empty sequences
|
||||
func TestTakeEmpty(t *testing.T) {
|
||||
t.Run("returns empty from empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(Take[int](5)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty from empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
result := toSlice(Take[string](3)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("returns empty when taking zero from empty", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(Take[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithComplexTypes tests Take with complex data types
|
||||
func TestTakeWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("takes structs", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
Person{"David", 28},
|
||||
)
|
||||
result := toSlice(Take[Person](2)(seq))
|
||||
expected := []Person{
|
||||
{"Alice", 30},
|
||||
{"Bob", 25},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("takes pointers", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
p3 := &Person{"Charlie", 35}
|
||||
seq := From(p1, p2, p3)
|
||||
result := toSlice(Take[*Person](2)(seq))
|
||||
assert.Equal(t, []*Person{p1, p2}, result)
|
||||
})
|
||||
|
||||
t.Run("takes slices", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3, 4}, []int{5, 6}, []int{7, 8})
|
||||
result := toSlice(Take[[]int](2)(seq))
|
||||
expected := [][]int{{1, 2}, {3, 4}}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithChainedOperations tests Take with other sequence operations
|
||||
func TestTakeWithChainedOperations(t *testing.T) {
|
||||
t.Run("take after map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
result := toSlice(Take[int](3)(mapped))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("take after filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(Take[int](3)(filtered))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("map after take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
taken := Take[int](3)(seq)
|
||||
result := toSlice(MonadMap(taken, N.Mul(10)))
|
||||
assert.Equal(t, []int{10, 20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("filter after take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8)
|
||||
taken := Take[int](6)(seq)
|
||||
result := toSlice(MonadFilter(taken, func(x int) bool { return x%2 == 0 }))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("take after chain", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
chained := MonadChain(seq, func(x int) Seq[int] {
|
||||
return From(x, x*10)
|
||||
})
|
||||
result := toSlice(Take[int](4)(chained))
|
||||
assert.Equal(t, []int{1, 10, 2, 20}, result)
|
||||
})
|
||||
|
||||
t.Run("multiple takes", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
taken1 := Take[int](7)(seq)
|
||||
taken2 := Take[int](3)(taken1)
|
||||
result := toSlice(taken2)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithReplicate tests Take with Replicate
|
||||
func TestTakeWithReplicate(t *testing.T) {
|
||||
t.Run("takes from replicated sequence", func(t *testing.T) {
|
||||
seq := Replicate(10, 42)
|
||||
result := toSlice(Take[int](3)(seq))
|
||||
assert.Equal(t, []int{42, 42, 42}, result)
|
||||
})
|
||||
|
||||
t.Run("takes all from short replicate", func(t *testing.T) {
|
||||
seq := Replicate(2, "hello")
|
||||
result := toSlice(Take[string](5)(seq))
|
||||
assert.Equal(t, []string{"hello", "hello"}, result)
|
||||
})
|
||||
|
||||
t.Run("takes zero from replicate", func(t *testing.T) {
|
||||
seq := Replicate(5, 100)
|
||||
result := toSlice(Take[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithMakeBy tests Take with MakeBy
|
||||
func TestTakeWithMakeBy(t *testing.T) {
|
||||
t.Run("takes from generated sequence", func(t *testing.T) {
|
||||
seq := MakeBy(10, func(i int) int { return i * i })
|
||||
result := toSlice(Take[int](5)(seq))
|
||||
assert.Equal(t, []int{0, 1, 4, 9, 16}, result)
|
||||
})
|
||||
|
||||
t.Run("takes more than generated", func(t *testing.T) {
|
||||
seq := MakeBy(3, func(i int) int { return i + 1 })
|
||||
result := toSlice(Take[int](10)(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithPrependAppend tests Take with Prepend and Append
|
||||
func TestTakeWithPrependAppend(t *testing.T) {
|
||||
t.Run("take from prepended sequence", func(t *testing.T) {
|
||||
seq := From(2, 3, 4, 5)
|
||||
prepended := Prepend(1)(seq)
|
||||
result := toSlice(Take[int](3)(prepended))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("take from appended sequence", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(Take[int](2)(appended))
|
||||
assert.Equal(t, []int{1, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("take includes appended element", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
appended := Append(4)(seq)
|
||||
result := toSlice(Take[int](4)(appended))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeWithFlatten tests Take with Flatten
|
||||
func TestTakeWithFlatten(t *testing.T) {
|
||||
t.Run("takes from flattened sequence", func(t *testing.T) {
|
||||
nested := From(From(1, 2), From(3, 4), From(5, 6))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(Take[int](4)(flattened))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("takes from flattened with empty inner sequences", func(t *testing.T) {
|
||||
nested := From(From(1, 2), Empty[int](), From(3, 4))
|
||||
flattened := Flatten(nested)
|
||||
result := toSlice(Take[int](3)(flattened))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeDoesNotConsumeEntireSequence tests that Take is lazy
|
||||
func TestTakeDoesNotConsumeEntireSequence(t *testing.T) {
|
||||
t.Run("only consumes needed elements", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := MonadMap(From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), func(x int) int {
|
||||
callCount++
|
||||
return x * 2
|
||||
})
|
||||
|
||||
taken := Take[int](3)(seq)
|
||||
|
||||
// Manually iterate to verify lazy evaluation
|
||||
result := []int{}
|
||||
for v := range taken {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
// The map function may be called one extra time to check if there are more elements
|
||||
// This is expected behavior with Go's range over iterators
|
||||
assert.LessOrEqual(t, callCount, 4, "should not consume significantly more than needed")
|
||||
assert.GreaterOrEqual(t, callCount, 3, "should consume at least the needed elements")
|
||||
})
|
||||
|
||||
t.Run("stops early with filter", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
filtered := MonadFilter(seq, func(x int) bool {
|
||||
callCount++
|
||||
return x%2 == 0
|
||||
})
|
||||
|
||||
taken := Take[int](2)(filtered)
|
||||
|
||||
// Manually iterate to verify lazy evaluation
|
||||
result := []int{}
|
||||
for v := range taken {
|
||||
result = append(result, v)
|
||||
}
|
||||
|
||||
assert.Equal(t, []int{2, 4}, result)
|
||||
// Should stop after finding 2 even numbers, may check a few more elements
|
||||
assert.LessOrEqual(t, callCount, 7, "should not consume significantly more than needed")
|
||||
assert.GreaterOrEqual(t, callCount, 4, "should consume at least enough to find 2 evens")
|
||||
})
|
||||
}
|
||||
|
||||
// TestTakeEdgeCases tests edge cases
|
||||
func TestTakeEdgeCases(t *testing.T) {
|
||||
t.Run("take 1 from single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Take[int](1)(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("take 0 from single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(Take[int](0)(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("take large number from small sequence", func(t *testing.T) {
|
||||
seq := From(1, 2)
|
||||
result := toSlice(Take[int](1000000)(seq))
|
||||
assert.Equal(t, []int{1, 2}, result)
|
||||
})
|
||||
|
||||
t.Run("take with very large n", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
result := toSlice(Take[int](int(^uint(0) >> 1))(seq)) // max int
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkTake(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
taken := Take[int](5)(seq)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeLargeSequence(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
taken := Take[int](100)(seq)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWithMap(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mapped := MonadMap(seq, N.Mul(2))
|
||||
taken := Take[int](5)(mapped)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTakeWithFilter(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
taken := Take[int](3)(filtered)
|
||||
for range taken {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleTake() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
taken := Take[int](3)(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleTake_moreThanAvailable() {
|
||||
seq := From(1, 2, 3)
|
||||
taken := Take[int](10)(seq)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3
|
||||
}
|
||||
|
||||
func ExampleTake_zero() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
taken := Take[int](0)(seq)
|
||||
|
||||
count := 0
|
||||
for range taken {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
|
||||
func ExampleTake_withFilter() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
evens := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
taken := Take[int](3)(evens)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6
|
||||
}
|
||||
|
||||
func ExampleTake_withMap() {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
doubled := MonadMap(seq, N.Mul(2))
|
||||
taken := Take[int](3)(doubled)
|
||||
|
||||
for v := range taken {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 2 4 6
|
||||
}
|
||||
|
||||
func ExampleTake_chained() {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := Take[int](5)(
|
||||
MonadFilter(seq, func(x int) bool { return x > 3 }),
|
||||
)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 4 5 6 7 8
|
||||
}
|
||||
@@ -18,8 +18,12 @@ package iter
|
||||
import (
|
||||
I "iter"
|
||||
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/iterator/stateless"
|
||||
"github.com/IBM/fp-go/v2/optics/lens/option"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
@@ -54,4 +58,12 @@ type (
|
||||
|
||||
// Operator2 represents a transformation from one key-value sequence to another.
|
||||
Operator2[K, A, B any] = Kleisli2[K, Seq2[K, A], B]
|
||||
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Prism[S, A any] = prism.Prism[S, A]
|
||||
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Pair[A, B any] = pair.Pair[A, B]
|
||||
)
|
||||
|
||||
167
v2/iterator/iter/uniq.go
Normal file
167
v2/iterator/iter/uniq.go
Normal file
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import F "github.com/IBM/fp-go/v2/function"
|
||||
|
||||
// Uniq returns an operator that filters a sequence to contain only unique elements,
|
||||
// where uniqueness is determined by a key extraction function.
|
||||
//
|
||||
// This function takes a key extraction function and returns an operator that removes
|
||||
// duplicate elements from a sequence. Two elements are considered duplicates if the
|
||||
// key extraction function returns the same key for both. Only the first occurrence
|
||||
// of each unique key is kept in the output sequence.
|
||||
//
|
||||
// The operation maintains a map of seen keys internally, so memory usage grows with
|
||||
// the number of unique keys encountered. The operation is lazy - elements are processed
|
||||
// and filtered as they are consumed.
|
||||
//
|
||||
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the sequence
|
||||
// - K: The type of the key used for uniqueness comparison (must be comparable)
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that extracts a comparable key from each element
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that filters the sequence to contain only unique elements based on the key
|
||||
//
|
||||
// Example - Remove duplicate integers:
|
||||
//
|
||||
// seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
// unique := Uniq(func(x int) int { return x })
|
||||
// result := unique(seq)
|
||||
// // yields: 1, 2, 3, 4, 5
|
||||
//
|
||||
// Example - Unique by string length:
|
||||
//
|
||||
// seq := From("a", "bb", "c", "dd", "eee")
|
||||
// uniqueByLength := Uniq(func(s string) int { return len(s) })
|
||||
// result := uniqueByLength(seq)
|
||||
// // yields: "a", "bb", "eee" (first occurrence of each length)
|
||||
//
|
||||
// Example - Unique structs by field:
|
||||
//
|
||||
// type Person struct { ID int; Name string }
|
||||
// seq := From(
|
||||
// Person{1, "Alice"},
|
||||
// Person{2, "Bob"},
|
||||
// Person{1, "Alice2"}, // duplicate ID
|
||||
// )
|
||||
// uniqueByID := Uniq(func(p Person) int { return p.ID })
|
||||
// result := uniqueByID(seq)
|
||||
// // yields: Person{1, "Alice"}, Person{2, "Bob"}
|
||||
//
|
||||
// Example - Case-insensitive unique strings:
|
||||
//
|
||||
// seq := From("Hello", "world", "HELLO", "World", "test")
|
||||
// uniqueCaseInsensitive := Uniq(func(s string) string {
|
||||
// return strings.ToLower(s)
|
||||
// })
|
||||
// result := uniqueCaseInsensitive(seq)
|
||||
// // yields: "Hello", "world", "test"
|
||||
//
|
||||
// Example - Empty sequence:
|
||||
//
|
||||
// seq := Empty[int]()
|
||||
// unique := Uniq(func(x int) int { return x })
|
||||
// result := unique(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - All duplicates:
|
||||
//
|
||||
// seq := From(1, 1, 1, 1)
|
||||
// unique := Uniq(func(x int) int { return x })
|
||||
// result := unique(seq)
|
||||
// // yields: 1 (only first occurrence)
|
||||
func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
|
||||
return func(s Seq[A]) Seq[A] {
|
||||
return func(yield func(A) bool) {
|
||||
items := make(map[K]struct{})
|
||||
for a := range s {
|
||||
k := f(a)
|
||||
if _, ok := items[k]; !ok {
|
||||
items[k] = struct{}{}
|
||||
if !yield(a) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StrictUniq filters a sequence to contain only unique elements using direct comparison.
|
||||
//
|
||||
// This is a convenience function that uses the identity function as the key extractor,
|
||||
// meaning elements are compared directly for uniqueness. It's equivalent to calling
|
||||
// Uniq with the identity function, but provides a simpler API when the elements
|
||||
// themselves are comparable.
|
||||
//
|
||||
// The operation maintains a map of seen elements internally, so memory usage grows with
|
||||
// the number of unique elements. Only the first occurrence of each unique element is kept.
|
||||
//
|
||||
// RxJS Equivalent: [distinct] - https://rxjs.dev/api/operators/distinct
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the sequence (must be comparable)
|
||||
//
|
||||
// Parameters:
|
||||
// - as: The input sequence to filter for unique elements
|
||||
//
|
||||
// Returns:
|
||||
// - A sequence containing only the first occurrence of each unique element
|
||||
//
|
||||
// Example - Remove duplicate integers:
|
||||
//
|
||||
// seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: 1, 2, 3, 4, 5
|
||||
//
|
||||
// Example - Remove duplicate strings:
|
||||
//
|
||||
// seq := From("apple", "banana", "apple", "cherry", "banana")
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: "apple", "banana", "cherry"
|
||||
//
|
||||
// Example - Single element:
|
||||
//
|
||||
// seq := From(42)
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: 42
|
||||
//
|
||||
// Example - All duplicates:
|
||||
//
|
||||
// seq := From("x", "x", "x")
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: "x" (only first occurrence)
|
||||
//
|
||||
// Example - Empty sequence:
|
||||
//
|
||||
// seq := Empty[int]()
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: nothing (empty sequence)
|
||||
//
|
||||
// Example - Already unique:
|
||||
//
|
||||
// seq := From(1, 2, 3, 4, 5)
|
||||
// result := StrictUniq(seq)
|
||||
// // yields: 1, 2, 3, 4, 5 (no changes)
|
||||
func StrictUniq[A comparable](as Seq[A]) Seq[A] {
|
||||
return Uniq(F.Identity[A])(as)
|
||||
}
|
||||
433
v2/iterator/iter/uniq_test.go
Normal file
433
v2/iterator/iter/uniq_test.go
Normal file
@@ -0,0 +1,433 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package iter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestUniqBasic tests basic Uniq functionality
|
||||
func TestUniqBasic(t *testing.T) {
|
||||
t.Run("removes duplicate integers", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("removes duplicate strings", func(t *testing.T) {
|
||||
seq := From("apple", "banana", "apple", "cherry", "banana")
|
||||
unique := Uniq(F.Identity[string])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []string{"apple", "banana", "cherry"}, result)
|
||||
})
|
||||
|
||||
t.Run("keeps first occurrence", func(t *testing.T) {
|
||||
seq := From(1, 2, 1, 3, 2, 4)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4}, result)
|
||||
})
|
||||
|
||||
t.Run("single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("all duplicates", func(t *testing.T) {
|
||||
seq := From(5, 5, 5, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{5}, result)
|
||||
})
|
||||
|
||||
t.Run("already unique", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqEmpty tests Uniq with empty sequences
|
||||
func TestUniqEmpty(t *testing.T) {
|
||||
t.Run("empty integer sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("empty string sequence", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
unique := Uniq(F.Identity[string])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqWithKeyExtractor tests Uniq with custom key extraction
|
||||
func TestUniqWithKeyExtractor(t *testing.T) {
|
||||
t.Run("unique by string length", func(t *testing.T) {
|
||||
seq := From("a", "bb", "c", "dd", "eee", "f")
|
||||
uniqueByLength := Uniq(S.Size)
|
||||
result := toSlice(uniqueByLength(seq))
|
||||
assert.Equal(t, []string{"a", "bb", "eee"}, result)
|
||||
})
|
||||
|
||||
t.Run("unique by absolute value", func(t *testing.T) {
|
||||
seq := From(1, -1, 2, -2, 3, 1, -3)
|
||||
uniqueByAbs := Uniq(func(x int) int {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
})
|
||||
result := toSlice(uniqueByAbs(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("case-insensitive unique strings", func(t *testing.T) {
|
||||
seq := From("Hello", "world", "HELLO", "World", "test")
|
||||
uniqueCaseInsensitive := Uniq(strings.ToLower)
|
||||
result := toSlice(uniqueCaseInsensitive(seq))
|
||||
assert.Equal(t, []string{"Hello", "world", "test"}, result)
|
||||
})
|
||||
|
||||
t.Run("unique by modulo", func(t *testing.T) {
|
||||
seq := From(1, 4, 7, 2, 5, 8, 3)
|
||||
uniqueByMod3 := Uniq(func(x int) int { return x % 3 })
|
||||
result := toSlice(uniqueByMod3(seq))
|
||||
assert.Equal(t, []int{1, 2, 3}, result) // 1%3=1, 4%3=1 (dup), 7%3=1 (dup), 2%3=2, 5%3=2 (dup), 8%3=2 (dup), 3%3=0
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqWithComplexTypes tests Uniq with structs and complex types
|
||||
func TestUniqWithComplexTypes(t *testing.T) {
|
||||
type Person struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
t.Run("unique structs by ID", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{1, "Alice"},
|
||||
Person{2, "Bob"},
|
||||
Person{1, "Alice2"}, // duplicate ID
|
||||
Person{3, "Charlie"},
|
||||
Person{2, "Bob2"}, // duplicate ID
|
||||
)
|
||||
uniqueByID := Uniq(func(p Person) int { return p.ID })
|
||||
result := toSlice(uniqueByID(seq))
|
||||
assert.Equal(t, []Person{
|
||||
{1, "Alice"},
|
||||
{2, "Bob"},
|
||||
{3, "Charlie"},
|
||||
}, result)
|
||||
})
|
||||
|
||||
t.Run("unique structs by name", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{1, "Alice"},
|
||||
Person{2, "Bob"},
|
||||
Person{3, "Alice"}, // duplicate name
|
||||
)
|
||||
uniqueByName := Uniq(func(p Person) string { return p.Name })
|
||||
result := toSlice(uniqueByName(seq))
|
||||
assert.Equal(t, []Person{
|
||||
{1, "Alice"},
|
||||
{2, "Bob"},
|
||||
}, result)
|
||||
})
|
||||
|
||||
t.Run("unique slices by length", func(t *testing.T) {
|
||||
seq := From([]int{1, 2}, []int{3}, []int{4, 5}, []int{6})
|
||||
uniqueByLength := Uniq(func(s []int) int { return len(s) })
|
||||
result := toSlice(uniqueByLength(seq))
|
||||
assert.Len(t, result, 2)
|
||||
assert.Equal(t, 2, len(result[0]))
|
||||
assert.Equal(t, 1, len(result[1]))
|
||||
})
|
||||
}
|
||||
|
||||
// TestStrictUniq tests StrictUniq functionality
|
||||
func TestStrictUniq(t *testing.T) {
|
||||
t.Run("removes duplicate integers", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("removes duplicate strings", func(t *testing.T) {
|
||||
seq := From("apple", "banana", "apple", "cherry", "banana")
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []string{"apple", "banana", "cherry"}, result)
|
||||
})
|
||||
|
||||
t.Run("single element", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []int{42}, result)
|
||||
})
|
||||
|
||||
t.Run("all duplicates", func(t *testing.T) {
|
||||
seq := From("x", "x", "x")
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []string{"x"}, result)
|
||||
})
|
||||
|
||||
t.Run("empty sequence", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("already unique", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
|
||||
t.Run("boolean values", func(t *testing.T) {
|
||||
seq := From(true, false, true, false, true)
|
||||
result := toSlice(StrictUniq(seq))
|
||||
assert.Equal(t, []bool{true, false}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqWithChainedOperations tests Uniq combined with other operations
|
||||
func TestUniqWithChainedOperations(t *testing.T) {
|
||||
t.Run("uniq then map", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1)
|
||||
unique := Uniq(F.Identity[int])
|
||||
mapped := MonadMap(unique(seq), func(x int) int { return x * 2 })
|
||||
result := toSlice(mapped)
|
||||
assert.Equal(t, []int{2, 4, 6, 8}, result)
|
||||
})
|
||||
|
||||
t.Run("map then uniq", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
mapped := MonadMap(seq, func(x int) int { return x % 3 })
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(mapped))
|
||||
assert.Equal(t, []int{1, 2, 0}, result)
|
||||
})
|
||||
|
||||
t.Run("filter then uniq", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 2, 4, 6)
|
||||
filtered := MonadFilter(seq, func(x int) bool { return x%2 == 0 })
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(filtered))
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("uniq then filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5, 6)
|
||||
unique := Uniq(F.Identity[int])
|
||||
filtered := MonadFilter(unique(seq), func(x int) bool { return x%2 == 0 })
|
||||
result := toSlice(filtered)
|
||||
assert.Equal(t, []int{2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("uniq then take", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
taken := Take[int](3)(unique(seq))
|
||||
result := toSlice(taken)
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
|
||||
t.Run("take then uniq", func(t *testing.T) {
|
||||
seq := From(1, 2, 1, 3, 2, 4, 5)
|
||||
taken := Take[int](5)(seq)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(taken))
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqEarlyTermination tests that Uniq respects early termination
|
||||
func TestUniqEarlyTermination(t *testing.T) {
|
||||
t.Run("terminates when yield returns false", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 2, 6, 7)
|
||||
unique := Uniq(F.Identity[int])
|
||||
|
||||
count := 0
|
||||
for v := range unique(seq) {
|
||||
count++
|
||||
if v >= 4 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, 4, count) // Should stop at 4
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqLargeSequence tests Uniq with larger sequences
|
||||
func TestUniqLargeSequence(t *testing.T) {
|
||||
t.Run("uniq large sequence with many duplicates", func(t *testing.T) {
|
||||
// Create sequence with repeating pattern
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i % 10 // Only 10 unique values
|
||||
}
|
||||
seq := From(data...)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
|
||||
assert.Len(t, result, 10)
|
||||
for i := 0; i < 10; i++ {
|
||||
assert.Equal(t, i, result[i])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("uniq large sequence all unique", func(t *testing.T) {
|
||||
data := make([]int, 100)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := From(data...)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
|
||||
assert.Len(t, result, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
assert.Equal(t, i, result[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestUniqPreservesOrder tests that Uniq maintains element order
|
||||
func TestUniqPreservesOrder(t *testing.T) {
|
||||
t.Run("maintains order of first occurrences", func(t *testing.T) {
|
||||
seq := From(5, 3, 5, 1, 3, 2, 1, 4)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := toSlice(unique(seq))
|
||||
assert.Equal(t, []int{5, 3, 1, 2, 4}, result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkUniq(b *testing.B) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5, 3, 6, 4, 7, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range unique(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStrictUniq(b *testing.B) {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5, 3, 6, 4, 7, 5)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range StrictUniq(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUniqLarge(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i % 100
|
||||
}
|
||||
seq := From(data...)
|
||||
unique := Uniq(F.Identity[int])
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for range unique(seq) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleUniq() {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := unique(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 4 5
|
||||
}
|
||||
|
||||
func ExampleUniq_byLength() {
|
||||
seq := From("a", "bb", "c", "dd", "eee")
|
||||
uniqueByLength := Uniq(func(s string) int { return len(s) })
|
||||
result := uniqueByLength(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: a bb eee
|
||||
}
|
||||
|
||||
func ExampleUniq_caseInsensitive() {
|
||||
seq := From("Hello", "world", "HELLO", "World", "test")
|
||||
uniqueCaseInsensitive := Uniq(func(s string) string {
|
||||
return strings.ToLower(s)
|
||||
})
|
||||
result := uniqueCaseInsensitive(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: Hello world test
|
||||
}
|
||||
|
||||
func ExampleStrictUniq() {
|
||||
seq := From(1, 2, 3, 2, 4, 1, 5)
|
||||
result := StrictUniq(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%d ", v)
|
||||
}
|
||||
// Output: 1 2 3 4 5
|
||||
}
|
||||
|
||||
func ExampleStrictUniq_strings() {
|
||||
seq := From("apple", "banana", "apple", "cherry", "banana")
|
||||
result := StrictUniq(seq)
|
||||
|
||||
for v := range result {
|
||||
fmt.Printf("%s ", v)
|
||||
}
|
||||
// Output: apple banana cherry
|
||||
}
|
||||
|
||||
func ExampleUniq_empty() {
|
||||
seq := Empty[int]()
|
||||
unique := Uniq(F.Identity[int])
|
||||
result := unique(seq)
|
||||
|
||||
count := 0
|
||||
for range result {
|
||||
count++
|
||||
}
|
||||
fmt.Printf("Count: %d\n", count)
|
||||
// Output: Count: 0
|
||||
}
|
||||
@@ -19,8 +19,28 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
|
||||
)
|
||||
|
||||
// DropWhile creates an [Iterator] that drops elements from the [Iterator] as long as the predicate is true; afterwards, returns every element.
|
||||
// Note, the [Iterator] does not produce any output until the predicate first becomes false
|
||||
// Cycle creates an [Iterator] that repeats the elements of the input [Iterator] indefinitely.
|
||||
// The iterator cycles through all elements of the input, and when it reaches the end, it starts over from the beginning.
|
||||
// This creates an infinite iterator, so it should be used with caution and typically combined with operations that limit the output.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the iterator
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The input iterator to cycle through
|
||||
//
|
||||
// Returns:
|
||||
// - An iterator that infinitely repeats the elements of the input iterator
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// iter := stateless.FromArray([]int{1, 2, 3})
|
||||
// cycled := stateless.Cycle(iter)
|
||||
// // Produces: 1, 2, 3, 1, 2, 3, 1, 2, 3, ... (infinitely)
|
||||
//
|
||||
// // Typically used with Take to limit output:
|
||||
// limited := stateless.Take(7)(cycled)
|
||||
// // Produces: 1, 2, 3, 1, 2, 3, 1
|
||||
func Cycle[U any](ma Iterator[U]) Iterator[U] {
|
||||
return G.Cycle(ma)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,39 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
|
||||
)
|
||||
|
||||
// First returns the first item in an iterator if such an item exists
|
||||
// First returns the first element from an [Iterator] wrapped in an [Option].
|
||||
//
|
||||
// This function attempts to retrieve the first element from the iterator. If the iterator
|
||||
// contains at least one element, it returns Some(element). If the iterator is empty,
|
||||
// it returns None. The function consumes only the first element of the iterator.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - U: The type of elements in the iterator
|
||||
//
|
||||
// Parameters:
|
||||
// - mu: The input iterator to get the first element from
|
||||
//
|
||||
// Returns:
|
||||
// - Option[U]: Some(first element) if the iterator is non-empty, None otherwise
|
||||
//
|
||||
// Example with non-empty iterator:
|
||||
//
|
||||
// iter := stateless.From(1, 2, 3, 4, 5)
|
||||
// first := stateless.First(iter)
|
||||
// // Returns: Some(1)
|
||||
//
|
||||
// Example with empty iterator:
|
||||
//
|
||||
// iter := stateless.Empty[int]()
|
||||
// first := stateless.First(iter)
|
||||
// // Returns: None
|
||||
//
|
||||
// Example with filtered iterator:
|
||||
//
|
||||
// iter := stateless.From(1, 2, 3, 4, 5)
|
||||
// filtered := stateless.Filter(func(x int) bool { return x > 3 })(iter)
|
||||
// first := stateless.First(filtered)
|
||||
// // Returns: Some(4)
|
||||
func First[U any](mu Iterator[U]) Option[U] {
|
||||
return G.First(mu)
|
||||
}
|
||||
|
||||
@@ -16,26 +16,240 @@
|
||||
package stateless
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestFirst tests getting the first element from a non-empty iterator
|
||||
func TestFirst(t *testing.T) {
|
||||
t.Run("returns first element from integer iterator", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
})
|
||||
|
||||
seq := From(1, 2, 3)
|
||||
t.Run("returns first element from string iterator", func(t *testing.T) {
|
||||
seq := From("a", "b", "c")
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of("a"), fst)
|
||||
})
|
||||
|
||||
fst := First(seq)
|
||||
t.Run("returns first element from single element iterator", func(t *testing.T) {
|
||||
seq := From(42)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(42), fst)
|
||||
})
|
||||
|
||||
assert.Equal(t, O.Of(1), fst)
|
||||
t.Run("returns first element from large iterator", func(t *testing.T) {
|
||||
seq := From(100, 200, 300, 400, 500)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(100), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestNoFirst tests getting the first element from an empty iterator
|
||||
func TestNoFirst(t *testing.T) {
|
||||
t.Run("returns None for empty integer iterator", func(t *testing.T) {
|
||||
seq := Empty[int]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
|
||||
seq := Empty[int]()
|
||||
t.Run("returns None for empty string iterator", func(t *testing.T) {
|
||||
seq := Empty[string]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[string](), fst)
|
||||
})
|
||||
|
||||
fst := First(seq)
|
||||
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
t.Run("returns None for empty struct iterator", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Value int
|
||||
}
|
||||
seq := Empty[TestStruct]()
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.None[TestStruct](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithFiltered tests First with filtered iterators
|
||||
func TestFirstWithFiltered(t *testing.T) {
|
||||
t.Run("returns first element matching filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
filtered := Filter(N.MoreThan(3))(seq)
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.Of(4), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None when no elements match filter", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
filtered := Filter(N.MoreThan(10))(seq)
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first even number", func(t *testing.T) {
|
||||
seq := From(1, 3, 5, 6, 7, 8)
|
||||
filtered := Filter(func(x int) bool { return x%2 == 0 })(seq)
|
||||
fst := First(filtered)
|
||||
assert.Equal(t, O.Of(6), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithMapped tests First with mapped iterators
|
||||
func TestFirstWithMapped(t *testing.T) {
|
||||
t.Run("returns first element after mapping", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := Map(N.Mul(2))(seq)
|
||||
fst := First(mapped)
|
||||
assert.Equal(t, O.Of(2), fst)
|
||||
})
|
||||
|
||||
t.Run("returns first string after mapping", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
mapped := Map(S.Format[int]("num-%d"))(seq)
|
||||
fst := First(mapped)
|
||||
assert.Equal(t, O.Of("num-1"), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithTake tests First with Take
|
||||
func TestFirstWithTake(t *testing.T) {
|
||||
t.Run("returns first element from taken subset", func(t *testing.T) {
|
||||
seq := From(10, 20, 30, 40, 50)
|
||||
taken := Take[int](3)(seq)
|
||||
fst := First(taken)
|
||||
assert.Equal(t, O.Of(10), fst)
|
||||
})
|
||||
|
||||
t.Run("returns None when taking zero elements", func(t *testing.T) {
|
||||
seq := From(1, 2, 3)
|
||||
taken := Take[int](0)(seq)
|
||||
fst := First(taken)
|
||||
assert.Equal(t, O.None[int](), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithComplex tests First with complex types
|
||||
func TestFirstWithComplex(t *testing.T) {
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
t.Run("returns first person", func(t *testing.T) {
|
||||
seq := From(
|
||||
Person{"Alice", 30},
|
||||
Person{"Bob", 25},
|
||||
Person{"Charlie", 35},
|
||||
)
|
||||
fst := First(seq)
|
||||
expected := O.Of(Person{"Alice", 30})
|
||||
assert.Equal(t, expected, fst)
|
||||
})
|
||||
|
||||
t.Run("returns first pointer", func(t *testing.T) {
|
||||
p1 := &Person{"Alice", 30}
|
||||
p2 := &Person{"Bob", 25}
|
||||
seq := From(p1, p2)
|
||||
fst := First(seq)
|
||||
assert.Equal(t, O.Of(p1), fst)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstDoesNotConsumeEntireIterator tests that First only consumes the first element
|
||||
func TestFirstDoesNotConsumeEntireIterator(t *testing.T) {
|
||||
t.Run("only consumes first element", func(t *testing.T) {
|
||||
callCount := 0
|
||||
seq := Map(func(x int) int {
|
||||
callCount++
|
||||
return x * 2
|
||||
})(From(1, 2, 3, 4, 5))
|
||||
|
||||
fst := First(seq)
|
||||
|
||||
assert.Equal(t, O.Of(2), fst)
|
||||
// Should only have called the map function once for the first element
|
||||
assert.Equal(t, 1, callCount)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFirstWithChainedOperations tests First with multiple chained operations
|
||||
func TestFirstWithChainedOperations(t *testing.T) {
|
||||
t.Run("chains filter, map, and first", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := First(
|
||||
Map(N.Mul(10))(
|
||||
Filter(N.MoreThan(5))(seq),
|
||||
),
|
||||
)
|
||||
assert.Equal(t, O.Of(60), result)
|
||||
})
|
||||
|
||||
t.Run("chains take, filter, and first", func(t *testing.T) {
|
||||
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
|
||||
result := First(
|
||||
Filter(N.MoreThan(3))(
|
||||
Take[int](7)(seq),
|
||||
),
|
||||
)
|
||||
assert.Equal(t, O.Of(4), result)
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkFirst(b *testing.B) {
|
||||
seq := From(1, 2, 3, 4, 5)
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
First(seq)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFirstLargeIterator(b *testing.B) {
|
||||
data := make([]int, 1000)
|
||||
for i := range data {
|
||||
data[i] = i
|
||||
}
|
||||
seq := FromArray(data)
|
||||
|
||||
for b.Loop() {
|
||||
First(seq)
|
||||
}
|
||||
}
|
||||
|
||||
// Example tests for documentation
|
||||
func ExampleFirst() {
|
||||
iter := From(1, 2, 3, 4, 5)
|
||||
first := First(iter)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First element: %d\n", value)
|
||||
}
|
||||
// Output: First element: 1
|
||||
}
|
||||
|
||||
func ExampleFirst_empty() {
|
||||
iter := Empty[int]()
|
||||
first := First(iter)
|
||||
|
||||
if _, ok := O.Unwrap(first); !ok {
|
||||
fmt.Println("Iterator is empty")
|
||||
}
|
||||
// Output: Iterator is empty
|
||||
}
|
||||
|
||||
func ExampleFirst_filtered() {
|
||||
iter := From(1, 2, 3, 4, 5)
|
||||
filtered := Filter(N.MoreThan(3))(iter)
|
||||
first := First(filtered)
|
||||
|
||||
if value, ok := O.Unwrap(first); ok {
|
||||
fmt.Printf("First element > 3: %d\n", value)
|
||||
}
|
||||
// Output: First element > 3: 4
|
||||
}
|
||||
|
||||
@@ -64,10 +64,10 @@ updated := nameLens.Set("Bob")(person)
|
||||
|
||||
## 🛠️ Core Optics Types
|
||||
|
||||
### 🔎 [Lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens) - Product Types ([Structs](https://go.dev/ref/spec#Struct_types))
|
||||
Focus on a single field within a [struct](https://go.dev/ref/spec#Struct_types). Provides get and set operations.
|
||||
### 🔎 [Lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens) - Product Types (Structs)
|
||||
Focus on a single field within a struct. Provides get and set operations.
|
||||
|
||||
**Use when:** Working with [struct](https://go.dev/ref/spec#Struct_types) fields that always exist.
|
||||
**Use when:** Working with struct fields that always exist.
|
||||
|
||||
```go
|
||||
ageLens := lens.MakeLens(
|
||||
@@ -218,10 +218,10 @@ Lenses can be automatically generated using the `fp-go` CLI tool and a simple an
|
||||
|
||||
### 📝 How to Use
|
||||
|
||||
1. **Annotate your [struct](https://go.dev/ref/spec#Struct_types)** with the `fp-go:Lens` comment:
|
||||
1. **Annotate your struct** with the `fp-go:Lens` comment:
|
||||
|
||||
```go
|
||||
//go:generate go run github.com/IBM/fp-go/v2 lens
|
||||
//go:generate go run github.com/IBM/fp-go/v2/main.go lens --dir . --filename gen_lens.go
|
||||
|
||||
// fp-go:Lens
|
||||
type Person struct {
|
||||
@@ -232,7 +232,7 @@ type Person struct {
|
||||
}
|
||||
```
|
||||
|
||||
2. **Run [`go generate`](https://go.dev/blog/generate)**:
|
||||
2. **Run `go generate`**:
|
||||
|
||||
```bash
|
||||
go generate ./...
|
||||
@@ -256,7 +256,7 @@ personWithEmail := lenses.EmailO.Set(option.Some("new@example.com"))(person)
|
||||
|
||||
### 🎁 What Gets Generated
|
||||
|
||||
For each annotated [struct](https://go.dev/ref/spec#Struct_types), the generator creates:
|
||||
For each annotated struct, the generator creates:
|
||||
|
||||
- **`StructNameLenses`**: Lenses for value types with optional variants (`LensO`) for comparable fields
|
||||
- **`StructNameRefLenses`**: Lenses for pointer types with prisms for constructing values
|
||||
@@ -264,8 +264,8 @@ For each annotated [struct](https://go.dev/ref/spec#Struct_types), the generator
|
||||
- Constructor functions: `MakeStructNameLenses()`, `MakeStructNameRefLenses()`, `MakeStructNamePrisms()`
|
||||
|
||||
The generator supports:
|
||||
- ✅ [Generic types](https://go.dev/doc/tutorial/generics) with type parameters
|
||||
- ✅ Embedded [structs](https://go.dev/ref/spec#Struct_types) (fields are promoted)
|
||||
- ✅ Generic types with type parameters
|
||||
- ✅ Embedded structs (fields are promoted)
|
||||
- ✅ Optional fields (pointers and `omitempty` tags)
|
||||
- ✅ Custom package imports
|
||||
|
||||
@@ -273,7 +273,7 @@ See [samples/lens](../samples/lens) for complete examples.
|
||||
|
||||
## 📊 Optics Hierarchy
|
||||
|
||||
```markdown
|
||||
```
|
||||
[Iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso)[S, A]
|
||||
↓
|
||||
[Lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens)[S, A]
|
||||
|
||||
84
v2/optics/iso/format.go
Normal file
84
v2/optics/iso/format.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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 iso
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
)
|
||||
|
||||
// String returns a string representation of the isomorphism.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// tempIso := iso.MakeIso(...)
|
||||
// fmt.Println(tempIso) // Prints: "Iso"
|
||||
func (i Iso[S, T]) String() string {
|
||||
return "Iso"
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Iso.
|
||||
// Supports all standard format verbs:
|
||||
// - %s, %v, %+v: uses String() representation
|
||||
// - %#v: uses GoString() representation
|
||||
// - %q: quoted String() representation
|
||||
// - other verbs: uses String() representation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// tempIso := iso.MakeIso(...)
|
||||
// fmt.Printf("%s", tempIso) // "Iso"
|
||||
// fmt.Printf("%v", tempIso) // "Iso"
|
||||
// fmt.Printf("%#v", tempIso) // "iso.Iso[Celsius, Fahrenheit]"
|
||||
//
|
||||
//go:noinline
|
||||
func (i Iso[S, T]) Format(f fmt.State, c rune) {
|
||||
formatting.FmtString(i, f, c)
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Iso.
|
||||
// Returns a Go-syntax representation of the Iso value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// tempIso := iso.MakeIso(...)
|
||||
// tempIso.GoString() // "iso.Iso[Celsius, Fahrenheit]"
|
||||
//
|
||||
//go:noinline
|
||||
func (i Iso[S, T]) GoString() string {
|
||||
return fmt.Sprintf("iso.Iso[%s, %s]",
|
||||
formatting.TypeInfo(new(S)),
|
||||
formatting.TypeInfo(new(T)),
|
||||
)
|
||||
}
|
||||
|
||||
// LogValue implements slog.LogValuer for Iso.
|
||||
// Returns a slog.Value that represents the Iso for structured logging.
|
||||
// Logs the type information as a string value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logger := slog.Default()
|
||||
// tempIso := iso.MakeIso(...)
|
||||
// logger.Info("using iso", "iso", tempIso)
|
||||
// // Logs: {"msg":"using iso","iso":"Iso"}
|
||||
//
|
||||
//go:noinline
|
||||
func (i Iso[S, T]) LogValue() slog.Value {
|
||||
return slog.StringValue("Iso")
|
||||
}
|
||||
@@ -17,8 +17,6 @@
|
||||
package iso
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
@@ -405,11 +403,3 @@ func IMap[S, A, B any](ab func(A) B, ba func(B) A) func(Iso[S, A]) Iso[S, B] {
|
||||
return imap(sa, ab, ba)
|
||||
}
|
||||
}
|
||||
|
||||
func (l Iso[S, T]) String() string {
|
||||
return "Iso"
|
||||
}
|
||||
|
||||
func (l Iso[S, T]) Format(f fmt.State, c rune) {
|
||||
fmt.Fprint(f, l.String())
|
||||
}
|
||||
|
||||
85
v2/optics/lens/format.go
Normal file
85
v2/optics/lens/format.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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 lens
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
)
|
||||
|
||||
// String returns the name of the lens for debugging and display purposes.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nameLens := lens.MakeLensWithName(..., "Person.Name")
|
||||
// fmt.Println(nameLens) // Prints: "Person.Name"
|
||||
func (l Lens[S, T]) String() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Lens.
|
||||
// Supports all standard format verbs:
|
||||
// - %s, %v, %+v: uses String() representation (lens name)
|
||||
// - %#v: uses GoString() representation
|
||||
// - %q: quoted String() representation
|
||||
// - other verbs: uses String() representation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nameLens := lens.MakeLensWithName(..., "Person.Name")
|
||||
// fmt.Printf("%s", nameLens) // "Person.Name"
|
||||
// fmt.Printf("%v", nameLens) // "Person.Name"
|
||||
// fmt.Printf("%#v", nameLens) // "lens.Lens[Person, string]{name: \"Person.Name\"}"
|
||||
//
|
||||
//go:noinline
|
||||
func (l Lens[S, T]) Format(f fmt.State, c rune) {
|
||||
formatting.FmtString(l, f, c)
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Lens.
|
||||
// Returns a Go-syntax representation of the Lens value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nameLens := lens.MakeLensWithName(..., "Person.Name")
|
||||
// nameLens.GoString() // "lens.Lens[Person, string]{name: \"Person.Name\"}"
|
||||
//
|
||||
//go:noinline
|
||||
func (l Lens[S, T]) GoString() string {
|
||||
return fmt.Sprintf("lens.Lens[%s, %s]{name: %q}",
|
||||
formatting.TypeInfo(new(S)),
|
||||
formatting.TypeInfo(new(T)),
|
||||
l.name,
|
||||
)
|
||||
}
|
||||
|
||||
// LogValue implements slog.LogValuer for Lens.
|
||||
// Returns a slog.Value that represents the Lens for structured logging.
|
||||
// Logs the lens name as a string value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logger := slog.Default()
|
||||
// nameLens := lens.MakeLensWithName(..., "Person.Name")
|
||||
// logger.Info("using lens", "lens", nameLens)
|
||||
// // Logs: {"msg":"using lens","lens":"Person.Name"}
|
||||
//
|
||||
//go:noinline
|
||||
func (l Lens[S, T]) LogValue() slog.Value {
|
||||
return slog.StringValue(l.name)
|
||||
}
|
||||
@@ -979,26 +979,3 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[
|
||||
// "Person.Name",
|
||||
// )
|
||||
// fmt.Println(nameLens) // Prints: "Person.Name"
|
||||
func (l Lens[S, T]) String() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
// Format implements the fmt.Formatter interface for custom formatting of lenses.
|
||||
//
|
||||
// This allows lenses to be used with fmt.Printf and related functions with
|
||||
// various format verbs. All format operations delegate to the String() method,
|
||||
// which returns the lens name.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The format state containing formatting options
|
||||
// - c: The format verb (currently unused, all verbs produce the same output)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nameLens := lens.MakeLensWithName(..., "Person.Name")
|
||||
// fmt.Printf("Lens: %v\n", nameLens) // Prints: "Lens: Person.Name"
|
||||
// fmt.Printf("Lens: %s\n", nameLens) // Prints: "Lens: Person.Name"
|
||||
// fmt.Printf("Lens: %q\n", nameLens) // Prints: "Lens: Person.Name"
|
||||
func (l Lens[S, T]) Format(f fmt.State, c rune) {
|
||||
fmt.Fprint(f, l.String())
|
||||
}
|
||||
|
||||
85
v2/optics/optional/format.go
Normal file
85
v2/optics/optional/format.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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 optional
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
)
|
||||
|
||||
// String returns the name of the optional for debugging and display purposes.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fieldOptional := optional.MakeOptionalWithName(..., "Person.Email")
|
||||
// fmt.Println(fieldOptional) // Prints: "Person.Email"
|
||||
func (o Optional[S, T]) String() string {
|
||||
return o.name
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Optional.
|
||||
// Supports all standard format verbs:
|
||||
// - %s, %v, %+v: uses String() representation (optional name)
|
||||
// - %#v: uses GoString() representation
|
||||
// - %q: quoted String() representation
|
||||
// - other verbs: uses String() representation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fieldOptional := optional.MakeOptionalWithName(..., "Person.Email")
|
||||
// fmt.Printf("%s", fieldOptional) // "Person.Email"
|
||||
// fmt.Printf("%v", fieldOptional) // "Person.Email"
|
||||
// fmt.Printf("%#v", fieldOptional) // "optional.Optional[Person, string]{name: \"Person.Email\"}"
|
||||
//
|
||||
//go:noinline
|
||||
func (o Optional[S, T]) Format(f fmt.State, c rune) {
|
||||
formatting.FmtString(o, f, c)
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Optional.
|
||||
// Returns a Go-syntax representation of the Optional value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// fieldOptional := optional.MakeOptionalWithName(..., "Person.Email")
|
||||
// fieldOptional.GoString() // "optional.Optional[Person, string]{name: \"Person.Email\"}"
|
||||
//
|
||||
//go:noinline
|
||||
func (o Optional[S, T]) GoString() string {
|
||||
return fmt.Sprintf("optional.Optional[%s, %s]{name: %q}",
|
||||
formatting.TypeInfo(new(S)),
|
||||
formatting.TypeInfo(new(T)),
|
||||
o.name,
|
||||
)
|
||||
}
|
||||
|
||||
// LogValue implements slog.LogValuer for Optional.
|
||||
// Returns a slog.Value that represents the Optional for structured logging.
|
||||
// Logs the optional name as a string value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logger := slog.Default()
|
||||
// fieldOptional := optional.MakeOptionalWithName(..., "Person.Email")
|
||||
// logger.Info("using optional", "optional", fieldOptional)
|
||||
// // Logs: {"msg":"using optional","optional":"Person.Email"}
|
||||
//
|
||||
//go:noinline
|
||||
func (o Optional[S, T]) LogValue() slog.Value {
|
||||
return slog.StringValue(o.name)
|
||||
}
|
||||
@@ -18,8 +18,6 @@
|
||||
package optional
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
@@ -225,11 +223,3 @@ func IChainAny[S, A any]() Operator[S, any, A] {
|
||||
return ichain(sa, fromAny, toAny)
|
||||
}
|
||||
}
|
||||
|
||||
func (l Optional[S, T]) String() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l Optional[S, T]) Format(f fmt.State, c rune) {
|
||||
fmt.Fprint(f, l.String())
|
||||
}
|
||||
|
||||
85
v2/optics/prism/format.go
Normal file
85
v2/optics/prism/format.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// 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 prism
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
)
|
||||
|
||||
// String returns the name of the prism for debugging and display purposes.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// successPrism := prism.MakePrismWithName(..., "Result.Success")
|
||||
// fmt.Println(successPrism) // Prints: "Result.Success"
|
||||
func (p Prism[S, T]) String() string {
|
||||
return p.name
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Prism.
|
||||
// Supports all standard format verbs:
|
||||
// - %s, %v, %+v: uses String() representation (prism name)
|
||||
// - %#v: uses GoString() representation
|
||||
// - %q: quoted String() representation
|
||||
// - other verbs: uses String() representation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// successPrism := prism.MakePrismWithName(..., "Result.Success")
|
||||
// fmt.Printf("%s", successPrism) // "Result.Success"
|
||||
// fmt.Printf("%v", successPrism) // "Result.Success"
|
||||
// fmt.Printf("%#v", successPrism) // "prism.Prism[Result, int]{name: \"Result.Success\"}"
|
||||
//
|
||||
//go:noinline
|
||||
func (p Prism[S, T]) Format(f fmt.State, c rune) {
|
||||
formatting.FmtString(p, f, c)
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Prism.
|
||||
// Returns a Go-syntax representation of the Prism value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// successPrism := prism.MakePrismWithName(..., "Result.Success")
|
||||
// successPrism.GoString() // "prism.Prism[Result, int]{name: \"Result.Success\"}"
|
||||
//
|
||||
//go:noinline
|
||||
func (p Prism[S, T]) GoString() string {
|
||||
return fmt.Sprintf("prism.Prism[%s, %s]{name: %q}",
|
||||
formatting.TypeInfo(new(S)),
|
||||
formatting.TypeInfo(new(T)),
|
||||
p.name,
|
||||
)
|
||||
}
|
||||
|
||||
// LogValue implements slog.LogValuer for Prism.
|
||||
// Returns a slog.Value that represents the Prism for structured logging.
|
||||
// Logs the prism name as a string value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logger := slog.Default()
|
||||
// successPrism := prism.MakePrismWithName(..., "Result.Success")
|
||||
// logger.Info("using prism", "prism", successPrism)
|
||||
// // Logs: {"msg":"using prism","prism":"Result.Success"}
|
||||
//
|
||||
//go:noinline
|
||||
func (p Prism[S, T]) LogValue() slog.Value {
|
||||
return slog.StringValue(p.name)
|
||||
}
|
||||
@@ -270,11 +270,3 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[
|
||||
return imap(sa, ab, ba)
|
||||
}
|
||||
}
|
||||
|
||||
func (l Prism[S, T]) String() string {
|
||||
return l.name
|
||||
}
|
||||
|
||||
func (l Prism[S, T]) Format(f fmt.State, c rune) {
|
||||
fmt.Fprint(f, l.String())
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package option
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
@@ -48,38 +47,17 @@ type (
|
||||
Operator[A, B any] = Kleisli[Option[A], B]
|
||||
)
|
||||
|
||||
// optString prints some debug info for the object
|
||||
// String implements fmt.Stringer for Option.
|
||||
// Returns a human-readable string representation.
|
||||
//
|
||||
//go:noinline
|
||||
func optString(isSome bool, value any) string {
|
||||
if isSome {
|
||||
return fmt.Sprintf("Some[%T](%v)", value, value)
|
||||
}
|
||||
return fmt.Sprintf("None[%T]", value)
|
||||
}
|
||||
|
||||
// optFormat prints some debug info for the object
|
||||
// Example:
|
||||
//
|
||||
//go:noinline
|
||||
func optFormat(isSome bool, value any, f fmt.State, c rune) {
|
||||
switch c {
|
||||
case 's':
|
||||
fmt.Fprint(f, optString(isSome, value))
|
||||
default:
|
||||
fmt.Fprint(f, optString(isSome, value))
|
||||
}
|
||||
}
|
||||
|
||||
// String prints some debug info for the object
|
||||
// Some(42).String() // "Some[int](42)"
|
||||
// None[int]().String() // "None[int]"
|
||||
func (s Option[A]) String() string {
|
||||
return optString(s.isSome, s.value)
|
||||
}
|
||||
|
||||
// Format prints some debug info for the object
|
||||
func (s Option[A]) Format(f fmt.State, c rune) {
|
||||
optFormat(s.isSome, s.value, f, c)
|
||||
}
|
||||
|
||||
func optMarshalJSON(isSome bool, value any) ([]byte, error) {
|
||||
if isSome {
|
||||
return json.Marshal(value)
|
||||
|
||||
@@ -48,7 +48,7 @@ func ExampleOption_creation() {
|
||||
// Output:
|
||||
// None[int]
|
||||
// Some[string](value)
|
||||
// None[*string]
|
||||
// None[string]
|
||||
// true
|
||||
// None[int]
|
||||
// Some[int](4)
|
||||
|
||||
164
v2/option/examples_format_test.go
Normal file
164
v2/option/examples_format_test.go
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package option_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// ExampleOption_String demonstrates the fmt.Stringer interface implementation.
|
||||
func ExampleOption_String() {
|
||||
some := O.Some(42)
|
||||
none := O.None[int]()
|
||||
|
||||
fmt.Println(some.String())
|
||||
fmt.Println(none.String())
|
||||
|
||||
// Output:
|
||||
// Some[int](42)
|
||||
// None[int]
|
||||
}
|
||||
|
||||
// ExampleOption_GoString demonstrates the fmt.GoStringer interface implementation.
|
||||
func ExampleOption_GoString() {
|
||||
some := O.Some(42)
|
||||
none := O.None[int]()
|
||||
|
||||
fmt.Printf("%#v\n", some)
|
||||
fmt.Printf("%#v\n", none)
|
||||
|
||||
// Output:
|
||||
// option.Some[int](42)
|
||||
// option.None[int]
|
||||
}
|
||||
|
||||
// ExampleOption_Format demonstrates the fmt.Formatter interface implementation.
|
||||
func ExampleOption_Format() {
|
||||
result := O.Some(42)
|
||||
|
||||
// Different format verbs
|
||||
fmt.Printf("%%s: %s\n", result)
|
||||
fmt.Printf("%%v: %v\n", result)
|
||||
fmt.Printf("%%+v: %+v\n", result)
|
||||
fmt.Printf("%%#v: %#v\n", result)
|
||||
|
||||
// Output:
|
||||
// %s: Some[int](42)
|
||||
// %v: Some[int](42)
|
||||
// %+v: Some[int](42)
|
||||
// %#v: option.Some[int](42)
|
||||
}
|
||||
|
||||
// ExampleOption_LogValue demonstrates the slog.LogValuer interface implementation.
|
||||
func ExampleOption_LogValue() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
// Remove time for consistent output
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
|
||||
// Some value
|
||||
someResult := O.Some(42)
|
||||
logger.Info("computation succeeded", "result", someResult)
|
||||
|
||||
// None value
|
||||
noneResult := O.None[int]()
|
||||
logger.Info("computation failed", "result", noneResult)
|
||||
|
||||
// Output:
|
||||
// level=INFO msg="computation succeeded" result.some=42
|
||||
// level=INFO msg="computation failed" result.none={}
|
||||
}
|
||||
|
||||
// ExampleOption_formatting_comparison demonstrates different formatting options.
|
||||
func ExampleOption_formatting_comparison() {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
result := O.Some(user)
|
||||
|
||||
fmt.Printf("String(): %s\n", result.String())
|
||||
fmt.Printf("GoString(): %s\n", result.GoString())
|
||||
fmt.Printf("%%v: %v\n", result)
|
||||
fmt.Printf("%%#v: %#v\n", result)
|
||||
|
||||
// Output:
|
||||
// String(): Some[option_test.User]({123 Alice})
|
||||
// GoString(): option.Some[option_test.User](option_test.User{ID:123, Name:"Alice"})
|
||||
// %v: Some[option_test.User]({123 Alice})
|
||||
// %#v: option.Some[option_test.User](option_test.User{ID:123, Name:"Alice"})
|
||||
}
|
||||
|
||||
// ExampleOption_LogValue_structured demonstrates structured logging with Option.
|
||||
func ExampleOption_LogValue_structured() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
|
||||
// Simulate a computation pipeline
|
||||
compute := func(x int) O.Option[int] {
|
||||
if x < 0 {
|
||||
return O.None[int]()
|
||||
}
|
||||
return O.Some(x * 2)
|
||||
}
|
||||
|
||||
// Log successful computation
|
||||
result1 := compute(21)
|
||||
logger.Info("computation", "input", 21, "output", result1)
|
||||
|
||||
// Log failed computation
|
||||
result2 := compute(-5)
|
||||
logger.Warn("computation", "input", -5, "output", result2)
|
||||
|
||||
// Output:
|
||||
// level=INFO msg=computation input=21 output.some=42
|
||||
// level=WARN msg=computation input=-5 output.none={}
|
||||
}
|
||||
|
||||
// Example_none_formatting demonstrates formatting of None values.
|
||||
func Example_none_formatting() {
|
||||
none := O.None[string]()
|
||||
|
||||
fmt.Printf("String(): %s\n", none.String())
|
||||
fmt.Printf("GoString(): %s\n", none.GoString())
|
||||
fmt.Printf("%%v: %v\n", none)
|
||||
fmt.Printf("%%#v: %#v\n", none)
|
||||
|
||||
// Output:
|
||||
// String(): None[string]
|
||||
// GoString(): option.None[string]
|
||||
// %v: None[string]
|
||||
// %#v: option.None[string]
|
||||
}
|
||||
100
v2/option/format.go
Normal file
100
v2/option/format.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package option
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
)
|
||||
|
||||
const (
|
||||
noneGoTemplate = "option.None[%s]"
|
||||
someGoTemplate = "option.Some[%s](%#v)"
|
||||
|
||||
noneFmtTemplate = "None[%s]"
|
||||
someFmtTemplate = "Some[%s](%v)"
|
||||
)
|
||||
|
||||
// GoString implements fmt.GoStringer for Option.
|
||||
// Returns a Go-syntax representation of the Option value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// Some(42).GoString() // "option.Some[int](42)"
|
||||
// None[int]().GoString() // "option.None[int]()"
|
||||
//
|
||||
//go:noinline
|
||||
func (s Option[A]) GoString() string {
|
||||
if s.isSome {
|
||||
return fmt.Sprintf(someGoTemplate, formatting.TypeInfo(s.value), s.value)
|
||||
}
|
||||
return fmt.Sprintf(noneGoTemplate, formatting.TypeInfo(new(A)))
|
||||
}
|
||||
|
||||
// LogValue implements slog.LogValuer for Option.
|
||||
// Returns a slog.Value that represents the Option for structured logging.
|
||||
// Returns a group value with "some" key for Some values and "none" key for None values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logger := slog.Default()
|
||||
// result := Some(42)
|
||||
// logger.Info("result", "value", result)
|
||||
// // Logs: {"msg":"result","value":{"some":42}}
|
||||
//
|
||||
// empty := None[int]()
|
||||
// logger.Info("empty", "value", empty)
|
||||
// // Logs: {"msg":"empty","value":{"none":{}}}
|
||||
//
|
||||
//go:noinline
|
||||
func (s Option[A]) LogValue() slog.Value {
|
||||
if s.isSome {
|
||||
return slog.GroupValue(slog.Any("some", s.value))
|
||||
}
|
||||
return slog.GroupValue(slog.Any("none", struct{}{}))
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Option.
|
||||
// Supports all standard format verbs:
|
||||
// - %s, %v, %+v: uses String() representation
|
||||
// - %#v: uses GoString() representation
|
||||
// - %q: quoted String() representation
|
||||
// - other verbs: uses String() representation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// opt := Some(42)
|
||||
// fmt.Printf("%s", opt) // "Some[int](42)"
|
||||
// fmt.Printf("%v", opt) // "Some[int](42)"
|
||||
// fmt.Printf("%#v", opt) // "option.Some[int](42)"
|
||||
//
|
||||
//go:noinline
|
||||
func (s Option[A]) Format(f fmt.State, c rune) {
|
||||
formatting.FmtString(s, f, c)
|
||||
}
|
||||
|
||||
// optString prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func optString(isSome bool, value any) string {
|
||||
if isSome {
|
||||
return fmt.Sprintf(someFmtTemplate, formatting.TypeInfo(value), value)
|
||||
}
|
||||
// For None, just show the type without ()
|
||||
return fmt.Sprintf(noneFmtTemplate, formatting.TypeInfo(value))
|
||||
}
|
||||
307
v2/option/format_test.go
Normal file
307
v2/option/format_test.go
Normal file
@@ -0,0 +1,307 @@
|
||||
// Copyright (c) 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package option
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
t.Run("Some value", func(t *testing.T) {
|
||||
opt := Some(42)
|
||||
result := opt.String()
|
||||
assert.Equal(t, "Some[int](42)", result)
|
||||
})
|
||||
|
||||
t.Run("None value", func(t *testing.T) {
|
||||
opt := None[int]()
|
||||
result := opt.String()
|
||||
assert.Equal(t, "None[int]", result)
|
||||
})
|
||||
|
||||
t.Run("Some with string", func(t *testing.T) {
|
||||
opt := Some("hello")
|
||||
result := opt.String()
|
||||
assert.Equal(t, "Some[string](hello)", result)
|
||||
})
|
||||
|
||||
t.Run("None with string", func(t *testing.T) {
|
||||
opt := None[string]()
|
||||
result := opt.String()
|
||||
assert.Equal(t, "None[string]", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoString(t *testing.T) {
|
||||
t.Run("Some value", func(t *testing.T) {
|
||||
opt := Some(42)
|
||||
result := opt.GoString()
|
||||
assert.Contains(t, result, "option.Some")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("None value", func(t *testing.T) {
|
||||
opt := None[int]()
|
||||
result := opt.GoString()
|
||||
assert.Contains(t, result, "option.None")
|
||||
assert.Contains(t, result, "int")
|
||||
})
|
||||
|
||||
t.Run("Some with struct", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
opt := Some(TestStruct{Name: "Alice", Age: 30})
|
||||
result := opt.GoString()
|
||||
assert.Contains(t, result, "option.Some")
|
||||
assert.Contains(t, result, "Alice")
|
||||
assert.Contains(t, result, "30")
|
||||
})
|
||||
|
||||
t.Run("None with custom type", func(t *testing.T) {
|
||||
opt := None[string]()
|
||||
result := opt.GoString()
|
||||
assert.Contains(t, result, "option.None")
|
||||
assert.Contains(t, result, "string")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatInterface(t *testing.T) {
|
||||
t.Run("Some value with %s", func(t *testing.T) {
|
||||
opt := Some(42)
|
||||
result := fmt.Sprintf("%s", opt)
|
||||
assert.Equal(t, "Some[int](42)", result)
|
||||
})
|
||||
|
||||
t.Run("None value with %s", func(t *testing.T) {
|
||||
opt := None[int]()
|
||||
result := fmt.Sprintf("%s", opt)
|
||||
assert.Equal(t, "None[int]", result)
|
||||
})
|
||||
|
||||
t.Run("Some value with %v", func(t *testing.T) {
|
||||
opt := Some(42)
|
||||
result := fmt.Sprintf("%v", opt)
|
||||
assert.Equal(t, "Some[int](42)", result)
|
||||
})
|
||||
|
||||
t.Run("None value with %v", func(t *testing.T) {
|
||||
opt := None[string]()
|
||||
result := fmt.Sprintf("%v", opt)
|
||||
assert.Equal(t, "None[string]", result)
|
||||
})
|
||||
|
||||
t.Run("Some value with %+v", func(t *testing.T) {
|
||||
opt := Some(42)
|
||||
result := fmt.Sprintf("%+v", opt)
|
||||
assert.Contains(t, result, "Some")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Some value with %#v (GoString)", func(t *testing.T) {
|
||||
opt := Some(42)
|
||||
result := fmt.Sprintf("%#v", opt)
|
||||
assert.Contains(t, result, "option.Some")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("None value with %#v (GoString)", func(t *testing.T) {
|
||||
opt := None[int]()
|
||||
result := fmt.Sprintf("%#v", opt)
|
||||
assert.Contains(t, result, "option.None")
|
||||
assert.Contains(t, result, "int")
|
||||
})
|
||||
|
||||
t.Run("Some value with %q", func(t *testing.T) {
|
||||
opt := Some("hello")
|
||||
result := fmt.Sprintf("%q", opt)
|
||||
assert.Contains(t, result, "Some")
|
||||
})
|
||||
|
||||
t.Run("Some value with %T", func(t *testing.T) {
|
||||
opt := Some(42)
|
||||
result := fmt.Sprintf("%T", opt)
|
||||
assert.Contains(t, result, "option.Option")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogValue(t *testing.T) {
|
||||
t.Run("Some value", func(t *testing.T) {
|
||||
opt := Some(42)
|
||||
logValue := opt.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "some", attrs[0].Key)
|
||||
assert.Equal(t, int64(42), attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("None value", func(t *testing.T) {
|
||||
opt := None[int]()
|
||||
logValue := opt.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "none", attrs[0].Key)
|
||||
// Value should be struct{}{}
|
||||
assert.Equal(t, struct{}{}, attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Some with string", func(t *testing.T) {
|
||||
opt := Some("success")
|
||||
logValue := opt.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "some", attrs[0].Key)
|
||||
assert.Equal(t, "success", attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("None with string", func(t *testing.T) {
|
||||
opt := None[string]()
|
||||
logValue := opt.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 1)
|
||||
assert.Equal(t, "none", attrs[0].Key)
|
||||
assert.Equal(t, struct{}{}, attrs[0].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Integration with slog - Some", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
opt := Some(42)
|
||||
logger.Info("test message", "result", opt)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "test message")
|
||||
assert.Contains(t, output, "result")
|
||||
assert.Contains(t, output, "some")
|
||||
assert.Contains(t, output, "42")
|
||||
})
|
||||
|
||||
t.Run("Integration with slog - None", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
opt := None[int]()
|
||||
logger.Info("test message", "result", opt)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "test message")
|
||||
assert.Contains(t, output, "result")
|
||||
assert.Contains(t, output, "none")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatComprehensive(t *testing.T) {
|
||||
t.Run("All format verbs for Some", func(t *testing.T) {
|
||||
opt := Some(42)
|
||||
|
||||
tests := []struct {
|
||||
verb string
|
||||
contains []string
|
||||
}{
|
||||
{"%s", []string{"Some", "42"}},
|
||||
{"%v", []string{"Some", "42"}},
|
||||
{"%+v", []string{"Some", "42"}},
|
||||
{"%#v", []string{"option.Some", "42"}},
|
||||
{"%T", []string{"option.Option"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.verb, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.verb, opt)
|
||||
for _, substr := range tt.contains {
|
||||
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("All format verbs for None", func(t *testing.T) {
|
||||
opt := None[int]()
|
||||
|
||||
tests := []struct {
|
||||
verb string
|
||||
contains []string
|
||||
}{
|
||||
{"%s", []string{"None", "int"}},
|
||||
{"%v", []string{"None", "int"}},
|
||||
{"%+v", []string{"None", "int"}},
|
||||
{"%#v", []string{"option.None", "int"}},
|
||||
{"%T", []string{"option.Option"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.verb, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.verb, opt)
|
||||
for _, substr := range tt.contains {
|
||||
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInterfaceImplementations(t *testing.T) {
|
||||
t.Run("fmt.Stringer interface", func(t *testing.T) {
|
||||
var _ fmt.Stringer = Some(42)
|
||||
var _ fmt.Stringer = None[int]()
|
||||
})
|
||||
|
||||
t.Run("fmt.GoStringer interface", func(t *testing.T) {
|
||||
var _ fmt.GoStringer = Some(42)
|
||||
var _ fmt.GoStringer = None[int]()
|
||||
})
|
||||
|
||||
t.Run("fmt.Formatter interface", func(t *testing.T) {
|
||||
var _ fmt.Formatter = Some(42)
|
||||
var _ fmt.Formatter = None[int]()
|
||||
})
|
||||
|
||||
t.Run("slog.LogValuer interface", func(t *testing.T) {
|
||||
var _ slog.LogValuer = Some(42)
|
||||
var _ slog.LogValuer = None[int]()
|
||||
})
|
||||
}
|
||||
175
v2/pair/examples_format_test.go
Normal file
175
v2/pair/examples_format_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2024 - 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 pair_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
// ExamplePair_String demonstrates the fmt.Stringer interface implementation.
|
||||
func ExamplePair_String() {
|
||||
p1 := P.MakePair("username", 42)
|
||||
p2 := P.MakePair(100, "active")
|
||||
|
||||
fmt.Println(p1.String())
|
||||
fmt.Println(p2.String())
|
||||
|
||||
// Output:
|
||||
// Pair[string, int](username, 42)
|
||||
// Pair[int, string](100, active)
|
||||
}
|
||||
|
||||
// ExamplePair_GoString demonstrates the fmt.GoStringer interface implementation.
|
||||
func ExamplePair_GoString() {
|
||||
p1 := P.MakePair("key", 42)
|
||||
p2 := P.MakePair(errors.New("error"), "value")
|
||||
|
||||
fmt.Printf("%#v\n", p1)
|
||||
fmt.Printf("%#v\n", p2)
|
||||
|
||||
// Output:
|
||||
// pair.MakePair[string, int]("key", 42)
|
||||
// pair.MakePair[error, string](&errors.errorString{s:"error"}, "value")
|
||||
}
|
||||
|
||||
// ExamplePair_Format demonstrates the fmt.Formatter interface implementation.
|
||||
func ExamplePair_Format() {
|
||||
p := P.MakePair("config", 8080)
|
||||
|
||||
// Different format verbs
|
||||
fmt.Printf("%%s: %s\n", p)
|
||||
fmt.Printf("%%v: %v\n", p)
|
||||
fmt.Printf("%%+v: %+v\n", p)
|
||||
fmt.Printf("%%#v: %#v\n", p)
|
||||
|
||||
// Output:
|
||||
// %s: Pair[string, int](config, 8080)
|
||||
// %v: Pair[string, int](config, 8080)
|
||||
// %+v: Pair[string, int](config, 8080)
|
||||
// %#v: pair.MakePair[string, int]("config", 8080)
|
||||
}
|
||||
|
||||
// ExamplePair_LogValue demonstrates the slog.LogValuer interface implementation.
|
||||
func ExamplePair_LogValue() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
// Remove time for consistent output
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
|
||||
// Pair with string and int
|
||||
p1 := P.MakePair("username", 42)
|
||||
logger.Info("user data", "data", p1)
|
||||
|
||||
// Pair with error and string
|
||||
p2 := P.MakePair(errors.New("connection failed"), "retry")
|
||||
logger.Error("operation failed", "status", p2)
|
||||
|
||||
// Output:
|
||||
// level=INFO msg="user data" data.head=username data.tail=42
|
||||
// level=ERROR msg="operation failed" status.head="connection failed" status.tail=retry
|
||||
}
|
||||
|
||||
// ExamplePair_formatting_comparison demonstrates different formatting options.
|
||||
func ExamplePair_formatting_comparison() {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
config := Config{Host: "localhost", Port: 8080}
|
||||
p := P.MakePair(config, []string{"api", "web"})
|
||||
|
||||
fmt.Printf("String(): %s\n", p.String())
|
||||
fmt.Printf("GoString(): %s\n", p.GoString())
|
||||
fmt.Printf("%%v: %v\n", p)
|
||||
fmt.Printf("%%#v: %#v\n", p)
|
||||
|
||||
// Output:
|
||||
// String(): Pair[pair_test.Config, []string]({localhost 8080}, [api web])
|
||||
// GoString(): pair.MakePair[pair_test.Config, []string](pair_test.Config{Host:"localhost", Port:8080}, []string{"api", "web"})
|
||||
// %v: Pair[pair_test.Config, []string]({localhost 8080}, [api web])
|
||||
// %#v: pair.MakePair[pair_test.Config, []string](pair_test.Config{Host:"localhost", Port:8080}, []string{"api", "web"})
|
||||
}
|
||||
|
||||
// ExamplePair_LogValue_structured demonstrates structured logging with Pair.
|
||||
func ExamplePair_LogValue_structured() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
|
||||
if a.Key == slog.TimeKey {
|
||||
return slog.Attr{}
|
||||
}
|
||||
return a
|
||||
},
|
||||
}))
|
||||
|
||||
// Simulate a key-value store operation
|
||||
operation := func(key string, value int) P.Pair[string, int] {
|
||||
return P.MakePair(key, value)
|
||||
}
|
||||
|
||||
// Log successful operation
|
||||
result1 := operation("counter", 42)
|
||||
logger.Info("store operation", "key", "counter", "result", result1)
|
||||
|
||||
// Log another operation
|
||||
result2 := operation("timeout", 30)
|
||||
logger.Info("store operation", "key", "timeout", "result", result2)
|
||||
|
||||
// Output:
|
||||
// level=INFO msg="store operation" key=counter result.head=counter result.tail=42
|
||||
// level=INFO msg="store operation" key=timeout result.head=timeout result.tail=30
|
||||
}
|
||||
|
||||
// ExamplePair_formatting_with_maps demonstrates formatting pairs containing maps.
|
||||
func ExamplePair_formatting_with_maps() {
|
||||
metadata := map[string]string{
|
||||
"version": "1.0",
|
||||
"author": "Alice",
|
||||
}
|
||||
p := P.MakePair("config", metadata)
|
||||
|
||||
fmt.Printf("%%v: %v\n", p)
|
||||
fmt.Printf("%%s: %s\n", p)
|
||||
|
||||
// Output:
|
||||
// %v: Pair[string, map[string]string](config, map[author:Alice version:1.0])
|
||||
// %s: Pair[string, map[string]string](config, map[author:Alice version:1.0])
|
||||
}
|
||||
|
||||
// ExamplePair_formatting_nested demonstrates formatting nested pairs.
|
||||
func ExamplePair_formatting_nested() {
|
||||
inner := P.MakePair("inner", 10)
|
||||
outer := P.MakePair(inner, "outer")
|
||||
|
||||
fmt.Printf("%%v: %v\n", outer)
|
||||
fmt.Printf("%%#v: %#v\n", outer)
|
||||
|
||||
// Output:
|
||||
// %v: Pair[pair.Pair[string,int], string](Pair[string, int](inner, 10), outer)
|
||||
// %#v: pair.MakePair[pair.Pair[string,int], string](pair.MakePair[string, int]("inner", 10), "outer")
|
||||
}
|
||||
89
v2/pair/format.go
Normal file
89
v2/pair/format.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2024 - 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 pair
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
)
|
||||
|
||||
const (
|
||||
pairGoTemplate = "pair.MakePair[%s, %s](%#v, %#v)"
|
||||
pairFmtTemplate = "Pair[%T, %T](%v, %v)"
|
||||
)
|
||||
|
||||
func goString[L, R any](l L, r R) string {
|
||||
return fmt.Sprintf(pairGoTemplate, formatting.TypeInfo(new(L)), formatting.TypeInfo(new(R)), l, r)
|
||||
}
|
||||
|
||||
// String prints some debug info for the object
|
||||
//
|
||||
//go:noinline
|
||||
func (p Pair[L, R]) String() string {
|
||||
return fmt.Sprintf(pairFmtTemplate, p.l, p.r, p.l, p.r)
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Pair.
|
||||
// Supports all standard format verbs:
|
||||
// - %s, %v, %+v: uses String() representation
|
||||
// - %#v: uses GoString() representation
|
||||
// - %q: quoted String() representation
|
||||
// - other verbs: uses String() representation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// p := pair.MakePair("key", 42)
|
||||
// fmt.Printf("%s", p) // "Pair[string, int](key, 42)"
|
||||
// fmt.Printf("%v", p) // "Pair[string, int](key, 42)"
|
||||
// fmt.Printf("%#v", p) // "pair.MakePair[string, int]("key", 42)"
|
||||
//
|
||||
//go:noinline
|
||||
func (p Pair[L, R]) Format(f fmt.State, c rune) {
|
||||
formatting.FmtString(p, f, c)
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Pair.
|
||||
// Returns a Go-syntax representation of the Pair value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// pair.MakePair("key", 42).GoString() // "pair.MakePair[string, int]("key", 42)"
|
||||
//
|
||||
//go:noinline
|
||||
func (p Pair[L, R]) GoString() string {
|
||||
return goString(p.l, p.r)
|
||||
}
|
||||
|
||||
// LogValue implements slog.LogValuer for Pair.
|
||||
// Returns a slog.Value that represents the Pair for structured logging.
|
||||
// Returns a group value with "head" and "tail" keys.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logger := slog.Default()
|
||||
// p := pair.MakePair("key", 42)
|
||||
// logger.Info("pair value", "data", p)
|
||||
// // Logs: {"msg":"pair value","data":{"head":"key","tail":42}}
|
||||
//
|
||||
//go:noinline
|
||||
func (p Pair[L, R]) LogValue() slog.Value {
|
||||
return slog.GroupValue(
|
||||
slog.Any("head", p.l),
|
||||
slog.Any("tail", p.r),
|
||||
)
|
||||
}
|
||||
272
v2/pair/format_test.go
Normal file
272
v2/pair/format_test.go
Normal file
@@ -0,0 +1,272 @@
|
||||
// Copyright (c) 2024 - 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 pair
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
t.Run("Pair with int and string", func(t *testing.T) {
|
||||
p := MakePair("hello", 42)
|
||||
result := p.String()
|
||||
assert.Equal(t, "Pair[string, int](hello, 42)", result)
|
||||
})
|
||||
|
||||
t.Run("Pair with string and string", func(t *testing.T) {
|
||||
p := MakePair("key", "value")
|
||||
result := p.String()
|
||||
assert.Equal(t, "Pair[string, string](key, value)", result)
|
||||
})
|
||||
|
||||
t.Run("Pair with error", func(t *testing.T) {
|
||||
p := MakePair(errors.New("test error"), 42)
|
||||
result := p.String()
|
||||
assert.Contains(t, result, "Pair[*errors.errorString, int]")
|
||||
assert.Contains(t, result, "test error")
|
||||
})
|
||||
|
||||
t.Run("Pair with struct", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
p := MakePair(User{Name: "Alice", Age: 30}, "active")
|
||||
result := p.String()
|
||||
assert.Contains(t, result, "Pair")
|
||||
assert.Contains(t, result, "Alice")
|
||||
assert.Contains(t, result, "30")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGoString(t *testing.T) {
|
||||
t.Run("Pair with int and string", func(t *testing.T) {
|
||||
p := MakePair("hello", 42)
|
||||
result := p.GoString()
|
||||
assert.Contains(t, result, "pair.MakePair")
|
||||
assert.Contains(t, result, "string")
|
||||
assert.Contains(t, result, "int")
|
||||
assert.Contains(t, result, "hello")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Pair with error", func(t *testing.T) {
|
||||
p := MakePair(errors.New("test error"), 42)
|
||||
result := p.GoString()
|
||||
assert.Contains(t, result, "pair.MakePair")
|
||||
assert.Contains(t, result, "test error")
|
||||
})
|
||||
|
||||
t.Run("Pair with struct", func(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
p := MakePair(TestStruct{Name: "Bob", Age: 25}, 100)
|
||||
result := p.GoString()
|
||||
assert.Contains(t, result, "pair.MakePair")
|
||||
assert.Contains(t, result, "Bob")
|
||||
assert.Contains(t, result, "25")
|
||||
assert.Contains(t, result, "100")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatInterface(t *testing.T) {
|
||||
t.Run("Pair with %s", func(t *testing.T) {
|
||||
p := MakePair("key", 42)
|
||||
result := fmt.Sprintf("%s", p)
|
||||
assert.Equal(t, "Pair[string, int](key, 42)", result)
|
||||
})
|
||||
|
||||
t.Run("Pair with %v", func(t *testing.T) {
|
||||
p := MakePair("key", 42)
|
||||
result := fmt.Sprintf("%v", p)
|
||||
assert.Equal(t, "Pair[string, int](key, 42)", result)
|
||||
})
|
||||
|
||||
t.Run("Pair with %+v", func(t *testing.T) {
|
||||
p := MakePair("key", 42)
|
||||
result := fmt.Sprintf("%+v", p)
|
||||
assert.Contains(t, result, "Pair")
|
||||
assert.Contains(t, result, "key")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Pair with %#v (GoString)", func(t *testing.T) {
|
||||
p := MakePair("key", 42)
|
||||
result := fmt.Sprintf("%#v", p)
|
||||
assert.Contains(t, result, "pair.MakePair")
|
||||
assert.Contains(t, result, "key")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Pair with %q", func(t *testing.T) {
|
||||
p := MakePair("key", "value")
|
||||
result := fmt.Sprintf("%q", p)
|
||||
// Should use String() representation
|
||||
assert.Contains(t, result, "Pair")
|
||||
})
|
||||
|
||||
t.Run("Pair with %T", func(t *testing.T) {
|
||||
p := MakePair("key", 42)
|
||||
result := fmt.Sprintf("%T", p)
|
||||
assert.Contains(t, result, "pair.Pair")
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogValue(t *testing.T) {
|
||||
t.Run("Pair with int and string", func(t *testing.T) {
|
||||
p := MakePair("key", 42)
|
||||
logValue := p.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 2)
|
||||
assert.Equal(t, "head", attrs[0].Key)
|
||||
assert.Equal(t, "key", attrs[0].Value.Any())
|
||||
assert.Equal(t, "tail", attrs[1].Key)
|
||||
assert.Equal(t, int64(42), attrs[1].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Pair with error", func(t *testing.T) {
|
||||
p := MakePair(errors.New("test error"), "value")
|
||||
logValue := p.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 2)
|
||||
assert.Equal(t, "head", attrs[0].Key)
|
||||
assert.NotNil(t, attrs[0].Value.Any())
|
||||
assert.Equal(t, "tail", attrs[1].Key)
|
||||
assert.Equal(t, "value", attrs[1].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Pair with strings", func(t *testing.T) {
|
||||
p := MakePair("first", "second")
|
||||
logValue := p.LogValue()
|
||||
|
||||
// Should be a group value
|
||||
assert.Equal(t, slog.KindGroup, logValue.Kind())
|
||||
|
||||
// Extract the group attributes
|
||||
attrs := logValue.Group()
|
||||
assert.Len(t, attrs, 2)
|
||||
assert.Equal(t, "head", attrs[0].Key)
|
||||
assert.Equal(t, "first", attrs[0].Value.Any())
|
||||
assert.Equal(t, "tail", attrs[1].Key)
|
||||
assert.Equal(t, "second", attrs[1].Value.Any())
|
||||
})
|
||||
|
||||
t.Run("Integration with slog", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
p := MakePair("username", 42)
|
||||
logger.Info("test message", "data", p)
|
||||
|
||||
output := buf.String()
|
||||
assert.Contains(t, output, "test message")
|
||||
assert.Contains(t, output, "data")
|
||||
assert.Contains(t, output, "head")
|
||||
assert.Contains(t, output, "username")
|
||||
assert.Contains(t, output, "tail")
|
||||
assert.Contains(t, output, "42")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatComprehensive(t *testing.T) {
|
||||
t.Run("All format verbs", func(t *testing.T) {
|
||||
p := MakePair("key", 42)
|
||||
|
||||
tests := []struct {
|
||||
verb string
|
||||
contains []string
|
||||
}{
|
||||
{"%s", []string{"Pair", "key", "42"}},
|
||||
{"%v", []string{"Pair", "key", "42"}},
|
||||
{"%+v", []string{"Pair", "key", "42"}},
|
||||
{"%#v", []string{"pair.MakePair", "key", "42"}},
|
||||
{"%T", []string{"pair.Pair"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.verb, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.verb, p)
|
||||
for _, substr := range tt.contains {
|
||||
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Complex types", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
p := MakePair(Config{Host: "localhost", Port: 8080}, []string{"a", "b", "c"})
|
||||
|
||||
tests := []struct {
|
||||
verb string
|
||||
contains []string
|
||||
}{
|
||||
{"%s", []string{"Pair", "localhost", "8080"}},
|
||||
{"%v", []string{"Pair", "localhost", "8080"}},
|
||||
{"%#v", []string{"pair.MakePair", "localhost", "8080"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.verb, func(t *testing.T) {
|
||||
result := fmt.Sprintf(tt.verb, p)
|
||||
for _, substr := range tt.contains {
|
||||
assert.Contains(t, result, substr, "Format %s should contain %s", tt.verb, substr)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInterfaceImplementations(t *testing.T) {
|
||||
t.Run("fmt.Stringer interface", func(t *testing.T) {
|
||||
var _ fmt.Stringer = MakePair("key", 42)
|
||||
})
|
||||
|
||||
t.Run("fmt.GoStringer interface", func(t *testing.T) {
|
||||
var _ fmt.GoStringer = MakePair("key", 42)
|
||||
})
|
||||
|
||||
t.Run("fmt.Formatter interface", func(t *testing.T) {
|
||||
var _ fmt.Formatter = MakePair("key", 42)
|
||||
})
|
||||
|
||||
t.Run("slog.LogValuer interface", func(t *testing.T) {
|
||||
var _ slog.LogValuer = MakePair("key", 42)
|
||||
})
|
||||
}
|
||||
@@ -16,27 +16,10 @@
|
||||
package pair
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
// String prints some debug info for the object
|
||||
func (s Pair[A, B]) String() string {
|
||||
return fmt.Sprintf("Pair[%T, %T](%v, %v)", s.l, s.r, s.l, s.r)
|
||||
}
|
||||
|
||||
// Format prints some debug info for the object
|
||||
func (s Pair[A, B]) Format(f fmt.State, c rune) {
|
||||
switch c {
|
||||
case 's':
|
||||
fmt.Fprint(f, s.String())
|
||||
default:
|
||||
fmt.Fprint(f, s.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Of creates a [Pair] with the same value in both the head and tail positions.
|
||||
//
|
||||
// Example:
|
||||
|
||||
@@ -367,22 +367,6 @@ func TestFromStrictEquals(t *testing.T) {
|
||||
assert.False(t, pairEq.Equals(p1, p3))
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
p := MakePair("hello", 42)
|
||||
str := p.String()
|
||||
assert.Contains(t, str, "Pair")
|
||||
assert.Contains(t, str, "hello")
|
||||
assert.Contains(t, str, "42")
|
||||
}
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
p := MakePair("test", 100)
|
||||
str := fmt.Sprintf("%s", p)
|
||||
assert.Contains(t, str, "Pair")
|
||||
assert.Contains(t, str, "test")
|
||||
assert.Contains(t, str, "100")
|
||||
}
|
||||
|
||||
func TestMonadHead(t *testing.T) {
|
||||
stringMonoid := S.Monoid
|
||||
monad := MonadHead[int, string, string](stringMonoid)
|
||||
|
||||
@@ -85,7 +85,7 @@ func ChainReaderK[
|
||||
GEB ~func(E) ET.Either[L, B],
|
||||
GB ~func(E) B,
|
||||
L, E, A, B any](f func(A) GB) func(GEA) GEB {
|
||||
return Chain[GEA, GEB, L, E, A, B](F.Flow2(f, FromReader[GB, GEB, L, E, B]))
|
||||
return Chain[GEA](F.Flow2(f, FromReader[GB, GEB, L, E, B]))
|
||||
}
|
||||
|
||||
func Of[GEA ~func(E) ET.Either[L, A], L, E, A any](a A) GEA {
|
||||
|
||||
@@ -15,24 +15,26 @@
|
||||
|
||||
package readereither
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[R, E, A, B any](f Kleisli[R, E, A, Either[A, B]]) Kleisli[R, E, A, B] {
|
||||
func TailRec[R, E, A, B any](f Kleisli[R, E, A, tailrec.Trampoline[A, B]]) Kleisli[R, E, A, B] {
|
||||
return func(a A) ReaderEither[R, E, B] {
|
||||
initialReader := f(a)
|
||||
return func(r R) Either[E, B] {
|
||||
return func(r R) either.Either[E, B] {
|
||||
current := initialReader(r)
|
||||
for {
|
||||
rec, e := either.Unwrap(current)
|
||||
if either.IsLeft(current) {
|
||||
return either.Left[B](e)
|
||||
}
|
||||
b, a := either.Unwrap(rec)
|
||||
if either.IsRight(rec) {
|
||||
return either.Right[E](b)
|
||||
if rec.Landed {
|
||||
return either.Right[E](rec.Land)
|
||||
}
|
||||
current = f(a)(r)
|
||||
current = f(rec.Bounce)(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package readerio
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
// TailRec implements stack-safe tail recursion for the ReaderIO monad.
|
||||
//
|
||||
// This function enables recursive computations that depend on an environment (Reader aspect)
|
||||
@@ -10,12 +8,12 @@ import "github.com/IBM/fp-go/v2/either"
|
||||
//
|
||||
// # How It Works
|
||||
//
|
||||
// TailRec takes a Kleisli arrow that returns Either[A, B]:
|
||||
// - Left(A): Continue recursion with the new state A
|
||||
// - Right(B): Terminate recursion and return the final result B
|
||||
// TailRec takes a Kleisli arrow that returns Trampoline[A, B]:
|
||||
// - Bounce(A): Continue recursion with the new state A
|
||||
// - Land(B): Terminate recursion and return the final result B
|
||||
//
|
||||
// The function iteratively applies the Kleisli arrow, passing the environment R to each
|
||||
// iteration, until a Right(B) value is produced. This combines:
|
||||
// iteration, until a Land(B) value is produced. This combines:
|
||||
// - Environment dependency (Reader monad): Access to configuration, context, or dependencies
|
||||
// - Side effects (IO monad): Logging, file I/O, network calls, etc.
|
||||
// - Stack safety: Iterative execution prevents stack overflow
|
||||
@@ -29,9 +27,9 @@ import "github.com/IBM/fp-go/v2/either"
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow (A => ReaderIO[R, Either[A, B]]) that:
|
||||
// * Takes the current state A
|
||||
// * Returns a ReaderIO that depends on environment R
|
||||
// * Produces Either[A, B] to control recursion flow
|
||||
// - Takes the current state A
|
||||
// - Returns a ReaderIO that depends on environment R
|
||||
// - Produces Either[A, B] to control recursion flow
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
@@ -117,13 +115,13 @@ import "github.com/IBM/fp-go/v2/either"
|
||||
// (thousands or millions of iterations) will not cause stack overflow:
|
||||
//
|
||||
// // Safe for very large inputs
|
||||
// sumToZero := readerio.TailRec(func(n int) readerio.ReaderIO[Env, either.Either[int, int]] {
|
||||
// return func(env Env) io.IO[either.Either[int, int]] {
|
||||
// return func() either.Either[int, int] {
|
||||
// sumToZero := readerio.TailRec(func(n int) readerio.ReaderIO[Env, tailrec.Trampoline[int, int]] {
|
||||
// return func(env Env) io.IO[tailrec.Trampoline[int, int]] {
|
||||
// return func() tailrec.Trampoline[int, int] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[int](0)
|
||||
// return tailrec.Land[int](0)
|
||||
// }
|
||||
// return either.Left[int](n - 1)
|
||||
// return tailrec.Bounce[int](n - 1)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
@@ -143,7 +141,7 @@ import "github.com/IBM/fp-go/v2/either"
|
||||
// - [Chain]: For sequencing ReaderIO computations
|
||||
// - [Ask]: For accessing the environment
|
||||
// - [Asks]: For extracting values from the environment
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, Trampoline[A, B]]) Kleisli[R, A, B] {
|
||||
return func(a A) ReaderIO[R, B] {
|
||||
initialReader := f(a)
|
||||
return func(r R) IO[B] {
|
||||
@@ -151,11 +149,10 @@ func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
|
||||
return func() B {
|
||||
current := initialB()
|
||||
for {
|
||||
b, a := either.Unwrap(current)
|
||||
if either.IsRight(current) {
|
||||
return b
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = f(a)(r)()
|
||||
current = f(current.Bounce)(r)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
"testing"
|
||||
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
G "github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -54,15 +54,15 @@ func TestTailRecFactorial(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderIO[LoggerEnv, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
factorialStep := func(state State) ReaderIO[LoggerEnv, Trampoline[State, int]] {
|
||||
return func(env LoggerEnv) G.IO[Trampoline[State, int]] {
|
||||
return func() Trampoline[State, int] {
|
||||
if state.n <= 0 {
|
||||
env.Logger(fmt.Sprintf("Complete: %d", state.acc))
|
||||
return E.Right[State](state.acc)
|
||||
return tailrec.Land[State](state.acc)
|
||||
}
|
||||
env.Logger(fmt.Sprintf("Step: %d * %d", state.n, state.acc))
|
||||
return E.Left[int](State{state.n - 1, state.acc * state.n})
|
||||
return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,13 +86,13 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
|
||||
env := TestEnv{Multiplier: 1, Logs: []string{}}
|
||||
|
||||
fibStep := func(state State) ReaderIO[TestEnv, E.Either[State, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
fibStep := func(state State) ReaderIO[TestEnv, Trampoline[State, int]] {
|
||||
return func(env TestEnv) G.IO[Trampoline[State, int]] {
|
||||
return func() Trampoline[State, int] {
|
||||
if state.n <= 0 {
|
||||
return E.Right[State](state.curr * env.Multiplier)
|
||||
return tailrec.Land[State](state.curr * env.Multiplier)
|
||||
}
|
||||
return E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr})
|
||||
return tailrec.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,13 +107,13 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
config := ConfigEnv{MinValue: 0, Step: 2}
|
||||
|
||||
countdownStep := func(n int) ReaderIO[ConfigEnv, E.Either[int, int]] {
|
||||
return func(cfg ConfigEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
countdownStep := func(n int) ReaderIO[ConfigEnv, Trampoline[int, int]] {
|
||||
return func(cfg ConfigEnv) G.IO[Trampoline[int, int]] {
|
||||
return func() Trampoline[int, int] {
|
||||
if n <= cfg.MinValue {
|
||||
return E.Right[int](n)
|
||||
return tailrec.Land[int](n)
|
||||
}
|
||||
return E.Left[int](n - cfg.Step)
|
||||
return tailrec.Bounce[int](n - cfg.Step)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,13 +128,13 @@ func TestTailRecCountdown(t *testing.T) {
|
||||
func TestTailRecCountdownOddStep(t *testing.T) {
|
||||
config := ConfigEnv{MinValue: 0, Step: 3}
|
||||
|
||||
countdownStep := func(n int) ReaderIO[ConfigEnv, E.Either[int, int]] {
|
||||
return func(cfg ConfigEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
countdownStep := func(n int) ReaderIO[ConfigEnv, Trampoline[int, int]] {
|
||||
return func(cfg ConfigEnv) G.IO[Trampoline[int, int]] {
|
||||
return func() Trampoline[int, int] {
|
||||
if n <= cfg.MinValue {
|
||||
return E.Right[int](n)
|
||||
return tailrec.Land[int](n)
|
||||
}
|
||||
return E.Left[int](n - cfg.Step)
|
||||
return tailrec.Bounce[int](n - cfg.Step)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,13 +154,13 @@ func TestTailRecSumList(t *testing.T) {
|
||||
|
||||
env := TestEnv{Multiplier: 2, Logs: []string{}}
|
||||
|
||||
sumStep := func(state State) ReaderIO[TestEnv, E.Either[State, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
sumStep := func(state State) ReaderIO[TestEnv, Trampoline[State, int]] {
|
||||
return func(env TestEnv) G.IO[Trampoline[State, int]] {
|
||||
return func() Trampoline[State, int] {
|
||||
if A.IsEmpty(state.list) {
|
||||
return E.Right[State](state.sum * env.Multiplier)
|
||||
return tailrec.Land[State](state.sum * env.Multiplier)
|
||||
}
|
||||
return E.Left[int](State{state.list[1:], state.sum + state.list[0]})
|
||||
return tailrec.Bounce[int](State{state.list[1:], state.sum + state.list[0]})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,10 +175,10 @@ func TestTailRecSumList(t *testing.T) {
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, Logs: []string{}}
|
||||
|
||||
immediateStep := func(n int) ReaderIO[TestEnv, E.Either[int, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
return E.Right[int](n * env.Multiplier)
|
||||
immediateStep := func(n int) ReaderIO[TestEnv, Trampoline[int, int]] {
|
||||
return func(env TestEnv) G.IO[Trampoline[int, int]] {
|
||||
return func() Trampoline[int, int] {
|
||||
return tailrec.Land[int](n * env.Multiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,13 +193,13 @@ func TestTailRecImmediateTermination(t *testing.T) {
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, Logs: []string{}}
|
||||
|
||||
countdownStep := func(n int) ReaderIO[TestEnv, E.Either[int, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
countdownStep := func(n int) ReaderIO[TestEnv, Trampoline[int, int]] {
|
||||
return func(env TestEnv) G.IO[Trampoline[int, int]] {
|
||||
return func() Trampoline[int, int] {
|
||||
if n <= 0 {
|
||||
return E.Right[int](n)
|
||||
return tailrec.Land[int](n)
|
||||
}
|
||||
return E.Left[int](n - 1)
|
||||
return tailrec.Bounce[int](n - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,16 +223,16 @@ func TestTailRecFindInRange(t *testing.T) {
|
||||
|
||||
env := FindEnv{Target: 42}
|
||||
|
||||
findStep := func(state State) ReaderIO[FindEnv, E.Either[State, int]] {
|
||||
return func(env FindEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
findStep := func(state State) ReaderIO[FindEnv, Trampoline[State, int]] {
|
||||
return func(env FindEnv) G.IO[Trampoline[State, int]] {
|
||||
return func() Trampoline[State, int] {
|
||||
if state.current >= state.max {
|
||||
return E.Right[State](-1) // Not found
|
||||
return tailrec.Land[State](-1) // Not found
|
||||
}
|
||||
if state.current == env.Target {
|
||||
return E.Right[State](state.current) // Found
|
||||
return tailrec.Land[State](state.current) // Found
|
||||
}
|
||||
return E.Left[int](State{state.current + 1, state.max})
|
||||
return tailrec.Bounce[int](State{state.current + 1, state.max})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,16 +256,16 @@ func TestTailRecFindNotInRange(t *testing.T) {
|
||||
|
||||
env := FindEnv{Target: 200}
|
||||
|
||||
findStep := func(state State) ReaderIO[FindEnv, E.Either[State, int]] {
|
||||
return func(env FindEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
findStep := func(state State) ReaderIO[FindEnv, Trampoline[State, int]] {
|
||||
return func(env FindEnv) G.IO[Trampoline[State, int]] {
|
||||
return func() Trampoline[State, int] {
|
||||
if state.current >= state.max {
|
||||
return E.Right[State](-1) // Not found
|
||||
return tailrec.Land[State](-1) // Not found
|
||||
}
|
||||
if state.current == env.Target {
|
||||
return E.Right[State](state.current) // Found
|
||||
return tailrec.Land[State](state.current) // Found
|
||||
}
|
||||
return E.Left[int](State{state.current + 1, state.max})
|
||||
return tailrec.Bounce[int](State{state.current + 1, state.max})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,14 +285,14 @@ func TestTailRecWithLogging(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
countdownStep := func(n int) ReaderIO[LoggerEnv, E.Either[int, int]] {
|
||||
return func(env LoggerEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
countdownStep := func(n int) ReaderIO[LoggerEnv, Trampoline[int, int]] {
|
||||
return func(env LoggerEnv) G.IO[Trampoline[int, int]] {
|
||||
return func() Trampoline[int, int] {
|
||||
env.Logger(fmt.Sprintf("Count: %d", n))
|
||||
if n <= 0 {
|
||||
return E.Right[int](n)
|
||||
return tailrec.Land[int](n)
|
||||
}
|
||||
return E.Left[int](n - 1)
|
||||
return tailrec.Bounce[int](n - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,17 +315,17 @@ func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
collatzStep := func(n int) ReaderIO[LoggerEnv, E.Either[int, int]] {
|
||||
return func(env LoggerEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
collatzStep := func(n int) ReaderIO[LoggerEnv, Trampoline[int, int]] {
|
||||
return func(env LoggerEnv) G.IO[Trampoline[int, int]] {
|
||||
return func() Trampoline[int, int] {
|
||||
env.Logger(fmt.Sprintf("n=%d", n))
|
||||
if n <= 1 {
|
||||
return E.Right[int](n)
|
||||
return tailrec.Land[int](n)
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return E.Left[int](n / 2)
|
||||
return tailrec.Bounce[int](n / 2)
|
||||
}
|
||||
return E.Left[int](3*n + 1)
|
||||
return tailrec.Bounce[int](3*n + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,13 +352,13 @@ func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
|
||||
env := PowerEnv{MaxExponent: 10}
|
||||
|
||||
powerStep := func(state State) ReaderIO[PowerEnv, E.Either[State, int]] {
|
||||
return func(env PowerEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
powerStep := func(state State) ReaderIO[PowerEnv, Trampoline[State, int]] {
|
||||
return func(env PowerEnv) G.IO[Trampoline[State, int]] {
|
||||
return func() Trampoline[State, int] {
|
||||
if state.exponent >= env.MaxExponent {
|
||||
return E.Right[State](state.result)
|
||||
return tailrec.Land[State](state.result)
|
||||
}
|
||||
return E.Left[int](State{state.exponent + 1, state.result * 2})
|
||||
return tailrec.Bounce[int](State{state.exponent + 1, state.result * 2})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -383,14 +383,14 @@ func TestTailRecGCD(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
gcdStep := func(state State) ReaderIO[LoggerEnv, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
gcdStep := func(state State) ReaderIO[LoggerEnv, Trampoline[State, int]] {
|
||||
return func(env LoggerEnv) G.IO[Trampoline[State, int]] {
|
||||
return func() Trampoline[State, int] {
|
||||
env.Logger(fmt.Sprintf("gcd(%d, %d)", state.a, state.b))
|
||||
if state.b == 0 {
|
||||
return E.Right[State](state.a)
|
||||
return tailrec.Land[State](state.a)
|
||||
}
|
||||
return E.Left[int](State{state.b, state.a % state.b})
|
||||
return tailrec.Bounce[int](State{state.b, state.a % state.b})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -412,13 +412,13 @@ func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
|
||||
|
||||
env := CounterEnv{Increment: 3, Limit: 20}
|
||||
|
||||
counterStep := func(n int) ReaderIO[CounterEnv, E.Either[int, int]] {
|
||||
return func(env CounterEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
counterStep := func(n int) ReaderIO[CounterEnv, Trampoline[int, int]] {
|
||||
return func(env CounterEnv) G.IO[Trampoline[int, int]] {
|
||||
return func() Trampoline[int, int] {
|
||||
if n >= env.Limit {
|
||||
return E.Right[int](n)
|
||||
return tailrec.Land[int](n)
|
||||
}
|
||||
return E.Left[int](n + env.Increment)
|
||||
return tailrec.Bounce[int](n + env.Increment)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -431,13 +431,13 @@ func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
|
||||
|
||||
// TestTailRecDifferentEnvironments tests that different environments produce different results
|
||||
func TestTailRecDifferentEnvironments(t *testing.T) {
|
||||
multiplyStep := func(n int) ReaderIO[TestEnv, E.Either[int, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
multiplyStep := func(n int) ReaderIO[TestEnv, Trampoline[int, int]] {
|
||||
return func(env TestEnv) G.IO[Trampoline[int, int]] {
|
||||
return func() Trampoline[int, int] {
|
||||
if n <= 0 {
|
||||
return E.Right[int](n)
|
||||
return tailrec.Land[int](n)
|
||||
}
|
||||
return E.Left[int](n - 1)
|
||||
return tailrec.Bounce[int](n - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/io"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -52,4 +53,6 @@ type (
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderIO[R, A], B]
|
||||
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
|
||||
Trampoline[B, L any] = tailrec.Trampoline[B, L]
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ package readerioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
// TailRec implements stack-safe tail recursion for the ReaderIOEither monad.
|
||||
@@ -31,10 +32,10 @@ import (
|
||||
//
|
||||
// # How It Works
|
||||
//
|
||||
// TailRec takes a Kleisli arrow that returns Either[E, Either[A, B]]:
|
||||
// TailRec takes a Kleisli arrow that returns IOEither[E, Trampoline[A, B]]:
|
||||
// - Left(E): Computation failed with error E - recursion terminates
|
||||
// - Right(Left(A)): Continue recursion with the new state A
|
||||
// - Right(Right(B)): Terminate recursion successfully and return the final result B
|
||||
// - Right(Bounce(A)): Continue recursion with the new state A
|
||||
// - Right(Land(B)): Terminate recursion successfully and return the final result B
|
||||
//
|
||||
// The function iteratively applies the Kleisli arrow, passing the environment R to each
|
||||
// iteration, until either an error (Left) or a final result (Right(Right(B))) is produced.
|
||||
@@ -100,18 +101,18 @@ import (
|
||||
// }
|
||||
//
|
||||
// // Factorial that logs each step and validates input
|
||||
// factorialStep := func(state State) readerioeither.ReaderIOEither[Env, string, either.Either[State, int]] {
|
||||
// return func(env Env) ioeither.IOEither[string, either.Either[State, int]] {
|
||||
// return func() either.Either[string, either.Either[State, int]] {
|
||||
// factorialStep := func(state State) readerioeither.ReaderIOEither[Env, string, tailrec.Trampoline[State, int]] {
|
||||
// return func(env Env) ioeither.IOEither[string, tailrec.Trampoline[State, int]] {
|
||||
// return func() either.Either[string, tailrec.Trampoline[State, int]] {
|
||||
// if state.n > env.MaxN {
|
||||
// return either.Left[either.Either[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
|
||||
// return either.Left[tailrec.Trampoline[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
|
||||
// }
|
||||
// if state.n <= 0 {
|
||||
// env.Logger(fmt.Sprintf("Factorial complete: %d", state.acc))
|
||||
// return either.Right[string](either.Right[State](state.acc))
|
||||
// return either.Right[string](tailrec.Land[State](state.acc))
|
||||
// }
|
||||
// env.Logger(fmt.Sprintf("Computing: %d * %d", state.n, state.acc))
|
||||
// return either.Right[string](either.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
// return either.Right[string](tailrec.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -134,12 +135,12 @@ import (
|
||||
// retries int
|
||||
// }
|
||||
//
|
||||
// processFilesStep := func(state ProcessState) readerioeither.ReaderIOEither[Config, error, either.Either[ProcessState, []string]] {
|
||||
// return func(cfg Config) ioeither.IOEither[error, either.Either[ProcessState, []string]] {
|
||||
// return func() either.Either[error, either.Either[ProcessState, []string]] {
|
||||
// processFilesStep := func(state ProcessState) readerioeither.ReaderIOEither[Config, error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func(cfg Config) ioeither.IOEither[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// if len(state.files) == 0 {
|
||||
// cfg.Logger("All files processed")
|
||||
// return either.Right[error](either.Right[ProcessState](state.results))
|
||||
// return either.Right[error](tailrec.Land[ProcessState](state.results))
|
||||
// }
|
||||
// file := state.files[0]
|
||||
// cfg.Logger(fmt.Sprintf("Processing: %s", file))
|
||||
@@ -147,18 +148,18 @@ import (
|
||||
// // Simulate file processing that might fail
|
||||
// if err := processFile(file); err != nil {
|
||||
// if state.retries >= cfg.MaxRetries {
|
||||
// return either.Left[either.Either[ProcessState, []string]](
|
||||
// return either.Left[tailrec.Trampoline[ProcessState, []string]](
|
||||
// fmt.Errorf("max retries exceeded for %s: %w", file, err))
|
||||
// }
|
||||
// cfg.Logger(fmt.Sprintf("Retry %d for %s", state.retries+1, file))
|
||||
// return either.Right[error](either.Left[[]string](ProcessState{
|
||||
// return either.Right[error](tailrec.Bounce[[]string](ProcessState{
|
||||
// files: state.files,
|
||||
// results: state.results,
|
||||
// retries: state.retries + 1,
|
||||
// }))
|
||||
// }
|
||||
//
|
||||
// return either.Right[error](either.Left[[]string](ProcessState{
|
||||
// return either.Right[error](tailrec.Bounce[[]string](ProcessState{
|
||||
// files: state.files[1:],
|
||||
// results: append(state.results, file),
|
||||
// retries: 0,
|
||||
@@ -179,13 +180,13 @@ import (
|
||||
// (thousands or millions of iterations) will not cause stack overflow:
|
||||
//
|
||||
// // Safe for very large inputs
|
||||
// countdownStep := func(n int) readerioeither.ReaderIOEither[Env, error, either.Either[int, int]] {
|
||||
// return func(env Env) ioeither.IOEither[error, either.Either[int, int]] {
|
||||
// return func() either.Either[error, either.Either[int, int]] {
|
||||
// countdownStep := func(n int) readerioeither.ReaderIOEither[Env, error, tailrec.Trampoline[int, int]] {
|
||||
// return func(env Env) ioeither.IOEither[error, tailrec.Trampoline[int, int]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[int, int]] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[error](either.Right[int](0))
|
||||
// return either.Right[error](tailrec.Land[int](0))
|
||||
// }
|
||||
// return either.Right[error](either.Left[int](n - 1))
|
||||
// return either.Right[error](tailrec.Bounce[int](n - 1))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -194,16 +195,16 @@ import (
|
||||
//
|
||||
// # Error Handling Patterns
|
||||
//
|
||||
// The Either[E, Either[A, B]] structure provides two levels of control:
|
||||
// The Either[E, Trampoline[A, B]] structure provides two levels of control:
|
||||
//
|
||||
// 1. Outer Either (Left(E)): Unrecoverable errors that terminate recursion
|
||||
// - Validation failures
|
||||
// - Resource exhaustion
|
||||
// - Fatal errors
|
||||
//
|
||||
// 2. Inner Either (Right(Left(A)) or Right(Right(B))): Recursion control
|
||||
// - Left(A): Continue with new state
|
||||
// - Right(B): Terminate successfully
|
||||
// 2. Inner Trampoline (Right(Bounce(A)) or Right(Land(B))): Recursion control
|
||||
// - Bounce(A): Continue with new state
|
||||
// - Land(B): Terminate successfully
|
||||
//
|
||||
// This separation allows for:
|
||||
// - Early termination on errors
|
||||
@@ -226,23 +227,22 @@ import (
|
||||
// - [Chain]: For sequencing ReaderIOEither computations
|
||||
// - [Ask]: For accessing the environment
|
||||
// - [Left]/[Right]: For creating error/success values
|
||||
func TailRec[R, E, A, B any](f Kleisli[R, E, A, Either[A, B]]) Kleisli[R, E, A, B] {
|
||||
func TailRec[R, E, A, B any](f Kleisli[R, E, A, tailrec.Trampoline[A, B]]) Kleisli[R, E, A, B] {
|
||||
return func(a A) ReaderIOEither[R, E, B] {
|
||||
initialReader := f(a)
|
||||
return func(r R) IOEither[E, B] {
|
||||
initialB := initialReader(r)
|
||||
return func() Either[E, B] {
|
||||
return func() either.Either[E, B] {
|
||||
current := initialB()
|
||||
for {
|
||||
rec, e := either.Unwrap(current)
|
||||
if either.IsLeft(current) {
|
||||
return either.Left[B](e)
|
||||
}
|
||||
b, a := either.Unwrap(rec)
|
||||
if either.IsRight(rec) {
|
||||
return either.Right[E](b)
|
||||
if rec.Landed {
|
||||
return either.Right[E](rec.Land)
|
||||
}
|
||||
current = f(a)(r)()
|
||||
current = f(rec.Bounce)(r)()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
A "github.com/IBM/fp-go/v2/array"
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
TR "github.com/IBM/fp-go/v2/tailrec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -58,18 +59,18 @@ func TestTailRecFactorial(t *testing.T) {
|
||||
MaxN: 20,
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.n > env.MaxN {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
|
||||
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
env.Logger(fmt.Sprintf("Complete: %d", state.acc))
|
||||
return E.Right[string](E.Right[State](state.acc))
|
||||
return E.Right[string](TR.Land[State](state.acc))
|
||||
}
|
||||
env.Logger(fmt.Sprintf("Step: %d * %d", state.n, state.acc))
|
||||
return E.Right[string](E.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,16 +96,16 @@ func TestTailRecFactorialError(t *testing.T) {
|
||||
MaxN: 10,
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.n > env.MaxN {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
|
||||
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
return E.Right[string](E.Right[State](state.acc))
|
||||
return E.Right[string](TR.Land[State](state.acc))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.n - 1, state.acc * state.n}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,16 +128,16 @@ func TestTailRecFibonacci(t *testing.T) {
|
||||
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 1000, Logs: []string{}}
|
||||
|
||||
fibStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
fibStep := func(state State) ReaderIOEither[TestEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.curr > env.MaxValue {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
|
||||
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
return E.Right[string](E.Right[State](state.curr * env.Multiplier))
|
||||
return E.Right[string](TR.Land[State](state.curr * env.Multiplier))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,16 +158,16 @@ func TestTailRecFibonacciError(t *testing.T) {
|
||||
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 50, Logs: []string{}}
|
||||
|
||||
fibStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
fibStep := func(state State) ReaderIOEither[TestEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.curr > env.MaxValue {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
|
||||
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
return E.Right[string](E.Right[State](state.curr))
|
||||
return E.Right[string](TR.Land[State](state.curr))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,16 +184,16 @@ func TestTailRecFibonacciError(t *testing.T) {
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
config := ConfigEnv{MinValue: 0, Step: 2, MaxRetries: 3}
|
||||
|
||||
countdownStep := func(n int) ReaderIOEither[ConfigEnv, string, E.Either[int, int]] {
|
||||
return func(cfg ConfigEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderIOEither[ConfigEnv, string, TR.Trampoline[int, int]] {
|
||||
return func(cfg ConfigEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[int, int]] {
|
||||
if n < 0 {
|
||||
return E.Left[E.Either[int, int]]("negative value")
|
||||
return E.Left[TR.Trampoline[int, int]]("negative value")
|
||||
}
|
||||
if n <= cfg.MinValue {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
return E.Right[string](TR.Land[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - cfg.Step))
|
||||
return E.Right[string](TR.Bounce[int](n - cfg.Step))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,16 +213,16 @@ func TestTailRecSumList(t *testing.T) {
|
||||
|
||||
env := TestEnv{Multiplier: 2, MaxValue: 100, Logs: []string{}}
|
||||
|
||||
sumStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
sumStep := func(state State) ReaderIOEither[TestEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.sum > env.MaxValue {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
|
||||
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
|
||||
}
|
||||
if A.IsEmpty(state.list) {
|
||||
return E.Right[string](E.Right[State](state.sum * env.Multiplier))
|
||||
return E.Right[string](TR.Land[State](state.sum * env.Multiplier))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,16 +242,16 @@ func TestTailRecSumListError(t *testing.T) {
|
||||
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 10, Logs: []string{}}
|
||||
|
||||
sumStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
sumStep := func(state State) ReaderIOEither[TestEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.sum > env.MaxValue {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
|
||||
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
|
||||
}
|
||||
if A.IsEmpty(state.list) {
|
||||
return E.Right[string](E.Right[State](state.sum))
|
||||
return E.Right[string](TR.Land[State](state.sum))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,10 +268,10 @@ func TestTailRecSumListError(t *testing.T) {
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 100, Logs: []string{}}
|
||||
|
||||
immediateStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
return E.Right[string](E.Right[int](n * env.Multiplier))
|
||||
immediateStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[int, int]] {
|
||||
return E.Right[string](TR.Land[int](n * env.Multiplier))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,10 +286,10 @@ func TestTailRecImmediateTermination(t *testing.T) {
|
||||
func TestTailRecImmediateError(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 100, Logs: []string{}}
|
||||
|
||||
immediateErrorStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
return E.Left[E.Either[int, int]]("immediate error")
|
||||
immediateErrorStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[int, int]] {
|
||||
return E.Left[TR.Trampoline[int, int]]("immediate error")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,16 +306,16 @@ func TestTailRecImmediateError(t *testing.T) {
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 2000000, Logs: []string{}}
|
||||
|
||||
countdownStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[int, int]] {
|
||||
if n > env.MaxValue {
|
||||
return E.Left[E.Either[int, int]]("value too large")
|
||||
return E.Left[TR.Trampoline[int, int]]("value too large")
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
return E.Right[string](TR.Land[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - 1))
|
||||
return E.Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,19 +340,19 @@ func TestTailRecFindInRange(t *testing.T) {
|
||||
|
||||
env := FindEnv{Target: 42, MaxN: 1000}
|
||||
|
||||
findStep := func(state State) ReaderIOEither[FindEnv, string, E.Either[State, int]] {
|
||||
return func(env FindEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
findStep := func(state State) ReaderIOEither[FindEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(env FindEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.current > env.MaxN {
|
||||
return E.Left[E.Either[State, int]]("search exceeded max")
|
||||
return E.Left[TR.Trampoline[State, int]]("search exceeded max")
|
||||
}
|
||||
if state.current >= state.max {
|
||||
return E.Right[string](E.Right[State](-1)) // Not found
|
||||
return E.Right[string](TR.Land[State](-1)) // Not found
|
||||
}
|
||||
if state.current == env.Target {
|
||||
return E.Right[string](E.Right[State](state.current)) // Found
|
||||
return E.Right[string](TR.Land[State](state.current)) // Found
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.current + 1, state.max}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.current + 1, state.max}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,19 +377,19 @@ func TestTailRecFindNotInRange(t *testing.T) {
|
||||
|
||||
env := FindEnv{Target: 200, MaxN: 1000}
|
||||
|
||||
findStep := func(state State) ReaderIOEither[FindEnv, string, E.Either[State, int]] {
|
||||
return func(env FindEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
findStep := func(state State) ReaderIOEither[FindEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(env FindEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.current > env.MaxN {
|
||||
return E.Left[E.Either[State, int]]("search exceeded max")
|
||||
return E.Left[TR.Trampoline[State, int]]("search exceeded max")
|
||||
}
|
||||
if state.current >= state.max {
|
||||
return E.Right[string](E.Right[State](-1)) // Not found
|
||||
return E.Right[string](TR.Land[State](-1)) // Not found
|
||||
}
|
||||
if state.current == env.Target {
|
||||
return E.Right[string](E.Right[State](state.current)) // Found
|
||||
return E.Right[string](TR.Land[State](state.current)) // Found
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.current + 1, state.max}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.current + 1, state.max}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,17 +410,17 @@ func TestTailRecWithLogging(t *testing.T) {
|
||||
MaxN: 100,
|
||||
}
|
||||
|
||||
countdownStep := func(n int) ReaderIOEither[LoggerEnv, string, E.Either[int, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderIOEither[LoggerEnv, string, TR.Trampoline[int, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[int, int]] {
|
||||
env.Logger(fmt.Sprintf("Count: %d", n))
|
||||
if n > env.MaxN {
|
||||
return E.Left[E.Either[int, int]]("value too large")
|
||||
return E.Left[TR.Trampoline[int, int]]("value too large")
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
return E.Right[string](TR.Land[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - 1))
|
||||
return E.Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,17 +449,17 @@ func TestTailRecGCD(t *testing.T) {
|
||||
MaxN: 1000,
|
||||
}
|
||||
|
||||
gcdStep := func(state State) ReaderIOEither[LoggerEnv, string, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
gcdStep := func(state State) ReaderIOEither[LoggerEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
env.Logger(fmt.Sprintf("gcd(%d, %d)", state.a, state.b))
|
||||
if state.a > env.MaxN || state.b > env.MaxN {
|
||||
return E.Left[E.Either[State, int]]("values too large")
|
||||
return E.Left[TR.Trampoline[State, int]]("values too large")
|
||||
}
|
||||
if state.b == 0 {
|
||||
return E.Right[string](E.Right[State](state.a))
|
||||
return E.Right[string](TR.Land[State](state.a))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.b, state.a % state.b}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.b, state.a % state.b}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -480,17 +481,17 @@ func TestTailRecRetryLogic(t *testing.T) {
|
||||
|
||||
config := ConfigEnv{MinValue: 0, Step: 1, MaxRetries: 3}
|
||||
|
||||
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, E.Either[State, int]] {
|
||||
return func(cfg ConfigEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(cfg ConfigEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.attempt > cfg.MaxRetries {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
|
||||
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
|
||||
}
|
||||
// Simulate success on 3rd attempt
|
||||
if state.attempt == 3 {
|
||||
return E.Right[string](E.Right[State](state.value))
|
||||
return E.Right[string](TR.Land[State](state.value))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.attempt + 1, state.value}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.attempt + 1, state.value}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -510,14 +511,14 @@ func TestTailRecRetryExceeded(t *testing.T) {
|
||||
|
||||
config := ConfigEnv{MinValue: 0, Step: 1, MaxRetries: 2}
|
||||
|
||||
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, E.Either[State, int]] {
|
||||
return func(cfg ConfigEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, TR.Trampoline[State, int]] {
|
||||
return func(cfg ConfigEnv) IOE.IOEither[string, TR.Trampoline[State, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[State, int]] {
|
||||
if state.attempt > cfg.MaxRetries {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
|
||||
return E.Left[TR.Trampoline[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
|
||||
}
|
||||
// Never succeeds
|
||||
return E.Right[string](E.Left[int](State{state.attempt + 1, state.value}))
|
||||
return E.Right[string](TR.Bounce[int](State{state.attempt + 1, state.value}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -540,16 +541,16 @@ func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
|
||||
|
||||
env := CounterEnv{Increment: 3, Limit: 20, MaxValue: 100}
|
||||
|
||||
counterStep := func(n int) ReaderIOEither[CounterEnv, string, E.Either[int, int]] {
|
||||
return func(env CounterEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
counterStep := func(n int) ReaderIOEither[CounterEnv, string, TR.Trampoline[int, int]] {
|
||||
return func(env CounterEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[int, int]] {
|
||||
if n > env.MaxValue {
|
||||
return E.Left[E.Either[int, int]]("value exceeds max")
|
||||
return E.Left[TR.Trampoline[int, int]]("value exceeds max")
|
||||
}
|
||||
if n >= env.Limit {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
return E.Right[string](TR.Land[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n + env.Increment))
|
||||
return E.Right[string](TR.Bounce[int](n + env.Increment))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -564,16 +565,16 @@ func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
|
||||
func TestTailRecErrorInMiddle(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 50, Logs: []string{}}
|
||||
|
||||
countdownStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
countdownStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[int, int]] {
|
||||
if n == 5 {
|
||||
return E.Left[E.Either[int, int]]("error at 5")
|
||||
return E.Left[TR.Trampoline[int, int]]("error at 5")
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
return E.Right[string](TR.Land[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - 1))
|
||||
return E.Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -588,13 +589,13 @@ func TestTailRecErrorInMiddle(t *testing.T) {
|
||||
|
||||
// TestTailRecDifferentEnvironments tests that different environments produce different results
|
||||
func TestTailRecDifferentEnvironments(t *testing.T) {
|
||||
multiplyStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
multiplyStep := func(n int) ReaderIOEither[TestEnv, string, TR.Trampoline[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, TR.Trampoline[int, int]] {
|
||||
return func() E.Either[string, TR.Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[string](E.Right[int](n * env.Multiplier))
|
||||
return E.Right[string](TR.Land[int](n * env.Multiplier))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - 1))
|
||||
return E.Right[string](TR.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ package readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/readerioeither"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, tailrec.Trampoline[A, B]]) Kleisli[R, A, B] {
|
||||
return readerioeither.TailRec(f)
|
||||
}
|
||||
|
||||
@@ -16,26 +16,25 @@
|
||||
package readeroption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, tailrec.Trampoline[A, B]]) Kleisli[R, A, B] {
|
||||
return func(a A) ReaderOption[R, B] {
|
||||
initialReader := f(a)
|
||||
return func(r R) Option[B] {
|
||||
return func(r R) option.Option[B] {
|
||||
current := initialReader(r)
|
||||
for {
|
||||
rec, ok := option.Unwrap(current)
|
||||
if !ok {
|
||||
return option.None[B]()
|
||||
}
|
||||
b, a := either.Unwrap(rec)
|
||||
if either.IsRight(rec) {
|
||||
return option.Some(b)
|
||||
if rec.Landed {
|
||||
return option.Some(rec.Land)
|
||||
}
|
||||
current = f(a)(r)
|
||||
current = f(rec.Bounce)(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,10 @@ package readerresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, tailrec.Trampoline[A, B]]) Kleisli[R, A, B] {
|
||||
return readereither.TailRec(f)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
Mo "github.com/IBM/fp-go/v2/monoid"
|
||||
G "github.com/IBM/fp-go/v2/record/generic"
|
||||
)
|
||||
|
||||
@@ -30,8 +29,8 @@ import (
|
||||
// Count int
|
||||
// }
|
||||
// result := record.Do[string, State]()
|
||||
func Do[K comparable, S any]() map[K]S {
|
||||
return G.Do[map[K]S]()
|
||||
func Do[K comparable, S any]() Record[K, S] {
|
||||
return G.Do[Record[K, S]]()
|
||||
}
|
||||
|
||||
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
@@ -68,29 +67,87 @@ func Do[K comparable, S any]() map[K]S {
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
func Bind[S1, T any, K comparable, S2 any](m Mo.Monoid[map[K]S2]) func(setter func(T) func(S1) S2, f func(S1) map[K]T) func(map[K]S1) map[K]S2 {
|
||||
return G.Bind[map[K]S1, map[K]S2, map[K]T](m)
|
||||
func Bind[S1, T any, K comparable, S2 any](m Monoid[Record[K, S2]]) func(
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[K, S1, T],
|
||||
) Operator[K, S1, S2] {
|
||||
return G.Bind[Record[K, S1], Record[K, S2], Record[K, T]](m)
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2].
|
||||
// Unlike Bind, Let does not require a Monoid because it transforms each value independently
|
||||
// without merging multiple maps.
|
||||
//
|
||||
// The setter function takes the computed value and returns a function that updates the context.
|
||||
// The computation function f takes the current context and produces a value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Length int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// map[string]State{"a": {Name: "Alice"}},
|
||||
// record.Let(
|
||||
// func(length int) func(State) State {
|
||||
// return func(s State) State { s.Length = length; return s }
|
||||
// },
|
||||
// func(s State) int { return len(s.Name) },
|
||||
// ),
|
||||
// ) // map[string]State{"a": {Name: "Alice", Length: 5}}
|
||||
func Let[S1, T any, K comparable, S2 any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
) func(map[K]S1) map[K]S2 {
|
||||
return G.Let[map[K]S1, map[K]S2](setter, f)
|
||||
) Operator[K, S1, S2] {
|
||||
return G.Let[Record[K, S1], Record[K, S2]](setter, f)
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
|
||||
// This is similar to Let but uses a fixed value instead of computing it from the context.
|
||||
//
|
||||
// The setter function takes the value and returns a function that updates the context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// Version int
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe2(
|
||||
// map[string]State{"a": {Name: "Alice"}},
|
||||
// record.LetTo(
|
||||
// func(version int) func(State) State {
|
||||
// return func(s State) State { s.Version = version; return s }
|
||||
// },
|
||||
// 2,
|
||||
// ),
|
||||
// ) // map[string]State{"a": {Name: "Alice", Version: 2}}
|
||||
func LetTo[S1, T any, K comparable, S2 any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
) func(map[K]S1) map[K]S2 {
|
||||
return G.LetTo[map[K]S1, map[K]S2](setter, b)
|
||||
) Operator[K, S1, S2] {
|
||||
return G.LetTo[Record[K, S1], Record[K, S2]](setter, b)
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
func BindTo[S1, T any, K comparable](setter func(T) S1) func(map[K]T) map[K]S1 {
|
||||
return G.BindTo[map[K]S1, map[K]T](setter)
|
||||
// BindTo initializes a new state [S1] from a value [T].
|
||||
// This is typically used as the first step in a do-notation chain to convert
|
||||
// a simple map of values into a map of state objects.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type State struct {
|
||||
// Name string
|
||||
// }
|
||||
//
|
||||
// result := F.Pipe1(
|
||||
// map[string]string{"a": "Alice", "b": "Bob"},
|
||||
// record.BindTo(func(name string) State { return State{Name: name} }),
|
||||
// ) // map[string]State{"a": {Name: "Alice"}, "b": {Name: "Bob"}}
|
||||
func BindTo[S1, T any, K comparable](setter func(T) S1) Operator[K, T, S1] {
|
||||
return G.BindTo[Record[K, S1], Record[K, T]](setter)
|
||||
}
|
||||
|
||||
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
|
||||
@@ -126,6 +183,9 @@ func BindTo[S1, T any, K comparable](setter func(T) S1) func(map[K]T) map[K]S1 {
|
||||
// counts,
|
||||
// ),
|
||||
// ) // map[string]State{"a": {Name: "Alice", Count: 10}, "b": {Name: "Bob", Count: 20}}
|
||||
func ApS[S1, T any, K comparable, S2 any](m Mo.Monoid[map[K]S2]) func(setter func(T) func(S1) S2, fa map[K]T) func(map[K]S1) map[K]S2 {
|
||||
return G.ApS[map[K]S1, map[K]S2, map[K]T](m)
|
||||
func ApS[S1, T any, K comparable, S2 any](m Monoid[Record[K, S2]]) func(
|
||||
setter func(T) func(S1) S2,
|
||||
fa Record[K, T],
|
||||
) Operator[K, S1, S2] {
|
||||
return G.ApS[Record[K, S1], Record[K, S2], Record[K, T]](m)
|
||||
}
|
||||
|
||||
225
v2/record/bind_test.go
Normal file
225
v2/record/bind_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
// 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 record
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestState struct {
|
||||
Name string
|
||||
Count int
|
||||
Version int
|
||||
}
|
||||
|
||||
func TestDo(t *testing.T) {
|
||||
result := Do[string, TestState]()
|
||||
assert.NotNil(t, result)
|
||||
assert.Empty(t, result)
|
||||
assert.Equal(t, map[string]TestState{}, result)
|
||||
}
|
||||
|
||||
func TestBindTo(t *testing.T) {
|
||||
input := map[string]string{"a": "Alice", "b": "Bob"}
|
||||
result := F.Pipe1(
|
||||
input,
|
||||
BindTo[TestState, string, string](func(name string) TestState {
|
||||
return TestState{Name: name}
|
||||
}),
|
||||
)
|
||||
expected := map[string]TestState{
|
||||
"a": {Name: "Alice"},
|
||||
"b": {Name: "Bob"},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestLet(t *testing.T) {
|
||||
input := map[string]TestState{
|
||||
"a": {Name: "Alice"},
|
||||
"b": {Name: "Bob"},
|
||||
}
|
||||
result := F.Pipe1(
|
||||
input,
|
||||
Let[TestState, int, string](
|
||||
func(length int) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Count = length
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s TestState) int {
|
||||
return len(s.Name)
|
||||
},
|
||||
),
|
||||
)
|
||||
expected := map[string]TestState{
|
||||
"a": {Name: "Alice", Count: 5},
|
||||
"b": {Name: "Bob", Count: 3},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestLetTo(t *testing.T) {
|
||||
input := map[string]TestState{
|
||||
"a": {Name: "Alice"},
|
||||
"b": {Name: "Bob"},
|
||||
}
|
||||
result := F.Pipe1(
|
||||
input,
|
||||
LetTo[TestState, int, string](
|
||||
func(version int) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Version = version
|
||||
return s
|
||||
}
|
||||
},
|
||||
2,
|
||||
),
|
||||
)
|
||||
expected := map[string]TestState{
|
||||
"a": {Name: "Alice", Version: 2},
|
||||
"b": {Name: "Bob", Version: 2},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestBind(t *testing.T) {
|
||||
monoid := MergeMonoid[string, TestState]()
|
||||
|
||||
// Bind chains computations where each step can depend on previous results
|
||||
result := F.Pipe1(
|
||||
map[string]string{"x": "test"},
|
||||
Bind[string, int](monoid)(
|
||||
func(length int) func(string) TestState {
|
||||
return func(s string) TestState {
|
||||
return TestState{Name: s, Count: length}
|
||||
}
|
||||
},
|
||||
func(s string) map[string]int {
|
||||
return map[string]int{"x": len(s)}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expected := map[string]TestState{
|
||||
"x": {Name: "test", Count: 4},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestApS(t *testing.T) {
|
||||
monoid := MergeMonoid[string, TestState]()
|
||||
|
||||
// ApS applies independent computations
|
||||
names := map[string]string{"x": "Alice"}
|
||||
counts := map[string]int{"x": 10}
|
||||
|
||||
result := F.Pipe2(
|
||||
map[string]TestState{"x": {}},
|
||||
ApS[TestState, string](monoid)(
|
||||
func(name string) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Name = name
|
||||
return s
|
||||
}
|
||||
},
|
||||
names,
|
||||
),
|
||||
ApS[TestState, int](monoid)(
|
||||
func(count int) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Count = count
|
||||
return s
|
||||
}
|
||||
},
|
||||
counts,
|
||||
),
|
||||
)
|
||||
|
||||
expected := map[string]TestState{
|
||||
"x": {Name: "Alice", Count: 10},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestBindChain(t *testing.T) {
|
||||
// Test a complete do-notation chain with BindTo, Let, and LetTo
|
||||
result := F.Pipe3(
|
||||
map[string]string{"x": "Alice", "y": "Bob"},
|
||||
BindTo[TestState, string, string](func(name string) TestState {
|
||||
return TestState{Name: name}
|
||||
}),
|
||||
Let[TestState, int, string](
|
||||
func(count int) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Count = count
|
||||
return s
|
||||
}
|
||||
},
|
||||
func(s TestState) int {
|
||||
return len(s.Name)
|
||||
},
|
||||
),
|
||||
LetTo[TestState, int, string](
|
||||
func(version int) func(TestState) TestState {
|
||||
return func(s TestState) TestState {
|
||||
s.Version = version
|
||||
return s
|
||||
}
|
||||
},
|
||||
1,
|
||||
),
|
||||
)
|
||||
|
||||
expected := map[string]TestState{
|
||||
"x": {Name: "Alice", Count: 5, Version: 1},
|
||||
"y": {Name: "Bob", Count: 3, Version: 1},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestBindWithDependentComputation(t *testing.T) {
|
||||
// Test Bind where the computation creates new keys based on input
|
||||
monoid := MergeMonoid[string, TestState]()
|
||||
|
||||
result := F.Pipe1(
|
||||
map[string]int{"x": 5},
|
||||
Bind[int, string](monoid)(
|
||||
func(str string) func(int) TestState {
|
||||
return func(n int) TestState {
|
||||
return TestState{Name: str, Count: n}
|
||||
}
|
||||
},
|
||||
func(n int) map[string]string {
|
||||
// Create a string based on the number
|
||||
result := ""
|
||||
for i := 0; i < n; i++ {
|
||||
result += "a"
|
||||
}
|
||||
return map[string]string{"x": result}
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
expected := map[string]TestState{
|
||||
"x": {Name: "aaaaa", Count: 5},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
85
v2/record/coverage.out
Normal file
85
v2/record/coverage.out
Normal file
@@ -0,0 +1,85 @@
|
||||
mode: set
|
||||
github.com/IBM/fp-go/v2/record/bind.go:33.40,35.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/bind.go:71.144,73.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/bind.go:79.27,81.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/bind.go:87.27,89.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/bind.go:92.80,94.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/bind.go:129.135,131.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/eq.go:23.55,25.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/eq.go:28.56,30.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/monoid.go:25.75,27.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/monoid.go:30.63,32.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/monoid.go:35.64,37.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/monoid.go:40.59,42.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:28.51,30.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:33.54,35.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:38.47,40.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:43.49,45.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:48.72,50.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:53.92,55.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:57.80,59.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:61.92,63.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:65.84,67.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:69.96,71.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:73.71,75.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:77.83,79.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:81.87,83.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:85.75,87.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:89.69,91.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:93.73,95.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:97.81,99.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:101.85,103.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:106.65,108.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:111.67,113.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:116.52,118.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:120.84,122.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:125.70,127.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:130.43,132.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:135.47,137.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:139.60,141.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:143.62,145.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:147.65,149.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:151.68,153.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:155.63,157.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:160.55,162.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:165.103,167.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:170.91,172.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:175.72,177.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:180.84,182.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:185.49,187.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:190.52,192.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:195.46,197.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:199.124,201.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:203.112,205.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:207.125,209.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:211.113,213.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:216.85,218.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:221.141,223.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:226.129,228.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:231.86,233.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:236.98,238.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:241.64,243.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:246.104,248.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:251.92,253.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:256.108,258.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:261.86,263.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:266.120,268.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:271.69,273.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:276.71,278.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:280.78,282.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:284.74,286.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:289.51,291.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:294.80,296.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:305.88,307.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:314.73,316.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:324.60,326.2 1 0
|
||||
github.com/IBM/fp-go/v2/record/record.go:332.55,334.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:336.105,338.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:340.106,342.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/record.go:344.48,346.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/semigroup.go:53.81,55.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/semigroup.go:80.69,82.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/semigroup.go:107.70,109.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/traverse.go:27.41,29.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/traverse.go:39.38,41.2 1 1
|
||||
github.com/IBM/fp-go/v2/record/traverse.go:50.23,53.2 1 1
|
||||
@@ -20,11 +20,11 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/record/generic"
|
||||
)
|
||||
|
||||
func Eq[K comparable, V any](e E.Eq[V]) E.Eq[map[K]V] {
|
||||
return G.Eq[map[K]V](e)
|
||||
func Eq[K comparable, V any](e E.Eq[V]) E.Eq[Record[K, V]] {
|
||||
return G.Eq[Record[K, V]](e)
|
||||
}
|
||||
|
||||
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
|
||||
func FromStrictEquals[K, V comparable]() E.Eq[map[K]V] {
|
||||
return G.FromStrictEquals[map[K]V]()
|
||||
func FromStrictEquals[K, V comparable]() E.Eq[Record[K, V]] {
|
||||
return G.FromStrictEquals[Record[K, V]]()
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
Mo "github.com/IBM/fp-go/v2/monoid"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/ord"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
)
|
||||
|
||||
func IsEmpty[M ~map[K]V, K comparable, V any](r M) bool {
|
||||
@@ -59,9 +59,9 @@ func ValuesOrd[M ~map[K]V, GV ~[]V, K comparable, V any](o ord.Ord[K]) func(r M)
|
||||
|
||||
func collectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K], r M, f func(K, V) R) GR {
|
||||
// create the entries
|
||||
entries := toEntriesOrd[M, []T.Tuple2[K, V]](o, r)
|
||||
entries := toEntriesOrd[M, []pair.Pair[K, V]](o, r)
|
||||
// collect this array
|
||||
ft := T.Tupled2(f)
|
||||
ft := pair.Paired(f)
|
||||
count := len(entries)
|
||||
result := make(GR, count)
|
||||
for i := count - 1; i >= 0; i-- {
|
||||
@@ -73,13 +73,13 @@ func collectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K], r M,
|
||||
|
||||
func reduceOrd[M ~map[K]V, K comparable, V, R any](o ord.Ord[K], r M, f func(K, R, V) R, initial R) R {
|
||||
// create the entries
|
||||
entries := toEntriesOrd[M, []T.Tuple2[K, V]](o, r)
|
||||
entries := toEntriesOrd[M, []pair.Pair[K, V]](o, r)
|
||||
// collect this array
|
||||
current := initial
|
||||
count := len(entries)
|
||||
for i := 0; i < count; i++ {
|
||||
t := entries[i]
|
||||
current = f(T.First(t), current, T.Second(t))
|
||||
current = f(pair.Head(t), current, pair.Tail(t))
|
||||
}
|
||||
// done
|
||||
return current
|
||||
@@ -318,32 +318,32 @@ func Size[M ~map[K]V, K comparable, V any](r M) int {
|
||||
return len(r)
|
||||
}
|
||||
|
||||
func ToArray[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
|
||||
return collect[M, GT](r, T.MakeTuple2[K, V])
|
||||
func ToArray[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](r M) GT {
|
||||
return collect[M, GT](r, pair.MakePair[K, V])
|
||||
}
|
||||
|
||||
func toEntriesOrd[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](o ord.Ord[K], r M) GT {
|
||||
func toEntriesOrd[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](o ord.Ord[K], r M) GT {
|
||||
// total number of elements
|
||||
count := len(r)
|
||||
// produce an array that we can sort by key
|
||||
entries := make(GT, count)
|
||||
idx := 0
|
||||
for k, v := range r {
|
||||
entries[idx] = T.MakeTuple2(k, v)
|
||||
entries[idx] = pair.MakePair(k, v)
|
||||
idx++
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool {
|
||||
return o.Compare(T.First(entries[i]), T.First(entries[j])) < 0
|
||||
return o.Compare(pair.Head(entries[i]), pair.Head(entries[j])) < 0
|
||||
})
|
||||
// final entries
|
||||
return entries
|
||||
}
|
||||
|
||||
func ToEntriesOrd[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](o ord.Ord[K]) func(r M) GT {
|
||||
func ToEntriesOrd[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](o ord.Ord[K]) func(r M) GT {
|
||||
return F.Bind1st(toEntriesOrd[M, GT, K, V], o)
|
||||
}
|
||||
|
||||
func ToEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
|
||||
func ToEntries[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](r M) GT {
|
||||
return ToArray[M, GT](r)
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ func ToEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](r M) GT {
|
||||
// its values into a tuple. The key and value are then used to populate the map. Duplicate
|
||||
// values are resolved via the provided [Mg.Magma]
|
||||
func FromFoldableMap[
|
||||
FCT ~func(A) T.Tuple2[K, V],
|
||||
FCT ~func(A) pair.Pair[K, V],
|
||||
HKTA any,
|
||||
FOLDABLE ~func(func(M, A) M, M) func(HKTA) M,
|
||||
M ~map[K]V,
|
||||
@@ -364,12 +364,12 @@ func FromFoldableMap[
|
||||
dst = make(M)
|
||||
}
|
||||
e := f(a)
|
||||
k := T.First(e)
|
||||
k := pair.Head(e)
|
||||
old, ok := dst[k]
|
||||
if ok {
|
||||
dst[k] = m.Concat(old, T.Second(e))
|
||||
dst[k] = m.Concat(old, pair.Tail(e))
|
||||
} else {
|
||||
dst[k] = T.Second(e)
|
||||
dst[k] = pair.Tail(e)
|
||||
}
|
||||
return dst
|
||||
}, Empty[M]())
|
||||
@@ -378,15 +378,15 @@ func FromFoldableMap[
|
||||
|
||||
func FromFoldable[
|
||||
HKTA any,
|
||||
FOLDABLE ~func(func(M, T.Tuple2[K, V]) M, M) func(HKTA) M,
|
||||
FOLDABLE ~func(func(M, pair.Pair[K, V]) M, M) func(HKTA) M,
|
||||
M ~map[K]V,
|
||||
K comparable,
|
||||
V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) M {
|
||||
return FromFoldableMap[func(T.Tuple2[K, V]) T.Tuple2[K, V]](m, red)(F.Identity[T.Tuple2[K, V]])
|
||||
return FromFoldableMap[func(pair.Pair[K, V]) pair.Pair[K, V]](m, red)(F.Identity[pair.Pair[K, V]])
|
||||
}
|
||||
|
||||
func FromArrayMap[
|
||||
FCT ~func(A) T.Tuple2[K, V],
|
||||
FCT ~func(A) pair.Pair[K, V],
|
||||
GA ~[]A,
|
||||
M ~map[K]V,
|
||||
A any,
|
||||
@@ -396,17 +396,17 @@ func FromArrayMap[
|
||||
}
|
||||
|
||||
func FromArray[
|
||||
GA ~[]T.Tuple2[K, V],
|
||||
GA ~[]pair.Pair[K, V],
|
||||
M ~map[K]V,
|
||||
K comparable,
|
||||
V any](m Mg.Magma[V]) func(fa GA) M {
|
||||
return FromFoldable(m, F.Bind23of3(RAG.Reduce[GA, T.Tuple2[K, V], M]))
|
||||
return FromFoldable(m, F.Bind23of3(RAG.Reduce[GA, pair.Pair[K, V], M]))
|
||||
}
|
||||
|
||||
func FromEntries[M ~map[K]V, GT ~[]T.Tuple2[K, V], K comparable, V any](fa GT) M {
|
||||
func FromEntries[M ~map[K]V, GT ~[]pair.Pair[K, V], K comparable, V any](fa GT) M {
|
||||
m := make(M)
|
||||
for _, t := range fa {
|
||||
upsertAtReadWrite(m, t.F1, t.F2)
|
||||
upsertAtReadWrite(m, pair.Head(t), pair.Tail(t))
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -16,27 +16,34 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
M "github.com/IBM/fp-go/v2/monoid"
|
||||
G "github.com/IBM/fp-go/v2/record/generic"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
// UnionMonoid computes the union of two maps of the same type
|
||||
func UnionMonoid[K comparable, V any](s S.Semigroup[V]) M.Monoid[map[K]V] {
|
||||
return G.UnionMonoid[map[K]V](s)
|
||||
//
|
||||
//go:inline
|
||||
func UnionMonoid[K comparable, V any](s S.Semigroup[V]) Monoid[Record[K, V]] {
|
||||
return G.UnionMonoid[Record[K, V]](s)
|
||||
}
|
||||
|
||||
// UnionLastMonoid computes the union of two maps of the same type giving the last map precedence
|
||||
func UnionLastMonoid[K comparable, V any]() M.Monoid[map[K]V] {
|
||||
return G.UnionLastMonoid[map[K]V]()
|
||||
//
|
||||
//go:inline
|
||||
func UnionLastMonoid[K comparable, V any]() Monoid[Record[K, V]] {
|
||||
return G.UnionLastMonoid[Record[K, V]]()
|
||||
}
|
||||
|
||||
// UnionFirstMonoid computes the union of two maps of the same type giving the first map precedence
|
||||
func UnionFirstMonoid[K comparable, V any]() M.Monoid[map[K]V] {
|
||||
return G.UnionFirstMonoid[map[K]V]()
|
||||
//
|
||||
//go:inline
|
||||
func UnionFirstMonoid[K comparable, V any]() Monoid[Record[K, V]] {
|
||||
return G.UnionFirstMonoid[Record[K, V]]()
|
||||
}
|
||||
|
||||
// MergeMonoid computes the union of two maps of the same type giving the last map precedence
|
||||
func MergeMonoid[K comparable, V any]() M.Monoid[map[K]V] {
|
||||
return G.UnionLastMonoid[map[K]V]()
|
||||
//
|
||||
//go:inline
|
||||
func MergeMonoid[K comparable, V any]() Monoid[Record[K, V]] {
|
||||
return G.UnionLastMonoid[Record[K, V]]()
|
||||
}
|
||||
|
||||
@@ -16,295 +16,316 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
EM "github.com/IBM/fp-go/v2/endomorphism"
|
||||
Mg "github.com/IBM/fp-go/v2/magma"
|
||||
Mo "github.com/IBM/fp-go/v2/monoid"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/ord"
|
||||
G "github.com/IBM/fp-go/v2/record/generic"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
)
|
||||
|
||||
// IsEmpty tests if a map is empty
|
||||
func IsEmpty[K comparable, V any](r map[K]V) bool {
|
||||
func IsEmpty[K comparable, V any](r Record[K, V]) bool {
|
||||
return G.IsEmpty(r)
|
||||
}
|
||||
|
||||
// IsNonEmpty tests if a map is not empty
|
||||
func IsNonEmpty[K comparable, V any](r map[K]V) bool {
|
||||
func IsNonEmpty[K comparable, V any](r Record[K, V]) bool {
|
||||
return G.IsNonEmpty(r)
|
||||
}
|
||||
|
||||
// Keys returns the key in a map
|
||||
func Keys[K comparable, V any](r map[K]V) []K {
|
||||
return G.Keys[map[K]V, []K](r)
|
||||
func Keys[K comparable, V any](r Record[K, V]) []K {
|
||||
return G.Keys[Record[K, V], []K](r)
|
||||
}
|
||||
|
||||
// Values returns the values in a map
|
||||
func Values[K comparable, V any](r map[K]V) []V {
|
||||
return G.Values[map[K]V, []V](r)
|
||||
func Values[K comparable, V any](r Record[K, V]) []V {
|
||||
return G.Values[Record[K, V], []V](r)
|
||||
}
|
||||
|
||||
// Collect applies a collector function to the key value pairs in a map and returns the result as an array
|
||||
func Collect[K comparable, V, R any](f func(K, V) R) func(map[K]V) []R {
|
||||
return G.Collect[map[K]V, []R](f)
|
||||
func Collect[K comparable, V, R any](f func(K, V) R) func(Record[K, V]) []R {
|
||||
return G.Collect[Record[K, V], []R](f)
|
||||
}
|
||||
|
||||
// CollectOrd applies a collector function to the key value pairs in a map and returns the result as an array
|
||||
func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(map[K]V) []R {
|
||||
return G.CollectOrd[map[K]V, []R](o)
|
||||
func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(Record[K, V]) []R {
|
||||
return G.CollectOrd[Record[K, V], []R](o)
|
||||
}
|
||||
|
||||
func Reduce[K comparable, V, R any](f func(R, V) R, initial R) func(map[K]V) R {
|
||||
return G.Reduce[map[K]V](f, initial)
|
||||
// Reduce reduces a map to a single value by applying a reducer function to each value
|
||||
func Reduce[K comparable, V, R any](f func(R, V) R, initial R) func(Record[K, V]) R {
|
||||
return G.Reduce[Record[K, V]](f, initial)
|
||||
}
|
||||
|
||||
func ReduceWithIndex[K comparable, V, R any](f func(K, R, V) R, initial R) func(map[K]V) R {
|
||||
return G.ReduceWithIndex[map[K]V](f, initial)
|
||||
// ReduceWithIndex reduces a map to a single value by applying a reducer function to each key-value pair
|
||||
func ReduceWithIndex[K comparable, V, R any](f func(K, R, V) R, initial R) func(Record[K, V]) R {
|
||||
return G.ReduceWithIndex[Record[K, V]](f, initial)
|
||||
}
|
||||
|
||||
func ReduceRef[K comparable, V, R any](f func(R, *V) R, initial R) func(map[K]V) R {
|
||||
return G.ReduceRef[map[K]V](f, initial)
|
||||
// ReduceRef reduces a map to a single value by applying a reducer function to each value reference
|
||||
func ReduceRef[K comparable, V, R any](f func(R, *V) R, initial R) func(Record[K, V]) R {
|
||||
return G.ReduceRef[Record[K, V]](f, initial)
|
||||
}
|
||||
|
||||
func ReduceRefWithIndex[K comparable, V, R any](f func(K, R, *V) R, initial R) func(map[K]V) R {
|
||||
return G.ReduceRefWithIndex[map[K]V](f, initial)
|
||||
// ReduceRefWithIndex reduces a map to a single value by applying a reducer function to each key-value pair with value references
|
||||
func ReduceRefWithIndex[K comparable, V, R any](f func(K, R, *V) R, initial R) func(Record[K, V]) R {
|
||||
return G.ReduceRefWithIndex[Record[K, V]](f, initial)
|
||||
}
|
||||
|
||||
func MonadMap[K comparable, V, R any](r map[K]V, f func(V) R) map[K]R {
|
||||
return G.MonadMap[map[K]V, map[K]R](r, f)
|
||||
// MonadMap transforms each value in a map using the provided function
|
||||
func MonadMap[K comparable, V, R any](r Record[K, V], f func(V) R) Record[K, R] {
|
||||
return G.MonadMap[Record[K, V], Record[K, R]](r, f)
|
||||
}
|
||||
|
||||
func MonadMapWithIndex[K comparable, V, R any](r map[K]V, f func(K, V) R) map[K]R {
|
||||
return G.MonadMapWithIndex[map[K]V, map[K]R](r, f)
|
||||
// MonadMapWithIndex transforms each key-value pair in a map using the provided function
|
||||
func MonadMapWithIndex[K comparable, V, R any](r Record[K, V], f func(K, V) R) Record[K, R] {
|
||||
return G.MonadMapWithIndex[Record[K, V], Record[K, R]](r, f)
|
||||
}
|
||||
|
||||
func MonadMapRefWithIndex[K comparable, V, R any](r map[K]V, f func(K, *V) R) map[K]R {
|
||||
return G.MonadMapRefWithIndex[map[K]V, map[K]R](r, f)
|
||||
// MonadMapRefWithIndex transforms each key-value pair in a map using the provided function with value references
|
||||
func MonadMapRefWithIndex[K comparable, V, R any](r Record[K, V], f func(K, *V) R) Record[K, R] {
|
||||
return G.MonadMapRefWithIndex[Record[K, V], Record[K, R]](r, f)
|
||||
}
|
||||
|
||||
func MonadMapRef[K comparable, V, R any](r map[K]V, f func(*V) R) map[K]R {
|
||||
return G.MonadMapRef[map[K]V, map[K]R](r, f)
|
||||
// MonadMapRef transforms each value in a map using the provided function with value references
|
||||
func MonadMapRef[K comparable, V, R any](r Record[K, V], f func(*V) R) Record[K, R] {
|
||||
return G.MonadMapRef[Record[K, V], Record[K, R]](r, f)
|
||||
}
|
||||
|
||||
func Map[K comparable, V, R any](f func(V) R) func(map[K]V) map[K]R {
|
||||
return G.Map[map[K]V, map[K]R](f)
|
||||
// Map returns a function that transforms each value in a map using the provided function
|
||||
func Map[K comparable, V, R any](f func(V) R) Operator[K, V, R] {
|
||||
return G.Map[Record[K, V], Record[K, R]](f)
|
||||
}
|
||||
|
||||
func MapRef[K comparable, V, R any](f func(*V) R) func(map[K]V) map[K]R {
|
||||
return G.MapRef[map[K]V, map[K]R](f)
|
||||
// MapRef returns a function that transforms each value in a map using the provided function with value references
|
||||
func MapRef[K comparable, V, R any](f func(*V) R) Operator[K, V, R] {
|
||||
return G.MapRef[Record[K, V], Record[K, R]](f)
|
||||
}
|
||||
|
||||
func MapWithIndex[K comparable, V, R any](f func(K, V) R) func(map[K]V) map[K]R {
|
||||
return G.MapWithIndex[map[K]V, map[K]R](f)
|
||||
// MapWithIndex returns a function that transforms each key-value pair in a map using the provided function
|
||||
func MapWithIndex[K comparable, V, R any](f func(K, V) R) Operator[K, V, R] {
|
||||
return G.MapWithIndex[Record[K, V], Record[K, R]](f)
|
||||
}
|
||||
|
||||
func MapRefWithIndex[K comparable, V, R any](f func(K, *V) R) func(map[K]V) map[K]R {
|
||||
return G.MapRefWithIndex[map[K]V, map[K]R](f)
|
||||
// MapRefWithIndex returns a function that transforms each key-value pair in a map using the provided function with value references
|
||||
func MapRefWithIndex[K comparable, V, R any](f func(K, *V) R) Operator[K, V, R] {
|
||||
return G.MapRefWithIndex[Record[K, V], Record[K, R]](f)
|
||||
}
|
||||
|
||||
// Lookup returns the entry for a key in a map if it exists
|
||||
func Lookup[V any, K comparable](k K) func(map[K]V) O.Option[V] {
|
||||
return G.Lookup[map[K]V](k)
|
||||
func Lookup[V any, K comparable](k K) option.Kleisli[Record[K, V], V] {
|
||||
return G.Lookup[Record[K, V]](k)
|
||||
}
|
||||
|
||||
// MonadLookup returns the entry for a key in a map if it exists
|
||||
func MonadLookup[V any, K comparable](m map[K]V, k K) O.Option[V] {
|
||||
func MonadLookup[V any, K comparable](m Record[K, V], k K) Option[V] {
|
||||
return G.MonadLookup(m, k)
|
||||
}
|
||||
|
||||
// Has tests if a key is contained in a map
|
||||
func Has[K comparable, V any](k K, r map[K]V) bool {
|
||||
func Has[K comparable, V any](k K, r Record[K, V]) bool {
|
||||
return G.Has(k, r)
|
||||
}
|
||||
|
||||
func Union[K comparable, V any](m Mg.Magma[V]) func(map[K]V) func(map[K]V) map[K]V {
|
||||
return G.Union[map[K]V](m)
|
||||
// Union combines two maps using the provided Magma to resolve conflicts for duplicate keys
|
||||
func Union[K comparable, V any](m Mg.Magma[V]) func(Record[K, V]) Operator[K, V, V] {
|
||||
return G.Union[Record[K, V]](m)
|
||||
}
|
||||
|
||||
// Merge combines two maps giving the values in the right one precedence. Also refer to [MergeMonoid]
|
||||
func Merge[K comparable, V any](right map[K]V) func(map[K]V) map[K]V {
|
||||
func Merge[K comparable, V any](right Record[K, V]) Operator[K, V, V] {
|
||||
return G.Merge(right)
|
||||
}
|
||||
|
||||
// Empty creates an empty map
|
||||
func Empty[K comparable, V any]() map[K]V {
|
||||
return G.Empty[map[K]V]()
|
||||
func Empty[K comparable, V any]() Record[K, V] {
|
||||
return G.Empty[Record[K, V]]()
|
||||
}
|
||||
|
||||
// Size returns the number of elements in a map
|
||||
func Size[K comparable, V any](r map[K]V) int {
|
||||
func Size[K comparable, V any](r Record[K, V]) int {
|
||||
return G.Size(r)
|
||||
}
|
||||
|
||||
func ToArray[K comparable, V any](r map[K]V) []T.Tuple2[K, V] {
|
||||
return G.ToArray[map[K]V, []T.Tuple2[K, V]](r)
|
||||
// ToArray converts a map to an array of key-value pairs
|
||||
func ToArray[K comparable, V any](r Record[K, V]) Entries[K, V] {
|
||||
return G.ToArray[Record[K, V], Entries[K, V]](r)
|
||||
}
|
||||
|
||||
func ToEntries[K comparable, V any](r map[K]V) []T.Tuple2[K, V] {
|
||||
return G.ToEntries[map[K]V, []T.Tuple2[K, V]](r)
|
||||
// ToEntries converts a map to an array of key-value pairs (alias for ToArray)
|
||||
func ToEntries[K comparable, V any](r Record[K, V]) Entries[K, V] {
|
||||
return G.ToEntries[Record[K, V], Entries[K, V]](r)
|
||||
}
|
||||
|
||||
func FromEntries[K comparable, V any](fa []T.Tuple2[K, V]) map[K]V {
|
||||
return G.FromEntries[map[K]V](fa)
|
||||
// FromEntries creates a map from an array of key-value pairs
|
||||
func FromEntries[K comparable, V any](fa Entries[K, V]) Record[K, V] {
|
||||
return G.FromEntries[Record[K, V]](fa)
|
||||
}
|
||||
|
||||
func UpsertAt[K comparable, V any](k K, v V) func(map[K]V) map[K]V {
|
||||
return G.UpsertAt[map[K]V](k, v)
|
||||
// UpsertAt returns a function that inserts or updates a key-value pair in a map
|
||||
func UpsertAt[K comparable, V any](k K, v V) Operator[K, V, V] {
|
||||
return G.UpsertAt[Record[K, V]](k, v)
|
||||
}
|
||||
|
||||
func DeleteAt[K comparable, V any](k K) func(map[K]V) map[K]V {
|
||||
return G.DeleteAt[map[K]V](k)
|
||||
// DeleteAt returns a function that removes a key from a map
|
||||
func DeleteAt[K comparable, V any](k K) Operator[K, V, V] {
|
||||
return G.DeleteAt[Record[K, V]](k)
|
||||
}
|
||||
|
||||
// Singleton creates a new map with a single entry
|
||||
func Singleton[K comparable, V any](k K, v V) map[K]V {
|
||||
return G.Singleton[map[K]V](k, v)
|
||||
func Singleton[K comparable, V any](k K, v V) Record[K, V] {
|
||||
return G.Singleton[Record[K, V]](k, v)
|
||||
}
|
||||
|
||||
// FilterMapWithIndex creates a new map with only the elements for which the transformation function creates a Some
|
||||
func FilterMapWithIndex[K comparable, V1, V2 any](f func(K, V1) O.Option[V2]) func(map[K]V1) map[K]V2 {
|
||||
return G.FilterMapWithIndex[map[K]V1, map[K]V2](f)
|
||||
func FilterMapWithIndex[K comparable, V1, V2 any](f func(K, V1) Option[V2]) Operator[K, V1, V2] {
|
||||
return G.FilterMapWithIndex[Record[K, V1], Record[K, V2]](f)
|
||||
}
|
||||
|
||||
// FilterMap creates a new map with only the elements for which the transformation function creates a Some
|
||||
func FilterMap[K comparable, V1, V2 any](f func(V1) O.Option[V2]) func(map[K]V1) map[K]V2 {
|
||||
return G.FilterMap[map[K]V1, map[K]V2](f)
|
||||
func FilterMap[K comparable, V1, V2 any](f option.Kleisli[V1, V2]) Operator[K, V1, V2] {
|
||||
return G.FilterMap[Record[K, V1], Record[K, V2]](f)
|
||||
}
|
||||
|
||||
// Filter creates a new map with only the elements that match the predicate
|
||||
func Filter[K comparable, V any](f func(K) bool) func(map[K]V) map[K]V {
|
||||
return G.Filter[map[K]V](f)
|
||||
func Filter[K comparable, V any](f Predicate[K]) Operator[K, V, V] {
|
||||
return G.Filter[Record[K, V]](f)
|
||||
}
|
||||
|
||||
// FilterWithIndex creates a new map with only the elements that match the predicate
|
||||
func FilterWithIndex[K comparable, V any](f func(K, V) bool) func(map[K]V) map[K]V {
|
||||
return G.FilterWithIndex[map[K]V](f)
|
||||
func FilterWithIndex[K comparable, V any](f PredicateWithIndex[K, V]) Operator[K, V, V] {
|
||||
return G.FilterWithIndex[Record[K, V]](f)
|
||||
}
|
||||
|
||||
// IsNil checks if the map is set to nil
|
||||
func IsNil[K comparable, V any](m map[K]V) bool {
|
||||
func IsNil[K comparable, V any](m Record[K, V]) bool {
|
||||
return G.IsNil(m)
|
||||
}
|
||||
|
||||
// IsNonNil checks if the map is set to nil
|
||||
func IsNonNil[K comparable, V any](m map[K]V) bool {
|
||||
func IsNonNil[K comparable, V any](m Record[K, V]) bool {
|
||||
return G.IsNonNil(m)
|
||||
}
|
||||
|
||||
// ConstNil return a nil map
|
||||
func ConstNil[K comparable, V any]() map[K]V {
|
||||
return map[K]V(nil)
|
||||
func ConstNil[K comparable, V any]() Record[K, V] {
|
||||
return Record[K, V](nil)
|
||||
}
|
||||
|
||||
func MonadChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2], r map[K]V1, f func(K, V1) map[K]V2) map[K]V2 {
|
||||
// MonadChainWithIndex chains a map transformation function that produces maps, combining results using the provided Monoid
|
||||
func MonadChainWithIndex[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]], r Record[K, V1], f KleisliWithIndex[K, V1, V2]) Record[K, V2] {
|
||||
return G.MonadChainWithIndex(m, r, f)
|
||||
}
|
||||
|
||||
func MonadChain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2], r map[K]V1, f func(V1) map[K]V2) map[K]V2 {
|
||||
// MonadChain chains a map transformation function that produces maps, combining results using the provided Monoid
|
||||
func MonadChain[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]], r Record[K, V1], f Kleisli[K, V1, V2]) Record[K, V2] {
|
||||
return G.MonadChain(m, r, f)
|
||||
}
|
||||
|
||||
func ChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(K, V1) map[K]V2) func(map[K]V1) map[K]V2 {
|
||||
return G.ChainWithIndex[map[K]V1](m)
|
||||
// ChainWithIndex returns a function that chains a map transformation function that produces maps, combining results using the provided Monoid
|
||||
func ChainWithIndex[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(KleisliWithIndex[K, V1, V2]) Operator[K, V1, V2] {
|
||||
return G.ChainWithIndex[Record[K, V1]](m)
|
||||
}
|
||||
|
||||
func Chain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(V1) map[K]V2) func(map[K]V1) map[K]V2 {
|
||||
return G.Chain[map[K]V1](m)
|
||||
// Chain returns a function that chains a map transformation function that produces maps, combining results using the provided Monoid
|
||||
func Chain[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(Kleisli[K, V1, V2]) Operator[K, V1, V2] {
|
||||
return G.Chain[Record[K, V1]](m)
|
||||
}
|
||||
|
||||
// Flatten converts a nested map into a regular map
|
||||
func Flatten[K comparable, V any](m Mo.Monoid[map[K]V]) func(map[K]map[K]V) map[K]V {
|
||||
return G.Flatten[map[K]map[K]V](m)
|
||||
func Flatten[K comparable, V any](m Monoid[Record[K, V]]) func(Record[K, Record[K, V]]) Record[K, V] {
|
||||
return G.Flatten[Record[K, Record[K, V]]](m)
|
||||
}
|
||||
|
||||
// FilterChainWithIndex creates a new map with only the elements for which the transformation function creates a Some
|
||||
func FilterChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(K, V1) O.Option[map[K]V2]) func(map[K]V1) map[K]V2 {
|
||||
return G.FilterChainWithIndex[map[K]V1](m)
|
||||
func FilterChainWithIndex[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(func(K, V1) Option[Record[K, V2]]) Operator[K, V1, V2] {
|
||||
return G.FilterChainWithIndex[Record[K, V1]](m)
|
||||
}
|
||||
|
||||
// FilterChain creates a new map with only the elements for which the transformation function creates a Some
|
||||
func FilterChain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(V1) O.Option[map[K]V2]) func(map[K]V1) map[K]V2 {
|
||||
return G.FilterChain[map[K]V1](m)
|
||||
func FilterChain[V1 any, K comparable, V2 any](m Monoid[Record[K, V2]]) func(option.Kleisli[V1, Record[K, V2]]) Operator[K, V1, V2] {
|
||||
return G.FilterChain[Record[K, V1]](m)
|
||||
}
|
||||
|
||||
// FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid.
|
||||
func FoldMap[K comparable, A, B any](m Mo.Monoid[B]) func(func(A) B) func(map[K]A) B {
|
||||
return G.FoldMap[map[K]A](m)
|
||||
func FoldMap[K comparable, A, B any](m Monoid[B]) func(func(A) B) func(Record[K, A]) B {
|
||||
return G.FoldMap[Record[K, A]](m)
|
||||
}
|
||||
|
||||
// FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid.
|
||||
func FoldMapWithIndex[K comparable, A, B any](m Mo.Monoid[B]) func(func(K, A) B) func(map[K]A) B {
|
||||
return G.FoldMapWithIndex[map[K]A](m)
|
||||
func FoldMapWithIndex[K comparable, A, B any](m Monoid[B]) func(func(K, A) B) func(Record[K, A]) B {
|
||||
return G.FoldMapWithIndex[Record[K, A]](m)
|
||||
}
|
||||
|
||||
// Fold folds the record using the provided Monoid.
|
||||
func Fold[K comparable, A any](m Mo.Monoid[A]) func(map[K]A) A {
|
||||
return G.Fold[map[K]A](m)
|
||||
func Fold[K comparable, A any](m Monoid[A]) func(Record[K, A]) A {
|
||||
return G.Fold[Record[K, A]](m)
|
||||
}
|
||||
|
||||
// ReduceOrdWithIndex reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order
|
||||
func ReduceOrdWithIndex[V, R any, K comparable](o ord.Ord[K]) func(func(K, R, V) R, R) func(map[K]V) R {
|
||||
return G.ReduceOrdWithIndex[map[K]V, K, V, R](o)
|
||||
func ReduceOrdWithIndex[V, R any, K comparable](o ord.Ord[K]) func(func(K, R, V) R, R) func(Record[K, V]) R {
|
||||
return G.ReduceOrdWithIndex[Record[K, V], K, V, R](o)
|
||||
}
|
||||
|
||||
// ReduceOrd reduces a map into a single value via a reducer function making sure that the keys are passed to the reducer in the specified order
|
||||
func ReduceOrd[V, R any, K comparable](o ord.Ord[K]) func(func(R, V) R, R) func(map[K]V) R {
|
||||
return G.ReduceOrd[map[K]V, K, V, R](o)
|
||||
func ReduceOrd[V, R any, K comparable](o ord.Ord[K]) func(func(R, V) R, R) func(Record[K, V]) R {
|
||||
return G.ReduceOrd[Record[K, V], K, V, R](o)
|
||||
}
|
||||
|
||||
// FoldMap maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order
|
||||
func FoldMapOrd[A, B any, K comparable](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(A) B) func(map[K]A) B {
|
||||
return G.FoldMapOrd[map[K]A, K, A, B](o)
|
||||
func FoldMapOrd[A, B any, K comparable](o ord.Ord[K]) func(m Monoid[B]) func(func(A) B) func(Record[K, A]) B {
|
||||
return G.FoldMapOrd[Record[K, A], K, A, B](o)
|
||||
}
|
||||
|
||||
// Fold folds the record using the provided Monoid with the items passed in the given order
|
||||
func FoldOrd[A any, K comparable](o ord.Ord[K]) func(m Mo.Monoid[A]) func(map[K]A) A {
|
||||
return G.FoldOrd[map[K]A](o)
|
||||
func FoldOrd[A any, K comparable](o ord.Ord[K]) func(m Monoid[A]) func(Record[K, A]) A {
|
||||
return G.FoldOrd[Record[K, A]](o)
|
||||
}
|
||||
|
||||
// FoldMapWithIndex maps and folds a record. Map the record passing each value to the iterating function. Then fold the results using the provided Monoid and the items in the provided order
|
||||
func FoldMapOrdWithIndex[K comparable, A, B any](o ord.Ord[K]) func(m Mo.Monoid[B]) func(func(K, A) B) func(map[K]A) B {
|
||||
return G.FoldMapOrdWithIndex[map[K]A, K, A, B](o)
|
||||
func FoldMapOrdWithIndex[K comparable, A, B any](o ord.Ord[K]) func(m Monoid[B]) func(func(K, A) B) func(Record[K, A]) B {
|
||||
return G.FoldMapOrdWithIndex[Record[K, A], K, A, B](o)
|
||||
}
|
||||
|
||||
// KeysOrd returns the keys in the map in their given order
|
||||
func KeysOrd[V any, K comparable](o ord.Ord[K]) func(r map[K]V) []K {
|
||||
return G.KeysOrd[map[K]V, []K](o)
|
||||
func KeysOrd[V any, K comparable](o ord.Ord[K]) func(r Record[K, V]) []K {
|
||||
return G.KeysOrd[Record[K, V], []K](o)
|
||||
}
|
||||
|
||||
// ValuesOrd returns the values in the map ordered by their keys in the given order
|
||||
func ValuesOrd[V any, K comparable](o ord.Ord[K]) func(r map[K]V) []V {
|
||||
return G.ValuesOrd[map[K]V, []V](o)
|
||||
func ValuesOrd[V any, K comparable](o ord.Ord[K]) func(r Record[K, V]) []V {
|
||||
return G.ValuesOrd[Record[K, V], []V](o)
|
||||
}
|
||||
|
||||
func MonadFlap[B any, K comparable, A any](fab map[K]func(A) B, a A) map[K]B {
|
||||
return G.MonadFlap[map[K]func(A) B, map[K]B](fab, a)
|
||||
// MonadFlap applies a value to a map of functions, producing a map of results
|
||||
func MonadFlap[B any, K comparable, A any](fab Record[K, func(A) B], a A) Record[K, B] {
|
||||
return G.MonadFlap[Record[K, func(A) B], Record[K, B]](fab, a)
|
||||
}
|
||||
|
||||
func Flap[B any, K comparable, A any](a A) func(map[K]func(A) B) map[K]B {
|
||||
return G.Flap[map[K]func(A) B, map[K]B](a)
|
||||
// Flap returns a function that applies a value to a map of functions, producing a map of results
|
||||
func Flap[B any, K comparable, A any](a A) Operator[K, func(A) B, B] {
|
||||
return G.Flap[Record[K, func(A) B], Record[K, B]](a)
|
||||
}
|
||||
|
||||
// Copy creates a shallow copy of the map
|
||||
func Copy[K comparable, V any](m map[K]V) map[K]V {
|
||||
func Copy[K comparable, V any](m Record[K, V]) Record[K, V] {
|
||||
return G.Copy(m)
|
||||
}
|
||||
|
||||
// Clone creates a deep copy of the map using the provided endomorphism to clone the values
|
||||
func Clone[K comparable, V any](f EM.Endomorphism[V]) EM.Endomorphism[map[K]V] {
|
||||
return G.Clone[map[K]V](f)
|
||||
func Clone[K comparable, V any](f Endomorphism[V]) Endomorphism[Record[K, V]] {
|
||||
return G.Clone[Record[K, V]](f)
|
||||
}
|
||||
|
||||
// FromFoldableMap converts from a reducer to a map
|
||||
// Duplicate keys are resolved by the provided [Mg.Magma]
|
||||
func FromFoldableMap[
|
||||
FOLDABLE ~func(func(map[K]V, A) map[K]V, map[K]V) func(HKTA) map[K]V, // the reduce function
|
||||
FOLDABLE ~func(func(Record[K, V], A) Record[K, V], Record[K, V]) func(HKTA) Record[K, V], // the reduce function
|
||||
A any,
|
||||
HKTA any,
|
||||
K comparable,
|
||||
V any](m Mg.Magma[V], red FOLDABLE) func(f func(A) T.Tuple2[K, V]) func(fa HKTA) map[K]V {
|
||||
return G.FromFoldableMap[func(A) T.Tuple2[K, V]](m, red)
|
||||
V any](m Mg.Magma[V], red FOLDABLE) func(f func(A) Entry[K, V]) Kleisli[K, HKTA, V] {
|
||||
return G.FromFoldableMap[func(A) Entry[K, V]](m, red)
|
||||
}
|
||||
|
||||
// FromArrayMap converts from an array to a map
|
||||
@@ -312,17 +333,17 @@ func FromFoldableMap[
|
||||
func FromArrayMap[
|
||||
A any,
|
||||
K comparable,
|
||||
V any](m Mg.Magma[V]) func(f func(A) T.Tuple2[K, V]) func(fa []A) map[K]V {
|
||||
return G.FromArrayMap[func(A) T.Tuple2[K, V], []A, map[K]V](m)
|
||||
V any](m Mg.Magma[V]) func(f func(A) Entry[K, V]) Kleisli[K, []A, V] {
|
||||
return G.FromArrayMap[func(A) Entry[K, V], []A, Record[K, V]](m)
|
||||
}
|
||||
|
||||
// FromFoldable converts from a reducer to a map
|
||||
// Duplicate keys are resolved by the provided [Mg.Magma]
|
||||
func FromFoldable[
|
||||
HKTA any,
|
||||
FOLDABLE ~func(func(map[K]V, T.Tuple2[K, V]) map[K]V, map[K]V) func(HKTA) map[K]V, // the reduce function
|
||||
FOLDABLE ~func(func(Record[K, V], Entry[K, V]) Record[K, V], Record[K, V]) func(HKTA) Record[K, V], // the reduce function
|
||||
K comparable,
|
||||
V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) map[K]V {
|
||||
V any](m Mg.Magma[V], red FOLDABLE) Kleisli[K, HKTA, V] {
|
||||
return G.FromFoldable(m, red)
|
||||
}
|
||||
|
||||
@@ -330,14 +351,21 @@ func FromFoldable[
|
||||
// Duplicate keys are resolved by the provided [Mg.Magma]
|
||||
func FromArray[
|
||||
K comparable,
|
||||
V any](m Mg.Magma[V]) func(fa []T.Tuple2[K, V]) map[K]V {
|
||||
return G.FromArray[[]T.Tuple2[K, V], map[K]V](m)
|
||||
V any](m Mg.Magma[V]) Kleisli[K, Entries[K, V], V] {
|
||||
return G.FromArray[Entries[K, V], Record[K, V]](m)
|
||||
}
|
||||
|
||||
func MonadAp[A any, K comparable, B any](m Mo.Monoid[map[K]B], fab map[K]func(A) B, fa map[K]A) map[K]B {
|
||||
// MonadAp applies a map of functions to a map of values, combining results using the provided Monoid
|
||||
func MonadAp[A any, K comparable, B any](m Monoid[Record[K, B]], fab Record[K, func(A) B], fa Record[K, A]) Record[K, B] {
|
||||
return G.MonadAp(m, fab, fa)
|
||||
}
|
||||
|
||||
func Ap[A any, K comparable, B any](m Mo.Monoid[map[K]B]) func(fa map[K]A) func(map[K]func(A) B) map[K]B {
|
||||
return G.Ap[map[K]B, map[K]func(A) B, map[K]A](m)
|
||||
// Ap returns a function that applies a map of functions to a map of values, combining results using the provided Monoid
|
||||
func Ap[A any, K comparable, B any](m Monoid[Record[K, B]]) func(fa Record[K, A]) Operator[K, func(A) B, B] {
|
||||
return G.Ap[Record[K, B], Record[K, func(A) B], Record[K, A]](m)
|
||||
}
|
||||
|
||||
// Of creates a map with a single key-value pair
|
||||
func Of[K comparable, A any](k K, a A) Record[K, A] {
|
||||
return Record[K, A]{k: a}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/utils"
|
||||
Mg "github.com/IBM/fp-go/v2/magma"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
P "github.com/IBM/fp-go/v2/pair"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
T "github.com/IBM/fp-go/v2/tuple"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -156,7 +156,7 @@ func TestFromArrayMap(t *testing.T) {
|
||||
src1 := A.From("a", "b", "c", "a")
|
||||
frm := FromArrayMap[string, string](Mg.Second[string]())
|
||||
|
||||
f := frm(T.Replicate2[string])
|
||||
f := frm(P.Of[string])
|
||||
|
||||
res1 := f(src1)
|
||||
|
||||
@@ -198,3 +198,555 @@ func TestHas(t *testing.T) {
|
||||
assert.True(t, Has("a", nonEmpty))
|
||||
assert.False(t, Has("c", nonEmpty))
|
||||
}
|
||||
|
||||
func TestCollect(t *testing.T) {
|
||||
data := map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
}
|
||||
collector := Collect(func(k string, v int) string {
|
||||
return fmt.Sprintf("%s=%d", k, v)
|
||||
})
|
||||
result := collector(data)
|
||||
sort.Strings(result)
|
||||
assert.Equal(t, []string{"a=1", "b=2", "c=3"}, result)
|
||||
}
|
||||
|
||||
func TestCollectOrd(t *testing.T) {
|
||||
data := map[string]int{
|
||||
"c": 3,
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
}
|
||||
collector := CollectOrd[int, string](S.Ord)(func(k string, v int) string {
|
||||
return fmt.Sprintf("%s=%d", k, v)
|
||||
})
|
||||
result := collector(data)
|
||||
assert.Equal(t, []string{"a=1", "b=2", "c=3"}, result)
|
||||
}
|
||||
|
||||
func TestReduce(t *testing.T) {
|
||||
data := map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
}
|
||||
sum := Reduce[string](func(acc, v int) int {
|
||||
return acc + v
|
||||
}, 0)
|
||||
result := sum(data)
|
||||
assert.Equal(t, 6, result)
|
||||
}
|
||||
|
||||
func TestReduceWithIndex(t *testing.T) {
|
||||
data := map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
}
|
||||
concat := ReduceWithIndex(func(k string, acc string, v int) string {
|
||||
if acc == "" {
|
||||
return fmt.Sprintf("%s:%d", k, v)
|
||||
}
|
||||
return fmt.Sprintf("%s,%s:%d", acc, k, v)
|
||||
}, "")
|
||||
result := concat(data)
|
||||
// Result order is non-deterministic, so check it contains all parts
|
||||
assert.Contains(t, result, "a:1")
|
||||
assert.Contains(t, result, "b:2")
|
||||
assert.Contains(t, result, "c:3")
|
||||
}
|
||||
|
||||
func TestMonadMap(t *testing.T) {
|
||||
data := map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
}
|
||||
result := MonadMap(data, func(v int) int { return v * 2 })
|
||||
assert.Equal(t, map[string]int{"a": 2, "b": 4, "c": 6}, result)
|
||||
}
|
||||
|
||||
func TestMonadMapWithIndex(t *testing.T) {
|
||||
data := map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
}
|
||||
result := MonadMapWithIndex(data, func(k string, v int) string {
|
||||
return fmt.Sprintf("%s=%d", k, v)
|
||||
})
|
||||
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
|
||||
}
|
||||
|
||||
func TestMapWithIndex(t *testing.T) {
|
||||
data := map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
}
|
||||
mapper := MapWithIndex(func(k string, v int) string {
|
||||
return fmt.Sprintf("%s=%d", k, v)
|
||||
})
|
||||
result := mapper(data)
|
||||
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
|
||||
}
|
||||
|
||||
func TestMonadLookup(t *testing.T) {
|
||||
data := map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
}
|
||||
assert.Equal(t, O.Some(1), MonadLookup(data, "a"))
|
||||
assert.Equal(t, O.None[int](), MonadLookup(data, "c"))
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
left := map[string]int{"a": 1, "b": 2}
|
||||
right := map[string]int{"b": 3, "c": 4}
|
||||
result := Merge(right)(left)
|
||||
assert.Equal(t, map[string]int{"a": 1, "b": 3, "c": 4}, result)
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
assert.Equal(t, 3, Size(data))
|
||||
assert.Equal(t, 0, Size(Empty[string, int]()))
|
||||
}
|
||||
|
||||
func TestToArray(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
result := ToArray(data)
|
||||
assert.Len(t, result, 2)
|
||||
// Check both entries exist (order is non-deterministic)
|
||||
found := make(map[string]int)
|
||||
for _, entry := range result {
|
||||
found[P.Head(entry)] = P.Tail(entry)
|
||||
}
|
||||
assert.Equal(t, data, found)
|
||||
}
|
||||
|
||||
func TestToEntries(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
result := ToEntries(data)
|
||||
assert.Len(t, result, 2)
|
||||
}
|
||||
|
||||
func TestFromEntries(t *testing.T) {
|
||||
entries := Entries[string, int]{
|
||||
P.MakePair("a", 1),
|
||||
P.MakePair("b", 2),
|
||||
}
|
||||
result := FromEntries(entries)
|
||||
assert.Equal(t, map[string]int{"a": 1, "b": 2}, result)
|
||||
}
|
||||
|
||||
func TestUpsertAt(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
result := UpsertAt("c", 3)(data)
|
||||
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 3}, result)
|
||||
// Original should be unchanged
|
||||
assert.Equal(t, map[string]int{"a": 1, "b": 2}, data)
|
||||
|
||||
// Update existing
|
||||
result2 := UpsertAt("a", 10)(data)
|
||||
assert.Equal(t, map[string]int{"a": 10, "b": 2}, result2)
|
||||
}
|
||||
|
||||
func TestDeleteAt(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
result := DeleteAt[string, int]("b")(data)
|
||||
assert.Equal(t, map[string]int{"a": 1, "c": 3}, result)
|
||||
// Original should be unchanged
|
||||
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 3}, data)
|
||||
}
|
||||
|
||||
func TestSingleton(t *testing.T) {
|
||||
result := Singleton("key", 42)
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
}
|
||||
|
||||
func TestFilterMapWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
filter := FilterMapWithIndex(func(k string, v int) O.Option[int] {
|
||||
if v%2 == 0 {
|
||||
return O.Some(v * 10)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
result := filter(data)
|
||||
assert.Equal(t, map[string]int{"b": 20}, result)
|
||||
}
|
||||
|
||||
func TestFilterMap(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
filter := FilterMap[string](func(v int) O.Option[int] {
|
||||
if v%2 == 0 {
|
||||
return O.Some(v * 10)
|
||||
}
|
||||
return O.None[int]()
|
||||
})
|
||||
result := filter(data)
|
||||
assert.Equal(t, map[string]int{"b": 20}, result)
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
filter := Filter[string, int](func(k string) bool {
|
||||
return k != "b"
|
||||
})
|
||||
result := filter(data)
|
||||
assert.Equal(t, map[string]int{"a": 1, "c": 3}, result)
|
||||
}
|
||||
|
||||
func TestFilterWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
filter := FilterWithIndex(func(k string, v int) bool {
|
||||
return v%2 == 0
|
||||
})
|
||||
result := filter(data)
|
||||
assert.Equal(t, map[string]int{"b": 2}, result)
|
||||
}
|
||||
|
||||
func TestIsNil(t *testing.T) {
|
||||
var nilMap map[string]int
|
||||
nonNilMap := map[string]int{}
|
||||
|
||||
assert.True(t, IsNil(nilMap))
|
||||
assert.False(t, IsNil(nonNilMap))
|
||||
}
|
||||
|
||||
func TestIsNonNil(t *testing.T) {
|
||||
var nilMap map[string]int
|
||||
nonNilMap := map[string]int{}
|
||||
|
||||
assert.False(t, IsNonNil(nilMap))
|
||||
assert.True(t, IsNonNil(nonNilMap))
|
||||
}
|
||||
|
||||
func TestConstNil(t *testing.T) {
|
||||
result := ConstNil[string, int]()
|
||||
assert.Nil(t, result)
|
||||
assert.True(t, IsNil(result))
|
||||
}
|
||||
|
||||
func TestMonadChain(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
monoid := MergeMonoid[string, int]()
|
||||
result := MonadChain(monoid, data, func(v int) map[string]int {
|
||||
return map[string]int{
|
||||
fmt.Sprintf("x%d", v): v * 10,
|
||||
}
|
||||
})
|
||||
assert.Equal(t, map[string]int{"x1": 10, "x2": 20}, result)
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
monoid := MergeMonoid[string, int]()
|
||||
chain := Chain[int](monoid)(func(v int) map[string]int {
|
||||
return map[string]int{
|
||||
fmt.Sprintf("x%d", v): v * 10,
|
||||
}
|
||||
})
|
||||
result := chain(data)
|
||||
assert.Equal(t, map[string]int{"x1": 10, "x2": 20}, result)
|
||||
}
|
||||
|
||||
func TestFlatten(t *testing.T) {
|
||||
nested := map[string]map[string]int{
|
||||
"a": {"x": 1, "y": 2},
|
||||
"b": {"z": 3},
|
||||
}
|
||||
monoid := MergeMonoid[string, int]()
|
||||
flatten := Flatten(monoid)
|
||||
result := flatten(nested)
|
||||
assert.Equal(t, map[string]int{"x": 1, "y": 2, "z": 3}, result)
|
||||
}
|
||||
|
||||
func TestFoldMap(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
// Use string monoid for simplicity
|
||||
fold := FoldMap[string, int](S.Monoid)(func(v int) string {
|
||||
return fmt.Sprintf("%d", v)
|
||||
})
|
||||
result := fold(data)
|
||||
// Result contains all digits but order is non-deterministic
|
||||
assert.Contains(t, result, "1")
|
||||
assert.Contains(t, result, "2")
|
||||
assert.Contains(t, result, "3")
|
||||
}
|
||||
|
||||
func TestFold(t *testing.T) {
|
||||
data := map[string]string{"a": "A", "b": "B", "c": "C"}
|
||||
fold := Fold[string](S.Monoid)
|
||||
result := fold(data)
|
||||
// Result contains all letters but order is non-deterministic
|
||||
assert.Contains(t, result, "A")
|
||||
assert.Contains(t, result, "B")
|
||||
assert.Contains(t, result, "C")
|
||||
}
|
||||
|
||||
func TestKeysOrd(t *testing.T) {
|
||||
data := map[string]int{"c": 3, "a": 1, "b": 2}
|
||||
keys := KeysOrd[int](S.Ord)(data)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, keys)
|
||||
}
|
||||
|
||||
func TestMonadFlap(t *testing.T) {
|
||||
fns := map[string]func(int) int{
|
||||
"double": func(x int) int { return x * 2 },
|
||||
"triple": func(x int) int { return x * 3 },
|
||||
}
|
||||
result := MonadFlap(fns, 5)
|
||||
assert.Equal(t, map[string]int{"double": 10, "triple": 15}, result)
|
||||
}
|
||||
|
||||
func TestFlap(t *testing.T) {
|
||||
fns := map[string]func(int) int{
|
||||
"double": func(x int) int { return x * 2 },
|
||||
"triple": func(x int) int { return x * 3 },
|
||||
}
|
||||
flap := Flap[int, string](5)
|
||||
result := flap(fns)
|
||||
assert.Equal(t, map[string]int{"double": 10, "triple": 15}, result)
|
||||
}
|
||||
|
||||
func TestFromArray(t *testing.T) {
|
||||
entries := Entries[string, int]{
|
||||
P.MakePair("a", 1),
|
||||
P.MakePair("b", 2),
|
||||
P.MakePair("a", 3), // Duplicate key
|
||||
}
|
||||
// Use Second magma to keep last value
|
||||
from := FromArray[string](Mg.Second[int]())
|
||||
result := from(entries)
|
||||
assert.Equal(t, map[string]int{"a": 3, "b": 2}, result)
|
||||
}
|
||||
|
||||
func TestMonadAp(t *testing.T) {
|
||||
fns := map[string]func(int) int{
|
||||
"double": func(x int) int { return x * 2 },
|
||||
}
|
||||
vals := map[string]int{
|
||||
"double": 5,
|
||||
}
|
||||
monoid := MergeMonoid[string, int]()
|
||||
result := MonadAp(monoid, fns, vals)
|
||||
assert.Equal(t, map[string]int{"double": 10}, result)
|
||||
}
|
||||
|
||||
func TestAp(t *testing.T) {
|
||||
fns := map[string]func(int) int{
|
||||
"double": func(x int) int { return x * 2 },
|
||||
}
|
||||
vals := map[string]int{
|
||||
"double": 5,
|
||||
}
|
||||
monoid := MergeMonoid[string, int]()
|
||||
ap := Ap[int](monoid)(vals)
|
||||
result := ap(fns)
|
||||
assert.Equal(t, map[string]int{"double": 10}, result)
|
||||
}
|
||||
|
||||
func TestOf(t *testing.T) {
|
||||
result := Of("key", 42)
|
||||
assert.Equal(t, map[string]int{"key": 42}, result)
|
||||
}
|
||||
|
||||
func TestReduceRef(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
sum := ReduceRef[string](func(acc int, v *int) int {
|
||||
return acc + *v
|
||||
}, 0)
|
||||
result := sum(data)
|
||||
assert.Equal(t, 6, result)
|
||||
}
|
||||
|
||||
func TestReduceRefWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
concat := ReduceRefWithIndex(func(k string, acc string, v *int) string {
|
||||
if acc == "" {
|
||||
return fmt.Sprintf("%s:%d", k, *v)
|
||||
}
|
||||
return fmt.Sprintf("%s,%s:%d", acc, k, *v)
|
||||
}, "")
|
||||
result := concat(data)
|
||||
assert.Contains(t, result, "a:1")
|
||||
assert.Contains(t, result, "b:2")
|
||||
}
|
||||
|
||||
func TestMonadMapRef(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
result := MonadMapRef(data, func(v *int) int { return *v * 2 })
|
||||
assert.Equal(t, map[string]int{"a": 2, "b": 4}, result)
|
||||
}
|
||||
|
||||
func TestMapRef(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
mapper := MapRef[string](func(v *int) int { return *v * 2 })
|
||||
result := mapper(data)
|
||||
assert.Equal(t, map[string]int{"a": 2, "b": 4}, result)
|
||||
}
|
||||
|
||||
func TestMonadMapRefWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
result := MonadMapRefWithIndex(data, func(k string, v *int) string {
|
||||
return fmt.Sprintf("%s=%d", k, *v)
|
||||
})
|
||||
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
|
||||
}
|
||||
|
||||
func TestMapRefWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
mapper := MapRefWithIndex(func(k string, v *int) string {
|
||||
return fmt.Sprintf("%s=%d", k, *v)
|
||||
})
|
||||
result := mapper(data)
|
||||
assert.Equal(t, map[string]string{"a": "a=1", "b": "b=2"}, result)
|
||||
}
|
||||
|
||||
func TestUnion(t *testing.T) {
|
||||
left := map[string]int{"a": 1, "b": 2}
|
||||
right := map[string]int{"b": 3, "c": 4}
|
||||
// Union combines maps, with the magma resolving conflicts
|
||||
// The order is union(left)(right), which means right is merged into left
|
||||
// First magma keeps the first value (from right in this case)
|
||||
union := Union[string](Mg.First[int]())
|
||||
result := union(left)(right)
|
||||
assert.Equal(t, map[string]int{"a": 1, "b": 3, "c": 4}, result)
|
||||
|
||||
// Second magma keeps the second value (from left in this case)
|
||||
union2 := Union[string](Mg.Second[int]())
|
||||
result2 := union2(left)(right)
|
||||
assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 4}, result2)
|
||||
}
|
||||
|
||||
func TestMonadChainWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
monoid := MergeMonoid[string, string]()
|
||||
result := MonadChainWithIndex(monoid, data, func(k string, v int) map[string]string {
|
||||
return map[string]string{
|
||||
fmt.Sprintf("%s%d", k, v): fmt.Sprintf("val%d", v),
|
||||
}
|
||||
})
|
||||
assert.Equal(t, map[string]string{"a1": "val1", "b2": "val2"}, result)
|
||||
}
|
||||
|
||||
func TestChainWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2}
|
||||
monoid := MergeMonoid[string, string]()
|
||||
chain := ChainWithIndex[int](monoid)(func(k string, v int) map[string]string {
|
||||
return map[string]string{
|
||||
fmt.Sprintf("%s%d", k, v): fmt.Sprintf("val%d", v),
|
||||
}
|
||||
})
|
||||
result := chain(data)
|
||||
assert.Equal(t, map[string]string{"a1": "val1", "b2": "val2"}, result)
|
||||
}
|
||||
|
||||
func TestFilterChainWithIndex(t *testing.T) {
|
||||
src := map[string]int{
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"c": 3,
|
||||
}
|
||||
|
||||
f := func(k string, value int) O.Option[map[string]string] {
|
||||
if value%2 != 0 {
|
||||
return O.Of(map[string]string{
|
||||
k: fmt.Sprintf("%s%d", k, value),
|
||||
})
|
||||
}
|
||||
return O.None[map[string]string]()
|
||||
}
|
||||
|
||||
monoid := MergeMonoid[string, string]()
|
||||
res := FilterChainWithIndex[int](monoid)(f)(src)
|
||||
|
||||
assert.Equal(t, map[string]string{
|
||||
"a": "a1",
|
||||
"c": "c3",
|
||||
}, res)
|
||||
}
|
||||
|
||||
func TestFoldMapWithIndex(t *testing.T) {
|
||||
data := map[string]int{"a": 1, "b": 2, "c": 3}
|
||||
fold := FoldMapWithIndex[string, int](S.Monoid)(func(k string, v int) string {
|
||||
return fmt.Sprintf("%s:%d", k, v)
|
||||
})
|
||||
result := fold(data)
|
||||
// Result contains all pairs but order is non-deterministic
|
||||
assert.Contains(t, result, "a:1")
|
||||
assert.Contains(t, result, "b:2")
|
||||
assert.Contains(t, result, "c:3")
|
||||
}
|
||||
|
||||
func TestReduceOrd(t *testing.T) {
|
||||
data := map[string]int{"c": 3, "a": 1, "b": 2}
|
||||
sum := ReduceOrd[int, int](S.Ord)(func(acc, v int) int {
|
||||
return acc + v
|
||||
}, 0)
|
||||
result := sum(data)
|
||||
assert.Equal(t, 6, result)
|
||||
}
|
||||
|
||||
func TestReduceOrdWithIndex(t *testing.T) {
|
||||
data := map[string]int{"c": 3, "a": 1, "b": 2}
|
||||
concat := ReduceOrdWithIndex[int, string](S.Ord)(func(k string, acc string, v int) string {
|
||||
if acc == "" {
|
||||
return fmt.Sprintf("%s:%d", k, v)
|
||||
}
|
||||
return fmt.Sprintf("%s,%s:%d", acc, k, v)
|
||||
}, "")
|
||||
result := concat(data)
|
||||
// With Ord, keys should be in order
|
||||
assert.Equal(t, "a:1,b:2,c:3", result)
|
||||
}
|
||||
|
||||
func TestFoldMapOrdWithIndex(t *testing.T) {
|
||||
data := map[string]int{"c": 3, "a": 1, "b": 2}
|
||||
fold := FoldMapOrdWithIndex[string, int, string](S.Ord)(S.Monoid)(func(k string, v int) string {
|
||||
return fmt.Sprintf("%s:%d,", k, v)
|
||||
})
|
||||
result := fold(data)
|
||||
assert.Equal(t, "a:1,b:2,c:3,", result)
|
||||
}
|
||||
|
||||
func TestFoldOrd(t *testing.T) {
|
||||
data := map[string]string{"c": "C", "a": "A", "b": "B"}
|
||||
fold := FoldOrd[string](S.Ord)(S.Monoid)
|
||||
result := fold(data)
|
||||
assert.Equal(t, "ABC", result)
|
||||
}
|
||||
|
||||
func TestFromFoldableMap(t *testing.T) {
|
||||
src := A.From("a", "b", "c", "a")
|
||||
// Create a reducer function
|
||||
reducer := A.Reduce[string, map[string]string]
|
||||
from := FromFoldableMap(
|
||||
Mg.Second[string](),
|
||||
reducer,
|
||||
)
|
||||
f := from(P.Of[string])
|
||||
result := f(src)
|
||||
assert.Equal(t, map[string]string{
|
||||
"a": "a",
|
||||
"b": "b",
|
||||
"c": "c",
|
||||
}, result)
|
||||
}
|
||||
|
||||
func TestFromFoldable(t *testing.T) {
|
||||
entries := Entries[string, int]{
|
||||
P.MakePair("a", 1),
|
||||
P.MakePair("b", 2),
|
||||
P.MakePair("a", 3), // Duplicate key
|
||||
}
|
||||
reducer := A.Reduce[Entry[string, int], map[string]int]
|
||||
from := FromFoldable(
|
||||
Mg.Second[int](),
|
||||
reducer,
|
||||
)
|
||||
result := from(entries)
|
||||
assert.Equal(t, map[string]int{"a": 3, "b": 2}, result)
|
||||
}
|
||||
|
||||
@@ -17,17 +17,98 @@ package record
|
||||
|
||||
import (
|
||||
G "github.com/IBM/fp-go/v2/record/generic"
|
||||
S "github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
func UnionSemigroup[K comparable, V any](s S.Semigroup[V]) S.Semigroup[map[K]V] {
|
||||
return G.UnionSemigroup[map[K]V](s)
|
||||
// UnionSemigroup creates a semigroup for maps that combines two maps using the provided
|
||||
// semigroup for resolving conflicts when the same key exists in both maps.
|
||||
//
|
||||
// When concatenating two maps:
|
||||
// - Keys that exist in only one map are included in the result
|
||||
// - Keys that exist in both maps have their values combined using the provided semigroup
|
||||
//
|
||||
// This is useful when you want custom conflict resolution logic beyond simple "first wins"
|
||||
// or "last wins" semantics.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a semigroup that sums values for duplicate keys
|
||||
// sumSemigroup := number.SemigroupSum[int]()
|
||||
// mapSemigroup := UnionSemigroup[string, int](sumSemigroup)
|
||||
//
|
||||
// map1 := map[string]int{"a": 1, "b": 2}
|
||||
// map2 := map[string]int{"b": 3, "c": 4}
|
||||
// result := mapSemigroup.Concat(map1, map2)
|
||||
// // result: {"a": 1, "b": 5, "c": 4} // b values are summed: 2 + 3 = 5
|
||||
//
|
||||
// Example with string concatenation:
|
||||
//
|
||||
// stringSemigroup := string.Semigroup
|
||||
// mapSemigroup := UnionSemigroup[string, string](stringSemigroup)
|
||||
//
|
||||
// map1 := map[string]string{"a": "Hello", "b": "World"}
|
||||
// map2 := map[string]string{"b": "!", "c": "Goodbye"}
|
||||
// result := mapSemigroup.Concat(map1, map2)
|
||||
// // result: {"a": "Hello", "b": "World!", "c": "Goodbye"}
|
||||
//
|
||||
//go:inline
|
||||
func UnionSemigroup[K comparable, V any](s Semigroup[V]) Semigroup[Record[K, V]] {
|
||||
return G.UnionSemigroup[Record[K, V]](s)
|
||||
}
|
||||
|
||||
func UnionLastSemigroup[K comparable, V any]() S.Semigroup[map[K]V] {
|
||||
return G.UnionLastSemigroup[map[K]V]()
|
||||
// UnionLastSemigroup creates a semigroup for maps where the last (right) value wins
|
||||
// when the same key exists in both maps being concatenated.
|
||||
//
|
||||
// This is the most common conflict resolution strategy and is equivalent to using
|
||||
// the standard map merge operation where right-side values take precedence.
|
||||
//
|
||||
// When concatenating two maps:
|
||||
// - Keys that exist in only one map are included in the result
|
||||
// - Keys that exist in both maps take the value from the second (right) map
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// semigroup := UnionLastSemigroup[string, int]()
|
||||
//
|
||||
// map1 := map[string]int{"a": 1, "b": 2}
|
||||
// map2 := map[string]int{"b": 3, "c": 4}
|
||||
// result := semigroup.Concat(map1, map2)
|
||||
// // result: {"a": 1, "b": 3, "c": 4} // b takes value from map2 (last wins)
|
||||
//
|
||||
// This is useful for:
|
||||
// - Configuration overrides (later configs override earlier ones)
|
||||
// - Applying updates to a base map
|
||||
// - Merging user preferences where newer values should win
|
||||
//
|
||||
//go:inline
|
||||
func UnionLastSemigroup[K comparable, V any]() Semigroup[Record[K, V]] {
|
||||
return G.UnionLastSemigroup[Record[K, V]]()
|
||||
}
|
||||
|
||||
func UnionFirstSemigroup[K comparable, V any]() S.Semigroup[map[K]V] {
|
||||
return G.UnionFirstSemigroup[map[K]V]()
|
||||
// UnionFirstSemigroup creates a semigroup for maps where the first (left) value wins
|
||||
// when the same key exists in both maps being concatenated.
|
||||
//
|
||||
// This is useful when you want to preserve original values and ignore updates for
|
||||
// keys that already exist.
|
||||
//
|
||||
// When concatenating two maps:
|
||||
// - Keys that exist in only one map are included in the result
|
||||
// - Keys that exist in both maps keep the value from the first (left) map
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// semigroup := UnionFirstSemigroup[string, int]()
|
||||
//
|
||||
// map1 := map[string]int{"a": 1, "b": 2}
|
||||
// map2 := map[string]int{"b": 3, "c": 4}
|
||||
// result := semigroup.Concat(map1, map2)
|
||||
// // result: {"a": 1, "b": 2, "c": 4} // b keeps value from map1 (first wins)
|
||||
//
|
||||
// This is useful for:
|
||||
// - Default values (defaults are set first, user values don't override)
|
||||
// - Caching (first cached value is kept, subsequent updates ignored)
|
||||
// - Immutable registries (first registration wins, duplicates are ignored)
|
||||
//
|
||||
//go:inline
|
||||
func UnionFirstSemigroup[K comparable, V any]() Semigroup[Record[K, V]] {
|
||||
return G.UnionFirstSemigroup[Record[K, V]]()
|
||||
}
|
||||
|
||||
227
v2/record/semigroup_test.go
Normal file
227
v2/record/semigroup_test.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// 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 record
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
S "github.com/IBM/fp-go/v2/string"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUnionSemigroup(t *testing.T) {
|
||||
// Test with sum semigroup - values should be added for duplicate keys
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
mapSemigroup := UnionSemigroup[string](sumSemigroup)
|
||||
|
||||
map1 := map[string]int{"a": 1, "b": 2}
|
||||
map2 := map[string]int{"b": 3, "c": 4}
|
||||
result := mapSemigroup.Concat(map1, map2)
|
||||
|
||||
expected := map[string]int{"a": 1, "b": 5, "c": 4}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnionSemigroupString(t *testing.T) {
|
||||
// Test with string semigroup - strings should be concatenated
|
||||
stringSemigroup := S.Semigroup
|
||||
mapSemigroup := UnionSemigroup[string](stringSemigroup)
|
||||
|
||||
map1 := map[string]string{"a": "Hello", "b": "World"}
|
||||
map2 := map[string]string{"b": "!", "c": "Goodbye"}
|
||||
result := mapSemigroup.Concat(map1, map2)
|
||||
|
||||
expected := map[string]string{"a": "Hello", "b": "World!", "c": "Goodbye"}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnionSemigroupProduct(t *testing.T) {
|
||||
// Test with product semigroup - values should be multiplied
|
||||
prodSemigroup := N.SemigroupProduct[int]()
|
||||
mapSemigroup := UnionSemigroup[string](prodSemigroup)
|
||||
|
||||
map1 := map[string]int{"a": 2, "b": 3}
|
||||
map2 := map[string]int{"b": 4, "c": 5}
|
||||
result := mapSemigroup.Concat(map1, map2)
|
||||
|
||||
expected := map[string]int{"a": 2, "b": 12, "c": 5}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnionSemigroupEmpty(t *testing.T) {
|
||||
// Test with empty maps
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
mapSemigroup := UnionSemigroup[string](sumSemigroup)
|
||||
|
||||
map1 := map[string]int{"a": 1}
|
||||
empty := map[string]int{}
|
||||
|
||||
result1 := mapSemigroup.Concat(map1, empty)
|
||||
assert.Equal(t, map1, result1)
|
||||
|
||||
result2 := mapSemigroup.Concat(empty, map1)
|
||||
assert.Equal(t, map1, result2)
|
||||
}
|
||||
|
||||
func TestUnionLastSemigroup(t *testing.T) {
|
||||
// Test that last (right) value wins for duplicate keys
|
||||
semigroup := UnionLastSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1, "b": 2}
|
||||
map2 := map[string]int{"b": 3, "c": 4}
|
||||
result := semigroup.Concat(map1, map2)
|
||||
|
||||
expected := map[string]int{"a": 1, "b": 3, "c": 4}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnionLastSemigroupNoOverlap(t *testing.T) {
|
||||
// Test with no overlapping keys
|
||||
semigroup := UnionLastSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1, "b": 2}
|
||||
map2 := map[string]int{"c": 3, "d": 4}
|
||||
result := semigroup.Concat(map1, map2)
|
||||
|
||||
expected := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnionLastSemigroupAllOverlap(t *testing.T) {
|
||||
// Test with all keys overlapping
|
||||
semigroup := UnionLastSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1, "b": 2}
|
||||
map2 := map[string]int{"a": 10, "b": 20}
|
||||
result := semigroup.Concat(map1, map2)
|
||||
|
||||
expected := map[string]int{"a": 10, "b": 20}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnionLastSemigroupEmpty(t *testing.T) {
|
||||
// Test with empty maps
|
||||
semigroup := UnionLastSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1}
|
||||
empty := map[string]int{}
|
||||
|
||||
result1 := semigroup.Concat(map1, empty)
|
||||
assert.Equal(t, map1, result1)
|
||||
|
||||
result2 := semigroup.Concat(empty, map1)
|
||||
assert.Equal(t, map1, result2)
|
||||
}
|
||||
|
||||
func TestUnionFirstSemigroup(t *testing.T) {
|
||||
// Test that first (left) value wins for duplicate keys
|
||||
semigroup := UnionFirstSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1, "b": 2}
|
||||
map2 := map[string]int{"b": 3, "c": 4}
|
||||
result := semigroup.Concat(map1, map2)
|
||||
|
||||
expected := map[string]int{"a": 1, "b": 2, "c": 4}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnionFirstSemigroupNoOverlap(t *testing.T) {
|
||||
// Test with no overlapping keys
|
||||
semigroup := UnionFirstSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1, "b": 2}
|
||||
map2 := map[string]int{"c": 3, "d": 4}
|
||||
result := semigroup.Concat(map1, map2)
|
||||
|
||||
expected := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnionFirstSemigroupAllOverlap(t *testing.T) {
|
||||
// Test with all keys overlapping
|
||||
semigroup := UnionFirstSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1, "b": 2}
|
||||
map2 := map[string]int{"a": 10, "b": 20}
|
||||
result := semigroup.Concat(map1, map2)
|
||||
|
||||
expected := map[string]int{"a": 1, "b": 2}
|
||||
assert.Equal(t, expected, result)
|
||||
}
|
||||
|
||||
func TestUnionFirstSemigroupEmpty(t *testing.T) {
|
||||
// Test with empty maps
|
||||
semigroup := UnionFirstSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1}
|
||||
empty := map[string]int{}
|
||||
|
||||
result1 := semigroup.Concat(map1, empty)
|
||||
assert.Equal(t, map1, result1)
|
||||
|
||||
result2 := semigroup.Concat(empty, map1)
|
||||
assert.Equal(t, map1, result2)
|
||||
}
|
||||
|
||||
// Test associativity law for UnionSemigroup
|
||||
func TestUnionSemigroupAssociativity(t *testing.T) {
|
||||
sumSemigroup := N.SemigroupSum[int]()
|
||||
mapSemigroup := UnionSemigroup[string](sumSemigroup)
|
||||
|
||||
map1 := map[string]int{"a": 1}
|
||||
map2 := map[string]int{"a": 2, "b": 3}
|
||||
map3 := map[string]int{"b": 4, "c": 5}
|
||||
|
||||
// (map1 + map2) + map3
|
||||
left := mapSemigroup.Concat(mapSemigroup.Concat(map1, map2), map3)
|
||||
// map1 + (map2 + map3)
|
||||
right := mapSemigroup.Concat(map1, mapSemigroup.Concat(map2, map3))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
}
|
||||
|
||||
// Test associativity law for UnionLastSemigroup
|
||||
func TestUnionLastSemigroupAssociativity(t *testing.T) {
|
||||
semigroup := UnionLastSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1}
|
||||
map2 := map[string]int{"a": 2, "b": 3}
|
||||
map3 := map[string]int{"b": 4, "c": 5}
|
||||
|
||||
// (map1 + map2) + map3
|
||||
left := semigroup.Concat(semigroup.Concat(map1, map2), map3)
|
||||
// map1 + (map2 + map3)
|
||||
right := semigroup.Concat(map1, semigroup.Concat(map2, map3))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
}
|
||||
|
||||
// Test associativity law for UnionFirstSemigroup
|
||||
func TestUnionFirstSemigroupAssociativity(t *testing.T) {
|
||||
semigroup := UnionFirstSemigroup[string, int]()
|
||||
|
||||
map1 := map[string]int{"a": 1}
|
||||
map2 := map[string]int{"a": 2, "b": 3}
|
||||
map3 := map[string]int{"b": 4, "c": 5}
|
||||
|
||||
// (map1 + map2) + map3
|
||||
left := semigroup.Concat(semigroup.Concat(map1, map2), map3)
|
||||
// map1 + (map2 + map3)
|
||||
right := semigroup.Concat(map1, semigroup.Concat(map2, map3))
|
||||
|
||||
assert.Equal(t, left, right)
|
||||
}
|
||||
@@ -19,6 +19,36 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/internal/record"
|
||||
)
|
||||
|
||||
// TraverseWithIndex transforms a map of values into a value of a map by applying an effectful function
|
||||
// to each key-value pair. The function has access to both the key and value.
|
||||
//
|
||||
// This is useful when you need to perform an operation that may fail or have side effects on each
|
||||
// element of a map, and you want to collect the results in the same applicative context.
|
||||
//
|
||||
// Type parameters:
|
||||
// - K: The key type (must be comparable)
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
// - HKTB: Higher-kinded type representing the effect containing B (e.g., Option[B], Either[E, B])
|
||||
// - HKTAB: Higher-kinded type representing a function from B to map[K]B in the effect
|
||||
// - HKTRB: Higher-kinded type representing the effect containing map[K]B
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a pure map[K]B into the effect (the "of" or "pure" function)
|
||||
// - fmap: Maps a function over the effect (the "map" or "fmap" function)
|
||||
// - fap: Applies an effectful function to an effectful value (the "ap" function)
|
||||
// - f: The transformation function that takes a key and value and returns an effect
|
||||
//
|
||||
// Example with Option:
|
||||
//
|
||||
// f := func(k string, n int) O.Option[int] {
|
||||
// if n > 0 {
|
||||
// return O.Some(n * 2)
|
||||
// }
|
||||
// return O.None[int]()
|
||||
// }
|
||||
// traverse := TraverseWithIndex(O.Of[map[string]int], O.Map[...], O.Ap[...], f)
|
||||
// result := traverse(map[string]int{"a": 1, "b": 2}) // O.Some(map[string]int{"a": 2, "b": 4})
|
||||
func TraverseWithIndex[K comparable, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(map[K]B) HKTRB,
|
||||
fmap func(func(map[K]B) func(B) map[K]B) func(HKTRB) HKTAB,
|
||||
@@ -28,10 +58,36 @@ func TraverseWithIndex[K comparable, A, B, HKTB, HKTAB, HKTRB any](
|
||||
return G.TraverseWithIndex[map[K]A](fof, fmap, fap, f)
|
||||
}
|
||||
|
||||
// HKTA = HKT<A>
|
||||
// HKTB = HKT<B>
|
||||
// HKTAB = HKT<func(A)B>
|
||||
// HKTRB = HKT<map[K]B>
|
||||
// Traverse transforms a map of values into a value of a map by applying an effectful function
|
||||
// to each value. Unlike TraverseWithIndex, this function does not provide access to the keys.
|
||||
//
|
||||
// This is useful when you need to perform an operation that may fail or have side effects on each
|
||||
// element of a map, and you want to collect the results in the same applicative context.
|
||||
//
|
||||
// Type parameters:
|
||||
// - K: The key type (must be comparable)
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
// - HKTB: Higher-kinded type representing the effect containing B (e.g., Option[B], Either[E, B])
|
||||
// - HKTAB: Higher-kinded type representing a function from B to map[K]B in the effect
|
||||
// - HKTRB: Higher-kinded type representing the effect containing map[K]B
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a pure map[K]B into the effect (the "of" or "pure" function)
|
||||
// - fmap: Maps a function over the effect (the "map" or "fmap" function)
|
||||
// - fap: Applies an effectful function to an effectful value (the "ap" function)
|
||||
// - f: The transformation function that takes a value and returns an effect
|
||||
//
|
||||
// Example with Option:
|
||||
//
|
||||
// f := func(s string) O.Option[string] {
|
||||
// if s != "" {
|
||||
// return O.Some(strings.ToUpper(s))
|
||||
// }
|
||||
// return O.None[string]()
|
||||
// }
|
||||
// traverse := Traverse(O.Of[map[string]string], O.Map[...], O.Ap[...], f)
|
||||
// result := traverse(map[string]string{"a": "hello"}) // O.Some(map[string]string{"a": "HELLO"})
|
||||
func Traverse[K comparable, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(map[K]B) HKTRB,
|
||||
fmap func(func(map[K]B) func(B) map[K]B) func(HKTRB) HKTAB,
|
||||
@@ -40,9 +96,39 @@ func Traverse[K comparable, A, B, HKTB, HKTAB, HKTRB any](
|
||||
return G.Traverse[map[K]A](fof, fmap, fap, f)
|
||||
}
|
||||
|
||||
// HKTA = HKT[A]
|
||||
// HKTAA = HKT[func(A)map[K]A]
|
||||
// HKTRA = HKT[map[K]A]
|
||||
// Sequence transforms a map of effects into an effect of a map.
|
||||
// This is the dual of Traverse where the transformation function is the identity.
|
||||
//
|
||||
// This is useful when you have a map where each value is already in an effect context
|
||||
// (like Option, Either, etc.) and you want to "flip" the nesting to get a single effect
|
||||
// containing a map of plain values.
|
||||
//
|
||||
// If any value in the map is a "failure" (e.g., None, Left), the entire result will be
|
||||
// a failure. If all values are "successes", the result will be a success containing a map
|
||||
// of all the unwrapped values.
|
||||
//
|
||||
// Type parameters:
|
||||
// - K: The key type (must be comparable)
|
||||
// - A: The value type inside the effect
|
||||
// - HKTA: Higher-kinded type representing the effect containing A (e.g., Option[A])
|
||||
// - HKTAA: Higher-kinded type representing a function from A to map[K]A in the effect
|
||||
// - HKTRA: Higher-kinded type representing the effect containing map[K]A
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Lifts a pure map[K]A into the effect (the "of" or "pure" function)
|
||||
// - fmap: Maps a function over the effect (the "map" or "fmap" function)
|
||||
// - fap: Applies an effectful function to an effectful value (the "ap" function)
|
||||
// - ma: The input map where each value is in an effect context
|
||||
//
|
||||
// Example with Option:
|
||||
//
|
||||
// input := map[string]O.Option[int]{"a": O.Some(1), "b": O.Some(2)}
|
||||
// result := Sequence(O.Of[map[string]int], O.Map[...], O.Ap[...], input)
|
||||
// // result: O.Some(map[string]int{"a": 1, "b": 2})
|
||||
//
|
||||
// input2 := map[string]O.Option[int]{"a": O.Some(1), "b": O.None[int]()}
|
||||
// result2 := Sequence(O.Of[map[string]int], O.Map[...], O.Ap[...], input2)
|
||||
// // result2: O.None[map[string]int]()
|
||||
func Sequence[K comparable, A, HKTA, HKTAA, HKTRA any](
|
||||
fof func(map[K]A) HKTRA,
|
||||
fmap func(func(map[K]A) func(A) map[K]A) func(HKTRA) HKTAA,
|
||||
|
||||
162
v2/record/types.go
Normal file
162
v2/record/types.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// 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 record
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
"github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
type (
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
Monoid[A any] = monoid.Monoid[A]
|
||||
Semigroup[A any] = semigroup.Semigroup[A]
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Record represents a map with comparable keys and values of any type.
|
||||
// This is the primary data structure for the record package, providing
|
||||
// functional operations over Go's native map type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type UserRecord = Record[string, User]
|
||||
// users := UserRecord{
|
||||
// "alice": User{Name: "Alice", Age: 30},
|
||||
// "bob": User{Name: "Bob", Age: 25},
|
||||
// }
|
||||
Record[K comparable, V any] = map[K]V
|
||||
|
||||
// Predicate is a function that tests whether a key satisfies a condition.
|
||||
// Used in filtering operations to determine which entries to keep.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isVowel := func(k string) bool {
|
||||
// return strings.ContainsAny(k, "aeiou")
|
||||
// }
|
||||
Predicate[K any] = predicate.Predicate[K]
|
||||
|
||||
// PredicateWithIndex is a function that tests whether a key-value pair satisfies a condition.
|
||||
// Used in filtering operations that need access to both key and value.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// isAdult := func(name string, user User) bool {
|
||||
// return user.Age >= 18
|
||||
// }
|
||||
PredicateWithIndex[K comparable, V any] = func(K, V) bool
|
||||
|
||||
// Operator transforms a record from one value type to another while preserving keys.
|
||||
// This is the fundamental transformation type for record operations.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// doubleValues := Map(func(x int) int { return x * 2 })
|
||||
// result := doubleValues(Record[string, int]{"a": 1, "b": 2})
|
||||
// // result: {"a": 2, "b": 4}
|
||||
Operator[K comparable, V1, V2 any] = func(Record[K, V1]) Record[K, V2]
|
||||
|
||||
// OperatorWithIndex transforms a record using both key and value information.
|
||||
// Useful when the transformation depends on the key.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// prefixWithKey := MapWithIndex(func(k string, v string) string {
|
||||
// return k + ":" + v
|
||||
// })
|
||||
OperatorWithIndex[K comparable, V1, V2 any] = func(func(K, V1) V2) Operator[K, V1, V2]
|
||||
|
||||
// Kleisli represents a monadic function that transforms a value into a record.
|
||||
// Used in chain operations for composing record-producing functions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// expand := func(x int) Record[string, int] {
|
||||
// return Record[string, int]{
|
||||
// "double": x * 2,
|
||||
// "triple": x * 3,
|
||||
// }
|
||||
// }
|
||||
Kleisli[K comparable, V1, V2 any] = func(V1) Record[K, V2]
|
||||
|
||||
// KleisliWithIndex is a monadic function that uses both key and value to produce a record.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// expandWithKey := func(k string, v int) Record[string, int] {
|
||||
// return Record[string, int]{
|
||||
// k + "_double": v * 2,
|
||||
// k + "_triple": v * 3,
|
||||
// }
|
||||
// }
|
||||
KleisliWithIndex[K comparable, V1, V2 any] = func(K, V1) Record[K, V2]
|
||||
|
||||
// Reducer accumulates values from a record into a single result.
|
||||
// The function receives the accumulator and current value, returning the new accumulator.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// sum := Reduce(func(acc int, v int) int {
|
||||
// return acc + v
|
||||
// }, 0)
|
||||
Reducer[V, R any] = func(R, V) R
|
||||
|
||||
// ReducerWithIndex accumulates values using both key and value information.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// weightedSum := ReduceWithIndex(func(k string, acc int, v int) int {
|
||||
// weight := len(k)
|
||||
// return acc + (v * weight)
|
||||
// }, 0)
|
||||
ReducerWithIndex[K comparable, V, R any] = func(K, R, V) R
|
||||
|
||||
// Collector transforms key-value pairs into a result type and collects them into an array.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// toStrings := Collect(func(k string, v int) string {
|
||||
// return fmt.Sprintf("%s=%d", k, v)
|
||||
// })
|
||||
Collector[K comparable, V, R any] = func(K, V) R
|
||||
|
||||
// Entry represents a single key-value pair from a record.
|
||||
// This is an alias for Tuple2 to provide semantic clarity.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// entries := ToEntries(record)
|
||||
// for _, entry := range entries {
|
||||
// key := entry.F1
|
||||
// value := entry.F2
|
||||
// }
|
||||
Entry[K comparable, V any] = pair.Pair[K, V]
|
||||
|
||||
// Entries is a slice of key-value pairs.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// entries := Entries[string, int]{
|
||||
// T.MakeTuple2("a", 1),
|
||||
// T.MakeTuple2("b", 2),
|
||||
// }
|
||||
// record := FromEntries(entries)
|
||||
Entries[K comparable, V any] = []Entry[K, V]
|
||||
)
|
||||
@@ -15,9 +15,12 @@
|
||||
|
||||
package result
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
|
||||
func TailRec[A, B any](f Kleisli[A, tailrec.Trampoline[A, B]]) Kleisli[A, B] {
|
||||
return either.TailRec(f)
|
||||
}
|
||||
|
||||
214
v2/tailrec/doc.go
Normal file
214
v2/tailrec/doc.go
Normal file
@@ -0,0 +1,214 @@
|
||||
// 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 tailrec provides a trampoline implementation for tail-call optimization in Go.
|
||||
//
|
||||
// # Overview
|
||||
//
|
||||
// Go does not support tail-call optimization (TCO) at the language level, which means
|
||||
// deeply recursive functions can cause stack overflow errors. The trampoline pattern
|
||||
// provides a way to convert recursive algorithms into iterative ones, avoiding stack
|
||||
// overflow while maintaining the clarity of recursive code.
|
||||
//
|
||||
// A trampoline works by returning instructions about what to do next instead of
|
||||
// directly making recursive calls. The trampoline executor then interprets these
|
||||
// instructions in a loop, effectively converting recursion into iteration.
|
||||
//
|
||||
// # Core Concepts
|
||||
//
|
||||
// The package provides three main operations:
|
||||
//
|
||||
// **Bounce**: Indicates that the computation should continue with a new value.
|
||||
// This represents a recursive call in the original algorithm.
|
||||
//
|
||||
// **Land**: Indicates that the computation is complete and returns a final result.
|
||||
// This represents the base case in the original algorithm.
|
||||
//
|
||||
// **Unwrap**: Extracts the state from a Trampoline, allowing the executor to
|
||||
// determine whether to continue (Bounce) or terminate (Land).
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// The Trampoline type has two type parameters:
|
||||
//
|
||||
// - B: The "bounce" type - the intermediate state passed between recursive steps
|
||||
// - L: The "land" type - the final result type when computation completes
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Converting a recursive factorial function to use trampolines:
|
||||
//
|
||||
// // Traditional recursive factorial (can overflow stack)
|
||||
// func factorial(n int) int {
|
||||
// if n <= 1 {
|
||||
// return 1
|
||||
// }
|
||||
// return n * factorial(n-1)
|
||||
// }
|
||||
//
|
||||
// // Trampoline-based factorial (stack-safe)
|
||||
// type State struct {
|
||||
// n int
|
||||
// acc int
|
||||
// }
|
||||
//
|
||||
// func factorialStep(state State) tailrec.Trampoline[State, int] {
|
||||
// if state.n <= 1 {
|
||||
// return tailrec.Land[State](state.acc)
|
||||
// }
|
||||
// return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n})
|
||||
// }
|
||||
//
|
||||
// // Execute the trampoline
|
||||
// func factorial(n int) int {
|
||||
// current := tailrec.Bounce[int](State{n, 1})
|
||||
// for {
|
||||
// bounce, land, isLand := tailrec.Unwrap(current)
|
||||
// if isLand {
|
||||
// return land
|
||||
// }
|
||||
// current = factorialStep(bounce)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # Fibonacci Example
|
||||
//
|
||||
// Computing Fibonacci numbers with tail recursion:
|
||||
//
|
||||
// type FibState struct {
|
||||
// n int
|
||||
// curr int
|
||||
// prev int
|
||||
// }
|
||||
//
|
||||
// func fibStep(state FibState) tailrec.Trampoline[FibState, int] {
|
||||
// if state.n <= 0 {
|
||||
// return tailrec.Land[FibState](state.curr)
|
||||
// }
|
||||
// return tailrec.Bounce[int](FibState{
|
||||
// n: state.n - 1,
|
||||
// curr: state.prev + state.curr,
|
||||
// prev: state.curr,
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func fibonacci(n int) int {
|
||||
// current := tailrec.Bounce[int](FibState{n, 1, 0})
|
||||
// for {
|
||||
// bounce, land, isLand := tailrec.Unwrap(current)
|
||||
// if isLand {
|
||||
// return land
|
||||
// }
|
||||
// current = fibStep(bounce)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # List Processing Example
|
||||
//
|
||||
// Summing a list with tail recursion:
|
||||
//
|
||||
// type SumState struct {
|
||||
// list []int
|
||||
// sum int
|
||||
// }
|
||||
//
|
||||
// func sumStep(state SumState) tailrec.Trampoline[SumState, int] {
|
||||
// if len(state.list) == 0 {
|
||||
// return tailrec.Land[SumState](state.sum)
|
||||
// }
|
||||
// return tailrec.Bounce[int](SumState{
|
||||
// list: state.list[1:],
|
||||
// sum: state.sum + state.list[0],
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// func sumList(list []int) int {
|
||||
// current := tailrec.Bounce[int](SumState{list, 0})
|
||||
// for {
|
||||
// bounce, land, isLand := tailrec.Unwrap(current)
|
||||
// if isLand {
|
||||
// return land
|
||||
// }
|
||||
// current = sumStep(bounce)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// # Integration with Reader Monads
|
||||
//
|
||||
// The tailrec package is commonly used with Reader monads (readerio, context/readerio)
|
||||
// to implement stack-safe recursive computations that also depend on an environment:
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/readerio"
|
||||
// "github.com/IBM/fp-go/v2/tailrec"
|
||||
// )
|
||||
//
|
||||
// type Env struct {
|
||||
// Multiplier int
|
||||
// }
|
||||
//
|
||||
// func compute(n int) readerio.ReaderIO[Env, int] {
|
||||
// return readerio.TailRec(
|
||||
// n,
|
||||
// func(n int) readerio.ReaderIO[Env, tailrec.Trampoline[int, int]] {
|
||||
// return func(env Env) func() tailrec.Trampoline[int, int] {
|
||||
// return func() tailrec.Trampoline[int, int] {
|
||||
// if n <= 0 {
|
||||
// return tailrec.Land[int](n * env.Multiplier)
|
||||
// }
|
||||
// return tailrec.Bounce[int](n - 1)
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// # Benefits
|
||||
//
|
||||
// - **Stack Safety**: Prevents stack overflow for deep recursion
|
||||
// - **Clarity**: Maintains the structure of recursive algorithms
|
||||
// - **Performance**: Converts recursion to iteration without manual rewriting
|
||||
// - **Composability**: Works well with functional programming patterns
|
||||
//
|
||||
// # When to Use
|
||||
//
|
||||
// Use trampolines when:
|
||||
// - You have a naturally recursive algorithm
|
||||
// - The recursion depth could be large (thousands of calls)
|
||||
// - You want to maintain the clarity of recursive code
|
||||
// - You're working with functional programming patterns
|
||||
//
|
||||
// # Performance Considerations
|
||||
//
|
||||
// While trampolines prevent stack overflow, they do have some overhead:
|
||||
// - Each step allocates a Trampoline struct
|
||||
// - The executor loop adds some indirection
|
||||
//
|
||||
// For shallow recursion (< 1000 calls), direct recursion may be faster.
|
||||
// For deep recursion, trampolines are essential to avoid stack overflow.
|
||||
//
|
||||
// # Key Functions
|
||||
//
|
||||
// **Bounce**: Create a trampoline that continues computation with a new state
|
||||
//
|
||||
// **Land**: Create a trampoline that terminates with a final result
|
||||
//
|
||||
// **Unwrap**: Extract the state and determine if computation should continue
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - readerio.TailRec: Tail-recursive Reader IO computations
|
||||
// - context/readerio.TailRec: Tail-recursive Reader IO with context
|
||||
package tailrec
|
||||
303
v2/tailrec/example_test.go
Normal file
303
v2/tailrec/example_test.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// 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 tailrec_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/IBM/fp-go/v2/tailrec"
|
||||
)
|
||||
|
||||
// ExampleBounce demonstrates creating a trampoline that continues computation.
|
||||
func ExampleBounce() {
|
||||
// Create a bounce trampoline with value 42
|
||||
tramp := tailrec.Bounce[string](42)
|
||||
|
||||
// Access fields directly to inspect the state
|
||||
fmt.Printf("Is computation complete? %v\n", tramp.Landed)
|
||||
fmt.Printf("Next value to process: %d\n", tramp.Bounce)
|
||||
|
||||
// Output:
|
||||
// Is computation complete? false
|
||||
// Next value to process: 42
|
||||
}
|
||||
|
||||
// ExampleLand demonstrates creating a trampoline that completes computation.
|
||||
func ExampleLand() {
|
||||
// Create a land trampoline with final result
|
||||
tramp := tailrec.Land[int]("done")
|
||||
|
||||
// Access fields directly to inspect the state
|
||||
fmt.Printf("Is computation complete? %v\n", tramp.Landed)
|
||||
fmt.Printf("Final result: %s\n", tramp.Land)
|
||||
|
||||
// Output:
|
||||
// Is computation complete? true
|
||||
// Final result: done
|
||||
}
|
||||
|
||||
// Example_fieldAccess demonstrates accessing trampoline fields directly.
|
||||
func Example_fieldAccess() {
|
||||
// Create a bounce trampoline
|
||||
bounceTramp := tailrec.Bounce[string](42)
|
||||
fmt.Printf("Bounce: value=%d, landed=%v\n", bounceTramp.Bounce, bounceTramp.Landed)
|
||||
|
||||
// Create a land trampoline
|
||||
landTramp := tailrec.Land[int]("result")
|
||||
fmt.Printf("Land: value=%s, landed=%v\n", landTramp.Land, landTramp.Landed)
|
||||
|
||||
// Output:
|
||||
// Bounce: value=42, landed=false
|
||||
// Land: value=result, landed=true
|
||||
}
|
||||
|
||||
// Example_factorial demonstrates implementing factorial using trampolines.
|
||||
func Example_factorial() {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
// Define the step function
|
||||
factorialStep := func(state State) tailrec.Trampoline[State, int] {
|
||||
if state.n <= 1 {
|
||||
return tailrec.Land[State](state.acc)
|
||||
}
|
||||
return tailrec.Bounce[int](State{state.n - 1, state.acc * state.n})
|
||||
}
|
||||
|
||||
// Execute the trampoline
|
||||
factorial := func(n int) int {
|
||||
current := tailrec.Bounce[int](State{n, 1})
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = factorialStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate factorial of 5
|
||||
result := factorial(5)
|
||||
fmt.Printf("5! = %d\n", result)
|
||||
|
||||
// Output:
|
||||
// 5! = 120
|
||||
}
|
||||
|
||||
// Example_fibonacci demonstrates computing Fibonacci numbers using trampolines.
|
||||
func Example_fibonacci() {
|
||||
type State struct {
|
||||
n int
|
||||
curr int
|
||||
prev int
|
||||
}
|
||||
|
||||
// Define the step function
|
||||
fibStep := func(state State) tailrec.Trampoline[State, int] {
|
||||
if state.n <= 0 {
|
||||
return tailrec.Land[State](state.curr)
|
||||
}
|
||||
return tailrec.Bounce[int](State{
|
||||
n: state.n - 1,
|
||||
curr: state.prev + state.curr,
|
||||
prev: state.curr,
|
||||
})
|
||||
}
|
||||
|
||||
// Execute the trampoline
|
||||
fibonacci := func(n int) int {
|
||||
current := tailrec.Bounce[int](State{n, 1, 0})
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = fibStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate 10th Fibonacci number
|
||||
result := fibonacci(10)
|
||||
fmt.Printf("fib(10) = %d\n", result)
|
||||
|
||||
// Output:
|
||||
// fib(10) = 89
|
||||
}
|
||||
|
||||
// Example_sumList demonstrates processing a list using trampolines.
|
||||
func Example_sumList() {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
// Define the step function
|
||||
sumStep := func(state State) tailrec.Trampoline[State, int] {
|
||||
if len(state.list) == 0 {
|
||||
return tailrec.Land[State](state.sum)
|
||||
}
|
||||
return tailrec.Bounce[int](State{
|
||||
list: state.list[1:],
|
||||
sum: state.sum + state.list[0],
|
||||
})
|
||||
}
|
||||
|
||||
// Execute the trampoline
|
||||
sumList := func(list []int) int {
|
||||
current := tailrec.Bounce[int](State{list, 0})
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = sumStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
// Sum a list of numbers
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
result := sumList(numbers)
|
||||
fmt.Printf("sum([1,2,3,4,5]) = %d\n", result)
|
||||
|
||||
// Output:
|
||||
// sum([1,2,3,4,5]) = 15
|
||||
}
|
||||
|
||||
// Example_countdown demonstrates a simple countdown using trampolines.
|
||||
func Example_countdown() {
|
||||
// Define the step function
|
||||
countdownStep := func(n int) tailrec.Trampoline[int, int] {
|
||||
if n <= 0 {
|
||||
return tailrec.Land[int](0)
|
||||
}
|
||||
return tailrec.Bounce[int](n - 1)
|
||||
}
|
||||
|
||||
// Execute the trampoline
|
||||
countdown := func(n int) int {
|
||||
current := tailrec.Bounce[int](n)
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = countdownStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
// Countdown from 5
|
||||
result := countdown(5)
|
||||
fmt.Printf("countdown(5) = %d\n", result)
|
||||
|
||||
// Output:
|
||||
// countdown(5) = 0
|
||||
}
|
||||
|
||||
// Example_gcd demonstrates computing greatest common divisor using trampolines.
|
||||
func Example_gcd() {
|
||||
type State struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
|
||||
// Define the step function (Euclidean algorithm)
|
||||
gcdStep := func(state State) tailrec.Trampoline[State, int] {
|
||||
if state.b == 0 {
|
||||
return tailrec.Land[State](state.a)
|
||||
}
|
||||
return tailrec.Bounce[int](State{state.b, state.a % state.b})
|
||||
}
|
||||
|
||||
// Execute the trampoline
|
||||
gcd := func(a, b int) int {
|
||||
current := tailrec.Bounce[int](State{a, b})
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = gcdStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate GCD of 48 and 18
|
||||
result := gcd(48, 18)
|
||||
fmt.Printf("gcd(48, 18) = %d\n", result)
|
||||
|
||||
// Output:
|
||||
// gcd(48, 18) = 6
|
||||
}
|
||||
|
||||
// Example_deepRecursion demonstrates handling deep recursion without stack overflow.
|
||||
func Example_deepRecursion() {
|
||||
// Define the step function
|
||||
countdownStep := func(n int) tailrec.Trampoline[int, int] {
|
||||
if n <= 0 {
|
||||
return tailrec.Land[int](0)
|
||||
}
|
||||
return tailrec.Bounce[int](n - 1)
|
||||
}
|
||||
|
||||
// Execute the trampoline
|
||||
countdown := func(n int) int {
|
||||
current := tailrec.Bounce[int](n)
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = countdownStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
// This would cause stack overflow with regular recursion
|
||||
// but works fine with trampolines
|
||||
result := countdown(100000)
|
||||
fmt.Printf("countdown(100000) = %d (no stack overflow!)\n", result)
|
||||
|
||||
// Output:
|
||||
// countdown(100000) = 0 (no stack overflow!)
|
||||
}
|
||||
|
||||
// Example_collatz demonstrates the Collatz conjecture using trampolines.
|
||||
func Example_collatz() {
|
||||
// Define the step function
|
||||
collatzStep := func(n int) tailrec.Trampoline[int, int] {
|
||||
if n <= 1 {
|
||||
return tailrec.Land[int](n)
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return tailrec.Bounce[int](n / 2)
|
||||
}
|
||||
return tailrec.Bounce[int](3*n + 1)
|
||||
}
|
||||
|
||||
// Execute the trampoline and count steps
|
||||
collatzSteps := func(n int) int {
|
||||
current := tailrec.Bounce[int](n)
|
||||
steps := 0
|
||||
for {
|
||||
if current.Landed {
|
||||
return steps
|
||||
}
|
||||
current = collatzStep(current.Bounce)
|
||||
steps++
|
||||
}
|
||||
}
|
||||
|
||||
// Count steps for Collatz sequence starting at 27
|
||||
result := collatzSteps(27)
|
||||
fmt.Printf("Collatz(27) takes %d steps to reach 1\n", result)
|
||||
|
||||
// Output:
|
||||
// Collatz(27) takes 112 steps to reach 1
|
||||
}
|
||||
97
v2/tailrec/format_test.go
Normal file
97
v2/tailrec/format_test.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package tailrec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestStringerInterface verifies fmt.Stringer implementation
|
||||
func TestStringerInterface(t *testing.T) {
|
||||
t.Run("Bounce String", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
result := tramp.String()
|
||||
assert.Equal(t, "Bounce(42)", result)
|
||||
})
|
||||
|
||||
t.Run("Land String", func(t *testing.T) {
|
||||
tramp := Land[int]("done")
|
||||
result := tramp.String()
|
||||
assert.Equal(t, "Land(done)", result)
|
||||
})
|
||||
|
||||
t.Run("fmt.Sprint uses String", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
result := fmt.Sprint(tramp)
|
||||
assert.Equal(t, "Bounce(42)", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFormatterInterface verifies fmt.Formatter implementation
|
||||
func TestFormatterInterface(t *testing.T) {
|
||||
t.Run("default %v format", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
result := fmt.Sprintf("%v", tramp)
|
||||
assert.Equal(t, "Bounce(42)", result)
|
||||
})
|
||||
|
||||
t.Run("detailed %+v format for Bounce", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
result := fmt.Sprintf("%+v", tramp)
|
||||
assert.Contains(t, result, "Trampoline[Bounce]")
|
||||
assert.Contains(t, result, "Bounce: 42")
|
||||
assert.Contains(t, result, "Landed: false")
|
||||
})
|
||||
|
||||
t.Run("detailed %+v format for Land", func(t *testing.T) {
|
||||
tramp := Land[int]("done")
|
||||
result := fmt.Sprintf("%+v", tramp)
|
||||
assert.Contains(t, result, "Trampoline[Land]")
|
||||
assert.Contains(t, result, "Land: done")
|
||||
assert.Contains(t, result, "Landed: true")
|
||||
})
|
||||
|
||||
t.Run("%#v format delegates to GoString", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
result := fmt.Sprintf("%#v", tramp)
|
||||
assert.Contains(t, result, "tailrec.Bounce")
|
||||
})
|
||||
|
||||
t.Run("%s format", func(t *testing.T) {
|
||||
tramp := Land[int]("result")
|
||||
result := fmt.Sprintf("%s", tramp)
|
||||
assert.Equal(t, "Land(result)", result)
|
||||
})
|
||||
|
||||
t.Run("%q format", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
result := fmt.Sprintf("%q", tramp)
|
||||
assert.Equal(t, "\"Bounce(42)\"", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestGoStringerInterface verifies fmt.GoStringer implementation
|
||||
func TestGoStringerInterface(t *testing.T) {
|
||||
t.Run("Bounce GoString", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
result := tramp.GoString()
|
||||
assert.Contains(t, result, "tailrec.Bounce")
|
||||
assert.Contains(t, result, "string")
|
||||
assert.Contains(t, result, "42")
|
||||
})
|
||||
|
||||
t.Run("Land GoString", func(t *testing.T) {
|
||||
tramp := Land[int]("done")
|
||||
result := tramp.GoString()
|
||||
assert.Contains(t, result, "tailrec.Land")
|
||||
assert.Contains(t, result, "int")
|
||||
assert.Contains(t, result, "done")
|
||||
})
|
||||
|
||||
t.Run("fmt with %#v uses GoString", func(t *testing.T) {
|
||||
tramp := Land[int]("result")
|
||||
result := fmt.Sprintf("%#v", tramp)
|
||||
assert.Contains(t, result, "tailrec.Land")
|
||||
})
|
||||
}
|
||||
53
v2/tailrec/logger.go
Normal file
53
v2/tailrec/logger.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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 tailrec
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
// 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:
|
||||
// - 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:
|
||||
//
|
||||
// trampoline := tailrec.Bounce[int](42)
|
||||
// slog.Info("Processing", "state", trampoline)
|
||||
// // Logs: {"level":"info","msg":"Processing","state":{"bouncing":42}}
|
||||
//
|
||||
// result := tailrec.Land[int](100)
|
||||
// slog.Info("Complete", "result", result)
|
||||
// // Logs: {"level":"info","msg":"Complete","result":{"landed":100}}
|
||||
//
|
||||
// This is particularly useful for debugging tail-recursive computations and
|
||||
// understanding the flow of recursive algorithms at runtime.
|
||||
func (t Trampoline[B, L]) LogValue() slog.Value {
|
||||
if t.Landed {
|
||||
return slog.GroupValue(
|
||||
slog.Any("landed", t.Land),
|
||||
)
|
||||
}
|
||||
return slog.GroupValue(
|
||||
slog.Any("bouncing", t.Bounce),
|
||||
)
|
||||
}
|
||||
119
v2/tailrec/logger_test.go
Normal file
119
v2/tailrec/logger_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// 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 tailrec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTrampolineLogValue(t *testing.T) {
|
||||
t.Run("Bounce state logs correctly", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, nil))
|
||||
|
||||
bounce := Bounce[int](42)
|
||||
logger.Info("test", "trampoline", bounce)
|
||||
|
||||
var logEntry map[string]any
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the trampoline field
|
||||
trampolineField, ok := logEntry["trampoline"].(map[string]any)
|
||||
require.True(t, ok, "trampoline field should be a map")
|
||||
|
||||
// When Landed is false, only the "bouncing" field is present with the value
|
||||
assert.Equal(t, float64(42), trampolineField["bouncing"]) // JSON numbers are float64
|
||||
_, hasLanded := trampolineField["landed"]
|
||||
assert.False(t, hasLanded, "landed field should not be present when bouncing")
|
||||
})
|
||||
|
||||
t.Run("Land state logs correctly", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, nil))
|
||||
|
||||
land := Land[int](100)
|
||||
logger.Info("test", "trampoline", land)
|
||||
|
||||
var logEntry map[string]any
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the trampoline field
|
||||
trampolineField, ok := logEntry["trampoline"].(map[string]any)
|
||||
require.True(t, ok, "trampoline field should be a map")
|
||||
|
||||
// When Landed is true, only the "landed" field is present with the value
|
||||
assert.Equal(t, float64(100), trampolineField["landed"]) // JSON numbers are float64
|
||||
_, hasValue := trampolineField["value"]
|
||||
assert.False(t, hasValue, "value field should not be present when landed")
|
||||
})
|
||||
|
||||
t.Run("Complex type in Bounce", func(t *testing.T) {
|
||||
type State struct {
|
||||
N int
|
||||
Acc int
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, nil))
|
||||
|
||||
bounce := Bounce[int](State{N: 5, Acc: 120})
|
||||
logger.Info("test", "state", bounce)
|
||||
|
||||
var logEntry map[string]any
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
require.NoError(t, err)
|
||||
|
||||
stateField, ok := logEntry["state"].(map[string]any)
|
||||
require.True(t, ok, "state field should be a map")
|
||||
|
||||
// When Landed is false, only the "bouncing" field is present
|
||||
bouncing, ok := stateField["bouncing"].(map[string]any)
|
||||
require.True(t, ok, "bouncing should be a map")
|
||||
assert.Equal(t, float64(5), bouncing["N"])
|
||||
assert.Equal(t, float64(120), bouncing["Acc"])
|
||||
|
||||
_, hasLanded := stateField["landed"]
|
||||
assert.False(t, hasLanded, "landed field should not be present when bouncing")
|
||||
})
|
||||
|
||||
t.Run("String type in Land", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewJSONHandler(&buf, nil))
|
||||
|
||||
land := Land[int]("completed")
|
||||
logger.Info("test", "result", land)
|
||||
|
||||
var logEntry map[string]any
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
require.NoError(t, err)
|
||||
|
||||
resultField, ok := logEntry["result"].(map[string]any)
|
||||
require.True(t, ok, "result field should be a map")
|
||||
|
||||
// When Landed is true, only the "landed" field is present with the value
|
||||
assert.Equal(t, "completed", resultField["landed"])
|
||||
_, hasValue := resultField["value"]
|
||||
assert.False(t, hasValue, "value field should not be present when landed")
|
||||
})
|
||||
}
|
||||
115
v2/tailrec/trampoline.go
Normal file
115
v2/tailrec/trampoline.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package tailrec
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Bounce creates a Trampoline that indicates the computation should continue
|
||||
// with a new intermediate state.
|
||||
//
|
||||
// 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)
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The new intermediate state to process in the next step
|
||||
//
|
||||
// Returns:
|
||||
// - A Trampoline in the "bounce" state containing the intermediate value
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Countdown that bounces until reaching zero
|
||||
// func countdownStep(n int) Trampoline[int, int] {
|
||||
// if n <= 0 {
|
||||
// return Land[int](0)
|
||||
// }
|
||||
// return Bounce[int](n - 1) // Continue with n-1
|
||||
// }
|
||||
//
|
||||
//go:inline
|
||||
func Bounce[L, B any](b B) Trampoline[B, L] {
|
||||
return Trampoline[B, L]{Bounce: b, Landed: false}
|
||||
}
|
||||
|
||||
// Land creates a Trampoline that indicates the computation is complete
|
||||
// with a final result.
|
||||
//
|
||||
// This represents the base case in the original recursive algorithm. When
|
||||
// a Land trampoline is encountered, the executor should stop iterating and
|
||||
// return the final result.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - B: The intermediate state type (bounce type)
|
||||
// - L: The final result type (land type)
|
||||
//
|
||||
// Parameters:
|
||||
// - l: The final result value
|
||||
//
|
||||
// Returns:
|
||||
// - A Trampoline in the "land" state containing the final result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Factorial base case
|
||||
// func factorialStep(state State) Trampoline[State, int] {
|
||||
// if state.n <= 1 {
|
||||
// return Land[State](state.acc) // Computation complete
|
||||
// }
|
||||
// return Bounce[int](State{state.n - 1, state.acc * state.n})
|
||||
// }
|
||||
//
|
||||
//go:inline
|
||||
func Land[B, L any](l L) Trampoline[B, L] {
|
||||
return Trampoline[B, L]{Land: l, Landed: true}
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer for Trampoline.
|
||||
// Returns a human-readable string representation of the trampoline state.
|
||||
func (t Trampoline[B, L]) String() string {
|
||||
if t.Landed {
|
||||
return fmt.Sprintf("Land(%v)", t.Land)
|
||||
}
|
||||
return fmt.Sprintf("Bounce(%v)", t.Bounce)
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Trampoline.
|
||||
// Supports various formatting verbs for detailed output.
|
||||
func (t Trampoline[B, L]) Format(f fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
if f.Flag('+') {
|
||||
// %+v: detailed format with type information
|
||||
if t.Landed {
|
||||
fmt.Fprintf(f, "Trampoline[Land]{Land: %+v, Landed: true}", t.Land)
|
||||
} else {
|
||||
fmt.Fprintf(f, "Trampoline[Bounce]{Bounce: %+v, Landed: false}", t.Bounce)
|
||||
}
|
||||
} else if f.Flag('#') {
|
||||
// %#v: Go-syntax representation (delegates to GoString)
|
||||
fmt.Fprint(f, t.GoString())
|
||||
} else {
|
||||
// %v: default format (delegates to String)
|
||||
fmt.Fprint(f, t.String())
|
||||
}
|
||||
case 's':
|
||||
// %s: string format
|
||||
fmt.Fprint(f, t.String())
|
||||
case 'q':
|
||||
// %q: quoted string format
|
||||
fmt.Fprintf(f, "%q", t.String())
|
||||
default:
|
||||
// Unknown verb: print with %!verb notation
|
||||
fmt.Fprintf(f, "%%!%c(Trampoline[B, L]=%s)", verb, t.String())
|
||||
}
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Trampoline.
|
||||
// Returns a Go-syntax representation that could be used to recreate the value.
|
||||
func (t Trampoline[B, L]) GoString() string {
|
||||
if t.Landed {
|
||||
return fmt.Sprintf("tailrec.Land[%T](%#v)", t.Bounce, t.Land)
|
||||
}
|
||||
return fmt.Sprintf("tailrec.Bounce[%T](%#v)", t.Land, t.Bounce)
|
||||
}
|
||||
382
v2/tailrec/trampoline_test.go
Normal file
382
v2/tailrec/trampoline_test.go
Normal file
@@ -0,0 +1,382 @@
|
||||
// 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 tailrec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestBounce verifies that Bounce creates a trampoline in the bounce state
|
||||
func TestBounce(t *testing.T) {
|
||||
t.Run("creates bounce state with integer", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
|
||||
assert.False(t, tramp.Landed, "should be in bounce state")
|
||||
assert.Equal(t, 42, tramp.Bounce, "bounce value should match")
|
||||
assert.Equal(t, "", tramp.Land, "land value should be zero value")
|
||||
})
|
||||
|
||||
t.Run("creates bounce state with string", func(t *testing.T) {
|
||||
tramp := Bounce[int]("hello")
|
||||
|
||||
assert.False(t, tramp.Landed, "should be in bounce state")
|
||||
assert.Equal(t, "hello", tramp.Bounce, "bounce value should match")
|
||||
assert.Equal(t, 0, tramp.Land, "land value should be zero value")
|
||||
})
|
||||
|
||||
t.Run("creates bounce state with struct", func(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
state := State{n: 5, acc: 10}
|
||||
tramp := Bounce[int](state)
|
||||
|
||||
assert.False(t, tramp.Landed, "should be in bounce state")
|
||||
assert.Equal(t, state, tramp.Bounce, "bounce value should match")
|
||||
assert.Equal(t, 0, tramp.Land, "land value should be zero value")
|
||||
})
|
||||
}
|
||||
|
||||
// TestLand verifies that Land creates a trampoline in the land state
|
||||
func TestLand(t *testing.T) {
|
||||
t.Run("creates land state with integer", func(t *testing.T) {
|
||||
tramp := Land[string](42)
|
||||
|
||||
assert.True(t, tramp.Landed, "should be in land state")
|
||||
assert.Equal(t, 42, tramp.Land, "land value should match")
|
||||
assert.Equal(t, "", tramp.Bounce, "bounce value should be zero value")
|
||||
})
|
||||
|
||||
t.Run("creates land state with string", func(t *testing.T) {
|
||||
tramp := Land[int]("result")
|
||||
|
||||
assert.True(t, tramp.Landed, "should be in land state")
|
||||
assert.Equal(t, "result", tramp.Land, "land value should match")
|
||||
assert.Equal(t, 0, tramp.Bounce, "bounce value should be zero value")
|
||||
})
|
||||
|
||||
t.Run("creates land state with struct", func(t *testing.T) {
|
||||
type Result struct {
|
||||
value int
|
||||
done bool
|
||||
}
|
||||
result := Result{value: 100, done: true}
|
||||
tramp := Land[int](result)
|
||||
|
||||
assert.True(t, tramp.Landed, "should be in land state")
|
||||
assert.Equal(t, result, tramp.Land, "land value should match")
|
||||
assert.Equal(t, 0, tramp.Bounce, "bounce value should be zero value")
|
||||
})
|
||||
}
|
||||
|
||||
// TestFieldAccess verifies that trampoline fields can be accessed directly
|
||||
func TestFieldAccess(t *testing.T) {
|
||||
t.Run("accesses bounce state fields", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
|
||||
assert.False(t, tramp.Landed)
|
||||
assert.Equal(t, 42, tramp.Bounce)
|
||||
assert.Equal(t, "", tramp.Land)
|
||||
})
|
||||
|
||||
t.Run("accesses land state fields", func(t *testing.T) {
|
||||
tramp := Land[int]("done")
|
||||
|
||||
assert.True(t, tramp.Landed)
|
||||
assert.Equal(t, "done", tramp.Land)
|
||||
assert.Equal(t, 0, tramp.Bounce)
|
||||
})
|
||||
}
|
||||
|
||||
// TestFactorial demonstrates a complete factorial implementation using trampolines
|
||||
func TestFactorial(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
factorialStep := func(state State) Trampoline[State, int] {
|
||||
if state.n <= 1 {
|
||||
return Land[State](state.acc)
|
||||
}
|
||||
return Bounce[int](State{state.n - 1, state.acc * state.n})
|
||||
}
|
||||
|
||||
factorial := func(n int) int {
|
||||
current := Bounce[int](State{n, 1})
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = factorialStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input int
|
||||
expected int
|
||||
}{
|
||||
{"factorial of 0", 0, 1},
|
||||
{"factorial of 1", 1, 1},
|
||||
{"factorial of 5", 5, 120},
|
||||
{"factorial of 10", 10, 3628800},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := factorial(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFibonacci demonstrates Fibonacci sequence using trampolines
|
||||
func TestFibonacci(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
curr int
|
||||
prev int
|
||||
}
|
||||
|
||||
fibStep := func(state State) Trampoline[State, int] {
|
||||
if state.n <= 0 {
|
||||
return Land[State](state.curr)
|
||||
}
|
||||
return Bounce[int](State{
|
||||
n: state.n - 1,
|
||||
curr: state.prev + state.curr,
|
||||
prev: state.curr,
|
||||
})
|
||||
}
|
||||
|
||||
fibonacci := func(n int) int {
|
||||
current := Bounce[int](State{n, 1, 0})
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = fibStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input int
|
||||
expected int
|
||||
}{
|
||||
{"fib(0)", 0, 1},
|
||||
{"fib(1)", 1, 1},
|
||||
{"fib(5)", 5, 8},
|
||||
{"fib(10)", 10, 89},
|
||||
{"fib(20)", 20, 10946},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := fibonacci(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestSumList demonstrates list processing using trampolines
|
||||
func TestSumList(t *testing.T) {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
sumStep := func(state State) Trampoline[State, int] {
|
||||
if len(state.list) == 0 {
|
||||
return Land[State](state.sum)
|
||||
}
|
||||
return Bounce[int](State{
|
||||
list: state.list[1:],
|
||||
sum: state.sum + state.list[0],
|
||||
})
|
||||
}
|
||||
|
||||
sumList := func(list []int) int {
|
||||
current := Bounce[int](State{list, 0})
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = sumStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input []int
|
||||
expected int
|
||||
}{
|
||||
{"empty list", []int{}, 0},
|
||||
{"single element", []int{42}, 42},
|
||||
{"multiple elements", []int{1, 2, 3, 4, 5}, 15},
|
||||
{"negative numbers", []int{-1, -2, -3}, -6},
|
||||
{"mixed numbers", []int{10, -5, 3, -2}, 6},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := sumList(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestCountdown demonstrates a simple countdown using trampolines
|
||||
func TestCountdown(t *testing.T) {
|
||||
countdownStep := func(n int) Trampoline[int, int] {
|
||||
if n <= 0 {
|
||||
return Land[int](0)
|
||||
}
|
||||
return Bounce[int](n - 1)
|
||||
}
|
||||
|
||||
countdown := func(n int) int {
|
||||
current := Bounce[int](n)
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = countdownStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input int
|
||||
expected int
|
||||
}{
|
||||
{"countdown from 0", 0, 0},
|
||||
{"countdown from 1", 1, 0},
|
||||
{"countdown from 10", 10, 0},
|
||||
{"countdown from 1000", 1000, 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := countdown(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGCD demonstrates greatest common divisor using trampolines
|
||||
func TestGCD(t *testing.T) {
|
||||
type State struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
|
||||
gcdStep := func(state State) Trampoline[State, int] {
|
||||
if state.b == 0 {
|
||||
return Land[State](state.a)
|
||||
}
|
||||
return Bounce[int](State{state.b, state.a % state.b})
|
||||
}
|
||||
|
||||
gcd := func(a, b int) int {
|
||||
current := Bounce[int](State{a, b})
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = gcdStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a int
|
||||
b int
|
||||
expected int
|
||||
}{
|
||||
{"gcd(48, 18)", 48, 18, 6},
|
||||
{"gcd(100, 50)", 100, 50, 50},
|
||||
{"gcd(17, 13)", 17, 13, 1},
|
||||
{"gcd(0, 5)", 0, 5, 5},
|
||||
{"gcd(5, 0)", 5, 0, 5},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := gcd(tt.a, tt.b)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeepRecursion verifies that trampolines can handle very deep recursion
|
||||
// without stack overflow
|
||||
func TestDeepRecursion(t *testing.T) {
|
||||
countdownStep := func(n int) Trampoline[int, int] {
|
||||
if n <= 0 {
|
||||
return Land[int](0)
|
||||
}
|
||||
return Bounce[int](n - 1)
|
||||
}
|
||||
|
||||
countdown := func(n int) int {
|
||||
current := Bounce[int](n)
|
||||
for {
|
||||
if current.Landed {
|
||||
return current.Land
|
||||
}
|
||||
current = countdownStep(current.Bounce)
|
||||
}
|
||||
}
|
||||
|
||||
// This would cause stack overflow with regular recursion
|
||||
result := countdown(100000)
|
||||
assert.Equal(t, 0, result, "should handle deep recursion without stack overflow")
|
||||
}
|
||||
|
||||
// TestDifferentTypes verifies trampolines work with various type combinations
|
||||
func TestDifferentTypes(t *testing.T) {
|
||||
t.Run("int to string", func(t *testing.T) {
|
||||
tramp := Land[int]("result")
|
||||
assert.True(t, tramp.Landed)
|
||||
assert.Equal(t, "result", tramp.Land)
|
||||
})
|
||||
|
||||
t.Run("string to bool", func(t *testing.T) {
|
||||
tramp := Bounce[bool]("state")
|
||||
assert.False(t, tramp.Landed)
|
||||
assert.Equal(t, "state", tramp.Bounce)
|
||||
})
|
||||
|
||||
t.Run("struct to struct", func(t *testing.T) {
|
||||
type Input struct{ x int }
|
||||
type Output struct{ y string }
|
||||
|
||||
tramp := Land[Input](Output{y: "done"})
|
||||
assert.True(t, tramp.Landed)
|
||||
assert.Equal(t, Output{y: "done"}, tramp.Land)
|
||||
})
|
||||
|
||||
t.Run("slice to map", func(t *testing.T) {
|
||||
tramp := Bounce[map[string]int]([]string{"a", "b"})
|
||||
assert.False(t, tramp.Landed)
|
||||
assert.Equal(t, []string{"a", "b"}, tramp.Bounce)
|
||||
})
|
||||
}
|
||||
44
v2/tailrec/types.go
Normal file
44
v2/tailrec/types.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package tailrec
|
||||
|
||||
type (
|
||||
// Trampoline represents a step in a tail-recursive computation.
|
||||
//
|
||||
// A Trampoline can be in one of two states:
|
||||
// - 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:
|
||||
// - 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:
|
||||
//
|
||||
// // Factorial using trampolines
|
||||
// type State struct { n, acc int }
|
||||
//
|
||||
// func factorialStep(state State) Trampoline[State, int] {
|
||||
// if state.n <= 1 {
|
||||
// return Land[State](state.acc) // Base case
|
||||
// }
|
||||
// return Bounce[int](State{state.n - 1, state.acc * state.n}) // Recursive case
|
||||
// }
|
||||
//
|
||||
// See package documentation for more examples and usage patterns.
|
||||
Trampoline[B, L any] struct {
|
||||
// Land holds the final result value when the computation has completed.
|
||||
// This field is only meaningful when Landed is true.
|
||||
Land L
|
||||
|
||||
// Bounce holds the intermediate state for the next recursive step.
|
||||
// This field is only meaningful when Landed is false.
|
||||
Bounce B
|
||||
|
||||
// Landed indicates whether the computation has completed.
|
||||
// When true, the Land field contains the final result.
|
||||
// When false, the Bounce field contains the state for the next iteration.
|
||||
Landed bool
|
||||
}
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user