1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-03-10 13:31:01 +02:00

Compare commits

..

9 Commits

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

View File

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

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

@@ -0,0 +1,130 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package reader provides a specialization of the Reader monad for [context.Context].
//
// This package offers a context-aware Reader monad that simplifies working with
// Go's [context.Context] in a functional programming style. It eliminates the need
// to explicitly thread context through function calls while maintaining type safety
// and composability.
//
// # Core Concept
//
// The Reader monad represents computations that depend on a shared environment.
// In this package, that environment is fixed to [context.Context], making it
// particularly useful for:
//
// - Request-scoped data propagation
// - Cancellation and timeout handling
// - Dependency injection via context values
// - Avoiding explicit context parameter threading
//
// # Type Definitions
//
// - Reader[A]: A computation that depends on context.Context and produces A
// - Kleisli[A, B]: A function from A to Reader[B] for composing computations
// - Operator[A, B]: A transformation from Reader[A] to Reader[B]
//
// # Usage Pattern
//
// Instead of passing context explicitly through every function:
//
// func processUser(ctx context.Context, userID string) (User, error) {
// user := fetchUser(ctx, userID)
// profile := fetchProfile(ctx, user.ProfileID)
// return enrichUser(ctx, user, profile), nil
// }
//
// You can use Reader to compose context-dependent operations:
//
// fetchUser := func(userID string) Reader[User] {
// return func(ctx context.Context) User {
// // Use ctx for database access, cancellation, etc.
// return queryDatabase(ctx, userID)
// }
// }
//
// processUser := func(userID string) Reader[User] {
// return F.Pipe2(
// fetchUser(userID),
// reader.Chain(func(user User) Reader[Profile] {
// return fetchProfile(user.ProfileID)
// }),
// reader.Map(func(profile Profile) User {
// return enrichUser(user, profile)
// }),
// )
// }
//
// // Execute with context
// ctx := context.Background()
// user := processUser("user123")(ctx)
//
// # Integration with Standard Library
//
// This package works seamlessly with Go's standard [context] package:
//
// - Context cancellation and deadlines are preserved
// - Context values can be accessed within Reader computations
// - Readers can be composed with context-aware libraries
//
// # Relationship to Other Packages
//
// This package is a specialization of [github.com/IBM/fp-go/v2/reader] where
// the environment type R is fixed to [context.Context]. For more general
// Reader operations, see the base reader package.
//
// For combining Reader with other monads:
// - [github.com/IBM/fp-go/v2/context/readerio]: Reader + IO effects
// - [github.com/IBM/fp-go/v2/readeroption]: Reader + Option
// - [github.com/IBM/fp-go/v2/readerresult]: Reader + Result (Either)
//
// # Example: HTTP Request Handler
//
// type RequestContext struct {
// UserID string
// RequestID string
// }
//
// // Extract request context from context.Context
// getRequestContext := func(ctx context.Context) RequestContext {
// return RequestContext{
// UserID: ctx.Value("userID").(string),
// RequestID: ctx.Value("requestID").(string),
// }
// }
//
// // A Reader that logs with request context
// logInfo := func(message string) Reader[function.Void] {
// return func(ctx context.Context) function.Void {
// reqCtx := getRequestContext(ctx)
// log.Printf("[%s] User %s: %s", reqCtx.RequestID, reqCtx.UserID, message)
// return function.VOID
// }
// }
//
// // Compose operations
// handleRequest := func(data string) Reader[Response] {
// return F.Pipe2(
// logInfo("Processing request"),
// reader.Chain(func(_ function.Void) Reader[Result] {
// return processData(data)
// }),
// reader.Map(func(result Result) Response {
// return Response{Data: result}
// }),
// )
// }
package reader

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

@@ -0,0 +1,142 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package reader
import (
"context"
R "github.com/IBM/fp-go/v2/reader"
)
type (
// Reader represents a computation that depends on a [context.Context] and produces a value of type A.
//
// This is a specialization of the generic Reader monad where the environment type is fixed
// to [context.Context]. This is particularly useful for Go applications that need to thread
// context through computations for cancellation, deadlines, and request-scoped values.
//
// Type Parameters:
// - A: The result type produced by the computation
//
// Reader[A] is equivalent to func(context.Context) A
//
// The Reader monad enables:
// - Dependency injection using context values
// - Cancellation and timeout handling
// - Request-scoped data propagation
// - Avoiding explicit context parameter threading
//
// Example:
//
// // A Reader that extracts a user ID from context
// getUserID := func(ctx context.Context) string {
// if userID, ok := ctx.Value("userID").(string); ok {
// return userID
// }
// return "anonymous"
// }
//
// // A Reader that checks if context is cancelled
// isCancelled := func(ctx context.Context) bool {
// select {
// case <-ctx.Done():
// return true
// default:
// return false
// }
// }
//
// // Use the readers with a context
// ctx := context.WithValue(context.Background(), "userID", "user123")
// userID := getUserID(ctx) // "user123"
// cancelled := isCancelled(ctx) // false
Reader[A any] = R.Reader[context.Context, A]
// Kleisli represents a Kleisli arrow for the context-based Reader monad.
//
// It's a function from A to Reader[B], used for composing Reader computations
// that all depend on the same [context.Context].
//
// Type Parameters:
// - A: The input type
// - B: The output type wrapped in Reader
//
// Kleisli[A, B] is equivalent to func(A) func(context.Context) B
//
// Kleisli arrows are fundamental for monadic composition, allowing you to chain
// operations that depend on context without explicitly passing the context through
// each function call.
//
// Example:
//
// // A Kleisli arrow that creates a greeting Reader from a name
// greet := func(name string) Reader[string] {
// return func(ctx context.Context) string {
// if deadline, ok := ctx.Deadline(); ok {
// return fmt.Sprintf("Hello %s (deadline: %v)", name, deadline)
// }
// return fmt.Sprintf("Hello %s", name)
// }
// }
//
// // Use the Kleisli arrow
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// defer cancel()
// greeting := greet("Alice")(ctx) // "Hello Alice (deadline: ...)"
Kleisli[A, B any] = R.Reader[A, Reader[B]]
// Operator represents a transformation from one Reader to another.
//
// It takes a Reader[A] and produces a Reader[B], where both readers depend on
// the same [context.Context]. This type is commonly used for operations like
// Map, Chain, and other transformations that convert readers while preserving
// the context dependency.
//
// Type Parameters:
// - A: The input Reader's result type
// - B: The output Reader's result type
//
// Operator[A, B] is equivalent to func(Reader[A]) func(context.Context) B
//
// Operators enable building pipelines of context-dependent computations where
// each step can transform the result of the previous computation while maintaining
// access to the shared context.
//
// Example:
//
// // An operator that transforms int readers to string readers
// intToString := func(r Reader[int]) Reader[string] {
// return func(ctx context.Context) string {
// value := r(ctx)
// return strconv.Itoa(value)
// }
// }
//
// // A Reader that extracts a timeout value from context
// getTimeout := func(ctx context.Context) int {
// if deadline, ok := ctx.Deadline(); ok {
// return int(time.Until(deadline).Seconds())
// }
// return 0
// }
//
// // Transform the Reader
// getTimeoutStr := intToString(getTimeout)
// ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
// defer cancel()
// result := getTimeoutStr(ctx) // "30" (approximately)
Operator[A, B any] = Kleisli[Reader[A], B]
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,7 +40,7 @@
// increment := N.Add(1)
//
// // Compose them (RIGHT-TO-LEFT execution)
// composed := endomorphism.Compose(double, increment)
// composed := endomorphism.MonadCompose(double, increment)
// result := composed(5) // increment(5) then double: (5 + 1) * 2 = 12
//
// // Chain them (LEFT-TO-RIGHT execution)
@@ -61,11 +61,11 @@
// monoid := endomorphism.Monoid[int]()
//
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(
// combined := M.ConcatAll(monoid)([]endomorphism.Endomorphism[int]{
// N.Mul(2), // applied third
// N.Add(1), // applied second
// N.Mul(3), // applied first
// )
// })
// result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
//
// # Monad Operations
@@ -87,7 +87,7 @@
// increment := N.Add(1)
//
// // Compose: RIGHT-TO-LEFT (mathematical composition)
// composed := endomorphism.Compose(double, increment)
// composed := endomorphism.MonadCompose(double, increment)
// result1 := composed(5) // increment(5) * 2 = (5 + 1) * 2 = 12
//
// // MonadChain: LEFT-TO-RIGHT (sequential application)

View File

@@ -111,15 +111,19 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
// This is the functor map operation for endomorphisms.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - g is applied first to the input
// - ma is applied first to the input
// - f is applied to the result
//
// Note: unlike most other packages where MonadMap takes (fa, f) with the container
// first, here f (the morphism) comes first to match the right-to-left composition
// convention: MonadMap(f, ma) = f ∘ ma.
//
// Parameters:
// - f: The function to map (outer function)
// - g: The endomorphism to map over (inner function)
// - f: The function to map (outer function, applied second)
// - ma: The endomorphism to map over (inner function, applied first)
//
// Returns:
// - A new endomorphism that applies g, then f
// - A new endomorphism that applies ma, then f
//
// Example:
//
@@ -127,8 +131,8 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
// increment := N.Add(1)
// mapped := endomorphism.MonadMap(double, increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
return MonadCompose(f, g)
func MonadMap[A any](f, ma Endomorphism[A]) Endomorphism[A] {
return MonadCompose(f, ma)
}
// Compose returns a function that composes an endomorphism with another, executing right to left.
@@ -386,3 +390,91 @@ func Join[A any](f Kleisli[A]) Endomorphism[A] {
return f(a)(a)
}
}
// Read captures a value and returns a function that applies endomorphisms to it.
//
// This function implements a "reader" pattern for endomorphisms. It takes a value
// and returns a function that can apply any endomorphism to that captured value.
// This is useful for creating reusable evaluation contexts where you want to apply
// different transformations to the same initial value.
//
// The returned function has the signature func(Endomorphism[A]) A, which means
// it takes an endomorphism and returns the result of applying that endomorphism
// to the captured value.
//
// # Type Parameters
//
// - A: The type of the value being captured and transformed
//
// # Parameters
//
// - a: The value to capture for later transformation
//
// # Returns
//
// - A function that applies endomorphisms to the captured value
//
// # Example - Basic Usage
//
// // Capture a value
// applyTo5 := Read(5)
//
// // Apply different endomorphisms to the same value
// doubled := applyTo5(N.Mul(2)) // 10
// incremented := applyTo5(N.Add(1)) // 6
// squared := applyTo5(func(x int) int { return x * x }) // 25
//
// # Example - Reusable Evaluation Context
//
// type Config struct {
// Timeout int
// Retries int
// }
//
// baseConfig := Config{Timeout: 30, Retries: 3}
// applyToBase := Read(baseConfig)
//
// // Apply different transformations to the same base config
// withLongTimeout := applyToBase(func(c Config) Config {
// c.Timeout = 60
// return c
// })
//
// withMoreRetries := applyToBase(func(c Config) Config {
// c.Retries = 5
// return c
// })
//
// # Example - Testing Different Transformations
//
// // Useful for testing multiple transformations on the same input
// testValue := "hello"
// applyToTest := Read(testValue)
//
// upperCase := applyToTest(strings.ToUpper) // "HELLO"
// withSuffix := applyToTest(func(s string) string {
// return s + " world"
// }) // "hello world"
//
// # Use Cases
//
// 1. **Testing**: Apply multiple transformations to the same test value
// 2. **Configuration**: Create variations of a base configuration
// 3. **Data Processing**: Evaluate different processing pipelines on the same data
// 4. **Benchmarking**: Compare different endomorphisms on the same input
// 5. **Functional Composition**: Build evaluation contexts for composed operations
//
// # Relationship to Other Functions
//
// Read is complementary to other endomorphism operations:
// - Build applies an endomorphism to the zero value
// - Read applies endomorphisms to a specific captured value
// - Reduce applies multiple endomorphisms sequentially
// - ConcatAll composes multiple endomorphisms
//
//go:inline
func Read[A any](a A) func(Endomorphism[A]) A {
return func(f Endomorphism[A]) A {
return f(a)
}
}

View File

@@ -1071,3 +1071,226 @@ func TestReduceWithBuild(t *testing.T) {
assert.NotEqual(t, reduceResult, buildResult, "Reduce and Build(ConcatAll) produce different results due to execution order")
}
// TestRead tests the Read function
func TestRead(t *testing.T) {
t.Run("applies endomorphism to captured value", func(t *testing.T) {
applyTo5 := Read(5)
result := applyTo5(double)
assert.Equal(t, 10, result, "Read should apply double to captured value 5")
result2 := applyTo5(increment)
assert.Equal(t, 6, result2, "Read should apply increment to captured value 5")
result3 := applyTo5(square)
assert.Equal(t, 25, result3, "Read should apply square to captured value 5")
})
t.Run("captures value for reuse", func(t *testing.T) {
applyTo10 := Read(10)
// Apply multiple different endomorphisms to the same captured value
doubled := applyTo10(double)
incremented := applyTo10(increment)
negated := applyTo10(negate)
assert.Equal(t, 20, doubled, "Should double 10")
assert.Equal(t, 11, incremented, "Should increment 10")
assert.Equal(t, -10, negated, "Should negate 10")
})
t.Run("works with identity", func(t *testing.T) {
applyTo42 := Read(42)
result := applyTo42(Identity[int]())
assert.Equal(t, 42, result, "Read with identity should return original value")
})
t.Run("works with composed endomorphisms", func(t *testing.T) {
applyTo5 := Read(5)
// Compose: double then increment (RIGHT-TO-LEFT)
composed := MonadCompose(increment, double)
result := applyTo5(composed)
assert.Equal(t, 11, result, "Read should work with composed endomorphisms: (5 * 2) + 1 = 11")
})
t.Run("works with chained endomorphisms", func(t *testing.T) {
applyTo5 := Read(5)
// Chain: double then increment (LEFT-TO-RIGHT)
chained := MonadChain(double, increment)
result := applyTo5(chained)
assert.Equal(t, 11, result, "Read should work with chained endomorphisms: (5 * 2) + 1 = 11")
})
t.Run("works with ConcatAll", func(t *testing.T) {
applyTo5 := Read(5)
// ConcatAll composes RIGHT-TO-LEFT
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
result := applyTo5(combined)
// Execution: square(5) = 25, increment(25) = 26, double(26) = 52
assert.Equal(t, 52, result, "Read should work with ConcatAll")
})
t.Run("works with different types", func(t *testing.T) {
// Test with string
applyToHello := Read("hello")
toUpper := func(s string) string { return s + " WORLD" }
result := applyToHello(toUpper)
assert.Equal(t, "hello WORLD", result, "Read should work with strings")
// Test with struct
type Point struct {
X, Y int
}
applyToPoint := Read(Point{X: 3, Y: 4})
scaleX := func(p Point) Point {
p.X *= 2
return p
}
result2 := applyToPoint(scaleX)
assert.Equal(t, Point{X: 6, Y: 4}, result2, "Read should work with structs")
})
t.Run("creates independent evaluation contexts", func(t *testing.T) {
applyTo5 := Read(5)
applyTo10 := Read(10)
// Same endomorphism, different contexts
result5 := applyTo5(double)
result10 := applyTo10(double)
assert.Equal(t, 10, result5, "First context should double 5")
assert.Equal(t, 20, result10, "Second context should double 10")
})
t.Run("useful for testing transformations", func(t *testing.T) {
testValue := 100
applyToTest := Read(testValue)
// Test multiple transformations on the same value
transformations := []struct {
name string
endo Endomorphism[int]
expected int
}{
{"double", double, 200},
{"increment", increment, 101},
{"negate", negate, -100},
{"square", square, 10000},
}
for _, tt := range transformations {
t.Run(tt.name, func(t *testing.T) {
result := applyToTest(tt.endo)
assert.Equal(t, tt.expected, result)
})
}
})
t.Run("works with monoid operations", func(t *testing.T) {
applyTo5 := Read(5)
// Use monoid to combine endomorphisms
combined := M.ConcatAll(Monoid[int]())([]Endomorphism[int]{
double,
increment,
})
result := applyTo5(combined)
// RIGHT-TO-LEFT: increment(5) = 6, double(6) = 12
assert.Equal(t, 12, result, "Read should work with monoid operations")
})
t.Run("configuration example", func(t *testing.T) {
type Config struct {
Timeout int
Retries int
}
baseConfig := Config{Timeout: 30, Retries: 3}
applyToBase := Read(baseConfig)
withLongTimeout := func(c Config) Config {
c.Timeout = 60
return c
}
withMoreRetries := func(c Config) Config {
c.Retries = 5
return c
}
result1 := applyToBase(withLongTimeout)
assert.Equal(t, Config{Timeout: 60, Retries: 3}, result1)
result2 := applyToBase(withMoreRetries)
assert.Equal(t, Config{Timeout: 30, Retries: 5}, result2)
// Original is unchanged
result3 := applyToBase(Identity[Config]())
assert.Equal(t, baseConfig, result3)
})
}
// TestReadWithBuild tests the relationship between Read and Build
func TestReadWithBuild(t *testing.T) {
t.Run("Read applies to specific value, Build to zero value", func(t *testing.T) {
endo := double
// Build applies to zero value
builtResult := Build(endo)
assert.Equal(t, 0, builtResult, "Build should apply to zero value: 0 * 2 = 0")
// Read applies to specific value
readResult := Read(5)(endo)
assert.Equal(t, 10, readResult, "Read should apply to captured value: 5 * 2 = 10")
})
t.Run("Read can evaluate Build results", func(t *testing.T) {
// Build an endomorphism
builder := ConcatAll([]Endomorphism[int]{double, increment})
// Apply it to zero value
builtValue := Build(builder)
// RIGHT-TO-LEFT: increment(0) = 1, double(1) = 2
assert.Equal(t, 2, builtValue)
// Now use Read to apply the same builder to a different value
readValue := Read(5)(builder)
// RIGHT-TO-LEFT: increment(5) = 6, double(6) = 12
assert.Equal(t, 12, readValue)
})
}
// BenchmarkRead benchmarks the Read function
func BenchmarkRead(b *testing.B) {
applyTo5 := Read(5)
b.Run("simple endomorphism", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = applyTo5(double)
}
})
b.Run("composed endomorphism", func(b *testing.B) {
composed := MonadCompose(double, increment)
for i := 0; i < b.N; i++ {
_ = applyTo5(composed)
}
})
b.Run("ConcatAll endomorphism", func(b *testing.B) {
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
for i := 0; i < b.N; i++ {
_ = applyTo5(combined)
}
})
}

View File

@@ -144,8 +144,8 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// square := func(x int) int { return x * x }
//
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(double, increment, square)
// result := combined(5) // square(increment(double(5))) = square(increment(10)) = square(11) = 121
// combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
// result := combined(5) // double(increment(square(5))) = double(increment(25)) = double(26) = 52
func Monoid[A any]() M.Monoid[Endomorphism[A]] {
return M.MakeMonoid(MonadCompose[A], Identity[A]())
}

View File

@@ -41,20 +41,22 @@ type (
// It's a function from A to Endomorphism[A], used for composing endomorphic operations.
Kleisli[A any] = func(A) Endomorphism[A]
// Operator represents a transformation from one endomorphism to another.
// Operator represents a higher-order transformation on endomorphisms of the same type.
//
// An Operator takes an endomorphism on type A and produces an endomorphism on type B.
// This is useful for lifting operations or transforming endomorphisms in a generic way.
// An Operator takes an endomorphism on type A and produces another endomorphism on type A.
// Since Operator[A] = Endomorphism[Endomorphism[A]] = func(func(A)A) func(A)A,
// both the input and output endomorphisms operate on the same type A.
//
// This is the return type of curried operations such as Compose, Map, and Chain.
//
// Example:
//
// // An operator that converts an int endomorphism to a string endomorphism
// intToString := func(f endomorphism.Endomorphism[int]) endomorphism.Endomorphism[string] {
// return func(s string) string {
// n, _ := strconv.Atoi(s)
// result := f(n)
// return strconv.Itoa(result)
// }
// // An operator that applies any endomorphism twice
// var applyTwice endomorphism.Operator[int] = func(f endomorphism.Endomorphism[int]) endomorphism.Endomorphism[int] {
// return func(x int) int { return f(f(x)) }
// }
// double := N.Mul(2)
// result := applyTwice(double) // double ∘ double
// // result(5) = double(double(5)) = double(10) = 20
Operator[A any] = Endomorphism[Endomorphism[A]]
)

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

@@ -0,0 +1,195 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package iooption
import (
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestTraverseArray_Success(t *testing.T) {
f := func(n int) IOOption[int] {
return Of(n * 2)
}
input := []int{1, 2, 3, 4, 5}
result := TraverseArray(f)(input)()
assert.Equal(t, O.Some([]int{2, 4, 6, 8, 10}), result)
}
func TestTraverseArray_WithNone(t *testing.T) {
f := func(n int) IOOption[int] {
if n > 0 {
return Of(n * 2)
}
return None[int]()
}
input := []int{1, 2, -3, 4}
result := TraverseArray(f)(input)()
assert.Equal(t, O.None[[]int](), result)
}
func TestTraverseArray_EmptyArray(t *testing.T) {
f := func(n int) IOOption[int] {
return Of(n * 2)
}
input := []int{}
result := TraverseArray(f)(input)()
assert.Equal(t, O.Some([]int{}), result)
}
func TestTraverseArrayWithIndex_Success(t *testing.T) {
f := func(idx, n int) IOOption[int] {
return Of(n + idx)
}
input := []int{10, 20, 30}
result := TraverseArrayWithIndex(f)(input)()
assert.Equal(t, O.Some([]int{10, 21, 32}), result)
}
func TestTraverseArrayWithIndex_WithNone(t *testing.T) {
f := func(idx, n int) IOOption[int] {
if idx < 2 {
return Of(n + idx)
}
return None[int]()
}
input := []int{10, 20, 30}
result := TraverseArrayWithIndex(f)(input)()
assert.Equal(t, O.None[[]int](), result)
}
func TestTraverseArrayWithIndex_EmptyArray(t *testing.T) {
f := func(idx, n int) IOOption[int] {
return Of(n + idx)
}
input := []int{}
result := TraverseArrayWithIndex(f)(input)()
assert.Equal(t, O.Some([]int{}), result)
}
func TestSequenceArray_AllSome(t *testing.T) {
input := []IOOption[int]{
Of(1),
Of(2),
Of(3),
}
result := SequenceArray(input)()
assert.Equal(t, O.Some([]int{1, 2, 3}), result)
}
func TestSequenceArray_WithNone(t *testing.T) {
input := []IOOption[int]{
Of(1),
None[int](),
Of(3),
}
result := SequenceArray(input)()
assert.Equal(t, O.None[[]int](), result)
}
func TestSequenceArray_Empty(t *testing.T) {
input := []IOOption[int]{}
result := SequenceArray(input)()
assert.Equal(t, O.Some([]int{}), result)
}
func TestSequenceArray_AllNone(t *testing.T) {
input := []IOOption[int]{
None[int](),
None[int](),
None[int](),
}
result := SequenceArray(input)()
assert.Equal(t, O.None[[]int](), result)
}
func TestTraverseArray_Composition(t *testing.T) {
// Test composing traverse with other operations
f := func(n int) IOOption[int] {
if n%2 == 0 {
return Of(n / 2)
}
return None[int]()
}
input := []int{2, 4, 6, 8}
result := F.Pipe1(
input,
TraverseArray(f),
)()
assert.Equal(t, O.Some([]int{1, 2, 3, 4}), result)
}
func TestTraverseArray_WithMap(t *testing.T) {
// Test traverse followed by map
f := func(n int) IOOption[int] {
return Of(n * 2)
}
input := []int{1, 2, 3}
result := F.Pipe2(
input,
TraverseArray(f),
Map(func(arr []int) int {
sum := 0
for _, v := range arr {
sum += v
}
return sum
}),
)()
assert.Equal(t, O.Some(12), result) // (1*2 + 2*2 + 3*2) = 12
}
func TestTraverseArrayWithIndex_UseIndex(t *testing.T) {
// Test that index is properly used
f := func(idx, n int) IOOption[string] {
return Of(fmt.Sprintf("%d", idx*n*2))
}
input := []int{1, 2, 3}
result := TraverseArrayWithIndex(f)(input)()
assert.Equal(t, O.Some([]string{"0", "4", "12"}), result)
}

View File

@@ -0,0 +1,433 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package iooption
import (
"fmt"
"testing"
"time"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
I "github.com/IBM/fp-go/v2/io"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestOf(t *testing.T) {
result := Of(42)()
assert.Equal(t, O.Some(42), result)
}
func TestSome(t *testing.T) {
result := Some("test")()
assert.Equal(t, O.Some("test"), result)
}
func TestNone(t *testing.T) {
result := None[int]()()
assert.Equal(t, O.None[int](), result)
}
func TestMonadOf(t *testing.T) {
result := MonadOf(100)()
assert.Equal(t, O.Some(100), result)
}
func TestFromOptionComprehensive(t *testing.T) {
t.Run("from Some", func(t *testing.T) {
result := FromOption(O.Some(42))()
assert.Equal(t, O.Some(42), result)
})
t.Run("from None", func(t *testing.T) {
result := FromOption(O.None[int]())()
assert.Equal(t, O.None[int](), result)
})
}
func TestFromIO(t *testing.T) {
ioValue := I.Of(42)
result := FromIO(ioValue)()
assert.Equal(t, O.Some(42), result)
}
func TestMonadMap(t *testing.T) {
t.Run("map over Some", func(t *testing.T) {
result := MonadMap(Of(5), utils.Double)()
assert.Equal(t, O.Some(10), result)
})
t.Run("map over None", func(t *testing.T) {
result := MonadMap(None[int](), utils.Double)()
assert.Equal(t, O.None[int](), result)
})
}
func TestMonadChain(t *testing.T) {
t.Run("chain Some to Some", func(t *testing.T) {
f := func(n int) IOOption[int] {
return Of(n * 2)
}
result := MonadChain(Of(5), f)()
assert.Equal(t, O.Some(10), result)
})
t.Run("chain Some to None", func(t *testing.T) {
f := func(n int) IOOption[int] {
return None[int]()
}
result := MonadChain(Of(5), f)()
assert.Equal(t, O.None[int](), result)
})
t.Run("chain None", func(t *testing.T) {
f := func(n int) IOOption[int] {
return Of(n * 2)
}
result := MonadChain(None[int](), f)()
assert.Equal(t, O.None[int](), result)
})
}
func TestChain(t *testing.T) {
f := func(n int) IOOption[string] {
if n > 0 {
return Of("positive")
}
return None[string]()
}
t.Run("chain positive", func(t *testing.T) {
result := F.Pipe1(Of(5), Chain(f))()
assert.Equal(t, O.Some("positive"), result)
})
t.Run("chain negative", func(t *testing.T) {
result := F.Pipe1(Of(-5), Chain(f))()
assert.Equal(t, O.None[string](), result)
})
}
func TestMonadAp(t *testing.T) {
t.Run("apply Some function to Some value", func(t *testing.T) {
mab := Of(utils.Double)
ma := Of(5)
result := MonadAp(mab, ma)()
assert.Equal(t, O.Some(10), result)
})
t.Run("apply None function", func(t *testing.T) {
mab := None[func(int) int]()
ma := Of(5)
result := MonadAp(mab, ma)()
assert.Equal(t, O.None[int](), result)
})
t.Run("apply to None value", func(t *testing.T) {
mab := Of(utils.Double)
ma := None[int]()
result := MonadAp(mab, ma)()
assert.Equal(t, O.None[int](), result)
})
}
func TestAp(t *testing.T) {
ma := Of(5)
result := F.Pipe1(Of(utils.Double), Ap[int, int](ma))()
assert.Equal(t, O.Some(10), result)
}
func TestApSeq(t *testing.T) {
ma := Of(5)
result := F.Pipe1(Of(utils.Double), ApSeq[int, int](ma))()
assert.Equal(t, O.Some(10), result)
}
func TestApPar(t *testing.T) {
ma := Of(5)
result := F.Pipe1(Of(utils.Double), ApPar[int, int](ma))()
assert.Equal(t, O.Some(10), result)
}
func TestFlatten(t *testing.T) {
t.Run("flatten Some(Some)", func(t *testing.T) {
nested := Of(Of(42))
result := Flatten(nested)()
assert.Equal(t, O.Some(42), result)
})
t.Run("flatten Some(None)", func(t *testing.T) {
nested := Of(None[int]())
result := Flatten(nested)()
assert.Equal(t, O.None[int](), result)
})
t.Run("flatten None", func(t *testing.T) {
nested := None[IOOption[int]]()
result := Flatten(nested)()
assert.Equal(t, O.None[int](), result)
})
}
func TestOptionize0(t *testing.T) {
f := func() (int, bool) {
return 42, true
}
result := Optionize0(f)()()
assert.Equal(t, O.Some(42), result)
f2 := func() (int, bool) {
return 0, false
}
result2 := Optionize0(f2)()()
assert.Equal(t, O.None[int](), result2)
}
func TestOptionize2(t *testing.T) {
f := func(a, b int) (int, bool) {
if b != 0 {
return a / b, true
}
return 0, false
}
result := Optionize2(f)(10, 2)()
assert.Equal(t, O.Some(5), result)
result2 := Optionize2(f)(10, 0)()
assert.Equal(t, O.None[int](), result2)
}
func TestOptionize3(t *testing.T) {
f := func(a, b, c int) (int, bool) {
if c != 0 {
return (a + b) / c, true
}
return 0, false
}
result := Optionize3(f)(10, 5, 3)()
assert.Equal(t, O.Some(5), result)
result2 := Optionize3(f)(10, 5, 0)()
assert.Equal(t, O.None[int](), result2)
}
func TestOptionize4(t *testing.T) {
f := func(a, b, c, d int) (int, bool) {
if d != 0 {
return (a + b + c) / d, true
}
return 0, false
}
result := Optionize4(f)(10, 5, 3, 2)()
assert.Equal(t, O.Some(9), result)
result2 := Optionize4(f)(10, 5, 3, 0)()
assert.Equal(t, O.None[int](), result2)
}
func TestMemoize(t *testing.T) {
callCount := 0
ioOpt := func() Option[int] {
callCount++
return O.Some(42)
}
memoized := Memoize(ioOpt)
// First call
result1 := memoized()
assert.Equal(t, O.Some(42), result1)
assert.Equal(t, 1, callCount)
// Second call should use cached value
result2 := memoized()
assert.Equal(t, O.Some(42), result2)
assert.Equal(t, 1, callCount)
}
func TestFold(t *testing.T) {
onNone := I.Of("none")
onSome := func(n int) I.IO[string] {
return I.Of(fmt.Sprintf("%d", n))
}
t.Run("fold Some", func(t *testing.T) {
result := Fold(onNone, onSome)(Of(42))()
assert.Equal(t, "42", result)
})
t.Run("fold None", func(t *testing.T) {
result := Fold(onNone, onSome)(None[int]())()
assert.Equal(t, "none", result)
})
}
func TestDefer(t *testing.T) {
callCount := 0
gen := func() IOOption[int] {
callCount++
return Of(42)
}
deferred := Defer(gen)
// Each call should invoke the generator
result1 := deferred()
assert.Equal(t, O.Some(42), result1)
assert.Equal(t, 1, callCount)
result2 := deferred()
assert.Equal(t, O.Some(42), result2)
assert.Equal(t, 2, callCount)
}
func TestFromEither(t *testing.T) {
t.Run("from Right", func(t *testing.T) {
either := ET.Right[string](42)
result := FromEither(either)()
assert.Equal(t, O.Some(42), result)
})
t.Run("from Left", func(t *testing.T) {
either := ET.Left[int]("error")
result := FromEither(either)()
assert.Equal(t, O.None[int](), result)
})
}
func TestMonadAlt(t *testing.T) {
t.Run("first is Some", func(t *testing.T) {
result := MonadAlt(Of(1), Of(2))()
assert.Equal(t, O.Some(1), result)
})
t.Run("first is None, second is Some", func(t *testing.T) {
result := MonadAlt(None[int](), Of(2))()
assert.Equal(t, O.Some(2), result)
})
t.Run("both are None", func(t *testing.T) {
result := MonadAlt(None[int](), None[int]())()
assert.Equal(t, O.None[int](), result)
})
}
func TestAlt(t *testing.T) {
t.Run("first is Some", func(t *testing.T) {
result := F.Pipe1(Of(1), Alt(Of(2)))()
assert.Equal(t, O.Some(1), result)
})
t.Run("first is None", func(t *testing.T) {
result := F.Pipe1(None[int](), Alt(Of(2)))()
assert.Equal(t, O.Some(2), result)
})
}
func TestMonadChainFirst(t *testing.T) {
sideEffect := 0
f := func(n int) IOOption[string] {
sideEffect = n * 2
return Of("side effect")
}
result := MonadChainFirst(Of(5), f)()
assert.Equal(t, O.Some(5), result)
assert.Equal(t, 10, sideEffect)
}
func TestChainFirst(t *testing.T) {
sideEffect := 0
f := func(n int) IOOption[string] {
sideEffect = n * 2
return Of("side effect")
}
result := F.Pipe1(Of(5), ChainFirst(f))()
assert.Equal(t, O.Some(5), result)
assert.Equal(t, 10, sideEffect)
}
func TestMonadChainFirstIOK(t *testing.T) {
sideEffect := 0
f := func(n int) I.IO[string] {
return func() string {
sideEffect = n * 2
return "side effect"
}
}
result := MonadChainFirstIOK(Of(5), f)()
assert.Equal(t, O.Some(5), result)
assert.Equal(t, 10, sideEffect)
}
func TestChainFirstIOK(t *testing.T) {
sideEffect := 0
f := func(n int) I.IO[string] {
return func() string {
sideEffect = n * 2
return "side effect"
}
}
result := F.Pipe1(Of(5), ChainFirstIOK(f))()
assert.Equal(t, O.Some(5), result)
assert.Equal(t, 10, sideEffect)
}
func TestDelay(t *testing.T) {
start := time.Now()
delay := 50 * time.Millisecond
result := F.Pipe1(Of(42), Delay[int](delay))()
elapsed := time.Since(start)
assert.Equal(t, O.Some(42), result)
assert.True(t, elapsed >= delay, "Expected delay of at least %v, got %v", delay, elapsed)
}
func TestAfter(t *testing.T) {
timestamp := time.Now().Add(50 * time.Millisecond)
result := F.Pipe1(Of(42), After[int](timestamp))()
assert.Equal(t, O.Some(42), result)
assert.True(t, time.Now().After(timestamp) || time.Now().Equal(timestamp))
}
func TestMonadChainIOK(t *testing.T) {
f := func(n int) I.IO[string] {
return I.Of(fmt.Sprintf("%d", n))
}
t.Run("chain Some", func(t *testing.T) {
result := MonadChainIOK(Of(42), f)()
assert.Equal(t, O.Some("42"), result)
})
t.Run("chain None", func(t *testing.T) {
result := MonadChainIOK(None[int](), f)()
assert.Equal(t, O.None[string](), result)
})
}

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

@@ -0,0 +1,241 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"errors"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestBracket_Success(t *testing.T) {
acquired := false
used := false
released := false
acquire := func() IOResult[int] {
return func() Result[int] {
acquired = true
return result.Of(42)
}
}()
use := func(n int) IOResult[string] {
return func() Result[string] {
used = true
return result.Of("success")
}
}
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
return result.Of(F.VOID)
}
}
res := Bracket(acquire, use, release)()
assert.True(t, acquired, "Resource should be acquired")
assert.True(t, used, "Resource should be used")
assert.True(t, released, "Resource should be released")
assert.Equal(t, result.Of("success"), res)
}
func TestBracket_UseFailure(t *testing.T) {
acquired := false
released := false
releaseResult := result.Result[string]{}
acquire := func() IOResult[int] {
return func() Result[int] {
acquired = true
return result.Of(42)
}
}()
useErr := errors.New("use error")
use := func(n int) IOResult[string] {
return func() Result[string] {
return result.Left[string](useErr)
}
}
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
releaseResult = res
return result.Of(F.VOID)
}
}
res := Bracket(acquire, use, release)()
assert.True(t, acquired, "Resource should be acquired")
assert.True(t, released, "Resource should be released even on use failure")
assert.Equal(t, result.Left[string](useErr), res)
assert.Equal(t, result.Left[string](useErr), releaseResult)
}
func TestBracket_AcquireFailure(t *testing.T) {
used := false
released := false
acquireErr := errors.New("acquire error")
acquire := func() IOResult[int] {
return func() Result[int] {
return result.Left[int](acquireErr)
}
}()
use := func(n int) IOResult[string] {
return func() Result[string] {
used = true
return result.Of("success")
}
}
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
return result.Of(F.VOID)
}
}
res := Bracket(acquire, use, release)()
assert.False(t, used, "Use should not be called if acquire fails")
assert.False(t, released, "Release should not be called if acquire fails")
assert.Equal(t, result.Left[string](acquireErr), res)
}
func TestBracket_ReleaseFailure(t *testing.T) {
acquired := false
used := false
released := false
acquire := func() IOResult[int] {
return func() Result[int] {
acquired = true
return result.Of(42)
}
}()
use := func(n int) IOResult[string] {
return func() Result[string] {
used = true
return result.Of("success")
}
}
releaseErr := errors.New("release error")
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
return result.Left[F.Void](releaseErr)
}
}
res := Bracket(acquire, use, release)()
assert.True(t, acquired, "Resource should be acquired")
assert.True(t, used, "Resource should be used")
assert.True(t, released, "Release should be attempted")
// When release fails, the release error is returned
assert.Equal(t, result.Left[string](releaseErr), res)
}
func TestBracket_BothUseAndReleaseFail(t *testing.T) {
acquired := false
released := false
acquire := func() IOResult[int] {
return func() Result[int] {
acquired = true
return result.Of(42)
}
}()
useErr := errors.New("use error")
use := func(n int) IOResult[string] {
return func() Result[string] {
return result.Left[string](useErr)
}
}
releaseErr := errors.New("release error")
release := func(n int, res Result[string]) IOResult[F.Void] {
return func() Result[F.Void] {
released = true
return result.Left[F.Void](releaseErr)
}
}
res := Bracket(acquire, use, release)()
assert.True(t, acquired, "Resource should be acquired")
assert.True(t, released, "Release should be attempted")
// When both fail, the release error is returned
assert.Equal(t, result.Left[string](releaseErr), res)
}
func TestBracket_ResourceValue(t *testing.T) {
// Test that the acquired resource value is passed correctly
var usedValue int
var releasedValue int
acquire := Of(100)
use := func(n int) IOResult[string] {
usedValue = n
return Of("result")
}
release := func(n int, res Result[string]) IOResult[F.Void] {
releasedValue = n
return Of(F.VOID)
}
Bracket(acquire, use, release)()
assert.Equal(t, 100, usedValue, "Use should receive acquired value")
assert.Equal(t, 100, releasedValue, "Release should receive acquired value")
}
func TestBracket_ResultValue(t *testing.T) {
// Test that the use result is passed to release
var releaseReceivedResult Result[string]
acquire := Of(42)
use := func(n int) IOResult[string] {
return Of("test result")
}
release := func(n int, res Result[string]) IOResult[F.Void] {
releaseReceivedResult = res
return Of(F.VOID)
}
Bracket(acquire, use, release)()
assert.Equal(t, result.Of("test result"), releaseReceivedResult)
}

View File

@@ -0,0 +1,581 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioresult
import (
"errors"
"fmt"
"testing"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
"github.com/IBM/fp-go/v2/io"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestLeft(t *testing.T) {
err := errors.New("test error")
res := Left[int](err)()
assert.Equal(t, result.Left[int](err), res)
}
func TestRight(t *testing.T) {
res := Right(42)()
assert.Equal(t, result.Of(42), res)
}
func TestOf(t *testing.T) {
res := Of(42)()
assert.Equal(t, result.Of(42), res)
}
func TestMonadOf(t *testing.T) {
res := MonadOf(42)()
assert.Equal(t, result.Of(42), res)
}
func TestLeftIO(t *testing.T) {
err := errors.New("test error")
res := LeftIO[int](io.Of(err))()
assert.Equal(t, result.Left[int](err), res)
}
func TestRightIO(t *testing.T) {
res := RightIO(io.Of(42))()
assert.Equal(t, result.Of(42), res)
}
func TestFromEither(t *testing.T) {
t.Run("from Right", func(t *testing.T) {
either := result.Of(42)
res := FromEither(either)()
assert.Equal(t, result.Of(42), res)
})
t.Run("from Left", func(t *testing.T) {
err := errors.New("test error")
either := result.Left[int](err)
res := FromEither(either)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestFromResult(t *testing.T) {
t.Run("from success", func(t *testing.T) {
res := FromResult(result.Of(42))()
assert.Equal(t, result.Of(42), res)
})
t.Run("from error", func(t *testing.T) {
err := errors.New("test error")
res := FromResult(result.Left[int](err))()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestFromEitherI(t *testing.T) {
t.Run("with nil error", func(t *testing.T) {
res := FromEitherI(42, nil)()
assert.Equal(t, result.Of(42), res)
})
t.Run("with error", func(t *testing.T) {
err := errors.New("test error")
res := FromEitherI(0, err)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestFromResultI(t *testing.T) {
t.Run("with nil error", func(t *testing.T) {
res := FromResultI(42, nil)()
assert.Equal(t, result.Of(42), res)
})
t.Run("with error", func(t *testing.T) {
err := errors.New("test error")
res := FromResultI(0, err)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestFromOption_Success(t *testing.T) {
onNone := func() error {
return errors.New("none")
}
t.Run("from Some", func(t *testing.T) {
res := FromOption[int](onNone)(O.Some(42))()
assert.Equal(t, result.Of(42), res)
})
t.Run("from None", func(t *testing.T) {
res := FromOption[int](onNone)(O.None[int]())()
assert.Equal(t, result.Left[int](errors.New("none")), res)
})
}
func TestFromIO(t *testing.T) {
ioValue := io.Of(42)
res := FromIO(ioValue)()
assert.Equal(t, result.Of(42), res)
}
func TestFromLazy(t *testing.T) {
lazy := func() int { return 42 }
res := FromLazy(lazy)()
assert.Equal(t, result.Of(42), res)
}
func TestMonadMap(t *testing.T) {
t.Run("map over Right", func(t *testing.T) {
res := MonadMap(Of(5), utils.Double)()
assert.Equal(t, result.Of(10), res)
})
t.Run("map over Left", func(t *testing.T) {
err := errors.New("test error")
res := MonadMap(Left[int](err), utils.Double)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestMap_Comprehensive(t *testing.T) {
double := func(n int) int { return n * 2 }
t.Run("map Right", func(t *testing.T) {
res := F.Pipe1(Of(5), Map(double))()
assert.Equal(t, result.Of(10), res)
})
t.Run("map Left", func(t *testing.T) {
err := errors.New("test error")
res := F.Pipe1(Left[int](err), Map(double))()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestMonadMapTo(t *testing.T) {
t.Run("mapTo Right", func(t *testing.T) {
res := MonadMapTo(Of(5), "constant")()
assert.Equal(t, result.Of("constant"), res)
})
t.Run("mapTo Left", func(t *testing.T) {
err := errors.New("test error")
res := MonadMapTo(Left[int](err), "constant")()
assert.Equal(t, result.Left[string](err), res)
})
}
func TestMapTo(t *testing.T) {
res := F.Pipe1(Of(5), MapTo[int]("constant"))()
assert.Equal(t, result.Of("constant"), res)
}
func TestMonadChain(t *testing.T) {
f := func(n int) IOResult[int] {
return Of(n * 2)
}
t.Run("chain Right to Right", func(t *testing.T) {
res := MonadChain(Of(5), f)()
assert.Equal(t, result.Of(10), res)
})
t.Run("chain Right to Left", func(t *testing.T) {
err := errors.New("test error")
f := func(n int) IOResult[int] {
return Left[int](err)
}
res := MonadChain(Of(5), f)()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("chain Left", func(t *testing.T) {
err := errors.New("test error")
res := MonadChain(Left[int](err), f)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestChain_Comprehensive(t *testing.T) {
f := func(n int) IOResult[string] {
if n > 0 {
return Of(fmt.Sprintf("%d", n))
}
return Left[string](errors.New("negative"))
}
t.Run("chain positive", func(t *testing.T) {
res := F.Pipe1(Of(5), Chain(f))()
assert.Equal(t, result.Of("5"), res)
})
t.Run("chain negative", func(t *testing.T) {
res := F.Pipe1(Of(-5), Chain(f))()
assert.Equal(t, result.Left[string](errors.New("negative")), res)
})
}
func TestMonadChainEitherK(t *testing.T) {
f := func(n int) result.Result[int] {
if n > 0 {
return result.Of(n * 2)
}
return result.Left[int](errors.New("non-positive"))
}
t.Run("chain to success", func(t *testing.T) {
res := MonadChainEitherK(Of(5), f)()
assert.Equal(t, result.Of(10), res)
})
t.Run("chain to error", func(t *testing.T) {
res := MonadChainEitherK(Of(-5), f)()
assert.Equal(t, result.Left[int](errors.New("non-positive")), res)
})
}
func TestMonadChainResultK(t *testing.T) {
f := func(n int) result.Result[int] {
return result.Of(n * 2)
}
res := MonadChainResultK(Of(5), f)()
assert.Equal(t, result.Of(10), res)
}
func TestChainResultK(t *testing.T) {
f := func(n int) result.Result[int] {
return result.Of(n * 2)
}
res := F.Pipe1(Of(5), ChainResultK(f))()
assert.Equal(t, result.Of(10), res)
}
func TestMonadAp_Comprehensive(t *testing.T) {
t.Run("apply Right function to Right value", func(t *testing.T) {
mab := Of(utils.Double)
ma := Of(5)
res := MonadAp(mab, ma)()
assert.Equal(t, result.Of(10), res)
})
t.Run("apply Left function", func(t *testing.T) {
err := errors.New("function error")
mab := Left[func(int) int](err)
ma := Of(5)
res := MonadAp(mab, ma)()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("apply to Left value", func(t *testing.T) {
err := errors.New("value error")
mab := Of(utils.Double)
ma := Left[int](err)
res := MonadAp(mab, ma)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestAp_Comprehensive(t *testing.T) {
ma := Of(5)
res := F.Pipe1(Of(utils.Double), Ap[int, int](ma))()
assert.Equal(t, result.Of(10), res)
}
func TestApPar(t *testing.T) {
ma := Of(5)
res := F.Pipe1(Of(utils.Double), ApPar[int, int](ma))()
assert.Equal(t, result.Of(10), res)
}
func TestApSeq(t *testing.T) {
ma := Of(5)
res := F.Pipe1(Of(utils.Double), ApSeq[int, int](ma))()
assert.Equal(t, result.Of(10), res)
}
func TestFlatten_Comprehensive(t *testing.T) {
t.Run("flatten Right(Right)", func(t *testing.T) {
nested := Of(Of(42))
res := Flatten(nested)()
assert.Equal(t, result.Of(42), res)
})
t.Run("flatten Right(Left)", func(t *testing.T) {
err := errors.New("inner error")
nested := Of(Left[int](err))
res := Flatten(nested)()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("flatten Left", func(t *testing.T) {
err := errors.New("outer error")
nested := Left[IOResult[int]](err)
res := Flatten(nested)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestTryCatch(t *testing.T) {
t.Run("successful function", func(t *testing.T) {
f := func() (int, error) {
return 42, nil
}
res := TryCatch(f, F.Identity[error])()
assert.Equal(t, result.Of(42), res)
})
t.Run("failing function", func(t *testing.T) {
err := errors.New("test error")
f := func() (int, error) {
return 0, err
}
res := TryCatch(f, F.Identity[error])()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("with error transformation", func(t *testing.T) {
err := errors.New("original")
f := func() (int, error) {
return 0, err
}
onThrow := func(e error) error {
return fmt.Errorf("wrapped: %w", e)
}
res := TryCatch(f, onThrow)()
assert.True(t, result.IsLeft(res))
})
}
func TestTryCatchError_Comprehensive(t *testing.T) {
t.Run("successful function", func(t *testing.T) {
f := func() (int, error) {
return 42, nil
}
res := TryCatchError(f)()
assert.Equal(t, result.Of(42), res)
})
t.Run("failing function", func(t *testing.T) {
err := errors.New("test error")
f := func() (int, error) {
return 0, err
}
res := TryCatchError(f)()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestMemoize_Comprehensive(t *testing.T) {
callCount := 0
ioRes := func() Result[int] {
callCount++
return result.Of(42)
}
memoized := Memoize(ioRes)
// First call
res1 := memoized()
assert.Equal(t, result.Of(42), res1)
assert.Equal(t, 1, callCount)
// Second call should use cached value
res2 := memoized()
assert.Equal(t, result.Of(42), res2)
assert.Equal(t, 1, callCount)
}
func TestMonadMapLeft(t *testing.T) {
t.Run("map Left error", func(t *testing.T) {
err := errors.New("original")
f := func(e error) string {
return e.Error()
}
res := MonadMapLeft(Left[int](err), f)()
// Result is IOEither[string, int], check it's a left
assert.True(t, ET.IsLeft(res))
})
t.Run("map Right unchanged", func(t *testing.T) {
f := func(e error) string {
return e.Error()
}
res := MonadMapLeft(Of(42), f)()
// MapLeft changes the error type, so result is IOEither[string, int]
assert.True(t, ET.IsRight(res))
assert.Equal(t, 42, ET.MonadFold(res, func(string) int { return 0 }, F.Identity[int]))
})
}
func TestMapLeft_Comprehensive(t *testing.T) {
f := func(e error) string {
return fmt.Sprintf("wrapped: %s", e.Error())
}
t.Run("map Left", func(t *testing.T) {
err := errors.New("original")
res := F.Pipe1(Left[int](err), MapLeft[int](f))()
// Result is IOEither[string, int], check it's a left
assert.True(t, ET.IsLeft(res))
})
t.Run("map Right unchanged", func(t *testing.T) {
res := F.Pipe1(Of(42), MapLeft[int](f))()
// MapLeft changes the error type, so result is IOEither[string, int]
assert.True(t, ET.IsRight(res))
assert.Equal(t, 42, ET.MonadFold(res, func(string) int { return 0 }, F.Identity[int]))
})
}
func TestMonadBiMap(t *testing.T) {
leftF := func(e error) string {
return e.Error()
}
rightF := func(n int) string {
return fmt.Sprintf("%d", n)
}
t.Run("bimap Right", func(t *testing.T) {
res := MonadBiMap(Of(42), leftF, rightF)()
// BiMap changes both types, so result is IOEither[string, string]
assert.True(t, ET.IsRight(res))
assert.Equal(t, "42", ET.MonadFold(res, F.Identity[string], F.Identity[string]))
})
t.Run("bimap Left", func(t *testing.T) {
err := errors.New("test")
res := MonadBiMap(Left[int](err), leftF, rightF)()
// Result is IOEither[string, string], check it's a left
assert.True(t, ET.IsLeft(res))
})
}
func TestBiMap_Comprehensive(t *testing.T) {
leftF := func(e error) string {
return e.Error()
}
rightF := func(n int) string {
return fmt.Sprintf("%d", n)
}
t.Run("bimap Right", func(t *testing.T) {
res := F.Pipe1(Of(42), BiMap(leftF, rightF))()
// BiMap changes both types, so result is IOEither[string, string]
assert.True(t, ET.IsRight(res))
assert.Equal(t, "42", ET.MonadFold(res, F.Identity[string], F.Identity[string]))
})
t.Run("bimap Left", func(t *testing.T) {
err := errors.New("test")
res := F.Pipe1(Left[int](err), BiMap(leftF, rightF))()
// Result is IOEither[string, string], check it's a left
assert.True(t, ET.IsLeft(res))
})
}
func TestFold_Comprehensive(t *testing.T) {
onLeft := func(e error) io.IO[string] {
return io.Of(fmt.Sprintf("error: %s", e.Error()))
}
onRight := func(n int) io.IO[string] {
return io.Of(fmt.Sprintf("value: %d", n))
}
t.Run("fold Right", func(t *testing.T) {
res := Fold(onLeft, onRight)(Of(42))()
assert.Equal(t, "value: 42", res)
})
t.Run("fold Left", func(t *testing.T) {
err := errors.New("test")
res := Fold(onLeft, onRight)(Left[int](err))()
assert.Equal(t, "error: test", res)
})
}
func TestGetOrElse_Comprehensive(t *testing.T) {
onLeft := func(e error) io.IO[int] {
return io.Of(0)
}
t.Run("get Right value", func(t *testing.T) {
res := GetOrElse(onLeft)(Of(42))()
assert.Equal(t, 42, res)
})
t.Run("get default on Left", func(t *testing.T) {
err := errors.New("test")
res := GetOrElse(onLeft)(Left[int](err))()
assert.Equal(t, 0, res)
})
}
func TestGetOrElseOf(t *testing.T) {
onLeft := func(e error) int {
return 0
}
t.Run("get Right value", func(t *testing.T) {
res := GetOrElseOf(onLeft)(Of(42))()
assert.Equal(t, 42, res)
})
t.Run("get default on Left", func(t *testing.T) {
err := errors.New("test")
res := GetOrElseOf(onLeft)(Left[int](err))()
assert.Equal(t, 0, res)
})
}
func TestMonadChainTo(t *testing.T) {
t.Run("chain Right to Right", func(t *testing.T) {
res := MonadChainTo(Of(1), Of(2))()
assert.Equal(t, result.Of(2), res)
})
t.Run("chain Right to Left", func(t *testing.T) {
err := errors.New("test")
res := MonadChainTo(Of(1), Left[int](err))()
assert.Equal(t, result.Left[int](err), res)
})
t.Run("chain Left", func(t *testing.T) {
err := errors.New("test")
res := MonadChainTo(Left[int](err), Of(2))()
assert.Equal(t, result.Left[int](err), res)
})
}
func TestChainLazyK(t *testing.T) {
f := func(n int) Lazy[string] {
return func() string {
return fmt.Sprintf("%d", n)
}
}
res := F.Pipe1(Of(42), ChainLazyK(f))()
assert.Equal(t, result.Of("42"), res)
}

View File

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

View File

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

View File

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

View File

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