1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-03-12 13:36:56 +02:00

Compare commits

..

8 Commits

Author SHA1 Message Date
Dr. Carsten Leue
cb2e0b23e8 fix: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-09 20:41:56 +01:00
Dr. Carsten Leue
8d5dc7ea1f fix: increase test coverage
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:52:08 +01:00
Dr. Carsten Leue
69a11bc681 fix: increase test coverage
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:51:44 +01:00
Dr. Carsten Leue
a0910b8279 fix: add -coverpkg=./... to v2
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:34:17 +01:00
Dr. Carsten Leue
029d7be52d fix: better collection of coverage results
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:32:14 +01:00
Dr. Carsten Leue
c6d30bb642 fix: increase test timeout
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 23:21:27 +01:00
Dr. Carsten Leue
1821f00fbe fix: introduce effect.LocalReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 22:52:20 +01:00
Dr. Carsten Leue
f0ec0b2541 fix: optimize record performance
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 22:20:19 +01:00
21 changed files with 2846 additions and 41 deletions

View File

@@ -39,7 +39,7 @@ jobs:
- name: Run tests
run: |
go mod tidy
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
go test -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
- name: Upload coverage to Coveralls
continue-on-error: true
@@ -79,7 +79,7 @@ jobs:
run: |
cd v2
go mod tidy
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
go test -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
- name: Upload coverage to Coveralls
continue-on-error: true

130
v2/context/reader/reader.go Normal file
View File

@@ -0,0 +1,130 @@
// 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 reader provides a specialization of the Reader monad for [context.Context].
//
// This package offers a context-aware Reader monad that simplifies working with
// Go's [context.Context] in a functional programming style. It eliminates the need
// to explicitly thread context through function calls while maintaining type safety
// and composability.
//
// # Core Concept
//
// The Reader monad represents computations that depend on a shared environment.
// In this package, that environment is fixed to [context.Context], making it
// particularly useful for:
//
// - Request-scoped data propagation
// - Cancellation and timeout handling
// - Dependency injection via context values
// - Avoiding explicit context parameter threading
//
// # Type Definitions
//
// - Reader[A]: A computation that depends on context.Context and produces A
// - Kleisli[A, B]: A function from A to Reader[B] for composing computations
// - Operator[A, B]: A transformation from Reader[A] to Reader[B]
//
// # Usage Pattern
//
// Instead of passing context explicitly through every function:
//
// func processUser(ctx context.Context, userID string) (User, error) {
// user := fetchUser(ctx, userID)
// profile := fetchProfile(ctx, user.ProfileID)
// return enrichUser(ctx, user, profile), nil
// }
//
// You can use Reader to compose context-dependent operations:
//
// fetchUser := func(userID string) Reader[User] {
// return func(ctx context.Context) User {
// // Use ctx for database access, cancellation, etc.
// return queryDatabase(ctx, userID)
// }
// }
//
// processUser := func(userID string) Reader[User] {
// return F.Pipe2(
// fetchUser(userID),
// reader.Chain(func(user User) Reader[Profile] {
// return fetchProfile(user.ProfileID)
// }),
// reader.Map(func(profile Profile) User {
// return enrichUser(user, profile)
// }),
// )
// }
//
// // Execute with context
// ctx := context.Background()
// user := processUser("user123")(ctx)
//
// # Integration with Standard Library
//
// This package works seamlessly with Go's standard [context] package:
//
// - Context cancellation and deadlines are preserved
// - Context values can be accessed within Reader computations
// - Readers can be composed with context-aware libraries
//
// # Relationship to Other Packages
//
// This package is a specialization of [github.com/IBM/fp-go/v2/reader] where
// the environment type R is fixed to [context.Context]. For more general
// Reader operations, see the base reader package.
//
// For combining Reader with other monads:
// - [github.com/IBM/fp-go/v2/context/readerio]: Reader + IO effects
// - [github.com/IBM/fp-go/v2/readeroption]: Reader + Option
// - [github.com/IBM/fp-go/v2/readerresult]: Reader + Result (Either)
//
// # Example: HTTP Request Handler
//
// type RequestContext struct {
// UserID string
// RequestID string
// }
//
// // Extract request context from context.Context
// getRequestContext := func(ctx context.Context) RequestContext {
// return RequestContext{
// UserID: ctx.Value("userID").(string),
// RequestID: ctx.Value("requestID").(string),
// }
// }
//
// // A Reader that logs with request context
// logInfo := func(message string) Reader[function.Void] {
// return func(ctx context.Context) function.Void {
// reqCtx := getRequestContext(ctx)
// log.Printf("[%s] User %s: %s", reqCtx.RequestID, reqCtx.UserID, message)
// return function.VOID
// }
// }
//
// // Compose operations
// handleRequest := func(data string) Reader[Response] {
// return F.Pipe2(
// logInfo("Processing request"),
// reader.Chain(func(_ function.Void) Reader[Result] {
// return processData(data)
// }),
// reader.Map(func(result Result) Response {
// return Response{Data: result}
// }),
// )
// }
package reader

142
v2/context/reader/types.go Normal file
View File

@@ -0,0 +1,142 @@
// 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 reader
import (
"context"
R "github.com/IBM/fp-go/v2/reader"
)
type (
// Reader represents a computation that depends on a [context.Context] and produces a value of type A.
//
// This is a specialization of the generic Reader monad where the environment type is fixed
// to [context.Context]. This is particularly useful for Go applications that need to thread
// context through computations for cancellation, deadlines, and request-scoped values.
//
// Type Parameters:
// - A: The result type produced by the computation
//
// Reader[A] is equivalent to func(context.Context) A
//
// The Reader monad enables:
// - Dependency injection using context values
// - Cancellation and timeout handling
// - Request-scoped data propagation
// - Avoiding explicit context parameter threading
//
// Example:
//
// // A Reader that extracts a user ID from context
// getUserID := func(ctx context.Context) string {
// if userID, ok := ctx.Value("userID").(string); ok {
// return userID
// }
// return "anonymous"
// }
//
// // A Reader that checks if context is cancelled
// isCancelled := func(ctx context.Context) bool {
// select {
// case <-ctx.Done():
// return true
// default:
// return false
// }
// }
//
// // Use the readers with a context
// ctx := context.WithValue(context.Background(), "userID", "user123")
// userID := getUserID(ctx) // "user123"
// cancelled := isCancelled(ctx) // false
Reader[A any] = R.Reader[context.Context, A]
// Kleisli represents a Kleisli arrow for the context-based Reader monad.
//
// It's a function from A to Reader[B], used for composing Reader computations
// that all depend on the same [context.Context].
//
// Type Parameters:
// - A: The input type
// - B: The output type wrapped in Reader
//
// Kleisli[A, B] is equivalent to func(A) func(context.Context) B
//
// Kleisli arrows are fundamental for monadic composition, allowing you to chain
// operations that depend on context without explicitly passing the context through
// each function call.
//
// Example:
//
// // A Kleisli arrow that creates a greeting Reader from a name
// greet := func(name string) Reader[string] {
// return func(ctx context.Context) string {
// if deadline, ok := ctx.Deadline(); ok {
// return fmt.Sprintf("Hello %s (deadline: %v)", name, deadline)
// }
// return fmt.Sprintf("Hello %s", name)
// }
// }
//
// // Use the Kleisli arrow
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// greeting := greet("Alice")(ctx) // "Hello Alice (deadline: ...)"
Kleisli[A, B any] = R.Reader[A, Reader[B]]
// Operator represents a transformation from one Reader to another.
//
// It takes a Reader[A] and produces a Reader[B], where both readers depend on
// the same [context.Context]. This type is commonly used for operations like
// Map, Chain, and other transformations that convert readers while preserving
// the context dependency.
//
// Type Parameters:
// - A: The input Reader's result type
// - B: The output Reader's result type
//
// Operator[A, B] is equivalent to func(Reader[A]) func(context.Context) B
//
// Operators enable building pipelines of context-dependent computations where
// each step can transform the result of the previous computation while maintaining
// access to the shared context.
//
// Example:
//
// // An operator that transforms int readers to string readers
// intToString := func(r Reader[int]) Reader[string] {
// return func(ctx context.Context) string {
// value := r(ctx)
// return strconv.Itoa(value)
// }
// }
//
// // A Reader that extracts a timeout value from context
// getTimeout := func(ctx context.Context) int {
// if deadline, ok := ctx.Deadline(); ok {
// return int(time.Until(deadline).Seconds())
// }
// return 0
// }
//
// // Transform the Reader
// getTimeoutStr := intToString(getTimeout)
// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
// defer cancel()
// result := getTimeoutStr(ctx) // "30" (approximately)
Operator[A, B any] = Kleisli[Reader[A], B]
)

View File

@@ -52,7 +52,7 @@ import (
//
// - f: A Kleisli arrow (A => ReaderIOResult[Trampoline[A, B]]) that:
// - Takes the current state A
// - Returns a ReaderIOResult that depends on [context.Context]
// - Returns a ReaderIOResult that depends on context.Context
// - Can fail with error (Left in the outer Either)
// - Produces Trampoline[A, B] to control recursion flow (Right in the outer Either)
//
@@ -60,13 +60,13 @@ import (
//
// A Kleisli arrow (A => ReaderIOResult[B]) that:
// - Takes an initial state A
// - Returns a ReaderIOResult that requires [context.Context]
// - Returns a ReaderIOResult that requires context.Context
// - Can fail with error or context cancellation
// - Produces the final result B after recursion completes
//
// # Context Cancellation
//
// Unlike the base [readerioresult.TailRec], this version automatically integrates
// Unlike the base readerioresult.TailRec, this version automatically integrates
// context cancellation checking:
// - Each recursive iteration checks if the context is cancelled
// - If cancelled, recursion terminates immediately with a cancellation error
@@ -92,9 +92,9 @@ import (
//
// # Example: Cancellable Countdown
//
// countdownStep := func(n int) readerioresult.ReaderIOResult[tailrec.Trampoline[int, string]] {
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[int, string]] {
// return func() either.Either[error, tailrec.Trampoline[int, string]] {
// countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
// return func(ctx context.Context) IOEither[Trampoline[int, string]] {
// return func() Either[Trampoline[int, string]] {
// if n <= 0 {
// return either.Right[error](tailrec.Land[int]("Done!"))
// }
@@ -105,7 +105,7 @@ import (
// }
// }
//
// countdown := readerioresult.TailRec(countdownStep)
// countdown := TailRec(countdownStep)
//
// // With cancellation
// ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
@@ -119,9 +119,9 @@ import (
// processed []string
// }
//
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[tailrec.Trampoline[ProcessState, []string]] {
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[ProcessState, []string]] {
// return func() either.Either[error, tailrec.Trampoline[ProcessState, []string]] {
// processStep := func(state ProcessState) ReaderIOResult[Trampoline[ProcessState, []string]] {
// return func(ctx context.Context) IOEither[Trampoline[ProcessState, []string]] {
// return func() Either[Trampoline[ProcessState, []string]] {
// if len(state.files) == 0 {
// return either.Right[error](tailrec.Land[ProcessState](state.processed))
// }
@@ -140,7 +140,7 @@ import (
// }
// }
//
// processFiles := readerioresult.TailRec(processStep)
// processFiles := TailRec(processStep)
// ctx, cancel := context.WithCancel(t.Context())
//
// // Can be cancelled at any point during processing
@@ -158,7 +158,7 @@ import (
// still respecting context cancellation:
//
// // Safe for very large inputs with cancellation support
// largeCountdown := readerioresult.TailRec(countdownStep)
// largeCountdown := TailRec(countdownStep)
// ctx := t.Context()
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
//
@@ -171,11 +171,11 @@ import (
//
// # See Also
//
// - [readerioresult.TailRec]: Base tail recursion without automatic context checking
// - [WithContext]: Context cancellation wrapper used internally
// - [Chain]: For sequencing ReaderIOResult computations
// - [Ask]: For accessing the context
// - [Left]/[Right]: For creating error/success values
// - readerioresult.TailRec: Base tail recursion without automatic context checking
// - WithContext: Context cancellation wrapper used internally
// - Chain: For sequencing ReaderIOResult computations
// - Ask: For accessing the context
// - Left/Right: For creating error/success values
//
//go:inline
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {

View File

@@ -30,6 +30,16 @@ import (
"github.com/stretchr/testify/require"
)
// CustomError is a test error type
type CustomError struct {
Code int
Message string
}
func (e *CustomError) Error() string {
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
}
func TestTailRec_BasicRecursion(t *testing.T) {
// Test basic countdown recursion
countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
@@ -432,3 +442,237 @@ func TestTailRec_ContextWithValue(t *testing.T) {
assert.Equal(t, E.Of[error]("Done!"), result)
}
func TestTailRec_MultipleErrorTypes(t *testing.T) {
// Test that different error types are properly handled
errorStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
if n == 5 {
customErr := &CustomError{Code: 500, Message: "custom error"}
return E.Left[Trampoline[int, string]](error(customErr))
}
if n <= 0 {
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}
errorRecursion := TailRec(errorStep)
result := errorRecursion(10)(t.Context())()
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
customErr, ok := err.(*CustomError)
require.True(t, ok, "Expected CustomError type")
assert.Equal(t, 500, customErr.Code)
assert.Equal(t, "custom error", customErr.Message)
}
func TestTailRec_ContextCancelDuringBounce(t *testing.T) {
// Test cancellation happens between bounces, not during computation
var iterationCount int32
ctx, cancel := context.WithCancel(t.Context())
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
count := atomic.AddInt32(&iterationCount, 1)
// Cancel after 3 iterations
if count == 3 {
cancel()
}
if n <= 0 {
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}
slowRecursion := TailRec(slowStep)
result := slowRecursion(10)(ctx)()
// Should be cancelled after a few iterations
assert.True(t, E.IsLeft(result))
iterations := atomic.LoadInt32(&iterationCount)
assert.Greater(t, iterations, int32(2))
assert.Less(t, iterations, int32(10))
}
func TestTailRec_EmptyState(t *testing.T) {
// Test with empty/zero-value state
type EmptyState struct{}
emptyStep := func(state EmptyState) ReaderIOResult[Trampoline[EmptyState, int]] {
return func(ctx context.Context) IOEither[Trampoline[EmptyState, int]] {
return func() Either[Trampoline[EmptyState, int]] {
return E.Right[error](tailrec.Land[EmptyState](42))
}
}
}
emptyRecursion := TailRec(emptyStep)
result := emptyRecursion(EmptyState{})(t.Context())()
assert.Equal(t, E.Of[error](42), result)
}
func TestTailRec_PointerState(t *testing.T) {
// Test with pointer state to ensure proper handling
type Node struct {
Value int
Next *Node
}
// Create a linked list: 1 -> 2 -> 3 -> nil
list := &Node{Value: 1, Next: &Node{Value: 2, Next: &Node{Value: 3, Next: nil}}}
sumStep := func(node *Node) ReaderIOResult[Trampoline[*Node, int]] {
return func(ctx context.Context) IOEither[Trampoline[*Node, int]] {
return func() Either[Trampoline[*Node, int]] {
if node == nil {
return E.Right[error](tailrec.Land[*Node](0))
}
if node.Next == nil {
return E.Right[error](tailrec.Land[*Node](node.Value))
}
// Accumulate value and continue
node.Next.Value += node.Value
return E.Right[error](tailrec.Bounce[int](node.Next))
}
}
}
sumList := TailRec(sumStep)
result := sumList(list)(t.Context())()
assert.Equal(t, E.Of[error](6), result) // 1 + 2 + 3 = 6
}
func TestTailRec_ConcurrentCancellation(t *testing.T) {
// Test that cancellation works correctly with concurrent operations
var iterationCount int32
ctx, cancel := context.WithCancel(t.Context())
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
atomic.AddInt32(&iterationCount, 1)
time.Sleep(10 * time.Millisecond)
if n <= 0 {
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}
slowRecursion := TailRec(slowStep)
// Cancel from another goroutine after 50ms
go func() {
time.Sleep(50 * time.Millisecond)
cancel()
}()
start := time.Now()
result := slowRecursion(20)(ctx)()
elapsed := time.Since(start)
// Should be cancelled
assert.True(t, E.IsLeft(result))
// Should complete quickly due to cancellation
assert.Less(t, elapsed, 100*time.Millisecond)
// Should have executed some but not all iterations
iterations := atomic.LoadInt32(&iterationCount)
assert.Greater(t, iterations, int32(0))
assert.Less(t, iterations, int32(20))
}
func TestTailRec_NestedContextValues(t *testing.T) {
// Test that nested context values are preserved
type contextKey string
const (
key1 contextKey = "key1"
key2 contextKey = "key2"
)
nestedStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
return func() Either[Trampoline[int, string]] {
val1 := ctx.Value(key1)
val2 := ctx.Value(key2)
require.NotNil(t, val1)
require.NotNil(t, val2)
assert.Equal(t, "value1", val1.(string))
assert.Equal(t, "value2", val2.(string))
if n <= 0 {
return E.Right[error](tailrec.Land[int]("Done!"))
}
return E.Right[error](tailrec.Bounce[string](n - 1))
}
}
}
nestedRecursion := TailRec(nestedStep)
ctx := context.WithValue(t.Context(), key1, "value1")
ctx = context.WithValue(ctx, key2, "value2")
result := nestedRecursion(3)(ctx)()
assert.Equal(t, E.Of[error]("Done!"), result)
}
func BenchmarkTailRec_SimpleCountdown(b *testing.B) {
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
return func() Either[Trampoline[int, int]] {
if n <= 0 {
return E.Right[error](tailrec.Land[int](0))
}
return E.Right[error](tailrec.Bounce[int](n - 1))
}
}
}
countdown := TailRec(countdownStep)
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = countdown(1000)(ctx)()
}
}
func BenchmarkTailRec_WithCancellation(b *testing.B) {
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
return func() Either[Trampoline[int, int]] {
if n <= 0 {
return E.Right[error](tailrec.Land[int](0))
}
return E.Right[error](tailrec.Bounce[int](n - 1))
}
}
}
countdown := TailRec(countdownStep)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = countdown(1000)(ctx)()
}
}

View File

@@ -3,6 +3,7 @@ package readerreaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/context/reader"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
@@ -196,6 +197,65 @@ func LocalReaderIOResultK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(
return RRIOE.LocalReaderIOEitherK[A](f)
}
// LocalReaderK transforms the outer environment of a ReaderReaderIOResult using a Reader-based Kleisli arrow.
// It allows you to modify the outer environment through a pure computation that depends on the inner context
// before passing it to the ReaderReaderIOResult.
//
// This is useful when the outer environment transformation is a pure computation that requires access
// to the inner context (e.g., context.Context) but cannot fail. Common use cases include:
// - Extracting configuration from context values
// - Computing derived environment values based on context
// - Transforming environment based on context metadata
//
// The transformation happens in two stages:
// 1. The Reader function f is executed with the R2 outer environment and inner context to produce an R1 value
// 2. The resulting R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A Reader Kleisli arrow that transforms R2 to R1 using the inner context
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
// Example Usage:
//
// type ctxKey string
// const configKey ctxKey = "config"
//
// // Extract config from context and transform environment
// extractConfig := func(path string) reader.Reader[DetailedConfig] {
// return func(ctx context.Context) DetailedConfig {
// if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
// return cfg
// }
// return DetailedConfig{Host: "localhost", Port: 8080}
// }
// }
//
// // Use the config
// useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
// return func(ctx context.Context) ioresult.IOResult[string] {
// return func() result.Result[string] {
// return result.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
// }
// }
// }
//
// // Compose using LocalReaderK
// adapted := LocalReaderK[string](extractConfig)(useConfig)
// ctx := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
// result := adapted("config.json")(ctx)() // Result: "api.example.com:443"
//
//go:inline
func LocalReaderK[A, R1, R2 any](f reader.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderK[error, A](f)
}
// LocalReaderReaderIOEitherK transforms the outer environment of a ReaderReaderIOResult using a ReaderReaderIOResult-based Kleisli arrow.
// It allows you to modify the outer environment through a computation that depends on both the outer environment
// and the inner context, and can perform IO effects that may fail.

View File

@@ -21,6 +21,7 @@ import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/reader"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
@@ -426,3 +427,226 @@ func TestLocalReaderIOResultK(t *testing.T) {
assert.True(t, result.IsLeft(resErr))
})
}
// TestLocalReaderK tests LocalReaderK functionality
func TestLocalReaderK(t *testing.T) {
ctx := context.Background()
t.Run("basic Reader transformation", func(t *testing.T) {
// Reader that transforms string path to SimpleConfig using context
loadConfig := func(path string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
// Could extract values from context here
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalReaderK
adapted := LocalReaderK[string](loadConfig)(useConfig)
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
})
t.Run("extract config from context", func(t *testing.T) {
type ctxKey string
const configKey ctxKey = "config"
// Reader that extracts config from context
extractConfig := func(path string) reader.Reader[DetailedConfig] {
return func(ctx context.Context) DetailedConfig {
if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
return cfg
}
// Default config if not in context
return DetailedConfig{Host: "localhost", Port: 8080}
}
}
// Use the config
useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
}
}
}
adapted := LocalReaderK[string](extractConfig)(useConfig)
// With context value
ctxWithConfig := context.WithValue(ctx, configKey, DetailedConfig{Host: "api.example.com", Port: 443})
res := adapted("ignored")(ctxWithConfig)()
assert.Equal(t, result.Of("api.example.com:443"), res)
// Without context value (uses default)
resDefault := adapted("ignored")(ctx)()
assert.Equal(t, result.Of("localhost:8080"), resDefault)
})
t.Run("context-aware transformation", func(t *testing.T) {
type ctxKey string
const multiplierKey ctxKey = "multiplier"
// Reader that uses context to compute environment
computeValue := func(base int) reader.Reader[int] {
return func(ctx context.Context) int {
if mult, ok := ctx.Value(multiplierKey).(int); ok {
return base * mult
}
return base
}
}
// Use the computed value
formatValue := func(val int) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Value: %d", val))
}
}
}
adapted := LocalReaderK[string](computeValue)(formatValue)
// With multiplier in context
ctxWithMult := context.WithValue(ctx, multiplierKey, 10)
res := adapted(5)(ctxWithMult)()
assert.Equal(t, result.Of("Value: 50"), res)
// Without multiplier (uses base value)
resBase := adapted(5)(ctx)()
assert.Equal(t, result.Of("Value: 5"), resBase)
})
t.Run("compose multiple LocalReaderK", func(t *testing.T) {
type ctxKey string
const prefixKey ctxKey = "prefix"
// First transformation: int -> string using context
intToString := func(n int) reader.Reader[string] {
return func(ctx context.Context) string {
if prefix, ok := ctx.Value(prefixKey).(string); ok {
return fmt.Sprintf("%s-%d", prefix, n)
}
return fmt.Sprintf("%d", n)
}
}
// Second transformation: string -> SimpleConfig
stringToConfig := func(s string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
return SimpleConfig{Port: len(s) * 100}
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose transformations
step1 := LocalReaderK[string](stringToConfig)(formatConfig)
step2 := LocalReaderK[string](intToString)(step1)
// With prefix in context
ctxWithPrefix := context.WithValue(ctx, prefixKey, "test")
res := step2(42)(ctxWithPrefix)()
// "test-42" has length 7, so port = 700
assert.Equal(t, result.Of("Port: 700"), res)
// Without prefix
resNoPrefix := step2(42)(ctx)()
// "42" has length 2, so port = 200
assert.Equal(t, result.Of("Port: 200"), resNoPrefix)
})
t.Run("error propagation in ReaderReaderIOResult", func(t *testing.T) {
// Reader transformation (pure, cannot fail)
loadConfig := func(path string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that returns an error
failingOperation := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Left[string](errors.New("operation failed"))
}
}
}
adapted := LocalReaderK[string](loadConfig)(failingOperation)
res := adapted("config.json")(ctx)()
// Error from the ReaderReaderIOResult should propagate
assert.True(t, result.IsLeft(res))
})
t.Run("real-world: environment selection based on context", func(t *testing.T) {
type Environment string
const (
Dev Environment = "dev"
Prod Environment = "prod"
)
type ctxKey string
const envKey ctxKey = "environment"
type EnvConfig struct {
Name string
}
// Reader that selects config based on context environment
selectConfig := func(envName EnvConfig) reader.Reader[DetailedConfig] {
return func(ctx context.Context) DetailedConfig {
env := Dev
if e, ok := ctx.Value(envKey).(Environment); ok {
env = e
}
switch env {
case Prod:
return DetailedConfig{Host: "api.production.com", Port: 443}
default:
return DetailedConfig{Host: "localhost", Port: 8080}
}
}
}
// Use the selected config
useConfig := func(cfg DetailedConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Connecting to %s:%d", cfg.Host, cfg.Port))
}
}
}
adapted := LocalReaderK[string](selectConfig)(useConfig)
// Production environment
ctxProd := context.WithValue(ctx, envKey, Prod)
resProd := adapted(EnvConfig{Name: "app"})(ctxProd)()
assert.Equal(t, result.Of("Connecting to api.production.com:443"), resProd)
// Development environment (default)
resDev := adapted(EnvConfig{Name: "app"})(ctx)()
assert.Equal(t, result.Of("Connecting to localhost:8080"), resDev)
})
}

View File

@@ -16,6 +16,7 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/reader"
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/io"
@@ -267,10 +268,89 @@ func LocalThunkK[A, C1, C2 any](f thunk.Kleisli[C2, C1]) func(Effect[C1, A]) Eff
// - Local/Contramap: Pure context transformation (C2 -> C1)
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
// - LocalReaderIOResultK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
// - LocalThunkK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
//
//go:inline
func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderReaderIOEitherK[A](f)
}
// LocalReaderK transforms the context of an Effect using a Reader-based Kleisli arrow.
// It allows you to modify the context through a pure computation that depends on the runtime context
// before passing it to the Effect.
//
// This is useful when the context transformation is a pure computation that requires access
// to the runtime context (context.Context) but cannot fail. Common use cases include:
// - Extracting configuration from context values
// - Computing derived context values based on runtime context
// - Transforming context based on runtime metadata
//
// The transformation happens in two stages:
// 1. The Reader function f is executed with the C2 context and runtime context to produce a C1 value
// 2. The resulting C1 value is passed as the context to the Effect[C1, A]
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (required by the original effect)
// - C2: The outer context type (provided to the transformed effect)
//
// # Parameters
//
// - f: A Reader Kleisli arrow that transforms C2 to C1 using the runtime context
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect to use C2
//
// # Example
//
// type ctxKey string
// const configKey ctxKey = "config"
//
// type DetailedConfig struct {
// Host string
// Port int
// }
//
// type SimpleConfig struct {
// Port int
// }
//
// // Extract config from runtime context and transform
// extractConfig := func(path string) reader.Reader[DetailedConfig] {
// return func(ctx context.Context) DetailedConfig {
// if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
// return cfg
// }
// return DetailedConfig{Host: "localhost", Port: 8080}
// }
// }
//
// // Effect that uses DetailedConfig
// configEffect := effect.Of[DetailedConfig]("connected")
//
// // Transform to use string path instead
// transform := effect.LocalReaderK[string](extractConfig)
// pathEffect := transform(configEffect)
//
// // Run with runtime context containing config
// ctx := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
// ioResult := effect.Provide[string]("config.json")(pathEffect)
// readerResult := effect.RunSync(ioResult)
// result, err := readerResult(ctx) // Uses config from context
//
// # Comparison with other Local functions
//
// - Local/Contramap: Pure context transformation (C2 -> C1)
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
// - LocalReaderK: Reader-based pure transformation with runtime context access (C2 -> Reader[C1])
// - LocalThunkK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
//
//go:inline
func LocalReaderK[A, C1, C2 any](f reader.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderK[A](f)
}

View File

@@ -19,7 +19,9 @@ import (
"context"
"fmt"
"testing"
"time"
"github.com/IBM/fp-go/v2/context/reader"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/stretchr/testify/assert"
)
@@ -618,3 +620,347 @@ func TestLocalEffectK(t *testing.T) {
assert.Equal(t, 60, result) // 3 * 10 * 2
})
}
func TestLocalReaderK(t *testing.T) {
t.Run("basic Reader transformation", func(t *testing.T) {
type SimpleConfig struct {
Port int
}
// Reader that transforms string path to SimpleConfig using runtime context
loadConfig := func(path string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
// Could extract values from runtime context here
return SimpleConfig{Port: 8080}
}
}
// Effect that uses the config
configEffect := Of[SimpleConfig]("connected")
// Transform using LocalReaderK
transform := LocalReaderK[string](loadConfig)
pathEffect := transform(configEffect)
// Run with path
ioResult := Provide[string]("config.json")(pathEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "connected", result)
})
t.Run("extract config from runtime context", func(t *testing.T) {
type ctxKey string
const configKey ctxKey = "config"
type DetailedConfig struct {
Host string
Port int
}
// Reader that extracts config from runtime context
extractConfig := func(path string) reader.Reader[DetailedConfig] {
return func(ctx context.Context) DetailedConfig {
if cfg, ok := ctx.Value(configKey).(DetailedConfig); ok {
return cfg
}
// Default config if not in runtime context
return DetailedConfig{Host: "localhost", Port: 8080}
}
}
// Effect that uses the config
configEffect := Chain(func(cfg DetailedConfig) Effect[DetailedConfig, string] {
return Of[DetailedConfig](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
})(readerreaderioresult.Ask[DetailedConfig]())
transform := LocalReaderK[string](extractConfig)
pathEffect := transform(configEffect)
// With config in runtime context
ctxWithConfig := context.WithValue(context.Background(), configKey, DetailedConfig{Host: "api.example.com", Port: 443})
ioResult := Provide[string]("ignored")(pathEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(ctxWithConfig)
assert.NoError(t, err)
assert.Equal(t, "api.example.com:443", result)
// Without config in runtime context (uses default)
ioResult2 := Provide[string]("ignored")(pathEffect)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, "localhost:8080", result2)
})
t.Run("runtime context-aware transformation", func(t *testing.T) {
type ctxKey string
const multiplierKey ctxKey = "multiplier"
// Reader that uses runtime context to compute context
computeValue := func(base int) reader.Reader[int] {
return func(ctx context.Context) int {
if mult, ok := ctx.Value(multiplierKey).(int); ok {
return base * mult
}
return base
}
}
// Effect that uses the computed value
valueEffect := Chain(func(val int) Effect[int, string] {
return Of[int](fmt.Sprintf("Value: %d", val))
})(readerreaderioresult.Ask[int]())
transform := LocalReaderK[string](computeValue)
baseEffect := transform(valueEffect)
// With multiplier in runtime context
ctxWithMult := context.WithValue(context.Background(), multiplierKey, 10)
ioResult := Provide[string](5)(baseEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(ctxWithMult)
assert.NoError(t, err)
assert.Equal(t, "Value: 50", result)
// Without multiplier (uses base value)
ioResult2 := Provide[string](5)(baseEffect)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, "Value: 5", result2)
})
t.Run("compose multiple LocalReaderK", func(t *testing.T) {
type ctxKey string
const prefixKey ctxKey = "prefix"
// First transformation: int -> string using runtime context
intToString := func(n int) reader.Reader[string] {
return func(ctx context.Context) string {
if prefix, ok := ctx.Value(prefixKey).(string); ok {
return fmt.Sprintf("%s-%d", prefix, n)
}
return fmt.Sprintf("%d", n)
}
}
// Second transformation: string -> SimpleConfig
type SimpleConfig struct {
Port int
}
stringToConfig := func(s string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
return SimpleConfig{Port: len(s) * 100}
}
}
// Effect that uses the config
configEffect := Chain(func(cfg SimpleConfig) Effect[SimpleConfig, string] {
return Of[SimpleConfig](fmt.Sprintf("Port: %d", cfg.Port))
})(readerreaderioresult.Ask[SimpleConfig]())
// Compose transformations
step1 := LocalReaderK[string](stringToConfig)
step2 := LocalReaderK[string](intToString)
effect1 := step1(configEffect)
effect2 := step2(effect1)
// With prefix in runtime context
ctxWithPrefix := context.WithValue(context.Background(), prefixKey, "test")
ioResult := Provide[string](42)(effect2)
readerResult := RunSync(ioResult)
result, err := readerResult(ctxWithPrefix)
assert.NoError(t, err)
// "test-42" has length 7, so port = 700
assert.Equal(t, "Port: 700", result)
// Without prefix
ioResult2 := Provide[string](42)(effect2)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
// "42" has length 2, so port = 200
assert.Equal(t, "Port: 200", result2)
})
t.Run("error propagation from Effect", func(t *testing.T) {
type SimpleConfig struct {
Port int
}
// Reader transformation (pure, cannot fail)
loadConfig := func(path string) reader.Reader[SimpleConfig] {
return func(ctx context.Context) SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// Effect that returns an error
expectedErr := assert.AnError
failingEffect := Fail[SimpleConfig, string](expectedErr)
transform := LocalReaderK[string](loadConfig)
pathEffect := transform(failingEffect)
ioResult := Provide[string]("config.json")(pathEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
// Error from the Effect should propagate
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("real-world: environment selection based on runtime context", func(t *testing.T) {
type Environment string
const (
Dev Environment = "dev"
Prod Environment = "prod"
)
type ctxKey string
const envKey ctxKey = "environment"
type EnvConfig struct {
Name string
}
type DetailedConfig struct {
Host string
Port int
}
// Reader that selects config based on runtime context environment
selectConfig := func(envName EnvConfig) reader.Reader[DetailedConfig] {
return func(ctx context.Context) DetailedConfig {
env := Dev
if e, ok := ctx.Value(envKey).(Environment); ok {
env = e
}
switch env {
case Prod:
return DetailedConfig{Host: "api.production.com", Port: 443}
default:
return DetailedConfig{Host: "localhost", Port: 8080}
}
}
}
// Effect that uses the selected config
configEffect := Chain(func(cfg DetailedConfig) Effect[DetailedConfig, string] {
return Of[DetailedConfig](fmt.Sprintf("Connecting to %s:%d", cfg.Host, cfg.Port))
})(readerreaderioresult.Ask[DetailedConfig]())
transform := LocalReaderK[string](selectConfig)
envEffect := transform(configEffect)
// Production environment
ctxProd := context.WithValue(context.Background(), envKey, Prod)
ioResult := Provide[string](EnvConfig{Name: "app"})(envEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(ctxProd)
assert.NoError(t, err)
assert.Equal(t, "Connecting to api.production.com:443", result)
// Development environment (default)
ioResult2 := Provide[string](EnvConfig{Name: "app"})(envEffect)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, "Connecting to localhost:8080", result2)
})
t.Run("composes with other Local functions", func(t *testing.T) {
type Level1 struct {
Value string
}
type Level2 struct {
Data string
}
type Level3 struct {
Info string
}
// Effect at deepest level
effect3 := Of[Level3]("result")
// Use LocalReaderK for first transformation (with runtime context access)
localReaderK23 := LocalReaderK[string](func(l2 Level2) reader.Reader[Level3] {
return func(ctx context.Context) Level3 {
return Level3{Info: l2.Data}
}
})
// Use Local for second transformation (pure)
local12 := Local[string](func(l1 Level1) Level2 {
return Level2{Data: l1.Value}
})
// Compose them
effect2 := localReaderK23(effect3)
effect1 := local12(effect2)
// Run
ioResult := Provide[string](Level1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("runtime context deadline awareness", func(t *testing.T) {
type Config struct {
HasDeadline bool
}
// Reader that checks runtime context for deadline
checkContext := func(path string) reader.Reader[Config] {
return func(ctx context.Context) Config {
_, hasDeadline := ctx.Deadline()
return Config{HasDeadline: hasDeadline}
}
}
// Effect that uses the config
configEffect := Chain(func(cfg Config) Effect[Config, string] {
return Of[Config](fmt.Sprintf("Has deadline: %v", cfg.HasDeadline))
})(readerreaderioresult.Ask[Config]())
transform := LocalReaderK[string](checkContext)
pathEffect := transform(configEffect)
// Without deadline
ioResult := Provide[string]("config.json")(pathEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "Has deadline: false", result)
// With deadline
ctxWithDeadline, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
ioResult2 := Provide[string]("config.json")(pathEffect)
readerResult2 := RunSync(ioResult2)
result2, err2 := readerResult2(ctxWithDeadline)
assert.NoError(t, err2)
assert.Equal(t, "Has deadline: true", result2)
})
}

195
v2/iooption/array_test.go Normal file
View File

@@ -0,0 +1,195 @@
// 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 iooption
import (
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestTraverseArray_Success(t *testing.T) {
f := func(n int) IOOption[int] {
return Of(n * 2)
}
input := []int{1, 2, 3, 4, 5}
result := TraverseArray(f)(input)()
assert.Equal(t, O.Some([]int{2, 4, 6, 8, 10}), result)
}
func TestTraverseArray_WithNone(t *testing.T) {
f := func(n int) IOOption[int] {
if n > 0 {
return Of(n * 2)
}
return None[int]()
}
input := []int{1, 2, -3, 4}
result := TraverseArray(f)(input)()
assert.Equal(t, O.None[[]int](), result)
}
func TestTraverseArray_EmptyArray(t *testing.T) {
f := func(n int) IOOption[int] {
return Of(n * 2)
}
input := []int{}
result := TraverseArray(f)(input)()
assert.Equal(t, O.Some([]int{}), result)
}
func TestTraverseArrayWithIndex_Success(t *testing.T) {
f := func(idx, n int) IOOption[int] {
return Of(n + idx)
}
input := []int{10, 20, 30}
result := TraverseArrayWithIndex(f)(input)()
assert.Equal(t, O.Some([]int{10, 21, 32}), result)
}
func TestTraverseArrayWithIndex_WithNone(t *testing.T) {
f := func(idx, n int) IOOption[int] {
if idx < 2 {
return Of(n + idx)
}
return None[int]()
}
input := []int{10, 20, 30}
result := TraverseArrayWithIndex(f)(input)()
assert.Equal(t, O.None[[]int](), result)
}
func TestTraverseArrayWithIndex_EmptyArray(t *testing.T) {
f := func(idx, n int) IOOption[int] {
return Of(n + idx)
}
input := []int{}
result := TraverseArrayWithIndex(f)(input)()
assert.Equal(t, O.Some([]int{}), result)
}
func TestSequenceArray_AllSome(t *testing.T) {
input := []IOOption[int]{
Of(1),
Of(2),
Of(3),
}
result := SequenceArray(input)()
assert.Equal(t, O.Some([]int{1, 2, 3}), result)
}
func TestSequenceArray_WithNone(t *testing.T) {
input := []IOOption[int]{
Of(1),
None[int](),
Of(3),
}
result := SequenceArray(input)()
assert.Equal(t, O.None[[]int](), result)
}
func TestSequenceArray_Empty(t *testing.T) {
input := []IOOption[int]{}
result := SequenceArray(input)()
assert.Equal(t, O.Some([]int{}), result)
}
func TestSequenceArray_AllNone(t *testing.T) {
input := []IOOption[int]{
None[int](),
None[int](),
None[int](),
}
result := SequenceArray(input)()
assert.Equal(t, O.None[[]int](), result)
}
func TestTraverseArray_Composition(t *testing.T) {
// Test composing traverse with other operations
f := func(n int) IOOption[int] {
if n%2 == 0 {
return Of(n / 2)
}
return None[int]()
}
input := []int{2, 4, 6, 8}
result := F.Pipe1(
input,
TraverseArray(f),
)()
assert.Equal(t, O.Some([]int{1, 2, 3, 4}), result)
}
func TestTraverseArray_WithMap(t *testing.T) {
// Test traverse followed by map
f := func(n int) IOOption[int] {
return Of(n * 2)
}
input := []int{1, 2, 3}
result := F.Pipe2(
input,
TraverseArray(f),
Map(func(arr []int) int {
sum := 0
for _, v := range arr {
sum += v
}
return sum
}),
)()
assert.Equal(t, O.Some(12), result) // (1*2 + 2*2 + 3*2) = 12
}
func TestTraverseArrayWithIndex_UseIndex(t *testing.T) {
// Test that index is properly used
f := func(idx, n int) IOOption[string] {
return Of(fmt.Sprintf("%d", idx*n*2))
}
input := []int{1, 2, 3}
result := TraverseArrayWithIndex(f)(input)()
assert.Equal(t, O.Some([]string{"0", "4", "12"}), result)
}

View File

@@ -0,0 +1,433 @@
// 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 iooption
import (
"fmt"
"testing"
"time"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
I "github.com/IBM/fp-go/v2/io"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestOf(t *testing.T) {
result := Of(42)()
assert.Equal(t, O.Some(42), result)
}
func TestSome(t *testing.T) {
result := Some("test")()
assert.Equal(t, O.Some("test"), result)
}
func TestNone(t *testing.T) {
result := None[int]()()
assert.Equal(t, O.None[int](), result)
}
func TestMonadOf(t *testing.T) {
result := MonadOf(100)()
assert.Equal(t, O.Some(100), result)
}
func TestFromOptionComprehensive(t *testing.T) {
t.Run("from Some", func(t *testing.T) {
result := FromOption(O.Some(42))()
assert.Equal(t, O.Some(42), result)
})
t.Run("from None", func(t *testing.T) {
result := FromOption(O.None[int]())()
assert.Equal(t, O.None[int](), result)
})
}
func TestFromIO(t *testing.T) {
ioValue := I.Of(42)
result := FromIO(ioValue)()
assert.Equal(t, O.Some(42), result)
}
func TestMonadMap(t *testing.T) {
t.Run("map over Some", func(t *testing.T) {
result := MonadMap(Of(5), utils.Double)()
assert.Equal(t, O.Some(10), result)
})
t.Run("map over None", func(t *testing.T) {
result := MonadMap(None[int](), utils.Double)()
assert.Equal(t, O.None[int](), result)
})
}
func TestMonadChain(t *testing.T) {
t.Run("chain Some to Some", func(t *testing.T) {
f := func(n int) IOOption[int] {
return Of(n * 2)
}
result := MonadChain(Of(5), f)()
assert.Equal(t, O.Some(10), result)
})
t.Run("chain Some to None", func(t *testing.T) {
f := func(n int) IOOption[int] {
return None[int]()
}
result := MonadChain(Of(5), f)()
assert.Equal(t, O.None[int](), result)
})
t.Run("chain None", func(t *testing.T) {
f := func(n int) IOOption[int] {
return Of(n * 2)
}
result := MonadChain(None[int](), f)()
assert.Equal(t, O.None[int](), result)
})
}
func TestChain(t *testing.T) {
f := func(n int) IOOption[string] {
if n > 0 {
return Of("positive")
}
return None[string]()
}
t.Run("chain positive", func(t *testing.T) {
result := F.Pipe1(Of(5), Chain(f))()
assert.Equal(t, O.Some("positive"), result)
})
t.Run("chain negative", func(t *testing.T) {
result := F.Pipe1(Of(-5), Chain(f))()
assert.Equal(t, O.None[string](), result)
})
}
func TestMonadAp(t *testing.T) {
t.Run("apply Some function to Some value", func(t *testing.T) {
mab := Of(utils.Double)
ma := Of(5)
result := MonadAp(mab, ma)()
assert.Equal(t, O.Some(10), result)
})
t.Run("apply None function", func(t *testing.T) {
mab := None[func(int) int]()
ma := Of(5)
result := MonadAp(mab, ma)()
assert.Equal(t, O.None[int](), result)
})
t.Run("apply to None value", func(t *testing.T) {
mab := Of(utils.Double)
ma := None[int]()
result := MonadAp(mab, ma)()
assert.Equal(t, O.None[int](), result)
})
}
func TestAp(t *testing.T) {
ma := Of(5)
result := F.Pipe1(Of(utils.Double), Ap[int, int](ma))()
assert.Equal(t, O.Some(10), result)
}
func TestApSeq(t *testing.T) {
ma := Of(5)
result := F.Pipe1(Of(utils.Double), ApSeq[int, int](ma))()
assert.Equal(t, O.Some(10), result)
}
func TestApPar(t *testing.T) {
ma := Of(5)
result := F.Pipe1(Of(utils.Double), ApPar[int, int](ma))()
assert.Equal(t, O.Some(10), result)
}
func TestFlatten(t *testing.T) {
t.Run("flatten Some(Some)", func(t *testing.T) {
nested := Of(Of(42))
result := Flatten(nested)()
assert.Equal(t, O.Some(42), result)
})
t.Run("flatten Some(None)", func(t *testing.T) {
nested := Of(None[int]())
result := Flatten(nested)()
assert.Equal(t, O.None[int](), result)
})
t.Run("flatten None", func(t *testing.T) {
nested := None[IOOption[int]]()
result := Flatten(nested)()
assert.Equal(t, O.None[int](), result)
})
}
func TestOptionize0(t *testing.T) {
f := func() (int, bool) {
return 42, true
}
result := Optionize0(f)()()
assert.Equal(t, O.Some(42), result)
f2 := func() (int, bool) {
return 0, false
}
result2 := Optionize0(f2)()()
assert.Equal(t, O.None[int](), result2)
}
func TestOptionize2(t *testing.T) {
f := func(a, b int) (int, bool) {
if b != 0 {
return a / b, true
}
return 0, false
}
result := Optionize2(f)(10, 2)()
assert.Equal(t, O.Some(5), result)
result2 := Optionize2(f)(10, 0)()
assert.Equal(t, O.None[int](), result2)
}
func TestOptionize3(t *testing.T) {
f := func(a, b, c int) (int, bool) {
if c != 0 {
return (a + b) / c, true
}
return 0, false
}
result := Optionize3(f)(10, 5, 3)()
assert.Equal(t, O.Some(5), result)
result2 := Optionize3(f)(10, 5, 0)()
assert.Equal(t, O.None[int](), result2)
}
func TestOptionize4(t *testing.T) {
f := func(a, b, c, d int) (int, bool) {
if d != 0 {
return (a + b + c) / d, true
}
return 0, false
}
result := Optionize4(f)(10, 5, 3, 2)()
assert.Equal(t, O.Some(9), result)
result2 := Optionize4(f)(10, 5, 3, 0)()
assert.Equal(t, O.None[int](), result2)
}
func TestMemoize(t *testing.T) {
callCount := 0
ioOpt := func() Option[int] {
callCount++
return O.Some(42)
}
memoized := Memoize(ioOpt)
// First call
result1 := memoized()
assert.Equal(t, O.Some(42), result1)
assert.Equal(t, 1, callCount)
// Second call should use cached value
result2 := memoized()
assert.Equal(t, O.Some(42), result2)
assert.Equal(t, 1, callCount)
}
func TestFold(t *testing.T) {
onNone := I.Of("none")
onSome := func(n int) I.IO[string] {
return I.Of(fmt.Sprintf("%d", n))
}
t.Run("fold Some", func(t *testing.T) {
result := Fold(onNone, onSome)(Of(42))()
assert.Equal(t, "42", result)
})
t.Run("fold None", func(t *testing.T) {
result := Fold(onNone, onSome)(None[int]())()
assert.Equal(t, "none", result)
})
}
func TestDefer(t *testing.T) {
callCount := 0
gen := func() IOOption[int] {
callCount++
return Of(42)
}
deferred := Defer(gen)
// Each call should invoke the generator
result1 := deferred()
assert.Equal(t, O.Some(42), result1)
assert.Equal(t, 1, callCount)
result2 := deferred()
assert.Equal(t, O.Some(42), result2)
assert.Equal(t, 2, callCount)
}
func TestFromEither(t *testing.T) {
t.Run("from Right", func(t *testing.T) {
either := ET.Right[string](42)
result := FromEither(either)()
assert.Equal(t, O.Some(42), result)
})
t.Run("from Left", func(t *testing.T) {
either := ET.Left[int]("error")
result := FromEither(either)()
assert.Equal(t, O.None[int](), result)
})
}
func TestMonadAlt(t *testing.T) {
t.Run("first is Some", func(t *testing.T) {
result := MonadAlt(Of(1), Of(2))()
assert.Equal(t, O.Some(1), result)
})
t.Run("first is None, second is Some", func(t *testing.T) {
result := MonadAlt(None[int](), Of(2))()
assert.Equal(t, O.Some(2), result)
})
t.Run("both are None", func(t *testing.T) {
result := MonadAlt(None[int](), None[int]())()
assert.Equal(t, O.None[int](), result)
})
}
func TestAlt(t *testing.T) {
t.Run("first is Some", func(t *testing.T) {
result := F.Pipe1(Of(1), Alt(Of(2)))()
assert.Equal(t, O.Some(1), result)
})
t.Run("first is None", func(t *testing.T) {
result := F.Pipe1(None[int](), Alt(Of(2)))()
assert.Equal(t, O.Some(2), result)
})
}
func TestMonadChainFirst(t *testing.T) {
sideEffect := 0
f := func(n int) IOOption[string] {
sideEffect = n * 2
return Of("side effect")
}
result := MonadChainFirst(Of(5), f)()
assert.Equal(t, O.Some(5), result)
assert.Equal(t, 10, sideEffect)
}
func TestChainFirst(t *testing.T) {
sideEffect := 0
f := func(n int) IOOption[string] {
sideEffect = n * 2
return Of("side effect")
}
result := F.Pipe1(Of(5), ChainFirst(f))()
assert.Equal(t, O.Some(5), result)
assert.Equal(t, 10, sideEffect)
}
func TestMonadChainFirstIOK(t *testing.T) {
sideEffect := 0
f := func(n int) I.IO[string] {
return func() string {
sideEffect = n * 2
return "side effect"
}
}
result := MonadChainFirstIOK(Of(5), f)()
assert.Equal(t, O.Some(5), result)
assert.Equal(t, 10, sideEffect)
}
func TestChainFirstIOK(t *testing.T) {
sideEffect := 0
f := func(n int) I.IO[string] {
return func() string {
sideEffect = n * 2
return "side effect"
}
}
result := F.Pipe1(Of(5), ChainFirstIOK(f))()
assert.Equal(t, O.Some(5), result)
assert.Equal(t, 10, sideEffect)
}
func TestDelay(t *testing.T) {
start := time.Now()
delay := 50 * time.Millisecond
result := F.Pipe1(Of(42), Delay[int](delay))()
elapsed := time.Since(start)
assert.Equal(t, O.Some(42), result)
assert.True(t, elapsed >= delay, "Expected delay of at least %v, got %v", delay, elapsed)
}
func TestAfter(t *testing.T) {
timestamp := time.Now().Add(50 * time.Millisecond)
result := F.Pipe1(Of(42), After[int](timestamp))()
assert.Equal(t, O.Some(42), result)
assert.True(t, time.Now().After(timestamp) || time.Now().Equal(timestamp))
}
func TestMonadChainIOK(t *testing.T) {
f := func(n int) I.IO[string] {
return I.Of(fmt.Sprintf("%d", n))
}
t.Run("chain Some", func(t *testing.T) {
result := MonadChainIOK(Of(42), f)()
assert.Equal(t, O.Some("42"), result)
})
t.Run("chain None", func(t *testing.T) {
result := MonadChainIOK(None[int](), f)()
assert.Equal(t, O.None[string](), result)
})
}

241
v2/ioresult/bracket_test.go Normal file
View File

@@ -0,0 +1,241 @@
// 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 ioresult
import (
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestBracket_Success(t *testing.T) {
acquired := false
used := false
released := false
acquire := func() IOResult[int] {
return func() Result[int] {
acquired = true
return result.Of(42)
}
}()
use := func(n int) IOResult[string] {
return func() Result[string] {
used = true
return result.Of("success")
}
}
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
return result.Of(F.VOID)
}
}
res := Bracket(acquire, use, release)()
assert.True(t, acquired, "Resource should be acquired")
assert.True(t, used, "Resource should be used")
assert.True(t, released, "Resource should be released")
assert.Equal(t, result.Of("success"), res)
}
func TestBracket_UseFailure(t *testing.T) {
acquired := false
released := false
releaseResult := result.Result[string]{}
acquire := func() IOResult[int] {
return func() Result[int] {
acquired = true
return result.Of(42)
}
}()
useErr := errors.New("use error")
use := func(n int) IOResult[string] {
return func() Result[string] {
return result.Left[string](useErr)
}
}
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
releaseResult = res
return result.Of(F.VOID)
}
}
res := Bracket(acquire, use, release)()
assert.True(t, acquired, "Resource should be acquired")
assert.True(t, released, "Resource should be released even on use failure")
assert.Equal(t, result.Left[string](useErr), res)
assert.Equal(t, result.Left[string](useErr), releaseResult)
}
func TestBracket_AcquireFailure(t *testing.T) {
used := false
released := false
acquireErr := errors.New("acquire error")
acquire := func() IOResult[int] {
return func() Result[int] {
return result.Left[int](acquireErr)
}
}()
use := func(n int) IOResult[string] {
return func() Result[string] {
used = true
return result.Of("success")
}
}
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
return result.Of(F.VOID)
}
}
res := Bracket(acquire, use, release)()
assert.False(t, used, "Use should not be called if acquire fails")
assert.False(t, released, "Release should not be called if acquire fails")
assert.Equal(t, result.Left[string](acquireErr), res)
}
func TestBracket_ReleaseFailure(t *testing.T) {
acquired := false
used := false
released := false
acquire := func() IOResult[int] {
return func() Result[int] {
acquired = true
return result.Of(42)
}
}()
use := func(n int) IOResult[string] {
return func() Result[string] {
used = true
return result.Of("success")
}
}
releaseErr := errors.New("release error")
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
return result.Left[F.Void](releaseErr)
}
}
res := Bracket(acquire, use, release)()
assert.True(t, acquired, "Resource should be acquired")
assert.True(t, used, "Resource should be used")
assert.True(t, released, "Release should be attempted")
// When release fails, the release error is returned
assert.Equal(t, result.Left[string](releaseErr), res)
}
func TestBracket_BothUseAndReleaseFail(t *testing.T) {
acquired := false
released := false
acquire := func() IOResult[int] {
return func() Result[int] {
acquired = true
return result.Of(42)
}
}()
useErr := errors.New("use error")
use := func(n int) IOResult[string] {
return func() Result[string] {
return result.Left[string](useErr)
}
}
releaseErr := errors.New("release error")
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
return result.Left[F.Void](releaseErr)
}
}
res := Bracket(acquire, use, release)()
assert.True(t, acquired, "Resource should be acquired")
assert.True(t, released, "Release should be attempted")
// When both fail, the release error is returned
assert.Equal(t, result.Left[string](releaseErr), res)
}
func TestBracket_ResourceValue(t *testing.T) {
// Test that the acquired resource value is passed correctly
var usedValue int
var releasedValue int
acquire := Of(100)
use := func(n int) IOResult[string] {
usedValue = n
return Of("result")
}
release := func(n int, res Result[string]) IOResult[F.Void] {
releasedValue = n
return Of(F.VOID)
}
Bracket(acquire, use, release)()
assert.Equal(t, 100, usedValue, "Use should receive acquired value")
assert.Equal(t, 100, releasedValue, "Release should receive acquired value")
}
func TestBracket_ResultValue(t *testing.T) {
// Test that the use result is passed to release
var releaseReceivedResult Result[string]
acquire := Of(42)
use := func(n int) IOResult[string] {
return Of("test result")
}
release := func(n int, res Result[string]) IOResult[F.Void] {
releaseReceivedResult = res
return Of(F.VOID)
}
Bracket(acquire, use, release)()
assert.Equal(t, result.Of("test result"), releaseReceivedResult)
}

View File

@@ -0,0 +1,581 @@
// 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 ioresult
import (
"errors"
"fmt"
"testing"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
"github.com/IBM/fp-go/v2/io"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestLeft(t *testing.T) {
err := errors.New("test error")
res := Left[int](err)()
assert.Equal(t, result.Left[int](err), res)
}
func TestRight(t *testing.T) {
res := Right(42)()
assert.Equal(t, result.Of(42), res)
}
func TestOf(t *testing.T) {
res := Of(42)()
assert.Equal(t, result.Of(42), res)
}
func TestMonadOf(t *testing.T) {
res := MonadOf(42)()
assert.Equal(t, result.Of(42), res)
}
func TestLeftIO(t *testing.T) {
err := errors.New("test error")
res := LeftIO[int](io.Of(err))()
assert.Equal(t, result.Left[int](err), res)
}
func TestRightIO(t *testing.T) {
res := RightIO(io.Of(42))()
assert.Equal(t, result.Of(42), res)
}
func TestFromEither(t *testing.T) {
t.Run("from Right", func(t *testing.T) {
either := result.Of(42)
res := FromEither(either)()
assert.Equal(t, result.Of(42), res)
})
t.Run("from Left", func(t *testing.T) {
err := errors.New("test error")
either := result.Left[int](err)
res := FromEither(either)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestFromResult(t *testing.T) {
t.Run("from success", func(t *testing.T) {
res := FromResult(result.Of(42))()
assert.Equal(t, result.Of(42), res)
})
t.Run("from error", func(t *testing.T) {
err := errors.New("test error")
res := FromResult(result.Left[int](err))()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestFromEitherI(t *testing.T) {
t.Run("with nil error", func(t *testing.T) {
res := FromEitherI(42, nil)()
assert.Equal(t, result.Of(42), res)
})
t.Run("with error", func(t *testing.T) {
err := errors.New("test error")
res := FromEitherI(0, err)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestFromResultI(t *testing.T) {
t.Run("with nil error", func(t *testing.T) {
res := FromResultI(42, nil)()
assert.Equal(t, result.Of(42), res)
})
t.Run("with error", func(t *testing.T) {
err := errors.New("test error")
res := FromResultI(0, err)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestFromOption_Success(t *testing.T) {
onNone := func() error {
return errors.New("none")
}
t.Run("from Some", func(t *testing.T) {
res := FromOption[int](onNone)(O.Some(42))()
assert.Equal(t, result.Of(42), res)
})
t.Run("from None", func(t *testing.T) {
res := FromOption[int](onNone)(O.None[int]())()
assert.Equal(t, result.Left[int](errors.New("none")), res)
})
}
func TestFromIO(t *testing.T) {
ioValue := io.Of(42)
res := FromIO(ioValue)()
assert.Equal(t, result.Of(42), res)
}
func TestFromLazy(t *testing.T) {
lazy := func() int { return 42 }
res := FromLazy(lazy)()
assert.Equal(t, result.Of(42), res)
}
func TestMonadMap(t *testing.T) {
t.Run("map over Right", func(t *testing.T) {
res := MonadMap(Of(5), utils.Double)()
assert.Equal(t, result.Of(10), res)
})
t.Run("map over Left", func(t *testing.T) {
err := errors.New("test error")
res := MonadMap(Left[int](err), utils.Double)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestMap_Comprehensive(t *testing.T) {
double := func(n int) int { return n * 2 }
t.Run("map Right", func(t *testing.T) {
res := F.Pipe1(Of(5), Map(double))()
assert.Equal(t, result.Of(10), res)
})
t.Run("map Left", func(t *testing.T) {
err := errors.New("test error")
res := F.Pipe1(Left[int](err), Map(double))()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestMonadMapTo(t *testing.T) {
t.Run("mapTo Right", func(t *testing.T) {
res := MonadMapTo(Of(5), "constant")()
assert.Equal(t, result.Of("constant"), res)
})
t.Run("mapTo Left", func(t *testing.T) {
err := errors.New("test error")
res := MonadMapTo(Left[int](err), "constant")()
assert.Equal(t, result.Left[string](err), res)
})
}
func TestMapTo(t *testing.T) {
res := F.Pipe1(Of(5), MapTo[int]("constant"))()
assert.Equal(t, result.Of("constant"), res)
}
func TestMonadChain(t *testing.T) {
f := func(n int) IOResult[int] {
return Of(n * 2)
}
t.Run("chain Right to Right", func(t *testing.T) {
res := MonadChain(Of(5), f)()
assert.Equal(t, result.Of(10), res)
})
t.Run("chain Right to Left", func(t *testing.T) {
err := errors.New("test error")
f := func(n int) IOResult[int] {
return Left[int](err)
}
res := MonadChain(Of(5), f)()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("chain Left", func(t *testing.T) {
err := errors.New("test error")
res := MonadChain(Left[int](err), f)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestChain_Comprehensive(t *testing.T) {
f := func(n int) IOResult[string] {
if n > 0 {
return Of(fmt.Sprintf("%d", n))
}
return Left[string](errors.New("negative"))
}
t.Run("chain positive", func(t *testing.T) {
res := F.Pipe1(Of(5), Chain(f))()
assert.Equal(t, result.Of("5"), res)
})
t.Run("chain negative", func(t *testing.T) {
res := F.Pipe1(Of(-5), Chain(f))()
assert.Equal(t, result.Left[string](errors.New("negative")), res)
})
}
func TestMonadChainEitherK(t *testing.T) {
f := func(n int) result.Result[int] {
if n > 0 {
return result.Of(n * 2)
}
return result.Left[int](errors.New("non-positive"))
}
t.Run("chain to success", func(t *testing.T) {
res := MonadChainEitherK(Of(5), f)()
assert.Equal(t, result.Of(10), res)
})
t.Run("chain to error", func(t *testing.T) {
res := MonadChainEitherK(Of(-5), f)()
assert.Equal(t, result.Left[int](errors.New("non-positive")), res)
})
}
func TestMonadChainResultK(t *testing.T) {
f := func(n int) result.Result[int] {
return result.Of(n * 2)
}
res := MonadChainResultK(Of(5), f)()
assert.Equal(t, result.Of(10), res)
}
func TestChainResultK(t *testing.T) {
f := func(n int) result.Result[int] {
return result.Of(n * 2)
}
res := F.Pipe1(Of(5), ChainResultK(f))()
assert.Equal(t, result.Of(10), res)
}
func TestMonadAp_Comprehensive(t *testing.T) {
t.Run("apply Right function to Right value", func(t *testing.T) {
mab := Of(utils.Double)
ma := Of(5)
res := MonadAp(mab, ma)()
assert.Equal(t, result.Of(10), res)
})
t.Run("apply Left function", func(t *testing.T) {
err := errors.New("function error")
mab := Left[func(int) int](err)
ma := Of(5)
res := MonadAp(mab, ma)()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("apply to Left value", func(t *testing.T) {
err := errors.New("value error")
mab := Of(utils.Double)
ma := Left[int](err)
res := MonadAp(mab, ma)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestAp_Comprehensive(t *testing.T) {
ma := Of(5)
res := F.Pipe1(Of(utils.Double), Ap[int, int](ma))()
assert.Equal(t, result.Of(10), res)
}
func TestApPar(t *testing.T) {
ma := Of(5)
res := F.Pipe1(Of(utils.Double), ApPar[int, int](ma))()
assert.Equal(t, result.Of(10), res)
}
func TestApSeq(t *testing.T) {
ma := Of(5)
res := F.Pipe1(Of(utils.Double), ApSeq[int, int](ma))()
assert.Equal(t, result.Of(10), res)
}
func TestFlatten_Comprehensive(t *testing.T) {
t.Run("flatten Right(Right)", func(t *testing.T) {
nested := Of(Of(42))
res := Flatten(nested)()
assert.Equal(t, result.Of(42), res)
})
t.Run("flatten Right(Left)", func(t *testing.T) {
err := errors.New("inner error")
nested := Of(Left[int](err))
res := Flatten(nested)()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("flatten Left", func(t *testing.T) {
err := errors.New("outer error")
nested := Left[IOResult[int]](err)
res := Flatten(nested)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestTryCatch(t *testing.T) {
t.Run("successful function", func(t *testing.T) {
f := func() (int, error) {
return 42, nil
}
res := TryCatch(f, F.Identity[error])()
assert.Equal(t, result.Of(42), res)
})
t.Run("failing function", func(t *testing.T) {
err := errors.New("test error")
f := func() (int, error) {
return 0, err
}
res := TryCatch(f, F.Identity[error])()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("with error transformation", func(t *testing.T) {
err := errors.New("original")
f := func() (int, error) {
return 0, err
}
onThrow := func(e error) error {
return fmt.Errorf("wrapped: %w", e)
}
res := TryCatch(f, onThrow)()
assert.True(t, result.IsLeft(res))
})
}
func TestTryCatchError_Comprehensive(t *testing.T) {
t.Run("successful function", func(t *testing.T) {
f := func() (int, error) {
return 42, nil
}
res := TryCatchError(f)()
assert.Equal(t, result.Of(42), res)
})
t.Run("failing function", func(t *testing.T) {
err := errors.New("test error")
f := func() (int, error) {
return 0, err
}
res := TryCatchError(f)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestMemoize_Comprehensive(t *testing.T) {
callCount := 0
ioRes := func() Result[int] {
callCount++
return result.Of(42)
}
memoized := Memoize(ioRes)
// First call
res1 := memoized()
assert.Equal(t, result.Of(42), res1)
assert.Equal(t, 1, callCount)
// Second call should use cached value
res2 := memoized()
assert.Equal(t, result.Of(42), res2)
assert.Equal(t, 1, callCount)
}
func TestMonadMapLeft(t *testing.T) {
t.Run("map Left error", func(t *testing.T) {
err := errors.New("original")
f := func(e error) string {
return e.Error()
}
res := MonadMapLeft(Left[int](err), f)()
// Result is IOEither[string, int], check it's a left
assert.True(t, ET.IsLeft(res))
})
t.Run("map Right unchanged", func(t *testing.T) {
f := func(e error) string {
return e.Error()
}
res := MonadMapLeft(Of(42), f)()
// MapLeft changes the error type, so result is IOEither[string, int]
assert.True(t, ET.IsRight(res))
assert.Equal(t, 42, ET.MonadFold(res, func(string) int { return 0 }, F.Identity[int]))
})
}
func TestMapLeft_Comprehensive(t *testing.T) {
f := func(e error) string {
return fmt.Sprintf("wrapped: %s", e.Error())
}
t.Run("map Left", func(t *testing.T) {
err := errors.New("original")
res := F.Pipe1(Left[int](err), MapLeft[int](f))()
// Result is IOEither[string, int], check it's a left
assert.True(t, ET.IsLeft(res))
})
t.Run("map Right unchanged", func(t *testing.T) {
res := F.Pipe1(Of(42), MapLeft[int](f))()
// MapLeft changes the error type, so result is IOEither[string, int]
assert.True(t, ET.IsRight(res))
assert.Equal(t, 42, ET.MonadFold(res, func(string) int { return 0 }, F.Identity[int]))
})
}
func TestMonadBiMap(t *testing.T) {
leftF := func(e error) string {
return e.Error()
}
rightF := func(n int) string {
return fmt.Sprintf("%d", n)
}
t.Run("bimap Right", func(t *testing.T) {
res := MonadBiMap(Of(42), leftF, rightF)()
// BiMap changes both types, so result is IOEither[string, string]
assert.True(t, ET.IsRight(res))
assert.Equal(t, "42", ET.MonadFold(res, F.Identity[string], F.Identity[string]))
})
t.Run("bimap Left", func(t *testing.T) {
err := errors.New("test")
res := MonadBiMap(Left[int](err), leftF, rightF)()
// Result is IOEither[string, string], check it's a left
assert.True(t, ET.IsLeft(res))
})
}
func TestBiMap_Comprehensive(t *testing.T) {
leftF := func(e error) string {
return e.Error()
}
rightF := func(n int) string {
return fmt.Sprintf("%d", n)
}
t.Run("bimap Right", func(t *testing.T) {
res := F.Pipe1(Of(42), BiMap(leftF, rightF))()
// BiMap changes both types, so result is IOEither[string, string]
assert.True(t, ET.IsRight(res))
assert.Equal(t, "42", ET.MonadFold(res, F.Identity[string], F.Identity[string]))
})
t.Run("bimap Left", func(t *testing.T) {
err := errors.New("test")
res := F.Pipe1(Left[int](err), BiMap(leftF, rightF))()
// Result is IOEither[string, string], check it's a left
assert.True(t, ET.IsLeft(res))
})
}
func TestFold_Comprehensive(t *testing.T) {
onLeft := func(e error) io.IO[string] {
return io.Of(fmt.Sprintf("error: %s", e.Error()))
}
onRight := func(n int) io.IO[string] {
return io.Of(fmt.Sprintf("value: %d", n))
}
t.Run("fold Right", func(t *testing.T) {
res := Fold(onLeft, onRight)(Of(42))()
assert.Equal(t, "value: 42", res)
})
t.Run("fold Left", func(t *testing.T) {
err := errors.New("test")
res := Fold(onLeft, onRight)(Left[int](err))()
assert.Equal(t, "error: test", res)
})
}
func TestGetOrElse_Comprehensive(t *testing.T) {
onLeft := func(e error) io.IO[int] {
return io.Of(0)
}
t.Run("get Right value", func(t *testing.T) {
res := GetOrElse(onLeft)(Of(42))()
assert.Equal(t, 42, res)
})
t.Run("get default on Left", func(t *testing.T) {
err := errors.New("test")
res := GetOrElse(onLeft)(Left[int](err))()
assert.Equal(t, 0, res)
})
}
func TestGetOrElseOf(t *testing.T) {
onLeft := func(e error) int {
return 0
}
t.Run("get Right value", func(t *testing.T) {
res := GetOrElseOf(onLeft)(Of(42))()
assert.Equal(t, 42, res)
})
t.Run("get default on Left", func(t *testing.T) {
err := errors.New("test")
res := GetOrElseOf(onLeft)(Left[int](err))()
assert.Equal(t, 0, res)
})
}
func TestMonadChainTo(t *testing.T) {
t.Run("chain Right to Right", func(t *testing.T) {
res := MonadChainTo(Of(1), Of(2))()
assert.Equal(t, result.Of(2), res)
})
t.Run("chain Right to Left", func(t *testing.T) {
err := errors.New("test")
res := MonadChainTo(Of(1), Left[int](err))()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("chain Left", func(t *testing.T) {
err := errors.New("test")
res := MonadChainTo(Left[int](err), Of(2))()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestChainLazyK(t *testing.T) {
f := func(n int) Lazy[string] {
return func() string {
return fmt.Sprintf("%d", n)
}
}
res := F.Pipe1(Of(42), ChainLazyK(f))()
assert.Equal(t, result.Of("42"), res)
}

View File

@@ -61,6 +61,18 @@ func LocalReaderIOEitherK[A, C, E, R1, R2 any](f readerioeither.Kleisli[C, E, R2
}
}
//go:inline
func LocalReaderK[E, A, C, R1, R2 any](f reader.Kleisli[C, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow4(
f,
readerioeither.FromReader,
readerioeither.Map[C, E](rri),
readerioeither.Flatten,
)
}
}
//go:inline
func LocalReaderReaderIOEitherK[A, C, E, R1, R2 any](f Kleisli[R2, C, E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {

View File

@@ -38,21 +38,41 @@ func IsNonEmpty[M ~map[K]V, K comparable, V any](r M) bool {
}
func Keys[M ~map[K]V, GK ~[]K, K comparable, V any](r M) GK {
// fast path
if len(r) == 0 {
return nil
}
// full implementation
return collect[M, GK](r, F.First[K, V])
}
func Values[M ~map[K]V, GV ~[]V, K comparable, V any](r M) GV {
// fast path
if len(r) == 0 {
return nil
}
// full implementation
return collect[M, GV](r, F.Second[K, V])
}
func KeysOrd[M ~map[K]V, GK ~[]K, K comparable, V any](o ord.Ord[K]) func(r M) GK {
return func(r M) GK {
// fast path
if len(r) == 0 {
return nil
}
// full implementation
return collectOrd[M, GK](o, r, F.First[K, V])
}
}
func ValuesOrd[M ~map[K]V, GV ~[]V, K comparable, V any](o ord.Ord[K]) func(r M) GV {
return func(r M) GV {
// fast path
if len(r) == 0 {
return nil
}
// full implementation
return collectOrd[M, GV](o, r, F.Second[K, V])
}
}
@@ -97,12 +117,18 @@ func collect[M ~map[K]V, GR ~[]R, K comparable, V, R any](r M, f func(K, V) R) G
}
func Collect[M ~map[K]V, GR ~[]R, K comparable, V, R any](f func(K, V) R) func(M) GR {
// full implementation
return F.Bind2nd(collect[M, GR, K, V, R], f)
}
func CollectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K]) func(f func(K, V) R) func(M) GR {
return func(f func(K, V) R) func(M) GR {
return func(r M) GR {
// fast path
if len(r) == 0 {
return nil
}
// full implementation
return collectOrd[M, GR](o, r, f)
}
}
@@ -416,12 +442,22 @@ func duplicate[M ~map[K]V, K comparable, V any](r M) M {
}
func upsertAt[M ~map[K]V, K comparable, V any](r M, k K, v V) M {
// fast path
if len(r) == 0 {
return Singleton[M](k, v)
}
// duplicate and update
dup := duplicate(r)
dup[k] = v
return dup
}
func deleteAt[M ~map[K]V, K comparable, V any](r M, k K) M {
// fast path
if len(r) == 0 {
return r
}
// duplicate and update
dup := duplicate(r)
delete(dup, k)
return dup

View File

@@ -55,10 +55,16 @@ func IsNonEmpty[K comparable, V any](r Record[K, V]) bool {
// The order of keys is non-deterministic due to Go's map iteration behavior.
// Use KeysOrd if you need keys in a specific order.
//
// Note: The return value can be nil in case of an empty map, since nil is a
// valid representation of an empty slice in Go.
//
// Example:
//
// record := Record[string, int]{"a": 1, "b": 2, "c": 3}
// keys := Keys(record) // ["a", "b", "c"] in any order
//
// emptyRecord := Record[string, int]{}
// emptyKeys := Keys(emptyRecord) // nil or []string{}
func Keys[K comparable, V any](r Record[K, V]) []K {
return G.Keys[Record[K, V], []K](r)
}
@@ -68,10 +74,16 @@ func Keys[K comparable, V any](r Record[K, V]) []K {
// The order of values is non-deterministic due to Go's map iteration behavior.
// Use ValuesOrd if you need values ordered by their keys.
//
// Note: The return value can be nil in case of an empty map, since nil is a
// valid representation of an empty slice in Go.
//
// Example:
//
// record := Record[string, int]{"a": 1, "b": 2, "c": 3}
// values := Values(record) // [1, 2, 3] in any order
//
// emptyRecord := Record[string, int]{}
// emptyValues := Values(emptyRecord) // nil or []int{}
func Values[K comparable, V any](r Record[K, V]) []V {
return G.Values[Record[K, V], []V](r)
}
@@ -98,6 +110,9 @@ func Collect[K comparable, V, R any](f func(K, V) R) func(Record[K, V]) []R {
//
// Unlike Collect, this function guarantees the order of results based on key ordering.
//
// Note: The return value can be nil in case of an empty map, since nil is a
// valid representation of an empty slice in Go.
//
// Example:
//
// record := Record[string, int]{"c": 3, "a": 1, "b": 2}
@@ -105,6 +120,9 @@ func Collect[K comparable, V, R any](f func(K, V) R) func(Record[K, V]) []R {
// return fmt.Sprintf("%s=%d", k, v)
// })
// result := toStrings(record) // ["a=1", "b=2", "c=3"] (ordered by key)
//
// emptyRecord := Record[string, int]{}
// emptyResult := toStrings(emptyRecord) // nil or []string{}
func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(Record[K, V]) []R {
return G.CollectOrd[Record[K, V], []R](o)
}
@@ -458,11 +476,18 @@ func UpsertAt[K comparable, V any](k K, v V) Operator[K, V, V] {
// If the key doesn't exist, the record is returned unchanged.
// The original record is not modified; a new record is returned.
//
// In case of an empty input map (including nil maps), the identical map is returned,
// since deleting from an empty map is an idempotent operation.
//
// Example:
//
// record := Record[string, int]{"a": 1, "b": 2, "c": 3}
// removeB := DeleteAt[string, int]("b")
// result := removeB(record) // {"a": 1, "c": 3}
//
// // Deleting from empty map returns empty map
// emptyRecord := Record[string, int]{}
// result2 := removeB(emptyRecord) // {}
func DeleteAt[K comparable, V any](k K) Operator[K, V, V] {
return G.DeleteAt[Record[K, V]](k)
}

View File

@@ -42,7 +42,7 @@ func TestNilMap_IsNonEmpty(t *testing.T) {
func TestNilMap_Keys(t *testing.T) {
var nilMap Record[string, int]
keys := Keys(nilMap)
assert.NotNil(t, keys, "Keys should return non-nil slice")
// Keys can return nil for empty map, which is a valid representation of an empty slice
assert.Equal(t, 0, len(keys), "Keys should return empty slice for nil map")
}
@@ -50,7 +50,7 @@ func TestNilMap_Keys(t *testing.T) {
func TestNilMap_Values(t *testing.T) {
var nilMap Record[string, int]
values := Values(nilMap)
assert.NotNil(t, values, "Values should return non-nil slice")
// Values can return nil for empty map, which is a valid representation of an empty slice
assert.Equal(t, 0, len(values), "Values should return empty slice for nil map")
}
@@ -288,8 +288,16 @@ func TestNilMap_DeleteAt(t *testing.T) {
var nilMap Record[string, int]
deleteFunc := DeleteAt[string, int]("key")
result := deleteFunc(nilMap)
assert.NotNil(t, result, "DeleteAt should return non-nil map")
assert.Equal(t, 0, len(result), "DeleteAt should return empty map for nil input")
// DeleteAt returns the identical map for nil input (idempotent operation)
assert.Nil(t, result, "DeleteAt should return nil for nil input (idempotent)")
assert.Equal(t, nilMap, result, "DeleteAt should return identical map for nil input")
// Verify that deleting from empty (non-nil) map returns identical map (idempotent)
emptyMap := Record[string, int]{}
result2 := deleteFunc(emptyMap)
assert.NotNil(t, result2, "DeleteAt should return non-nil map for empty input")
assert.Equal(t, 0, len(result2), "DeleteAt should return empty map for empty input")
assert.Equal(t, emptyMap, result2, "DeleteAt on empty map should be idempotent")
}
// TestNilMap_Filter verifies that Filter handles nil maps correctly

View File

@@ -69,6 +69,14 @@ func TestFormatterInterface(t *testing.T) {
result := fmt.Sprintf("%q", tramp)
assert.Equal(t, "\"Bounce(42)\"", result)
})
t.Run("unknown verb format", func(t *testing.T) {
tramp := Bounce[string](42)
result := fmt.Sprintf("%x", tramp)
assert.Contains(t, result, "%!x")
assert.Contains(t, result, "Trampoline[B, L]")
assert.Contains(t, result, "Bounce(42)")
})
}
// TestGoStringerInterface verifies fmt.GoStringer implementation

View File

@@ -22,14 +22,18 @@ import (
// LogValue implements the slog.LogValuer interface for Trampoline.
//
// This method allows Trampoline values to be logged using Go's structured logging
// (log/slog) with proper representation of their state:
// with proper representation of their state:
// - When Landed is true: returns a group with a single "landed" attribute containing the Land value
// - When Landed is false: returns a group with a single "bouncing" attribute containing the Bounce value
//
// The implementation ensures that Trampoline values are logged in a structured,
// readable format that clearly shows the current state of the tail-recursive computation.
//
// Example usage:
// # Returns
//
// - slog.Value: A structured log value representing the trampoline state
//
// # Example
//
// trampoline := tailrec.Bounce[int](42)
// slog.Info("Processing", "state", trampoline)

View File

@@ -8,17 +8,20 @@ import "fmt"
// This represents a recursive call in the original algorithm. The computation
// will continue by processing the provided state value in the next iteration.
//
// Type Parameters:
// - L: The final result type (land type)
// - B: The intermediate state type (bounce type)
// # Type Parameters
//
// - B: The intermediate state type (bounce type)
// - L: The final result type (land type)
//
// # Parameters
//
// Parameters:
// - b: The new intermediate state to process in the next step
//
// Returns:
// - A Trampoline in the "bounce" state containing the intermediate value
// # Returns
//
// Example:
// - Trampoline[B, L]: A Trampoline in the "bounce" state containing the intermediate value
//
// # Example
//
// // Countdown that bounces until reaching zero
// func countdownStep(n int) Trampoline[int, int] {
@@ -40,17 +43,20 @@ func Bounce[L, B any](b B) Trampoline[B, L] {
// a Land trampoline is encountered, the executor should stop iterating and
// return the final result.
//
// Type Parameters:
// # Type Parameters
//
// - B: The intermediate state type (bounce type)
// - L: The final result type (land type)
//
// Parameters:
// # Parameters
//
// - l: The final result value
//
// Returns:
// - A Trampoline in the "land" state containing the final result
// # Returns
//
// Example:
// - Trampoline[B, L]: A Trampoline in the "land" state containing the final result
//
// # Example
//
// // Factorial base case
// func factorialStep(state State) Trampoline[State, int] {
@@ -66,7 +72,13 @@ func Land[B, L any](l L) Trampoline[B, L] {
}
// String implements fmt.Stringer for Trampoline.
//
// Returns a human-readable string representation of the trampoline state.
// For bounce states, returns "Bounce(value)". For land states, returns "Land(value)".
//
// # Returns
//
// - string: A formatted string representation of the trampoline state
func (t Trampoline[B, L]) String() string {
if t.Landed {
return fmt.Sprintf("Land(%v)", t.Land)
@@ -75,7 +87,18 @@ func (t Trampoline[B, L]) String() string {
}
// Format implements fmt.Formatter for Trampoline.
// Supports various formatting verbs for detailed output.
//
// Supports various formatting verbs for detailed output:
// - %v: Default format (delegates to String)
// - %+v: Detailed format with type information
// - %#v: Go-syntax representation (delegates to GoString)
// - %s: String format
// - %q: Quoted string format
//
// # Parameters
//
// - f: The format state
// - verb: The formatting verb
func (t Trampoline[B, L]) Format(f fmt.State, verb rune) {
switch verb {
case 'v':
@@ -106,7 +129,13 @@ func (t Trampoline[B, L]) Format(f fmt.State, verb rune) {
}
// GoString implements fmt.GoStringer for Trampoline.
//
// Returns a Go-syntax representation that could be used to recreate the value.
// The output includes the package name, function name, type parameters, and value.
//
// # Returns
//
// - string: A Go-syntax representation of the trampoline
func (t Trampoline[B, L]) GoString() string {
if t.Landed {
return fmt.Sprintf("tailrec.Land[%T](%#v)", t.Bounce, t.Land)

View File

@@ -7,14 +7,21 @@ type (
// - Bounce: The computation should continue with a new intermediate state (type B)
// - Land: The computation is complete with a final result (type L)
//
// Type Parameters:
// # Type Parameters
//
// - B: The "bounce" type - intermediate state passed between recursive steps
// - L: The "land" type - the final result type when computation completes
//
// The trampoline pattern allows converting recursive algorithms into iterative ones,
// preventing stack overflow for deep recursion while maintaining code clarity.
//
// Example:
// # Design Note
//
// This type uses a struct with a boolean flag rather than the Either type to avoid
// a cyclic dependency. The either package depends on tailrec for its own tail-recursive
// operations, so using Either here would create a circular import.
//
// # Example
//
// // Factorial using trampolines
// type State struct { n, acc int }