mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-28 13:12:03 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3df1dca146 | ||
|
|
a0132e2e92 | ||
|
|
c6b342908d | ||
|
|
962237492f | ||
|
|
168a6e1072 | ||
|
|
4d67b1d254 |
@@ -151,6 +151,11 @@ func TestFromReaderResult_Success(t *testing.T) {
|
||||
- Don't manually handle `(value, error)` tuples when helpers exist
|
||||
- Don't use `either.MonadFold` in tests unless necessary
|
||||
|
||||
4. **Use Void Type for Unit Values**
|
||||
- Use `function.Void` (or `F.Void`) instead of `struct{}`
|
||||
- Use `function.VOID` (or `F.VOID`) instead of `struct{}{}`
|
||||
- Example: `Empty[F.Void, F.Void, any](lazy.Of(pair.MakePair(F.VOID, F.VOID)))`
|
||||
|
||||
### Error Handling
|
||||
|
||||
1. **In Production Code**
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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] {
|
||||
|
||||
213
v2/context/readerreaderioresult/eitherize.go
Normal file
213
v2/context/readerreaderioresult/eitherize.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/ioresult"
|
||||
)
|
||||
|
||||
// Eitherize converts a function that returns a value and error into a ReaderReaderIOResult.
|
||||
//
|
||||
// This function takes a function that accepts an outer context R and context.Context,
|
||||
// returning a value T and an error, and converts it into a ReaderReaderIOResult[R, T].
|
||||
// The error is automatically converted into the Left case of the Result, while successful
|
||||
// values become the Right case.
|
||||
//
|
||||
// This is particularly useful for integrating standard Go error-handling patterns into
|
||||
// the functional programming style of ReaderReaderIOResult. It is especially helpful
|
||||
// for adapting interface member functions that accept a context. When you have an
|
||||
// interface method with signature (receiver, context.Context) (T, error), you can
|
||||
// use Eitherize to convert it into a ReaderReaderIOResult where the receiver becomes
|
||||
// the outer reader context R.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - R: The outer reader context type (e.g., application configuration)
|
||||
// - T: The success value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes R and context.Context and returns (T, error)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - ReaderReaderIOResult[R, T]: A computation that depends on R and context.Context,
|
||||
// performs IO, and produces a Result[T]
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // A function using standard Go error handling
|
||||
// func fetchUser(cfg AppConfig, ctx context.Context) (*User, error) {
|
||||
// // Implementation that may return an error
|
||||
// return &User{ID: 1, Name: "Alice"}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to ReaderReaderIOResult
|
||||
// fetchUserRR := Eitherize(fetchUser)
|
||||
//
|
||||
// // Use in functional composition
|
||||
// result := F.Pipe1(
|
||||
// fetchUserRR,
|
||||
// Map[AppConfig](func(u *User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// // Execute with config and context
|
||||
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
|
||||
// outcome := result(cfg)(context.Background())()
|
||||
//
|
||||
// # Adapting Interface Methods
|
||||
//
|
||||
// Eitherize is particularly useful for adapting interface member functions:
|
||||
//
|
||||
// type UserRepository interface {
|
||||
// GetUser(ctx context.Context, id int) (*User, error)
|
||||
// }
|
||||
//
|
||||
// type UserRepo struct {
|
||||
// db *sql.DB
|
||||
// }
|
||||
//
|
||||
// func (r *UserRepo) GetUser(ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation
|
||||
// return &User{ID: id}, nil
|
||||
// }
|
||||
//
|
||||
// // Adapt the method by binding the first parameter (receiver)
|
||||
// repo := &UserRepo{db: db}
|
||||
// getUserRR := Eitherize(func(id int, ctx context.Context) (*User, error) {
|
||||
// return repo.GetUser(ctx, id)
|
||||
// })
|
||||
//
|
||||
// // Now getUserRR has type: ReaderReaderIOResult[int, *User]
|
||||
// // The receiver (repo) is captured in the closure
|
||||
// // The id becomes the outer reader context R
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Eitherize1: For functions that take an additional parameter
|
||||
// - ioresult.Eitherize2: The underlying conversion function
|
||||
func Eitherize[R, T any](f func(R, context.Context) (T, error)) ReaderReaderIOResult[R, T] {
|
||||
return F.Pipe1(
|
||||
ioresult.Eitherize2(f),
|
||||
F.Curry2,
|
||||
)
|
||||
}
|
||||
|
||||
// Eitherize1 converts a function that takes an additional parameter and returns a value
|
||||
// and error into a Kleisli arrow.
|
||||
//
|
||||
// This function takes a function that accepts an outer context R, context.Context, and
|
||||
// an additional parameter A, returning a value T and an error, and converts it into a
|
||||
// Kleisli arrow (A -> ReaderReaderIOResult[R, T]). The error is automatically converted
|
||||
// into the Left case of the Result, while successful values become the Right case.
|
||||
//
|
||||
// This is useful for creating composable operations that depend on both contexts and
|
||||
// an input value, following standard Go error-handling patterns. It is especially helpful
|
||||
// for adapting interface member functions that accept a context and additional parameters.
|
||||
// When you have an interface method with signature (receiver, context.Context, A) (T, error),
|
||||
// you can use Eitherize1 to convert it into a Kleisli arrow where the receiver becomes
|
||||
// the outer reader context R and A becomes the input parameter.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - R: The outer reader context type (e.g., application configuration)
|
||||
// - A: The input parameter type
|
||||
// - T: The success value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes R, context.Context, and A, returning (T, error)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Kleisli[R, A, T]: A function from A to ReaderReaderIOResult[R, T]
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // A function using standard Go error handling
|
||||
// func fetchUserByID(cfg AppConfig, ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation that may return an error
|
||||
// return &User{ID: id, Name: "Alice"}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to Kleisli arrow
|
||||
// fetchUserKleisli := Eitherize1(fetchUserByID)
|
||||
//
|
||||
// // Use in functional composition with Chain
|
||||
// pipeline := F.Pipe1(
|
||||
// Of[AppConfig](123),
|
||||
// Chain[AppConfig](fetchUserKleisli),
|
||||
// )
|
||||
//
|
||||
// // Execute with config and context
|
||||
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
|
||||
// outcome := pipeline(cfg)(context.Background())()
|
||||
//
|
||||
// # Adapting Interface Methods
|
||||
//
|
||||
// Eitherize1 is particularly useful for adapting interface member functions with parameters:
|
||||
//
|
||||
// type UserRepository interface {
|
||||
// GetUserByID(ctx context.Context, id int) (*User, error)
|
||||
// UpdateUser(ctx context.Context, user *User) error
|
||||
// }
|
||||
//
|
||||
// type UserRepo struct {
|
||||
// db *sql.DB
|
||||
// }
|
||||
//
|
||||
// func (r *UserRepo) GetUserByID(ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation
|
||||
// return &User{ID: id}, nil
|
||||
// }
|
||||
//
|
||||
// // Adapt the method - receiver becomes R, id becomes A
|
||||
// repo := &UserRepo{db: db}
|
||||
// getUserKleisli := Eitherize1(func(r *UserRepo, ctx context.Context, id int) (*User, error) {
|
||||
// return r.GetUserByID(ctx, id)
|
||||
// })
|
||||
//
|
||||
// // Now getUserKleisli has type: Kleisli[*UserRepo, int, *User]
|
||||
// // Which is: func(int) ReaderReaderIOResult[*UserRepo, *User]
|
||||
// // Use it in composition:
|
||||
// pipeline := F.Pipe1(
|
||||
// Of[*UserRepo](123),
|
||||
// Chain[*UserRepo](getUserKleisli),
|
||||
// )
|
||||
// result := pipeline(repo)(context.Background())()
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Eitherize: For functions without an additional parameter
|
||||
// - Chain: For composing Kleisli arrows
|
||||
// - ioresult.Eitherize3: The underlying conversion function
|
||||
func Eitherize1[R, A, T any](f func(R, context.Context, A) (T, error)) Kleisli[R, A, T] {
|
||||
return F.Flow2(
|
||||
F.Bind3of3(ioresult.Eitherize3(f)),
|
||||
F.Curry2,
|
||||
)
|
||||
}
|
||||
507
v2/context/readerreaderioresult/eitherize_test.go
Normal file
507
v2/context/readerreaderioresult/eitherize_test.go
Normal file
@@ -0,0 +1,507 @@
|
||||
// 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 readerreaderioresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestConfig struct {
|
||||
Prefix string
|
||||
MaxLen int
|
||||
}
|
||||
|
||||
var testConfig = TestConfig{
|
||||
Prefix: "test",
|
||||
MaxLen: 100,
|
||||
}
|
||||
|
||||
// TestEitherize_Success tests successful conversion with Eitherize
|
||||
func TestEitherize_Success(t *testing.T) {
|
||||
t.Run("converts successful function to ReaderReaderIOResult", func(t *testing.T) {
|
||||
// Arrange
|
||||
successFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return cfg.Prefix + "-success", nil
|
||||
}
|
||||
rr := Eitherize(successFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("test-success"), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves context values", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ctxKey string
|
||||
key := ctxKey("testKey")
|
||||
expectedValue := "contextValue"
|
||||
|
||||
contextFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
value := ctx.Value(key)
|
||||
if value == nil {
|
||||
return "", errors.New("context value not found")
|
||||
}
|
||||
return value.(string), nil
|
||||
}
|
||||
rr := Eitherize(contextFunc)
|
||||
|
||||
ctx := context.WithValue(context.Background(), key, expectedValue)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(ctx)()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(expectedValue), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// Arrange
|
||||
intFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return cfg.MaxLen, nil
|
||||
}
|
||||
rr := Eitherize(intFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(100), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_Failure tests error handling with Eitherize
|
||||
func TestEitherize_Failure(t *testing.T) {
|
||||
t.Run("converts error to Left", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("operation failed")
|
||||
failFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return "", expectedErr
|
||||
}
|
||||
rr := Eitherize(failFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
assert.Equal(t, result.Left[string](expectedErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error message", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := fmt.Errorf("validation error: field is required")
|
||||
failFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return 0, expectedErr
|
||||
}
|
||||
rr := Eitherize(failFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
leftValue := result.MonadFold(outcome,
|
||||
F.Identity[error],
|
||||
func(int) error { return nil },
|
||||
)
|
||||
assert.Equal(t, expectedErr, leftValue)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_EdgeCases tests edge cases for Eitherize
|
||||
func TestEitherize_EdgeCases(t *testing.T) {
|
||||
t.Run("handles nil context", func(t *testing.T) {
|
||||
// Arrange
|
||||
nilCtxFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
if ctx == nil {
|
||||
return "nil-context", nil
|
||||
}
|
||||
return "non-nil-context", nil
|
||||
}
|
||||
rr := Eitherize(nilCtxFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(nil)()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("nil-context"), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles zero value config", func(t *testing.T) {
|
||||
// Arrange
|
||||
zeroFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return cfg.Prefix, nil
|
||||
}
|
||||
rr := Eitherize(zeroFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(TestConfig{})(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(""), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles pointer types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context) (*User, error) {
|
||||
return &User{Name: "Alice"}, nil
|
||||
}
|
||||
rr := Eitherize(ptrFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsRight(outcome))
|
||||
user := result.MonadFold(outcome,
|
||||
func(error) *User { return nil },
|
||||
F.Identity[*User],
|
||||
)
|
||||
assert.NotNil(t, user)
|
||||
assert.Equal(t, "Alice", user.Name)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_Integration tests integration with other operations
|
||||
func TestEitherize_Integration(t *testing.T) {
|
||||
t.Run("composes with Map", func(t *testing.T) {
|
||||
// Arrange
|
||||
baseFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return 42, nil
|
||||
}
|
||||
rr := Eitherize(baseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
rr,
|
||||
Map[TestConfig](func(n int) string { return strconv.Itoa(n) }),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("42"), outcome)
|
||||
})
|
||||
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
firstFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return 10, nil
|
||||
}
|
||||
secondFunc := func(n int) ReaderReaderIOResult[TestConfig, string] {
|
||||
return Of[TestConfig](fmt.Sprintf("value: %d", n))
|
||||
}
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
Eitherize(firstFunc),
|
||||
Chain[TestConfig](secondFunc),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("value: 10"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Success tests successful conversion with Eitherize1
|
||||
func TestEitherize1_Success(t *testing.T) {
|
||||
t.Run("converts successful function to Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
addFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
return n + cfg.MaxLen, nil
|
||||
}
|
||||
kleisli := Eitherize1(addFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(10)(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(110), outcome)
|
||||
})
|
||||
|
||||
t.Run("works with string input", func(t *testing.T) {
|
||||
// Arrange
|
||||
concatFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
|
||||
return cfg.Prefix + "-" + s, nil
|
||||
}
|
||||
kleisli := Eitherize1(concatFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli("input")(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("test-input"), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves context in Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ctxKey string
|
||||
key := ctxKey("multiplier")
|
||||
|
||||
multiplyFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
multiplier := ctx.Value(key)
|
||||
if multiplier == nil {
|
||||
return n, nil
|
||||
}
|
||||
return n * multiplier.(int), nil
|
||||
}
|
||||
kleisli := Eitherize1(multiplyFunc)
|
||||
|
||||
ctx := context.WithValue(context.Background(), key, 3)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(5)(testConfig)(ctx)()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(15), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Failure tests error handling with Eitherize1
|
||||
func TestEitherize1_Failure(t *testing.T) {
|
||||
t.Run("converts error to Left in Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("division by zero")
|
||||
divideFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
if n == 0 {
|
||||
return 0, expectedErr
|
||||
}
|
||||
return 100 / n, nil
|
||||
}
|
||||
kleisli := Eitherize1(divideFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(0)(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
assert.Equal(t, result.Left[int](expectedErr), outcome)
|
||||
})
|
||||
|
||||
t.Run("preserves error context", func(t *testing.T) {
|
||||
// Arrange
|
||||
validateFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
|
||||
if len(s) > cfg.MaxLen {
|
||||
return "", fmt.Errorf("string too long: %d > %d", len(s), cfg.MaxLen)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
kleisli := Eitherize1(validateFunc)
|
||||
|
||||
longString := string(make([]byte, 200))
|
||||
|
||||
// Act
|
||||
outcome := kleisli(longString)(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
leftValue := result.MonadFold(outcome,
|
||||
F.Identity[error],
|
||||
func(string) error { return nil },
|
||||
)
|
||||
assert.Contains(t, leftValue.Error(), "string too long")
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_EdgeCases tests edge cases for Eitherize1
|
||||
func TestEitherize1_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero value input", func(t *testing.T) {
|
||||
// Arrange
|
||||
zeroFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
kleisli := Eitherize1(zeroFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(0)(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(0), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles pointer input", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
Value int
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
|
||||
if in == nil {
|
||||
return 0, errors.New("nil input")
|
||||
}
|
||||
return in.Value, nil
|
||||
}
|
||||
kleisli := Eitherize1(ptrFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(&Input{Value: 42})(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(42), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles nil pointer input", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
Value int
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
|
||||
if in == nil {
|
||||
return 0, errors.New("nil input")
|
||||
}
|
||||
return in.Value, nil
|
||||
}
|
||||
kleisli := Eitherize1(ptrFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli((*Input)(nil))(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Integration tests integration with other operations
|
||||
func TestEitherize1_Integration(t *testing.T) {
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
doubleFunc := func(n int) ReaderReaderIOResult[TestConfig, int] {
|
||||
return Of[TestConfig](n * 2)
|
||||
}
|
||||
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe2(
|
||||
Of[TestConfig]("42"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
Chain[TestConfig](doubleFunc),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(84), outcome)
|
||||
})
|
||||
|
||||
t.Run("handles error in chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
Of[TestConfig]("not-a-number"),
|
||||
Chain(parseKleisli),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsLeft(outcome))
|
||||
})
|
||||
|
||||
t.Run("composes multiple Kleisli arrows", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
formatFunc := func(cfg TestConfig, ctx context.Context, n int) (string, error) {
|
||||
return fmt.Sprintf("%s-%d", cfg.Prefix, n), nil
|
||||
}
|
||||
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
formatKleisli := Eitherize1(formatFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe2(
|
||||
Of[TestConfig]("123"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
Chain[TestConfig](formatKleisli),
|
||||
)
|
||||
outcome := pipeline(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of("test-123"), outcome)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_TypeSafety tests type safety across different scenarios
|
||||
func TestEitherize_TypeSafety(t *testing.T) {
|
||||
t.Run("Eitherize with complex types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ComplexResult struct {
|
||||
Data map[string]int
|
||||
Count int
|
||||
}
|
||||
|
||||
complexFunc := func(cfg TestConfig, ctx context.Context) (ComplexResult, error) {
|
||||
return ComplexResult{
|
||||
Data: map[string]int{"key": 42},
|
||||
Count: 1,
|
||||
}, nil
|
||||
}
|
||||
rr := Eitherize(complexFunc)
|
||||
|
||||
// Act
|
||||
outcome := rr(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.True(t, result.IsRight(outcome))
|
||||
value := result.MonadFold(outcome,
|
||||
func(error) ComplexResult { return ComplexResult{} },
|
||||
F.Identity[ComplexResult],
|
||||
)
|
||||
assert.Equal(t, 42, value.Data["key"])
|
||||
assert.Equal(t, 1, value.Count)
|
||||
})
|
||||
|
||||
t.Run("Eitherize1 with different input and output types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
ID int
|
||||
}
|
||||
type Output struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
convertFunc := func(cfg TestConfig, ctx context.Context, in Input) (Output, error) {
|
||||
return Output{Name: fmt.Sprintf("%s-%d", cfg.Prefix, in.ID)}, nil
|
||||
}
|
||||
kleisli := Eitherize1(convertFunc)
|
||||
|
||||
// Act
|
||||
outcome := kleisli(Input{ID: 99})(testConfig)(context.Background())()
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, result.Of(Output{Name: "test-99"}), outcome)
|
||||
})
|
||||
}
|
||||
208
v2/effect/eitherize.go
Normal file
208
v2/effect/eitherize.go
Normal file
@@ -0,0 +1,208 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
|
||||
)
|
||||
|
||||
// Eitherize converts a function that returns a value and error into an Effect.
|
||||
//
|
||||
// This function takes a function that accepts a context C and context.Context,
|
||||
// returning a value T and an error, and converts it into an Effect[C, T].
|
||||
// The error is automatically converted into a failure, while successful
|
||||
// values become successes.
|
||||
//
|
||||
// This is particularly useful for integrating standard Go error-handling patterns into
|
||||
// the effect system. It is especially helpful for adapting interface member functions
|
||||
// that accept a context. When you have an interface method with signature
|
||||
// (receiver, context.Context) (T, error), you can use Eitherize to convert it into
|
||||
// an Effect where the receiver becomes the context C.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - T: The success value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes C and context.Context and returns (T, error)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Effect[C, T]: An effect that depends on C, performs IO, and produces T
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // A function using standard Go error handling
|
||||
// func fetchUser(cfg AppConfig, ctx context.Context) (*User, error) {
|
||||
// // Implementation that may return an error
|
||||
// return &User{ID: 1, Name: "Alice"}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to Effect
|
||||
// fetchUserEffect := effect.Eitherize(fetchUser)
|
||||
//
|
||||
// // Use in functional composition
|
||||
// pipeline := F.Pipe1(
|
||||
// fetchUserEffect,
|
||||
// effect.Map[AppConfig](func(u *User) string { return u.Name }),
|
||||
// )
|
||||
//
|
||||
// // Execute with config
|
||||
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
|
||||
// result, err := effect.RunSync(effect.Provide[*User](cfg)(pipeline))(context.Background())
|
||||
//
|
||||
// # Adapting Interface Methods
|
||||
//
|
||||
// Eitherize is particularly useful for adapting interface member functions:
|
||||
//
|
||||
// type UserRepository interface {
|
||||
// GetUser(ctx context.Context, id int) (*User, error)
|
||||
// }
|
||||
//
|
||||
// type UserRepo struct {
|
||||
// db *sql.DB
|
||||
// }
|
||||
//
|
||||
// func (r *UserRepo) GetUser(ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation
|
||||
// return &User{ID: id}, nil
|
||||
// }
|
||||
//
|
||||
// // Adapt the method by binding the first parameter (receiver)
|
||||
// repo := &UserRepo{db: db}
|
||||
// getUserEffect := effect.Eitherize(func(id int, ctx context.Context) (*User, error) {
|
||||
// return repo.GetUser(ctx, id)
|
||||
// })
|
||||
//
|
||||
// // Now getUserEffect has type: Effect[int, *User]
|
||||
// // The receiver (repo) is captured in the closure
|
||||
// // The id becomes the context C
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Eitherize1: For functions that take an additional parameter
|
||||
// - readerreaderioresult.Eitherize: The underlying implementation
|
||||
//
|
||||
//go:inline
|
||||
func Eitherize[C, T any](f func(C, context.Context) (T, error)) Effect[C, T] {
|
||||
return readerreaderioresult.Eitherize(f)
|
||||
}
|
||||
|
||||
// Eitherize1 converts a function that takes an additional parameter and returns a value
|
||||
// and error into a Kleisli arrow.
|
||||
//
|
||||
// This function takes a function that accepts a context C, context.Context, and
|
||||
// an additional parameter A, returning a value T and an error, and converts it into a
|
||||
// Kleisli arrow (A -> Effect[C, T]). The error is automatically converted into a failure,
|
||||
// while successful values become successes.
|
||||
//
|
||||
// This is useful for creating composable operations that depend on context and
|
||||
// an input value, following standard Go error-handling patterns. It is especially helpful
|
||||
// for adapting interface member functions that accept a context and additional parameters.
|
||||
// When you have an interface method with signature (receiver, context.Context, A) (T, error),
|
||||
// you can use Eitherize1 to convert it into a Kleisli arrow where the receiver becomes
|
||||
// the context C and A becomes the input parameter.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - C: The context type required by the effect
|
||||
// - A: The input parameter type
|
||||
// - T: The success value type
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A function that takes C, context.Context, and A, returning (T, error)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - Kleisli[C, A, T]: A function from A to Effect[C, T]
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// type AppConfig struct {
|
||||
// DatabaseURL string
|
||||
// }
|
||||
//
|
||||
// // A function using standard Go error handling
|
||||
// func fetchUserByID(cfg AppConfig, ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation that may return an error
|
||||
// return &User{ID: id, Name: "Alice"}, nil
|
||||
// }
|
||||
//
|
||||
// // Convert to Kleisli arrow
|
||||
// fetchUserKleisli := effect.Eitherize1(fetchUserByID)
|
||||
//
|
||||
// // Use in functional composition with Chain
|
||||
// pipeline := F.Pipe1(
|
||||
// effect.Succeed[AppConfig](123),
|
||||
// effect.Chain[AppConfig](fetchUserKleisli),
|
||||
// )
|
||||
//
|
||||
// // Execute with config
|
||||
// cfg := AppConfig{DatabaseURL: "postgres://localhost"}
|
||||
// result, err := effect.RunSync(effect.Provide[*User](cfg)(pipeline))(context.Background())
|
||||
//
|
||||
// # Adapting Interface Methods
|
||||
//
|
||||
// Eitherize1 is particularly useful for adapting interface member functions with parameters:
|
||||
//
|
||||
// type UserRepository interface {
|
||||
// GetUserByID(ctx context.Context, id int) (*User, error)
|
||||
// UpdateUser(ctx context.Context, user *User) error
|
||||
// }
|
||||
//
|
||||
// type UserRepo struct {
|
||||
// db *sql.DB
|
||||
// }
|
||||
//
|
||||
// func (r *UserRepo) GetUserByID(ctx context.Context, id int) (*User, error) {
|
||||
// // Implementation
|
||||
// return &User{ID: id}, nil
|
||||
// }
|
||||
//
|
||||
// // Adapt the method - receiver becomes C, id becomes A
|
||||
// repo := &UserRepo{db: db}
|
||||
// getUserKleisli := effect.Eitherize1(func(r *UserRepo, ctx context.Context, id int) (*User, error) {
|
||||
// return r.GetUserByID(ctx, id)
|
||||
// })
|
||||
//
|
||||
// // Now getUserKleisli has type: Kleisli[*UserRepo, int, *User]
|
||||
// // Which is: func(int) Effect[*UserRepo, *User]
|
||||
// // Use it in composition:
|
||||
// pipeline := F.Pipe1(
|
||||
// effect.Succeed[*UserRepo](123),
|
||||
// effect.Chain[*UserRepo](getUserKleisli),
|
||||
// )
|
||||
// result, err := effect.RunSync(effect.Provide[*User](repo)(pipeline))(context.Background())
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Eitherize: For functions without an additional parameter
|
||||
// - Chain: For composing Kleisli arrows
|
||||
// - readerreaderioresult.Eitherize1: The underlying implementation
|
||||
//
|
||||
//go:inline
|
||||
func Eitherize1[C, A, T any](f func(C, context.Context, A) (T, error)) Kleisli[C, A, T] {
|
||||
return readerreaderioresult.Eitherize1(f)
|
||||
}
|
||||
507
v2/effect/eitherize_test.go
Normal file
507
v2/effect/eitherize_test.go
Normal file
@@ -0,0 +1,507 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package effect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestEitherize_Success tests successful conversion with Eitherize
|
||||
func TestEitherize_Success(t *testing.T) {
|
||||
t.Run("converts successful function to Effect", func(t *testing.T) {
|
||||
// Arrange
|
||||
successFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return cfg.Prefix + "-success", nil
|
||||
}
|
||||
eff := Eitherize(successFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG-success", result)
|
||||
})
|
||||
|
||||
t.Run("preserves context values", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ctxKey string
|
||||
key := ctxKey("testKey")
|
||||
expectedValue := "contextValue"
|
||||
|
||||
contextFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
value := ctx.Value(key)
|
||||
if value == nil {
|
||||
return "", errors.New("context value not found")
|
||||
}
|
||||
return value.(string), nil
|
||||
}
|
||||
eff := Eitherize(contextFunc)
|
||||
|
||||
// Act
|
||||
ioResult := Provide[string](testConfig)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
ctx := context.WithValue(context.Background(), key, expectedValue)
|
||||
result, err := readerResult(ctx)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedValue, result)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// Arrange
|
||||
intFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return cfg.Multiplier, nil
|
||||
}
|
||||
eff := Eitherize(intFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_Failure tests error handling with Eitherize
|
||||
func TestEitherize_Failure(t *testing.T) {
|
||||
t.Run("converts error to failure", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("operation failed")
|
||||
failFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return "", expectedErr
|
||||
}
|
||||
eff := Eitherize(failFunc)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("preserves error message", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := fmt.Errorf("validation error: field is required")
|
||||
failFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return 0, expectedErr
|
||||
}
|
||||
eff := Eitherize(failFunc)
|
||||
|
||||
// Act
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_EdgeCases tests edge cases for Eitherize
|
||||
func TestEitherize_EdgeCases(t *testing.T) {
|
||||
t.Run("handles nil context", func(t *testing.T) {
|
||||
// Arrange
|
||||
nilCtxFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
if ctx == nil {
|
||||
return "nil-context", nil
|
||||
}
|
||||
return "non-nil-context", nil
|
||||
}
|
||||
eff := Eitherize(nilCtxFunc)
|
||||
|
||||
// Act
|
||||
ioResult := Provide[string](testConfig)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
result, err := readerResult(nil)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "nil-context", result)
|
||||
})
|
||||
|
||||
t.Run("handles zero value config", func(t *testing.T) {
|
||||
// Arrange
|
||||
zeroFunc := func(cfg TestConfig, ctx context.Context) (string, error) {
|
||||
return cfg.Prefix, nil
|
||||
}
|
||||
eff := Eitherize(zeroFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, TestConfig{})
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", result)
|
||||
})
|
||||
|
||||
t.Run("handles pointer types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type User struct {
|
||||
Name string
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context) (*User, error) {
|
||||
return &User{Name: cfg.Prefix}, nil
|
||||
}
|
||||
eff := Eitherize(ptrFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, result)
|
||||
assert.Equal(t, "LOG", result.Name)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_Integration tests integration with other operations
|
||||
func TestEitherize_Integration(t *testing.T) {
|
||||
t.Run("composes with Map", func(t *testing.T) {
|
||||
// Arrange
|
||||
baseFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return cfg.Multiplier, nil
|
||||
}
|
||||
eff := Eitherize(baseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
eff,
|
||||
Map[TestConfig](func(n int) string { return strconv.Itoa(n) }),
|
||||
)
|
||||
result, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "3", result)
|
||||
})
|
||||
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
firstFunc := func(cfg TestConfig, ctx context.Context) (int, error) {
|
||||
return cfg.Multiplier, nil
|
||||
}
|
||||
secondFunc := func(n int) Effect[TestConfig, string] {
|
||||
return Succeed[TestConfig](fmt.Sprintf("value: %d", n))
|
||||
}
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
Eitherize(firstFunc),
|
||||
Chain[TestConfig](secondFunc),
|
||||
)
|
||||
result, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "value: 3", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Success tests successful conversion with Eitherize1
|
||||
func TestEitherize1_Success(t *testing.T) {
|
||||
t.Run("converts successful function to Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
multiplyFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
return n * cfg.Multiplier, nil
|
||||
}
|
||||
kleisli := Eitherize1(multiplyFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(10)
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 30, result)
|
||||
})
|
||||
|
||||
t.Run("works with string input", func(t *testing.T) {
|
||||
// Arrange
|
||||
concatFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
|
||||
return cfg.Prefix + "-" + s, nil
|
||||
}
|
||||
kleisli := Eitherize1(concatFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli("input")
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG-input", result)
|
||||
})
|
||||
|
||||
t.Run("preserves context in Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ctxKey string
|
||||
key := ctxKey("factor")
|
||||
|
||||
scaleFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
factor := ctx.Value(key)
|
||||
if factor == nil {
|
||||
return n * cfg.Multiplier, nil
|
||||
}
|
||||
return n * factor.(int), nil
|
||||
}
|
||||
kleisli := Eitherize1(scaleFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(5)
|
||||
ioResult := Provide[int](testConfig)(eff)
|
||||
readerResult := RunSync(ioResult)
|
||||
ctx := context.WithValue(context.Background(), key, 7)
|
||||
result, err := readerResult(ctx)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 35, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Failure tests error handling with Eitherize1
|
||||
func TestEitherize1_Failure(t *testing.T) {
|
||||
t.Run("converts error to failure in Kleisli", func(t *testing.T) {
|
||||
// Arrange
|
||||
expectedErr := errors.New("division by zero")
|
||||
divideFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
if n == 0 {
|
||||
return 0, expectedErr
|
||||
}
|
||||
return 100 / n, nil
|
||||
}
|
||||
kleisli := Eitherize1(divideFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(0)
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, expectedErr, err)
|
||||
})
|
||||
|
||||
t.Run("preserves error context", func(t *testing.T) {
|
||||
// Arrange
|
||||
validateFunc := func(cfg TestConfig, ctx context.Context, s string) (string, error) {
|
||||
if len(s) > 10 {
|
||||
return "", fmt.Errorf("string too long: %d > 10", len(s))
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
kleisli := Eitherize1(validateFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli("this-string-is-too-long")
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "string too long")
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_EdgeCases tests edge cases for Eitherize1
|
||||
func TestEitherize1_EdgeCases(t *testing.T) {
|
||||
t.Run("handles zero value input", func(t *testing.T) {
|
||||
// Arrange
|
||||
zeroFunc := func(cfg TestConfig, ctx context.Context, n int) (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
kleisli := Eitherize1(zeroFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(0)
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, result)
|
||||
})
|
||||
|
||||
t.Run("handles pointer input", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
Value int
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
|
||||
if in == nil {
|
||||
return 0, errors.New("nil input")
|
||||
}
|
||||
return in.Value * cfg.Multiplier, nil
|
||||
}
|
||||
kleisli := Eitherize1(ptrFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(&Input{Value: 7})
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 21, result)
|
||||
})
|
||||
|
||||
t.Run("handles nil pointer input", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
Value int
|
||||
}
|
||||
ptrFunc := func(cfg TestConfig, ctx context.Context, in *Input) (int, error) {
|
||||
if in == nil {
|
||||
return 0, errors.New("nil input")
|
||||
}
|
||||
return in.Value, nil
|
||||
}
|
||||
kleisli := Eitherize1(ptrFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli((*Input)(nil))
|
||||
_, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "nil input")
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize1_Integration tests integration with other operations
|
||||
func TestEitherize1_Integration(t *testing.T) {
|
||||
t.Run("composes with Chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
doubleFunc := func(n int) Effect[TestConfig, int] {
|
||||
return Succeed[TestConfig](n * 2)
|
||||
}
|
||||
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe2(
|
||||
Succeed[TestConfig]("42"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
Chain[TestConfig](doubleFunc),
|
||||
)
|
||||
result, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 84, result)
|
||||
})
|
||||
|
||||
t.Run("handles error in chain", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe1(
|
||||
Succeed[TestConfig]("not-a-number"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
)
|
||||
_, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("composes multiple Kleisli arrows", func(t *testing.T) {
|
||||
// Arrange
|
||||
parseFunc := func(cfg TestConfig, ctx context.Context, s string) (int, error) {
|
||||
return strconv.Atoi(s)
|
||||
}
|
||||
formatFunc := func(cfg TestConfig, ctx context.Context, n int) (string, error) {
|
||||
return fmt.Sprintf("%s-%d", cfg.Prefix, n), nil
|
||||
}
|
||||
|
||||
parseKleisli := Eitherize1(parseFunc)
|
||||
formatKleisli := Eitherize1(formatFunc)
|
||||
|
||||
// Act
|
||||
pipeline := F.Pipe2(
|
||||
Succeed[TestConfig]("123"),
|
||||
Chain[TestConfig](parseKleisli),
|
||||
Chain[TestConfig](formatKleisli),
|
||||
)
|
||||
result, err := runEffect(pipeline, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG-123", result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEitherize_TypeSafety tests type safety across different scenarios
|
||||
func TestEitherize_TypeSafety(t *testing.T) {
|
||||
t.Run("Eitherize with complex types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type ComplexResult struct {
|
||||
Data map[string]int
|
||||
Count int
|
||||
}
|
||||
|
||||
complexFunc := func(cfg TestConfig, ctx context.Context) (ComplexResult, error) {
|
||||
return ComplexResult{
|
||||
Data: map[string]int{cfg.Prefix: cfg.Multiplier},
|
||||
Count: cfg.Multiplier,
|
||||
}, nil
|
||||
}
|
||||
eff := Eitherize(complexFunc)
|
||||
|
||||
// Act
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, result.Data["LOG"])
|
||||
assert.Equal(t, 3, result.Count)
|
||||
})
|
||||
|
||||
t.Run("Eitherize1 with different input and output types", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Input struct {
|
||||
ID int
|
||||
}
|
||||
type Output struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
convertFunc := func(cfg TestConfig, ctx context.Context, in Input) (Output, error) {
|
||||
return Output{Name: fmt.Sprintf("%s-%d", cfg.Prefix, in.ID)}, nil
|
||||
}
|
||||
kleisli := Eitherize1(convertFunc)
|
||||
|
||||
// Act
|
||||
eff := kleisli(Input{ID: 99})
|
||||
result, err := runEffect(eff, testConfig)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "LOG-99", result.Name)
|
||||
})
|
||||
}
|
||||
@@ -55,5 +55,7 @@ type (
|
||||
// It's commonly used for filtering and conditional operations.
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
|
||||
// Pair represents a tuple of two values of types L and R.
|
||||
// It's commonly used to return multiple values from functions or to group related data.
|
||||
Pair[L, R any] = pair.Pair[L, R]
|
||||
)
|
||||
|
||||
@@ -288,5 +288,3 @@ func BenchmarkVoidMonoid_Empty(b *testing.B) {
|
||||
_ = m.Empty()
|
||||
}
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -814,5 +814,3 @@ func TestApSO_ErrorAccumulation(t *testing.T) {
|
||||
assert.NotEmpty(t, errors, "Should have validation errors")
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
@@ -100,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()),
|
||||
@@ -747,3 +748,114 @@ func FromRefinement[A, B any](refinement Refinement[A, B]) Type[B, A, A] {
|
||||
refinement.ReverseGet,
|
||||
)
|
||||
}
|
||||
|
||||
// Empty creates a Type codec that ignores input during decoding and uses a default value,
|
||||
// and ignores the value during encoding, using a default output.
|
||||
//
|
||||
// This codec is useful for:
|
||||
// - Providing default values for optional fields
|
||||
// - Creating placeholder codecs in generic contexts
|
||||
// - Implementing constant codecs that always produce the same value
|
||||
// - Building codecs for phantom types or unit-like types
|
||||
//
|
||||
// The codec uses a lazily-evaluated Pair[O, A] to provide both the default output
|
||||
// for encoding and the default value for decoding. The lazy evaluation ensures that
|
||||
// the defaults are only computed when needed.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The target type (what we decode to and encode from)
|
||||
// - O: The output type (what we encode to)
|
||||
// - I: The input type (what we decode from, but is ignored)
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - e: A Lazy[Pair[O, A]] that provides the default values:
|
||||
// - pair.Head(e()): The default output value O used during encoding
|
||||
// - pair.Tail(e()): The default decoded value A used during decoding
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A Type[A, O, I] that:
|
||||
// - Decode: Always succeeds and returns the default value A, ignoring input I
|
||||
// - Encode: Always returns the default output O, ignoring the input value A
|
||||
// - Is: Checks if a value is of type A (standard type checking)
|
||||
// - Name: Returns "Empty"
|
||||
//
|
||||
// # Behavior
|
||||
//
|
||||
// Decoding:
|
||||
// - Ignores the input value completely
|
||||
// - Always succeeds with validation.Success
|
||||
// - Returns the default value from pair.Tail(e())
|
||||
//
|
||||
// Encoding:
|
||||
// - Ignores the input value completely
|
||||
// - Always returns the default output from pair.Head(e())
|
||||
//
|
||||
// # Example Usage
|
||||
//
|
||||
// Creating a codec with default values:
|
||||
//
|
||||
// // Create a codec that always decodes to 42 and encodes to "default"
|
||||
// defaultCodec := codec.Empty[int, string, any](lazy.Of(pair.MakePair("default", 42)))
|
||||
//
|
||||
// // Decode always returns 42, regardless of input
|
||||
// result := defaultCodec.Decode("anything") // Success: Right(42)
|
||||
// result = defaultCodec.Decode(123) // Success: Right(42)
|
||||
// result = defaultCodec.Decode(nil) // Success: Right(42)
|
||||
//
|
||||
// // Encode always returns "default", regardless of input
|
||||
// encoded := defaultCodec.Encode(100) // Returns: "default"
|
||||
// encoded = defaultCodec.Encode(0) // Returns: "default"
|
||||
//
|
||||
// Using with struct fields for default values:
|
||||
//
|
||||
// type Config struct {
|
||||
// Timeout int
|
||||
// Retries int
|
||||
// }
|
||||
//
|
||||
// // Codec that provides default retries value
|
||||
// defaultRetries := codec.Empty[int, int, any](lazy.Of(pair.MakePair(3, 3)))
|
||||
//
|
||||
// configCodec := F.Pipe2(
|
||||
// codec.Struct[Config]("Config"),
|
||||
// codec.ApSL(S.Monoid, timeoutLens, codec.Int()),
|
||||
// codec.ApSL(S.Monoid, retriesLens, defaultRetries),
|
||||
// )
|
||||
//
|
||||
// Creating a unit-like codec:
|
||||
//
|
||||
// // Codec for a unit type that always produces Void
|
||||
// unitCodec := codec.Empty[function.Void, function.Void, any](
|
||||
// lazy.Of(pair.MakePair(function.VOID, function.VOID)),
|
||||
// )
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// - Default values: Provide fallback values when decoding optional fields
|
||||
// - Constant codecs: Always produce the same value regardless of input
|
||||
// - Placeholder codecs: Use in generic contexts where a codec is required but not used
|
||||
// - Unit types: Encode/decode unit-like types that carry no information
|
||||
// - Testing: Create simple codecs for testing codec composition
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The lazy evaluation of the Pair ensures defaults are only computed when needed
|
||||
// - Both encoding and decoding always succeed (no validation errors)
|
||||
// - The input values are completely ignored in both directions
|
||||
// - The Is method still performs standard type checking for type A
|
||||
// - This codec is useful in applicative composition where some fields have defaults
|
||||
//
|
||||
// See also:
|
||||
// - Id: For identity codecs that preserve values
|
||||
// - MakeType: For creating custom codecs with validation logic
|
||||
func Empty[I, A, O any](e Lazy[Pair[O, A]]) Type[A, O, I] {
|
||||
return MakeType(
|
||||
"Empty",
|
||||
Is[A](),
|
||||
validate.OfLazy[I](F.Pipe1(e, lazy.Map(pair.Tail[O, A]))),
|
||||
reader.OfLazy[A](F.Pipe1(e, lazy.Map(pair.Head[O, A]))),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import (
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/pair"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -19,12 +21,7 @@ func TestString(t *testing.T) {
|
||||
stringType := String()
|
||||
result := stringType.Decode("hello")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "hello", value)
|
||||
assert.Equal(t, validation.Of("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-string", func(t *testing.T) {
|
||||
@@ -57,12 +54,7 @@ func TestString(t *testing.T) {
|
||||
stringType := String()
|
||||
result := stringType.Decode("")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "error" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
assert.Equal(t, validation.Of(""), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,12 +63,7 @@ func TestInt(t *testing.T) {
|
||||
intType := Int()
|
||||
result := intType.Decode(42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("fails to decode string as int", func(t *testing.T) {
|
||||
@@ -109,24 +96,14 @@ func TestInt(t *testing.T) {
|
||||
intType := Int()
|
||||
result := intType.Decode(-42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, -42, value)
|
||||
assert.Equal(t, validation.Of(-42), result)
|
||||
})
|
||||
|
||||
t.Run("decodes zero", func(t *testing.T) {
|
||||
intType := Int()
|
||||
result := intType.Decode(0)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
assert.Equal(t, validation.Of(0), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -135,24 +112,14 @@ func TestBool(t *testing.T) {
|
||||
boolType := Bool()
|
||||
result := boolType.Decode(true)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) bool { return false },
|
||||
F.Identity[bool],
|
||||
)
|
||||
assert.Equal(t, true, value)
|
||||
assert.Equal(t, validation.Of(true), result)
|
||||
})
|
||||
|
||||
t.Run("decodes false", func(t *testing.T) {
|
||||
boolType := Bool()
|
||||
result := boolType.Decode(false)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) bool { return true },
|
||||
F.Identity[bool],
|
||||
)
|
||||
assert.Equal(t, false, value)
|
||||
assert.Equal(t, validation.Of(false), result)
|
||||
})
|
||||
|
||||
t.Run("fails to decode int as bool", func(t *testing.T) {
|
||||
@@ -189,36 +156,21 @@ func TestArray(t *testing.T) {
|
||||
intArray := Array(Int())
|
||||
result := intArray.Decode([]int{1, 2, 3})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3}, value)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes valid string array", func(t *testing.T) {
|
||||
stringArray := Array(String())
|
||||
result := stringArray.Decode([]string{"a", "b", "c"})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []string { return nil },
|
||||
F.Identity[[]string],
|
||||
)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, value)
|
||||
assert.Equal(t, validation.Of([]string{"a", "b", "c"}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes empty array", func(t *testing.T) {
|
||||
intArray := Array(Int())
|
||||
result := intArray.Decode([]int{})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{}, value)
|
||||
assert.Equal(t, validation.Of([]int{}), result)
|
||||
})
|
||||
|
||||
t.Run("fails when array contains invalid element", func(t *testing.T) {
|
||||
@@ -256,12 +208,7 @@ func TestArray(t *testing.T) {
|
||||
nestedArray := Array(Array(Int()))
|
||||
result := nestedArray.Decode([][]int{{1, 2}, {3, 4}})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) [][]int { return nil },
|
||||
F.Identity[[][]int],
|
||||
)
|
||||
assert.Equal(t, [][]int{{1, 2}, {3, 4}}, value)
|
||||
assert.Equal(t, validation.Of([][]int{{1, 2}, {3, 4}}), result)
|
||||
})
|
||||
|
||||
t.Run("fails to decode non-iterable", func(t *testing.T) {
|
||||
@@ -275,12 +222,7 @@ func TestArray(t *testing.T) {
|
||||
boolArray := Array(Bool())
|
||||
result := boolArray.Decode([]bool{true, false, true})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []bool { return nil },
|
||||
F.Identity[[]bool],
|
||||
)
|
||||
assert.Equal(t, []bool{true, false, true}, value)
|
||||
assert.Equal(t, validation.Of([]bool{true, false, true}), result)
|
||||
})
|
||||
|
||||
t.Run("collects multiple validation errors", func(t *testing.T) {
|
||||
@@ -360,24 +302,14 @@ func TestTranscodeArray(t *testing.T) {
|
||||
intTranscode := TranscodeArray(Int())
|
||||
result := intTranscode.Decode([]any{1, 2, 3})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3}, value)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes valid string array from string slice", func(t *testing.T) {
|
||||
stringTranscode := TranscodeArray(String())
|
||||
result := stringTranscode.Decode([]any{"a", "b", "c"})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []string { return nil },
|
||||
F.Identity[[]string],
|
||||
)
|
||||
assert.Equal(t, []string{"a", "b", "c"}, value)
|
||||
assert.Equal(t, validation.Of([]string{"a", "b", "c"}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes empty array", func(t *testing.T) {
|
||||
@@ -411,24 +343,14 @@ func TestTranscodeArray(t *testing.T) {
|
||||
nestedTranscode := TranscodeArray(TranscodeArray(Int()))
|
||||
result := nestedTranscode.Decode([][]any{{1, 2}, {3, 4}})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) [][]int { return nil },
|
||||
F.Identity[[][]int],
|
||||
)
|
||||
assert.Equal(t, [][]int{{1, 2}, {3, 4}}, value)
|
||||
assert.Equal(t, validation.Of([][]int{{1, 2}, {3, 4}}), result)
|
||||
})
|
||||
|
||||
t.Run("decodes array of bools", func(t *testing.T) {
|
||||
boolTranscode := TranscodeArray(Bool())
|
||||
result := boolTranscode.Decode([]any{true, false, true})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []bool { return nil },
|
||||
F.Identity[[]bool],
|
||||
)
|
||||
assert.Equal(t, []bool{true, false, true}, value)
|
||||
assert.Equal(t, validation.Of([]bool{true, false, true}), result)
|
||||
})
|
||||
|
||||
t.Run("encodes empty array", func(t *testing.T) {
|
||||
@@ -481,12 +403,7 @@ func TestTranscodeArrayWithTransformation(t *testing.T) {
|
||||
arrayTranscode := TranscodeArray(stringToInt)
|
||||
result := arrayTranscode.Decode([]string{"a", "bb", "ccc"})
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3}, value)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3}), result)
|
||||
})
|
||||
|
||||
t.Run("encodes int slice to string slice", func(t *testing.T) {
|
||||
@@ -1358,24 +1275,14 @@ func TestId(t *testing.T) {
|
||||
idCodec := Id[string]()
|
||||
result := idCodec.Decode("hello")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "hello", value)
|
||||
assert.Equal(t, validation.Of("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes int successfully", func(t *testing.T) {
|
||||
idCodec := Id[int]()
|
||||
result := idCodec.Decode(42)
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 42, value)
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
t.Run("encodes with identity function", func(t *testing.T) {
|
||||
@@ -1431,13 +1338,7 @@ func TestId(t *testing.T) {
|
||||
person := Person{Name: "Alice", Age: 30}
|
||||
|
||||
result := idCodec.Decode(person)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Person { return Person{} },
|
||||
F.Identity[Person],
|
||||
)
|
||||
assert.Equal(t, person, value)
|
||||
assert.Equal(t, validation.Of(person), result)
|
||||
|
||||
encoded := idCodec.Encode(person)
|
||||
assert.Equal(t, person, encoded)
|
||||
@@ -1450,13 +1351,7 @@ func TestIdWithTranscodeArray(t *testing.T) {
|
||||
arrayCodec := TranscodeArray(intId)
|
||||
|
||||
result := arrayCodec.Decode([]int{1, 2, 3, 4, 5})
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, value)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3, 4, 5}), result)
|
||||
})
|
||||
|
||||
t.Run("Id codec encodes array with identity", func(t *testing.T) {
|
||||
@@ -1473,13 +1368,7 @@ func TestIdWithTranscodeArray(t *testing.T) {
|
||||
|
||||
input := [][]int{{1, 2}, {3, 4}, {5}}
|
||||
result := nestedCodec.Decode(input)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) [][]int { return nil },
|
||||
F.Identity[[][]int],
|
||||
)
|
||||
assert.Equal(t, input, value)
|
||||
assert.Equal(t, validation.Of(input), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1748,7 +1637,7 @@ func TestFromRefinementComposition(t *testing.T) {
|
||||
positiveCodec := FromRefinement(positiveIntPrism)
|
||||
|
||||
// Compose with Int codec using Pipe
|
||||
composed := Pipe[int, any, int, int](positiveCodec)(Int())
|
||||
composed := Pipe[int, any](positiveCodec)(Int())
|
||||
|
||||
t.Run("ComposedDecodeValid", func(t *testing.T) {
|
||||
result := composed.Decode(42)
|
||||
@@ -1849,3 +1738,416 @@ func TestFromRefinementValidationContext(t *testing.T) {
|
||||
assert.Equal(t, -5, err.Value)
|
||||
})
|
||||
}
|
||||
|
||||
// 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[any, int, string](lazy.Of(pair.MakePair("default", 42)))
|
||||
|
||||
// Test with various input types
|
||||
testCases := []struct {
|
||||
name string
|
||||
input any
|
||||
}{
|
||||
{"string input", "anything"},
|
||||
{"int input", 123},
|
||||
{"nil input", nil},
|
||||
{"bool input", true},
|
||||
{"struct input", struct{ X int }{X: 10}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := defaultCodec.Decode(tc.input)
|
||||
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("always returns same default value", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("output", "default")))
|
||||
|
||||
result1 := defaultCodec.Decode(123)
|
||||
result2 := defaultCodec.Decode("different")
|
||||
result3 := defaultCodec.Decode(nil)
|
||||
|
||||
assert.True(t, either.IsRight(result1))
|
||||
assert.True(t, either.IsRight(result2))
|
||||
assert.True(t, either.IsRight(result3))
|
||||
|
||||
value1 := either.MonadFold(result1, func(validation.Errors) string { return "" }, F.Identity[string])
|
||||
value2 := either.MonadFold(result2, func(validation.Errors) string { return "" }, F.Identity[string])
|
||||
value3 := either.MonadFold(result3, func(validation.Errors) string { return "" }, F.Identity[string])
|
||||
|
||||
assert.Equal(t, "default", value1)
|
||||
assert.Equal(t, "default", value2)
|
||||
assert.Equal(t, "default", value3)
|
||||
})
|
||||
}
|
||||
|
||||
// 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[any, int, string](lazy.Of(pair.MakePair("default", 42)))
|
||||
|
||||
// Test with various input values
|
||||
testCases := []struct {
|
||||
name string
|
||||
input int
|
||||
}{
|
||||
{"zero value", 0},
|
||||
{"positive value", 100},
|
||||
{"negative value", -50},
|
||||
{"default value", 42},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
encoded := defaultCodec.Encode(tc.input)
|
||||
assert.Equal(t, "default", encoded)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("always returns same default output", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, int](lazy.Of(pair.MakePair(999, "ignored")))
|
||||
|
||||
encoded1 := defaultCodec.Encode("value1")
|
||||
encoded2 := defaultCodec.Encode("value2")
|
||||
encoded3 := defaultCodec.Encode("")
|
||||
|
||||
assert.Equal(t, 999, encoded1)
|
||||
assert.Equal(t, 999, encoded2)
|
||||
assert.Equal(t, 999, encoded3)
|
||||
})
|
||||
}
|
||||
|
||||
// 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[any, int, int](lazy.Of(pair.MakePair(0, 0)))
|
||||
assert.Equal(t, "Empty", defaultCodec.Name())
|
||||
})
|
||||
}
|
||||
|
||||
// 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[any, int, string](lazy.Of(pair.MakePair("default", 42)))
|
||||
|
||||
// Should succeed for int
|
||||
result := defaultCodec.Is(100)
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should fail for non-int
|
||||
result = defaultCodec.Is("not an int")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("Is checks for string type", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("out", "in")))
|
||||
|
||||
// Should succeed for string
|
||||
result := defaultCodec.Is("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should fail for non-string
|
||||
result = defaultCodec.Is(123)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_LazyEvaluation tests that the Pair parameter allows dynamic values
|
||||
func TestEmpty_LazyEvaluation(t *testing.T) {
|
||||
t.Run("lazy pair allows dynamic values", func(t *testing.T) {
|
||||
counter := 0
|
||||
lazyPair := func() pair.Pair[int, int] {
|
||||
counter++
|
||||
return pair.MakePair(counter, counter*10)
|
||||
}
|
||||
|
||||
defaultCodec := Empty[any, int, int](lazyPair)
|
||||
|
||||
// Each decode can get a different value if the lazy function is dynamic
|
||||
result1 := defaultCodec.Decode("input1")
|
||||
value1 := either.MonadFold(result1,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
result2 := defaultCodec.Decode("input2")
|
||||
value2 := either.MonadFold(result2,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Values can be different if lazy function produces different results
|
||||
assert.True(t, value1 > 0)
|
||||
assert.True(t, value2 > 0)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_WithStructs tests Empty with struct types
|
||||
func TestEmpty_WithStructs(t *testing.T) {
|
||||
type Config struct {
|
||||
Timeout int
|
||||
Retries int
|
||||
}
|
||||
|
||||
t.Run("provides default struct value", func(t *testing.T) {
|
||||
defaultConfig := Config{Timeout: 30, Retries: 3}
|
||||
defaultCodec := Empty[any, Config, Config](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) Config { return Config{} },
|
||||
F.Identity[Config],
|
||||
)
|
||||
assert.Equal(t, 30, value.Timeout)
|
||||
assert.Equal(t, 3, value.Retries)
|
||||
})
|
||||
|
||||
t.Run("encodes to default struct", func(t *testing.T) {
|
||||
defaultConfig := Config{Timeout: 30, Retries: 3}
|
||||
inputConfig := Config{Timeout: 60, Retries: 5}
|
||||
|
||||
defaultCodec := Empty[any, Config, Config](lazy.Of(pair.MakePair(defaultConfig, defaultConfig)))
|
||||
|
||||
encoded := defaultCodec.Encode(inputConfig)
|
||||
assert.Equal(t, 30, encoded.Timeout)
|
||||
assert.Equal(t, 3, encoded.Retries)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_WithPointers tests Empty with pointer types
|
||||
func TestEmpty_WithPointers(t *testing.T) {
|
||||
t.Run("provides default pointer value", func(t *testing.T) {
|
||||
defaultValue := 42
|
||||
defaultCodec := Empty[any, *int, *int](lazy.Of(pair.MakePair(&defaultValue, &defaultValue)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) *int { return nil },
|
||||
F.Identity[*int],
|
||||
)
|
||||
require.NotNil(t, value)
|
||||
assert.Equal(t, 42, *value)
|
||||
})
|
||||
|
||||
t.Run("provides nil pointer as default", func(t *testing.T) {
|
||||
var nilPtr *int
|
||||
defaultCodec := Empty[any, *int, *int](lazy.Of(pair.MakePair(nilPtr, nilPtr)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) *int { return new(int) },
|
||||
F.Identity[*int],
|
||||
)
|
||||
assert.Nil(t, value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_WithSlices tests Empty with slice types
|
||||
func TestEmpty_WithSlices(t *testing.T) {
|
||||
t.Run("provides default slice value", func(t *testing.T) {
|
||||
defaultSlice := []int{1, 2, 3}
|
||||
defaultCodec := Empty[any, []int, []int](lazy.Of(pair.MakePair(defaultSlice, defaultSlice)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{1, 2, 3}, value)
|
||||
})
|
||||
|
||||
t.Run("provides empty slice as default", func(t *testing.T) {
|
||||
emptySlice := []int{}
|
||||
defaultCodec := Empty[any, []int, []int](lazy.Of(pair.MakePair(emptySlice, emptySlice)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) []int { return nil },
|
||||
F.Identity[[]int],
|
||||
)
|
||||
assert.Equal(t, []int{}, value)
|
||||
})
|
||||
}
|
||||
|
||||
// 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[any, int, string](lazy.Of(pair.MakePair("default-output", 42)))
|
||||
|
||||
// Decode always returns 42
|
||||
result := defaultCodec.Decode("any input")
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
|
||||
// Encode always returns "default-output"
|
||||
encoded := defaultCodec.Encode(100)
|
||||
assert.Equal(t, "default-output", encoded)
|
||||
})
|
||||
|
||||
t.Run("decodes to string, encodes to int", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, int](lazy.Of(pair.MakePair(999, "default-value")))
|
||||
|
||||
// Decode always returns "default-value"
|
||||
result := defaultCodec.Decode(123)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "default-value", value)
|
||||
|
||||
// Encode always returns 999
|
||||
encoded := defaultCodec.Encode("any string")
|
||||
assert.Equal(t, 999, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_EdgeCases tests edge cases for Empty
|
||||
func TestEmpty_EdgeCases(t *testing.T) {
|
||||
t.Run("with zero values", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(0, 0)))
|
||||
|
||||
result := defaultCodec.Decode("anything")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) int { return -1 },
|
||||
F.Identity[int],
|
||||
)
|
||||
assert.Equal(t, 0, value)
|
||||
|
||||
encoded := defaultCodec.Encode(100)
|
||||
assert.Equal(t, 0, encoded)
|
||||
})
|
||||
|
||||
t.Run("with empty string", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, string, string](lazy.Of(pair.MakePair("", "")))
|
||||
|
||||
result := defaultCodec.Decode("non-empty")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) string { return "error" },
|
||||
F.Identity[string],
|
||||
)
|
||||
assert.Equal(t, "", value)
|
||||
|
||||
encoded := defaultCodec.Encode("non-empty")
|
||||
assert.Equal(t, "", encoded)
|
||||
})
|
||||
|
||||
t.Run("with false boolean", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, bool, bool](lazy.Of(pair.MakePair(false, false)))
|
||||
|
||||
result := defaultCodec.Decode(true)
|
||||
assert.Equal(t, validation.Of(false), result)
|
||||
|
||||
encoded := defaultCodec.Encode(true)
|
||||
assert.Equal(t, false, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// TestEmpty_Integration tests Empty in composition scenarios
|
||||
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[any, int, int](lazy.Of(pair.MakePair(42, 42)))
|
||||
|
||||
// Create a refinement that only accepts positive integers
|
||||
positiveIntPrism := prism.MakePrismWithName(
|
||||
func(n int) option.Option[int] {
|
||||
if n > 0 {
|
||||
return option.Some(n)
|
||||
}
|
||||
return option.None[int]()
|
||||
},
|
||||
func(n int) int { return n },
|
||||
"PositiveInt",
|
||||
)
|
||||
|
||||
positiveCodec := FromRefinement(positiveIntPrism)
|
||||
|
||||
// Compose: always decode to 42, then validate it's positive
|
||||
composed := Pipe[int, any](positiveCodec)(defaultIntCodec)
|
||||
|
||||
// Should succeed because 42 is positive
|
||||
result := composed.Decode("anything")
|
||||
assert.Equal(t, validation.Of(42), result)
|
||||
})
|
||||
|
||||
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[any, Void, Void](
|
||||
lazy.Of(pair.MakePair(F.VOID, F.VOID)),
|
||||
)
|
||||
|
||||
result := unitCodec.Decode("ignored")
|
||||
assert.Equal(t, validation.Of(F.VOID), result)
|
||||
|
||||
encoded := unitCodec.Encode(F.VOID)
|
||||
assert.Equal(t, F.VOID, encoded)
|
||||
})
|
||||
}
|
||||
|
||||
// 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[any, int, string](lazy.Of(pair.MakePair("output", 42)))
|
||||
|
||||
// Decode
|
||||
result := defaultCodec.Decode("input")
|
||||
require.True(t, either.IsRight(result))
|
||||
|
||||
decoded := either.MonadFold(result,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
|
||||
// Encode
|
||||
encoded := defaultCodec.Encode(decoded)
|
||||
|
||||
// Should get default output, not related to decoded value
|
||||
assert.Equal(t, "output", encoded)
|
||||
})
|
||||
|
||||
t.Run("multiple round trips are consistent", func(t *testing.T) {
|
||||
defaultCodec := Empty[any, int, int](lazy.Of(pair.MakePair(100, 50)))
|
||||
|
||||
// First round trip
|
||||
result1 := defaultCodec.Decode("input1")
|
||||
decoded1 := either.MonadFold(result1,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
encoded1 := defaultCodec.Encode(decoded1)
|
||||
|
||||
// Second round trip
|
||||
result2 := defaultCodec.Decode("input2")
|
||||
decoded2 := either.MonadFold(result2,
|
||||
func(validation.Errors) int { return 0 },
|
||||
F.Identity[int],
|
||||
)
|
||||
encoded2 := defaultCodec.Encode(decoded2)
|
||||
|
||||
// All decoded values should be the same
|
||||
assert.Equal(t, 50, decoded1)
|
||||
assert.Equal(t, 50, decoded2)
|
||||
|
||||
// All encoded values should be the same
|
||||
assert.Equal(t, 100, encoded1)
|
||||
assert.Equal(t, 100, encoded2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,12 +19,7 @@ func TestDo(t *testing.T) {
|
||||
decoder := Do[string](State{})
|
||||
result := decoder("input")
|
||||
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{}, value)
|
||||
assert.Equal(t, validation.Of(State{}), result)
|
||||
})
|
||||
|
||||
t.Run("creates decoder with initialized state", func(t *testing.T) {
|
||||
@@ -79,12 +74,7 @@ func TestBind(t *testing.T) {
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 42, y: 10}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 42, y: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("propagates failure", func(t *testing.T) {
|
||||
@@ -216,12 +206,7 @@ func TestLet(t *testing.T) {
|
||||
)
|
||||
|
||||
result := decoder("input")
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(validation.Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 60, y: 10, z: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,47 @@ func Of[I, A any](a A) Decode[I, A] {
|
||||
return readereither.Of[I, Errors](a)
|
||||
}
|
||||
|
||||
// OfLazy converts a lazy computation into a Decode that ignores its input.
|
||||
// The resulting Decode will evaluate the lazy computation when executed and wrap
|
||||
// the result in a successful validation, regardless of the input provided.
|
||||
//
|
||||
// This function is intended solely for deferring the computation of a value, NOT for
|
||||
// representing side effects. The lazy computation should be a pure function that
|
||||
// produces the same result each time it's called (referential transparency). For
|
||||
// operations with side effects, use appropriate effect types like IO or IOResult.
|
||||
//
|
||||
// This is useful for lifting deferred computations into the Decode context without
|
||||
// requiring access to the input, while maintaining the validation wrapper for consistency.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - I: The input type (ignored by the resulting Decode)
|
||||
// - A: The result type produced by the lazy computation
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: A lazy computation that produces a value of type A (must be pure, no side effects)
|
||||
//
|
||||
// Returns:
|
||||
// - A Decode that ignores its input, evaluates the lazy computation, and wraps the result in Validation[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// lazyValue := func() int { return 42 }
|
||||
// decoder := decode.OfLazy[string](lazyValue)
|
||||
// result := decoder("any input") // validation.Success(42)
|
||||
//
|
||||
// Example - Deferring expensive computation:
|
||||
//
|
||||
// expensiveCalc := func() Config {
|
||||
// // Expensive but pure computation here
|
||||
// return computeDefaultConfig()
|
||||
// }
|
||||
// decoder := decode.OfLazy[map[string]any](expensiveCalc)
|
||||
// // Computation is deferred until the Decode is executed
|
||||
// result := decoder(inputData) // validation.Success(config)
|
||||
func OfLazy[I, A any](fa Lazy[A]) Decode[I, A] {
|
||||
return readereither.OfLazy[I, Errors](fa)
|
||||
}
|
||||
|
||||
// Left creates a Decode that always fails with the given validation errors.
|
||||
// This is the dual of Of - while Of lifts a success value, Left lifts failure errors
|
||||
// into the Decode context.
|
||||
|
||||
@@ -51,6 +51,108 @@ func TestOf(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestOfLazy tests the OfLazy function
|
||||
func TestOfLazy(t *testing.T) {
|
||||
t.Run("evaluates lazy computation ignoring input", func(t *testing.T) {
|
||||
lazyValue := func() int { return 42 }
|
||||
decoder := OfLazy[string](lazyValue)
|
||||
res := decoder("any input")
|
||||
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("defers computation until Decode is executed", func(t *testing.T) {
|
||||
executed := false
|
||||
lazyComputation := func() string {
|
||||
executed = true
|
||||
return "computed"
|
||||
}
|
||||
decoder := OfLazy[string](lazyComputation)
|
||||
|
||||
// Computation should not be executed yet
|
||||
assert.False(t, executed, "lazy computation should not be executed during Decode creation")
|
||||
|
||||
// Execute the Decode
|
||||
res := decoder("input")
|
||||
|
||||
// Now computation should be executed
|
||||
assert.True(t, executed, "lazy computation should be executed when Decode runs")
|
||||
assert.Equal(t, validation.Of("computed"), res)
|
||||
})
|
||||
|
||||
t.Run("evaluates lazy computation each time Decode is called", func(t *testing.T) {
|
||||
counter := 0
|
||||
lazyCounter := func() int {
|
||||
counter++
|
||||
return counter
|
||||
}
|
||||
decoder := OfLazy[string](lazyCounter)
|
||||
|
||||
// First execution
|
||||
res1 := decoder("input")
|
||||
assert.Equal(t, validation.Of(1), res1)
|
||||
|
||||
// Second execution
|
||||
res2 := decoder("input")
|
||||
assert.Equal(t, validation.Of(2), res2)
|
||||
|
||||
// Third execution
|
||||
res3 := decoder("input")
|
||||
assert.Equal(t, validation.Of(3), res3)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
lazyString := func() string { return "hello" }
|
||||
decoder1 := OfLazy[int](lazyString)
|
||||
assert.Equal(t, validation.Of("hello"), decoder1(123))
|
||||
|
||||
lazySlice := func() []int { return []int{1, 2, 3} }
|
||||
decoder2 := OfLazy[string](lazySlice)
|
||||
assert.Equal(t, validation.Of([]int{1, 2, 3}), decoder2("input"))
|
||||
|
||||
type Person struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
lazyStruct := func() Person { return Person{Name: "Alice", Age: 30} }
|
||||
decoder3 := OfLazy[map[string]any](lazyStruct)
|
||||
assert.Equal(t, validation.Of(Person{Name: "Alice", Age: 30}), decoder3(map[string]any{}))
|
||||
})
|
||||
|
||||
t.Run("can be composed with other Decode operations", func(t *testing.T) {
|
||||
lazyValue := func() int { return 10 }
|
||||
decoder := MonadMap(
|
||||
OfLazy[string](lazyValue),
|
||||
func(x int) int { return x * 2 },
|
||||
)
|
||||
res := decoder("input")
|
||||
assert.Equal(t, validation.Of(20), res)
|
||||
})
|
||||
|
||||
t.Run("ignores input completely", func(t *testing.T) {
|
||||
lazyValue := func() string { return "constant" }
|
||||
decoder := OfLazy[string](lazyValue)
|
||||
|
||||
// Different inputs should produce same result
|
||||
res1 := decoder("input1")
|
||||
res2 := decoder("input2")
|
||||
|
||||
assert.Equal(t, validation.Of("constant"), res1)
|
||||
assert.Equal(t, validation.Of("constant"), res2)
|
||||
assert.Equal(t, res1, res2)
|
||||
})
|
||||
|
||||
t.Run("always wraps result in success validation", func(t *testing.T) {
|
||||
lazyValue := func() int { return 42 }
|
||||
decoder := OfLazy[string](lazyValue)
|
||||
res := decoder("input")
|
||||
|
||||
// Verify it's a successful validation
|
||||
assert.True(t, either.IsRight(res))
|
||||
assert.Equal(t, validation.Of(42), res)
|
||||
})
|
||||
}
|
||||
|
||||
// TestLeft tests the Left function
|
||||
func TestLeft(t *testing.T) {
|
||||
t.Run("creates decoder that always fails", func(t *testing.T) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package codec
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/formatting"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/monoid"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readerresult"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/IBM/fp-go/v2/semigroup"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -440,4 +442,56 @@ type (
|
||||
// - ApSO: Applicative sequencing with optional
|
||||
// - Lens: For fields that always exist
|
||||
Optional[S, A any] = optional.Optional[S, A]
|
||||
|
||||
// Semigroup represents an algebraic structure with an associative binary operation.
|
||||
//
|
||||
// A Semigroup[A] provides:
|
||||
// - Concat(A, A): Combines two values associatively
|
||||
//
|
||||
// Semigroup law:
|
||||
// - Associativity: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
|
||||
//
|
||||
// Unlike Monoid, Semigroup does not require an identity element (Empty).
|
||||
// This makes it more general but less powerful for certain operations.
|
||||
//
|
||||
// In the codec context, semigroups are used to:
|
||||
// - Combine validation errors
|
||||
// - Merge partial results
|
||||
// - Aggregate codec outputs
|
||||
//
|
||||
// Example semigroups:
|
||||
// - String concatenation (without empty string)
|
||||
// - Array concatenation (without empty array)
|
||||
// - Error accumulation
|
||||
//
|
||||
// Note: Every Monoid is also a Semigroup, but not every Semigroup is a Monoid.
|
||||
Semigroup[A any] = semigroup.Semigroup[A]
|
||||
|
||||
// Void represents a unit type with a single value.
|
||||
//
|
||||
// Void is used instead of struct{} to represent:
|
||||
// - Unit values in functional programming
|
||||
// - Placeholder types where no meaningful value is needed
|
||||
// - Return types for functions that produce no useful result
|
||||
//
|
||||
// The single value of type Void is VOID (function.VOID).
|
||||
//
|
||||
// Usage:
|
||||
// - Use function.Void (or F.Void) as the type
|
||||
// - Use function.VOID (or F.VOID) as the value
|
||||
//
|
||||
// Example:
|
||||
// unitCodec := codec.Empty[F.Void, F.Void, any](
|
||||
// lazy.Of(pair.MakePair(F.VOID, F.VOID)),
|
||||
// )
|
||||
//
|
||||
// Benefits over struct{}:
|
||||
// - More explicit intent (unit type vs empty struct)
|
||||
// - Consistent with functional programming conventions
|
||||
// - Better semantic meaning in type signatures
|
||||
//
|
||||
// See also:
|
||||
// - function.VOID: The single value of type Void
|
||||
// - Empty: Codec function that uses Void for unit types
|
||||
Void = function.Void
|
||||
)
|
||||
|
||||
@@ -170,12 +170,7 @@ func TestLet(t *testing.T) {
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 5, computed: 10}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 5, computed: 10}), result)
|
||||
})
|
||||
|
||||
t.Run("preserves failure", func(t *testing.T) {
|
||||
@@ -218,12 +213,7 @@ func TestLet(t *testing.T) {
|
||||
)
|
||||
|
||||
result := validator("input")(nil)
|
||||
assert.True(t, either.IsRight(result))
|
||||
value := either.MonadFold(result,
|
||||
func(Errors) State { return State{} },
|
||||
F.Identity[State],
|
||||
)
|
||||
assert.Equal(t, State{x: 60, y: 10, z: 20}, value)
|
||||
assert.Equal(t, validation.Of(State{x: 60, y: 10, z: 20}), result)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,109 @@ func Of[I, A any](a A) Validate[I, A] {
|
||||
return reader.Of[I](decode.Of[Context](a))
|
||||
}
|
||||
|
||||
// OfLazy creates a Validate that defers the computation of a value until needed.
|
||||
//
|
||||
// This function lifts a lazy computation into the validation context. The computation
|
||||
// is deferred until the validator is actually executed, allowing for efficient handling
|
||||
// of expensive operations or values that may not always be needed.
|
||||
//
|
||||
// **IMPORTANT**: The lazy function MUST be pure (referentially transparent). It should
|
||||
// always return the same value when called and must not perform side effects. For
|
||||
// computations with side effects, use IO or IOEither types instead.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - I: The input type (not used, but required for type consistency)
|
||||
// - A: The type of the value produced by the lazy computation
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - fa: A lazy computation that produces a value of type A. This function is called
|
||||
// each time the validator is executed.
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Validate[I, A] that ignores its input and returns a successful validation containing
|
||||
// the lazily computed value.
|
||||
//
|
||||
// # Purity Requirements
|
||||
//
|
||||
// The lazy function MUST be pure:
|
||||
// - Always returns the same result for the same (lack of) input
|
||||
// - No side effects (no I/O, no mutation, no randomness)
|
||||
// - Deterministic and referentially transparent
|
||||
//
|
||||
// For side effects, use:
|
||||
// - IO types for effectful computations
|
||||
// - IOEither for effectful computations that may fail
|
||||
//
|
||||
// # Example: Deferring Expensive Computation
|
||||
//
|
||||
// import (
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validate"
|
||||
// "github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
// )
|
||||
//
|
||||
// // Expensive computation deferred until needed
|
||||
// expensiveValue := validate.OfLazy[string, int](func() int {
|
||||
// // This computation only runs when the validator is executed
|
||||
// return computeExpensiveValue()
|
||||
// })
|
||||
//
|
||||
// result := expensiveValue("any input")(nil)
|
||||
// // result is validation.Success(computed value)
|
||||
//
|
||||
// # Example: Lazy Default Value
|
||||
//
|
||||
// // Provide a default value that's only computed if needed
|
||||
// withDefault := validate.OfLazy[Config, Config](func() Config {
|
||||
// return loadDefaultConfig()
|
||||
// })
|
||||
//
|
||||
// // Use in a validation pipeline
|
||||
// validator := F.Pipe1(
|
||||
// validateFromFile,
|
||||
// validate.Alt(func() validate.Validate[string, Config] {
|
||||
// return withDefault
|
||||
// }),
|
||||
// )
|
||||
// // Default config only loaded if file validation fails
|
||||
//
|
||||
// # Example: Composition with Other Validators
|
||||
//
|
||||
// // Combine lazy value with validation logic
|
||||
// lazyValidator := F.Pipe1(
|
||||
// validate.OfLazy[string, int](func() int { return 42 }),
|
||||
// validate.Chain(func(n int) validate.Validate[string, string] {
|
||||
// return func(input string) validate.Reader[validation.Context, validation.Validation[string]] {
|
||||
// return func(ctx validation.Context) validation.Validation[string] {
|
||||
// if len(input) > n {
|
||||
// return validation.FailureWithMessage[string](input, "too long")(ctx)
|
||||
// }
|
||||
// return validation.Success(input)
|
||||
// }
|
||||
// }
|
||||
// }),
|
||||
// )
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - The lazy function is evaluated each time the validator is executed
|
||||
// - The input value I is ignored; the validator succeeds regardless of input
|
||||
// - The result is always wrapped in a successful validation
|
||||
// - This is useful for deferring expensive computations or providing lazy defaults
|
||||
// - The lazy function must be pure - no side effects allowed
|
||||
// - For side effects, use IO or IOEither types instead
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - Of: For non-lazy values
|
||||
// - decode.OfLazy: The underlying decode operation
|
||||
// - reader.Of: The reader lifting operation
|
||||
func OfLazy[I, A any](fa Lazy[A]) Validate[I, A] {
|
||||
return reader.Of[I](decode.OfLazy[Context](fa))
|
||||
}
|
||||
|
||||
// MonadMap applies a function to the successful result of a validation.
|
||||
//
|
||||
// This is the functor map operation for Validate. It transforms the success value
|
||||
|
||||
@@ -1274,3 +1274,139 @@ func TestOrElse(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestOfLazy tests the OfLazy function
|
||||
func TestOfLazy(t *testing.T) {
|
||||
t.Run("evaluates lazy computation", func(t *testing.T) {
|
||||
// Create a validator with a lazy value
|
||||
validator := OfLazy[string, int](func() int {
|
||||
return 42
|
||||
})
|
||||
|
||||
result := validator("any input")(nil)
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("defers execution until called", func(t *testing.T) {
|
||||
executed := false
|
||||
validator := OfLazy[string, int](func() int {
|
||||
executed = true
|
||||
return 100
|
||||
})
|
||||
|
||||
// Lazy function not executed yet
|
||||
assert.False(t, executed)
|
||||
|
||||
// Execute the validator
|
||||
result := validator("input")(nil)
|
||||
|
||||
// Now it should be executed
|
||||
assert.True(t, executed)
|
||||
assert.Equal(t, validation.Success(100), result)
|
||||
})
|
||||
|
||||
t.Run("evaluates on each call", func(t *testing.T) {
|
||||
callCount := 0
|
||||
validator := OfLazy[string, int](func() int {
|
||||
callCount++
|
||||
return callCount
|
||||
})
|
||||
|
||||
// First call
|
||||
result1 := validator("input")(nil)
|
||||
assert.Equal(t, validation.Success(1), result1)
|
||||
|
||||
// Second call - evaluates again
|
||||
result2 := validator("input")(nil)
|
||||
assert.Equal(t, validation.Success(2), result2)
|
||||
|
||||
// Third call
|
||||
result3 := validator("input")(nil)
|
||||
assert.Equal(t, validation.Success(3), result3)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// String type
|
||||
stringValidator := OfLazy[int, string](func() string {
|
||||
return "hello"
|
||||
})
|
||||
result := stringValidator(42)(nil)
|
||||
assert.Equal(t, validation.Success("hello"), result)
|
||||
|
||||
// Struct type
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
configValidator := OfLazy[string, Config](func() Config {
|
||||
return Config{Host: "localhost", Port: 8080}
|
||||
})
|
||||
result2 := configValidator("input")(nil)
|
||||
assert.Equal(t, validation.Success(Config{Host: "localhost", Port: 8080}), result2)
|
||||
|
||||
// Slice type
|
||||
sliceValidator := OfLazy[string, []int](func() []int {
|
||||
return []int{1, 2, 3}
|
||||
})
|
||||
result3 := sliceValidator("input")(nil)
|
||||
assert.Equal(t, validation.Success([]int{1, 2, 3}), result3)
|
||||
})
|
||||
|
||||
t.Run("composes with other validators", func(t *testing.T) {
|
||||
// Create a lazy validator that produces a number
|
||||
lazyValue := OfLazy[string, int](func() int {
|
||||
return 42
|
||||
})
|
||||
|
||||
// Map to transform the value
|
||||
validator := MonadMap(lazyValue, func(n int) int {
|
||||
return n * 2
|
||||
})
|
||||
|
||||
result := validator("any input")(nil)
|
||||
assert.Equal(t, validation.Success(84), result)
|
||||
})
|
||||
|
||||
t.Run("ignores input value", func(t *testing.T) {
|
||||
validator := OfLazy[string, int](func() int {
|
||||
return 999
|
||||
})
|
||||
|
||||
// Different inputs should produce the same result
|
||||
result1 := validator("input1")(nil)
|
||||
result2 := validator("input2")(nil)
|
||||
result3 := validator("")(nil)
|
||||
|
||||
assert.Equal(t, validation.Success(999), result1)
|
||||
assert.Equal(t, validation.Success(999), result2)
|
||||
assert.Equal(t, validation.Success(999), result3)
|
||||
})
|
||||
|
||||
t.Run("always wraps in success validation", func(t *testing.T) {
|
||||
validator := OfLazy[string, int](func() int {
|
||||
return 42
|
||||
})
|
||||
|
||||
result := validator("input")(nil)
|
||||
|
||||
// Verify it's a Right (success)
|
||||
assert.True(t, E.IsRight(result))
|
||||
|
||||
// Extract and verify the value
|
||||
value, _ := E.Unwrap(result)
|
||||
assert.Equal(t, 42, value)
|
||||
})
|
||||
|
||||
t.Run("works with context", func(t *testing.T) {
|
||||
validator := OfLazy[string, string](func() string {
|
||||
return "validated"
|
||||
})
|
||||
|
||||
ctx := validation.Context{
|
||||
{Key: "field", Type: "string"},
|
||||
}
|
||||
|
||||
result := validator("input")(ctx)
|
||||
assert.Equal(t, validation.Success("validated"), result)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -227,6 +227,51 @@ func Of[R, A any](a A) Reader[R, A] {
|
||||
return function.Constant1[R](a)
|
||||
}
|
||||
|
||||
// OfLazy converts a lazy computation into a Reader that ignores its environment.
|
||||
// The resulting Reader will evaluate the lazy computation when executed, regardless
|
||||
// of the environment provided.
|
||||
//
|
||||
// This function is intended solely for deferring the computation of a value, NOT for
|
||||
// representing side effects. The lazy computation should be a pure function that
|
||||
// produces the same result each time it's called (referential transparency). For
|
||||
// operations with side effects, use appropriate effect types like IO or IOEither.
|
||||
//
|
||||
// This is useful for lifting deferred computations into the Reader context without
|
||||
// requiring access to the environment.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The environment type (ignored by the resulting Reader)
|
||||
// - A: The result type produced by the lazy computation
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: A lazy computation that produces a value of type A (must be pure, no side effects)
|
||||
//
|
||||
// Returns:
|
||||
// - A Reader that ignores its environment and evaluates the lazy computation
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Host string }
|
||||
// lazyValue := func() int { return 42 }
|
||||
// r := reader.OfLazy[Config](lazyValue)
|
||||
// result := r(Config{Host: "localhost"}) // 42
|
||||
//
|
||||
// Example - Deferring expensive computation:
|
||||
//
|
||||
// type Env struct { Debug bool }
|
||||
// expensiveCalc := func() string {
|
||||
// // Expensive but pure computation here
|
||||
// return "computed result"
|
||||
// }
|
||||
// r := reader.OfLazy[Env](expensiveCalc)
|
||||
// // Computation is deferred until the Reader is executed
|
||||
// result := r(Env{Debug: true}) // "computed result"
|
||||
func OfLazy[R, A any](fa Lazy[A]) Reader[R, A] {
|
||||
return func(_ R) A {
|
||||
return fa()
|
||||
}
|
||||
}
|
||||
|
||||
// MonadChain sequences two Reader computations where the second depends on the result of the first.
|
||||
// Both computations share the same environment.
|
||||
// This is the monadic bind operation (flatMap).
|
||||
|
||||
@@ -92,6 +92,91 @@ func TestOf(t *testing.T) {
|
||||
assert.Equal(t, "constant", result)
|
||||
}
|
||||
|
||||
func TestOfLazy(t *testing.T) {
|
||||
t.Run("evaluates lazy computation ignoring environment", func(t *testing.T) {
|
||||
lazyValue := func() int { return 42 }
|
||||
r := OfLazy[Config](lazyValue)
|
||||
result := r(Config{Host: "localhost", Port: 8080})
|
||||
assert.Equal(t, 42, result)
|
||||
})
|
||||
|
||||
t.Run("defers computation until Reader is executed", func(t *testing.T) {
|
||||
executed := false
|
||||
lazyComputation := func() string {
|
||||
executed = true
|
||||
return "computed"
|
||||
}
|
||||
r := OfLazy[Config](lazyComputation)
|
||||
|
||||
// Computation should not be executed yet
|
||||
assert.False(t, executed, "lazy computation should not be executed during Reader creation")
|
||||
|
||||
// Execute the Reader
|
||||
result := r(Config{Host: "localhost"})
|
||||
|
||||
// Now computation should be executed
|
||||
assert.True(t, executed, "lazy computation should be executed when Reader runs")
|
||||
assert.Equal(t, "computed", result)
|
||||
})
|
||||
|
||||
t.Run("evaluates lazy computation each time Reader is called", func(t *testing.T) {
|
||||
counter := 0
|
||||
lazyCounter := func() int {
|
||||
counter++
|
||||
return counter
|
||||
}
|
||||
r := OfLazy[Config](lazyCounter)
|
||||
|
||||
// First execution
|
||||
result1 := r(Config{Host: "localhost"})
|
||||
assert.Equal(t, 1, result1)
|
||||
|
||||
// Second execution
|
||||
result2 := r(Config{Host: "localhost"})
|
||||
assert.Equal(t, 2, result2)
|
||||
|
||||
// Third execution
|
||||
result3 := r(Config{Host: "localhost"})
|
||||
assert.Equal(t, 3, result3)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
lazyString := func() string { return "hello" }
|
||||
r1 := OfLazy[Config](lazyString)
|
||||
assert.Equal(t, "hello", r1(Config{}))
|
||||
|
||||
lazySlice := func() []int { return []int{1, 2, 3} }
|
||||
r2 := OfLazy[Config](lazySlice)
|
||||
assert.Equal(t, []int{1, 2, 3}, r2(Config{}))
|
||||
|
||||
lazyStruct := func() Config { return Config{Host: "test", Port: 9000} }
|
||||
r3 := OfLazy[string](lazyStruct)
|
||||
assert.Equal(t, Config{Host: "test", Port: 9000}, r3("ignored"))
|
||||
})
|
||||
|
||||
t.Run("can be composed with other Reader operations", func(t *testing.T) {
|
||||
lazyValue := func() int { return 10 }
|
||||
r := F.Pipe1(
|
||||
OfLazy[Config](lazyValue),
|
||||
Map[Config](func(x int) int { return x * 2 }),
|
||||
)
|
||||
result := r(Config{Host: "localhost"})
|
||||
assert.Equal(t, 20, result)
|
||||
})
|
||||
|
||||
t.Run("ignores environment completely", func(t *testing.T) {
|
||||
lazyValue := func() string { return "constant" }
|
||||
r := OfLazy[Config](lazyValue)
|
||||
|
||||
// Different environments should produce same result
|
||||
config1 := Config{Host: "host1", Port: 8080}
|
||||
config2 := Config{Host: "host2", Port: 9090}
|
||||
|
||||
assert.Equal(t, "constant", r(config1))
|
||||
assert.Equal(t, "constant", r(config2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestChain(t *testing.T) {
|
||||
config := Config{Port: 8080}
|
||||
getPort := Asks(func(c Config) int { return c.Port })
|
||||
|
||||
@@ -103,4 +103,6 @@ type (
|
||||
|
||||
// Seq represents an iterator sequence over values of type T.
|
||||
Seq[T any] = iter.Seq[T]
|
||||
|
||||
Lazy[A any] = func() A
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/internal/fromreader"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/readert"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
@@ -46,6 +47,13 @@ func Right[E, L, A any](r A) ReaderEither[E, L, A] {
|
||||
return eithert.Right(reader.Of[E, Either[L, A]], r)
|
||||
}
|
||||
|
||||
func OfLazy[E, L, A any](r Lazy[A]) ReaderEither[E, L, A] {
|
||||
return reader.OfLazy[E](function.Pipe1(
|
||||
r,
|
||||
lazy.Map(ET.Of[L, A]),
|
||||
))
|
||||
}
|
||||
|
||||
func FromReader[L, E, A any](r Reader[E, A]) ReaderEither[E, L, A] {
|
||||
return RightReader[L](r)
|
||||
}
|
||||
|
||||
@@ -23,10 +23,14 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Lazy represents a deferred computation that produces a value of type A.
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
|
||||
// Option represents an optional value that may or may not be present.
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
// Either represents a value of one of two possible types (a disjoint union).
|
||||
// An instance of Either is either Left (representing an error) or Right (representing a success).
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A.
|
||||
@@ -34,9 +38,9 @@ type (
|
||||
|
||||
// ReaderEither represents a computation that depends on an environment R and can fail
|
||||
// with an error E or succeed with a value A.
|
||||
// It combines Reader (dependency injection) with Either (error handling).
|
||||
|
||||
// It combines the Reader monad (for dependency injection) with the Either monad (for error handling).
|
||||
ReaderEither[R, E, A any] = Reader[R, Either[E, A]]
|
||||
|
||||
// Kleisli represents a Kleisli arrow for the ReaderEither monad.
|
||||
// It's a function from A to ReaderEither[R, E, B], used for composing operations that
|
||||
// depend on an environment and may fail.
|
||||
@@ -44,7 +48,6 @@ type (
|
||||
|
||||
// Operator represents a function that transforms one ReaderEither into another.
|
||||
// It takes a ReaderEither[R, E, A] and produces a ReaderEither[R, E, B].
|
||||
// This is commonly used for lifting functions into the ReaderEither context.
|
||||
Operator[R, E, A, B any] = Kleisli[R, E, ReaderEither[R, E, A], B]
|
||||
|
||||
Lazy[A any] = lazy.Lazy[A]
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
@@ -279,6 +280,49 @@ func Of[R, A any](a A) ReaderResult[R, A] {
|
||||
return readert.MonadOf[ReaderResult[R, A]](ET.Of[error, A], a)
|
||||
}
|
||||
|
||||
// OfLazy converts a lazy computation into a ReaderResult that ignores its environment.
|
||||
// The resulting ReaderResult will evaluate the lazy computation when executed and wrap
|
||||
// the result in a successful Result, regardless of the environment provided.
|
||||
//
|
||||
// This function is intended solely for deferring the computation of a value, NOT for
|
||||
// representing side effects. The lazy computation should be a pure function that
|
||||
// produces the same result each time it's called (referential transparency). For
|
||||
// operations with side effects, use appropriate effect types like IO or IOResult.
|
||||
//
|
||||
// This is useful for lifting deferred computations into the ReaderResult context without
|
||||
// requiring access to the environment, while maintaining the Result wrapper for consistency.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The environment type (ignored by the resulting ReaderResult)
|
||||
// - A: The result type produced by the lazy computation
|
||||
//
|
||||
// Parameters:
|
||||
// - r: A lazy computation that produces a value of type A (must be pure, no side effects)
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult that ignores its environment, evaluates the lazy computation, and wraps the result in Result[A]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Host string }
|
||||
// lazyValue := func() int { return 42 }
|
||||
// rr := readerresult.OfLazy[Config](lazyValue)
|
||||
// result := rr(Config{Host: "localhost"}) // result.Of(42)
|
||||
//
|
||||
// Example - Deferring expensive computation:
|
||||
//
|
||||
// type Env struct { Debug bool }
|
||||
// expensiveCalc := func() string {
|
||||
// // Expensive but pure computation here
|
||||
// return "computed result"
|
||||
// }
|
||||
// rr := readerresult.OfLazy[Env](expensiveCalc)
|
||||
// // Computation is deferred until the ReaderResult is executed
|
||||
// result := rr(Env{Debug: true}) // result.Of("computed result")
|
||||
func OfLazy[R, A any](r Lazy[A]) ReaderResult[R, A] {
|
||||
return readereither.OfLazy[R, error](r)
|
||||
}
|
||||
|
||||
// MonadAp applies a function wrapped in a ReaderResult to a value wrapped in a ReaderResult.
|
||||
// Both computations share the same environment. This is useful for combining independent
|
||||
// computations that don't depend on each other's results.
|
||||
|
||||
@@ -77,6 +77,101 @@ func TestOf(t *testing.T) {
|
||||
assert.Equal(t, result.Of(42), rr(defaultContext))
|
||||
}
|
||||
|
||||
func TestOfLazy(t *testing.T) {
|
||||
t.Run("evaluates lazy computation ignoring environment", func(t *testing.T) {
|
||||
lazyValue := func() int { return 42 }
|
||||
rr := OfLazy[MyContext](lazyValue)
|
||||
res := rr(defaultContext)
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
|
||||
t.Run("defers computation until ReaderResult is executed", func(t *testing.T) {
|
||||
executed := false
|
||||
lazyComputation := func() string {
|
||||
executed = true
|
||||
return "computed"
|
||||
}
|
||||
rr := OfLazy[MyContext](lazyComputation)
|
||||
|
||||
// Computation should not be executed yet
|
||||
assert.False(t, executed, "lazy computation should not be executed during ReaderResult creation")
|
||||
|
||||
// Execute the ReaderResult
|
||||
res := rr(defaultContext)
|
||||
|
||||
// Now computation should be executed
|
||||
assert.True(t, executed, "lazy computation should be executed when ReaderResult runs")
|
||||
assert.Equal(t, result.Of("computed"), res)
|
||||
})
|
||||
|
||||
t.Run("evaluates lazy computation each time ReaderResult is called", func(t *testing.T) {
|
||||
counter := 0
|
||||
lazyCounter := func() int {
|
||||
counter++
|
||||
return counter
|
||||
}
|
||||
rr := OfLazy[MyContext](lazyCounter)
|
||||
|
||||
// First execution
|
||||
res1 := rr(defaultContext)
|
||||
assert.Equal(t, result.Of(1), res1)
|
||||
|
||||
// Second execution
|
||||
res2 := rr(defaultContext)
|
||||
assert.Equal(t, result.Of(2), res2)
|
||||
|
||||
// Third execution
|
||||
res3 := rr(defaultContext)
|
||||
assert.Equal(t, result.Of(3), res3)
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
lazyString := func() string { return "hello" }
|
||||
rr1 := OfLazy[MyContext](lazyString)
|
||||
assert.Equal(t, result.Of("hello"), rr1(defaultContext))
|
||||
|
||||
lazySlice := func() []int { return []int{1, 2, 3} }
|
||||
rr2 := OfLazy[MyContext](lazySlice)
|
||||
assert.Equal(t, result.Of([]int{1, 2, 3}), rr2(defaultContext))
|
||||
|
||||
lazyStruct := func() MyContext { return "test" }
|
||||
rr3 := OfLazy[string](lazyStruct)
|
||||
assert.Equal(t, result.Of(MyContext("test")), rr3("ignored"))
|
||||
})
|
||||
|
||||
t.Run("can be composed with other ReaderResult operations", func(t *testing.T) {
|
||||
lazyValue := func() int { return 10 }
|
||||
rr := F.Pipe1(
|
||||
OfLazy[MyContext](lazyValue),
|
||||
Map[MyContext](func(x int) int { return x * 2 }),
|
||||
)
|
||||
res := rr(defaultContext)
|
||||
assert.Equal(t, result.Of(20), res)
|
||||
})
|
||||
|
||||
t.Run("ignores environment completely", func(t *testing.T) {
|
||||
lazyValue := func() string { return "constant" }
|
||||
rr := OfLazy[MyContext](lazyValue)
|
||||
|
||||
// Different environments should produce same result
|
||||
ctx1 := MyContext("context1")
|
||||
ctx2 := MyContext("context2")
|
||||
|
||||
assert.Equal(t, result.Of("constant"), rr(ctx1))
|
||||
assert.Equal(t, result.Of("constant"), rr(ctx2))
|
||||
})
|
||||
|
||||
t.Run("always wraps result in success", func(t *testing.T) {
|
||||
lazyValue := func() int { return 42 }
|
||||
rr := OfLazy[MyContext](lazyValue)
|
||||
res := rr(defaultContext)
|
||||
|
||||
// Verify it's a successful Result
|
||||
assert.True(t, result.IsRight(res))
|
||||
assert.Equal(t, result.Of(42), res)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromReader(t *testing.T) {
|
||||
r := func(ctx MyContext) string { return string(ctx) }
|
||||
rr := FromReader(r)
|
||||
|
||||
Reference in New Issue
Block a user