1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-19 23:42:05 +02:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
a6c6ea804f fix: overhaul record
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-18 18:32:45 +01:00
Dr. Carsten Leue
31ff98901e fix: latest doc fixes
BREAKING CHANGE: new v2

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-18 16:59:23 +01:00
Dr. Carsten Leue
255cf4353c fix: better formatting
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-18 16:07:26 +01:00
71 changed files with 5720 additions and 759 deletions

View File

@@ -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:**

View File

@@ -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)
}

View File

@@ -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]
)

View File

@@ -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))
}

View File

@@ -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))
}
}
}

View File

@@ -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]
)

View File

@@ -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)
}
}
}

View File

@@ -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))
}
}

View File

@@ -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]
)

View File

@@ -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 {

View File

@@ -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]
)

View File

@@ -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]
)

View File

@@ -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].

View 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
View 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
View 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"))
})
}

View File

@@ -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)
}
}
}

View 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
}
)

View 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(), "*")
}

View 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)
}
})
}
}

View File

@@ -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)()
}
}
}

View File

@@ -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,

View File

@@ -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)()
}
}
}

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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) {
@@ -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)
}

View File

@@ -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
View 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")
}

View File

@@ -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
View 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)
}

View File

@@ -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())
}

View 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)
}

View File

@@ -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
View 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)
}

View File

@@ -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())
}

View File

@@ -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)

View File

@@ -48,7 +48,7 @@ func ExampleOption_creation() {
// Output:
// None[int]
// Some[string](value)
// None[*string]
// None[string]
// true
// None[int]
// Some[int](4)

View 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
View 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
View 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]()
})
}

View 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
View 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
View 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)
})
}

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)
}
}
}

View File

@@ -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)()
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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]
)

View File

@@ -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)()
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}

85
v2/record/coverage.out Normal file
View 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

View File

@@ -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
}

View File

@@ -22,7 +22,6 @@ import (
O "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
@@ -55,51 +54,63 @@ func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(ma
return G.CollectOrd[map[K]V, []R](o)
}
// 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(map[K]V) R {
return G.Reduce[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(map[K]V) R {
return G.ReduceWithIndex[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(map[K]V) R {
return G.ReduceRef[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(map[K]V) R {
return G.ReduceRefWithIndex[map[K]V](f, initial)
}
// MonadMap transforms each value in a map using the provided function
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)
}
// MonadMapWithIndex transforms each key-value pair in a map using the provided function
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)
}
// MonadMapRefWithIndex transforms each key-value pair in a map using the provided function with value references
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)
}
// MonadMapRef transforms each value in a map using the provided function with value references
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)
}
func Map[K comparable, V, R any](f func(V) R) func(map[K]V) map[K]R {
// 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[map[K]V, map[K]R](f)
}
func MapRef[K comparable, V, R any](f func(*V) R) func(map[K]V) map[K]R {
// 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[map[K]V, map[K]R](f)
}
func MapWithIndex[K comparable, V, R any](f func(K, V) R) func(map[K]V) map[K]R {
// 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[map[K]V, map[K]R](f)
}
func MapRefWithIndex[K comparable, V, R any](f func(K, *V) R) func(map[K]V) map[K]R {
// 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[map[K]V, map[K]R](f)
}
@@ -118,12 +129,13 @@ func Has[K comparable, V any](k K, r map[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 {
// 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(map[K]V) Operator[K, V, V] {
return G.Union[map[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 map[K]V) Operator[K, V, V] {
return G.Merge(right)
}
@@ -137,23 +149,28 @@ func Size[K comparable, V any](r map[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 map[K]V) Entries[K, V] {
return G.ToArray[map[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 map[K]V) Entries[K, V] {
return G.ToEntries[map[K]V, Entries[K, V]](r)
}
func FromEntries[K comparable, V any](fa []T.Tuple2[K, V]) map[K]V {
// FromEntries creates a map from an array of key-value pairs
func FromEntries[K comparable, V any](fa Entries[K, V]) map[K]V {
return G.FromEntries[map[K]V](fa)
}
func UpsertAt[K comparable, V any](k K, v V) func(map[K]V) map[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[map[K]V](k, v)
}
func DeleteAt[K comparable, V any](k K) func(map[K]V) map[K]V {
// 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[map[K]V](k)
}
@@ -163,22 +180,22 @@ func Singleton[K comparable, V any](k K, v V) map[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 {
func FilterMapWithIndex[K comparable, V1, V2 any](f func(K, V1) O.Option[V2]) Operator[K, V1, V2] {
return G.FilterMapWithIndex[map[K]V1, map[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 {
func FilterMap[K comparable, V1, V2 any](f func(V1) O.Option[V2]) Operator[K, V1, V2] {
return G.FilterMap[map[K]V1, map[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 {
func Filter[K comparable, V any](f func(K) bool) Operator[K, V, V] {
return G.Filter[map[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 {
func FilterWithIndex[K comparable, V any](f func(K, V) bool) Operator[K, V, V] {
return G.FilterWithIndex[map[K]V](f)
}
@@ -197,19 +214,23 @@ func ConstNil[K comparable, V any]() map[K]V {
return map[K]V(nil)
}
// MonadChainWithIndex chains a map transformation function that produces maps, combining results using the provided Monoid
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 {
return G.MonadChainWithIndex(m, r, f)
}
// MonadChain chains a map transformation function that produces maps, combining results using the provided Monoid
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 {
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 {
// 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 Mo.Monoid[map[K]V2]) func(func(K, V1) map[K]V2) Operator[K, V1, V2] {
return G.ChainWithIndex[map[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 {
// 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 Mo.Monoid[map[K]V2]) func(func(V1) map[K]V2) Operator[K, V1, V2] {
return G.Chain[map[K]V1](m)
}
@@ -219,12 +240,12 @@ func Flatten[K comparable, V any](m Mo.Monoid[map[K]V]) func(map[K]map[K]V) map[
}
// 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 {
func FilterChainWithIndex[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(K, V1) O.Option[map[K]V2]) Operator[K, V1, V2] {
return G.FilterChainWithIndex[map[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 {
func FilterChain[V1 any, K comparable, V2 any](m Mo.Monoid[map[K]V2]) func(func(V1) O.Option[map[K]V2]) Operator[K, V1, V2] {
return G.FilterChain[map[K]V1](m)
}
@@ -278,10 +299,12 @@ func ValuesOrd[V any, K comparable](o ord.Ord[K]) func(r map[K]V) []V {
return G.ValuesOrd[map[K]V, []V](o)
}
// MonadFlap applies a value to a map of functions, producing a map of results
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)
}
// 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) func(map[K]func(A) B) map[K]B {
return G.Flap[map[K]func(A) B, map[K]B](a)
}
@@ -303,8 +326,8 @@ func FromFoldableMap[
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]) func(fa HKTA) map[K]V {
return G.FromFoldableMap[func(A) Entry[K, V]](m, red)
}
// FromArrayMap converts from an array to a map
@@ -312,15 +335,15 @@ 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]) func(fa []A) map[K]V {
return G.FromArrayMap[func(A) Entry[K, V], []A, map[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(map[K]V, Entry[K, V]) map[K]V, map[K]V) func(HKTA) map[K]V, // the reduce function
K comparable,
V any](m Mg.Magma[V], red FOLDABLE) func(fa HKTA) map[K]V {
return G.FromFoldable(m, red)
@@ -330,14 +353,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]) func(fa Entries[K, V]) map[K]V {
return G.FromArray[Entries[K, V], map[K]V](m)
}
// 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 Mo.Monoid[map[K]B], fab map[K]func(A) B, fa map[K]A) map[K]B {
return G.MonadAp(m, fab, fa)
}
// 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 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)
}
// Of creates a map with a single key-value pair
func Of[K comparable, A any](k K, a A) map[K]A {
return map[K]A{k: a}
}

View File

@@ -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[string, int, string](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, int, int](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[string, int, string](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[string, int, string](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[string, int, int](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, int, int](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[string, int](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, string, 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, string](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, int](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, int](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, string, 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, int, int](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[string, int, string](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, int, int](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[string, int, string](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, int](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, int](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, string, string](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, string](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[func(func(map[string]string, string) map[string]string, map[string]string) func([]string) map[string]string, string, []string, string, string](
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[[]Entry[string, int], func(func(map[string]int, Entry[string, int]) map[string]int, map[string]int) func([]Entry[string, int]) map[string]int, string, int](
Mg.Second[int](),
reducer,
)
result := from(entries)
assert.Equal(t, map[string]int{"a": 3, "b": 2}, result)
}

View File

@@ -20,14 +20,90 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// 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"}
func UnionSemigroup[K comparable, V any](s S.Semigroup[V]) S.Semigroup[map[K]V] {
return G.UnionSemigroup[map[K]V](s)
}
// 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
func UnionLastSemigroup[K comparable, V any]() S.Semigroup[map[K]V] {
return G.UnionLastSemigroup[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)
func UnionFirstSemigroup[K comparable, V any]() S.Semigroup[map[K]V] {
return G.UnionFirstSemigroup[map[K]V]()
}

229
v2/record/semigroup_test.go Normal file
View File

@@ -0,0 +1,229 @@
// 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, int](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, 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, int](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, int](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, int](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)
}
// Made with Bob

154
v2/record/types.go Normal file
View File

@@ -0,0 +1,154 @@
// 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/pair"
"github.com/IBM/fp-go/v2/predicate"
)
type (
// 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[K comparable, 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]
)

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
}
)

Binary file not shown.