mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-12 13:36:56 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a954e0d1f | ||
|
|
cb2e0b23e8 | ||
|
|
8d5dc7ea1f | ||
|
|
69a11bc681 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
- name: Run tests
|
||||
run: |
|
||||
go mod tidy
|
||||
go test -v -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
|
||||
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 -coverpkg=./... ./...
|
||||
go test -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... ./...
|
||||
|
||||
- name: Upload coverage to Coveralls
|
||||
continue-on-error: true
|
||||
|
||||
@@ -52,7 +52,7 @@ import (
|
||||
//
|
||||
// - f: A Kleisli arrow (A => ReaderIOResult[Trampoline[A, B]]) that:
|
||||
// - Takes the current state A
|
||||
// - Returns a ReaderIOResult that depends on [context.Context]
|
||||
// - Returns a ReaderIOResult that depends on context.Context
|
||||
// - Can fail with error (Left in the outer Either)
|
||||
// - Produces Trampoline[A, B] to control recursion flow (Right in the outer Either)
|
||||
//
|
||||
@@ -60,13 +60,13 @@ import (
|
||||
//
|
||||
// A Kleisli arrow (A => ReaderIOResult[B]) that:
|
||||
// - Takes an initial state A
|
||||
// - Returns a ReaderIOResult that requires [context.Context]
|
||||
// - Returns a ReaderIOResult that requires context.Context
|
||||
// - Can fail with error or context cancellation
|
||||
// - Produces the final result B after recursion completes
|
||||
//
|
||||
// # Context Cancellation
|
||||
//
|
||||
// Unlike the base [readerioresult.TailRec], this version automatically integrates
|
||||
// Unlike the base readerioresult.TailRec, this version automatically integrates
|
||||
// context cancellation checking:
|
||||
// - Each recursive iteration checks if the context is cancelled
|
||||
// - If cancelled, recursion terminates immediately with a cancellation error
|
||||
@@ -92,9 +92,9 @@ import (
|
||||
//
|
||||
// # Example: Cancellable Countdown
|
||||
//
|
||||
// countdownStep := func(n int) readerioresult.ReaderIOResult[tailrec.Trampoline[int, string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[int, string]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[int, string]] {
|
||||
// countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
// return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
// return func() Either[Trampoline[int, string]] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[error](tailrec.Land[int]("Done!"))
|
||||
// }
|
||||
@@ -105,7 +105,7 @@ import (
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// countdown := readerioresult.TailRec(countdownStep)
|
||||
// countdown := TailRec(countdownStep)
|
||||
//
|
||||
// // With cancellation
|
||||
// ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
|
||||
@@ -119,9 +119,9 @@ import (
|
||||
// processed []string
|
||||
// }
|
||||
//
|
||||
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func(ctx context.Context) ioeither.IOEither[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// return func() either.Either[error, tailrec.Trampoline[ProcessState, []string]] {
|
||||
// processStep := func(state ProcessState) ReaderIOResult[Trampoline[ProcessState, []string]] {
|
||||
// return func(ctx context.Context) IOEither[Trampoline[ProcessState, []string]] {
|
||||
// return func() Either[Trampoline[ProcessState, []string]] {
|
||||
// if len(state.files) == 0 {
|
||||
// return either.Right[error](tailrec.Land[ProcessState](state.processed))
|
||||
// }
|
||||
@@ -140,7 +140,7 @@ import (
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// processFiles := readerioresult.TailRec(processStep)
|
||||
// processFiles := TailRec(processStep)
|
||||
// ctx, cancel := context.WithCancel(t.Context())
|
||||
//
|
||||
// // Can be cancelled at any point during processing
|
||||
@@ -158,7 +158,7 @@ import (
|
||||
// still respecting context cancellation:
|
||||
//
|
||||
// // Safe for very large inputs with cancellation support
|
||||
// largeCountdown := readerioresult.TailRec(countdownStep)
|
||||
// largeCountdown := TailRec(countdownStep)
|
||||
// ctx := t.Context()
|
||||
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
|
||||
//
|
||||
@@ -171,11 +171,11 @@ import (
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - [readerioresult.TailRec]: Base tail recursion without automatic context checking
|
||||
// - [WithContext]: Context cancellation wrapper used internally
|
||||
// - [Chain]: For sequencing ReaderIOResult computations
|
||||
// - [Ask]: For accessing the context
|
||||
// - [Left]/[Right]: For creating error/success values
|
||||
// - readerioresult.TailRec: Base tail recursion without automatic context checking
|
||||
// - WithContext: Context cancellation wrapper used internally
|
||||
// - Chain: For sequencing ReaderIOResult computations
|
||||
// - Ask: For accessing the context
|
||||
// - Left/Right: For creating error/success values
|
||||
//
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Trampoline[A, B]]) Kleisli[A, B] {
|
||||
|
||||
@@ -30,6 +30,16 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// CustomError is a test error type
|
||||
type CustomError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *CustomError) Error() string {
|
||||
return fmt.Sprintf("error %d: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func TestTailRec_BasicRecursion(t *testing.T) {
|
||||
// Test basic countdown recursion
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
@@ -432,3 +442,237 @@ func TestTailRec_ContextWithValue(t *testing.T) {
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
|
||||
func TestTailRec_MultipleErrorTypes(t *testing.T) {
|
||||
// Test that different error types are properly handled
|
||||
errorStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
if n == 5 {
|
||||
customErr := &CustomError{Code: 500, Message: "custom error"}
|
||||
return E.Left[Trampoline[int, string]](error(customErr))
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
errorRecursion := TailRec(errorStep)
|
||||
result := errorRecursion(10)(t.Context())()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
err := E.ToError(result)
|
||||
customErr, ok := err.(*CustomError)
|
||||
require.True(t, ok, "Expected CustomError type")
|
||||
assert.Equal(t, 500, customErr.Code)
|
||||
assert.Equal(t, "custom error", customErr.Message)
|
||||
}
|
||||
|
||||
func TestTailRec_ContextCancelDuringBounce(t *testing.T) {
|
||||
// Test cancellation happens between bounces, not during computation
|
||||
var iterationCount int32
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
count := atomic.AddInt32(&iterationCount, 1)
|
||||
|
||||
// Cancel after 3 iterations
|
||||
if count == 3 {
|
||||
cancel()
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slowRecursion := TailRec(slowStep)
|
||||
result := slowRecursion(10)(ctx)()
|
||||
|
||||
// Should be cancelled after a few iterations
|
||||
assert.True(t, E.IsLeft(result))
|
||||
iterations := atomic.LoadInt32(&iterationCount)
|
||||
assert.Greater(t, iterations, int32(2))
|
||||
assert.Less(t, iterations, int32(10))
|
||||
}
|
||||
|
||||
func TestTailRec_EmptyState(t *testing.T) {
|
||||
// Test with empty/zero-value state
|
||||
type EmptyState struct{}
|
||||
|
||||
emptyStep := func(state EmptyState) ReaderIOResult[Trampoline[EmptyState, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[EmptyState, int]] {
|
||||
return func() Either[Trampoline[EmptyState, int]] {
|
||||
return E.Right[error](tailrec.Land[EmptyState](42))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emptyRecursion := TailRec(emptyStep)
|
||||
result := emptyRecursion(EmptyState{})(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
}
|
||||
|
||||
func TestTailRec_PointerState(t *testing.T) {
|
||||
// Test with pointer state to ensure proper handling
|
||||
type Node struct {
|
||||
Value int
|
||||
Next *Node
|
||||
}
|
||||
|
||||
// Create a linked list: 1 -> 2 -> 3 -> nil
|
||||
list := &Node{Value: 1, Next: &Node{Value: 2, Next: &Node{Value: 3, Next: nil}}}
|
||||
|
||||
sumStep := func(node *Node) ReaderIOResult[Trampoline[*Node, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[*Node, int]] {
|
||||
return func() Either[Trampoline[*Node, int]] {
|
||||
if node == nil {
|
||||
return E.Right[error](tailrec.Land[*Node](0))
|
||||
}
|
||||
if node.Next == nil {
|
||||
return E.Right[error](tailrec.Land[*Node](node.Value))
|
||||
}
|
||||
// Accumulate value and continue
|
||||
node.Next.Value += node.Value
|
||||
return E.Right[error](tailrec.Bounce[int](node.Next))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
result := sumList(list)(t.Context())()
|
||||
|
||||
assert.Equal(t, E.Of[error](6), result) // 1 + 2 + 3 = 6
|
||||
}
|
||||
|
||||
func TestTailRec_ConcurrentCancellation(t *testing.T) {
|
||||
// Test that cancellation works correctly with concurrent operations
|
||||
var iterationCount int32
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
slowStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
atomic.AddInt32(&iterationCount, 1)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slowRecursion := TailRec(slowStep)
|
||||
|
||||
// Cancel from another goroutine after 50ms
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
result := slowRecursion(20)(ctx)()
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// Should be cancelled
|
||||
assert.True(t, E.IsLeft(result))
|
||||
|
||||
// Should complete quickly due to cancellation
|
||||
assert.Less(t, elapsed, 100*time.Millisecond)
|
||||
|
||||
// Should have executed some but not all iterations
|
||||
iterations := atomic.LoadInt32(&iterationCount)
|
||||
assert.Greater(t, iterations, int32(0))
|
||||
assert.Less(t, iterations, int32(20))
|
||||
}
|
||||
|
||||
func TestTailRec_NestedContextValues(t *testing.T) {
|
||||
// Test that nested context values are preserved
|
||||
type contextKey string
|
||||
const (
|
||||
key1 contextKey = "key1"
|
||||
key2 contextKey = "key2"
|
||||
)
|
||||
|
||||
nestedStep := func(n int) ReaderIOResult[Trampoline[int, string]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, string]] {
|
||||
return func() Either[Trampoline[int, string]] {
|
||||
val1 := ctx.Value(key1)
|
||||
val2 := ctx.Value(key2)
|
||||
|
||||
require.NotNil(t, val1)
|
||||
require.NotNil(t, val2)
|
||||
assert.Equal(t, "value1", val1.(string))
|
||||
assert.Equal(t, "value2", val2.(string))
|
||||
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int]("Done!"))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[string](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nestedRecursion := TailRec(nestedStep)
|
||||
|
||||
ctx := context.WithValue(t.Context(), key1, "value1")
|
||||
ctx = context.WithValue(ctx, key2, "value2")
|
||||
|
||||
result := nestedRecursion(3)(ctx)()
|
||||
|
||||
assert.Equal(t, E.Of[error]("Done!"), result)
|
||||
}
|
||||
|
||||
func BenchmarkTailRec_SimpleCountdown(b *testing.B) {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
|
||||
return func() Either[Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = countdown(1000)(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTailRec_WithCancellation(b *testing.B) {
|
||||
countdownStep := func(n int) ReaderIOResult[Trampoline[int, int]] {
|
||||
return func(ctx context.Context) IOEither[Trampoline[int, int]] {
|
||||
return func() Either[Trampoline[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[error](tailrec.Land[int](0))
|
||||
}
|
||||
return E.Right[error](tailrec.Bounce[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = countdown(1000)(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,3 +354,20 @@ func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effe
|
||||
func LocalReaderK[A, C1, C2 any](f reader.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
|
||||
return readerreaderioresult.LocalReaderK[A](f)
|
||||
}
|
||||
|
||||
// Ask returns an Effect that produces the context C as its success value.
|
||||
// This is the fundamental operation of the reader/environment monad,
|
||||
// allowing effects to access their own context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type (also the produced value type)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, C]: An effect that succeeds with its own context value
|
||||
//
|
||||
//go:inline
|
||||
func Ask[C any]() Effect[C, C] {
|
||||
return readerreaderioresult.Ask[C]()
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/reader"
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -922,45 +920,77 @@ func TestLocalReaderK(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "result", result)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("runtime context deadline awareness", func(t *testing.T) {
|
||||
type Config struct {
|
||||
HasDeadline bool
|
||||
}
|
||||
|
||||
// Reader that checks runtime context for deadline
|
||||
checkContext := func(path string) reader.Reader[Config] {
|
||||
return func(ctx context.Context) Config {
|
||||
_, hasDeadline := ctx.Deadline()
|
||||
return Config{HasDeadline: hasDeadline}
|
||||
}
|
||||
}
|
||||
|
||||
// Effect that uses the config
|
||||
configEffect := Chain(func(cfg Config) Effect[Config, string] {
|
||||
return Of[Config](fmt.Sprintf("Has deadline: %v", cfg.HasDeadline))
|
||||
})(readerreaderioresult.Ask[Config]())
|
||||
|
||||
transform := LocalReaderK[string](checkContext)
|
||||
pathEffect := transform(configEffect)
|
||||
|
||||
// Without deadline
|
||||
ioResult := Provide[string]("config.json")(pathEffect)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(context.Background())
|
||||
func TestAsk(t *testing.T) {
|
||||
t.Run("returns context as value", func(t *testing.T) {
|
||||
ctx := "my-context"
|
||||
result, err := runEffect(Ask[string](), ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Has deadline: false", result)
|
||||
assert.Equal(t, ctx, result)
|
||||
})
|
||||
|
||||
// With deadline
|
||||
ctxWithDeadline, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
t.Run("works with struct context", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
ioResult2 := Provide[string]("config.json")(pathEffect)
|
||||
readerResult2 := RunSync(ioResult2)
|
||||
result2, err2 := readerResult2(ctxWithDeadline)
|
||||
cfg := Config{Host: "localhost", Port: 8080}
|
||||
result, err := runEffect(Ask[Config](), cfg)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, cfg, result)
|
||||
})
|
||||
|
||||
t.Run("can be chained with Map to extract a field", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
hostEffect := Map[Config](func(cfg Config) string {
|
||||
return cfg.Host
|
||||
})(Ask[Config]())
|
||||
|
||||
result, err := runEffect(hostEffect, Config{Host: "example.com", Port: 443})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "example.com", result)
|
||||
})
|
||||
|
||||
t.Run("can be chained with Chain to produce a derived effect", func(t *testing.T) {
|
||||
type Config struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
derived := Chain(func(cfg Config) Effect[Config, string] {
|
||||
if cfg.APIKey == "" {
|
||||
return Fail[Config, string](assert.AnError)
|
||||
}
|
||||
return Of[Config]("authenticated: " + cfg.APIKey)
|
||||
})(Ask[Config]())
|
||||
|
||||
// Valid key
|
||||
result, err := runEffect(derived, Config{APIKey: "secret"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "authenticated: secret", result)
|
||||
|
||||
// Empty key
|
||||
_, err = runEffect(derived, Config{APIKey: ""})
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, assert.AnError, err)
|
||||
})
|
||||
|
||||
t.Run("is idempotent - multiple calls return same context", func(t *testing.T) {
|
||||
ctx := TestContext{Value: "shared"}
|
||||
|
||||
r1, err1 := runEffect(Ask[TestContext](), ctx)
|
||||
r2, err2 := runEffect(Ask[TestContext](), ctx)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, "Has deadline: true", result2)
|
||||
assert.Equal(t, r1, r2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -612,3 +612,50 @@ func ChainReaderIOK[C, A, B any](f readerio.Kleisli[C, A, B]) Operator[C, A, B]
|
||||
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
|
||||
return readerreaderioresult.Read[A](c)
|
||||
}
|
||||
|
||||
// Asks creates an Effect that projects a value from the context using a Reader function.
|
||||
// This is useful for extracting specific fields or computing derived values from the context.
|
||||
// It's essentially a lifted version of the Reader pattern into the Effect context.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type
|
||||
// - A: The type of the projected value
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - r: A Reader function that extracts or computes a value from the context
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, A]: An effect that succeeds with the projected value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// type Config struct {
|
||||
// Host string
|
||||
// Port int
|
||||
// }
|
||||
//
|
||||
// // Extract a specific field
|
||||
// getHost := effect.Asks[Config](func(cfg Config) string {
|
||||
// return cfg.Host
|
||||
// })
|
||||
//
|
||||
// // Compute a derived value
|
||||
// getURL := effect.Asks[Config](func(cfg Config) string {
|
||||
// return fmt.Sprintf("http://%s:%d", cfg.Host, cfg.Port)
|
||||
// })
|
||||
//
|
||||
// result, err := runEffect(getHost, Config{Host: "localhost", Port: 8080})
|
||||
// // result == "localhost", err == nil
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Ask: Returns the entire context as the value
|
||||
// - Map: Transforms the value after extraction
|
||||
//
|
||||
//go:inline
|
||||
func Asks[C, A any](r Reader[C, A]) Effect[C, A] {
|
||||
return readerreaderioresult.Asks(r)
|
||||
}
|
||||
|
||||
@@ -677,3 +677,411 @@ func TestChainThunkK_Integration(t *testing.T) {
|
||||
assert.Equal(t, result.Of("Value: 100"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_Success(t *testing.T) {
|
||||
t.Run("extracts a field from context", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
getHost := Asks[Config](func(cfg Config) string {
|
||||
return cfg.Host
|
||||
})
|
||||
|
||||
result, err := runEffect(getHost, Config{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "localhost", result)
|
||||
})
|
||||
|
||||
t.Run("extracts multiple fields and computes derived value", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
getURL := Asks[Config](func(cfg Config) string {
|
||||
return fmt.Sprintf("http://%s:%d", cfg.Host, cfg.Port)
|
||||
})
|
||||
|
||||
result, err := runEffect(getURL, Config{Host: "example.com", Port: 443})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "http://example.com:443", result)
|
||||
})
|
||||
|
||||
t.Run("extracts numeric field", func(t *testing.T) {
|
||||
getPort := Asks[TestConfig](func(cfg TestConfig) int {
|
||||
return cfg.Multiplier
|
||||
})
|
||||
|
||||
result, err := runEffect(getPort, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, result)
|
||||
})
|
||||
|
||||
t.Run("computes value from context", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
getArea := Asks[Config](func(cfg Config) int {
|
||||
return cfg.Width * cfg.Height
|
||||
})
|
||||
|
||||
result, err := runEffect(getArea, Config{Width: 10, Height: 20})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, result)
|
||||
})
|
||||
|
||||
t.Run("transforms string field", func(t *testing.T) {
|
||||
getUpperPrefix := Asks[TestConfig](func(cfg TestConfig) string {
|
||||
return fmt.Sprintf("[%s]", cfg.Prefix)
|
||||
})
|
||||
|
||||
result, err := runEffect(getUpperPrefix, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[LOG]", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero values", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
getValue := Asks[Config](func(cfg Config) int {
|
||||
return cfg.Value
|
||||
})
|
||||
|
||||
result, err := runEffect(getValue, Config{Value: 0})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("handles empty string", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
getName := Asks[Config](func(cfg Config) string {
|
||||
return cfg.Name
|
||||
})
|
||||
|
||||
result, err := runEffect(getName, Config{Name: ""})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("handles nil pointer fields", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Data *string
|
||||
}
|
||||
|
||||
hasData := Asks[Config](func(cfg Config) bool {
|
||||
return cfg.Data != nil
|
||||
})
|
||||
|
||||
result, err := runEffect(hasData, Config{Data: nil})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("handles complex nested structures", func(t *testing.T) {
|
||||
type Database struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
type Config struct {
|
||||
DB Database
|
||||
}
|
||||
|
||||
getDBHost := Asks[Config](func(cfg Config) string {
|
||||
return cfg.DB.Host
|
||||
})
|
||||
|
||||
result, err := runEffect(getDBHost, Config{
|
||||
DB: Database{Host: "db.example.com", Port: 5432},
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "db.example.com", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_Integration(t *testing.T) {
|
||||
t.Run("composes with Map", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks[Config](func(cfg Config) int {
|
||||
return cfg.Value
|
||||
}),
|
||||
Map[Config](func(x int) int { return x * 2 }),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{Value: 21})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks[Config](func(cfg Config) int {
|
||||
return cfg.Multiplier
|
||||
}),
|
||||
Chain(func(mult int) Effect[Config, int] {
|
||||
return Of[Config](mult * 10)
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{Multiplier: 5})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 50, result)
|
||||
})
|
||||
|
||||
t.Run("composes with ChainReaderK", func(t *testing.T) {
|
||||
computation := F.Pipe1(
|
||||
Asks[TestConfig](func(cfg TestConfig) int {
|
||||
return cfg.Multiplier
|
||||
}),
|
||||
ChainReaderK(func(mult int) reader.Reader[TestConfig, int] {
|
||||
return func(cfg TestConfig) int {
|
||||
return mult + len(cfg.Prefix)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 6, result) // 3 + len("LOG")
|
||||
})
|
||||
|
||||
t.Run("composes with ChainReaderIOK", func(t *testing.T) {
|
||||
log := []string{}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Asks[TestConfig](func(cfg TestConfig) string {
|
||||
return cfg.Prefix
|
||||
}),
|
||||
ChainReaderIOK(func(prefix string) readerio.ReaderIO[TestConfig, string] {
|
||||
return func(cfg TestConfig) io.IO[string] {
|
||||
return func() string {
|
||||
log = append(log, "executed")
|
||||
return fmt.Sprintf("%s:%d", prefix, cfg.Multiplier)
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, testConfig)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG:3", result)
|
||||
assert.Equal(t, 1, len(log))
|
||||
})
|
||||
|
||||
t.Run("multiple Asks in sequence", func(t *testing.T) {
|
||||
type Config struct {
|
||||
First string
|
||||
Second string
|
||||
}
|
||||
|
||||
computation := F.Pipe2(
|
||||
Asks[Config](func(cfg Config) string {
|
||||
return cfg.First
|
||||
}),
|
||||
Chain(func(_ string) Effect[Config, string] {
|
||||
return Asks[Config](func(cfg Config) string {
|
||||
return cfg.Second
|
||||
})
|
||||
}),
|
||||
Map[Config](func(s string) string {
|
||||
return "Result: " + s
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{First: "A", Second: "B"})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "Result: B", result)
|
||||
})
|
||||
|
||||
t.Run("Asks combined with Ask", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
computation := F.Pipe1(
|
||||
Ask[Config](),
|
||||
Chain(func(cfg Config) Effect[Config, int] {
|
||||
return Asks[Config](func(c Config) int {
|
||||
return c.Value * 2
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
result, err := runEffect(computation, Config{Value: 15})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_Comparison(t *testing.T) {
|
||||
t.Run("Asks vs Ask with Map", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Port int
|
||||
}
|
||||
|
||||
// Using Asks
|
||||
asksVersion := Asks[Config](func(cfg Config) int {
|
||||
return cfg.Port
|
||||
})
|
||||
|
||||
// Using Ask + Map
|
||||
askMapVersion := F.Pipe1(
|
||||
Ask[Config](),
|
||||
Map[Config](func(cfg Config) int {
|
||||
return cfg.Port
|
||||
}),
|
||||
)
|
||||
|
||||
cfg := Config{Port: 8080}
|
||||
|
||||
result1, err1 := runEffect(asksVersion, cfg)
|
||||
result2, err2 := runEffect(askMapVersion, cfg)
|
||||
|
||||
assert.NoError(t, err1)
|
||||
assert.NoError(t, err2)
|
||||
assert.Equal(t, result1, result2)
|
||||
assert.Equal(t, 8080, result1)
|
||||
})
|
||||
|
||||
t.Run("Asks is more concise than Ask + Map", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// Asks is more direct for field extraction
|
||||
getHost := Asks[Config](func(cfg Config) string {
|
||||
return cfg.Host
|
||||
})
|
||||
|
||||
result, err := runEffect(getHost, Config{Host: "api.example.com", Port: 443})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "api.example.com", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAsks_RealWorldScenarios(t *testing.T) {
|
||||
t.Run("extract database connection string", func(t *testing.T) {
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Database string
|
||||
User string
|
||||
}
|
||||
|
||||
getConnectionString := Asks[DatabaseConfig](func(cfg DatabaseConfig) string {
|
||||
return fmt.Sprintf("postgres://%s@%s:%d/%s",
|
||||
cfg.User, cfg.Host, cfg.Port, cfg.Database)
|
||||
})
|
||||
|
||||
result, err := runEffect(getConnectionString, DatabaseConfig{
|
||||
Host: "localhost",
|
||||
Port: 5432,
|
||||
Database: "myapp",
|
||||
User: "admin",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "postgres://admin@localhost:5432/myapp", result)
|
||||
})
|
||||
|
||||
t.Run("compute API endpoint from config", func(t *testing.T) {
|
||||
type APIConfig struct {
|
||||
Protocol string
|
||||
Host string
|
||||
Port int
|
||||
BasePath string
|
||||
}
|
||||
|
||||
getEndpoint := Asks[APIConfig](func(cfg APIConfig) string {
|
||||
return fmt.Sprintf("%s://%s:%d%s",
|
||||
cfg.Protocol, cfg.Host, cfg.Port, cfg.BasePath)
|
||||
})
|
||||
|
||||
result, err := runEffect(getEndpoint, APIConfig{
|
||||
Protocol: "https",
|
||||
Host: "api.example.com",
|
||||
Port: 443,
|
||||
BasePath: "/v1",
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://api.example.com:443/v1", result)
|
||||
})
|
||||
|
||||
t.Run("validate configuration", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Timeout int
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
isValid := Asks[Config](func(cfg Config) bool {
|
||||
return cfg.Timeout > 0 && cfg.MaxRetries >= 0
|
||||
})
|
||||
|
||||
// Valid config
|
||||
result1, err1 := runEffect(isValid, Config{Timeout: 30, MaxRetries: 3})
|
||||
assert.NoError(t, err1)
|
||||
assert.True(t, result1)
|
||||
|
||||
// Invalid config
|
||||
result2, err2 := runEffect(isValid, Config{Timeout: 0, MaxRetries: 3})
|
||||
assert.NoError(t, err2)
|
||||
assert.False(t, result2)
|
||||
})
|
||||
|
||||
t.Run("extract feature flags", func(t *testing.T) {
|
||||
type FeatureFlags struct {
|
||||
EnableNewUI bool
|
||||
EnableBetaAPI bool
|
||||
EnableAnalytics bool
|
||||
}
|
||||
|
||||
hasNewUI := Asks[FeatureFlags](func(flags FeatureFlags) bool {
|
||||
return flags.EnableNewUI
|
||||
})
|
||||
|
||||
result, err := runEffect(hasNewUI, FeatureFlags{
|
||||
EnableNewUI: true,
|
||||
EnableBetaAPI: false,
|
||||
EnableAnalytics: true,
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
86
v2/effect/profunctor.go
Normal file
86
v2/effect/profunctor.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
|
||||
// Promap is the profunctor map operation that transforms both the input and output of an Effect.
|
||||
// It applies f to the input context (contravariantly) and g to the output value (covariantly).
|
||||
//
|
||||
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
|
||||
//
|
||||
// This operation allows you to:
|
||||
// - Modify the context before passing it to the Effect (via f)
|
||||
// - Transform the success value after the computation completes (via g)
|
||||
//
|
||||
// Promap is particularly useful for adapting effects to work with different context types
|
||||
// while simultaneously transforming their output values.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - E: The original context type expected by the Effect
|
||||
// - A: The original success type produced by the Effect
|
||||
// - D: The new input context type
|
||||
// - B: The new output success type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: Function to transform the input context from D to E (contravariant)
|
||||
// - g: Function to transform the output success value from A to B (covariant)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Kleisli arrow that takes an Effect[E, A] and returns a function from D to B
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// APIKey string
|
||||
// }
|
||||
//
|
||||
// type DBConfig struct {
|
||||
// URL string
|
||||
// }
|
||||
//
|
||||
// // Effect that uses DBConfig and returns an int
|
||||
// getUserCount := func(cfg DBConfig) effect.Effect[context.Context, int] {
|
||||
// return effect.Succeed[context.Context](42)
|
||||
// }
|
||||
//
|
||||
// // Transform AppConfig to DBConfig
|
||||
// extractDBConfig := func(app AppConfig) DBConfig {
|
||||
// return DBConfig{URL: app.DatabaseURL}
|
||||
// }
|
||||
//
|
||||
// // Transform int to string
|
||||
// formatCount := func(count int) string {
|
||||
// return fmt.Sprintf("Users: %d", count)
|
||||
// }
|
||||
//
|
||||
// // Adapt the effect to work with AppConfig and return string
|
||||
// adapted := effect.Promap(extractDBConfig, formatCount)(getUserCount)
|
||||
// result := adapted(AppConfig{DatabaseURL: "localhost:5432", APIKey: "secret"})
|
||||
//
|
||||
//go:inline
|
||||
func Promap[E, A, D, B any](f Reader[D, E], g Reader[A, B]) Kleisli[D, Effect[E, A], B] {
|
||||
return F.Flow2(
|
||||
Local[A](f),
|
||||
Map[D](g),
|
||||
)
|
||||
}
|
||||
373
v2/effect/profunctor_test.go
Normal file
373
v2/effect/profunctor_test.go
Normal file
@@ -0,0 +1,373 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
R "github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for profunctor tests
|
||||
type AppConfig struct {
|
||||
DatabaseURL string
|
||||
APIKey string
|
||||
Port int
|
||||
}
|
||||
|
||||
type DBConfig struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
// TestPromapBasic tests basic Promap functionality
|
||||
func TestPromapBasic(t *testing.T) {
|
||||
t.Run("transform both context and output", func(t *testing.T) {
|
||||
// Effect that uses DBConfig and returns an int
|
||||
getUserCount := Succeed[DBConfig](42)
|
||||
|
||||
// Transform AppConfig to DBConfig
|
||||
extractDBConfig := func(app AppConfig) DBConfig {
|
||||
return DBConfig{URL: app.DatabaseURL}
|
||||
}
|
||||
|
||||
// Transform int to string
|
||||
formatCount := func(count int) string {
|
||||
return fmt.Sprintf("Users: %d", count)
|
||||
}
|
||||
|
||||
// Adapt the effect to work with AppConfig and return string
|
||||
adapted := Promap(extractDBConfig, formatCount)(getUserCount)
|
||||
result := adapted(AppConfig{
|
||||
DatabaseURL: "localhost:5432",
|
||||
APIKey: "secret",
|
||||
Port: 8080,
|
||||
})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Users: 42"), result)
|
||||
})
|
||||
|
||||
t.Run("identity transformations", func(t *testing.T) {
|
||||
// Effect that returns a value
|
||||
getValue := Succeed[DBConfig](100)
|
||||
|
||||
// Identity transformations
|
||||
identity := func(x DBConfig) DBConfig { return x }
|
||||
identityInt := func(x int) int { return x }
|
||||
|
||||
// Apply identity transformations
|
||||
adapted := Promap(identity, identityInt)(getValue)
|
||||
result := adapted(DBConfig{URL: "localhost"})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(100), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapComposition tests that Promap composes correctly
|
||||
func TestPromapComposition(t *testing.T) {
|
||||
t.Run("compose multiple transformations", func(t *testing.T) {
|
||||
// Effect that uses ServerConfig and returns the port
|
||||
getPort := Map[ServerConfig](func(cfg ServerConfig) int {
|
||||
return cfg.Port
|
||||
})(Ask[ServerConfig]())
|
||||
|
||||
// First transformation: AppConfig -> ServerConfig
|
||||
extractServerConfig := func(app AppConfig) ServerConfig {
|
||||
return ServerConfig{Host: "localhost", Port: app.Port}
|
||||
}
|
||||
|
||||
// Second transformation: int -> string
|
||||
formatPort := func(port int) string {
|
||||
return fmt.Sprintf(":%d", port)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(extractServerConfig, formatPort)(getPort)
|
||||
result := adapted(AppConfig{
|
||||
DatabaseURL: "db.example.com",
|
||||
APIKey: "key123",
|
||||
Port: 9000,
|
||||
})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(":9000"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithErrors tests Promap with effects that can fail
|
||||
func TestPromapWithErrors(t *testing.T) {
|
||||
t.Run("propagates errors correctly", func(t *testing.T) {
|
||||
// Effect that fails
|
||||
failingEffect := Fail[DBConfig, int](fmt.Errorf("database connection failed"))
|
||||
|
||||
// Transformations
|
||||
extractDBConfig := func(app AppConfig) DBConfig {
|
||||
return DBConfig{URL: app.DatabaseURL}
|
||||
}
|
||||
formatCount := func(count int) string {
|
||||
return fmt.Sprintf("Count: %d", count)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(extractDBConfig, formatCount)(failingEffect)
|
||||
result := adapted(AppConfig{DatabaseURL: "localhost"})(context.Background())()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
err := R.MonadFold(result,
|
||||
func(e error) error { return e },
|
||||
func(string) error { return nil },
|
||||
)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "database connection failed")
|
||||
})
|
||||
|
||||
t.Run("output transformation not applied on error", func(t *testing.T) {
|
||||
callCount := 0
|
||||
|
||||
// Effect that fails
|
||||
failingEffect := Fail[DBConfig, int](fmt.Errorf("error"))
|
||||
|
||||
// Transformation that counts calls
|
||||
countingTransform := func(x int) string {
|
||||
callCount++
|
||||
return strconv.Itoa(x)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(
|
||||
func(app AppConfig) DBConfig { return DBConfig{URL: app.DatabaseURL} },
|
||||
countingTransform,
|
||||
)(failingEffect)
|
||||
result := adapted(AppConfig{DatabaseURL: "localhost"})(context.Background())()
|
||||
|
||||
assert.True(t, R.IsLeft(result))
|
||||
assert.Equal(t, 0, callCount, "output transformation should not be called on error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapWithComplexTypes tests Promap with more complex type transformations
|
||||
func TestPromapWithComplexTypes(t *testing.T) {
|
||||
t.Run("transform struct to different struct", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
type UserDTO struct {
|
||||
UserID int
|
||||
FullName string
|
||||
}
|
||||
|
||||
// Effect that uses User and returns a string
|
||||
getUserInfo := Map[User](func(user User) string {
|
||||
return fmt.Sprintf("User %s (ID: %d)", user.Name, user.ID)
|
||||
})(Ask[User]())
|
||||
|
||||
// Transform UserDTO to User
|
||||
dtoToUser := func(dto UserDTO) User {
|
||||
return User{ID: dto.UserID, Name: dto.FullName}
|
||||
}
|
||||
|
||||
// Transform string to uppercase
|
||||
toUpper := func(s string) string {
|
||||
return fmt.Sprintf("INFO: %s", s)
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
adapted := Promap(dtoToUser, toUpper)(getUserInfo)
|
||||
result := adapted(UserDTO{UserID: 123, FullName: "Alice"})(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("INFO: User Alice (ID: 123)"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapChaining tests chaining multiple Promap operations
|
||||
func TestPromapChaining(t *testing.T) {
|
||||
t.Run("chain multiple Promap operations", func(t *testing.T) {
|
||||
// Base effect that doubles the input
|
||||
baseEffect := Map[int](func(x int) int {
|
||||
return x * 2
|
||||
})(Ask[int]())
|
||||
|
||||
// First Promap: string -> int, int -> string
|
||||
step1 := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
strconv.Itoa,
|
||||
)(baseEffect)
|
||||
|
||||
// Second Promap: float64 -> string, string -> float64
|
||||
step2 := Promap(
|
||||
func(f float64) string {
|
||||
return fmt.Sprintf("%.0f", f)
|
||||
},
|
||||
func(s string) float64 {
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
return f
|
||||
},
|
||||
)(step1)
|
||||
|
||||
result := step2(21.0)(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(42.0), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapEdgeCases tests edge cases
|
||||
func TestPromapEdgeCases(t *testing.T) {
|
||||
t.Run("zero values", func(t *testing.T) {
|
||||
effect := Map[int](func(x int) int {
|
||||
return x
|
||||
})(Ask[int]())
|
||||
|
||||
adapted := Promap(
|
||||
func(s string) int { return 0 },
|
||||
func(x int) string { return "" },
|
||||
)(effect)
|
||||
|
||||
result := adapted("anything")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of(""), result)
|
||||
})
|
||||
|
||||
t.Run("nil context handling", func(t *testing.T) {
|
||||
effect := Succeed[int]("success")
|
||||
|
||||
adapted := Promap(
|
||||
func(s string) int { return 42 },
|
||||
func(s string) string { return s + "!" },
|
||||
)(effect)
|
||||
|
||||
// Using background context instead of nil
|
||||
result := adapted("test")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("success!"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPromapIntegration tests integration with other effect operations
|
||||
func TestPromapIntegration(t *testing.T) {
|
||||
t.Run("Promap with Map", func(t *testing.T) {
|
||||
// Base effect that adds 10
|
||||
baseEffect := Map[int](func(x int) int {
|
||||
return x + 10
|
||||
})(Ask[int]())
|
||||
|
||||
// Apply Promap
|
||||
promapped := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
func(x int) int { return x * 2 },
|
||||
)(baseEffect)
|
||||
|
||||
// Apply Map on top
|
||||
mapped := Map[string](func(x int) string {
|
||||
return fmt.Sprintf("Result: %d", x)
|
||||
})(promapped)
|
||||
|
||||
result := mapped("5")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Result: 30"), result)
|
||||
})
|
||||
|
||||
t.Run("Promap with Chain", func(t *testing.T) {
|
||||
// Base effect
|
||||
baseEffect := Ask[int]()
|
||||
|
||||
// Apply Promap
|
||||
promapped := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
func(x int) int { return x * 2 },
|
||||
)(baseEffect)
|
||||
|
||||
// Chain with another effect
|
||||
chained := Chain(func(x int) Effect[string, string] {
|
||||
return Succeed[string](fmt.Sprintf("Value: %d", x))
|
||||
})(promapped)
|
||||
|
||||
result := chained("10")(context.Background())()
|
||||
|
||||
assert.Equal(t, R.Of("Value: 20"), result)
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkPromap benchmarks the Promap operation
|
||||
func BenchmarkPromap(b *testing.B) {
|
||||
effect := Map[int](func(x int) int {
|
||||
return x * 2
|
||||
})(Ask[int]())
|
||||
|
||||
adapted := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
strconv.Itoa,
|
||||
)(effect)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = adapted("42")(ctx)()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPromapChained benchmarks chained Promap operations
|
||||
func BenchmarkPromapChained(b *testing.B) {
|
||||
baseEffect := Map[int](func(x int) int {
|
||||
return x * 2
|
||||
})(Ask[int]())
|
||||
|
||||
step1 := Promap(
|
||||
func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
},
|
||||
strconv.Itoa,
|
||||
)(baseEffect)
|
||||
|
||||
step2 := Promap(
|
||||
func(f float64) string {
|
||||
return fmt.Sprintf("%.0f", f)
|
||||
},
|
||||
func(s string) float64 {
|
||||
f, _ := strconv.ParseFloat(s, 64)
|
||||
return f
|
||||
},
|
||||
)(step1)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = step2(21.0)(ctx)()
|
||||
}
|
||||
}
|
||||
195
v2/iooption/array_test.go
Normal file
195
v2/iooption/array_test.go
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
433
v2/iooption/iooption_comprehensive_test.go
Normal file
433
v2/iooption/iooption_comprehensive_test.go
Normal 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
241
v2/ioresult/bracket_test.go
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
581
v2/ioresult/ioresult_comprehensive_test.go
Normal file
581
v2/ioresult/ioresult_comprehensive_test.go
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,14 @@ func TestFormatterInterface(t *testing.T) {
|
||||
result := fmt.Sprintf("%q", tramp)
|
||||
assert.Equal(t, "\"Bounce(42)\"", result)
|
||||
})
|
||||
|
||||
t.Run("unknown verb format", func(t *testing.T) {
|
||||
tramp := Bounce[string](42)
|
||||
result := fmt.Sprintf("%x", tramp)
|
||||
assert.Contains(t, result, "%!x")
|
||||
assert.Contains(t, result, "Trampoline[B, L]")
|
||||
assert.Contains(t, result, "Bounce(42)")
|
||||
})
|
||||
}
|
||||
|
||||
// TestGoStringerInterface verifies fmt.GoStringer implementation
|
||||
|
||||
@@ -22,14 +22,18 @@ import (
|
||||
// LogValue implements the slog.LogValuer interface for Trampoline.
|
||||
//
|
||||
// This method allows Trampoline values to be logged using Go's structured logging
|
||||
// (log/slog) with proper representation of their state:
|
||||
// with proper representation of their state:
|
||||
// - When Landed is true: returns a group with a single "landed" attribute containing the Land value
|
||||
// - When Landed is false: returns a group with a single "bouncing" attribute containing the Bounce value
|
||||
//
|
||||
// The implementation ensures that Trampoline values are logged in a structured,
|
||||
// readable format that clearly shows the current state of the tail-recursive computation.
|
||||
//
|
||||
// Example usage:
|
||||
// # Returns
|
||||
//
|
||||
// - slog.Value: A structured log value representing the trampoline state
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// trampoline := tailrec.Bounce[int](42)
|
||||
// slog.Info("Processing", "state", trampoline)
|
||||
|
||||
@@ -8,17 +8,20 @@ import "fmt"
|
||||
// This represents a recursive call in the original algorithm. The computation
|
||||
// will continue by processing the provided state value in the next iteration.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - L: The final result type (land type)
|
||||
// - B: The intermediate state type (bounce type)
|
||||
// # Type Parameters
|
||||
//
|
||||
// - B: The intermediate state type (bounce type)
|
||||
// - L: The final result type (land type)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The new intermediate state to process in the next step
|
||||
//
|
||||
// Returns:
|
||||
// - A Trampoline in the "bounce" state containing the intermediate value
|
||||
// # Returns
|
||||
//
|
||||
// Example:
|
||||
// - Trampoline[B, L]: A Trampoline in the "bounce" state containing the intermediate value
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Countdown that bounces until reaching zero
|
||||
// func countdownStep(n int) Trampoline[int, int] {
|
||||
@@ -40,17 +43,20 @@ func Bounce[L, B any](b B) Trampoline[B, L] {
|
||||
// a Land trampoline is encountered, the executor should stop iterating and
|
||||
// return the final result.
|
||||
//
|
||||
// Type Parameters:
|
||||
// # Type Parameters
|
||||
//
|
||||
// - B: The intermediate state type (bounce type)
|
||||
// - L: The final result type (land type)
|
||||
//
|
||||
// Parameters:
|
||||
// # Parameters
|
||||
//
|
||||
// - l: The final result value
|
||||
//
|
||||
// Returns:
|
||||
// - A Trampoline in the "land" state containing the final result
|
||||
// # Returns
|
||||
//
|
||||
// Example:
|
||||
// - Trampoline[B, L]: A Trampoline in the "land" state containing the final result
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Factorial base case
|
||||
// func factorialStep(state State) Trampoline[State, int] {
|
||||
@@ -66,7 +72,13 @@ func Land[B, L any](l L) Trampoline[B, L] {
|
||||
}
|
||||
|
||||
// String implements fmt.Stringer for Trampoline.
|
||||
//
|
||||
// Returns a human-readable string representation of the trampoline state.
|
||||
// For bounce states, returns "Bounce(value)". For land states, returns "Land(value)".
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - string: A formatted string representation of the trampoline state
|
||||
func (t Trampoline[B, L]) String() string {
|
||||
if t.Landed {
|
||||
return fmt.Sprintf("Land(%v)", t.Land)
|
||||
@@ -75,7 +87,18 @@ func (t Trampoline[B, L]) String() string {
|
||||
}
|
||||
|
||||
// Format implements fmt.Formatter for Trampoline.
|
||||
// Supports various formatting verbs for detailed output.
|
||||
//
|
||||
// Supports various formatting verbs for detailed output:
|
||||
// - %v: Default format (delegates to String)
|
||||
// - %+v: Detailed format with type information
|
||||
// - %#v: Go-syntax representation (delegates to GoString)
|
||||
// - %s: String format
|
||||
// - %q: Quoted string format
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: The format state
|
||||
// - verb: The formatting verb
|
||||
func (t Trampoline[B, L]) Format(f fmt.State, verb rune) {
|
||||
switch verb {
|
||||
case 'v':
|
||||
@@ -106,7 +129,13 @@ func (t Trampoline[B, L]) Format(f fmt.State, verb rune) {
|
||||
}
|
||||
|
||||
// GoString implements fmt.GoStringer for Trampoline.
|
||||
//
|
||||
// Returns a Go-syntax representation that could be used to recreate the value.
|
||||
// The output includes the package name, function name, type parameters, and value.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - string: A Go-syntax representation of the trampoline
|
||||
func (t Trampoline[B, L]) GoString() string {
|
||||
if t.Landed {
|
||||
return fmt.Sprintf("tailrec.Land[%T](%#v)", t.Bounce, t.Land)
|
||||
|
||||
@@ -7,14 +7,21 @@ type (
|
||||
// - Bounce: The computation should continue with a new intermediate state (type B)
|
||||
// - Land: The computation is complete with a final result (type L)
|
||||
//
|
||||
// Type Parameters:
|
||||
// # Type Parameters
|
||||
//
|
||||
// - B: The "bounce" type - intermediate state passed between recursive steps
|
||||
// - L: The "land" type - the final result type when computation completes
|
||||
//
|
||||
// The trampoline pattern allows converting recursive algorithms into iterative ones,
|
||||
// preventing stack overflow for deep recursion while maintaining code clarity.
|
||||
//
|
||||
// Example:
|
||||
// # Design Note
|
||||
//
|
||||
// This type uses a struct with a boolean flag rather than the Either type to avoid
|
||||
// a cyclic dependency. The either package depends on tailrec for its own tail-recursive
|
||||
// operations, so using Either here would create a circular import.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// // Factorial using trampolines
|
||||
// type State struct { n, acc int }
|
||||
|
||||
Reference in New Issue
Block a user