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

Compare commits

..

7 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
18 changed files with 2773 additions and 37 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

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