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

Compare commits

...

4 Commits

Author SHA1 Message Date
Dr. Carsten Leue
3df1dca146 fix: better result type for Pipe
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 17:03:12 +01:00
Dr. Carsten Leue
a0132e2e92 fix: parameter order
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 15:08:11 +01:00
Dr. Carsten Leue
c6b342908d doc: better explanation for logger
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 13:52:08 +01:00
Dr. Carsten Leue
962237492f doc: explain Effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 13:27:20 +01:00
6 changed files with 303 additions and 68 deletions

View File

@@ -1,6 +1,6 @@
# Functional I/O in Go: Context, Errors, and the Reader Pattern
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult` and `idiomatic/context/readerresult` packages.
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult`, `idiomatic/context/readerresult`, and `effect` packages.
## Table of Contents
@@ -10,6 +10,7 @@ This document explores how functional programming principles apply to I/O operat
- [Benefits of the Functional Approach](#benefits-of-the-functional-approach)
- [Side-by-Side Comparison](#side-by-side-comparison)
- [Advanced Patterns](#advanced-patterns)
- [The Effect Package: Higher-Level Abstraction](#the-effect-package-higher-level-abstraction)
- [When to Use Each Approach](#when-to-use-each-approach)
## Why Context in I/O Operations
@@ -775,6 +776,191 @@ func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
}
```
## The Effect Package: Higher-Level Abstraction
### What is Effect?
The `effect` package provides a higher-level abstraction over `ReaderReaderIOResult`, offering a complete effect system for managing dependencies, errors, and side effects in a composable way. It's inspired by [effect-ts](https://effect.website/) and provides a cleaner API for complex workflows.
### Core Type
```go
// Effect represents an effectful computation that:
// - Requires a context of type C (dependency injection)
// - Can perform I/O operations
// - Can fail with an error
// - Produces a value of type A on success
type Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
```
**Key difference from ReaderIOResult**: Effect adds an additional layer of dependency injection (the `C` type parameter) on top of the runtime `context.Context`, enabling type-safe dependency management.
### When to Use Effect
Use the Effect package when you need:
1. **Type-Safe Dependency Injection**: Your application has typed dependencies (config, services, repositories) that need to be threaded through operations
2. **Complex Workflows**: Multiple services and dependencies need to be composed
3. **Testability**: You want to easily mock dependencies by providing different contexts
4. **Separation of Concerns**: Clear separation between business logic, dependencies, and I/O
### Effect vs ReaderIOResult
```go
// ReaderIOResult - depends only on runtime context
type ReaderIOResult[A any] = func(context.Context) (A, error)
// Effect - depends on typed context C AND runtime context
type Effect[C, A any] = func(C) func(context.Context) (A, error)
```
**ReaderIOResult** is simpler and suitable when you only need runtime context (cancellation, deadlines, request-scoped values).
**Effect** adds typed dependency injection, making it ideal for applications with complex service dependencies.
### Basic Usage
#### Creating Effects
```go
type AppConfig struct {
DatabaseURL string
APIKey string
}
// Create a successful effect
successEffect := effect.Succeed[AppConfig, string]("hello")
// Create a failed effect
failEffect := effect.Fail[AppConfig, string](errors.New("failed"))
// Lift a pure value
pureEffect := effect.Of[AppConfig, int](42)
```
#### Integrating Standard Go Functions
The `Eitherize` function makes it easy to integrate standard Go functions that return `(value, error)`:
```go
type Database struct {
conn *sql.DB
}
// Convert a standard Go function to an Effect using Eitherize
func fetchUser(id int) effect.Effect[Database, User] {
return effect.Eitherize(func(db Database, ctx context.Context) (User, error) {
var user User
err := db.conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id).Scan(&user)
return user, err
})
}
// Use Eitherize1 for Kleisli arrows (functions with an additional parameter)
fetchUserKleisli := effect.Eitherize1(func(db Database, ctx context.Context, id int) (User, error) {
var user User
err := db.conn.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", id).Scan(&user)
return user, err
})
// fetchUserKleisli has type: func(int) Effect[Database, User]
```
#### Composing Effects
```go
type Services struct {
UserRepo UserRepository
EmailSvc EmailService
}
// Compose multiple effects with typed dependencies
func processUser(id int, newEmail string) effect.Effect[Services, User] {
return F.Pipe3(
// Fetch user from repository
effect.Eitherize(func(svc Services, ctx context.Context) (User, error) {
return svc.UserRepo.GetUser(ctx, id)
}),
// Validate user (pure function lifted into Effect)
effect.ChainEitherK[Services](validateUser),
// Update email
effect.Chain[Services](func(user User) effect.Effect[Services, User] {
return effect.Eitherize(func(svc Services, ctx context.Context) (User, error) {
if err := svc.EmailSvc.SendVerification(ctx, newEmail); err != nil {
return User{}, err
}
return svc.UserRepo.UpdateEmail(ctx, user.ID, newEmail)
})
}),
)
}
```
#### Running Effects
```go
func main() {
// Set up typed dependencies once
services := Services{
UserRepo: &PostgresUserRepo{db: db},
EmailSvc: &SMTPEmailService{host: "smtp.example.com"},
}
// Build the effect pipeline (no execution yet)
userEffect := processUser(42, "new@email.com")
// Provide dependencies - returns a Thunk (ReaderIOResult)
thunk := effect.Provide(services)(userEffect)
// Run synchronously - returns a func(context.Context) (User, error)
readerResult := effect.RunSync(thunk)
// Execute with runtime context
user, err := readerResult(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("Updated user: %+v\n", user)
}
```
### Comparison: Traditional vs ReaderIOResult vs Effect
| Aspect | Traditional | ReaderIOResult | Effect |
|---|---|---|---|
| Error propagation | Manual | Automatic | Automatic |
| Dependency injection | Function parameters | Closure / `context.Context` | Typed `C` parameter |
| Testability | Requires mocking | Mock `ReaderIOResult` | Provide mock `C` |
| Composability | Low | High | High |
| Type-safe dependencies | No | No | Yes |
| Complexity | Low | Medium | Medium-High |
### Testing with Effect
One of the key benefits of Effect is easy testing through dependency substitution:
```go
func TestProcessUser(t *testing.T) {
// Create mock services
mockServices := Services{
UserRepo: &MockUserRepo{
users: map[int]User{42: {ID: 42, Age: 25, Email: "old@email.com"}},
},
EmailSvc: &MockEmailService{},
}
// Run the effect with mock dependencies
user, err := effect.RunSync(
effect.Provide(mockServices)(processUser(42, "new@email.com")),
)(context.Background())
assert.NoError(t, err)
assert.Equal(t, "new@email.com", user.Email)
}
```
No database or SMTP server needed — just provide mock implementations of the `Services` struct.
## When to Use Each Approach
### Use Traditional Go Style When:
@@ -794,6 +980,17 @@ func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
5. **Resource management**: Need guaranteed cleanup (Bracket)
6. **Parallel execution**: Need to parallelize operations easily
7. **Type safety**: Want the type system to track I/O effects
8. **Simple dependencies**: Only need runtime `context.Context`, no typed dependencies
### Use Effect When:
1. **Type-safe dependency injection**: Application has typed dependencies (config, services, repositories)
2. **Complex service architectures**: Multiple services need to be composed with clear dependency management
3. **Testability with mocks**: Want to easily substitute dependencies for testing
4. **Separation of concerns**: Need clear separation between business logic, dependencies, and I/O
5. **Large applications**: Building applications where dependency management is critical
6. **Team experience**: Team is comfortable with functional programming and effect systems
7. **Integration with standard Go**: Need to integrate many standard `(value, error)` functions using `Eitherize`
### Use Idiomatic Functional Style (idiomatic/context/readerresult) When:
@@ -803,6 +1000,7 @@ func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
4. **Go integration**: Want seamless integration with existing Go code
5. **Production services**: Building high-throughput services
6. **Best of both worlds**: Want functional composition with Go's native patterns
7. **Simple dependencies**: Only need runtime `context.Context`, no typed dependencies
## Summary
@@ -814,11 +1012,18 @@ The functional approach to I/O in Go offers significant advantages:
4. **Type Safety**: I/O effects visible in the type system
5. **Lazy Evaluation**: Build pipelines, execute when ready
6. **Context Propagation**: Automatic threading of context
7. **Performance**: Idiomatic version offers 2-10x speedup
7. **Dependency Injection**: Type-safe dependency management with Effect
8. **Performance**: Idiomatic version offers 2-10x speedup
The key insight is that **I/O operations return descriptions of effects** (ReaderIOResult) rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
The key insight is that **I/O operations return descriptions of effects** rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
For production Go services, the **idiomatic/context/readerresult** package provides the best balance: full functional programming capabilities with native Go performance and familiar error handling patterns.
### Choosing the Right Abstraction
- **ReaderIOResult**: Best for simple I/O pipelines that only need runtime `context.Context`
- **Effect**: Best for complex applications with typed dependencies and service architectures
- **idiomatic/context/readerresult**: Best for production services needing high performance with functional patterns
For production Go services, the **idiomatic/context/readerresult** package provides the best balance of performance and functional capabilities. For applications with complex dependency management, the **effect** package provides type-safe dependency injection with a clean, composable API.
## Further Reading
@@ -826,4 +1031,6 @@ For production Go services, the **idiomatic/context/readerresult** package provi
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison
- [idiomatic/doc.go](./idiomatic/doc.go) - Idiomatic package overview
- [context/readerioresult](./context/readerioresult/) - ReaderIOResult package
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package
- [effect](./effect/) - Effect package for type-safe dependency injection
- [effect-ts](https://effect.website/) - TypeScript effect system that inspired this package

View File

@@ -461,7 +461,8 @@ func process() IOResult[string] {
- **Result** - Simplified Either with error as left type (recommended for error handling)
- **IO** - Lazy evaluation and side effect management
- **IOOption** - Combine IO with Option for optional values with side effects
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither when using standard `error` type)
- **Effect** - Composable effects with dependency injection and error handling
- **Reader** - Dependency injection pattern
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects

View File

@@ -369,7 +369,7 @@ func onExitAny(
// )
func LogEntryExitWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
cb Reader[context.Context, *slog.Logger],
name string) Operator[A, A] {
nameAttr := slog.String("name", name)
@@ -499,12 +499,12 @@ func LogEntryExit[A any](name string) Operator[A, A] {
func curriedLog(
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) func(slog.Attr) func(context.Context) func() struct{} {
return F.Curry2(func(a slog.Attr, ctx context.Context) func() struct{} {
message string) func(slog.Attr) ReaderIO[Void] {
return F.Curry2(func(a slog.Attr, ctx context.Context) IO[Void] {
logger := cb(ctx)
return func() struct{} {
return func() Void {
logger.LogAttrs(ctx, logLevel, message, a)
return struct{}{}
return F.VOID
}
})
}
@@ -571,7 +571,7 @@ func curriedLog(
// - Conditional logging: Enable/disable logging based on logger configuration
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
cb Reader[context.Context, *slog.Logger],
message string) Kleisli[Result[A], A] {
return F.Pipe1(
@@ -582,18 +582,23 @@ func SLogWithCallback[A any](
curriedLog(logLevel, cb, message),
),
// preserve the original context
reader.Chain(reader.Sequence(readerio.MapTo[struct{}, Result[A]])),
reader.Chain(reader.Sequence(readerio.MapTo[Void, Result[A]])),
)
}
// SLog creates a Kleisli arrow that logs a Result value (success or error) with a message.
//
// This function logs both successful values and errors at Info level using the logger from the context.
// This function logs both successful values and errors at Info level. It retrieves the logger
// using logging.GetLoggerFromContext, which returns either:
// - The logger stored in the context via logging.WithLogger, or
// - The global logger (set via logging.SetLogger or slog.Default())
//
// It's a convenience wrapper around SLogWithCallback with standard settings.
//
// The logged output includes:
// - For success: The message with the value as a structured "value" attribute
// - For error: The message with the error as a structured "error" attribute
// The message parameter becomes the main log message text, and the Result value or error
// is attached as a structured logging attribute:
// - For success: Logs message with attribute value=<the actual value>
// - For error: Logs message with attribute error=<the error>
//
// The Result is passed through unchanged after logging, making this function transparent in the
// computation pipeline.
@@ -647,25 +652,47 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}
// TapSLog creates an operator that logs only successful values with a message and passes them through unchanged.
// TapSLog creates an operator that logs both successful values and errors with a message,
// and passes the ReaderIOResult through unchanged.
//
// This function is useful for debugging and monitoring values as they flow through a ReaderIOResult
// computation chain. Unlike SLog which logs both successes and errors, TapSLog only logs when the
// computation is successful. If the computation contains an error, no logging occurs and the error
// is propagated unchanged.
// computation chain. It logs both successful values and errors at Info level. It retrieves the logger
// using logging.GetLoggerFromContext, which returns either:
// - The logger stored in the context via logging.WithLogger, or
// - The global logger (set via logging.SetLogger or slog.Default())
//
// The logged output includes:
// - The provided message
// - The value being passed through (as a structured "value" attribute)
// The ReaderIOResult is returned unchanged after logging.
//
// The difference between TapSLog and SLog is their position in the pipeline:
// - SLog is a Kleisli[Result[A], A] used with Chain to intercept the raw Result
// - TapSLog is an Operator[A, A] used directly in a pipe on a ReaderIOResult[A]
//
// Both log the same information (success value or error), but TapSLog is more ergonomic
// when composing ReaderIOResult pipelines with F.Pipe.
//
// The message parameter becomes the main log message text, and the Result value or error
// is attached as a structured logging attribute:
// - For success: Logs message with attribute value=<the actual value>
// - For error: Logs message with attribute error=<the error>
//
// For example, TapSLog[User]("Fetched user") with a successful result produces:
//
// Log message: "Fetched user"
// Structured attribute: value={ID:123 Name:"Alice"}
//
// With an error result, it produces:
//
// Log message: "Fetched user"
// Structured attribute: error="user not found"
//
// Type Parameters:
// - A: The type of the value to log and pass through
// - A: The success type of the ReaderIOResult to log and pass through
//
// Parameters:
// - message: A descriptive message to include in the log entry
//
// Returns:
// - An Operator that logs successful values and returns them unchanged
// - An Operator that logs the Result (value or error) and returns the ReaderIOResult unchanged
//
// Example with simple value logging:
//
@@ -680,7 +707,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
// )
//
// result := pipeline(t.Context())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Returns: result.Of("Alice")
//
// Example in a processing pipeline:
@@ -695,36 +722,36 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
// )
//
// result := processOrder(t.Context())()
// // Logs each successful step with the intermediate values
// // If any step fails, subsequent TapSLog calls don't log
// // Logs each step with its value or error
//
// Example with error handling:
//
// pipeline := F.Pipe3(
// fetchData(id),
// TapSLog[Data]("Data fetched"),
// Chain(func(d Data) ReaderIOResult[Result] {
// Chain(func(d Data) ReaderIOResult[Data] {
// if d.IsValid() {
// return Of(processData(d))
// }
// return Left[Result](errors.New("invalid data"))
// return Left[Data](errors.New("invalid data"))
// }),
// TapSLog[Result]("Data processed"),
// TapSLog[Data]("Data processed"),
// )
//
// // If fetchData succeeds: logs "Data fetched" with the data
// // If processing succeeds: logs "Data processed" with the result
// // If processing fails: "Data processed" is NOT logged (error propagates)
// // If fetchData succeeds: logs "Data fetched" value={...}
// // If fetchData fails: logs "Data fetched" error="..."
// // If processing succeeds: logs "Data processed" value={...}
// // If processing fails: logs "Data processed" error="invalid data"
//
// Use Cases:
// - Debugging: Inspect intermediate successful values in a computation pipeline
// - Monitoring: Track successful data flow through complex operations
// - Troubleshooting: Identify where successful computations stop (last logged value before error)
// - Auditing: Log important successful values for compliance or security
// - Development: Understand data transformations during development
// - Debugging: Inspect intermediate values and errors in a computation pipeline
// - Monitoring: Track both successful and failed data flow through complex operations
// - Troubleshooting: Identify where errors are introduced or propagated
// - Auditing: Log important values and failures for compliance or security
// - Development: Understand data transformations and error paths during development
//
// Note: This function only logs successful values. Errors are silently propagated without logging.
// For logging both successes and errors, use SLog instead.
// Note: This function logs both successful values and errors. It is equivalent to SLog
// but expressed as an Operator for direct use in F.Pipe pipelines on ReaderIOResult values.
//
//go:inline
func TapSLog[A any](message string) Operator[A, A] {

View File

@@ -421,7 +421,7 @@ func TestEitherize1_Integration(t *testing.T) {
// Act
pipeline := F.Pipe1(
Of[TestConfig]("not-a-number"),
Chain[TestConfig](parseKleisli),
Chain(parseKleisli),
)
outcome := pipeline(testConfig)(context.Background())()

View File

@@ -101,7 +101,7 @@ func (t *typeImpl[A, O, I]) Is(i any) Result[A] {
// stringToInt := codec.MakeType(...) // Type[int, string, string]
// intToPositive := codec.MakeType(...) // Type[PositiveInt, int, int]
// composed := codec.Pipe(intToPositive)(stringToInt) // Type[PositiveInt, string, string]
func Pipe[O, I, A, B any](ab Type[B, A, A]) func(Type[A, O, I]) Type[B, O, I] {
func Pipe[O, I, A, B any](ab Type[B, A, A]) Operator[A, B, O, I] {
return func(this Type[A, O, I]) Type[B, O, I] {
return MakeType(
fmt.Sprintf("Pipe(%s, %s)", this.Name(), ab.Name()),
@@ -851,7 +851,7 @@ func FromRefinement[A, B any](refinement Refinement[A, B]) Type[B, A, A] {
// See also:
// - Id: For identity codecs that preserve values
// - MakeType: For creating custom codecs with validation logic
func Empty[A, O, I any](e Lazy[Pair[O, A]]) Type[A, O, I] {
func Empty[I, A, O any](e Lazy[Pair[O, A]]) Type[A, O, I] {
return MakeType(
"Empty",
Is[A](),

View File

@@ -1742,7 +1742,7 @@ func TestFromRefinementValidationContext(t *testing.T) {
// TestEmpty_Success tests that Empty always succeeds during decoding
func TestEmpty_Success(t *testing.T) {
t.Run("decodes any input to default value", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default", 42)))
// Test with various input types
testCases := []struct {
@@ -1766,7 +1766,7 @@ func TestEmpty_Success(t *testing.T) {
})
t.Run("always returns same default value", func(t *testing.T) {
defaultCodec := Empty[string, string, any](lazy.Of(pair.MakePair("output", "default")))
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("output", "default")))
result1 := defaultCodec.Decode(123)
result2 := defaultCodec.Decode("different")
@@ -1789,7 +1789,7 @@ func TestEmpty_Success(t *testing.T) {
// TestEmpty_Encoding tests that Empty always uses default output during encoding
func TestEmpty_Encoding(t *testing.T) {
t.Run("encodes any value to default output", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default", 42)))
// Test with various input values
testCases := []struct {
@@ -1811,7 +1811,7 @@ func TestEmpty_Encoding(t *testing.T) {
})
t.Run("always returns same default output", func(t *testing.T) {
defaultCodec := Empty[string, int, any](lazy.Of(pair.MakePair(999, "ignored")))
defaultCodec := Empty[any, string, int](lazy.Of(pair.MakePair(999, "ignored")))
encoded1 := defaultCodec.Encode("value1")
encoded2 := defaultCodec.Encode("value2")
@@ -1826,7 +1826,7 @@ func TestEmpty_Encoding(t *testing.T) {
// TestEmpty_Name tests that Empty has correct name
func TestEmpty_Name(t *testing.T) {
t.Run("has name 'Empty'", func(t *testing.T) {
defaultCodec := Empty[int, int, any](lazy.Of(pair.MakePair(0, 0)))
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(0, 0)))
assert.Equal(t, "Empty", defaultCodec.Name())
})
}
@@ -1834,7 +1834,7 @@ func TestEmpty_Name(t *testing.T) {
// TestEmpty_TypeChecking tests that Empty performs standard type checking
func TestEmpty_TypeChecking(t *testing.T) {
t.Run("Is checks for correct type", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default", 42)))
// Should succeed for int
result := defaultCodec.Is(100)
@@ -1846,7 +1846,7 @@ func TestEmpty_TypeChecking(t *testing.T) {
})
t.Run("Is checks for string type", func(t *testing.T) {
defaultCodec := Empty[string, string, any](lazy.Of(pair.MakePair("out", "in")))
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("out", "in")))
// Should succeed for string
result := defaultCodec.Is("hello")
@@ -1867,7 +1867,7 @@ func TestEmpty_LazyEvaluation(t *testing.T) {
return pair.MakePair(counter, counter*10)
}
defaultCodec := Empty[int, int, any](lazyPair)
defaultCodec := Empty[any, int, int](lazyPair)
// Each decode can get a different value if the lazy function is dynamic
result1 := defaultCodec.Decode("input1")
@@ -1897,7 +1897,7 @@ func TestEmpty_WithStructs(t *testing.T) {
t.Run("provides default struct value", func(t *testing.T) {
defaultConfig := Config{Timeout: 30, Retries: 3}
defaultCodec := Empty[Config, Config, any](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
defaultCodec := Empty[any, Config, Config](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
@@ -1914,7 +1914,7 @@ func TestEmpty_WithStructs(t *testing.T) {
defaultConfig := Config{Timeout: 30, Retries: 3}
inputConfig := Config{Timeout: 60, Retries: 5}
defaultCodec := Empty[Config, Config, any](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
defaultCodec := Empty[any, Config, Config](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
encoded := defaultCodec.Encode(inputConfig)
assert.Equal(t, 30, encoded.Timeout)
@@ -1926,7 +1926,7 @@ func TestEmpty_WithStructs(t *testing.T) {
func TestEmpty_WithPointers(t *testing.T) {
t.Run("provides default pointer value", func(t *testing.T) {
defaultValue := 42
defaultCodec := Empty[*int, *int, any](lazy.Of(pair.MakePair(&defaultValue, &defaultValue)))
defaultCodec := Empty[any, *int, *int](lazy.Of(pair.MakePair(&defaultValue, &defaultValue)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
@@ -1941,7 +1941,7 @@ func TestEmpty_WithPointers(t *testing.T) {
t.Run("provides nil pointer as default", func(t *testing.T) {
var nilPtr *int
defaultCodec := Empty[*int, *int, any](lazy.Of(pair.MakePair(nilPtr, nilPtr)))
defaultCodec := Empty[any, *int, *int](lazy.Of(pair.MakePair(nilPtr, nilPtr)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
@@ -1958,7 +1958,7 @@ func TestEmpty_WithPointers(t *testing.T) {
func TestEmpty_WithSlices(t *testing.T) {
t.Run("provides default slice value", func(t *testing.T) {
defaultSlice := []int{1, 2, 3}
defaultCodec := Empty[[]int, []int, any](lazy.Of(pair.MakePair(defaultSlice, defaultSlice)))
defaultCodec := Empty[any, []int, []int](lazy.Of(pair.MakePair(defaultSlice, defaultSlice)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
@@ -1972,7 +1972,7 @@ func TestEmpty_WithSlices(t *testing.T) {
t.Run("provides empty slice as default", func(t *testing.T) {
emptySlice := []int{}
defaultCodec := Empty[[]int, []int, any](lazy.Of(pair.MakePair(emptySlice, emptySlice)))
defaultCodec := Empty[any, []int, []int](lazy.Of(pair.MakePair(emptySlice, emptySlice)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
@@ -1988,7 +1988,7 @@ func TestEmpty_WithSlices(t *testing.T) {
// TestEmpty_DifferentInputOutput tests Empty with different input and output types
func TestEmpty_DifferentInputOutput(t *testing.T) {
t.Run("decodes to int, encodes to string", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("default-output", 42)))
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("default-output", 42)))
// Decode always returns 42
result := defaultCodec.Decode("any input")
@@ -2000,7 +2000,7 @@ func TestEmpty_DifferentInputOutput(t *testing.T) {
})
t.Run("decodes to string, encodes to int", func(t *testing.T) {
defaultCodec := Empty[string, int, any](lazy.Of(pair.MakePair(999, "default-value")))
defaultCodec := Empty[any, string, int](lazy.Of(pair.MakePair(999, "default-value")))
// Decode always returns "default-value"
result := defaultCodec.Decode(123)
@@ -2020,7 +2020,7 @@ func TestEmpty_DifferentInputOutput(t *testing.T) {
// TestEmpty_EdgeCases tests edge cases for Empty
func TestEmpty_EdgeCases(t *testing.T) {
t.Run("with zero values", func(t *testing.T) {
defaultCodec := Empty[int, int, any](lazy.Of(pair.MakePair(0, 0)))
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(0, 0)))
result := defaultCodec.Decode("anything")
assert.True(t, either.IsRight(result))
@@ -2035,7 +2035,7 @@ func TestEmpty_EdgeCases(t *testing.T) {
})
t.Run("with empty string", func(t *testing.T) {
defaultCodec := Empty[string, string, any](lazy.Of(pair.MakePair("", "")))
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("", "")))
result := defaultCodec.Decode("non-empty")
assert.True(t, either.IsRight(result))
@@ -2050,7 +2050,7 @@ func TestEmpty_EdgeCases(t *testing.T) {
})
t.Run("with false boolean", func(t *testing.T) {
defaultCodec := Empty[bool, bool, any](lazy.Of(pair.MakePair(false, false)))
defaultCodec := Empty[any, bool, bool](lazy.Of(pair.MakePair(false, false)))
result := defaultCodec.Decode(true)
assert.Equal(t, validation.Of(false), result)
@@ -2064,7 +2064,7 @@ func TestEmpty_EdgeCases(t *testing.T) {
func TestEmpty_Integration(t *testing.T) {
t.Run("composes with other codecs using Pipe", func(t *testing.T) {
// Create a codec that always provides a default int
defaultIntCodec := Empty[int, int, any](lazy.Of(pair.MakePair(42, 42)))
defaultIntCodec := Empty[any, int, int](lazy.Of(pair.MakePair(42, 42)))
// Create a refinement that only accepts positive integers
positiveIntPrism := prism.MakePrismWithName(
@@ -2090,7 +2090,7 @@ func TestEmpty_Integration(t *testing.T) {
t.Run("used as placeholder in generic contexts", func(t *testing.T) {
// Empty can be used where a codec is required but not actually used
unitCodec := Empty[Void, Void, any](
unitCodec := Empty[any, Void, Void](
lazy.Of(pair.MakePair(F.VOID, F.VOID)),
)
@@ -2105,7 +2105,7 @@ func TestEmpty_Integration(t *testing.T) {
// TestEmpty_RoundTrip tests that Empty maintains consistency
func TestEmpty_RoundTrip(t *testing.T) {
t.Run("decode then encode returns default output", func(t *testing.T) {
defaultCodec := Empty[int, string, any](lazy.Of(pair.MakePair("output", 42)))
defaultCodec := Empty[any, int, string](lazy.Of(pair.MakePair("output", 42)))
// Decode
result := defaultCodec.Decode("input")
@@ -2124,7 +2124,7 @@ func TestEmpty_RoundTrip(t *testing.T) {
})
t.Run("multiple round trips are consistent", func(t *testing.T) {
defaultCodec := Empty[int, int, any](lazy.Of(pair.MakePair(100, 50)))
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(100, 50)))
// First round trip
result1 := defaultCodec.Decode("input1")