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

Compare commits

...

22 Commits

Author SHA1 Message Date
Dr. Carsten Leue
a0132e2e92 fix: parameter order
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 15:08:11 +01:00
Dr. Carsten Leue
c6b342908d doc: better explanation for logger
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 13:52:08 +01:00
Dr. Carsten Leue
962237492f doc: explain Effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 13:27:20 +01:00
Dr. Carsten Leue
168a6e1072 fix: add Eitherize to Effect
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 12:55:03 +01:00
Dr. Carsten Leue
4d67b1d254 fix: expose Empty for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-27 10:52:23 +01:00
Dr. Carsten Leue
77a8cc6b09 fix: implement ApSO
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:44:27 +01:00
Dr. Carsten Leue
bc8743fdfc fix: build error
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 18:21:37 +01:00
Dr. Carsten Leue
1837d3f86d fix: add semigroup helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 16:39:05 +01:00
Dr. Carsten Leue
b2d111e8ec fix: more doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 14:04:44 +01:00
Dr. Carsten Leue
ae141c85c6 fix: add tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 13:40:12 +01:00
Dr. Carsten Leue
1230b4581b doc: add doc links
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-26 10:12:20 +01:00
Dr. Carsten Leue
70c831c8f9 fix: simplify type arguments
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 16:27:21 +01:00
Dr. Carsten Leue
cc0c14c7cf fix: do not fail if coverage fails
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 11:35:16 +01:00
Dr. Carsten Leue
19159ad49e fix: WriteFile
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 10:42:46 +01:00
Dr. Carsten Leue
b9c8fb4ff1 fix: parameter order for Local and TapIOK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 17:55:32 +01:00
Dr. Carsten Leue
4529e720bc fix: add ChainThunkK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:59:51 +01:00
Dr. Carsten Leue
ef8b2ea65d fix: add ChainReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:31:51 +01:00
Dr. Carsten Leue
5bd7caafdd fix: better doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 13:42:29 +01:00
Dr. Carsten Leue
47ebcd79b1 fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:15:00 +01:00
Dr. Carsten Leue
dbad94806e fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:14:21 +01:00
Dr. Carsten Leue
c4cac1cb3e fix: add FromReaderResult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-21 16:44:51 +01:00
Dr. Carsten Leue
a3fdb03df4 fix: better assertions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-13 09:27:49 +01:00
112 changed files with 17642 additions and 973 deletions

View File

@@ -42,6 +42,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -81,6 +82,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -106,6 +108,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

231
v2/AGENTS.md Normal file
View File

@@ -0,0 +1,231 @@
# Agent Guidelines for fp-go/v2
This document provides guidelines for AI agents working on the fp-go/v2 project.
## Documentation Standards
### Go Doc Comments
1. **Use Standard Go Doc Format**
- Do NOT use markdown-style links like `[text](url)`
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
- Go's documentation system will automatically create links
2. **Structure**
```go
// FunctionName does something useful.
//
// Longer description explaining the purpose and behavior.
//
// # Type Parameters
//
// - T: Description of type parameter
//
// # Parameters
//
// - param: Description of parameter
//
// # Returns
//
// - ReturnType: Description of return value
//
// # Example Usage
//
// code example here
//
// # See Also
//
// - RelatedFunction: Brief description
func FunctionName[T any](param T) ReturnType {
```
3. **Code Examples**
- Use idiomatic Go patterns
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
- Show realistic, runnable examples
### File Headers
Always include the Apache 2.0 license header:
```go
// 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.
```
## Testing Standards
### Test Structure
1. **Organize Tests by Category**
```go
func TestFunctionName_Success(t *testing.T) {
t.Run("specific success case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_Failure(t *testing.T) {
t.Run("specific failure case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_EdgeCases(t *testing.T) {
// edge case tests
}
func TestFunctionName_Integration(t *testing.T) {
// integration tests
}
```
2. **Use Direct Assertions**
- Prefer: `assert.Equal(t, validation.Success(expected), actual)`
- Avoid: Verbose `either.MonadFold` patterns unless necessary
- Exception: When you need to verify pointer is not nil or extract specific fields
3. **Use Idiomatic Patterns**
- Use `result.Eitherize1` for converting `(T, error)` functions
- Use `result.Of` for success values
- Use `result.Left` for error values
### Test Coverage
Include tests for:
- **Success cases**: Normal operation with various input types
- **Failure cases**: Error handling and error preservation
- **Edge cases**: Nil, empty, zero values, boundary conditions
- **Integration**: Composition with other functions
- **Type safety**: Verify type parameters work correctly
- **Benchmarks**: Performance-critical paths
### Example Test Pattern
```go
func TestFromReaderResult_Success(t *testing.T) {
t.Run("converts successful ReaderResult", func(t *testing.T) {
// Arrange
parseIntRR := result.Eitherize1(strconv.Atoi)
validator := FromReaderResult[string, int](parseIntRR)
// Act
result := validator("42")(nil)
// Assert
assert.Equal(t, validation.Success(42), result)
})
}
```
## Code Style
### Functional Patterns
1. **Prefer Composition**
```go
validator := F.Pipe1(
FromReaderResult[string, int](parseIntRR),
Chain(validatePositive),
)
```
2. **Use Type-Safe Helpers**
- `result.Eitherize1` for `func(T) (R, error)`
- `result.Of` for wrapping success values
- `result.Left` for wrapping errors
3. **Avoid Verbose Patterns**
- 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**
- Use `validation.Success` for successful validations
- Use `validation.FailureWithMessage` for simple failures
- Use `validation.FailureWithError` to preserve error causes
2. **In Tests**
- Verify error messages and causes
- Check error context is preserved
- Test error accumulation when applicable
## Common Patterns
### Converting Error-Based Functions
```go
// Good: Use Eitherize1
parseIntRR := result.Eitherize1(strconv.Atoi)
// Avoid: Manual error handling
parseIntRR := func(input string) result.Result[int] {
val, err := strconv.Atoi(input)
if err != nil {
return result.Left[int](err)
}
return result.Of(val)
}
```
### Testing Validation Results
```go
// Good: Direct comparison
assert.Equal(t, validation.Success(42), result)
// Avoid: Verbose extraction (unless you need to verify specific fields)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
```
### Documentation Examples
```go
// Good: Concise and idiomatic
// parseIntRR := result.Eitherize1(strconv.Atoi)
// validator := FromReaderResult[string, int](parseIntRR)
// Avoid: Verbose manual patterns
// parseIntRR := func(input string) result.Result[int] {
// val, err := strconv.Atoi(input)
// if err != nil {
// return result.Left[int](err)
// }
// return result.Of(val)
// }
```
## Checklist for New Code
- [ ] Apache 2.0 license header included
- [ ] Go doc comments use standard format (no markdown links)
- [ ] Code examples are idiomatic and concise
- [ ] Tests cover success, failure, edge cases, and integration
- [ ] Tests use direct assertions where possible
- [ ] Benchmarks included for performance-critical code
- [ ] All tests pass
- [ ] Code uses functional composition patterns
- [ ] Error handling preserves context and causes

View File

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

View File

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

View File

@@ -194,6 +194,25 @@ func ArrayNotEmpty[T any](arr []T) Reader {
}
}
// ArrayEmpty checks if an array is empty.
//
// This is the complement of ArrayNotEmpty, asserting that a slice has no elements.
//
// Example:
//
// func TestArrayEmpty(t *testing.T) {
// empty := []int{}
// assert.ArrayEmpty(empty)(t) // Passes
//
// numbers := []int{1, 2, 3}
// assert.ArrayEmpty(numbers)(t) // Fails
// }
func ArrayEmpty[T any](arr []T) Reader {
return func(t *testing.T) bool {
return assert.Empty(t, arr)
}
}
// RecordNotEmpty checks if a map is not empty.
//
// Example:
@@ -211,6 +230,25 @@ func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
}
}
// RecordEmpty checks if a map is empty.
//
// This is the complement of RecordNotEmpty, asserting that a map has no key-value pairs.
//
// Example:
//
// func TestRecordEmpty(t *testing.T) {
// empty := map[string]int{}
// assert.RecordEmpty(empty)(t) // Passes
//
// config := map[string]int{"timeout": 30}
// assert.RecordEmpty(config)(t) // Fails
// }
func RecordEmpty[K comparable, T any](mp map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Empty(t, mp)
}
}
// StringNotEmpty checks if a string is not empty.
//
// Example:
@@ -504,15 +542,7 @@ func AllOf(readers []Reader) Reader {
//
//go:inline
func RunAll(testcases map[string]Reader) Reader {
return func(t *testing.T) bool {
current := true
for k, r := range testcases {
current = current && t.Run(k, func(t1 *testing.T) {
r(t1)
})
}
return current
}
return SequenceRecord(testcases)
}
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,

View File

@@ -85,6 +85,33 @@ func TestArrayNotEmpty(t *testing.T) {
})
}
func TestArrayEmpty(t *testing.T) {
t.Run("should pass for empty array", func(t *testing.T) {
arr := []int{}
result := ArrayEmpty(arr)(t)
if !result {
t.Error("Expected ArrayEmpty to pass for empty array")
}
})
t.Run("should fail for non-empty array", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{1, 2, 3}
result := ArrayEmpty(arr)(mockT)
if result {
t.Error("Expected ArrayEmpty to fail for non-empty array")
}
})
t.Run("should work with different types", func(t *testing.T) {
strArr := []string{}
result := ArrayEmpty(strArr)(t)
if !result {
t.Error("Expected ArrayEmpty to pass for empty string array")
}
})
}
func TestRecordNotEmpty(t *testing.T) {
t.Run("should pass for non-empty map", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
@@ -131,6 +158,33 @@ func TestArrayLength(t *testing.T) {
})
}
func TestRecordEmpty(t *testing.T) {
t.Run("should pass for empty map", func(t *testing.T) {
mp := map[string]int{}
result := RecordEmpty(mp)(t)
if !result {
t.Error("Expected RecordEmpty to pass for empty map")
}
})
t.Run("should fail for non-empty map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1, "b": 2}
result := RecordEmpty(mp)(mockT)
if result {
t.Error("Expected RecordEmpty to fail for non-empty map")
}
})
t.Run("should work with different key-value types", func(t *testing.T) {
mp := map[int]string{}
result := RecordEmpty(mp)(t)
if !result {
t.Error("Expected RecordEmpty to pass for empty map with int keys")
}
})
}
func TestRecordLength(t *testing.T) {
t.Run("should pass when map length matches", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
@@ -150,6 +204,33 @@ func TestRecordLength(t *testing.T) {
})
}
func TestStringNotEmpty(t *testing.T) {
t.Run("should pass for non-empty string", func(t *testing.T) {
str := "Hello, World!"
result := StringNotEmpty(str)(t)
if !result {
t.Error("Expected StringNotEmpty to pass for non-empty string")
}
})
t.Run("should fail for empty string", func(t *testing.T) {
mockT := &testing.T{}
str := ""
result := StringNotEmpty(str)(mockT)
if result {
t.Error("Expected StringNotEmpty to fail for empty string")
}
})
t.Run("should pass for string with whitespace", func(t *testing.T) {
str := " "
result := StringNotEmpty(str)(t)
if !result {
t.Error("Expected StringNotEmpty to pass for string with whitespace")
}
})
}
func TestStringLength(t *testing.T) {
t.Run("should pass when string length matches", func(t *testing.T) {
str := "hello"

122
v2/assert/from.go Normal file
View File

@@ -0,0 +1,122 @@
// 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 assert
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
// FromReaderIOResult converts a ReaderIOResult[Reader] into a Reader.
//
// This function bridges the gap between context-aware, IO-based computations that may fail
// (ReaderIOResult) and the simpler Reader type used for test assertions. It executes the
// ReaderIOResult computation using the test's context, handles any potential errors by
// converting them to test failures via NoError, and returns the resulting Reader.
//
// The conversion process:
// 1. Executes the ReaderIOResult with the test context (t.Context())
// 2. Runs the resulting IO operation ()
// 3. Extracts the Result, converting errors to test failures using NoError
// 4. Returns a Reader that can be applied to *testing.T
//
// This is particularly useful when you have test assertions that need to:
// - Access context for cancellation or deadlines
// - Perform IO operations (file access, network calls, etc.)
// - Handle potential errors gracefully in tests
//
// Parameters:
// - ri: A ReaderIOResult that produces a Reader when given a context and executed
//
// Returns:
// - A Reader that can be directly applied to *testing.T for assertion
//
// Example:
//
// func TestWithContext(t *testing.T) {
// // Create a ReaderIOResult that performs an IO operation
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
// return func() result.Result[assert.Reader] {
// // Simulate database check
// if err := db.PingContext(ctx); err != nil {
// return result.Error[assert.Reader](err)
// }
// return result.Of[assert.Reader](assert.NoError(nil))
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIOResult(checkDatabase)
// assertion(t)
// }
func FromReaderIOResult(ri ReaderIOResult[Reader]) Reader {
return func(t *testing.T) bool {
return F.Pipe1(
ri(t.Context())(),
result.GetOrElse(NoError),
)(t)
}
}
// FromReaderIO converts a ReaderIO[Reader] into a Reader.
//
// This function bridges the gap between context-aware, IO-based computations (ReaderIO)
// and the simpler Reader type used for test assertions. It executes the ReaderIO
// computation using the test's context and returns the resulting Reader.
//
// Unlike FromReaderIOResult, this function does not handle errors explicitly - it assumes
// the IO operation will succeed or that any errors are handled within the ReaderIO itself.
//
// The conversion process:
// 1. Executes the ReaderIO with the test context (t.Context())
// 2. Runs the resulting IO operation ()
// 3. Returns a Reader that can be applied to *testing.T
//
// This is particularly useful when you have test assertions that need to:
// - Access context for cancellation or deadlines
// - Perform IO operations that don't fail (or handle failures internally)
// - Integrate with context-aware testing utilities
//
// Parameters:
// - ri: A ReaderIO that produces a Reader when given a context and executed
//
// Returns:
// - A Reader that can be directly applied to *testing.T for assertion
//
// Example:
//
// func TestWithIO(t *testing.T) {
// // Create a ReaderIO that performs an IO operation
// logAndCheck := func(ctx context.Context) func() assert.Reader {
// return func() assert.Reader {
// // Log something using context
// logger.InfoContext(ctx, "Running test")
// // Return an assertion
// return assert.Equal(42)(computeValue())
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIO(logAndCheck)
// assertion(t)
// }
func FromReaderIO(ri ReaderIO[Reader]) Reader {
return func(t *testing.T) bool {
return ri(t.Context())()(t)
}
}

383
v2/assert/from_test.go Normal file
View File

@@ -0,0 +1,383 @@
// 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 assert
import (
"context"
"errors"
"testing"
"github.com/IBM/fp-go/v2/result"
)
func TestFromReaderIOResult(t *testing.T) {
t.Run("should pass when ReaderIOResult returns success with passing assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns a successful Reader
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Return a Reader that always passes
return result.Of(func(t *testing.T) bool {
return true
})
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass when ReaderIOResult returns success")
}
})
t.Run("should pass when ReaderIOResult returns success with Equal assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns a successful Equal assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of(Equal(42)(42))
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with Equal assertion")
}
})
t.Run("should fail when ReaderIOResult returns error", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIOResult that returns an error
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Left[Reader](errors.New("test error"))
}
}
reader := FromReaderIOResult(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIOResult to fail when ReaderIOResult returns error")
}
})
t.Run("should fail when ReaderIOResult returns success but assertion fails", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIOResult that returns a failing assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of(Equal(42)(43))
}
}
reader := FromReaderIOResult(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIOResult to fail when assertion fails")
}
})
t.Run("should use test context", func(t *testing.T) {
contextUsed := false
// Create a ReaderIOResult that checks if context is provided
ri := func(ctx context.Context) func() result.Result[Reader] {
if ctx != nil {
contextUsed = true
}
return func() result.Result[Reader] {
return result.Of(func(t *testing.T) bool {
return true
})
}
}
reader := FromReaderIOResult(ri)
reader(t)
if !contextUsed {
t.Error("Expected FromReaderIOResult to use test context")
}
})
t.Run("should work with NoError assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns NoError assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of(NoError(nil))
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with NoError assertion")
}
})
t.Run("should work with complex assertions", func(t *testing.T) {
// Create a ReaderIOResult with multiple composed assertions
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
arr := []int{1, 2, 3}
assertions := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](3)(arr),
ArrayContains(2)(arr),
})
return result.Of(assertions)
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with complex assertions")
}
})
}
func TestFromReaderIO(t *testing.T) {
t.Run("should pass when ReaderIO returns passing assertion", func(t *testing.T) {
// Create a ReaderIO that returns a Reader that always passes
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return func(t *testing.T) bool {
return true
}
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass when ReaderIO returns passing assertion")
}
})
t.Run("should pass when ReaderIO returns Equal assertion", func(t *testing.T) {
// Create a ReaderIO that returns an Equal assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Equal(42)(42)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Equal assertion")
}
})
t.Run("should fail when ReaderIO returns failing assertion", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIO that returns a failing assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Equal(42)(43)
}
}
reader := FromReaderIO(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIO to fail when assertion fails")
}
})
t.Run("should use test context", func(t *testing.T) {
contextUsed := false
// Create a ReaderIO that checks if context is provided
ri := func(ctx context.Context) func() Reader {
if ctx != nil {
contextUsed = true
}
return func() Reader {
return func(t *testing.T) bool {
return true
}
}
}
reader := FromReaderIO(ri)
reader(t)
if !contextUsed {
t.Error("Expected FromReaderIO to use test context")
}
})
t.Run("should work with NoError assertion", func(t *testing.T) {
// Create a ReaderIO that returns NoError assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return NoError(nil)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with NoError assertion")
}
})
t.Run("should work with Error assertion", func(t *testing.T) {
// Create a ReaderIO that returns Error assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Error(errors.New("expected error"))
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Error assertion")
}
})
t.Run("should work with complex assertions", func(t *testing.T) {
// Create a ReaderIO with multiple composed assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
mp := map[string]int{"a": 1, "b": 2}
return AllOf([]Reader{
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
ContainsKey[int]("a")(mp),
})
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with complex assertions")
}
})
t.Run("should work with string assertions", func(t *testing.T) {
// Create a ReaderIO with string assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
str := "hello world"
return AllOf([]Reader{
StringNotEmpty(str),
StringLength[any, any](11)(str),
})
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with string assertions")
}
})
t.Run("should work with Result assertions", func(t *testing.T) {
// Create a ReaderIO with Result assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
successResult := result.Of(42)
return Success(successResult)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Success assertion")
}
})
t.Run("should work with Failure assertion", func(t *testing.T) {
// Create a ReaderIO with Failure assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
failureResult := result.Left[int](errors.New("test error"))
return Failure(failureResult)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Failure assertion")
}
})
}
// TestFromReaderIOResultIntegration tests integration scenarios
func TestFromReaderIOResultIntegration(t *testing.T) {
t.Run("should work in a realistic scenario with context cancellation", func(t *testing.T) {
// Create a ReaderIOResult that uses the context
ri := func(testCtx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Check if context is valid
if testCtx == nil {
return result.Left[Reader](errors.New("context is nil"))
}
// Return a successful assertion
return result.Of(Equal("test")("test"))
}
}
// Use the actual testing.T from the subtest
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected integration test to pass")
}
})
}
// TestFromReaderIOIntegration tests integration scenarios
func TestFromReaderIOIntegration(t *testing.T) {
t.Run("should work in a realistic scenario with logging", func(t *testing.T) {
logCalled := false
// Create a ReaderIO that simulates logging
ri := func(ctx context.Context) func() Reader {
return func() Reader {
// Simulate logging with context
if ctx != nil {
logCalled = true
}
// Return an assertion
return Equal(100)(100)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected integration test to pass")
}
if !logCalled {
t.Error("Expected logging to be called")
}
})
}

207
v2/assert/logger.go Normal file
View File

@@ -0,0 +1,207 @@
// 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 assert
import (
"testing"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/readerio"
)
// Logf creates a logging function that outputs formatted test messages using Go's testing.T.Logf.
//
// This function provides a functional programming approach to test logging, returning a
// [ReaderIO] that can be composed with other test operations. It's particularly useful
// for debugging tests, tracing execution flow, or documenting test behavior without
// affecting test outcomes.
//
// The function uses a curried design pattern:
// 1. First, you provide a format string (prefix) with format verbs (like %v, %d, %s)
// 2. This returns a function that takes a value of type T
// 3. That function returns a ReaderIO that performs the logging when executed
//
// # Parameters
//
// - prefix: A format string compatible with fmt.Printf (e.g., "Value: %v", "Count: %d")
// The format string should contain exactly one format verb that matches type T
//
// # Returns
//
// - A function that takes a value of type T and returns a [ReaderIO][*testing.T, Void]
// When executed, this ReaderIO logs the formatted message to the test output
//
// # Type Parameters
//
// - T: The type of value to be logged. Can be any type that can be formatted by fmt
//
// # Use Cases
//
// - Debugging test execution by logging intermediate values
// - Tracing the flow of complex test scenarios
// - Documenting test behavior in the test output
// - Logging values in functional pipelines without breaking the chain
// - Creating reusable logging operations for specific types
//
// # Example - Basic Logging
//
// func TestBasicLogging(t *testing.T) {
// // Create a logger for integers
// logInt := assert.Logf[int]("Processing value: %d")
//
// // Use it to log a value
// value := 42
// logInt(value)(t)() // Outputs: "Processing value: 42"
// }
//
// # Example - Logging in Test Pipeline
//
// func TestPipelineWithLogging(t *testing.T) {
// type User struct {
// Name string
// Age int
// }
//
// user := User{Name: "Alice", Age: 30}
//
// // Create a logger for User
// logUser := assert.Logf[User]("Testing user: %+v")
//
// // Log the user being tested
// logUser(user)(t)()
//
// // Continue with assertions
// assert.StringNotEmpty(user.Name)(t)
// assert.That(func(age int) bool { return age > 0 })(user.Age)(t)
// }
//
// # Example - Multiple Loggers for Different Types
//
// func TestMultipleLoggers(t *testing.T) {
// // Create type-specific loggers
// logString := assert.Logf[string]("String value: %s")
// logInt := assert.Logf[int]("Integer value: %d")
// logFloat := assert.Logf[float64]("Float value: %.2f")
//
// // Use them throughout the test
// logString("hello")(t)() // Outputs: "String value: hello"
// logInt(42)(t)() // Outputs: "Integer value: 42"
// logFloat(3.14159)(t)() // Outputs: "Float value: 3.14"
// }
//
// # Example - Logging Complex Structures
//
// func TestComplexStructureLogging(t *testing.T) {
// type Config struct {
// Host string
// Port int
// Timeout int
// }
//
// config := Config{Host: "localhost", Port: 8080, Timeout: 30}
//
// // Use %+v to include field names
// logConfig := assert.Logf[Config]("Configuration: %+v")
// logConfig(config)(t)()
// // Outputs: "Configuration: {Host:localhost Port:8080 Timeout:30}"
//
// // Or use %#v for Go-syntax representation
// logConfigGo := assert.Logf[Config]("Config (Go syntax): %#v")
// logConfigGo(config)(t)()
// // Outputs: "Config (Go syntax): assert.Config{Host:"localhost", Port:8080, Timeout:30}"
// }
//
// # Example - Debugging Test Failures
//
// func TestWithDebugLogging(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
// logSlice := assert.Logf[[]int]("Testing slice: %v")
//
// // Log the input data
// logSlice(numbers)(t)()
//
// // Perform assertions
// assert.ArrayNotEmpty(numbers)(t)
// assert.ArrayLength[int](5)(numbers)(t)
//
// // Log intermediate results
// sum := 0
// for _, n := range numbers {
// sum += n
// }
// logInt := assert.Logf[int]("Sum: %d")
// logInt(sum)(t)()
//
// assert.Equal(15)(sum)(t)
// }
//
// # Example - Conditional Logging
//
// func TestConditionalLogging(t *testing.T) {
// logDebug := assert.Logf[string]("DEBUG: %s")
//
// values := []int{1, 2, 3, 4, 5}
// for _, v := range values {
// if v%2 == 0 {
// logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
// }
// }
// // Outputs:
// // DEBUG: Found even number: 2
// // DEBUG: Found even number: 4
// }
//
// # Format Verbs
//
// Common format verbs you can use in the prefix string:
// - %v: Default format
// - %+v: Default format with field names for structs
// - %#v: Go-syntax representation
// - %T: Type of the value
// - %d: Integer in base 10
// - %s: String
// - %f: Floating point number
// - %t: Boolean (true/false)
// - %p: Pointer address
//
// See the fmt package documentation for a complete list of format verbs.
//
// # Notes
//
// - Logging does not affect test pass/fail status
// - Log output appears in test results when running with -v flag or when tests fail
// - The function returns Void, indicating it's used for side effects only
// - The ReaderIO pattern allows logging to be composed with other operations
//
// # Related Functions
//
// - [FromReaderIO]: Converts ReaderIO operations into test assertions
// - testing.T.Logf: The underlying Go testing log function
//
// # References
//
// - Go testing package: https://pkg.go.dev/testing
// - fmt package format verbs: https://pkg.go.dev/fmt
// - ReaderIO pattern: Combines Reader (context dependency) with IO (side effects)
func Logf[T any](prefix string) func(T) readerio.ReaderIO[*testing.T, Void] {
return func(a T) readerio.ReaderIO[*testing.T, Void] {
return func(t *testing.T) IO[Void] {
return io.FromImpure(func() {
t.Logf(prefix, a)
})
}
}
}

406
v2/assert/logger_test.go Normal file
View File

@@ -0,0 +1,406 @@
// 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 assert
import (
"fmt"
"testing"
)
// TestLogf_BasicInteger tests basic integer logging
func TestLogf_BasicInteger(t *testing.T) {
logInt := Logf[int]("Processing value: %d")
// This should not panic and should log the value
logInt(42)(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicString tests basic string logging
func TestLogf_BasicString(t *testing.T) {
logString := Logf[string]("String value: %s")
logString("hello world")(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicFloat tests basic float logging
func TestLogf_BasicFloat(t *testing.T) {
logFloat := Logf[float64]("Float value: %.2f")
logFloat(3.14159)(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicBoolean tests basic boolean logging
func TestLogf_BasicBoolean(t *testing.T) {
logBool := Logf[bool]("Boolean value: %t")
logBool(true)(t)()
logBool(false)(t)()
// Test passes if no panic occurs
}
// TestLogf_ComplexStruct tests logging of complex structures
func TestLogf_ComplexStruct(t *testing.T) {
type User struct {
Name string
Age int
}
logUser := Logf[User]("User: %+v")
user := User{Name: "Alice", Age: 30}
logUser(user)(t)()
// Test passes if no panic occurs
}
// TestLogf_Slice tests logging of slices
func TestLogf_Slice(t *testing.T) {
logSlice := Logf[[]int]("Slice: %v")
numbers := []int{1, 2, 3, 4, 5}
logSlice(numbers)(t)()
// Test passes if no panic occurs
}
// TestLogf_Map tests logging of maps
func TestLogf_Map(t *testing.T) {
logMap := Logf[map[string]int]("Map: %v")
data := map[string]int{"a": 1, "b": 2, "c": 3}
logMap(data)(t)()
// Test passes if no panic occurs
}
// TestLogf_Pointer tests logging of pointers
func TestLogf_Pointer(t *testing.T) {
logPtr := Logf[*int]("Pointer: %p")
value := 42
logPtr(&value)(t)()
// Test passes if no panic occurs
}
// TestLogf_NilPointer tests logging of nil pointers
func TestLogf_NilPointer(t *testing.T) {
logPtr := Logf[*int]("Pointer: %v")
var nilPtr *int
logPtr(nilPtr)(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptyString tests logging of empty strings
func TestLogf_EmptyString(t *testing.T) {
logString := Logf[string]("String: '%s'")
logString("")(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptySlice tests logging of empty slices
func TestLogf_EmptySlice(t *testing.T) {
logSlice := Logf[[]int]("Slice: %v")
logSlice([]int{})(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptyMap tests logging of empty maps
func TestLogf_EmptyMap(t *testing.T) {
logMap := Logf[map[string]int]("Map: %v")
logMap(map[string]int{})(t)()
// Test passes if no panic occurs
}
// TestLogf_MultipleTypes tests using multiple loggers for different types
func TestLogf_MultipleTypes(t *testing.T) {
logString := Logf[string]("String: %s")
logInt := Logf[int]("Integer: %d")
logFloat := Logf[float64]("Float: %.2f")
logString("test")(t)()
logInt(42)(t)()
logFloat(3.14)(t)()
// Test passes if no panic occurs
}
// TestLogf_WithinTestPipeline tests logging within a test pipeline
func TestLogf_WithinTestPipeline(t *testing.T) {
type Config struct {
Host string
Port int
}
config := Config{Host: "localhost", Port: 8080}
logConfig := Logf[Config]("Testing config: %+v")
logConfig(config)(t)()
// Continue with assertions
StringNotEmpty(config.Host)(t)
That(func(port int) bool { return port > 0 })(config.Port)(t)
// Test passes if no panic occurs and assertions pass
}
// TestLogf_NestedStructures tests logging of nested structures
func TestLogf_NestedStructures(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
logPerson := Logf[Person]("Person: %+v")
person := Person{
Name: "Bob",
Address: Address{
Street: "123 Main St",
City: "Springfield",
},
}
logPerson(person)(t)()
// Test passes if no panic occurs
}
// TestLogf_Interface tests logging of interface values
func TestLogf_Interface(t *testing.T) {
logAny := Logf[any]("Value: %v")
logAny(42)(t)()
logAny("string")(t)()
logAny([]int{1, 2, 3})(t)()
// Test passes if no panic occurs
}
// TestLogf_GoSyntaxFormat tests logging with Go-syntax format
func TestLogf_GoSyntaxFormat(t *testing.T) {
type Point struct {
X int
Y int
}
logPoint := Logf[Point]("Point: %#v")
point := Point{X: 10, Y: 20}
logPoint(point)(t)()
// Test passes if no panic occurs
}
// TestLogf_TypeFormat tests logging with type format
func TestLogf_TypeFormat(t *testing.T) {
logType := Logf[any]("Type: %T, Value: %v")
logType(42)(t)()
logType("string")(t)()
logType(3.14)(t)()
// Test passes if no panic occurs
}
// TestLogf_LargeNumbers tests logging of large numbers
func TestLogf_LargeNumbers(t *testing.T) {
logInt := Logf[int64]("Large number: %d")
logInt(9223372036854775807)(t)() // Max int64
// Test passes if no panic occurs
}
// TestLogf_NegativeNumbers tests logging of negative numbers
func TestLogf_NegativeNumbers(t *testing.T) {
logInt := Logf[int]("Number: %d")
logInt(-42)(t)()
logInt(-100)(t)()
// Test passes if no panic occurs
}
// TestLogf_SpecialFloats tests logging of special float values
func TestLogf_SpecialFloats(t *testing.T) {
logFloat := Logf[float64]("Float: %v")
logFloat(0.0)(t)()
logFloat(-0.0)(t)()
// Test passes if no panic occurs
}
// TestLogf_UnicodeStrings tests logging of unicode strings
func TestLogf_UnicodeStrings(t *testing.T) {
logString := Logf[string]("Unicode: %s")
logString("Hello, 世界")(t)()
logString("Emoji: 🎉🎊")(t)()
// Test passes if no panic occurs
}
// TestLogf_MultilineStrings tests logging of multiline strings
func TestLogf_MultilineStrings(t *testing.T) {
logString := Logf[string]("Multiline:\n%s")
multiline := `Line 1
Line 2
Line 3`
logString(multiline)(t)()
// Test passes if no panic occurs
}
// TestLogf_ReuseLogger tests reusing the same logger multiple times
func TestLogf_ReuseLogger(t *testing.T) {
logInt := Logf[int]("Value: %d")
for i := 0; i < 5; i++ {
logInt(i)(t)()
}
// Test passes if no panic occurs
}
// TestLogf_ConditionalLogging tests conditional logging based on values
func TestLogf_ConditionalLogging(t *testing.T) {
logDebug := Logf[string]("DEBUG: %s")
values := []int{1, 2, 3, 4, 5}
for _, v := range values {
if v%2 == 0 {
logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
}
}
// Test passes if no panic occurs
}
// TestLogf_WithAssertions tests combining logging with assertions
func TestLogf_WithAssertions(t *testing.T) {
logInt := Logf[int]("Testing value: %d")
value := 42
logInt(value)(t)()
// Perform assertion after logging
Equal(42)(value)(t)
// Test passes if assertion passes
}
// TestLogf_DebuggingFailures demonstrates using logging to debug test failures
func TestLogf_DebuggingFailures(t *testing.T) {
logSlice := Logf[[]int]("Input slice: %v")
logInt := Logf[int]("Computed sum: %d")
numbers := []int{1, 2, 3, 4, 5}
logSlice(numbers)(t)()
sum := 0
for _, n := range numbers {
sum += n
}
logInt(sum)(t)()
Equal(15)(sum)(t)
// Test passes if assertion passes
}
// TestLogf_ComplexDataStructures tests logging of complex nested data
func TestLogf_ComplexDataStructures(t *testing.T) {
type Metadata struct {
Version string
Tags []string
}
type Document struct {
ID int
Title string
Metadata Metadata
}
logDoc := Logf[Document]("Document: %+v")
doc := Document{
ID: 1,
Title: "Test Document",
Metadata: Metadata{
Version: "1.0",
Tags: []string{"test", "example"},
},
}
logDoc(doc)(t)()
// Test passes if no panic occurs
}
// TestLogf_ArrayTypes tests logging of array types
func TestLogf_ArrayTypes(t *testing.T) {
logArray := Logf[[5]int]("Array: %v")
arr := [5]int{1, 2, 3, 4, 5}
logArray(arr)(t)()
// Test passes if no panic occurs
}
// TestLogf_ChannelTypes tests logging of channel types
func TestLogf_ChannelTypes(t *testing.T) {
logChan := Logf[chan int]("Channel: %v")
ch := make(chan int, 1)
logChan(ch)(t)()
close(ch)
// Test passes if no panic occurs
}
// TestLogf_FunctionTypes tests logging of function types
func TestLogf_FunctionTypes(t *testing.T) {
logFunc := Logf[func() int]("Function: %v")
fn := func() int { return 42 }
logFunc(fn)(t)()
// Test passes if no panic occurs
}

152
v2/assert/monoid.go Normal file
View File

@@ -0,0 +1,152 @@
// 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 assert
import (
"testing"
"github.com/IBM/fp-go/v2/boolean"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/reader"
)
// ApplicativeMonoid returns a [monoid.Monoid] for combining test assertion [Reader]s.
//
// This monoid combines multiple test assertions using logical AND (conjunction) semantics,
// meaning all assertions must pass for the combined assertion to pass. It leverages the
// applicative structure of Reader to execute multiple assertions with the same testing.T
// context and combines their boolean results using boolean.MonoidAll (logical AND).
//
// The monoid provides:
// - Concat: Combines two assertions such that both must pass (logical AND)
// - Empty: Returns an assertion that always passes (identity element)
//
// This is particularly useful for:
// - Composing multiple test assertions into a single assertion
// - Building complex test conditions from simpler ones
// - Creating reusable assertion combinators
// - Implementing test assertion DSLs
//
// # Monoid Laws
//
// The returned monoid satisfies the standard monoid laws:
//
// 1. Associativity:
// Concat(Concat(a1, a2), a3) ≡ Concat(a1, Concat(a2, a3))
//
// 2. Left Identity:
// Concat(Empty(), a) ≡ a
//
// 3. Right Identity:
// Concat(a, Empty()) ≡ a
//
// # Returns
//
// - A [monoid.Monoid][Reader] that combines assertions using logical AND
//
// # Example - Basic Usage
//
// func TestUserValidation(t *testing.T) {
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
// m := assert.ApplicativeMonoid()
//
// // Combine multiple assertions
// assertion := m.Concat(
// assert.Equal("Alice")(user.Name),
// m.Concat(
// assert.Equal(30)(user.Age),
// assert.StringNotEmpty(user.Email),
// ),
// )
//
// // Execute combined assertion
// assertion(t) // All three assertions must pass
// }
//
// # Example - Building Reusable Validators
//
// func TestWithReusableValidators(t *testing.T) {
// m := assert.ApplicativeMonoid()
//
// // Create a reusable validator
// validateUser := func(u User) assert.Reader {
// return m.Concat(
// assert.StringNotEmpty(u.Name),
// m.Concat(
// assert.True(u.Age > 0),
// assert.StringContains("@")(u.Email),
// ),
// )
// }
//
// user := User{Name: "Bob", Age: 25, Email: "bob@test.com"}
// validateUser(user)(t)
// }
//
// # Example - Using Empty for Identity
//
// func TestEmptyIdentity(t *testing.T) {
// m := assert.ApplicativeMonoid()
// assertion := assert.Equal(42)(42)
//
// // Empty is the identity - these are equivalent
// result1 := m.Concat(m.Empty(), assertion)(t)
// result2 := m.Concat(assertion, m.Empty())(t)
// result3 := assertion(t)
// // All three produce the same result
// }
//
// # Example - Combining with AllOf
//
// func TestCombiningWithAllOf(t *testing.T) {
// // ApplicativeMonoid provides the underlying mechanism for AllOf
// arr := []int{1, 2, 3, 4, 5}
//
// // These are conceptually equivalent:
// m := assert.ApplicativeMonoid()
// manual := m.Concat(
// assert.ArrayNotEmpty(arr),
// m.Concat(
// assert.ArrayLength[int](5)(arr),
// assert.ArrayContains(3)(arr),
// ),
// )
//
// // AllOf uses ApplicativeMonoid internally
// convenient := assert.AllOf([]assert.Reader{
// assert.ArrayNotEmpty(arr),
// assert.ArrayLength[int](5)(arr),
// assert.ArrayContains(3)(arr),
// })
//
// manual(t)
// convenient(t)
// }
//
// # Related Functions
//
// - [AllOf]: Convenient wrapper for combining multiple assertions using this monoid
// - [boolean.MonoidAll]: The underlying boolean monoid (logical AND with true as identity)
// - [reader.ApplicativeMonoid]: Generic applicative monoid for Reader types
//
// # References
//
// - Haskell Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
// - Applicative Functors: https://hackage.haskell.org/package/base/docs/Control-Applicative.html
// - Boolean Monoid (All): https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:All
func ApplicativeMonoid() monoid.Monoid[Reader] {
return reader.ApplicativeMonoid[*testing.T](boolean.MonoidAll)
}

454
v2/assert/monoid_test.go Normal file
View File

@@ -0,0 +1,454 @@
// 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 assert
import (
"testing"
)
// TestApplicativeMonoid_Empty tests that Empty returns an assertion that always passes
func TestApplicativeMonoid_Empty(t *testing.T) {
m := ApplicativeMonoid()
empty := m.Empty()
result := empty(t)
if !result {
t.Error("Expected Empty() to return an assertion that always passes")
}
}
// TestApplicativeMonoid_Concat_BothPass tests that Concat returns true when both assertions pass
func TestApplicativeMonoid_Concat_BothPass(t *testing.T) {
m := ApplicativeMonoid()
assertion1 := Equal(42)(42)
assertion2 := Equal("hello")("hello")
combined := m.Concat(assertion1, assertion2)
result := combined(t)
if !result {
t.Error("Expected Concat to pass when both assertions pass")
}
}
// TestApplicativeMonoid_Concat_FirstFails tests that Concat returns false when first assertion fails
func TestApplicativeMonoid_Concat_FirstFails(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(43) // This will fail
assertion2 := Equal("hello")("hello")
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when first assertion fails")
}
}
// TestApplicativeMonoid_Concat_SecondFails tests that Concat returns false when second assertion fails
func TestApplicativeMonoid_Concat_SecondFails(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(42)
assertion2 := Equal("hello")("world") // This will fail
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when second assertion fails")
}
}
// TestApplicativeMonoid_Concat_BothFail tests that Concat returns false when both assertions fail
func TestApplicativeMonoid_Concat_BothFail(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(43) // This will fail
assertion2 := Equal("hello")("world") // This will fail
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when both assertions fail")
}
}
// TestApplicativeMonoid_LeftIdentity tests the left identity law: Concat(Empty(), a) = a
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Concat(Empty(), assertion) should behave the same as assertion
combined := m.Concat(m.Empty(), assertion)
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Left identity law violated: Concat(Empty(), a) should equal a")
}
}
// TestApplicativeMonoid_RightIdentity tests the right identity law: Concat(a, Empty()) = a
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Concat(assertion, Empty()) should behave the same as assertion
combined := m.Concat(assertion, m.Empty())
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Right identity law violated: Concat(a, Empty()) should equal a")
}
}
// TestApplicativeMonoid_Associativity tests the associativity law: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
func TestApplicativeMonoid_Associativity(t *testing.T) {
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(2)
a3 := Equal(3)(3)
// Concat(Concat(a1, a2), a3)
left := m.Concat(m.Concat(a1, a2), a3)
// Concat(a1, Concat(a2, a3))
right := m.Concat(a1, m.Concat(a2, a3))
result1 := left(t)
result2 := right(t)
if result1 != result2 {
t.Error("Associativity law violated: Concat(Concat(a, b), c) should equal Concat(a, Concat(b, c))")
}
}
// TestApplicativeMonoid_AssociativityWithFailure tests associativity when assertions fail
func TestApplicativeMonoid_AssociativityWithFailure(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(3) // This will fail
a3 := Equal(3)(3)
// Concat(Concat(a1, a2), a3)
left := m.Concat(m.Concat(a1, a2), a3)
// Concat(a1, Concat(a2, a3))
right := m.Concat(a1, m.Concat(a2, a3))
result1 := left(mockT)
result2 := right(mockT)
if result1 != result2 {
t.Error("Associativity law violated even with failures")
}
if result1 || result2 {
t.Error("Expected both to fail when one assertion fails")
}
}
// TestApplicativeMonoid_ComplexAssertions tests combining complex assertions
func TestApplicativeMonoid_ComplexAssertions(t *testing.T) {
m := ApplicativeMonoid()
arr := []int{1, 2, 3, 4, 5}
mp := map[string]int{"a": 1, "b": 2}
arrayAssertions := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
),
)
mapAssertions := m.Concat(
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
)
combined := m.Concat(arrayAssertions, mapAssertions)
result := combined(t)
if !result {
t.Error("Expected complex combined assertions to pass")
}
}
// TestApplicativeMonoid_ComplexAssertionsWithFailure tests complex assertions when one fails
func TestApplicativeMonoid_ComplexAssertionsWithFailure(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
arr := []int{1, 2, 3}
mp := map[string]int{"a": 1, "b": 2}
arrayAssertions := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr), // This will fail - array has 3 elements, not 5
ArrayContains(3)(arr),
),
)
mapAssertions := m.Concat(
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
)
combined := m.Concat(arrayAssertions, mapAssertions)
result := combined(mockT)
if result {
t.Error("Expected complex combined assertions to fail when one assertion fails")
}
}
// TestApplicativeMonoid_MultipleConcat tests chaining multiple Concat operations
func TestApplicativeMonoid_MultipleConcat(t *testing.T) {
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(2)
a3 := Equal(3)(3)
a4 := Equal(4)(4)
combined := m.Concat(
m.Concat(a1, a2),
m.Concat(a3, a4),
)
result := combined(t)
if !result {
t.Error("Expected multiple Concat operations to pass when all assertions pass")
}
}
// TestApplicativeMonoid_WithStringAssertions tests combining string assertions
func TestApplicativeMonoid_WithStringAssertions(t *testing.T) {
m := ApplicativeMonoid()
str := "hello world"
combined := m.Concat(
StringNotEmpty(str),
StringLength[any, any](11)(str),
)
result := combined(t)
if !result {
t.Error("Expected string assertions to pass")
}
}
// TestApplicativeMonoid_WithBooleanAssertions tests combining boolean assertions
func TestApplicativeMonoid_WithBooleanAssertions(t *testing.T) {
m := ApplicativeMonoid()
combined := m.Concat(
Equal(true)(true),
m.Concat(
Equal(false)(false),
Equal(true)(true),
),
)
result := combined(t)
if !result {
t.Error("Expected boolean assertions to pass")
}
}
// TestApplicativeMonoid_WithErrorAssertions tests combining error assertions
func TestApplicativeMonoid_WithErrorAssertions(t *testing.T) {
m := ApplicativeMonoid()
combined := m.Concat(
NoError(nil),
Equal("test")("test"),
)
result := combined(t)
if !result {
t.Error("Expected error assertions to pass")
}
}
// TestApplicativeMonoid_EmptyWithMultipleConcat tests Empty with multiple Concat operations
func TestApplicativeMonoid_EmptyWithMultipleConcat(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Multiple Empty values should still act as identity
combined := m.Concat(
m.Empty(),
m.Concat(
assertion,
m.Empty(),
),
)
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Multiple Empty values should still act as identity")
}
}
// TestApplicativeMonoid_OnlyEmpty tests using only Empty values
func TestApplicativeMonoid_OnlyEmpty(t *testing.T) {
m := ApplicativeMonoid()
// Concat of Empty values should still be Empty (identity)
combined := m.Concat(m.Empty(), m.Empty())
result := combined(t)
if !result {
t.Error("Expected Concat of Empty values to pass")
}
}
// TestApplicativeMonoid_RealWorldExample tests a realistic use case
func TestApplicativeMonoid_RealWorldExample(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
m := ApplicativeMonoid()
validateUser := func(u User) Reader {
return m.Concat(
StringNotEmpty(u.Name),
m.Concat(
That(func(age int) bool { return age > 0 })(u.Age),
m.Concat(
That(func(age int) bool { return age < 150 })(u.Age),
That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(u.Email),
),
),
)
}
validUser := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
result := validateUser(validUser)(t)
if !result {
t.Error("Expected valid user to pass all validations")
}
}
// TestApplicativeMonoid_RealWorldExampleWithFailure tests a realistic use case with failure
func TestApplicativeMonoid_RealWorldExampleWithFailure(t *testing.T) {
mockT := &testing.T{}
type User struct {
Name string
Age int
Email string
}
m := ApplicativeMonoid()
validateUser := func(u User) Reader {
return m.Concat(
StringNotEmpty(u.Name),
m.Concat(
That(func(age int) bool { return age > 0 })(u.Age),
m.Concat(
That(func(age int) bool { return age < 150 })(u.Age),
That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(u.Email),
),
),
)
}
invalidUser := User{Name: "Bob", Age: 200, Email: "bob@test.com"} // Age > 150
result := validateUser(invalidUser)(mockT)
if result {
t.Error("Expected invalid user to fail validation")
}
}
// TestApplicativeMonoid_IntegrationWithAllOf demonstrates relationship with AllOf
func TestApplicativeMonoid_IntegrationWithAllOf(t *testing.T) {
m := ApplicativeMonoid()
arr := []int{1, 2, 3, 4, 5}
// Using ApplicativeMonoid directly
manualCombination := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
),
)
// Using AllOf (which uses ApplicativeMonoid internally)
allOfCombination := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
})
result1 := manualCombination(t)
result2 := allOfCombination(t)
if result1 != result2 {
t.Error("Expected manual combination and AllOf to produce same result")
}
if !result1 || !result2 {
t.Error("Expected both combinations to pass")
}
}

650
v2/assert/traverse.go Normal file
View File

@@ -0,0 +1,650 @@
// 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 assert
import (
"testing"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
)
// TraverseArray transforms an array of values into a test suite by applying a function
// that generates named test cases for each element.
//
// This function enables data-driven testing where you have a collection of test inputs
// and want to run a named subtest for each one. It follows the functional programming
// pattern of "traverse" - transforming a collection while preserving structure and
// accumulating effects (in this case, test execution).
//
// The function takes each element of the array, applies the provided function to generate
// a [Pair] of (test name, test assertion), and runs each as a separate subtest using
// Go's t.Run. All subtests must pass for the overall test to pass.
//
// # Parameters
//
// - f: A function that takes a value of type T and returns a [Pair] containing:
// - Head: The test name (string) for the subtest
// - Tail: The test assertion ([Reader]) to execute
//
// # Returns
//
// - A [Kleisli] function that takes an array of T and returns a [Reader] that:
// - Executes each element as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation and reporting via t.Run
//
// # Use Cases
//
// - Data-driven testing with multiple test cases
// - Parameterized tests where each parameter gets its own subtest
// - Testing collections where each element needs validation
// - Property-based testing with generated test data
//
// # Example - Basic Data-Driven Testing
//
// func TestMathOperations(t *testing.T) {
// type TestCase struct {
// Input int
// Expected int
// }
//
// testCases := []TestCase{
// {Input: 2, Expected: 4},
// {Input: 3, Expected: 9},
// {Input: 4, Expected: 16},
// }
//
// square := func(n int) int { return n * n }
//
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
// assertion := assert.Equal(tc.Expected)(square(tc.Input))
// return pair.MakePair(name, assertion)
// })
//
// traverse(testCases)(t)
// }
//
// # Example - String Validation
//
// func TestStringValidation(t *testing.T) {
// inputs := []string{"hello", "world", "test"}
//
// traverse := assert.TraverseArray(func(s string) assert.Pair[string, assert.Reader] {
// return pair.MakePair(
// fmt.Sprintf("validate_%s", s),
// assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(s),
// assert.That(func(str string) bool { return len(str) > 0 })(s),
// }),
// )
// })
//
// traverse(inputs)(t)
// }
//
// # Example - Complex Object Testing
//
// func TestUsers(t *testing.T) {
// type User struct {
// Name string
// Age int
// Email string
// }
//
// users := []User{
// {Name: "Alice", Age: 30, Email: "alice@example.com"},
// {Name: "Bob", Age: 25, Email: "bob@example.com"},
// }
//
// traverse := assert.TraverseArray(func(u User) assert.Pair[string, assert.Reader] {
// return pair.MakePair(
// fmt.Sprintf("user_%s", u.Name),
// assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(u.Name),
// assert.That(func(age int) bool { return age > 0 })(u.Age),
// assert.That(func(email string) bool {
// return len(email) > 0 && strings.Contains(email, "@")
// })(u.Email),
// }),
// )
// })
//
// traverse(users)(t)
// }
//
// # Comparison with RunAll
//
// TraverseArray and [RunAll] serve similar purposes but differ in their approach:
//
// - TraverseArray: Generates test cases from an array of data
//
// - Input: Array of values + function to generate test cases
//
// - Use when: You have test data and need to generate test cases from it
//
// - RunAll: Executes pre-defined named test cases
//
// - Input: Map of test names to assertions
//
// - Use when: You have already defined test cases with names
//
// # Related Functions
//
// - [SequenceSeq2]: Similar but works with Go iterators (Seq2) instead of arrays
// - [RunAll]: Executes a map of named test cases
// - [AllOf]: Combines multiple assertions without subtests
//
// # References
//
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
// - Go subtests: https://go.dev/blog/subtests
func TraverseArray[T any](f func(T) Pair[string, Reader]) Kleisli[[]T] {
return func(ts []T) Reader {
return func(t *testing.T) bool {
ok := true
for _, src := range ts {
test := f(src)
res := t.Run(pair.Head(test), func(t *testing.T) {
pair.Tail(test)(t)
})
ok = ok && res
}
return ok
}
}
}
// SequenceSeq2 executes a sequence of named test cases provided as a Go iterator.
//
// This function takes a [Seq2] iterator that yields (name, assertion) pairs and
// executes each as a separate subtest using Go's t.Run. It's similar to [TraverseArray]
// but works directly with Go's iterator protocol (introduced in Go 1.23) rather than
// requiring an array.
//
// The function iterates through all test cases, running each as a named subtest.
// All subtests must pass for the overall test to pass. This provides proper test
// isolation and clear reporting of which specific test cases fail.
//
// # Parameters
//
// - s: A [Seq2] iterator that yields pairs of:
// - Key: Test name (string) for the subtest
// - Value: Test assertion ([Reader]) to execute
//
// # Returns
//
// - A [Reader] that:
// - Executes each test case as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation via t.Run
//
// # Use Cases
//
// - Working with iterator-based test data
// - Lazy evaluation of test cases
// - Integration with Go 1.23+ iterator patterns
// - Memory-efficient testing of large test suites
//
// # Example - Basic Usage with Iterator
//
// func TestWithIterator(t *testing.T) {
// // Create an iterator of test cases
// testCases := func(yield func(string, assert.Reader) bool) {
// if !yield("test_addition", assert.Equal(4)(2+2)) {
// return
// }
// if !yield("test_subtraction", assert.Equal(1)(3-2)) {
// return
// }
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
// return
// }
// }
//
// assert.SequenceSeq2(testCases)(t)
// }
//
// # Example - Generated Test Cases
//
// func TestGeneratedCases(t *testing.T) {
// // Generate test cases on the fly
// generateTests := func(yield func(string, assert.Reader) bool) {
// for i := 1; i <= 5; i++ {
// name := fmt.Sprintf("test_%d", i)
// assertion := assert.Equal(i*i)(i * i)
// if !yield(name, assertion) {
// return
// }
// }
// }
//
// assert.SequenceSeq2(generateTests)(t)
// }
//
// # Example - Filtering Test Cases
//
// func TestFilteredCases(t *testing.T) {
// type TestCase struct {
// Name string
// Input int
// Expected int
// Skip bool
// }
//
// allCases := []TestCase{
// {Name: "test1", Input: 2, Expected: 4, Skip: false},
// {Name: "test2", Input: 3, Expected: 9, Skip: true},
// {Name: "test3", Input: 4, Expected: 16, Skip: false},
// }
//
// // Create iterator that filters out skipped tests
// activeTests := func(yield func(string, assert.Reader) bool) {
// for _, tc := range allCases {
// if !tc.Skip {
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
// if !yield(tc.Name, assertion) {
// return
// }
// }
// }
// }
//
// assert.SequenceSeq2(activeTests)(t)
// }
//
// # Comparison with TraverseArray
//
// SequenceSeq2 and [TraverseArray] serve similar purposes but differ in their input:
//
// - SequenceSeq2: Works with iterators (Seq2)
//
// - Input: Iterator yielding (name, assertion) pairs
//
// - Use when: Working with Go 1.23+ iterators or lazy evaluation
//
// - Memory: More efficient for large test suites (lazy evaluation)
//
// - TraverseArray: Works with arrays
//
// - Input: Array of values + transformation function
//
// - Use when: You have an array of test data
//
// - Memory: All test data must be in memory
//
// # Comparison with RunAll
//
// SequenceSeq2 and [RunAll] are very similar:
//
// - SequenceSeq2: Takes an iterator (Seq2)
// - RunAll: Takes a map[string]Reader
//
// Both execute named test cases as subtests. Choose based on your data structure:
// use SequenceSeq2 for iterators, RunAll for maps.
//
// # Related Functions
//
// - [TraverseArray]: Similar but works with arrays instead of iterators
// - [RunAll]: Executes a map of named test cases
// - [AllOf]: Combines multiple assertions without subtests
//
// # References
//
// - Go iterators: https://go.dev/blog/range-functions
// - Go subtests: https://go.dev/blog/subtests
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
func SequenceSeq2[T any](s Seq2[string, Reader]) Reader {
return func(t *testing.T) bool {
ok := true
for name, test := range s {
res := t.Run(name, func(t *testing.T) {
test(t)
})
ok = ok && res
}
return ok
}
}
// TraverseRecord transforms a map of values into a test suite by applying a function
// that generates test assertions for each map entry.
//
// This function enables data-driven testing where you have a map of test data and want
// to run a named subtest for each entry. The map keys become test names, and the function
// transforms each value into a test assertion. It follows the functional programming
// pattern of "traverse" - transforming a collection while preserving structure and
// accumulating effects (in this case, test execution).
//
// The function takes each key-value pair from the map, applies the provided function to
// generate a [Reader] assertion, and runs each as a separate subtest using Go's t.Run.
// All subtests must pass for the overall test to pass.
//
// # Parameters
//
// - f: A [Kleisli] function that takes a value of type T and returns a [Reader] assertion
//
// # Returns
//
// - A [Kleisli] function that takes a map[string]T and returns a [Reader] that:
// - Executes each map entry as a named subtest (using the key as the test name)
// - Returns true only if all subtests pass
// - Provides proper test isolation and reporting via t.Run
//
// # Use Cases
//
// - Data-driven testing with named test cases in a map
// - Testing configuration maps where keys are meaningful names
// - Validating collections where natural keys exist
// - Property-based testing with named scenarios
//
// # Example - Basic Configuration Testing
//
// func TestConfigurations(t *testing.T) {
// configs := map[string]int{
// "timeout": 30,
// "maxRetries": 3,
// "bufferSize": 1024,
// }
//
// validatePositive := assert.That(func(n int) bool { return n > 0 })
//
// traverse := assert.TraverseRecord(validatePositive)
// traverse(configs)(t)
// }
//
// # Example - User Validation
//
// func TestUserMap(t *testing.T) {
// type User struct {
// Name string
// Age int
// }
//
// users := map[string]User{
// "alice": {Name: "Alice", Age: 30},
// "bob": {Name: "Bob", Age: 25},
// "carol": {Name: "Carol", Age: 35},
// }
//
// validateUser := func(u User) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(u.Name),
// assert.That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
// })
// }
//
// traverse := assert.TraverseRecord(validateUser)
// traverse(users)(t)
// }
//
// # Example - API Endpoint Testing
//
// func TestEndpoints(t *testing.T) {
// type Endpoint struct {
// Path string
// Method string
// }
//
// endpoints := map[string]Endpoint{
// "get_users": {Path: "/api/users", Method: "GET"},
// "create_user": {Path: "/api/users", Method: "POST"},
// "delete_user": {Path: "/api/users/:id", Method: "DELETE"},
// }
//
// validateEndpoint := func(e Endpoint) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(e.Path),
// assert.That(func(path string) bool {
// return strings.HasPrefix(path, "/api/")
// })(e.Path),
// assert.That(func(method string) bool {
// return method == "GET" || method == "POST" ||
// method == "PUT" || method == "DELETE"
// })(e.Method),
// })
// }
//
// traverse := assert.TraverseRecord(validateEndpoint)
// traverse(endpoints)(t)
// }
//
// # Comparison with TraverseArray
//
// TraverseRecord and [TraverseArray] serve similar purposes but differ in their input:
//
// - TraverseRecord: Works with maps (records)
//
// - Input: Map with string keys + transformation function
//
// - Use when: You have named test data in a map
//
// - Test names: Derived from map keys
//
// - TraverseArray: Works with arrays
//
// - Input: Array of values + function that generates names and assertions
//
// - Use when: You have sequential test data
//
// - Test names: Generated by the transformation function
//
// # Comparison with SequenceRecord
//
// TraverseRecord and [SequenceRecord] are closely related:
//
// - TraverseRecord: Transforms values into assertions
//
// - Input: map[string]T + function T -> Reader
//
// - Use when: You need to transform data before asserting
//
// - SequenceRecord: Executes pre-defined assertions
//
// - Input: map[string]Reader
//
// - Use when: Assertions are already defined
//
// # Related Functions
//
// - [SequenceRecord]: Similar but takes pre-defined assertions
// - [TraverseArray]: Similar but works with arrays
// - [RunAll]: Alias for SequenceRecord
//
// # References
//
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
// - Go subtests: https://go.dev/blog/subtests
func TraverseRecord[T any](f Kleisli[T]) Kleisli[map[string]T] {
return func(m map[string]T) Reader {
return func(t *testing.T) bool {
ok := true
for name, src := range m {
res := t.Run(name, func(t *testing.T) {
f(src)(t)
})
ok = ok && res
}
return ok
}
}
}
// SequenceRecord executes a map of named test cases as subtests.
//
// This function takes a map where keys are test names and values are test assertions
// ([Reader]), and executes each as a separate subtest using Go's t.Run. It's the
// record (map) equivalent of [SequenceSeq2] and is actually aliased as [RunAll] for
// convenience.
//
// The function iterates through all map entries, running each as a named subtest.
// All subtests must pass for the overall test to pass. This provides proper test
// isolation and clear reporting of which specific test cases fail.
//
// # Parameters
//
// - m: A map[string]Reader where:
// - Keys: Test names (strings) for the subtests
// - Values: Test assertions ([Reader]) to execute
//
// # Returns
//
// - A [Reader] that:
// - Executes each map entry as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation via t.Run
//
// # Use Cases
//
// - Executing a collection of pre-defined named test cases
// - Organizing related tests in a map structure
// - Running multiple assertions with descriptive names
// - Building test suites programmatically
//
// # Example - Basic Named Tests
//
// func TestMathOperations(t *testing.T) {
// tests := map[string]assert.Reader{
// "addition": assert.Equal(4)(2 + 2),
// "subtraction": assert.Equal(1)(3 - 2),
// "multiplication": assert.Equal(6)(2 * 3),
// "division": assert.Equal(2)(6 / 3),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - String Validation Suite
//
// func TestStringValidations(t *testing.T) {
// testString := "hello world"
//
// tests := map[string]assert.Reader{
// "not_empty": assert.StringNotEmpty(testString),
// "correct_length": assert.StringLength[any, any](11)(testString),
// "has_space": assert.That(func(s string) bool {
// return strings.Contains(s, " ")
// })(testString),
// "lowercase": assert.That(func(s string) bool {
// return s == strings.ToLower(s)
// })(testString),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - Complex Object Validation
//
// func TestUserValidation(t *testing.T) {
// type User struct {
// Name string
// Age int
// Email string
// }
//
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
//
// tests := map[string]assert.Reader{
// "name_not_empty": assert.StringNotEmpty(user.Name),
// "age_positive": assert.That(func(age int) bool { return age > 0 })(user.Age),
// "age_reasonable": assert.That(func(age int) bool { return age < 150 })(user.Age),
// "email_valid": assert.That(func(email string) bool {
// return strings.Contains(email, "@") && strings.Contains(email, ".")
// })(user.Email),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - Array Validation Suite
//
// func TestArrayValidations(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
//
// tests := map[string]assert.Reader{
// "not_empty": assert.ArrayNotEmpty(numbers),
// "correct_length": assert.ArrayLength[int](5)(numbers),
// "contains_three": assert.ArrayContains(3)(numbers),
// "all_positive": assert.That(func(arr []int) bool {
// for _, n := range arr {
// if n <= 0 {
// return false
// }
// }
// return true
// })(numbers),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Comparison with TraverseRecord
//
// SequenceRecord and [TraverseRecord] are closely related:
//
// - SequenceRecord: Executes pre-defined assertions
//
// - Input: map[string]Reader (assertions already created)
//
// - Use when: You have already defined test cases with assertions
//
// - TraverseRecord: Transforms values into assertions
//
// - Input: map[string]T + function T -> Reader
//
// - Use when: You need to transform data before asserting
//
// # Comparison with SequenceSeq2
//
// SequenceRecord and [SequenceSeq2] serve similar purposes but differ in their input:
//
// - SequenceRecord: Works with maps
//
// - Input: map[string]Reader
//
// - Use when: You have named test cases in a map
//
// - Iteration order: Non-deterministic (map iteration)
//
// - SequenceSeq2: Works with iterators
//
// - Input: Seq2[string, Reader]
//
// - Use when: You have test cases in an iterator
//
// - Iteration order: Deterministic (iterator order)
//
// # Note on Map Iteration Order
//
// Go maps have non-deterministic iteration order. If test execution order matters,
// consider using [SequenceSeq2] with an iterator that provides deterministic ordering,
// or use [TraverseArray] with a slice of test cases.
//
// # Related Functions
//
// - [RunAll]: Alias for SequenceRecord
// - [TraverseRecord]: Similar but transforms values into assertions
// - [SequenceSeq2]: Similar but works with iterators
// - [TraverseArray]: Similar but works with arrays
//
// # References
//
// - Go subtests: https://go.dev/blog/subtests
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
func SequenceRecord(m map[string]Reader) Reader {
return TraverseRecord(reader.Ask[Reader]())(m)
}

960
v2/assert/traverse_test.go Normal file
View File

@@ -0,0 +1,960 @@
// 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 assert
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/pair"
)
// TestTraverseArray_EmptyArray tests that TraverseArray handles empty arrays correctly
func TestTraverseArray_EmptyArray(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(n)(n),
)
})
result := traverse([]int{})(t)
if !result {
t.Error("Expected TraverseArray to pass with empty array")
}
}
// TestTraverseArray_SingleElement tests TraverseArray with a single element
func TestTraverseArray_SingleElement(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(n*2)(n*2),
)
})
result := traverse([]int{5})(t)
if !result {
t.Error("Expected TraverseArray to pass with single element")
}
}
// TestTraverseArray_MultipleElements tests TraverseArray with multiple passing elements
func TestTraverseArray_MultipleElements(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("square_%d", n),
Equal(n*n)(n*n),
)
})
result := traverse([]int{1, 2, 3, 4, 5})(t)
if !result {
t.Error("Expected TraverseArray to pass with all passing elements")
}
}
// TestTraverseArray_WithFailure tests that TraverseArray fails when one element fails
func TestTraverseArray_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(10)(n), // Will fail for all except 10
)
})
// Run in a subtest - we expect the subtests to fail, so t.Run returns false
result := traverse([]int{1, 2, 3})(t)
// The traverse should return false because assertions fail
if result {
t.Error("Expected traverse to return false when elements don't match")
}
}
// TestTraverseArray_MixedResults tests TraverseArray with some passing and some failing
func TestTraverseArray_MixedResults(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("is_even_%d", n),
Equal(0)(n%2), // Only passes for even numbers
)
})
result := traverse([]int{2, 3, 4})(t) // 3 is odd, should fail
// The traverse should return false because one assertion fails
if result {
t.Error("Expected traverse to return false when some elements fail")
}
}
// TestTraverseArray_StringData tests TraverseArray with string data
func TestTraverseArray_StringData(t *testing.T) {
words := []string{"hello", "world", "test"}
traverse := TraverseArray(func(s string) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("validate_%s", s),
AllOf([]Reader{
StringNotEmpty(s),
That(func(str string) bool { return len(str) > 0 })(s),
}),
)
})
result := traverse(words)(t)
if !result {
t.Error("Expected TraverseArray to pass with valid strings")
}
}
// TestTraverseArray_ComplexObjects tests TraverseArray with complex objects
func TestTraverseArray_ComplexObjects(t *testing.T) {
type User struct {
Name string
Age int
}
users := []User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}
traverse := TraverseArray(func(u User) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("user_%s", u.Name),
AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
}),
)
})
result := traverse(users)(t)
if !result {
t.Error("Expected TraverseArray to pass with valid users")
}
}
// TestTraverseArray_ComplexObjectsWithFailure tests TraverseArray with invalid complex objects
func TestTraverseArray_ComplexObjectsWithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
type User struct {
Name string
Age int
}
users := []User{
{Name: "Alice", Age: 30},
{Name: "", Age: 25}, // Invalid: empty name
{Name: "Charlie", Age: 35},
}
traverse := TraverseArray(func(u User) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("user_%s", u.Name),
AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 })(u.Age),
}),
)
})
result := traverse(users)(t)
// The traverse should return false because one user is invalid
if result {
t.Error("Expected traverse to return false with invalid user")
}
}
// TestTraverseArray_DataDrivenTesting demonstrates data-driven testing pattern
func TestTraverseArray_DataDrivenTesting(t *testing.T) {
type TestCase struct {
Input int
Expected int
}
testCases := []TestCase{
{Input: 2, Expected: 4},
{Input: 3, Expected: 9},
{Input: 4, Expected: 16},
{Input: 5, Expected: 25},
}
square := func(n int) int { return n * n }
traverse := TraverseArray(func(tc TestCase) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected),
Equal(tc.Expected)(square(tc.Input)),
)
})
result := traverse(testCases)(t)
if !result {
t.Error("Expected all test cases to pass")
}
}
// TestSequenceSeq2_EmptySequence tests that SequenceSeq2 handles empty sequences correctly
func TestSequenceSeq2_EmptySequence(t *testing.T) {
emptySeq := func(yield func(string, Reader) bool) {
// Empty - yields nothing
}
result := SequenceSeq2[Reader](emptySeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with empty sequence")
}
}
// TestSequenceSeq2_SingleTest tests SequenceSeq2 with a single test
func TestSequenceSeq2_SingleTest(t *testing.T) {
singleSeq := func(yield func(string, Reader) bool) {
yield("test_one", Equal(42)(42))
}
result := SequenceSeq2[Reader](singleSeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with single test")
}
}
// TestSequenceSeq2_MultipleTests tests SequenceSeq2 with multiple passing tests
func TestSequenceSeq2_MultipleTests(t *testing.T) {
multiSeq := func(yield func(string, Reader) bool) {
if !yield("test_addition", Equal(4)(2+2)) {
return
}
if !yield("test_subtraction", Equal(1)(3-2)) {
return
}
if !yield("test_multiplication", Equal(6)(2*3)) {
return
}
}
result := SequenceSeq2[Reader](multiSeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with all passing tests")
}
}
// TestSequenceSeq2_WithFailure tests that SequenceSeq2 fails when one test fails
func TestSequenceSeq2_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
failSeq := func(yield func(string, Reader) bool) {
if !yield("test_pass", Equal(4)(2+2)) {
return
}
if !yield("test_fail", Equal(5)(2+2)) { // This will fail
return
}
if !yield("test_pass2", Equal(6)(2*3)) {
return
}
}
result := SequenceSeq2[Reader](failSeq)(t)
// The sequence should return false because one test fails
if result {
t.Error("Expected sequence to return false when one test fails")
}
}
// TestSequenceSeq2_GeneratedTests tests SequenceSeq2 with generated test cases
func TestSequenceSeq2_GeneratedTests(t *testing.T) {
generateTests := func(yield func(string, Reader) bool) {
for i := 1; i <= 5; i++ {
name := fmt.Sprintf("test_%d", i)
assertion := Equal(i * i)(i * i)
if !yield(name, assertion) {
return
}
}
}
result := SequenceSeq2[Reader](generateTests)(t)
if !result {
t.Error("Expected all generated tests to pass")
}
}
// TestSequenceSeq2_StringTests tests SequenceSeq2 with string assertions
func TestSequenceSeq2_StringTests(t *testing.T) {
stringSeq := func(yield func(string, Reader) bool) {
if !yield("test_hello", StringNotEmpty("hello")) {
return
}
if !yield("test_world", StringNotEmpty("world")) {
return
}
if !yield("test_length", StringLength[any, any](5)("hello")) {
return
}
}
result := SequenceSeq2[Reader](stringSeq)(t)
if !result {
t.Error("Expected all string tests to pass")
}
}
// TestSequenceSeq2_ArrayTests tests SequenceSeq2 with array assertions
func TestSequenceSeq2_ArrayTests(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
arraySeq := func(yield func(string, Reader) bool) {
if !yield("test_not_empty", ArrayNotEmpty(arr)) {
return
}
if !yield("test_length", ArrayLength[int](5)(arr)) {
return
}
if !yield("test_contains", ArrayContains(3)(arr)) {
return
}
}
result := SequenceSeq2[Reader](arraySeq)(t)
if !result {
t.Error("Expected all array tests to pass")
}
}
// TestSequenceSeq2_ComplexAssertions tests SequenceSeq2 with complex combined assertions
func TestSequenceSeq2_ComplexAssertions(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
userSeq := func(yield func(string, Reader) bool) {
if !yield("test_name", StringNotEmpty(user.Name)) {
return
}
if !yield("test_age", That(func(age int) bool { return age > 0 && age < 150 })(user.Age)) {
return
}
if !yield("test_email", That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(user.Email)) {
return
}
}
result := SequenceSeq2[Reader](userSeq)(t)
if !result {
t.Error("Expected all user validation tests to pass")
}
}
// TestSequenceSeq2_EarlyTermination tests that SequenceSeq2 respects early termination
func TestSequenceSeq2_EarlyTermination(t *testing.T) {
executionCount := 0
earlyTermSeq := func(yield func(string, Reader) bool) {
executionCount++
if !yield("test_1", Equal(1)(1)) {
return
}
executionCount++
if !yield("test_2", Equal(2)(2)) {
return
}
executionCount++
// This should execute even though we don't check the return
yield("test_3", Equal(3)(3))
executionCount++
}
SequenceSeq2[Reader](earlyTermSeq)(t)
// All iterations should execute since we're not terminating early
if executionCount != 4 {
t.Errorf("Expected 4 executions, got %d", executionCount)
}
}
// TestSequenceSeq2_WithMapConversion demonstrates converting a map to Seq2
func TestSequenceSeq2_WithMapConversion(t *testing.T) {
testMap := map[string]Reader{
"test_addition": Equal(4)(2 + 2),
"test_multiplication": Equal(6)(2 * 3),
"test_subtraction": Equal(1)(3 - 2),
}
// Convert map to Seq2
mapSeq := func(yield func(string, Reader) bool) {
for name, assertion := range testMap {
if !yield(name, assertion) {
return
}
}
}
result := SequenceSeq2[Reader](mapSeq)(t)
if !result {
t.Error("Expected all map-based tests to pass")
}
}
// TestTraverseArray_vs_SequenceSeq2 demonstrates the relationship between the two functions
func TestTraverseArray_vs_SequenceSeq2(t *testing.T) {
type TestCase struct {
Name string
Input int
Expected int
}
testCases := []TestCase{
{Name: "test_1", Input: 2, Expected: 4},
{Name: "test_2", Input: 3, Expected: 9},
{Name: "test_3", Input: 4, Expected: 16},
}
// Using TraverseArray
traverseResult := TraverseArray(func(tc TestCase) Pair[string, Reader] {
return pair.MakePair(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input))
})(testCases)(t)
// Using SequenceSeq2
seqResult := SequenceSeq2[Reader](func(yield func(string, Reader) bool) {
for _, tc := range testCases {
if !yield(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input)) {
return
}
}
})(t)
if traverseResult != seqResult {
t.Error("Expected TraverseArray and SequenceSeq2 to produce same result")
}
if !traverseResult || !seqResult {
t.Error("Expected both approaches to pass")
}
}
// TestTraverseRecord_EmptyMap tests that TraverseRecord handles empty maps correctly
func TestTraverseRecord_EmptyMap(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n)(n)
})
result := traverse(map[string]int{})(t)
if !result {
t.Error("Expected TraverseRecord to pass with empty map")
}
}
// TestTraverseRecord_SingleEntry tests TraverseRecord with a single map entry
func TestTraverseRecord_SingleEntry(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n * 2)(n * 2)
})
result := traverse(map[string]int{"test_5": 5})(t)
if !result {
t.Error("Expected TraverseRecord to pass with single entry")
}
}
// TestTraverseRecord_MultipleEntries tests TraverseRecord with multiple passing entries
func TestTraverseRecord_MultipleEntries(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n * n)(n * n)
})
result := traverse(map[string]int{
"square_1": 1,
"square_2": 2,
"square_3": 3,
"square_4": 4,
"square_5": 5,
})(t)
if !result {
t.Error("Expected TraverseRecord to pass with all passing entries")
}
}
// TestTraverseRecord_WithFailure tests that TraverseRecord fails when one entry fails
func TestTraverseRecord_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseRecord(func(n int) Reader {
return Equal(10)(n) // Will fail for all except 10
})
result := traverse(map[string]int{
"test_1": 1,
"test_2": 2,
"test_3": 3,
})(t)
// The traverse should return false because entries don't match
if result {
t.Error("Expected traverse to return false when entries don't match")
}
}
// TestTraverseRecord_MixedResults tests TraverseRecord with some passing and some failing
func TestTraverseRecord_MixedResults(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseRecord(func(n int) Reader {
return Equal(0)(n % 2) // Only passes for even numbers
})
result := traverse(map[string]int{
"even_2": 2,
"odd_3": 3,
"even_4": 4,
})(t)
// The traverse should return false because some entries fail
if result {
t.Error("Expected traverse to return false when some entries fail")
}
}
// TestTraverseRecord_StringData tests TraverseRecord with string data
func TestTraverseRecord_StringData(t *testing.T) {
words := map[string]string{
"greeting": "hello",
"world": "world",
"test": "test",
}
traverse := TraverseRecord(func(s string) Reader {
return AllOf([]Reader{
StringNotEmpty(s),
That(func(str string) bool { return len(str) > 0 })(s),
})
})
result := traverse(words)(t)
if !result {
t.Error("Expected TraverseRecord to pass with valid strings")
}
}
// TestTraverseRecord_ComplexObjects tests TraverseRecord with complex objects
func TestTraverseRecord_ComplexObjects(t *testing.T) {
type User struct {
Name string
Age int
}
users := map[string]User{
"alice": {Name: "Alice", Age: 30},
"bob": {Name: "Bob", Age: 25},
"charlie": {Name: "Charlie", Age: 35},
}
traverse := TraverseRecord(func(u User) Reader {
return AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
})
})
result := traverse(users)(t)
if !result {
t.Error("Expected TraverseRecord to pass with valid users")
}
}
// TestTraverseRecord_ComplexObjectsWithFailure tests TraverseRecord with invalid complex objects
func TestTraverseRecord_ComplexObjectsWithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
type User struct {
Name string
Age int
}
users := map[string]User{
"alice": {Name: "Alice", Age: 30},
"invalid": {Name: "", Age: 25}, // Invalid: empty name
"charlie": {Name: "Charlie", Age: 35},
}
traverse := TraverseRecord(func(u User) Reader {
return AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 })(u.Age),
})
})
result := traverse(users)(t)
// The traverse should return false because one user is invalid
if result {
t.Error("Expected traverse to return false with invalid user")
}
}
// TestTraverseRecord_ConfigurationTesting demonstrates configuration testing pattern
func TestTraverseRecord_ConfigurationTesting(t *testing.T) {
configs := map[string]int{
"timeout": 30,
"maxRetries": 3,
"bufferSize": 1024,
}
validatePositive := That(func(n int) bool { return n > 0 })
traverse := TraverseRecord(validatePositive)
result := traverse(configs)(t)
if !result {
t.Error("Expected all configuration values to be positive")
}
}
// TestTraverseRecord_APIEndpointTesting demonstrates API endpoint testing pattern
func TestTraverseRecord_APIEndpointTesting(t *testing.T) {
type Endpoint struct {
Path string
Method string
}
endpoints := map[string]Endpoint{
"get_users": {Path: "/api/users", Method: "GET"},
"create_user": {Path: "/api/users", Method: "POST"},
"delete_user": {Path: "/api/users/:id", Method: "DELETE"},
}
validateEndpoint := func(e Endpoint) Reader {
return AllOf([]Reader{
StringNotEmpty(e.Path),
That(func(path string) bool {
return len(path) > 0 && path[0] == '/'
})(e.Path),
That(func(method string) bool {
return method == "GET" || method == "POST" ||
method == "PUT" || method == "DELETE"
})(e.Method),
})
}
traverse := TraverseRecord(validateEndpoint)
result := traverse(endpoints)(t)
if !result {
t.Error("Expected all endpoints to be valid")
}
}
// TestSequenceRecord_EmptyMap tests that SequenceRecord handles empty maps correctly
func TestSequenceRecord_EmptyMap(t *testing.T) {
result := SequenceRecord(map[string]Reader{})(t)
if !result {
t.Error("Expected SequenceRecord to pass with empty map")
}
}
// TestSequenceRecord_SingleTest tests SequenceRecord with a single test
func TestSequenceRecord_SingleTest(t *testing.T) {
tests := map[string]Reader{
"test_one": Equal(42)(42),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected SequenceRecord to pass with single test")
}
}
// TestSequenceRecord_MultipleTests tests SequenceRecord with multiple passing tests
func TestSequenceRecord_MultipleTests(t *testing.T) {
tests := map[string]Reader{
"test_addition": Equal(4)(2 + 2),
"test_subtraction": Equal(1)(3 - 2),
"test_multiplication": Equal(6)(2 * 3),
"test_division": Equal(2)(6 / 3),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected SequenceRecord to pass with all passing tests")
}
}
// TestSequenceRecord_WithFailure tests that SequenceRecord fails when one test fails
func TestSequenceRecord_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
tests := map[string]Reader{
"test_pass": Equal(4)(2 + 2),
"test_fail": Equal(5)(2 + 2), // This will fail
"test_pass2": Equal(6)(2 * 3),
}
result := SequenceRecord(tests)(t)
// The sequence should return false because one test fails
if result {
t.Error("Expected sequence to return false when one test fails")
}
}
// TestSequenceRecord_StringTests tests SequenceRecord with string assertions
func TestSequenceRecord_StringTests(t *testing.T) {
testString := "hello world"
tests := map[string]Reader{
"not_empty": StringNotEmpty(testString),
"correct_length": StringLength[any, any](11)(testString),
"has_space": That(func(s string) bool {
for _, ch := range s {
if ch == ' ' {
return true
}
}
return false
})(testString),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all string tests to pass")
}
}
// TestSequenceRecord_ArrayTests tests SequenceRecord with array assertions
func TestSequenceRecord_ArrayTests(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
tests := map[string]Reader{
"not_empty": ArrayNotEmpty(arr),
"correct_length": ArrayLength[int](5)(arr),
"contains_three": ArrayContains(3)(arr),
"all_positive": That(func(arr []int) bool {
for _, n := range arr {
if n <= 0 {
return false
}
}
return true
})(arr),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all array tests to pass")
}
}
// TestSequenceRecord_ComplexAssertions tests SequenceRecord with complex combined assertions
func TestSequenceRecord_ComplexAssertions(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
tests := map[string]Reader{
"name_not_empty": StringNotEmpty(user.Name),
"age_positive": That(func(age int) bool { return age > 0 })(user.Age),
"age_reasonable": That(func(age int) bool { return age < 150 })(user.Age),
"email_valid": That(func(email string) bool {
hasAt := false
hasDot := false
for _, ch := range email {
if ch == '@' {
hasAt = true
}
if ch == '.' {
hasDot = true
}
}
return hasAt && hasDot
})(user.Email),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all user validation tests to pass")
}
}
// TestSequenceRecord_MathOperations demonstrates basic math operations testing
func TestSequenceRecord_MathOperations(t *testing.T) {
tests := map[string]Reader{
"addition": Equal(4)(2 + 2),
"subtraction": Equal(1)(3 - 2),
"multiplication": Equal(6)(2 * 3),
"division": Equal(2)(6 / 3),
"modulo": Equal(1)(7 % 3),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all math operations to pass")
}
}
// TestSequenceRecord_BooleanTests tests SequenceRecord with boolean assertions
func TestSequenceRecord_BooleanTests(t *testing.T) {
tests := map[string]Reader{
"true_is_true": Equal(true)(true),
"false_is_false": Equal(false)(false),
"not_true": Equal(false)(!true),
"not_false": Equal(true)(!false),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all boolean tests to pass")
}
}
// TestSequenceRecord_ErrorTests tests SequenceRecord with error assertions
func TestSequenceRecord_ErrorTests(t *testing.T) {
tests := map[string]Reader{
"no_error": NoError(nil),
"equal_value": Equal("test")("test"),
"not_empty": StringNotEmpty("hello"),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all error tests to pass")
}
}
// TestTraverseRecord_vs_SequenceRecord demonstrates the relationship between the two functions
func TestTraverseRecord_vs_SequenceRecord(t *testing.T) {
type TestCase struct {
Input int
Expected int
}
testData := map[string]TestCase{
"test_1": {Input: 2, Expected: 4},
"test_2": {Input: 3, Expected: 9},
"test_3": {Input: 4, Expected: 16},
}
// Using TraverseRecord
traverseResult := TraverseRecord(func(tc TestCase) Reader {
return Equal(tc.Expected)(tc.Input * tc.Input)
})(testData)(t)
// Using SequenceRecord (manually creating the map)
tests := make(map[string]Reader)
for name, tc := range testData {
tests[name] = Equal(tc.Expected)(tc.Input * tc.Input)
}
seqResult := SequenceRecord(tests)(t)
if traverseResult != seqResult {
t.Error("Expected TraverseRecord and SequenceRecord to produce same result")
}
if !traverseResult || !seqResult {
t.Error("Expected both approaches to pass")
}
}
// TestSequenceRecord_WithAllOf demonstrates combining SequenceRecord with AllOf
func TestSequenceRecord_WithAllOf(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
tests := map[string]Reader{
"array_validations": AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
}),
"element_checks": AllOf([]Reader{
That(func(a []int) bool { return a[0] == 1 })(arr),
That(func(a []int) bool { return a[4] == 5 })(arr),
}),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected combined assertions to pass")
}
}
// TestTraverseRecord_ConfigValidation demonstrates real-world configuration validation
func TestTraverseRecord_ConfigValidation(t *testing.T) {
type Config struct {
Value int
Min int
Max int
}
configs := map[string]Config{
"timeout": {Value: 30, Min: 1, Max: 60},
"maxRetries": {Value: 3, Min: 1, Max: 10},
"bufferSize": {Value: 1024, Min: 512, Max: 4096},
}
validateConfig := func(c Config) Reader {
return AllOf([]Reader{
That(func(val int) bool { return val >= c.Min })(c.Value),
That(func(val int) bool { return val <= c.Max })(c.Value),
})
}
traverse := TraverseRecord(validateConfig)
result := traverse(configs)(t)
if !result {
t.Error("Expected all configurations to be within valid ranges")
}
}
// TestSequenceRecord_RealWorldExample demonstrates a realistic use case
func TestSequenceRecord_RealWorldExample(t *testing.T) {
type Response struct {
StatusCode int
Body string
}
response := Response{StatusCode: 200, Body: `{"status":"ok"}`}
tests := map[string]Reader{
"status_ok": Equal(200)(response.StatusCode),
"body_not_empty": StringNotEmpty(response.Body),
"body_is_json": That(func(s string) bool {
return len(s) > 0 && s[0] == '{' && s[len(s)-1] == '}'
})(response.Body),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected response validation to pass")
}
}

View File

@@ -1,11 +1,32 @@
// 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 assert
import (
"iter"
"testing"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
@@ -13,23 +34,506 @@ import (
type (
// Result represents a computation that may fail with an error.
//
// This is an alias for [result.Result][T], which encapsulates either a successful
// value of type T or an error. It's commonly used in test assertions to represent
// operations that might fail, allowing for functional error handling without exceptions.
//
// A Result can be in one of two states:
// - Success: Contains a value of type T
// - Failure: Contains an error
//
// This type is particularly useful in testing scenarios where you need to:
// - Test functions that return results
// - Chain operations that might fail
// - Handle errors functionally
//
// Example:
//
// func TestResultHandling(t *testing.T) {
// successResult := result.Of[int](42)
// assert.Success(successResult)(t) // Passes
//
// failureResult := result.Error[int](errors.New("failed"))
// assert.Failure(failureResult)(t) // Passes
// }
//
// See also:
// - [Success]: Asserts a Result is successful
// - [Failure]: Asserts a Result contains an error
// - [result.Result]: The underlying Result type
Result[T any] = result.Result[T]
// Reader represents a test assertion that depends on a testing.T context and returns a boolean.
// Reader represents a test assertion that depends on a [testing.T] context and returns a boolean.
//
// This is the core type for all assertions in this package. It's an alias for
// [reader.Reader][*testing.T, bool], which is a function that takes a testing context
// and produces a boolean result indicating whether the assertion passed.
//
// The Reader pattern enables:
// - Composable assertions that can be combined using functional operators
// - Deferred execution - assertions are defined but not executed until applied to a test
// - Reusable assertion logic that can be applied to multiple tests
// - Functional composition of complex test conditions
//
// All assertion functions in this package return a Reader, which must be applied
// to a *testing.T to execute the assertion:
//
// assertion := assert.Equal(42)(result) // Creates a Reader
// assertion(t) // Executes the assertion
//
// Readers can be composed using functions like [AllOf], [ApplicativeMonoid], or
// functional operators from the reader package.
//
// Example:
//
// func TestReaderComposition(t *testing.T) {
// // Create individual assertions
// assertion1 := assert.Equal(42)(42)
// assertion2 := assert.StringNotEmpty("hello")
//
// // Combine them
// combined := assert.AllOf([]assert.Reader{assertion1, assertion2})
//
// // Execute the combined assertion
// combined(t)
// }
//
// See also:
// - [Kleisli]: Function that produces a Reader from a value
// - [AllOf]: Combines multiple Readers
// - [ApplicativeMonoid]: Monoid for combining Readers
Reader = reader.Reader[*testing.T, bool]
// Kleisli represents a function that produces a test assertion Reader from a value of type T.
// Kleisli represents a function that produces a test assertion [Reader] from a value of type T.
//
// This is an alias for [reader.Reader][T, Reader], which is a function that takes a value
// of type T and returns a Reader (test assertion). This pattern is fundamental to the
// "data last" principle used throughout this package.
//
// Kleisli functions enable:
// - Partial application of assertions - configure the expected value first, apply actual value later
// - Reusable assertion builders that can be applied to different values
// - Functional composition of assertion pipelines
// - Point-free style programming with assertions
//
// Most assertion functions in this package return a Kleisli, which must be applied
// to the actual value being tested, and then to a *testing.T:
//
// kleisli := assert.Equal(42) // Kleisli[int] - expects an int
// reader := kleisli(result) // Reader - assertion ready to execute
// reader(t) // Execute the assertion
//
// Or more concisely:
//
// assert.Equal(42)(result)(t)
//
// Example:
//
// func TestKleisliPattern(t *testing.T) {
// // Create a reusable assertion for positive numbers
// isPositive := assert.That(func(n int) bool { return n > 0 })
//
// // Apply it to different values
// isPositive(42)(t) // Passes
// isPositive(100)(t) // Passes
// // isPositive(-5)(t) would fail
//
// // Can be used with Local for property testing
// type User struct { Age int }
// checkAge := assert.Local(func(u User) int { return u.Age })(isPositive)
// checkAge(User{Age: 25})(t) // Passes
// }
//
// See also:
// - [Reader]: The assertion type produced by Kleisli
// - [Local]: Focuses a Kleisli on a property of a larger structure
Kleisli[T any] = reader.Reader[T, Reader]
// Predicate represents a function that tests a value of type T and returns a boolean.
//
// This is an alias for [predicate.Predicate][T], which is a simple function that
// takes a value and returns true or false based on some condition. Predicates are
// used with the [That] function to create custom assertions.
//
// Predicates enable:
// - Custom validation logic for any type
// - Reusable test conditions
// - Composition of complex validation rules
// - Integration with functional programming patterns
//
// Example:
//
// func TestPredicates(t *testing.T) {
// // Simple predicate
// isEven := func(n int) bool { return n%2 == 0 }
// assert.That(isEven)(42)(t) // Passes
//
// // String predicate
// hasPrefix := func(s string) bool { return strings.HasPrefix(s, "test") }
// assert.That(hasPrefix)("test_file.go")(t) // Passes
//
// // Complex predicate
// isValidEmail := func(s string) bool {
// return strings.Contains(s, "@") && strings.Contains(s, ".")
// }
// assert.That(isValidEmail)("user@example.com")(t) // Passes
// }
//
// See also:
// - [That]: Creates an assertion from a Predicate
// - [predicate.Predicate]: The underlying predicate type
Predicate[T any] = predicate.Predicate[T]
// Lens is a functional reference to a subpart of a data structure.
//
// This is an alias for [lens.Lens][S, T], which provides a composable way to focus
// on a specific field within a larger structure. Lenses enable getting and setting
// values in nested data structures in a functional, immutable way.
//
// In the context of testing, lenses are used with [LocalL] to focus assertions
// on specific properties of complex objects without manually extracting those properties.
//
// A Lens[S, T] focuses on a value of type T within a structure of type S.
//
// Example:
//
// func TestLensUsage(t *testing.T) {
// type Address struct { City string }
// type User struct { Name string; Address Address }
//
// // Define lenses (typically generated)
// addressLens := lens.Lens[User, Address]{...}
// cityLens := lens.Lens[Address, string]{...}
//
// // Compose lenses to focus on nested field
// userCityLens := lens.Compose(addressLens, cityLens)
//
// // Use with LocalL to assert on nested property
// user := User{Name: "Alice", Address: Address{City: "NYC"}}
// assert.LocalL(userCityLens)(assert.Equal("NYC"))(user)(t)
// }
//
// See also:
// - [LocalL]: Uses a Lens to focus assertions on a property
// - [lens.Lens]: The underlying lens type
// - [Optional]: Similar but for values that may not exist
Lens[S, T any] = lens.Lens[S, T]
// Optional is an optic that focuses on a value that may or may not be present.
//
// This is an alias for [optional.Optional][S, T], which is similar to a [Lens] but
// handles cases where the focused value might not exist. Optionals are useful for
// working with nullable fields, optional properties, or values that might be absent.
//
// In testing, Optionals are used with [FromOptional] to create assertions that
// verify whether an optional value is present and, if so, whether it satisfies
// certain conditions.
//
// An Optional[S, T] focuses on an optional value of type T within a structure of type S.
//
// Example:
//
// func TestOptionalUsage(t *testing.T) {
// type Config struct { Timeout *int }
//
// // Define optional (typically generated)
// timeoutOptional := optional.Optional[Config, int]{...}
//
// // Test when value is present
// config1 := Config{Timeout: ptr(30)}
// assert.FromOptional(timeoutOptional)(
// assert.Equal(30),
// )(config1)(t) // Passes
//
// // Test when value is absent
// config2 := Config{Timeout: nil}
// // FromOptional would fail because value is not present
// }
//
// See also:
// - [FromOptional]: Creates assertions for optional values
// - [optional.Optional]: The underlying optional type
// - [Lens]: Similar but for values that always exist
Optional[S, T any] = optional.Optional[S, T]
// Prism is an optic that focuses on a case of a sum type.
//
// This is an alias for [prism.Prism][S, T], which provides a way to focus on one
// variant of a sum type (like Result, Option, Either, etc.). Prisms enable pattern
// matching and extraction of values from sum types in a functional way.
//
// In testing, Prisms are used with [FromPrism] to create assertions that verify
// whether a value matches a specific case and, if so, whether the contained value
// satisfies certain conditions.
//
// A Prism[S, T] focuses on a value of type T that may be contained within a sum type S.
//
// Example:
//
// func TestPrismUsage(t *testing.T) {
// // Prism for extracting success value from Result
// successPrism := prism.Success[int]()
//
// // Test successful result
// successResult := result.Of[int](42)
// assert.FromPrism(successPrism)(
// assert.Equal(42),
// )(successResult)(t) // Passes
//
// // Prism for extracting error from Result
// failurePrism := prism.Failure[int]()
//
// // Test failed result
// failureResult := result.Error[int](errors.New("failed"))
// assert.FromPrism(failurePrism)(
// assert.Error,
// )(failureResult)(t) // Passes
// }
//
// See also:
// - [FromPrism]: Creates assertions for prism-focused values
// - [prism.Prism]: The underlying prism type
// - [Optional]: Similar but for optional values
Prism[S, T any] = prism.Prism[S, T]
// ReaderIOResult represents a context-aware, IO-based computation that may fail.
//
// This is an alias for [readerioresult.ReaderIOResult][A], which combines three
// computational effects:
// - Reader: Depends on a context (like context.Context)
// - IO: Performs side effects (like file I/O, network calls)
// - Result: May fail with an error
//
// In testing, ReaderIOResult is used with [FromReaderIOResult] to convert
// context-aware, effectful computations into test assertions. This is useful
// when your test assertions need to:
// - Access a context for cancellation or deadlines
// - Perform IO operations (database queries, API calls, file access)
// - Handle potential errors gracefully
//
// Example:
//
// func TestReaderIOResult(t *testing.T) {
// // Create a ReaderIOResult that performs IO and may fail
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
// return func() result.Result[assert.Reader] {
// // Perform database check with context
// if err := db.PingContext(ctx); err != nil {
// return result.Error[assert.Reader](err)
// }
// return result.Of[assert.Reader](assert.NoError(nil))
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIOResult(checkDatabase)
// assertion(t)
// }
//
// See also:
// - [FromReaderIOResult]: Converts ReaderIOResult to Reader
// - [ReaderIO]: Similar but without error handling
// - [readerioresult.ReaderIOResult]: The underlying type
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// ReaderIO represents a context-aware, IO-based computation.
//
// This is an alias for [readerio.ReaderIO][A], which combines two computational effects:
// - Reader: Depends on a context (like context.Context)
// - IO: Performs side effects (like logging, metrics)
//
// In testing, ReaderIO is used with [FromReaderIO] to convert context-aware,
// effectful computations into test assertions. This is useful when your test
// assertions need to:
// - Access a context for cancellation or deadlines
// - Perform IO operations that don't fail (or handle failures internally)
// - Integrate with context-aware utilities
//
// Example:
//
// func TestReaderIO(t *testing.T) {
// // Create a ReaderIO that performs IO
// logAndCheck := func(ctx context.Context) func() assert.Reader {
// return func() assert.Reader {
// // Log with context
// logger.InfoContext(ctx, "Running test")
// // Return assertion
// return assert.Equal(42)(computeValue())
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIO(logAndCheck)
// assertion(t)
// }
//
// See also:
// - [FromReaderIO]: Converts ReaderIO to Reader
// - [ReaderIOResult]: Similar but with error handling
// - [readerio.ReaderIO]: The underlying type
ReaderIO[A any] = readerio.ReaderIO[A]
// Seq2 represents a Go iterator that yields key-value pairs.
//
// This is an alias for [iter.Seq2][K, A], which is Go's standard iterator type
// introduced in Go 1.23. It represents a sequence of key-value pairs that can be
// iterated over using a for-range loop.
//
// In testing, Seq2 is used with [SequenceSeq2] to execute a sequence of named
// test cases provided as an iterator. This enables:
// - Lazy evaluation of test cases
// - Memory-efficient testing of large test suites
// - Integration with Go's iterator patterns
// - Dynamic generation of test cases
//
// Example:
//
// func TestSeq2Usage(t *testing.T) {
// // Create an iterator of test cases
// testCases := func(yield func(string, assert.Reader) bool) {
// if !yield("test_addition", assert.Equal(4)(2+2)) {
// return
// }
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
// return
// }
// }
//
// // Execute all test cases
// assert.SequenceSeq2[assert.Reader](testCases)(t)
// }
//
// See also:
// - [SequenceSeq2]: Executes a Seq2 of test cases
// - [TraverseArray]: Similar but for arrays
// - [iter.Seq2]: The underlying iterator type
Seq2[K, A any] = iter.Seq2[K, A]
// Pair represents a tuple of two values with potentially different types.
//
// This is an alias for [pair.Pair][L, R], which holds two values: a "head" (or "left")
// of type L and a "tail" (or "right") of type R. Pairs are useful for grouping
// related values together without defining a custom struct.
//
// In testing, Pairs are used with [TraverseArray] to associate test names with
// their corresponding assertions. Each element in the array is transformed into
// a Pair[string, Reader] where the string is the test name and the Reader is
// the assertion to execute.
//
// Example:
//
// func TestPairUsage(t *testing.T) {
// type TestCase struct {
// Input int
// Expected int
// }
//
// testCases := []TestCase{
// {Input: 2, Expected: 4},
// {Input: 3, Expected: 9},
// }
//
// // Transform each test case into a named assertion
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
// return pair.MakePair(name, assertion)
// })
//
// traverse(testCases)(t)
// }
//
// See also:
// - [TraverseArray]: Uses Pairs to create named test cases
// - [pair.Pair]: The underlying pair type
// - [pair.MakePair]: Creates a Pair
// - [pair.Head]: Extracts the first value
// - [pair.Tail]: Extracts the second value
Pair[L, R any] = pair.Pair[L, R]
// Void represents the absence of a meaningful value, similar to unit type in functional programming.
//
// This is an alias for [function.Void], which is used to represent operations that don't
// return a meaningful value but may perform side effects. In the context of testing, Void
// is used with IO operations that perform actions without producing a result.
//
// Void is conceptually similar to:
// - Unit type in functional languages (Haskell's (), Scala's Unit)
// - void in languages like C/Java (but as a value, not just a type)
// - Empty struct{} in Go (but with clearer semantic meaning)
//
// Example:
//
// func TestWithSideEffect(t *testing.T) {
// // An IO operation that logs but returns Void
// logOperation := func() function.Void {
// log.Println("Test executed")
// return function.Void{}
// }
//
// // Execute the operation
// logOperation()
// }
//
// See also:
// - [IO]: Wraps side-effecting operations
// - [function.Void]: The underlying void type
Void = function.Void
// IO represents a side-effecting computation that produces a value of type A.
//
// This is an alias for [io.IO][A], which encapsulates operations that perform side effects
// (like I/O operations, logging, or state mutations) and return a value. IO is a lazy
// computation - it describes an effect but doesn't execute it until explicitly run.
//
// In testing, IO is used to:
// - Defer execution of side effects until needed
// - Compose multiple side-effecting operations
// - Maintain referential transparency in test setup
// - Separate effect description from effect execution
//
// An IO[A] is essentially a function `func() A` that:
// - Encapsulates a side effect
// - Returns a value of type A when executed
// - Can be composed with other IO operations
//
// Example:
//
// func TestIOOperation(t *testing.T) {
// // Define an IO operation that reads a file
// readConfig := func() io.IO[string] {
// return func() string {
// data, _ := os.ReadFile("config.txt")
// return string(data)
// }
// }
//
// // The IO is not executed yet - it's just a description
// configIO := readConfig()
//
// // Execute the IO to get the result
// config := configIO()
// assert.StringNotEmpty(config)(t)
// }
//
// Example with composition:
//
// func TestIOComposition(t *testing.T) {
// // Chain multiple IO operations
// pipeline := io.Map(
// func(s string) int { return len(s) },
// )(readFileIO)
//
// // Execute the composed operation
// length := pipeline()
// assert.That(func(n int) bool { return n > 0 })(length)(t)
// }
//
// See also:
// - [ReaderIO]: Combines Reader and IO effects
// - [ReaderIOResult]: Adds error handling to ReaderIO
// - [io.IO]: The underlying IO type
// - [Void]: Represents operations without meaningful return values
IO[A any] = io.IO[A]
)

273
v2/cli/README.md Normal file
View File

@@ -0,0 +1,273 @@
# CLI Package - Functional Wrappers for urfave/cli/v3
This package provides functional programming wrappers for the `github.com/urfave/cli/v3` library, enabling Effect-based command actions and type-safe flag handling through Prisms.
## Features
### 1. Effect-Based Command Actions
Transform CLI command actions into composable Effects that follow functional programming principles.
#### Key Functions
- **`ToAction(effect CommandEffect) func(context.Context, *C.Command) error`**
- Converts a CommandEffect into a standard urfave/cli Action function
- Enables Effect-based command handlers to work with cli/v3 framework
- **`FromAction(action func(context.Context, *C.Command) error) CommandEffect`**
- Lifts existing cli/v3 action handlers into the Effect type
- Allows gradual migration to functional style
- **`MakeCommand(name, usage string, flags []C.Flag, effect CommandEffect) *C.Command`**
- Creates a new Command with an Effect-based action
- Convenience function combining command creation with Effect conversion
- **`MakeCommandWithSubcommands(...) *C.Command`**
- Creates a Command with subcommands and an Effect-based action
#### Example Usage
```go
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Define an Effect-based command action
processEffect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process input...
return R.Of(F.Void{})
}
}
}
// Create command with Effect
command := cli.MakeCommand(
"process",
"Process input files",
[]C.Flag{
&C.StringFlag{Name: "input", Usage: "Input file path"},
},
processEffect,
)
// Or convert existing action to Effect
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Existing logic...
return nil
}
effect := cli.FromAction(existingAction)
```
### 2. Flag Type Prisms
Type-safe extraction and manipulation of CLI flags using Prisms from the optics package.
#### Available Prisms
- `StringFlagPrism()` - Extract `*C.StringFlag` from `C.Flag`
- `IntFlagPrism()` - Extract `*C.IntFlag` from `C.Flag`
- `BoolFlagPrism()` - Extract `*C.BoolFlag` from `C.Flag`
- `Float64FlagPrism()` - Extract `*C.Float64Flag` from `C.Flag`
- `DurationFlagPrism()` - Extract `*C.DurationFlag` from `C.Flag`
- `TimestampFlagPrism()` - Extract `*C.TimestampFlag` from `C.Flag`
- `StringSliceFlagPrism()` - Extract `*C.StringSliceFlag` from `C.Flag`
- `IntSliceFlagPrism()` - Extract `*C.IntSliceFlag` from `C.Flag`
- `Float64SliceFlagPrism()` - Extract `*C.Float64SliceFlag` from `C.Flag`
- `UintFlagPrism()` - Extract `*C.UintFlag` from `C.Flag`
- `Uint64FlagPrism()` - Extract `*C.Uint64Flag` from `C.Flag`
- `Int64FlagPrism()` - Extract `*C.Int64Flag` from `C.Flag`
#### Example Usage
```go
import (
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Extract a StringFlag from a Flag interface
var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
prism := cli.StringFlagPrism()
// Safe extraction returns Option
result := prism.GetOption(flag)
if O.IsSome(result) {
strFlag := O.MonadFold(result,
func() *C.StringFlag { return nil },
func(f *C.StringFlag) *C.StringFlag { return f },
)
// Use strFlag...
}
// Type mismatch returns None
var intFlag C.Flag = &C.IntFlag{Name: "count"}
result = prism.GetOption(intFlag) // Returns None
// Convert back to Flag
strFlag := &C.StringFlag{Name: "output"}
flag = prism.ReverseGet(strFlag)
```
## Type Definitions
### CommandEffect
```go
type CommandEffect = E.Effect[*C.Command, F.Void]
```
A CommandEffect represents a CLI command action as an Effect. It takes a `*C.Command` as context and produces a result wrapped in the Effect monad.
The Effect structure is:
```
func(*C.Command) -> func(context.Context) -> func() -> Result[Void]
```
This allows for:
- **Composability**: Effects can be composed using standard functional combinators
- **Testability**: Pure functions are easier to test
- **Error Handling**: Errors are explicitly represented in the Result type
- **Context Management**: Context flows naturally through the Effect
## Benefits
### 1. Functional Composition
Effects can be composed using standard functional programming patterns:
```go
import (
F "github.com/IBM/fp-go/v2/function"
RRIOE "github.com/IBM/fp-go/v2/context/readerreaderioresult"
)
// Compose multiple effects
validateInput := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
processData := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
saveResults := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
// Chain effects together
pipeline := F.Pipe3(
validateInput,
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return processData }),
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return saveResults }),
)
```
### 2. Type Safety
Prisms provide compile-time type safety when working with flags:
```go
// Type-safe flag extraction
flags := []C.Flag{
&C.StringFlag{Name: "input"},
&C.IntFlag{Name: "count"},
}
for _, flag := range flags {
// Safe extraction with pattern matching
O.MonadFold(
cli.StringFlagPrism().GetOption(flag),
func() { /* Not a string flag */ },
func(sf *C.StringFlag) { /* Handle string flag */ },
)
}
```
### 3. Error Handling
Errors are explicitly represented in the Result type:
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
if err := validateInput(cmd); err != nil {
return R.Left[F.Void](err) // Explicit error
}
return R.Of(F.Void{}) // Success
}
}
}
```
### 4. Testability
Pure functions are easier to test:
```go
func TestCommandEffect(t *testing.T) {
cmd := &C.Command{Name: "test"}
effect := myCommandEffect(cmd)
// Execute effect
result := effect(context.Background())()
// Assert on result
assert.True(t, R.IsRight(result))
}
```
## Migration Guide
### From Standard Actions to Effects
**Before:**
```go
command := &C.Command{
Name: "process",
Action: func(ctx context.Context, cmd *C.Command) error {
input := cmd.String("input")
// Process...
return nil
},
}
```
**After:**
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process...
return R.Of(F.Void{})
}
}
}
command := cli.MakeCommand("process", "Process files", flags, effect)
```
### Gradual Migration
You can mix both styles during migration:
```go
// Wrap existing action
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Legacy code...
return nil
}
// Use as Effect
effect := cli.FromAction(existingAction)
command := cli.MakeCommand("legacy", "Legacy command", flags, effect)
```
## See Also
- [Effect Package](../effect/) - Core Effect type definitions
- [Optics Package](../optics/) - Prism and other optics
- [urfave/cli/v3](https://github.com/urfave/cli) - Underlying CLI framework

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -388,8 +387,8 @@ func generateApplyHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -266,8 +265,8 @@ func generateBindHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -189,8 +188,8 @@ func generateDIHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

199
v2/cli/effect.go Normal file
View File

@@ -0,0 +1,199 @@
// 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 cli
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
R "github.com/IBM/fp-go/v2/result"
C "github.com/urfave/cli/v3"
)
// CommandEffect represents a CLI command action as an Effect.
// The Effect takes a *C.Command as context and produces a result.
type CommandEffect = E.Effect[*C.Command, F.Void]
// ToAction converts a CommandEffect into a standard urfave/cli Action function.
// This allows Effect-based command handlers to be used with the cli/v3 framework.
//
// The conversion process:
// 1. Takes the Effect which expects a *C.Command context
// 2. Executes it with the provided command
// 3. Runs the resulting IO operation
// 4. Converts the Result to either nil (success) or error (failure)
//
// # Parameters
//
// - effect: The CommandEffect to convert
//
// # Returns
//
// - A function compatible with C.Command.Action signature
//
// # Example Usage
//
// effect := func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// // Command logic here
// return R.Of(F.Void{})
// }
// }
// }
// action := ToAction(effect)
// command := &C.Command{
// Name: "example",
// Action: action,
// }
func ToAction(effect CommandEffect) func(context.Context, *C.Command) error {
return func(ctx context.Context, cmd *C.Command) error {
// Execute the effect: cmd -> ctx -> IO -> Result
return F.Pipe3(
ctx,
effect(cmd),
io.Run,
// Convert Result[Void] to error
ET.Fold(F.Identity[error], F.Constant1[F.Void, error](nil)),
)
}
}
// FromAction converts a standard urfave/cli Action function into a CommandEffect.
// This allows existing cli/v3 action handlers to be lifted into the Effect type.
//
// The conversion process:
// 1. Takes a standard action function (context.Context, *C.Command) -> error
// 2. Wraps it in the Effect structure
// 3. Converts the error result to a Result type
//
// # Parameters
//
// - action: The standard cli/v3 action function to convert
//
// # Returns
//
// - A CommandEffect that wraps the original action
//
// # Example Usage
//
// standardAction := func(ctx context.Context, cmd *C.Command) error {
// // Existing command logic
// return nil
// }
// effect := FromAction(standardAction)
// // Now can be composed with other Effects
func FromAction(action func(context.Context, *C.Command) error) CommandEffect {
return func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
err := action(ctx, cmd)
if err != nil {
return R.Left[F.Void](err)
}
return R.Of(F.Void{})
}
}
}
}
// MakeCommand creates a new Command with an Effect-based action.
// This is a convenience function that combines command creation with Effect conversion.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommand(
// "process",
// "Process data files",
// []C.Flag{
// &C.StringFlag{Name: "input", Usage: "Input file"},
// },
// func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// input := cmd.String("input")
// // Process input...
// return R.Of(F.Void{})
// }
// }
// },
// )
func MakeCommand(
name string,
usage string,
flags []C.Flag,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Action: ToAction(effect),
}
}
// MakeCommandWithSubcommands creates a new Command with subcommands and an Effect-based action.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - commands: The subcommands
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with subcommands and the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommandWithSubcommands(
// "app",
// "Application commands",
// []C.Flag{},
// []*C.Command{subCmd1, subCmd2},
// defaultEffect,
// )
func MakeCommandWithSubcommands(
name string,
usage string,
flags []C.Flag,
commands []*C.Command,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Commands: commands,
Action: ToAction(effect),
}
}

204
v2/cli/effect_test.go Normal file
View File

@@ -0,0 +1,204 @@
// 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 cli
import (
"context"
"errors"
"testing"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestToAction_Success(t *testing.T) {
t.Run("converts successful Effect to action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
})
}
func TestToAction_Failure(t *testing.T) {
t.Run("converts failed Effect to error", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Left[F.Void](expectedErr)
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestFromAction_Success(t *testing.T) {
t.Run("converts successful action to Effect", func(t *testing.T) {
// Arrange
action := func(ctx context.Context, cmd *C.Command) error {
return nil
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsRight(result))
})
}
func TestFromAction_Failure(t *testing.T) {
t.Run("converts failed action to Effect", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
action := func(ctx context.Context, cmd *C.Command) error {
return expectedErr
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsLeft(result))
err := R.MonadFold(result, F.Identity[error], func(F.Void) error { return nil })
assert.Equal(t, expectedErr, err)
})
}
func TestMakeCommand(t *testing.T) {
t.Run("creates command with Effect-based action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommand(
"test",
"Test command",
[]C.Flag{},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "test", cmd.Name)
assert.Equal(t, "Test command", cmd.Usage)
assert.NotNil(t, cmd.Action)
// Test the action
err := cmd.Action(context.Background(), cmd)
assert.NoError(t, err)
})
}
func TestMakeCommandWithSubcommands(t *testing.T) {
t.Run("creates command with subcommands and Effect-based action", func(t *testing.T) {
// Arrange
subCmd := &C.Command{Name: "sub"}
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommandWithSubcommands(
"parent",
"Parent command",
[]C.Flag{},
[]*C.Command{subCmd},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "parent", cmd.Name)
assert.Equal(t, "Parent command", cmd.Usage)
assert.Len(t, cmd.Commands, 1)
assert.Equal(t, "sub", cmd.Commands[0].Name)
assert.NotNil(t, cmd.Action)
})
}
func TestToAction_Integration(t *testing.T) {
t.Run("Effect can access command flags", func(t *testing.T) {
// Arrange
var capturedValue string
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
capturedValue = cmd.String("input")
return R.Of(F.Void{})
}
}
}
cmd := &C.Command{
Name: "test",
Flags: []C.Flag{
&C.StringFlag{
Name: "input",
Value: "default-value",
},
},
Action: ToAction(effect),
}
// Act
err := cmd.Action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
assert.Equal(t, "default-value", capturedValue)
})
}

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -148,8 +147,8 @@ func generateEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

359
v2/cli/flags.go Normal file
View File

@@ -0,0 +1,359 @@
// 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 cli
import (
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
C "github.com/urfave/cli/v3"
)
// StringFlagPrism creates a Prism for extracting a StringFlag from a Flag.
// This provides a type-safe way to work with string flags, handling type
// mismatches gracefully through the Option type.
//
// The prism's GetOption attempts to cast a Flag to *C.StringFlag.
// If the cast succeeds, it returns Some(*C.StringFlag); if it fails, it returns None.
//
// The prism's ReverseGet converts a *C.StringFlag back to a Flag.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringFlag] for safe StringFlag extraction
//
// # Example Usage
//
// prism := StringFlagPrism()
//
// // Extract StringFlag from Flag
// var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
// result := prism.GetOption(flag) // Some(*C.StringFlag{...})
//
// // Type mismatch returns None
// var intFlag C.Flag = &C.IntFlag{Name: "count"}
// result = prism.GetOption(intFlag) // None[*C.StringFlag]()
//
// // Convert back to Flag
// strFlag := &C.StringFlag{Name: "output"}
// flag = prism.ReverseGet(strFlag)
func StringFlagPrism() P.Prism[C.Flag, *C.StringFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringFlag] {
if sf, ok := flag.(*C.StringFlag); ok {
return O.Some(sf)
}
return O.None[*C.StringFlag]()
},
func(f *C.StringFlag) C.Flag { return f },
)
}
// IntFlagPrism creates a Prism for extracting an IntFlag from a Flag.
// This provides a type-safe way to work with integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntFlag] for safe IntFlag extraction
//
// # Example Usage
//
// prism := IntFlagPrism()
//
// // Extract IntFlag from Flag
// var flag C.Flag = &C.IntFlag{Name: "count", Value: 10}
// result := prism.GetOption(flag) // Some(*C.IntFlag{...})
func IntFlagPrism() P.Prism[C.Flag, *C.IntFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntFlag] {
if f, ok := flag.(*C.IntFlag); ok {
return O.Some(f)
}
return O.None[*C.IntFlag]()
},
func(f *C.IntFlag) C.Flag { return f },
)
}
// BoolFlagPrism creates a Prism for extracting a BoolFlag from a Flag.
// This provides a type-safe way to work with boolean flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.BoolFlag] for safe BoolFlag extraction
//
// # Example Usage
//
// prism := BoolFlagPrism()
//
// // Extract BoolFlag from Flag
// var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// result := prism.GetOption(flag) // Some(*C.BoolFlag{...})
func BoolFlagPrism() P.Prism[C.Flag, *C.BoolFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.BoolFlag] {
if f, ok := flag.(*C.BoolFlag); ok {
return O.Some(f)
}
return O.None[*C.BoolFlag]()
},
func(f *C.BoolFlag) C.Flag { return f },
)
}
// Float64FlagPrism creates a Prism for extracting a Float64Flag from a Flag.
// This provides a type-safe way to work with float64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64Flag] for safe Float64Flag extraction
//
// # Example Usage
//
// prism := Float64FlagPrism()
//
// // Extract Float64Flag from Flag
// var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// result := prism.GetOption(flag) // Some(*C.Float64Flag{...})
func Float64FlagPrism() P.Prism[C.Flag, *C.Float64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64Flag] {
if f, ok := flag.(*C.Float64Flag); ok {
return O.Some(f)
}
return O.None[*C.Float64Flag]()
},
func(f *C.Float64Flag) C.Flag { return f },
)
}
// DurationFlagPrism creates a Prism for extracting a DurationFlag from a Flag.
// This provides a type-safe way to work with duration flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.DurationFlag] for safe DurationFlag extraction
//
// # Example Usage
//
// prism := DurationFlagPrism()
//
// // Extract DurationFlag from Flag
// var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: 30 * time.Second}
// result := prism.GetOption(flag) // Some(*C.DurationFlag{...})
func DurationFlagPrism() P.Prism[C.Flag, *C.DurationFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.DurationFlag] {
if f, ok := flag.(*C.DurationFlag); ok {
return O.Some(f)
}
return O.None[*C.DurationFlag]()
},
func(f *C.DurationFlag) C.Flag { return f },
)
}
// TimestampFlagPrism creates a Prism for extracting a TimestampFlag from a Flag.
// This provides a type-safe way to work with timestamp flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.TimestampFlag] for safe TimestampFlag extraction
//
// # Example Usage
//
// prism := TimestampFlagPrism()
//
// // Extract TimestampFlag from Flag
// var flag C.Flag = &C.TimestampFlag{Name: "created"}
// result := prism.GetOption(flag) // Some(*C.TimestampFlag{...})
func TimestampFlagPrism() P.Prism[C.Flag, *C.TimestampFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.TimestampFlag] {
if f, ok := flag.(*C.TimestampFlag); ok {
return O.Some(f)
}
return O.None[*C.TimestampFlag]()
},
func(f *C.TimestampFlag) C.Flag { return f },
)
}
// StringSliceFlagPrism creates a Prism for extracting a StringSliceFlag from a Flag.
// This provides a type-safe way to work with string slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringSliceFlag] for safe StringSliceFlag extraction
//
// # Example Usage
//
// prism := StringSliceFlagPrism()
//
// // Extract StringSliceFlag from Flag
// var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// result := prism.GetOption(flag) // Some(*C.StringSliceFlag{...})
func StringSliceFlagPrism() P.Prism[C.Flag, *C.StringSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringSliceFlag] {
if f, ok := flag.(*C.StringSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.StringSliceFlag]()
},
func(f *C.StringSliceFlag) C.Flag { return f },
)
}
// IntSliceFlagPrism creates a Prism for extracting an IntSliceFlag from a Flag.
// This provides a type-safe way to work with int slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntSliceFlag] for safe IntSliceFlag extraction
//
// # Example Usage
//
// prism := IntSliceFlagPrism()
//
// // Extract IntSliceFlag from Flag
// var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// result := prism.GetOption(flag) // Some(*C.IntSliceFlag{...})
func IntSliceFlagPrism() P.Prism[C.Flag, *C.IntSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntSliceFlag] {
if f, ok := flag.(*C.IntSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.IntSliceFlag]()
},
func(f *C.IntSliceFlag) C.Flag { return f },
)
}
// Float64SliceFlagPrism creates a Prism for extracting a Float64SliceFlag from a Flag.
// This provides a type-safe way to work with float64 slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64SliceFlag] for safe Float64SliceFlag extraction
//
// # Example Usage
//
// prism := Float64SliceFlagPrism()
//
// // Extract Float64SliceFlag from Flag
// var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// result := prism.GetOption(flag) // Some(*C.Float64SliceFlag{...})
func Float64SliceFlagPrism() P.Prism[C.Flag, *C.Float64SliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64SliceFlag] {
if f, ok := flag.(*C.Float64SliceFlag); ok {
return O.Some(f)
}
return O.None[*C.Float64SliceFlag]()
},
func(f *C.Float64SliceFlag) C.Flag { return f },
)
}
// UintFlagPrism creates a Prism for extracting a UintFlag from a Flag.
// This provides a type-safe way to work with unsigned integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.UintFlag] for safe UintFlag extraction
//
// # Example Usage
//
// prism := UintFlagPrism()
//
// // Extract UintFlag from Flag
// var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// result := prism.GetOption(flag) // Some(*C.UintFlag{...})
func UintFlagPrism() P.Prism[C.Flag, *C.UintFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.UintFlag] {
if f, ok := flag.(*C.UintFlag); ok {
return O.Some(f)
}
return O.None[*C.UintFlag]()
},
func(f *C.UintFlag) C.Flag { return f },
)
}
// Uint64FlagPrism creates a Prism for extracting a Uint64Flag from a Flag.
// This provides a type-safe way to work with uint64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Uint64Flag] for safe Uint64Flag extraction
//
// # Example Usage
//
// prism := Uint64FlagPrism()
//
// // Extract Uint64Flag from Flag
// var flag C.Flag = &C.Uint64Flag{Name: "size"}
// result := prism.GetOption(flag) // Some(*C.Uint64Flag{...})
func Uint64FlagPrism() P.Prism[C.Flag, *C.Uint64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Uint64Flag] {
if f, ok := flag.(*C.Uint64Flag); ok {
return O.Some(f)
}
return O.None[*C.Uint64Flag]()
},
func(f *C.Uint64Flag) C.Flag { return f },
)
}
// Int64FlagPrism creates a Prism for extracting an Int64Flag from a Flag.
// This provides a type-safe way to work with int64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Int64Flag] for safe Int64Flag extraction
//
// # Example Usage
//
// prism := Int64FlagPrism()
//
// // Extract Int64Flag from Flag
// var flag C.Flag = &C.Int64Flag{Name: "offset"}
// result := prism.GetOption(flag) // Some(*C.Int64Flag{...})
func Int64FlagPrism() P.Prism[C.Flag, *C.Int64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Int64Flag] {
if f, ok := flag.(*C.Int64Flag); ok {
return O.Some(f)
}
return O.None[*C.Int64Flag]()
},
func(f *C.Int64Flag) C.Flag { return f },
)
}

287
v2/cli/flags_test.go Normal file
View File

@@ -0,0 +1,287 @@
// 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 cli
import (
"testing"
"time"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestStringFlagPrism_Success(t *testing.T) {
t.Run("extracts StringFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.StringFlag{Name: "input", Value: "test"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringFlag { return nil }, func(f *C.StringFlag) *C.StringFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "input", extracted.Name)
assert.Equal(t, "test", extracted.Value)
})
}
func TestStringFlagPrism_Failure(t *testing.T) {
t.Run("returns None for non-StringFlag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsNone(result))
})
}
func TestStringFlagPrism_ReverseGet(t *testing.T) {
t.Run("converts StringFlag back to Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
strFlag := &C.StringFlag{Name: "output", Value: "result"}
// Act
flag := prism.ReverseGet(strFlag)
// Assert
assert.NotNil(t, flag)
assert.IsType(t, &C.StringFlag{}, flag)
})
}
func TestIntFlagPrism_Success(t *testing.T) {
t.Run("extracts IntFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count", Value: 42}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntFlag { return nil }, func(f *C.IntFlag) *C.IntFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "count", extracted.Name)
assert.Equal(t, 42, extracted.Value)
})
}
func TestBoolFlagPrism_Success(t *testing.T) {
t.Run("extracts BoolFlag from Flag", func(t *testing.T) {
// Arrange
prism := BoolFlagPrism()
var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.BoolFlag { return nil }, func(f *C.BoolFlag) *C.BoolFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "verbose", extracted.Name)
assert.Equal(t, true, extracted.Value)
})
}
func TestFloat64FlagPrism_Success(t *testing.T) {
t.Run("extracts Float64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Float64FlagPrism()
var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64Flag { return nil }, func(f *C.Float64Flag) *C.Float64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratio", extracted.Name)
assert.Equal(t, 0.5, extracted.Value)
})
}
func TestDurationFlagPrism_Success(t *testing.T) {
t.Run("extracts DurationFlag from Flag", func(t *testing.T) {
// Arrange
prism := DurationFlagPrism()
duration := 30 * time.Second
var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: duration}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.DurationFlag { return nil }, func(f *C.DurationFlag) *C.DurationFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "timeout", extracted.Name)
assert.Equal(t, duration, extracted.Value)
})
}
func TestTimestampFlagPrism_Success(t *testing.T) {
t.Run("extracts TimestampFlag from Flag", func(t *testing.T) {
// Arrange
prism := TimestampFlagPrism()
var flag C.Flag = &C.TimestampFlag{Name: "created"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.TimestampFlag { return nil }, func(f *C.TimestampFlag) *C.TimestampFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "created", extracted.Name)
})
}
func TestStringSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts StringSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringSliceFlagPrism()
var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringSliceFlag { return nil }, func(f *C.StringSliceFlag) *C.StringSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "tags", extracted.Name)
})
}
func TestIntSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts IntSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntSliceFlagPrism()
var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntSliceFlag { return nil }, func(f *C.IntSliceFlag) *C.IntSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ports", extracted.Name)
})
}
func TestFloat64SliceFlagPrism_Success(t *testing.T) {
t.Run("extracts Float64SliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := Float64SliceFlagPrism()
var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64SliceFlag { return nil }, func(f *C.Float64SliceFlag) *C.Float64SliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratios", extracted.Name)
})
}
func TestUintFlagPrism_Success(t *testing.T) {
t.Run("extracts UintFlag from Flag", func(t *testing.T) {
// Arrange
prism := UintFlagPrism()
var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.UintFlag { return nil }, func(f *C.UintFlag) *C.UintFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "workers", extracted.Name)
assert.Equal(t, uint(4), extracted.Value)
})
}
func TestUint64FlagPrism_Success(t *testing.T) {
t.Run("extracts Uint64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Uint64FlagPrism()
var flag C.Flag = &C.Uint64Flag{Name: "size", Value: 1024}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Uint64Flag { return nil }, func(f *C.Uint64Flag) *C.Uint64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "size", extracted.Name)
assert.Equal(t, uint64(1024), extracted.Value)
})
}
func TestInt64FlagPrism_Success(t *testing.T) {
t.Run("extracts Int64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Int64FlagPrism()
var flag C.Flag = &C.Int64Flag{Name: "offset", Value: -100}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Int64Flag { return nil }, func(f *C.Int64Flag) *C.Int64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "offset", extracted.Name)
assert.Equal(t, int64(-100), extracted.Value)
})
}
func TestPrisms_EdgeCases(t *testing.T) {
t.Run("all prisms return None for wrong type", func(t *testing.T) {
// Arrange
var flag C.Flag = &C.StringFlag{Name: "test"}
// Act & Assert
assert.True(t, O.IsNone(IntFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(BoolFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(DurationFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(TimestampFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(StringSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(IntSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64SliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(UintFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Uint64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Int64FlagPrism().GetOption(flag)))
})
}

View File

@@ -18,7 +18,6 @@ package cli
import (
"fmt"
"os"
"time"
)
func writePackage(f *os.File, pkg string) {
@@ -26,6 +25,6 @@ func writePackage(f *os.File, pkg string) {
fmt.Fprintf(f, "package %s\n\n", pkg)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
}

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -62,8 +61,8 @@ func generateIdentityHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -71,8 +70,8 @@ func generateIOHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -219,8 +218,8 @@ func generateIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -234,8 +233,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v3"
@@ -76,8 +75,8 @@ func generateIOOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -148,8 +147,8 @@ func generateOptionHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -378,8 +377,8 @@ func generatePipeHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n", pkg)

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -118,8 +117,8 @@ func generateReaderHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -131,8 +130,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -21,7 +21,6 @@ import (
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v3"
)
@@ -233,8 +232,8 @@ func generateReaderIOEitherHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)
@@ -246,8 +245,7 @@ import (
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintln(fg, "// This file was generated by robots.")
fmt.Fprintf(fg, "package generic\n\n")

View File

@@ -22,7 +22,6 @@ import (
"os"
"path/filepath"
"strings"
"time"
C "github.com/urfave/cli/v3"
)
@@ -399,8 +398,8 @@ func generateTupleHelpers(filename string, count int) error {
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintln(f, "// This file was generated by robots.")
fmt.Fprintln(f)
fmt.Fprintf(f, "package %s\n\n", pkg)

View File

@@ -13,6 +13,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package constant provides the Const functor, a phantom type that ignores its second type parameter.
//
// The Const functor is a fundamental building block in functional programming that wraps a value
// of type E while having a phantom type parameter A. This makes it useful for:
// - Accumulating values during traversals (e.g., collecting metadata)
// - Implementing optics (lenses, prisms) where you need to track information
// - Building applicative functors that combine values using a semigroup
//
// # The Const Functor
//
// Const[E, A] wraps a value of type E and has a phantom type parameter A that doesn't affect
// the runtime value. This allows it to participate in functor and applicative operations while
// maintaining the wrapped value unchanged.
//
// # Key Properties
//
// - Map operations ignore the function and preserve the wrapped value
// - Ap operations combine wrapped values using a semigroup
// - The phantom type A allows type-safe composition with other functors
//
// # Example Usage
//
// // Accumulate string values
// c1 := Make[string, int]("hello")
// c2 := Make[string, int]("world")
//
// // Map doesn't change the wrapped value
// mapped := Map[string, int, string](strconv.Itoa)(c1) // Still contains "hello"
//
// // Ap combines values using a semigroup
// combined := Ap[string, int, int](S.Monoid)(c1)(c2) // Contains "helloworld"
package constant
import (
@@ -21,36 +52,209 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// Const is a functor that wraps a value of type E with a phantom type parameter A.
//
// The Const functor is useful for accumulating values during traversals or implementing
// optics. The type parameter A is phantom - it doesn't affect the runtime value but allows
// the type to participate in functor and applicative operations.
//
// Type Parameters:
// - E: The type of the wrapped value (the actual data)
// - A: The phantom type parameter (not stored, only used for type-level operations)
//
// Example:
//
// // Create a Const that wraps a string
// c := Make[string, int]("metadata")
//
// // The int type parameter is phantom - no int value is stored
// value := Unwrap(c) // "metadata"
type Const[E, A any] struct {
value E
}
// Make creates a Const value wrapping the given value.
//
// This is the primary constructor for Const values. The second type parameter A
// is phantom and must be specified explicitly when needed for type inference.
//
// Type Parameters:
// - E: The type of the value to wrap
// - A: The phantom type parameter
//
// Parameters:
// - e: The value to wrap
//
// Returns:
// - A Const[E, A] wrapping the value
//
// Example:
//
// c := Make[string, int]("hello")
// value := Unwrap(c) // "hello"
func Make[E, A any](e E) Const[E, A] {
return Const[E, A]{value: e}
}
// Unwrap extracts the wrapped value from a Const.
//
// This is the inverse of Make, retrieving the actual value stored in the Const.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The phantom type parameter
//
// Parameters:
// - c: The Const to unwrap
//
// Returns:
// - The wrapped value of type E
//
// Example:
//
// c := Make[string, int]("world")
// value := Unwrap(c) // "world"
func Unwrap[E, A any](c Const[E, A]) E {
return c.value
}
// Of creates a Const containing the monoid's empty value, ignoring the input.
//
// This implements the Applicative's "pure" operation for Const. It creates a Const
// wrapping the monoid's identity element, regardless of the input value.
//
// Type Parameters:
// - E: The type of the wrapped value (must have a monoid)
// - A: The input type (ignored)
//
// Parameters:
// - m: The monoid providing the empty value
//
// Returns:
// - A function that ignores its input and returns Const[E, A] with the empty value
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// of := Of[string, int](S.Monoid)
// c := of(42) // Const[string, int] containing ""
// value := Unwrap(c) // ""
func Of[E, A any](m M.Monoid[E]) func(A) Const[E, A] {
return F.Constant1[A](Make[E, A](m.Empty()))
}
// MonadMap applies a function to the phantom type parameter without changing the wrapped value.
//
// This implements the Functor's map operation for Const. Since the type parameter A is phantom,
// the function is never actually called - the wrapped value E remains unchanged.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - fa: The Const to map over
// - _: The function to apply (ignored)
//
// Returns:
// - A Const[E, B] with the same wrapped value
//
// Example:
//
// c := Make[string, int]("hello")
// mapped := MonadMap(c, func(i int) string { return strconv.Itoa(i) })
// // mapped still contains "hello", function was never called
func MonadMap[E, A, B any](fa Const[E, A], _ func(A) B) Const[E, B] {
return Make[E, B](fa.value)
}
// MonadAp combines two Const values using a semigroup.
//
// This implements the Applicative's ap operation for Const. It combines the wrapped
// values from both Const instances using the provided semigroup, ignoring the function
// type in the first argument.
//
// Type Parameters:
// - E: The type of the wrapped values (must have a semigroup)
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - s: The semigroup for combining wrapped values
//
// Returns:
// - A function that takes two Const values and combines their wrapped values
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// ap := MonadAp[string, int, int](S.Monoid)
// c1 := Make[string, func(int) int]("hello")
// c2 := Make[string, int]("world")
// result := ap(c1, c2) // Const containing "helloworld"
func MonadAp[E, A, B any](s S.Semigroup[E]) func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
return func(fab Const[E, func(A) B], fa Const[E, A]) Const[E, B] {
return Make[E, B](s.Concat(fab.value, fa.value))
}
}
// Map applies a function to the phantom type parameter without changing the wrapped value.
//
// This is the curried version of MonadMap, providing a more functional programming style.
// The function is never actually called since A is a phantom type.
//
// Type Parameters:
// - E: The type of the wrapped value
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - f: The function to apply (ignored)
//
// Returns:
// - A function that transforms Const[E, A] to Const[E, B]
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// c := Make[string, int]("data")
// mapped := F.Pipe1(c, Map[string, int, string](strconv.Itoa))
// // mapped still contains "data"
func Map[E, A, B any](f func(A) B) func(fa Const[E, A]) Const[E, B] {
return F.Bind2nd(MonadMap[E, A, B], f)
}
// Ap combines Const values using a semigroup in a curried style.
//
// This is the curried version of MonadAp, providing data-last style for better composition.
// It combines the wrapped values from both Const instances using the provided semigroup.
//
// Type Parameters:
// - E: The type of the wrapped values (must have a semigroup)
// - A: The input phantom type
// - B: The output phantom type
//
// Parameters:
// - s: The semigroup for combining wrapped values
//
// Returns:
// - A curried function for combining Const values
//
// Example:
//
// import (
// F "github.com/IBM/fp-go/v2/function"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// c1 := Make[string, int]("hello")
// c2 := Make[string, func(int) int]("world")
// result := F.Pipe1(c1, Ap[string, int, int](S.Monoid)(c2))
// // result contains "helloworld"
func Ap[E, A, B any](s S.Semigroup[E]) func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {
monadap := MonadAp[E, A, B](s)
return func(fa Const[E, A]) func(fab Const[E, func(A) B]) Const[E, B] {

View File

@@ -16,25 +16,340 @@
package constant
import (
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestMap(t *testing.T) {
fa := Make[string, int]("foo")
assert.Equal(t, fa, F.Pipe1(fa, Map[string](utils.Double)))
// TestMake tests the Make constructor
func TestMake(t *testing.T) {
t.Run("creates Const with string value", func(t *testing.T) {
c := Make[string, int]("hello")
assert.Equal(t, "hello", Unwrap(c))
})
t.Run("creates Const with int value", func(t *testing.T) {
c := Make[int, string](42)
assert.Equal(t, 42, Unwrap(c))
})
t.Run("creates Const with struct value", func(t *testing.T) {
type Config struct {
Name string
Port int
}
cfg := Config{Name: "server", Port: 8080}
c := Make[Config, bool](cfg)
assert.Equal(t, cfg, Unwrap(c))
})
}
// TestUnwrap tests extracting values from Const
func TestUnwrap(t *testing.T) {
t.Run("unwraps string value", func(t *testing.T) {
c := Make[string, int]("world")
value := Unwrap(c)
assert.Equal(t, "world", value)
})
t.Run("unwraps empty string", func(t *testing.T) {
c := Make[string, int]("")
value := Unwrap(c)
assert.Equal(t, "", value)
})
t.Run("unwraps zero value", func(t *testing.T) {
c := Make[int, string](0)
value := Unwrap(c)
assert.Equal(t, 0, value)
})
}
// TestOf tests the Of function
func TestOf(t *testing.T) {
assert.Equal(t, Make[string, int](""), Of[string, int](S.Monoid)(1))
t.Run("creates Const with monoid empty value", func(t *testing.T) {
of := Of[string, int](S.Monoid)
c := of(42)
assert.Equal(t, "", Unwrap(c))
})
t.Run("ignores input value", func(t *testing.T) {
of := Of[string, int](S.Monoid)
c1 := of(1)
c2 := of(100)
assert.Equal(t, Unwrap(c1), Unwrap(c2))
})
t.Run("works with int monoid", func(t *testing.T) {
of := Of[int, string](N.MonoidSum[int]())
c := of("ignored")
assert.Equal(t, 0, Unwrap(c))
})
}
func TestAp(t *testing.T) {
fab := Make[string, int]("bar")
assert.Equal(t, Make[string, int]("foobar"), Ap[string, int, int](S.Monoid)(fab)(Make[string, func(int) int]("foo")))
// TestMap tests the Map function
func TestMap(t *testing.T) {
t.Run("preserves wrapped value", func(t *testing.T) {
fa := Make[string, int]("foo")
result := F.Pipe1(fa, Map[string](utils.Double))
assert.Equal(t, "foo", Unwrap(result))
})
t.Run("changes phantom type", func(t *testing.T) {
fa := Make[string, int]("data")
fb := Map[string, int, string](strconv.Itoa)(fa)
// Value unchanged, but type changed from Const[string, int] to Const[string, string]
assert.Equal(t, "data", Unwrap(fb))
})
t.Run("function is never called", func(t *testing.T) {
called := false
fa := Make[string, int]("test")
fb := Map[string, int, string](func(i int) string {
called = true
return strconv.Itoa(i)
})(fa)
assert.False(t, called, "Map function should not be called")
assert.Equal(t, "test", Unwrap(fb))
})
}
// TestMonadMap tests the MonadMap function
func TestMonadMap(t *testing.T) {
t.Run("preserves wrapped value", func(t *testing.T) {
fa := Make[string, int]("original")
fb := MonadMap(fa, func(i int) string { return strconv.Itoa(i) })
assert.Equal(t, "original", Unwrap(fb))
})
t.Run("works with different types", func(t *testing.T) {
fa := Make[int, string](42)
fb := MonadMap(fa, func(s string) bool { return len(s) > 0 })
assert.Equal(t, 42, Unwrap(fb))
})
}
// TestAp tests the Ap function
func TestAp(t *testing.T) {
t.Run("combines string values", func(t *testing.T) {
fab := Make[string, int]("bar")
fa := Make[string, func(int) int]("foo")
result := Ap[string, int, int](S.Monoid)(fab)(fa)
assert.Equal(t, "foobar", Unwrap(result))
})
t.Run("combines int values with sum", func(t *testing.T) {
fab := Make[int, string](10)
fa := Make[int, func(string) string](5)
result := Ap[int, string, string](N.SemigroupSum[int]())(fab)(fa)
assert.Equal(t, 15, Unwrap(result))
})
t.Run("combines int values with product", func(t *testing.T) {
fab := Make[int, bool](3)
fa := Make[int, func(bool) bool](4)
result := Ap[int, bool, bool](N.SemigroupProduct[int]())(fab)(fa)
assert.Equal(t, 12, Unwrap(result))
})
}
// TestMonadAp tests the MonadAp function
func TestMonadAp(t *testing.T) {
t.Run("combines values using semigroup", func(t *testing.T) {
ap := MonadAp[string, int, int](S.Monoid)
fab := Make[string, func(int) int]("hello")
fa := Make[string, int]("world")
result := ap(fab, fa)
assert.Equal(t, "helloworld", Unwrap(result))
})
t.Run("works with empty strings", func(t *testing.T) {
ap := MonadAp[string, int, int](S.Monoid)
fab := Make[string, func(int) int]("")
fa := Make[string, int]("test")
result := ap(fab, fa)
assert.Equal(t, "test", Unwrap(result))
})
}
// TestMonoid tests the Monoid function
func TestMonoid(t *testing.T) {
t.Run("always returns constant value", func(t *testing.T) {
m := Monoid(42)
assert.Equal(t, 42, m.Concat(1, 2))
assert.Equal(t, 42, m.Concat(100, 200))
assert.Equal(t, 42, m.Empty())
})
t.Run("works with strings", func(t *testing.T) {
m := Monoid("constant")
assert.Equal(t, "constant", m.Concat("a", "b"))
assert.Equal(t, "constant", m.Empty())
})
t.Run("works with structs", func(t *testing.T) {
type Point struct{ X, Y int }
p := Point{X: 1, Y: 2}
m := Monoid(p)
assert.Equal(t, p, m.Concat(Point{X: 3, Y: 4}, Point{X: 5, Y: 6}))
assert.Equal(t, p, m.Empty())
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := Monoid(10)
// Left identity: Concat(Empty(), x) = x (both return constant)
assert.Equal(t, 10, m.Concat(m.Empty(), 5))
// Right identity: Concat(x, Empty()) = x (both return constant)
assert.Equal(t, 10, m.Concat(5, m.Empty()))
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
left := m.Concat(m.Concat(1, 2), 3)
right := m.Concat(1, m.Concat(2, 3))
assert.Equal(t, left, right)
assert.Equal(t, 10, left)
})
}
// TestConstFunctorLaws tests functor laws for Const
func TestConstFunctorLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// map id = id
fa := Make[string, int]("test")
mapped := Map[string, int, int](F.Identity[int])(fa)
assert.Equal(t, Unwrap(fa), Unwrap(mapped))
})
t.Run("composition law", func(t *testing.T) {
// map (g . f) = map g . map f
fa := Make[string, int]("data")
f := func(i int) string { return strconv.Itoa(i) }
g := func(s string) bool { return len(s) > 0 }
// map (g . f)
composed := Map[string, int, bool](func(i int) bool { return g(f(i)) })(fa)
// map g . map f
intermediate := F.Pipe1(fa, Map[string, int, string](f))
chained := Map[string, string, bool](g)(intermediate)
assert.Equal(t, Unwrap(composed), Unwrap(chained))
})
}
// TestConstApplicativeLaws tests applicative laws for Const
func TestConstApplicativeLaws(t *testing.T) {
t.Run("identity law", func(t *testing.T) {
// For Const, ap combines the wrapped values using the semigroup
// ap (of id) v combines empty (from of) with v's value
v := Make[string, int]("value")
ofId := Of[string, func(int) int](S.Monoid)(F.Identity[int])
result := Ap[string, int, int](S.Monoid)(v)(ofId)
// Result combines "" (from Of) with "value" using string monoid
assert.Equal(t, "value", Unwrap(result))
})
t.Run("homomorphism law", func(t *testing.T) {
// ap (of f) (of x) = of (f x)
f := func(i int) string { return strconv.Itoa(i) }
x := 42
ofF := Of[string, func(int) string](S.Monoid)(f)
ofX := Of[string, int](S.Monoid)(x)
left := Ap[string, int, string](S.Monoid)(ofX)(ofF)
right := Of[string, string](S.Monoid)(f(x))
assert.Equal(t, Unwrap(left), Unwrap(right))
})
}
// TestConstEdgeCases tests edge cases
func TestConstEdgeCases(t *testing.T) {
t.Run("empty string values", func(t *testing.T) {
c := Make[string, int]("")
assert.Equal(t, "", Unwrap(c))
mapped := Map[string, int, string](strconv.Itoa)(c)
assert.Equal(t, "", Unwrap(mapped))
})
t.Run("zero values", func(t *testing.T) {
c := Make[int, string](0)
assert.Equal(t, 0, Unwrap(c))
})
t.Run("nil pointer", func(t *testing.T) {
var ptr *int
c := Make[*int, string](ptr)
assert.Nil(t, Unwrap(c))
})
t.Run("multiple map operations", func(t *testing.T) {
c := Make[string, int]("original")
// Chain multiple map operations
step1 := Map[string, int, string](strconv.Itoa)(c)
step2 := Map[string, string, bool](func(s string) bool { return len(s) > 0 })(step1)
result := Map[string, bool, int](func(b bool) int {
if b {
return 1
}
return 0
})(step2)
assert.Equal(t, "original", Unwrap(result))
})
}
// BenchmarkMake benchmarks the Make constructor
func BenchmarkMake(b *testing.B) {
b.ResetTimer()
for b.Loop() {
_ = Make[string, int]("test")
}
}
// BenchmarkUnwrap benchmarks the Unwrap function
func BenchmarkUnwrap(b *testing.B) {
c := Make[string, int]("test")
b.ResetTimer()
for b.Loop() {
_ = Unwrap(c)
}
}
// BenchmarkMap benchmarks the Map function
func BenchmarkMap(b *testing.B) {
c := Make[string, int]("test")
mapFn := Map[string, int, string](strconv.Itoa)
b.ResetTimer()
for b.Loop() {
_ = mapFn(c)
}
}
// BenchmarkAp benchmarks the Ap function
func BenchmarkAp(b *testing.B) {
fab := Make[string, int]("hello")
fa := Make[string, func(int) int]("world")
apFn := Ap[string, int, int](S.Monoid)
b.ResetTimer()
for b.Loop() {
_ = apFn(fab)(fa)
}
}
// BenchmarkMonoid benchmarks the Monoid function
func BenchmarkMonoid(b *testing.B) {
m := Monoid(42)
b.ResetTimer()
for b.Loop() {
_ = m.Concat(1, 2)
}
}

View File

@@ -1,3 +1,18 @@
// 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 constant
import (
@@ -5,7 +20,47 @@ import (
M "github.com/IBM/fp-go/v2/monoid"
)
// Monoid returns a [M.Monoid] that returns a constant value in all operations
// Monoid creates a monoid that always returns a constant value.
//
// This creates a trivial monoid where both the Concat operation and Empty
// always return the same constant value, regardless of inputs. This is useful
// for testing, placeholder implementations, or when you need a monoid instance
// but the actual combining behavior doesn't matter.
//
// # Monoid Laws
//
// The constant monoid satisfies all monoid laws trivially:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always returns 'a'
// - Left Identity: Concat(Empty(), x) = x - both return 'a'
// - Right Identity: Concat(x, Empty()) = x - both return 'a'
//
// Type Parameters:
// - A: The type of the constant value
//
// Parameters:
// - a: The constant value to return in all operations
//
// Returns:
// - A Monoid[A] that always returns the constant value
//
// Example:
//
// // Create a monoid that always returns 42
// m := Monoid(42)
// result := m.Concat(1, 2) // 42
// empty := m.Empty() // 42
//
// // Useful for testing or placeholder implementations
// type Config struct {
// Timeout int
// }
// defaultConfig := Monoid(Config{Timeout: 30})
// config := defaultConfig.Concat(Config{Timeout: 10}, Config{Timeout: 20})
// // config is Config{Timeout: 30}
//
// See also:
// - function.Constant2: The underlying constant function
// - M.MakeMonoid: The monoid constructor
func Monoid[A any](a A) M.Monoid[A] {
return M.MakeMonoid(function.Constant2[A, A](a), a)
}

View File

@@ -21,6 +21,7 @@ import (
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/pair"
)
// WithContext wraps an existing [ReaderIOResult] and performs a context check for cancellation before delegating.
@@ -85,3 +86,7 @@ func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
WithContext,
)
}
func pairFromContextCancel(newCtx context.Context, cancelFct context.CancelFunc) ContextCancel {
return pair.MakePair(cancelFct, newCtx)
}

View File

@@ -13,6 +13,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package file provides context-aware file operations that integrate with the ReaderIOResult monad.
// It offers safe, composable file I/O operations that respect context cancellation and properly
// manage resources using the RAII pattern.
//
// All operations in this package:
// - Respect context.Context for cancellation and timeouts
// - Return ReaderIOResult for composable error handling
// - Automatically manage resource cleanup
// - Are safe to use in concurrent environments
//
// # Example Usage
//
// // Read a file with automatic resource management
// readOp := ReadFile("data.txt")
// result := readOp(ctx)()
//
// // Open and manually manage a file
// fileOp := Open("config.json")
// fileResult := fileOp(ctx)()
package file
import (
@@ -29,32 +48,181 @@ import (
)
var (
// Open opens a file for reading within the given context
// Open opens a file for reading within the given context.
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or ReadFile.
//
// Parameters:
// - path: The path to the file to open
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that opens the file
//
// Example:
//
// openFile := Open("data.txt")
// result := openFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// // Use file...
// },
// )
//
// See Also:
// - ReadFile: For reading entire file contents with automatic resource management
// - Close: For closing file handles
Open = F.Flow3(
IOEF.Open,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name
// Create creates or truncates a file for writing within the given context.
// If the file already exists, it is truncated. If it doesn't exist, it is created
// with mode 0666 (before umask).
//
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or WriteFile.
//
// Parameters:
// - path: The path to the file to create or truncate
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that creates the file
//
// Example:
//
// createFile := Create("output.txt")
// result := createFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// f.WriteString("Hello, World!")
// },
// )
//
// See Also:
// - WriteFile: For writing data to a file with automatic resource management
// - Open: For opening files for reading
// - Close: For closing file handles
Create = F.Flow3(
IOEF.Create,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name.
// The operation returns the filename on success, allowing for easy composition
// with other file operations.
//
// Parameters:
// - name: The path to the file to remove
//
// Returns:
// - ReaderIOResult[string]: A computation that removes the file and returns its name
//
// Example:
//
// removeOp := Remove("temp.txt")
// result := removeOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Failed to remove: %v", err) },
// func(name string) { log.Printf("Removed: %s", name) },
// )
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading file contents
Remove = F.Flow2(
IOEF.Remove,
RIOE.FromIOEither[string],
)
)
// Close closes an object
func Close[C io.Closer](c C) RIOE.ReaderIOResult[struct{}] {
// Close closes an io.Closer resource and returns a ReaderIOResult.
// This function is generic and works with any type that implements io.Closer,
// including os.File, network connections, and other closeable resources.
//
// The function captures any error that occurs during closing and returns it
// as part of the ReaderIOResult. On success, it returns Void (empty struct).
//
// Type Parameters:
// - C: Any type that implements io.Closer
//
// Parameters:
// - c: The resource to close
//
// Returns:
// - ReaderIOResult[Void]: A computation that closes the resource
//
// Example:
//
// file, _ := os.Open("data.txt")
// closeOp := Close(file)
// result := closeOp(ctx)()
//
// Note: This function is typically used with WithResource for automatic resource management
// rather than being called directly.
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading files with automatic closing
func Close[C io.Closer](c C) ReaderIOResult[Void] {
return F.Pipe2(
c,
IOEF.Close[C],
RIOE.FromIOEither[struct{}],
RIOE.FromIOEither[Void],
)
}
// ReadFile reads a file in the scope of a context
func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOResult[[]byte] {
// ReadFile reads the entire contents of a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// The operation:
// - Opens the file for reading
// - Reads all contents into a byte slice
// - Automatically closes the file when done
// - Respects context cancellation during the read operation
//
// Parameters:
// - path: The path to the file to read
//
// Returns:
// - ReaderIOResult[[]byte]: A computation that reads the file contents
//
// Example:
//
// readOp := ReadFile("config.json")
// result := readOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Read error: %v", err) },
// func(data []byte) { log.Printf("Read %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// ReadFile(path) = WithResource(Open(path), Close)(readAllBytes)
//
// See Also:
// - Open: For opening files without automatic reading
// - Close: For closing file handles
// - WithResource: For custom resource management patterns
func ReadFile(path string) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) ReaderIOResult[[]byte] {
return func(ctx context.Context) IOE.IOEither[error, []byte] {
return func() ET.Either[error, []byte] {
return file.ReadAll(ctx, r)
@@ -62,3 +230,48 @@ func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
}
})
}
// WriteFile writes data to a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// If the file doesn't exist, it is created with mode 0666 (before umask).
// If the file already exists, it is truncated before writing.
//
// The operation:
// - Creates or truncates the file for writing
// - Writes all data to the file
// - Automatically closes the file when done
// - Respects context cancellation during the write operation
//
// Parameters:
// - data: The byte slice to write to the file
//
// Returns:
// - Kleisli[string, []byte]: A function that takes a file path and returns a computation
// that writes the data and returns the written bytes on success
//
// Example:
//
// writeOp := WriteFile([]byte("Hello, World!"))
// result := writeOp("output.txt")(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Write error: %v", err) },
// func(data []byte) { log.Printf("Wrote %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// WriteFile(data) = Create >> WriteAll(data) >> Close
//
// See Also:
// - ReadFile: For reading file contents with automatic resource management
// - Create: For creating files without automatic writing
// - WriteAll: For writing to an already-open file handle
func WriteFile(data []byte) Kleisli[string, []byte] {
return F.Flow2(
Create,
WriteAll[*os.File](data),
)
}

View File

@@ -18,11 +18,16 @@ package file
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
R "github.com/IBM/fp-go/v2/context/readerioresult"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
J "github.com/IBM/fp-go/v2/json"
"github.com/stretchr/testify/assert"
)
type RecordType struct {
@@ -49,3 +54,267 @@ func ExampleReadFile() {
// Output:
// Right[string](Carsten)
}
func TestCreate(t *testing.T) {
ctx := context.Background()
t.Run("Success - creates new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_create.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was created
_, err := os.Stat(tempFile)
assert.NoError(t, err)
// Clean up file handle
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
})
t.Run("Success - truncates existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_truncate.txt")
// Create file with initial content
err := os.WriteFile(tempFile, []byte("initial content"), 0644)
assert.NoError(t, err)
// Create should truncate
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Close the file
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
// Verify file was truncated
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
// Try to create file in non-existent directory
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
createOp := Create(invalidPath)
result := createOp(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - file can be written to", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Write to the file
E.MonadFold(result,
func(err error) *os.File { t.Fatalf("Unexpected error: %v", err); return nil },
func(f *os.File) *os.File {
defer f.Close()
_, err := f.WriteString("test content")
assert.NoError(t, err)
return f
},
)
// Verify content was written
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, "test content", string(content))
})
t.Run("Context cancellation", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
createOp := Create(tempFile)
result := createOp(cancelCtx)()
// Note: File creation itself doesn't check context, but this tests the pattern
// In practice, context cancellation would affect subsequent operations
_ = result
})
}
func TestWriteFile(t *testing.T) {
ctx := context.Background()
t.Run("Success - writes data to new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
testData := []byte("Hello, World!")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify returned data
E.MonadFold(result,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, testData, content)
})
t.Run("Success - overwrites existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_overwrite.txt")
// Write initial content
err := os.WriteFile(tempFile, []byte("old content"), 0644)
assert.NoError(t, err)
// Overwrite with new content
newData := []byte("new content")
writeOp := WriteFile(newData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was overwritten
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, newData, content)
})
t.Run("Success - writes empty data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_empty.txt")
emptyData := []byte{}
writeOp := WriteFile(emptyData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file is empty
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Success - writes large data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_large.txt")
largeData := make([]byte, 1024*1024) // 1MB
for i := range largeData {
largeData[i] = byte(i % 256)
}
writeOp := WriteFile(largeData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, largeData, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(invalidPath)(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - writes binary data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_binary.bin")
binaryData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
writeOp := WriteFile(binaryData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify binary content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, binaryData, content)
})
t.Run("Integration - write then read", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_roundtrip.txt")
testData := []byte("Round trip test data")
// Write data
writeOp := WriteFile(testData)
writeResult := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(writeResult))
// Read data back
readOp := ReadFile(tempFile)
readResult := readOp(ctx)()
assert.True(t, E.IsRight(readResult))
// Verify data matches
E.MonadFold(readResult,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
})
t.Run("Composition with Map", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_compose.txt")
testData := []byte("test data")
// Write and transform result
pipeline := F.Pipe1(
WriteFile(testData)(tempFile),
R.Map(func(data []byte) int { return len(data) }),
)
result := pipeline(ctx)()
assert.True(t, E.IsRight(result))
E.MonadFold(result,
func(err error) int { t.Fatalf("Unexpected error: %v", err); return 0 },
func(length int) int {
assert.Equal(t, len(testData), length)
return length
},
)
})
t.Run("Context cancellation during write", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(cancelCtx)()
// Note: The actual write may complete before cancellation is checked
// This test verifies the pattern works with cancelled contexts
_ = result
})
}

View File

@@ -38,7 +38,7 @@ var (
)
// CreateTemp created a temp file with proper parametrization
func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
func CreateTemp(dir, pattern string) ReaderIOResult[*os.File] {
return F.Pipe2(
IOEF.CreateTemp(dir, pattern),
RIOE.FromIOEither[*os.File],
@@ -47,6 +47,6 @@ func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
}
// WithTempFile creates a temporary file, then invokes a callback to create a resource based on the file, then close and remove the temp file
func WithTempFile[A any](f func(*os.File) RIOE.ReaderIOResult[A]) RIOE.ReaderIOResult[A] {
func WithTempFile[A any](f Kleisli[*os.File, A]) ReaderIOResult[A] {
return RIOE.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
}

View File

@@ -34,7 +34,7 @@ func TestWithTempFile(t *testing.T) {
func TestWithTempFileOnClosedFile(t *testing.T) {
res := WithTempFile(func(f *os.File) RIOE.ReaderIOResult[[]byte] {
res := WithTempFile(func(f *os.File) ReaderIOResult[[]byte] {
return F.Pipe2(
f,
onWriteAll[*os.File]([]byte("Carsten")),

View File

@@ -0,0 +1,90 @@
// 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 file
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
)
type (
// ReaderIOResult represents a context-aware computation that performs side effects
// and can fail with an error. This is the main type used throughout the file package
// for all file operations.
//
// ReaderIOResult[A] is equivalent to:
// func(context.Context) func() Either[error, A]
//
// The computation:
// - Takes a context.Context for cancellation and timeouts
// - Performs side effects (IO operations)
// - Returns Either an error or a value of type A
//
// See Also:
// - readerioresult.ReaderIOResult: The underlying type definition
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// Void represents the absence of a meaningful value, similar to unit type in other languages.
// It is used when a function performs side effects but doesn't return a meaningful result.
//
// Void is typically used as the success type in operations like Close that perform
// an action but don't produce a useful value.
//
// Example:
// Close[*os.File](file) // Returns ReaderIOResult[Void]
//
// See Also:
// - function.Void: The underlying type definition
Void = function.Void
// Kleisli represents a Kleisli arrow for ReaderIOResult.
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
//
// Kleisli arrows are used for monadic composition, allowing you to chain operations
// that produce ReaderIOResults. They are particularly useful with Chain and Bind operations.
//
// Kleisli[A, B] is equivalent to:
// func(A) ReaderIOResult[B]
//
// Example:
// // A Kleisli arrow that reads a file given its path
// var readFileK Kleisli[string, []byte] = ReadFile
//
// See Also:
// - readerioresult.Kleisli: The underlying type definition
// - Operator: For transforming ReaderIOResults
Kleisli[A, B any] = readerioresult.Kleisli[A, B]
// Operator represents a transformation from one ReaderIOResult to another.
// This is useful for point-free style composition and building reusable transformations.
//
// Operator[A, B] is equivalent to:
// func(ReaderIOResult[A]) ReaderIOResult[B]
//
// Operators are used to transform computations without executing them, enabling
// powerful composition patterns.
//
// Example:
// // An operator that maps over file contents
// var toUpper Operator[[]byte, string] = Map(func(data []byte) string {
// return strings.ToUpper(string(data))
// })
//
// See Also:
// - readerioresult.Operator: The underlying type definition
// - Kleisli: For functions that produce ReaderIOResults
Operator[A, B any] = readerioresult.Operator[A, B]
)

View File

@@ -23,8 +23,8 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte] {
return func(w W) RIOE.ReaderIOResult[[]byte] {
func onWriteAll[W io.Writer](data []byte) Kleisli[W, []byte] {
return func(w W) ReaderIOResult[[]byte] {
return F.Pipe1(
RIOE.TryCatch(func(_ context.Context) func() ([]byte, error) {
return func() ([]byte, error) {
@@ -38,9 +38,9 @@ func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte]
}
// WriteAll uses a generator function to create a stream, writes data to it and closes it
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
func WriteAll[W io.WriteCloser](data []byte) Operator[W, []byte] {
onWrite := onWriteAll[W](data)
return func(onCreate RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
return func(onCreate ReaderIOResult[W]) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](
onCreate,
Close[W])(
@@ -50,7 +50,7 @@ func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]
}
// Write uses a generator function to create a stream, writes data to it and closes it
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOResult[W]) func(use func(W) RIOE.ReaderIOResult[R]) RIOE.ReaderIOResult[R] {
func Write[R any, W io.WriteCloser](acquire ReaderIOResult[W]) Kleisli[Kleisli[W, R], R] {
return RIOE.WithResource[R](
acquire,
Close[W])

View File

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

View File

@@ -19,6 +19,10 @@ import (
"context"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/result"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIOResult.
@@ -45,7 +49,7 @@ import (
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context], g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
@@ -70,6 +74,139 @@ func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFu
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
func Contramap[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
return Local[A](f)
}
func ContramapIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return LocalIOK[A](f)
}
// LocalIOK transforms the context using an IO-based function before passing it to a ReaderIOResult.
// This is similar to Local but the context transformation itself is wrapped in an IO effect.
//
// The function f takes a context and returns an IO effect that produces a ContextCancel
// (a pair of CancelFunc and the new Context). This allows the context transformation to
// perform side effects.
//
// # Use Cases
//
// This function is useful for sharing information via the Context that is computed through
// side effects that cannot fail, such as:
// - Generating unique request IDs or trace IDs
// - Recording timestamps or metrics
// - Logging context information
// - Computing derived values from existing context data
//
// The side effect is executed during the context transformation, and the resulting data is
// stored in the context for downstream computations to access.
//
// # Type Parameters
//
// - A: The success type (unchanged through the transformation)
//
// # Parameters
//
// - f: An IO-based Kleisli function that transforms the context
//
// # Returns
//
// - An Operator that applies the context transformation before executing the ReaderIOResult
//
// # Example Usage
//
// // Generate a request ID via side effect and add to context
// addRequestID := func(ctx context.Context) io.IO[ContextCancel] {
// return func() ContextCancel {
// // Side effect: generate unique ID
// requestID := uuid.New().String()
// // Share the ID via context
// newCtx := context.WithValue(ctx, "requestID", requestID)
// return pair.MakePair(func() {}, newCtx)
// }
// }
// adapted := LocalIOK[int](addRequestID)(computation)
//
// # See Also
//
// - Local: For pure context transformations
// - LocalIOResultK: For context transformations that can fail
//
//go:inline
func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return LocalIOResultK[A](function.Flow2(f, ioresult.FromIO))
}
// LocalIOResultK transforms the context using an IOResult-based function before passing it to a ReaderIOResult.
// This is similar to Local but the context transformation can fail with an error.
//
// The function f takes a context and returns an IOResult that produces either an error or a ContextCancel
// (a pair of CancelFunc and the new Context). If the transformation fails, the error is propagated
// and the original ReaderIOResult is not executed.
//
// # Use Cases
//
// This function is particularly useful for sharing information via the Context that is computed
// through side effects, such as:
// - Loading configuration from a file or database
// - Fetching authentication tokens from an external service
// - Computing derived values that require I/O operations
// - Validating and enriching context with data from external sources
//
// The side effect is executed during the context transformation, and the resulting data is
// stored in the context for downstream computations to access.
//
// # Type Parameters
//
// - A: The success type (unchanged through the transformation)
//
// # Parameters
//
// - f: An IOResult-based Kleisli function that transforms the context and may fail
//
// # Returns
//
// - An Operator that applies the context transformation before executing the ReaderIOResult
//
// # Example Usage
//
// // Load configuration via side effect and add to context
// loadConfig := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
// return func() result.Result[ContextCancel] {
// // Side effect: read from file system
// config, err := os.ReadFile("config.json")
// if err != nil {
// return result.Left[ContextCancel](err)
// }
// // Share the loaded config via context
// newCtx := context.WithValue(ctx, "config", config)
// return result.Of(pair.MakePair(func() {}, newCtx))
// }
// }
// adapted := LocalIOResultK[int](loadConfig)(computation)
//
// # See Also
//
// - Local: For pure context transformations
// - LocalIOK: For context transformations with side effects that cannot fail
//
//go:inline
func LocalIOResultK[A any](f ioresult.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
p, err := result.Unwrap(f(ctx)())
if err != nil {
return result.Left[A](err)
}
// unwrap
otherCancel, otherCtx := pair.Unpack(p)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}

View File

@@ -20,6 +20,10 @@ import (
"strconv"
"testing"
"github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -36,9 +40,9 @@ func TestPromapBasic(t *testing.T) {
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
toString := strconv.Itoa
@@ -61,9 +65,9 @@ func TestContramapBasic(t *testing.T) {
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
adapted := Contramap[int](addKey)(getValue)
@@ -85,9 +89,9 @@ func TestLocalBasic(t *testing.T) {
}
}
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
addUser := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "user", "Alice")
return newCtx, func() {}
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
adapted := Local[string](addUser)(getValue)
@@ -96,3 +100,311 @@ func TestLocalBasic(t *testing.T) {
assert.Equal(t, R.Of("Alice"), result)
})
}
// TestLocalIOK_Success tests LocalIOK with successful context transformation
func TestLocalIOK_Success(t *testing.T) {
t.Run("transforms context with IO effect", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("user"); v != nil {
return R.Of(v.(string))
}
return R.Of("unknown")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Bob")
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := LocalIOK[string](addUser)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of("Bob"), result)
})
t.Run("preserves original value type", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("count"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addCount := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "count", 42)
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := LocalIOK[int](addCount)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(42), result)
})
}
// TestLocalIOK_CancelledContext tests LocalIOK with cancelled context
func TestLocalIOK_CancelledContext(t *testing.T) {
t.Run("returns error when context is cancelled", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Charlie")
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
adapted := LocalIOK[string](addUser)(getValue)
result := adapted(ctx)()
assert.True(t, R.IsLeft(result))
})
}
// TestLocalIOK_CancelFuncCalled tests that CancelFunc is properly called
func TestLocalIOK_CancelFuncCalled(t *testing.T) {
t.Run("calls cancel function after execution", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Dave")
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
return pair.MakePair(cancelFunc, newCtx)
}
}
adapted := LocalIOK[string](addUser)(getValue)
_ = adapted(t.Context())()
assert.True(t, cancelCalled, "cancel function should be called")
})
}
// TestLocalIOResultK_Success tests LocalIOResultK with successful context transformation
func TestLocalIOResultK_Success(t *testing.T) {
t.Run("transforms context with IOResult effect", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("role"); v != nil {
return R.Of(v.(string))
}
return R.Of("guest")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "admin")
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
adapted := LocalIOResultK[string](addRole)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of("admin"), result)
})
t.Run("preserves original value type", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("score"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addScore := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "score", 100)
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
adapted := LocalIOResultK[int](addScore)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(100), result)
})
}
// TestLocalIOResultK_Failure tests LocalIOResultK with failed context transformation
func TestLocalIOResultK_Failure(t *testing.T) {
t.Run("propagates transformation error", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
result := adapted(t.Context())()
assert.True(t, R.IsLeft(result))
_, err := R.UnwrapError(result)
assert.Equal(t, assert.AnError, err)
})
t.Run("does not execute original computation on transformation failure", func(t *testing.T) {
executed := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
executed = true
return R.Of("should not execute")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
_ = adapted(t.Context())()
assert.False(t, executed, "original computation should not execute")
})
}
// TestLocalIOResultK_CancelledContext tests LocalIOResultK with cancelled context
func TestLocalIOResultK_CancelledContext(t *testing.T) {
t.Run("returns error when context is cancelled", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "user")
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
adapted := LocalIOResultK[string](addRole)(getValue)
result := adapted(ctx)()
assert.True(t, R.IsLeft(result))
})
}
// TestLocalIOResultK_CancelFuncCalled tests that CancelFunc is properly called
func TestLocalIOResultK_CancelFuncCalled(t *testing.T) {
t.Run("calls cancel function after successful execution", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "user")
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
return R.Of(pair.MakePair(cancelFunc, newCtx))
}
}
adapted := LocalIOResultK[string](addRole)(getValue)
_ = adapted(t.Context())()
assert.True(t, cancelCalled, "cancel function should be called")
})
t.Run("does not call cancel function on transformation failure", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
_ = cancelFunc // avoid unused warning
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
_ = adapted(t.Context())()
assert.False(t, cancelCalled, "cancel function should not be called on failure")
})
}
// TestLocalIOResultK_Integration tests integration with other operations
func TestLocalIOResultK_Integration(t *testing.T) {
t.Run("composes with Map", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("value"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addValue := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "value", 10)
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
double := func(x int) int { return x * 2 }
adapted := F.Flow2(
LocalIOResultK[int](addValue),
Map(double),
)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(20), result)
})
}

View File

@@ -28,6 +28,7 @@ import (
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
@@ -452,7 +453,7 @@ func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
// Returns a function that chains Option-returning functions into ReaderIOResult.
//
//go:inline
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainOptionK[context.Context, A, B](onNone)
}
@@ -800,7 +801,7 @@ func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
}
//go:inline
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
func FromReaderOption[A any](onNone Lazy[error]) Kleisli[ReaderOption[context.Context, A], A] {
return RIOR.FromReaderOption[context.Context, A](onNone)
}
@@ -895,17 +896,17 @@ func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
}
//go:inline
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
func ChainReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
func ChainFirstReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
func TapReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
}
@@ -1054,14 +1055,14 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
func Local[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
otherCtx, otherCancel := f(ctx)
otherCancel, otherCtx := pair.Unpack(f(ctx))
defer otherCancel()
return rr(otherCtx)()
}
@@ -1123,9 +1124,10 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// )
// value, err := result(t.Context())() // Returns (Data{Value: "quick"}, nil)
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
return Local[A](
func(ctx context.Context) ContextCancel {
return pairFromContextCancel(context.WithTimeout(ctx, timeout))
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIOResult computation.
@@ -1188,7 +1190,7 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// )
// value, err := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
return Local[A](func(ctx context.Context) ContextCancel {
return pairFromContextCancel(context.WithDeadline(ctx, deadline))
})
}

View File

@@ -55,6 +55,10 @@ type (
// Either[A] is equivalent to Either[error, A] from the either package.
Either[A any] = either.Either[error, A]
// Result represents a computation that can either succeed with a value of type A
// or fail with an error. This is an alias for result.Result[A].
//
// Result[A] is equivalent to Either[error, A]
Result[A any] = result.Result[A]
// Lazy represents a deferred computation that produces a value of type A when executed.
@@ -73,6 +77,10 @@ type (
// IOEither[A] is equivalent to func() Either[error, A]
IOEither[A any] = ioeither.IOEither[error, A]
// IOResult represents a side-effectful computation that can fail with an error.
// This combines IO (side effects) with Result (error handling).
//
// IOResult[A] is equivalent to func() Result[A]
IOResult[A any] = ioresult.IOResult[A]
// Reader represents a computation that depends on a context of type R.
@@ -118,6 +126,13 @@ type (
// result := fetchUser("123")(ctx)()
ReaderIOResult[A any] = RIOR.ReaderIOResult[context.Context, A]
// Kleisli represents a Kleisli arrow for ReaderIOResult.
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
//
// Kleisli arrows are used for monadic composition, allowing you to chain operations
// that produce ReaderIOResults. They are particularly useful with Chain operations.
//
// Kleisli[A, B] is equivalent to func(A) ReaderIOResult[B]
Kleisli[A, B any] = reader.Reader[A, ReaderIOResult[B]]
// Operator represents a transformation from one ReaderIOResult to another.
@@ -133,26 +148,76 @@ type (
// result := toUpper(computation)
Operator[A, B any] = Kleisli[ReaderIOResult[A], B]
ReaderResult[A any] = readerresult.ReaderResult[A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// ReaderResult represents a context-dependent computation that can fail.
// This is specialized to use context.Context as the context type.
//
// ReaderResult[A] is equivalent to func(context.Context) Result[A]
ReaderResult[A any] = readerresult.ReaderResult[A]
// ReaderEither represents a context-dependent computation that can fail.
// It takes a context of type R and produces an Either[E, A].
//
// ReaderEither[R, E, A] is equivalent to func(R) Either[E, A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
// ReaderOption represents a context-dependent computation that may not produce a value.
// It takes a context of type R and produces an Option[A].
//
// ReaderOption[R, A] is equivalent to func(R) Option[A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// Endomorphism represents a function from a type to itself.
// It is used for transformations that preserve the type.
//
// Endomorphism[A] is equivalent to func(A) A
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Consumer represents a function that consumes a value without producing a result.
// It is used for side effects like logging or updating state.
//
// Consumer[A] is equivalent to func(A)
Consumer[A any] = consumer.Consumer[A]
// Prism represents an optic for working with sum types (tagged unions).
// It provides a way to focus on a specific variant of a sum type.
Prism[S, T any] = prism.Prism[S, T]
Lens[S, T any] = lens.Lens[S, T]
// Lens represents an optic for working with product types (records/structs).
// It provides a way to focus on a specific field of a product type.
Lens[S, T any] = lens.Lens[S, T]
// Trampoline represents a computation that can be executed in a stack-safe manner.
// It is used for tail-recursive computations that would otherwise overflow the stack.
Trampoline[B, L any] = tailrec.Trampoline[B, L]
// Predicate represents a function that tests a value of type A.
// It returns true if the value satisfies the predicate, false otherwise.
//
// Predicate[A] is equivalent to func(A) bool
Predicate[A any] = predicate.Predicate[A]
// Pair represents a tuple of two values of types A and B.
// It is used to group two related values together.
Pair[A, B any] = pair.Pair[A, B]
// IORef represents a mutable reference that can be safely accessed in IO computations.
// It provides thread-safe read and write operations.
IORef[A any] = ioref.IORef[A]
// State represents a stateful computation that transforms a state of type S
// and produces a value of type A.
//
// State[S, A] is equivalent to func(S) Pair[A, S]
State[S, A any] = state.State[S, A]
// Void represents the absence of a value, similar to unit type in other languages.
// It is used when a function performs side effects but doesn't return a meaningful value.
Void = function.Void
// ContextCancel represents a pair of a cancel function and a context.
// It is used in operations that create new contexts with cancellation capabilities.
//
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
)

View 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,
)
}

View 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)
})
}

View File

@@ -25,6 +25,31 @@ import (
"github.com/IBM/fp-go/v2/readerio"
)
// Do creates an Effect with an initial state value.
// This is the starting point for do-notation style effect composition,
// allowing you to build up complex state transformations step by step.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S: The state type
//
// # Parameters
//
// - empty: The initial state value
//
// # Returns
//
// - Effect[C, S]: An effect that produces the initial state
//
// # Example
//
// type State struct {
// Name string
// Age int
// }
// eff := effect.Do[MyContext](State{})
//
//go:inline
func Do[C, S any](
empty S,
@@ -32,6 +57,40 @@ func Do[C, S any](
return readerreaderioresult.Of[C](empty)
}
// Bind executes an effectful computation and binds its result to the state.
// This is the core operation for do-notation, allowing you to sequence effects
// while accumulating results in a state structure.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the effect
//
// # Parameters
//
// - setter: A function that takes the effect result and returns a state updater
// - f: An effectful computation that depends on the current state
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// eff := effect.Bind(
// func(age int) func(State) State {
// return func(s State) State {
// s.Age = age
// return s
// }
// },
// func(s State) Effect[MyContext, int] {
// return effect.Of[MyContext](30)
// },
// )(effect.Do[MyContext](State{}))
//
//go:inline
func Bind[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -40,6 +99,39 @@ func Bind[C, S1, S2, T any](
return readerreaderioresult.Bind(setter, f)
}
// Let computes a pure value from the current state and binds it to the state.
// Unlike Bind, this doesn't perform any effects - it's for pure computations.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S1: The input state type
// - S2: The output state type
// - T: The type of computed value
//
// # Parameters
//
// - setter: A function that takes the computed value and returns a state updater
// - f: A pure function that computes a value from the current state
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// eff := effect.Let[MyContext](
// func(nameLen int) func(State) State {
// return func(s State) State {
// s.NameLength = nameLen
// return s
// }
// },
// func(s State) int {
// return len(s.Name)
// },
// )(stateEff)
//
//go:inline
func Let[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -48,6 +140,37 @@ func Let[C, S1, S2, T any](
return readerreaderioresult.Let[C](setter, f)
}
// LetTo binds a constant value to the state.
// This is useful for setting fixed values in your state structure.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S1: The input state type
// - S2: The output state type
// - T: The type of the constant value
//
// # Parameters
//
// - setter: A function that takes the constant and returns a state updater
// - b: The constant value to bind
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// eff := effect.LetTo[MyContext](
// func(age int) func(State) State {
// return func(s State) State {
// s.Age = age
// return s
// }
// },
// 42,
// )(stateEff)
//
//go:inline
func LetTo[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -56,6 +179,30 @@ func LetTo[C, S1, S2, T any](
return readerreaderioresult.LetTo[C](setter, b)
}
// BindTo wraps a value in an initial state structure.
// This is typically used to start a bind chain by converting a simple value
// into a state structure.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S1: The state type to create
// - T: The type of the input value
//
// # Parameters
//
// - setter: A function that creates a state from the value
//
// # Returns
//
// - Operator[C, T, S1]: A function that wraps the value in state
//
// # Example
//
// eff := effect.BindTo[MyContext](func(name string) State {
// return State{Name: name}
// })(effect.Of[MyContext]("Alice"))
//
//go:inline
func BindTo[C, S1, T any](
setter func(T) S1,
@@ -63,6 +210,39 @@ func BindTo[C, S1, T any](
return readerreaderioresult.BindTo[C](setter)
}
// ApS applies an effect and binds its result to the state using a setter function.
// This is similar to Bind but takes a pre-existing effect rather than a function
// that creates an effect from the state.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the effect
//
// # Parameters
//
// - setter: A function that takes the effect result and returns a state updater
// - fa: The effect to apply
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// ageEffect := effect.Of[MyContext](30)
// eff := effect.ApS(
// func(age int) func(State) State {
// return func(s State) State {
// s.Age = age
// return s
// }
// },
// ageEffect,
// )(stateEff)
//
//go:inline
func ApS[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -71,6 +251,33 @@ func ApS[C, S1, S2, T any](
return readerreaderioresult.ApS(setter, fa)
}
// ApSL applies an effect and updates a field in the state using a lens.
// This provides a more ergonomic way to update nested state structures.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - fa: The effect producing the new field value
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// ageEffect := effect.Of[MyContext](30)
// eff := effect.ApSL(ageLens, ageEffect)(stateEff)
//
//go:inline
func ApSL[C, S, T any](
lens Lens[S, T],
@@ -79,6 +286,37 @@ func ApSL[C, S, T any](
return readerreaderioresult.ApSL(lens, fa)
}
// BindL executes an effectful computation on a field and updates it using a lens.
// The effect function receives the current field value and produces a new value.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - f: An effectful function that transforms the field value
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// eff := effect.BindL(
// ageLens,
// func(age int) Effect[MyContext, int] {
// return effect.Of[MyContext](age + 1)
// },
// )(stateEff)
//
//go:inline
func BindL[C, S, T any](
lens Lens[S, T],
@@ -87,6 +325,35 @@ func BindL[C, S, T any](
return readerreaderioresult.BindL(lens, f)
}
// LetL computes a new field value from the current value using a lens.
// This is a pure transformation of a field within the state.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - f: A pure function that transforms the field value
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// eff := effect.LetL[MyContext](
// ageLens,
// func(age int) int { return age * 2 },
// )(stateEff)
//
//go:inline
func LetL[C, S, T any](
lens Lens[S, T],
@@ -95,6 +362,31 @@ func LetL[C, S, T any](
return readerreaderioresult.LetL[C](lens, f)
}
// LetToL sets a field to a constant value using a lens.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - b: The constant value to set
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// eff := effect.LetToL[MyContext](ageLens, 42)(stateEff)
//
//go:inline
func LetToL[C, S, T any](
lens Lens[S, T],

32
v2/effect/common_test.go Normal file
View File

@@ -0,0 +1,32 @@
// 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"
)
// TestContext is a common test context type used across effect tests
type TestContext struct {
Value string
}
// runEffect is a helper function to run an effect with a context and return the result
func runEffect[C, A any](eff Effect[C, A], ctx C) (A, error) {
ioResult := Provide[A](ctx)(eff)
readerResult := RunSync(ioResult)
return readerResult(context.Background())
}

View File

@@ -1,3 +1,18 @@
// 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 (
@@ -8,31 +23,182 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// Local transforms the context required by an effect using a pure function.
// This allows you to adapt an effect that requires one context type to work
// with a different context type by providing a transformation function.
//
// # Type Parameters
//
// - C1: The outer context type (what you have)
// - C2: The inner context type (what the effect needs)
// - A: The value type produced by the effect
//
// # Parameters
//
// - acc: A pure function that transforms C1 to C2
//
// # Returns
//
// - Kleisli[C1, Effect[C2, A], A]: A function that adapts the effect to use C1
//
// # Example
//
// type AppConfig struct { DB DatabaseConfig }
// type DatabaseConfig struct { Host string }
// dbEffect := effect.Of[DatabaseConfig]("connected")
// appEffect := effect.Local[AppConfig, DatabaseConfig, string](
// func(app AppConfig) DatabaseConfig { return app.DB },
// )(dbEffect)
//
//go:inline
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
func Local[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
// Contramap is an alias for Local, following the contravariant functor naming convention.
// It transforms the context required by an effect using a pure function.
//
// # Type Parameters
//
// - C1: The outer context type (what you have)
// - C2: The inner context type (what the effect needs)
// - A: The value type produced by the effect
//
// # Parameters
//
// - acc: A pure function that transforms C1 to C2
//
// # Returns
//
// - Kleisli[C1, Effect[C2, A], A]: A function that adapts the effect to use C1
//
//go:inline
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
func Contramap[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
// LocalIOK transforms the context using an IO-based function.
// This allows the context transformation itself to perform I/O operations.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: An IO function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// loadConfig := func(path string) io.IO[Config] {
// return func() Config { /* load from file */ }
// }
// transform := effect.LocalIOK[string](loadConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalIOK[A, C1, C2 any](f io.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOK[A](f)
}
// LocalIOResultK transforms the context using an IOResult-based function.
// This allows the context transformation to perform I/O and handle errors.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: An IOResult function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// loadConfig := func(path string) ioresult.IOResult[Config] {
// return func() result.Result[Config] {
// // load from file, may fail
// }
// }
// transform := effect.LocalIOResultK[string](loadConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalIOResultK[A, C1, C2 any](f ioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOResultK[A](f)
}
// LocalResultK transforms the context using a Result-based function.
// This allows the context transformation to fail with an error.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: A Result function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// validateConfig := func(raw RawConfig) result.Result[Config] {
// if raw.IsValid() {
// return result.Of(raw.ToConfig())
// }
// return result.Left[Config](errors.New("invalid"))
// }
// transform := effect.LocalResultK[string](validateConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalResultK[A, C1, C2 any](f result.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalResultK[A](f)
}
// LocalThunkK transforms the context using a Thunk (ReaderIOResult) function.
// This allows the context transformation to depend on context.Context, perform I/O, and handle errors.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: A Thunk function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// loadConfig := func(path string) readerioresult.ReaderIOResult[Config] {
// return func(ctx context.Context) ioresult.IOResult[Config] {
// // load from file with context, may fail
// }
// }
// transform := effect.LocalThunkK[string](loadConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalThunkK[A, C1, C2 any](f thunk.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderIOResultK[A](f)

View File

@@ -44,11 +44,11 @@ func TestLocal(t *testing.T) {
}
// Apply Local to transform the context
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -70,11 +70,11 @@ func TestLocal(t *testing.T) {
return InnerContext{Value: outer.Value + " transformed"}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "original",
Number: 100,
})(outerEffect)
@@ -93,10 +93,10 @@ func TestLocal(t *testing.T) {
return InnerContext{Value: outer.Value}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -122,12 +122,12 @@ func TestLocal(t *testing.T) {
level3Effect := Of[Level3]("deep result")
// Transform Level2 -> Level3
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
local23 := Local[string](func(l2 Level2) Level3 {
return Level3{C: l2.B + "-c"}
})
// Transform Level1 -> Level2
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
local12 := Local[string](func(l1 Level1) Level2 {
return Level2{B: l1.A + "-b"}
})
@@ -136,7 +136,7 @@ func TestLocal(t *testing.T) {
level1Effect := local12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
ioResult := Provide[string](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -165,11 +165,11 @@ func TestLocal(t *testing.T) {
return app.DB
}
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
kleisli := Local[string](accessor)
appEffect := kleisli(dbEffect)
// Run with full AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string](AppConfig{
DB: DatabaseConfig{
Host: "localhost",
Port: 5432,
@@ -195,21 +195,21 @@ func TestContramap(t *testing.T) {
}
// Test Local
localKleisli := Local[OuterContext, InnerContext, int](accessor)
localKleisli := Local[int](accessor)
localEffect := localKleisli(innerEffect)
// Test Contramap
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
contramapKleisli := Contramap[int](accessor)
contramapEffect := contramapKleisli(innerEffect)
outerCtx := OuterContext{Value: "test", Number: 100}
// Run both
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
localIO := Provide[int](outerCtx)(localEffect)
localReader := RunSync(localIO)
localResult, localErr := localReader(context.Background())
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
contramapIO := Provide[int](outerCtx)(contramapEffect)
contramapReader := RunSync(contramapIO)
contramapResult, contramapErr := contramapReader(context.Background())
@@ -225,10 +225,10 @@ func TestContramap(t *testing.T) {
return InnerContext{Value: outer.Value + " modified"}
}
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
kleisli := Contramap[string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string](OuterContext{
Value: "original",
Number: 50,
})(outerEffect)
@@ -247,10 +247,10 @@ func TestContramap(t *testing.T) {
return InnerContext{Value: outer.Value}
}
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
kleisli := Contramap[int](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, int](OuterContext{
ioResult := Provide[int](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -278,12 +278,12 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
effect3 := Of[Config3]("result")
// Use Local for first transformation
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
local23 := Local[string](func(c2 Config2) Config3 {
return Config3{Info: c2.Data}
})
// Use Contramap for second transformation
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
contramap12 := Contramap[string](func(c1 Config1) Config2 {
return Config2{Data: c1.Value}
})
@@ -292,7 +292,7 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
effect1 := contramap12(effect2)
// Run
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
ioResult := Provide[string](Config1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -326,7 +326,7 @@ func TestLocalEffectK(t *testing.T) {
appEffect := transform(dbEffect)
// Run with AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string](AppConfig{
ConfigPath: "/etc/app.conf",
})(appEffect)
readerResult := RunSync(ioResult)
@@ -356,7 +356,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](failingTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
ioResult := Provide[string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -384,7 +384,7 @@ func TestLocalEffectK(t *testing.T) {
transformK := LocalEffectK[string](transform)
outerEffect := transformK(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
ioResult := Provide[string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -417,7 +417,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](loadConfigEffect)
appEffect := transform(configEffect)
ioResult := Provide[AppContext, string](AppContext{
ioResult := Provide[string](AppContext{
ConfigFile: "config.json",
})(appEffect)
readerResult := RunSync(ioResult)
@@ -456,7 +456,7 @@ func TestLocalEffectK(t *testing.T) {
level1Effect := transform12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
ioResult := Provide[string](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -497,7 +497,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](transformWithContext)
appEffect := transform(dbEffect)
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string](AppConfig{
Environment: "prod",
DBHost: "localhost",
DBPort: 5432,
@@ -534,14 +534,14 @@ func TestLocalEffectK(t *testing.T) {
outerEffect := transform(innerEffect)
// Test with invalid config
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
ioResult := Provide[string](RawConfig{APIKey: ""})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
// Test with valid config
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
ioResult2 := Provide[string](RawConfig{APIKey: "valid-key"})(outerEffect)
readerResult2 := RunSync(ioResult2)
result, err2 := readerResult2(context.Background())
@@ -569,7 +569,7 @@ func TestLocalEffectK(t *testing.T) {
})
// Use Local for second transformation (pure)
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
local12 := Local[string](func(l1 Level1) Level2 {
return Level2{Data: l1.Value}
})
@@ -578,7 +578,7 @@ func TestLocalEffectK(t *testing.T) {
effect1 := local12(effect2)
// Run
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
ioResult := Provide[string](Level1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -610,7 +610,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[int](complexTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
ioResult := Provide[int](OuterCtx{Multiplier: 3})(outerEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())

View File

@@ -1,51 +1,614 @@
// 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 (
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
)
// FromThunk lifts a Thunk (context-independent IO computation with error handling) into an Effect.
// This allows you to integrate computations that don't need the effect's context type C
// into effect chains. The Thunk will be executed with the runtime context when the effect runs.
//
// # Type Parameters
//
// - C: The context type required by the effect (not used by the thunk)
// - A: The type of the success value
//
// # Parameters
//
// - f: A Thunk[A] that performs IO with error handling
//
// # Returns
//
// - Effect[C, A]: An effect that ignores its context and executes the thunk
//
// # Example
//
// thunk := func(ctx context.Context) io.IO[result.Result[int]] {
// return func() result.Result[int] {
// // Perform IO operation
// return result.Of(42)
// }
// }
//
// eff := effect.FromThunk[MyContext](thunk)
// // eff can be used in any context but executes the thunk
//
//go:inline
func FromThunk[C, A any](f Thunk[A]) Effect[C, A] {
return reader.Of[C](f)
}
//go:inline
func FromResult[C, A any](r Result[A]) Effect[C, A] {
return readerreaderioresult.FromEither[C](r)
}
// Succeed creates a successful Effect that produces the given value.
// This is the primary way to lift a pure value into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - a: The value to wrap in a successful effect
//
// # Returns
//
// - Effect[C, A]: An effect that always succeeds with the given value
//
// # Example
//
// eff := effect.Succeed[MyContext](42)
// result, err := runEffect(eff, myContext)
// // result == 42, err == nil
func Succeed[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
// Fail creates a failed Effect with the given error.
// This is used to represent computations that have failed.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value (never produced)
//
// # Parameters
//
// - err: The error that caused the failure
//
// # Returns
//
// - Effect[C, A]: An effect that always fails with the given error
//
// # Example
//
// eff := effect.Fail[MyContext, int](errors.New("failed"))
// _, err := runEffect(eff, myContext)
// // err == errors.New("failed")
func Fail[C, A any](err error) Effect[C, A] {
return readerreaderioresult.Left[C, A](err)
}
// Of creates a successful Effect that produces the given value.
// This is an alias for Succeed and follows the pointed functor convention.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - a: The value to wrap in a successful effect
//
// # Returns
//
// - Effect[C, A]: An effect that always succeeds with the given value
//
// # Example
//
// eff := effect.Of[MyContext]("hello")
// result, err := runEffect(eff, myContext)
// // result == "hello", err == nil
func Of[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
// Map transforms the success value of an Effect using the provided function.
// If the effect fails, the error is propagated unchanged.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: The transformation function to apply to the success value
//
// # Returns
//
// - Operator[C, A, B]: A function that transforms Effect[C, A] to Effect[C, B]
//
// # Example
//
// eff := effect.Of[MyContext](42)
// mapped := effect.Map[MyContext](func(x int) string {
// return strconv.Itoa(x)
// })(eff)
// // mapped produces "42"
func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
return readerreaderioresult.Map[C](f)
}
// Chain sequences two effects, where the second effect depends on the result of the first.
// This is the monadic bind operation (flatMap) for effects.
// If the first effect fails, the second is not executed.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes the result of the first effect and returns a new effect
//
// # Returns
//
// - Operator[C, A, B]: A function that transforms Effect[C, A] to Effect[C, B]
//
// # Example
//
// eff := effect.Of[MyContext](42)
// chained := effect.Chain[MyContext](func(x int) Effect[MyContext, string] {
// return effect.Of[MyContext](strconv.Itoa(x * 2))
// })(eff)
// // chained produces "84"
//
//go:inline
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.Chain(f)
}
//go:inline
func ChainFirst[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirst(f)
}
// ChainIOK chains an effect with a function that returns an IO action.
// This is useful for integrating IO-based computations (synchronous side effects)
// into effect chains. The IO action is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns IO[B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the IO-returning function with the effect
//
// # Example
//
// performIO := func(n int) io.IO[string] {
// return func() string {
// // Perform synchronous side effect
// return fmt.Sprintf("Value: %d", n)
// }
// }
//
// eff := effect.Of[MyContext](42)
// chained := effect.ChainIOK[MyContext](performIO)(eff)
// // chained produces "Value: 42"
//
//go:inline
func ChainIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainIOK[C](f)
}
// ChainFirstIOK chains an effect with a function that returns an IO action,
// but discards the result and returns the original value.
// This is useful for performing side effects (like logging) without changing the value.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the IO action (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns IO[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
//
// # Example
//
// logValue := func(n int) io.IO[any] {
// return func() any {
// fmt.Printf("Processing: %d\n", n)
// return nil
// }
// }
//
// eff := effect.Of[MyContext](42)
// logged := effect.ChainFirstIOK[MyContext](logValue)(eff)
// // Prints "Processing: 42" but still produces 42
//
//go:inline
func ChainFirstIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirstIOK[C](f)
}
// TapIOK is an alias for ChainFirstIOK.
// It chains an effect with a function that returns an IO action for side effects,
// but preserves the original value. This is useful for logging, debugging, or
// performing actions without changing the result.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the IO action (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns IO[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
//
// # Example
//
// logValue := func(n int) io.IO[any] {
// return func() any {
// fmt.Printf("Value: %d\n", n)
// return nil
// }
// }
//
// eff := effect.Of[MyContext](42)
// tapped := effect.TapIOK[MyContext](logValue)(eff)
// // Prints "Value: 42" but still produces 42
//
//go:inline
func TapIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirstIOK[C](f)
}
// Ap applies a function wrapped in an Effect to a value wrapped in an Effect.
// This is the applicative apply operation, useful for applying effects in parallel.
//
// # Type Parameters
//
// - B: The output value type
// - C: The context type required by the effects
// - A: The input value type
//
// # Parameters
//
// - fa: The effect containing the value to apply the function to
//
// # Returns
//
// - Operator[C, func(A) B, B]: A function that applies the function effect to the value effect
//
// # Example
//
// fnEff := effect.Of[MyContext](func(x int) int { return x * 2 })
// valEff := effect.Of[MyContext](21)
// result := effect.Ap[int](valEff)(fnEff)
// // result produces 42
func Ap[B, C, A any](fa Effect[C, A]) Operator[C, func(A) B, B] {
return readerreaderioresult.Ap[B](fa)
}
// Suspend delays the evaluation of an effect until it is run.
// This is useful for recursive effects or when you need lazy evaluation.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - fa: A lazy computation that produces an effect
//
// # Returns
//
// - Effect[C, A]: An effect that evaluates the lazy computation when run
//
// # Example
//
// var recursiveEff func(int) Effect[MyContext, int]
// recursiveEff = func(n int) Effect[MyContext, int] {
// if n <= 0 {
// return effect.Of[MyContext](0)
// }
// return effect.Suspend(func() Effect[MyContext, int] {
// return effect.Map[MyContext](func(x int) int {
// return x + n
// })(recursiveEff(n - 1))
// })
// }
func Suspend[C, A any](fa Lazy[Effect[C, A]]) Effect[C, A] {
return readerreaderioresult.Defer(fa)
}
// Tap executes a side effect for its effect, but returns the original value.
// This is useful for logging, debugging, or performing actions without changing the result.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The value type
// - ANY: The type produced by the side effect (ignored)
//
// # Parameters
//
// - f: A function that performs a side effect based on the value
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the side effect but preserves the original value
//
// # Example
//
// eff := effect.Of[MyContext](42)
// tapped := effect.Tap[MyContext](func(x int) Effect[MyContext, any] {
// fmt.Println("Value:", x)
// return effect.Of[MyContext, any](nil)
// })(eff)
// // Prints "Value: 42" but still produces 42
func Tap[C, A, ANY any](f Kleisli[C, A, ANY]) Operator[C, A, A] {
return readerreaderioresult.Tap(f)
}
// Ternary creates a conditional effect based on a predicate.
// If the predicate returns true, onTrue is executed; otherwise, onFalse is executed.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - pred: A predicate function to test the input value
// - onTrue: The effect to execute if the predicate is true
// - onFalse: The effect to execute if the predicate is false
//
// # Returns
//
// - Kleisli[C, A, B]: A function that conditionally executes one of two effects
//
// # Example
//
// kleisli := effect.Ternary(
// func(x int) bool { return x > 10 },
// func(x int) Effect[MyContext, string] {
// return effect.Of[MyContext]("large")
// },
// func(x int) Effect[MyContext, string] {
// return effect.Of[MyContext]("small")
// },
// )
// result := kleisli(15) // produces "large"
func Ternary[C, A, B any](pred Predicate[A], onTrue, onFalse Kleisli[C, A, B]) Kleisli[C, A, B] {
return function.Ternary(pred, onTrue, onFalse)
}
// ChainResultK chains an effect with a function that returns a Result.
// This is useful for integrating Result-based computations into effect chains.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns Result[B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the Result-returning function with the effect
//
// # Example
//
// parseIntResult := result.Eitherize1(strconv.Atoi)
// eff := effect.Of[MyContext]("42")
// chained := effect.ChainResultK[MyContext](parseIntResult)(eff)
// // chained produces 42 as an int
//
//go:inline
func ChainResultK[C, A, B any](f result.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainResultK[C](f)
}
// ChainReaderK chains an effect with a function that returns a Reader.
// This is useful for integrating Reader-based computations (pure context-dependent functions)
// into effect chains. The Reader is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns Reader[C, B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the Reader-returning function with the effect
//
// # Example
//
// type Config struct { Multiplier int }
//
// getMultiplied := func(n int) reader.Reader[Config, int] {
// return func(cfg Config) int {
// return n * cfg.Multiplier
// }
// }
//
// eff := effect.Of[Config](5)
// chained := effect.ChainReaderK[Config](getMultiplied)(eff)
// // With Config{Multiplier: 3}, produces 15
//
//go:inline
func ChainReaderK[C, A, B any](f reader.Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainReaderK(f)
}
// ChainThunkK chains an effect with a function that returns a Thunk.
// This is useful for integrating Thunk-based computations (context-independent IO with error handling)
// into effect chains. The Thunk is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns Thunk[B] (readerioresult.Kleisli[A, B])
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the Thunk-returning function with the effect
//
// # Example
//
// performIO := func(n int) readerioresult.ReaderIOResult[string] {
// return func(ctx context.Context) io.IO[result.Result[string]] {
// return func() result.Result[string] {
// // Perform IO operation that doesn't need effect context
// return result.Of(fmt.Sprintf("Processed: %d", n))
// }
// }
// }
//
// eff := effect.Of[MyContext](42)
// chained := effect.ChainThunkK[MyContext](performIO)(eff)
// // chained produces "Processed: 42"
//
//go:inline
func ChainThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, B] {
return fromreader.ChainReaderK(
Chain[C, A, B],
FromThunk[C, B],
f,
)
}
// ChainReaderIOK chains an effect with a function that returns a ReaderIO.
// This is useful for integrating ReaderIO-based computations (context-dependent IO operations)
// into effect chains. The ReaderIO is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns ReaderIO[C, B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the ReaderIO-returning function with the effect
//
// # Example
//
// type Config struct { LogPrefix string }
//
// logAndDouble := func(n int) readerio.ReaderIO[Config, int] {
// return func(cfg Config) io.IO[int] {
// return func() int {
// fmt.Printf("%s: %d\n", cfg.LogPrefix, n)
// return n * 2
// }
// }
// }
//
// eff := effect.Of[Config](21)
// chained := effect.ChainReaderIOK[Config](logAndDouble)(eff)
// // Logs "prefix: 21" and produces 42
//
//go:inline
func ChainReaderIOK[C, A, B any](f readerio.Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainReaderIOK(f)
}
// Read provides a context to an effect, partially applying it.
// This converts an Effect[C, A] to a Thunk[A] by supplying the required context.
//
// # Type Parameters
//
// - A: The type of the success value
// - C: The context type
//
// # Parameters
//
// - c: The context to provide to the effect
//
// # Returns
//
// - func(Effect[C, A]) Thunk[A]: A function that converts an effect to a thunk
//
// # Example
//
// ctx := MyContext{Value: "test"}
// eff := effect.Of[MyContext](42)
// thunk := effect.Read[int](ctx)(eff)
// // thunk is now a Thunk[int] that can be run without context
//
//go:inline
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
return readerreaderioresult.Read[A](c)
}

View File

@@ -0,0 +1,649 @@
// 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/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestSucceed tests the Succeed function
func TestSucceed_Success(t *testing.T) {
t.Run("creates successful effect with int", func(t *testing.T) {
eff := Succeed[TestConfig](42)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("creates successful effect with string", func(t *testing.T) {
eff := Succeed[TestConfig]("hello")
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("hello"), outcome)
})
t.Run("creates successful effect with zero value", func(t *testing.T) {
eff := Succeed[TestConfig](0)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(0), outcome)
})
}
// TestFail tests the Fail function
func TestFail_Failure(t *testing.T) {
t.Run("creates failed effect with error", func(t *testing.T) {
testErr := errors.New("test error")
eff := Fail[TestConfig, int](testErr)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("preserves error message", func(t *testing.T) {
testErr := errors.New("specific error message")
eff := Fail[TestConfig, string](testErr)
outcome := eff(testConfig)(context.Background())()
assert.True(t, result.IsLeft(outcome))
extractedErr := result.MonadFold(outcome,
F.Identity[error],
func(string) error { return nil },
)
assert.Equal(t, testErr, extractedErr)
})
}
// TestOf tests the Of function
func TestOf_Success(t *testing.T) {
t.Run("creates successful effect with value", func(t *testing.T) {
eff := Of[TestConfig](100)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(100), outcome)
})
t.Run("is equivalent to Succeed", func(t *testing.T) {
value := "test"
eff1 := Of[TestConfig](value)
eff2 := Succeed[TestConfig](value)
outcome1 := eff1(testConfig)(context.Background())()
outcome2 := eff2(testConfig)(context.Background())()
assert.Equal(t, outcome1, outcome2)
})
}
// TestMap tests the Map function
func TestMap_Success(t *testing.T) {
t.Run("transforms success value", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(84), outcome)
})
t.Run("transforms type", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Map[TestConfig](func(x int) string { return strconv.Itoa(x) }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("chains multiple maps", func(t *testing.T) {
eff := F.Pipe2(
Of[TestConfig](10),
Map[TestConfig](func(x int) int { return x + 5 }),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
}
func TestMap_Failure(t *testing.T) {
t.Run("propagates error unchanged", func(t *testing.T) {
testErr := errors.New("test error")
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}
// TestChain tests the Chain function
func TestChain_Success(t *testing.T) {
t.Run("sequences two effects", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Chain(func(x int) Effect[TestConfig, string] {
return Of[TestConfig](strconv.Itoa(x))
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("chains multiple effects", func(t *testing.T) {
eff := F.Pipe2(
Of[TestConfig](10),
Chain(func(x int) Effect[TestConfig, int] {
return Of[TestConfig](x + 5)
}),
Chain(func(x int) Effect[TestConfig, int] {
return Of[TestConfig](x * 2)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
}
func TestChain_Failure(t *testing.T) {
t.Run("propagates error from first effect", func(t *testing.T) {
testErr := errors.New("first error")
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Chain(func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("should not execute")
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
})
t.Run("propagates error from second effect", func(t *testing.T) {
testErr := errors.New("second error")
eff := F.Pipe1(
Of[TestConfig](42),
Chain(func(x int) Effect[TestConfig, string] {
return Fail[TestConfig, string](testErr)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
})
}
// TestChainIOK tests the ChainIOK function
func TestChainIOK_Success(t *testing.T) {
t.Run("chains with IO action", func(t *testing.T) {
counter := 0
eff := F.Pipe1(
Of[TestConfig](42),
ChainIOK[TestConfig](func(x int) io.IO[string] {
return func() string {
counter++
return fmt.Sprintf("Value: %d", x)
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("Value: 42"), outcome)
assert.Equal(t, 1, counter)
})
t.Run("chains multiple IO actions", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
ChainIOK[TestConfig](func(x int) io.IO[int] {
return func() int {
log = append(log, "first")
return x + 5
}
}),
ChainIOK[TestConfig](func(x int) io.IO[int] {
return func() int {
log = append(log, "second")
return x * 2
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestChainIOK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
ChainIOK[TestConfig](func(x int) io.IO[string] {
return func() string {
executed = true
return "should not execute"
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
assert.False(t, executed)
})
}
// TestChainFirstIOK tests the ChainFirstIOK function
func TestChainFirstIOK_Success(t *testing.T) {
t.Run("executes IO but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, fmt.Sprintf("logged: %d", x))
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"logged: 42"}, log)
})
t.Run("chains multiple side effects", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, "first")
return nil
}
}),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, "second")
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(10), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestChainFirstIOK_Failure(t *testing.T) {
t.Run("propagates error without executing IO", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
executed = true
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestTapIOK tests the TapIOK function
func TestTapIOK_Success(t *testing.T) {
t.Run("executes IO but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, fmt.Sprintf("tapped: %d", x))
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"tapped: 42"}, log)
})
t.Run("is equivalent to ChainFirstIOK", func(t *testing.T) {
log1 := []string{}
log2 := []string{}
eff1 := F.Pipe1(
Of[TestConfig](10),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log1 = append(log1, "tap")
return nil
}
}),
)
eff2 := F.Pipe1(
Of[TestConfig](10),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log2 = append(log2, "tap")
return nil
}
}),
)
outcome1 := eff1(testConfig)(context.Background())()
outcome2 := eff2(testConfig)(context.Background())()
assert.Equal(t, outcome1, outcome2)
assert.Equal(t, log1, log2)
})
}
func TestTapIOK_Failure(t *testing.T) {
t.Run("propagates error without executing IO", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
executed = true
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestChainResultK tests the ChainResultK function
func TestChainResultK_Success(t *testing.T) {
t.Run("chains with Result-returning function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Of[TestConfig]("42"),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("chains multiple Result operations", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe2(
Of[TestConfig]("10"),
ChainResultK[TestConfig](parseIntResult),
ChainResultK[TestConfig](func(x int) result.Result[string] {
return result.Of(fmt.Sprintf("Value: %d", x*2))
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("Value: 20"), outcome)
})
}
func TestChainResultK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := errors.New("test error")
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Fail[TestConfig, string](testErr),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("propagates error from Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Of[TestConfig]("not a number"),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.True(t, result.IsLeft(outcome))
})
}
// TestAp tests the Ap function
func TestAp_Success(t *testing.T) {
t.Run("applies function effect to value effect", func(t *testing.T) {
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
valEff := Of[TestConfig](21)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("applies function with different types", func(t *testing.T) {
fnEff := Of[TestConfig](func(x int) string { return strconv.Itoa(x) })
valEff := Of[TestConfig](42)
eff := Ap[string](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
}
func TestAp_Failure(t *testing.T) {
t.Run("propagates error from function effect", func(t *testing.T) {
testErr := errors.New("function error")
fnEff := Fail[TestConfig, func(int) int](testErr)
valEff := Of[TestConfig](42)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("propagates error from value effect", func(t *testing.T) {
testErr := errors.New("value error")
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
valEff := Fail[TestConfig, int](testErr)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}
// TestSuspend tests the Suspend function
func TestSuspend_Success(t *testing.T) {
t.Run("delays evaluation of effect", func(t *testing.T) {
counter := 0
eff := Suspend(func() Effect[TestConfig, int] {
counter++
return Of[TestConfig](42)
})
assert.Equal(t, 0, counter, "should not evaluate immediately")
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, 1, counter, "should evaluate when run")
assert.Equal(t, result.Of(42), outcome)
})
t.Run("enables recursive effects", func(t *testing.T) {
var factorial func(int) Effect[TestConfig, int]
factorial = func(n int) Effect[TestConfig, int] {
if n <= 1 {
return Of[TestConfig](1)
}
return Suspend(func() Effect[TestConfig, int] {
return F.Pipe1(
factorial(n-1),
Map[TestConfig](func(x int) int { return x * n }),
)
})
}
outcome := factorial(5)(testConfig)(context.Background())()
assert.Equal(t, result.Of(120), outcome)
})
}
// TestTap tests the Tap function
func TestTap_Success(t *testing.T) {
t.Run("executes side effect but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
Tap(func(x int) Effect[TestConfig, any] {
log = append(log, fmt.Sprintf("tapped: %d", x))
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"tapped: 42"}, log)
})
t.Run("chains multiple taps", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
Tap(func(x int) Effect[TestConfig, any] {
log = append(log, "first")
return Of[TestConfig, any](nil)
}),
Tap(func(x int) Effect[TestConfig, any] {
log = append(log, "second")
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(10), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestTap_Failure(t *testing.T) {
t.Run("propagates error without executing tap", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Tap(func(x int) Effect[TestConfig, any] {
executed = true
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestTernary tests the Ternary function
func TestTernary_Success(t *testing.T) {
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("large")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("small")
},
)
outcome := kleisli(15)(testConfig)(context.Background())()
assert.Equal(t, result.Of("large"), outcome)
})
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("large")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("small")
},
)
outcome := kleisli(5)(testConfig)(context.Background())()
assert.Equal(t, result.Of("small"), outcome)
})
t.Run("works with boundary value", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x >= 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("gte")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("lt")
},
)
outcome := kleisli(10)(testConfig)(context.Background())()
assert.Equal(t, result.Of("gte"), outcome)
})
}
// TestRead tests the Read function
func TestRead_Success(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
eff := Of[TestConfig](42)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("converts effect to thunk", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](10),
Map[TestConfig](func(x int) int { return x * testConfig.Multiplier }),
)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
t.Run("works with different contexts", func(t *testing.T) {
cfg1 := TestConfig{Multiplier: 2, Prefix: "A", DatabaseURL: ""}
cfg2 := TestConfig{Multiplier: 5, Prefix: "B", DatabaseURL: ""}
// Create an effect that uses the context's Multiplier
eff := F.Pipe1(
Of[TestConfig](10),
ChainReaderK(func(x int) reader.Reader[TestConfig, int] {
return func(cfg TestConfig) int {
return x * cfg.Multiplier
}
}),
)
thunk1 := Read[int](cfg1)(eff)
thunk2 := Read[int](cfg2)(eff)
outcome1 := thunk1(context.Background())()
outcome2 := thunk2(context.Background())()
assert.Equal(t, result.Of(20), outcome1)
assert.Equal(t, result.Of(50), outcome2)
})
}
func TestRead_Failure(t *testing.T) {
t.Run("propagates error from effect", func(t *testing.T) {
testErr := errors.New("test error")
eff := Fail[TestConfig, int](testErr)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}

View 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 effect
import (
"context"
"errors"
"strconv"
"testing"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestRead(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
ctx := TestContext{Value: "test-context"}
eff := Of[TestContext](42)
thunk := Read[int](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsRight(res))
value, err := result.Unwrap(res)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("provides context to failing effect", func(t *testing.T) {
expectedErr := errors.New("read error")
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
thunk := Read[string](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("provides context to chained effects", func(t *testing.T) {
ctx := TestContext{Value: "base"}
eff := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x * 2))
})(Of[TestContext](21))
thunk := Read[string](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsRight(res))
value, err := result.Unwrap(res)
assert.NoError(t, err)
assert.Equal(t, "42", value)
})
t.Run("works with different context types", func(t *testing.T) {
type CustomContext struct {
ID int
Name string
}
ctx := CustomContext{ID: 100, Name: "custom"}
eff := Of[CustomContext]("result")
thunk := Read[string](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsRight(res))
value, err := result.Unwrap(res)
assert.NoError(t, err)
assert.Equal(t, "result", value)
})
t.Run("can be composed with RunSync", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](100)
thunk := Read[int](ctx)(eff)
readerResult := RunSync(thunk)
value, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, value)
})
}
func TestChainResultK(t *testing.T) {
t.Run("chains successful Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Of[TestContext]("42")
chained := ChainResultK[TestContext](parseIntResult)(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("chains failing Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Of[TestContext]("not-a-number")
chained := ChainResultK[TestContext](parseIntResult)(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})
t.Run("propagates error from original effect", func(t *testing.T) {
expectedErr := errors.New("original error")
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Fail[TestContext, string](expectedErr)
chained := ChainResultK[TestContext](parseIntResult)(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple Result functions", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
formatResult := func(x int) result.Result[string] {
return result.Of("value: " + strconv.Itoa(x))
}
eff := Of[TestContext]("42")
chained := ChainResultK[TestContext](formatResult)(
ChainResultK[TestContext](parseIntResult)(eff),
)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "value: 42", result)
})
t.Run("integrates with other effect operations", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Map[TestContext](func(x int) string {
return "final: " + strconv.Itoa(x)
})(ChainResultK[TestContext](parseIntResult)(Of[TestContext]("100")))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "final: 100", result)
})
t.Run("works with custom Result functions", func(t *testing.T) {
validatePositive := func(x int) result.Result[int] {
if x > 0 {
return result.Of(x)
}
return result.Left[int](errors.New("must be positive"))
}
parseIntResult := result.Eitherize1(strconv.Atoi)
// Test with positive number
eff1 := ChainResultK[TestContext](validatePositive)(
ChainResultK[TestContext](parseIntResult)(Of[TestContext]("42")),
)
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
assert.NoError(t, err1)
assert.Equal(t, 42, result1)
// Test with negative number
eff2 := ChainResultK[TestContext](validatePositive)(
ChainResultK[TestContext](parseIntResult)(Of[TestContext]("-5")),
)
_, err2 := runEffect(eff2, TestContext{Value: "test"})
assert.Error(t, err2)
assert.Contains(t, err2.Error(), "must be positive")
})
t.Run("preserves error context", func(t *testing.T) {
customError := errors.New("custom validation error")
validateFunc := func(s string) result.Result[string] {
if len(s) > 0 {
return result.Of(s)
}
return result.Left[string](customError)
}
eff := ChainResultK[TestContext](validateFunc)(Of[TestContext](""))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, customError, err)
})
}

File diff suppressed because it is too large Load Diff

208
v2/effect/eitherize.go Normal file
View 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
View 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)
})
}

View File

@@ -1,3 +1,18 @@
// 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 (
@@ -5,10 +20,66 @@ import (
"github.com/IBM/fp-go/v2/monoid"
)
// ApplicativeMonoid creates a monoid for effects using applicative semantics.
// This combines effects by running both and combining their results using the provided monoid.
// If either effect fails, the combined effect fails.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The value type that has a monoid instance
//
// # Parameters
//
// - m: The monoid instance for combining values of type A
//
// # Returns
//
// - Monoid[Effect[C, A]]: A monoid for combining effects
//
// # Example
//
// stringMonoid := monoid.MakeMonoid(
// func(a, b string) string { return a + b },
// "",
// )
// effectMonoid := effect.ApplicativeMonoid[MyContext](stringMonoid)
// eff1 := effect.Of[MyContext]("Hello")
// eff2 := effect.Of[MyContext](" World")
// combined := effectMonoid.Concat(eff1, eff2)
// // combined produces "Hello World"
func ApplicativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.ApplicativeMonoid[C](m)
}
// AlternativeMonoid creates a monoid for effects using alternative semantics.
// This tries the first effect, and if it fails, tries the second effect.
// If both succeed, their results are combined using the provided monoid.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The value type that has a monoid instance
//
// # Parameters
//
// - m: The monoid instance for combining values of type A
//
// # Returns
//
// - Monoid[Effect[C, A]]: A monoid for combining effects with fallback behavior
//
// # Example
//
// stringMonoid := monoid.MakeMonoid(
// func(a, b string) string { return a + b },
// "",
// )
// effectMonoid := effect.AlternativeMonoid[MyContext](stringMonoid)
// eff1 := effect.Fail[MyContext, string](errors.New("failed"))
// eff2 := effect.Of[MyContext]("fallback")
// combined := effectMonoid.Concat(eff1, eff2)
// // combined produces "fallback" (first failed, so second is used)
func AlternativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.AlternativeMonoid[C](m)
}

View File

@@ -1,3 +1,18 @@
// 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 (
@@ -5,6 +20,39 @@ import (
"github.com/IBM/fp-go/v2/retry"
)
// Retrying executes an effect with retry logic based on a policy and check predicate.
// The effect is retried according to the policy until either:
// - The effect succeeds and the check predicate returns false
// - The retry policy is exhausted
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - policy: The retry policy defining retry limits and delays
// - action: An effectful computation that receives retry status and produces a value
// - check: A predicate that determines if the result should trigger a retry
//
// # Returns
//
// - Effect[C, A]: An effect that retries according to the policy
//
// # Example
//
// policy := retry.LimitRetries(3)
// eff := effect.Retrying[MyContext, string](
// policy,
// func(status retry.RetryStatus) Effect[MyContext, string] {
// return fetchData() // may fail
// },
// func(result Result[string]) bool {
// return result.IsLeft() // retry on error
// },
// )
// // Retries up to 3 times if fetchData fails
func Retrying[C, A any](
policy retry.RetryPolicy,
action Kleisli[C, retry.RetryStatus, A],

View File

@@ -1,3 +1,18 @@
// 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 (
@@ -8,10 +23,64 @@ import (
"github.com/IBM/fp-go/v2/result"
)
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
// Provide supplies a context to an effect, converting it to a Thunk.
// This is the first step in running an effect - it eliminates the context dependency
// by providing the required context value.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - c: The context value to provide to the effect
//
// # Returns
//
// - func(Effect[C, A]) ReaderIOResult[A]: A function that converts an effect to a thunk
//
// # Example
//
// ctx := MyContext{APIKey: "secret"}
// eff := effect.Of[MyContext](42)
// thunk := effect.Provide[MyContext, int](ctx)(eff)
// // thunk is now a ReaderIOResult[int] that can be run
func Provide[A, C any](c C) func(Effect[C, A]) ReaderIOResult[A] {
return readerreaderioresult.Read[A](c)
}
// RunSync executes a Thunk synchronously, converting it to a standard Go function.
// This is the final step in running an effect - it executes the IO operations
// and returns the result as a standard (value, error) tuple.
//
// # Type Parameters
//
// - A: The type of the success value
//
// # Parameters
//
// - fa: The thunk to execute
//
// # Returns
//
// - readerresult.ReaderResult[A]: A function that takes a context.Context and returns (A, error)
//
// # Example
//
// ctx := MyContext{APIKey: "secret"}
// eff := effect.Of[MyContext](42)
// thunk := effect.Provide[MyContext, int](ctx)(eff)
// readerResult := effect.RunSync(thunk)
// value, err := readerResult(context.Background())
// // value == 42, err == nil
//
// # Complete Example
//
// // Typical usage pattern:
// result, err := effect.RunSync(
// effect.Provide[MyContext, string](myContext)(myEffect),
// )(context.Background())
func RunSync[A any](fa ReaderIOResult[A]) readerresult.ReaderResult[A] {
return func(ctx context.Context) (A, error) {
return result.Unwrap(fa(ctx)())

View File

@@ -28,7 +28,7 @@ func TestProvide(t *testing.T) {
ctx := TestContext{Value: "test-value"}
eff := Of[TestContext]("result")
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -45,7 +45,7 @@ func TestProvide(t *testing.T) {
cfg := Config{Host: "localhost", Port: 8080}
eff := Of[Config]("connected")
ioResult := Provide[Config, string](cfg)(eff)
ioResult := Provide[string](cfg)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -58,7 +58,7 @@ func TestProvide(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -74,7 +74,7 @@ func TestProvide(t *testing.T) {
ctx := SimpleContext{ID: 42}
eff := Of[SimpleContext](100)
ioResult := Provide[SimpleContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -89,7 +89,7 @@ func TestProvide(t *testing.T) {
return Of[TestContext]("result")
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -104,7 +104,7 @@ func TestProvide(t *testing.T) {
return "mapped"
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -118,7 +118,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -130,7 +130,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext]("hello")
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string](ctx)(eff)
readerResult := RunSync(ioResult)
bgCtx := context.Background()
@@ -145,7 +145,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, int](expectedErr)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -162,7 +162,7 @@ func TestRunSync(t *testing.T) {
return Of[TestContext](x + 10)
})(Of[TestContext](5)))
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -174,7 +174,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int](ctx)(eff)
readerResult := RunSync(ioResult)
// Run multiple times
@@ -200,7 +200,7 @@ func TestRunSync(t *testing.T) {
user := User{Name: "Alice", Age: 30}
eff := Of[TestContext](user)
ioResult := Provide[TestContext, User](ctx)(eff)
ioResult := Provide[User](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -222,7 +222,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
eff := Of[AppConfig]("API call successful")
// Provide config and run
result, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
result, err := RunSync(Provide[string](cfg)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "API call successful", result)
@@ -238,7 +238,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
eff := Fail[AppConfig, string](expectedErr)
_, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
_, err := RunSync(Provide[string](cfg)(eff))(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
@@ -253,7 +253,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return Of[TestContext](x * 2)
})(Of[TestContext](21)))
result, err := RunSync(Provide[TestContext, string](ctx)(eff))(context.Background())
result, err := RunSync(Provide[string](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "final", result)
@@ -281,7 +281,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return State{X: x}
})(Of[TestContext](10)))
result, err := RunSync(Provide[TestContext, State](ctx)(eff))(context.Background())
result, err := RunSync(Provide[State](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
@@ -300,11 +300,11 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
innerEff := Of[InnerCtx]("inner result")
// Transform context
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
transformedEff := Local[string](func(outer OuterCtx) InnerCtx {
return InnerCtx{Data: outer.Value + "-transformed"}
})(innerEff)
result, err := RunSync(Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
result, err := RunSync(Provide[string](outerCtx)(transformedEff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner result", result)
@@ -318,7 +318,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return Of[TestContext](x * 2)
})(input)
result, err := RunSync(Provide[TestContext, []int](ctx)(eff))(context.Background())
result, err := RunSync(Provide[[]int](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)

View File

@@ -1,7 +1,55 @@
// 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 "github.com/IBM/fp-go/v2/context/readerreaderioresult"
// TraverseArray applies an effectful function to each element of an array,
// collecting the results into a new array. If any effect fails, the entire
// traversal fails and returns the first error encountered.
//
// This is useful for performing effectful operations on collections while
// maintaining the sequential order of results.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - f: An effectful function to apply to each element
//
// # Returns
//
// - Kleisli[C, []A, []B]: A function that transforms an array of A to an effect producing an array of B
//
// # Example
//
// parseIntEff := func(s string) Effect[MyContext, int] {
// val, err := strconv.Atoi(s)
// if err != nil {
// return effect.Fail[MyContext, int](err)
// }
// return effect.Of[MyContext](val)
// }
// input := []string{"1", "2", "3"}
// eff := effect.TraverseArray[MyContext](parseIntEff)(input)
// // eff produces []int{1, 2, 3}
func TraverseArray[C, A, B any](f Kleisli[C, A, B]) Kleisli[C, []A, []B] {
return readerreaderioresult.TraverseArray(f)
}

View File

@@ -1,3 +1,18 @@
// 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 (
@@ -17,21 +32,61 @@ import (
)
type (
Either[E, A any] = either.Either[E, A]
Reader[R, A any] = reader.Reader[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
IO[A any] = io.IO[A]
IOEither[E, A any] = ioeither.IOEither[E, A]
Lazy[A any] = lazy.Lazy[A]
IOResult[A any] = ioresult.IOResult[A]
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
Monoid[A any] = monoid.Monoid[A]
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
Thunk[A any] = ReaderIOResult[A]
Predicate[A any] = predicate.Predicate[A]
Result[A any] = result.Result[A]
Lens[S, T any] = lens.Lens[S, T]
// Either represents a value that can be either a Left (error) or Right (success).
Either[E, A any] = either.Either[E, A]
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
// Reader represents a computation that depends on a context R and produces a value A.
Reader[R, A any] = reader.Reader[R, A]
// ReaderIO represents a computation that depends on a context R and produces an IO action returning A.
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// IO represents a synchronous side effect that produces a value A.
IO[A any] = io.IO[A]
// IOEither represents a synchronous side effect that can fail with error E or succeed with value A.
IOEither[E, A any] = ioeither.IOEither[E, A]
// Lazy represents a lazily evaluated computation that produces a value A.
Lazy[A any] = lazy.Lazy[A]
// IOResult represents a synchronous side effect that can fail with an error or succeed with value A.
IOResult[A any] = ioresult.IOResult[A]
// ReaderIOResult represents a computation that depends on context and performs IO with error handling.
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
Monoid[A any] = monoid.Monoid[A]
// Effect represents an effectful computation that:
// - Requires a context of type C
// - Can perform I/O operations
// - Can fail with an error
// - Produces a value of type A on success
//
// This is the core type of the effect package, providing a complete effect system
// for managing dependencies, errors, and side effects in a composable way.
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
// Thunk represents a computation that performs IO with error handling but doesn't require context.
// It's equivalent to ReaderIOResult and is used as an intermediate step when providing context to an Effect.
Thunk[A any] = ReaderIOResult[A]
// Predicate represents a function that tests a value of type A and returns a boolean.
Predicate[A any] = predicate.Predicate[A]
// Result represents a computation result that can be either an error (Left) or a success value (Right).
Result[A any] = result.Result[A]
// Lens represents an optic for focusing on a field T within a structure S.
Lens[S, T any] = lens.Lens[S, T]
// Kleisli represents a function from A to Effect[C, B], enabling monadic composition.
// It's the fundamental building block for chaining effectful computations.
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
// Operator represents a function that transforms Effect[C, A] to Effect[C, B].
// It's used for lifting operations over effects.
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
)

View File

@@ -379,7 +379,7 @@ func TestMonadChainLeft(t *testing.T) {
func TestChainLeft(t *testing.T) {
t.Run("Curried function transforms Left value", func(t *testing.T) {
// Create a reusable error handler
handleNotFound := ChainLeft[error, string](func(err error) Either[string, int] {
handleNotFound := ChainLeft(func(err error) Either[string, int] {
if err.Error() == "not found" {
return Right[string](0)
}
@@ -391,7 +391,7 @@ func TestChainLeft(t *testing.T) {
})
t.Run("Curried function with Right value", func(t *testing.T) {
handler := ChainLeft[error, string](func(err error) Either[string, int] {
handler := ChainLeft(func(err error) Either[string, int] {
return Left[int]("should not be called")
})
@@ -401,7 +401,7 @@ func TestChainLeft(t *testing.T) {
t.Run("Use in pipeline with Pipe", func(t *testing.T) {
// Create error transformer
toStringError := ChainLeft[int, string](func(code int) Either[string, string] {
toStringError := ChainLeft(func(code int) Either[string, string] {
return Left[string](fmt.Sprintf("Error: %d", code))
})
@@ -414,12 +414,12 @@ func TestChainLeft(t *testing.T) {
t.Run("Compose multiple ChainLeft operations", func(t *testing.T) {
// First handler: convert error to string
handler1 := ChainLeft[error, string](func(err error) Either[string, int] {
handler1 := ChainLeft(func(err error) Either[string, int] {
return Left[int](err.Error())
})
// Second handler: add prefix to string error
handler2 := ChainLeft[string, string](func(s string) Either[string, int] {
handler2 := ChainLeft(func(s string) Either[string, int] {
return Left[int]("Handled: " + s)
})

View File

@@ -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]
)

52
v2/monoid/types.go Normal file
View File

@@ -0,0 +1,52 @@
// 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 monoid
import "github.com/IBM/fp-go/v2/function"
// Void is an alias for function.Void, representing the unit type.
//
// The Void type (also known as Unit in functional programming) has exactly one value,
// making it useful for representing the absence of meaningful information. It's similar
// to void in other languages, but as a value rather than the absence of a value.
//
// This type alias is provided in the monoid package for convenience when working with
// VoidMonoid and other monoid operations that may use the unit type.
//
// Common use cases:
// - As a return type for functions that perform side effects but don't return meaningful data
// - As a placeholder type parameter when a type is required but no data needs to be passed
// - In monoid operations where you need to track that operations occurred without caring about results
//
// See also:
// - function.Void: The underlying type definition
// - function.VOID: The single inhabitant of the Void type
// - VoidMonoid: A monoid instance for the Void type
//
// Example:
//
// // Function that performs an action but returns no meaningful data
// func logMessage(msg string) Void {
// fmt.Println(msg)
// return function.VOID
// }
//
// // Using Void in monoid operations
// m := VoidMonoid()
// result := m.Concat(function.VOID, function.VOID) // function.VOID
type (
Void = function.Void
)

65
v2/monoid/void.go Normal file
View File

@@ -0,0 +1,65 @@
// 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 monoid
import (
"github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
// VoidMonoid creates a Monoid for the Void (unit) type.
//
// The Void type has exactly one value (function.VOID), making it trivial to define
// a monoid. This monoid uses the Last semigroup, which always returns the second
// argument, though since all Void values are identical, the choice of semigroup
// doesn't affect the result.
//
// This monoid is useful in contexts where:
// - A monoid instance is required but no meaningful data needs to be combined
// - You need to track that an operation occurred without caring about its result
// - Building generic abstractions that work with any monoid, including the trivial case
//
// # Monoid Laws
//
// The VoidMonoid satisfies all monoid laws trivially:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z)) - always VOID
// - Left Identity: Concat(Empty(), x) = x - always VOID
// - Right Identity: Concat(x, Empty()) = x - always VOID
//
// Returns:
// - A Monoid[Void] instance
//
// Example:
//
// m := VoidMonoid()
// result := m.Concat(function.VOID, function.VOID) // function.VOID
// empty := m.Empty() // function.VOID
//
// // Useful for tracking operations without data
// type Action = func() Void
// actions := []Action{
// func() Void { fmt.Println("Action 1"); return function.VOID },
// func() Void { fmt.Println("Action 2"); return function.VOID },
// }
// // Execute all actions and combine results
// results := A.Map(func(a Action) Void { return a() })(actions)
// _ = ConcatAll(m)(results) // All actions executed, result is VOID
func VoidMonoid() Monoid[Void] {
return MakeMonoid(
S.Last[Void]().Concat,
function.VOID,
)
}

290
v2/monoid/void_test.go Normal file
View File

@@ -0,0 +1,290 @@
// 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 monoid
import (
"testing"
"github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestVoidMonoid_Basic tests basic VoidMonoid functionality
func TestVoidMonoid_Basic(t *testing.T) {
m := VoidMonoid()
// Test Empty returns VOID
empty := m.Empty()
assert.Equal(t, function.VOID, empty)
// Test Concat returns VOID (since all Void values are identical)
result := m.Concat(function.VOID, function.VOID)
assert.Equal(t, function.VOID, result)
}
// TestVoidMonoid_Laws verifies VoidMonoid satisfies monoid laws
func TestVoidMonoid_Laws(t *testing.T) {
m := VoidMonoid()
// Since Void has only one value, we test with that value
v := function.VOID
// Left Identity: Concat(Empty(), x) = x
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), v)
assert.Equal(t, v, result, "Left identity law failed")
})
// Right Identity: Concat(x, Empty()) = x
t.Run("right identity", func(t *testing.T) {
result := m.Concat(v, m.Empty())
assert.Equal(t, v, result, "Right identity law failed")
})
// Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
t.Run("associativity", func(t *testing.T) {
left := m.Concat(m.Concat(v, v), v)
right := m.Concat(v, m.Concat(v, v))
assert.Equal(t, left, right, "Associativity law failed")
})
// All results should be VOID
t.Run("all operations return VOID", func(t *testing.T) {
assert.Equal(t, function.VOID, m.Concat(v, v))
assert.Equal(t, function.VOID, m.Empty())
assert.Equal(t, function.VOID, m.Concat(m.Empty(), v))
assert.Equal(t, function.VOID, m.Concat(v, m.Empty()))
})
}
// TestVoidMonoid_ConcatAll tests combining multiple Void values
func TestVoidMonoid_ConcatAll(t *testing.T) {
m := VoidMonoid()
concatAll := ConcatAll(m)
tests := []struct {
name string
input []Void
expected Void
}{
{
name: "empty slice",
input: []Void{},
expected: function.VOID,
},
{
name: "single element",
input: []Void{function.VOID},
expected: function.VOID,
},
{
name: "multiple elements",
input: []Void{function.VOID, function.VOID, function.VOID},
expected: function.VOID,
},
{
name: "many elements",
input: make([]Void, 100),
expected: function.VOID,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Initialize slice with VOID values
for i := range tt.input {
tt.input[i] = function.VOID
}
result := concatAll(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// TestVoidMonoid_Fold tests the Fold function with VoidMonoid
func TestVoidMonoid_Fold(t *testing.T) {
m := VoidMonoid()
fold := Fold(m)
// Fold should behave identically to ConcatAll
voids := []Void{function.VOID, function.VOID, function.VOID}
result := fold(voids)
assert.Equal(t, function.VOID, result)
// Empty fold
emptyResult := fold([]Void{})
assert.Equal(t, function.VOID, emptyResult)
}
// TestVoidMonoid_Reverse tests that Reverse doesn't affect VoidMonoid
func TestVoidMonoid_Reverse(t *testing.T) {
m := VoidMonoid()
reversed := Reverse(m)
// Since all Void values are identical, reverse should have no effect
v := function.VOID
assert.Equal(t, m.Concat(v, v), reversed.Concat(v, v))
assert.Equal(t, m.Empty(), reversed.Empty())
// Test identity laws still hold
assert.Equal(t, v, reversed.Concat(reversed.Empty(), v))
assert.Equal(t, v, reversed.Concat(v, reversed.Empty()))
}
// TestVoidMonoid_ToSemigroup tests conversion to Semigroup
func TestVoidMonoid_ToSemigroup(t *testing.T) {
m := VoidMonoid()
sg := ToSemigroup(m)
// Should work as a semigroup
result := sg.Concat(function.VOID, function.VOID)
assert.Equal(t, function.VOID, result)
// Verify it's the same underlying operation
assert.Equal(t, m.Concat(function.VOID, function.VOID), sg.Concat(function.VOID, function.VOID))
}
// TestVoidMonoid_FunctionMonoid tests VoidMonoid with FunctionMonoid
func TestVoidMonoid_FunctionMonoid(t *testing.T) {
m := VoidMonoid()
funcMonoid := FunctionMonoid[string](m)
// Create functions that return Void
f1 := func(s string) Void { return function.VOID }
f2 := func(s string) Void { return function.VOID }
// Combine functions
combined := funcMonoid.Concat(f1, f2)
// Test combined function
result := combined("test")
assert.Equal(t, function.VOID, result)
// Test empty function
emptyFunc := funcMonoid.Empty()
assert.Equal(t, function.VOID, emptyFunc("anything"))
}
// TestVoidMonoid_PracticalUsage demonstrates practical usage patterns
func TestVoidMonoid_PracticalUsage(t *testing.T) {
m := VoidMonoid()
// Simulate tracking that operations occurred without caring about results
type Action func() Void
actions := []Action{
func() Void { return function.VOID }, // Action 1
func() Void { return function.VOID }, // Action 2
func() Void { return function.VOID }, // Action 3
}
// Execute all actions and collect results
results := make([]Void, len(actions))
for i, action := range actions {
results[i] = action()
}
// Combine all results (all are VOID)
finalResult := ConcatAll(m)(results)
assert.Equal(t, function.VOID, finalResult)
}
// TestVoidMonoid_EdgeCases tests edge cases
func TestVoidMonoid_EdgeCases(t *testing.T) {
m := VoidMonoid()
t.Run("multiple concatenations", func(t *testing.T) {
// Chain multiple Concat operations
result := m.Concat(
m.Concat(
m.Concat(function.VOID, function.VOID),
function.VOID,
),
function.VOID,
)
assert.Equal(t, function.VOID, result)
})
t.Run("concat with empty", func(t *testing.T) {
// Various combinations with Empty()
assert.Equal(t, function.VOID, m.Concat(m.Empty(), m.Empty()))
assert.Equal(t, function.VOID, m.Concat(m.Concat(m.Empty(), function.VOID), m.Empty()))
})
t.Run("large slice", func(t *testing.T) {
// Test with a large number of elements
largeSlice := make([]Void, 10000)
for i := range largeSlice {
largeSlice[i] = function.VOID
}
result := ConcatAll(m)(largeSlice)
assert.Equal(t, function.VOID, result)
})
}
// TestVoidMonoid_TypeSafety verifies type safety
func TestVoidMonoid_TypeSafety(t *testing.T) {
m := VoidMonoid()
// Verify it implements Monoid interface
var _ Monoid[Void] = m
// Verify Empty returns correct type
empty := m.Empty()
var _ Void = empty
// Verify Concat returns correct type
result := m.Concat(function.VOID, function.VOID)
var _ Void = result
}
// BenchmarkVoidMonoid_Concat benchmarks the Concat operation
func BenchmarkVoidMonoid_Concat(b *testing.B) {
m := VoidMonoid()
v := function.VOID
b.ResetTimer()
for b.Loop() {
_ = m.Concat(v, v)
}
}
// BenchmarkVoidMonoid_ConcatAll benchmarks combining multiple Void values
func BenchmarkVoidMonoid_ConcatAll(b *testing.B) {
m := VoidMonoid()
concatAll := ConcatAll(m)
voids := make([]Void, 1000)
for i := range voids {
voids[i] = function.VOID
}
b.ResetTimer()
for b.Loop() {
_ = concatAll(voids)
}
}
// BenchmarkVoidMonoid_Empty benchmarks the Empty operation
func BenchmarkVoidMonoid_Empty(b *testing.B) {
m := VoidMonoid()
b.ResetTimer()
for b.Loop() {
_ = m.Empty()
}
}

View File

@@ -62,7 +62,7 @@ func TestMonadAltBasicFunctionality(t *testing.T) {
assert.True(t, either.IsRight(result), "should successfully decode with first codec")
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "HELLO", value)
})
@@ -105,7 +105,7 @@ func TestMonadAltBasicFunctionality(t *testing.T) {
assert.True(t, either.IsRight(result), "should successfully decode with second codec")
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
assert.Equal(t, -5, value)
})
@@ -302,19 +302,19 @@ func TestAltOperator(t *testing.T) {
// Test with "42" - should use base codec
result1 := pipeline.Decode("42")
assert.True(t, either.IsRight(result1))
value1 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result1)
value1 := either.GetOrElse(reader.Of[validation.Errors](0))(result1)
assert.Equal(t, 42, value1)
// Test with "100" - should use fallback1
result2 := pipeline.Decode("100")
assert.True(t, either.IsRight(result2))
value2 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result2)
value2 := either.GetOrElse(reader.Of[validation.Errors](0))(result2)
assert.Equal(t, 100, value2)
// Test with "999" - should use fallback2
result3 := pipeline.Decode("999")
assert.True(t, either.IsRight(result3))
value3 := either.GetOrElse(reader.Of[validation.Errors, int](0))(result3)
value3 := either.GetOrElse(reader.Of[validation.Errors](0))(result3)
assert.Equal(t, 999, value3)
})
}
@@ -449,7 +449,7 @@ func TestAltRoundTrip(t *testing.T) {
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
decoded := either.GetOrElse(reader.Of[validation.Errors](""))(decodeResult)
// Encode
encoded := altCodec.Encode(decoded)
@@ -487,7 +487,7 @@ func TestAltRoundTrip(t *testing.T) {
decodeResult := altCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
decoded := either.GetOrElse(reader.Of[validation.Errors, string](""))(decodeResult)
decoded := either.GetOrElse(reader.Of[validation.Errors](""))(decodeResult)
// Encode (uses first codec's encoder, which is identity)
encoded := altCodec.Encode(decoded)
@@ -619,7 +619,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
assert.Equal(t, 10, value, "first success should win")
})
@@ -628,7 +628,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("42")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](0))(result)
value := either.GetOrElse(reader.Of[validation.Errors](0))(result)
assert.Equal(t, 42, value)
})
@@ -637,7 +637,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("invalid")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
assert.Equal(t, 0, value, "should use default zero value")
})
})
@@ -768,21 +768,21 @@ func TestAltMonoid(t *testing.T) {
t.Run("uses primary when it succeeds", func(t *testing.T) {
result := combined.Decode("primary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "from primary", value)
})
t.Run("uses secondary when primary fails", func(t *testing.T) {
result := combined.Decode("secondary")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "from secondary", value)
})
t.Run("uses default when both fail", func(t *testing.T) {
result := combined.Decode("other")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, string](""))(result)
value := either.GetOrElse(reader.Of[validation.Errors](""))(result)
assert.Equal(t, "default", value)
})
})
@@ -841,7 +841,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
// Empty (0) comes first, so it wins
assert.Equal(t, 0, value)
})
@@ -852,7 +852,7 @@ func TestAltMonoid(t *testing.T) {
result := combined.Decode("input")
assert.True(t, either.IsRight(result))
value := either.GetOrElse(reader.Of[validation.Errors, int](-1))(result)
value := either.GetOrElse(reader.Of[validation.Errors](-1))(result)
assert.Equal(t, 10, value, "codec1 should win")
})
@@ -867,8 +867,8 @@ func TestAltMonoid(t *testing.T) {
assert.True(t, either.IsRight(resultLeft))
assert.True(t, either.IsRight(resultRight))
valueLeft := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultLeft)
valueRight := either.GetOrElse(reader.Of[validation.Errors, int](-1))(resultRight)
valueLeft := either.GetOrElse(reader.Of[validation.Errors](-1))(resultLeft)
valueRight := either.GetOrElse(reader.Of[validation.Errors](-1))(resultRight)
// Both should return 10 (first success)
assert.Equal(t, valueLeft, valueRight)

299
v2/optics/codec/bind.go Normal file
View File

@@ -0,0 +1,299 @@
// 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 codec
import (
"fmt"
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/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/semigroup"
)
// ApSL creates an applicative sequencing operator for codecs using a lens.
//
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs,
// allowing you to build up complex codecs by combining a base codec with a field
// accessed through a lens. It's particularly useful for building struct codecs
// field-by-field in a composable way.
//
// The function combines:
// - Encoding: Extracts the field value using the lens, encodes it with fa, and
// combines it with the base encoding using the monoid
// - Validation: Validates the field using the lens and combines the validation
// with the base validation
//
// # Type Parameters
//
// - S: The source struct type (what we're building a codec for)
// - T: The field type accessed by the lens
// - O: The output type for encoding (must have a monoid)
// - I: The input type for decoding
//
// # Parameters
//
// - m: A Monoid[O] for combining encoded outputs
// - l: A Lens[S, T] that focuses on a specific field in S
// - fa: A Type[T, O, I] codec for the field type T
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the field
// specified by the lens.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - Extract the field T using l.Get
// - Encode T to O using fa.Encode
// - Combine with the base encoding using the monoid
//
// 2. **Validation**: When validating input I:
// - Validate the field using fa.Validate through the lens
// - Combine with the base validation
//
// 3. **Type Checking**: Preserves the base type checker
//
// # Example
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/lens"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Person struct {
// Name string
// Age int
// }
//
// // Lenses for Person fields
// nameLens := lens.MakeLens(
// func(p *Person) string { return p.Name },
// func(p *Person, name string) *Person { p.Name = name; return p },
// )
//
// // Build a Person codec field by field
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSL(S.Monoid, nameLens, codec.String),
// // ... add more fields
// )
//
// # Use Cases
//
// - Building struct codecs incrementally
// - Composing codecs for nested structures
// - Creating type-safe serialization/deserialization
// - Implementing Do-notation style codec construction
//
// # Notes
//
// - The monoid determines how encoded outputs are combined
// - The lens must be total (handle all cases safely)
// - This is typically used with other ApS functions to build complete codecs
// - The name is automatically generated for debugging purposes
//
// See also:
// - validate.ApSL: The underlying validation combinator
// - reader.ApplicativeMonoid: The monoid-based applicative instance
// - Lens: The optic for accessing struct fields
func ApSL[S, T, O, I any](
m Monoid[O],
l Lens[S, T],
fa Type[T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("ApS[%s x %s]", l, fa)
rm := reader.ApplicativeMonoid[S](m)
encConcat := F.Pipe1(
F.Flow2(
l.Get,
fa.Encode,
),
semigroup.AppendTo(rm),
)
valConcat := validate.ApSL(l, fa.Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
valConcat,
),
encConcat(t.Encode),
)
}
}
// ApSO creates an applicative sequencing operator for codecs using an optional.
//
// This function implements the "ApS" (Applicative Sequencing) pattern for codecs
// with optional fields, allowing you to build up complex codecs by combining a base
// codec with a field that may or may not be present. It's particularly useful for
// building struct codecs with optional fields in a composable way.
//
// The function combines:
// - Encoding: Attempts to extract the optional field value, encodes it if present,
// and combines it with the base encoding using the monoid. If the field is absent,
// only the base encoding is used.
// - Validation: Validates the optional field and combines the validation with the
// base validation using applicative semantics (error accumulation).
//
// # Type Parameters
//
// - S: The source struct type (what we're building a codec for)
// - T: The optional field type accessed by the optional
// - O: The output type for encoding (must have a monoid)
// - I: The input type for decoding
//
// # Parameters
//
// - m: A Monoid[O] for combining encoded outputs
// - o: An Optional[S, T] that focuses on a field in S that may not exist
// - fa: A Type[T, O, I] codec for the optional field type T
//
// # Returns
//
// An Operator[S, S, O, I] that transforms a base codec by adding the optional field
// specified by the optional.
//
// # How It Works
//
// 1. **Encoding**: When encoding a value of type S:
// - Try to extract the optional field T using o.GetOption
// - If present (Some(T)): Encode T to O using fa.Encode and combine with base using monoid
// - If absent (None): Return only the base encoding unchanged
//
// 2. **Validation**: When validating input I:
// - Validate the optional field using fa.Validate through o.Set
// - Combine with the base validation using applicative semantics
// - Accumulates all validation errors from both base and field
//
// 3. **Type Checking**: Preserves the base type checker
//
// # Difference from ApSL
//
// Unlike ApSL which works with required fields via Lens, ApSO handles optional fields:
// - ApSL: Field always exists, always encoded
// - ApSO: Field may not exist, only encoded when present
// - ApSO uses Optional.GetOption which returns Option[T]
// - ApSO gracefully handles missing fields without errors
//
// # Example
//
// import (
// "github.com/IBM/fp-go/v2/optics/codec"
// "github.com/IBM/fp-go/v2/optics/optional"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// type Person struct {
// Name string
// Nickname *string // Optional field
// }
//
// // Optional for Person.Nickname
// nicknameOpt := optional.MakeOptional(
// func(p Person) option.Option[string] {
// if p.Nickname != nil {
// return option.Some(*p.Nickname)
// }
// return option.None[string]()
// },
// func(p Person, nick string) Person {
// p.Nickname = &nick
// return p
// },
// )
//
// // Build a Person codec with optional nickname
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
// )
//
// // Encoding with nickname present
// p1 := Person{Name: "Alice", Nickname: ptr("Ali")}
// encoded1 := personCodec.Encode(p1) // Includes nickname
//
// // Encoding with nickname absent
// p2 := Person{Name: "Bob", Nickname: nil}
// encoded2 := personCodec.Encode(p2) // No nickname in output
//
// # Use Cases
//
// - Building struct codecs with optional/nullable fields
// - Handling pointer fields that may be nil
// - Composing codecs for structures with optional nested data
// - Creating flexible serialization that omits absent fields
//
// # Notes
//
// - The monoid determines how encoded outputs are combined when field is present
// - When the optional field is absent, encoding returns base encoding unchanged
// - Validation still accumulates errors even for optional fields
// - The name is automatically generated for debugging purposes
//
// # See Also
//
// - ApSL: For required fields using Lens
// - validate.ApS: The underlying validation combinator
// - Optional: The optic for accessing optional fields
func ApSO[S, T, O, I any](
m Monoid[O],
o Optional[S, T],
fa Type[T, O, I],
) Operator[S, S, O, I] {
name := fmt.Sprintf("ApS[%s x %s]", o, fa)
encConcat := F.Flow2(
o.GetOption,
option.Map(F.Flow2(
fa.Encode,
semigroup.AppendTo(m),
)),
)
valConcat := validate.ApS(o.Set, fa.Validate)
return func(t Type[S, O, I]) Type[S, O, I] {
return MakeType(
name,
t.Is,
F.Pipe1(
t.Validate,
valConcat,
),
func(s S) O {
to := t.Encode(s)
return F.Pipe2(
encConcat(s),
option.Flap[O](to),
option.GetOrElse(lazy.Of(to)),
)
},
)
}
}

View File

@@ -0,0 +1,816 @@
// 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 codec
import (
"strconv"
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// Test types for ApSL
type Person struct {
Name string
Age int
}
func TestApSL_EncodingCombination(t *testing.T) {
t.Run("combines encodings using monoid", func(t *testing.T) {
// Create a lens for Person.Name
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Create base codec that encodes to "Person:"
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected Person",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "Person:" },
)
// Create field codec for Name
nameCodec := MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSL to combine encodings
operator := ApSL(S.Monoid, nameLens, nameCodec)
enhancedCodec := operator(baseCodec)
// Test encoding - should concatenate base encoding with field encoding
person := Person{Name: "Alice", Age: 30}
encoded := enhancedCodec.Encode(person)
// The monoid concatenates: base encoding + field encoding
// Note: The order depends on how the monoid is applied in ApSL
assert.Contains(t, encoded, "Person:")
assert.Contains(t, encoded, "Alice")
})
}
func TestApSL_ValidationCombination(t *testing.T) {
t.Run("validates field through lens", func(t *testing.T) {
// Create a lens for Person.Age
ageLens := lens.MakeLens(
func(p Person) int { return p.Age },
func(p Person, age int) Person {
return Person{Name: p.Name, Age: age}
},
)
// Create base codec that always succeeds
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected Person",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// Create field codec for Age that validates positive numbers
ageCodec := MakeType(
"Age",
func(i any) validation.Result[int] {
if n, ok := i.(int); ok {
if n > 0 {
return validation.ToResult(validation.Success(n))
}
return validation.ToResult(validation.Failures[int](validation.Errors{
&validation.ValidationError{
Value: n,
Messsage: "age must be positive",
},
}))
}
return validation.ToResult(validation.Failures[int](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected int",
},
}))
},
func(i any) Decode[Context, int] {
return func(ctx Context) validation.Validation[int] {
if n, ok := i.(int); ok {
if n > 0 {
return validation.Success(n)
}
return validation.FailureWithMessage[int](n, "age must be positive")(ctx)
}
return validation.FailureWithMessage[int](i, "expected int")(ctx)
}
},
strconv.Itoa,
)
// Apply ApSL
operator := ApSL(S.Monoid, ageLens, ageCodec)
enhancedCodec := operator(baseCodec)
// Test with invalid age (negative) - field validation should fail
invalidPerson := Person{Name: "Charlie", Age: -5}
invalidResult := enhancedCodec.Decode(invalidPerson)
assert.True(t, either.IsLeft(invalidResult), "Should fail with negative age")
// Extract and verify we have errors
errors := either.MonadFold(invalidResult,
F.Identity[validation.Errors],
func(Person) validation.Errors { return nil },
)
assert.NotEmpty(t, errors, "Should have validation errors")
})
}
func TestApSL_TypeChecking(t *testing.T) {
t.Run("preserves base type checker", func(t *testing.T) {
// Create a lens for Person.Name
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Create base codec with type checker
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected Person",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// Create field codec
nameCodec := MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSL
operator := ApSL(S.Monoid, nameLens, nameCodec)
enhancedCodec := operator(baseCodec)
// Test type checking with valid type
person := Person{Name: "Eve", Age: 22}
isResult := enhancedCodec.Is(person)
assert.True(t, either.IsRight(isResult), "Should accept Person type")
// Test type checking with invalid type
invalidResult := enhancedCodec.Is("not a person")
assert.True(t, either.IsLeft(invalidResult), "Should reject non-Person type")
})
}
func TestApSL_Naming(t *testing.T) {
t.Run("generates descriptive name", func(t *testing.T) {
// Create a lens for Person.Name
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
return Person{Name: name, Age: p.Age}
},
)
// Create base codec
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
if p, ok := i.(Person); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected Person",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
if p, ok := i.(Person); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[Person](i, "expected Person")(ctx)
}
},
func(p Person) string { return "" },
)
// Create field codec
nameCodec := MakeType(
"Name",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSL
operator := ApSL(S.Monoid, nameLens, nameCodec)
enhancedCodec := operator(baseCodec)
// Check that the name includes ApS
name := enhancedCodec.Name()
assert.Contains(t, name, "ApS", "Name should contain 'ApS'")
})
}
func TestApSL_ErrorAccumulation(t *testing.T) {
t.Run("accumulates validation errors", func(t *testing.T) {
// Create a lens for Person.Age
ageLens := lens.MakeLens(
func(p Person) int { return p.Age },
func(p Person, age int) Person {
return Person{Name: p.Name, Age: age}
},
)
// Create base codec that fails validation
baseCodec := MakeType(
"Person",
func(i any) validation.Result[Person] {
return validation.ToResult(validation.Failures[Person](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "base validation error",
},
}))
},
func(i any) Decode[Context, Person] {
return func(ctx Context) validation.Validation[Person] {
return validation.FailureWithMessage[Person](i, "base validation error")(ctx)
}
},
func(p Person) string { return "" },
)
// Create field codec that also fails
ageCodec := MakeType(
"Age",
func(i any) validation.Result[int] {
return validation.ToResult(validation.Failures[int](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "age validation error",
},
}))
},
func(i any) Decode[Context, int] {
return func(ctx Context) validation.Validation[int] {
return validation.FailureWithMessage[int](i, "age validation error")(ctx)
}
},
strconv.Itoa,
)
// Apply ApSL
operator := ApSL(S.Monoid, ageLens, ageCodec)
enhancedCodec := operator(baseCodec)
// Test validation - should accumulate errors
person := Person{Name: "Dave", Age: 30}
result := enhancedCodec.Decode(person)
// Should fail
assert.True(t, either.IsLeft(result), "Should fail validation")
// Extract errors
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(Person) validation.Errors { return nil },
)
// Should have errors from both base and field validation
assert.NotEmpty(t, errors, "Should have validation errors")
})
}
// Test types for ApSO
type PersonWithNickname struct {
Name string
Nickname *string
}
func TestApSO_EncodingWithPresentField(t *testing.T) {
t.Run("encodes optional field when present", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec that encodes to "Person:"
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "Person:" },
)
// Create field codec for Nickname
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO to combine encodings
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test encoding with nickname present
nickname := "Ali"
person := PersonWithNickname{Name: "Alice", Nickname: &nickname}
encoded := enhancedCodec.Encode(person)
// Should include both base and nickname
assert.Contains(t, encoded, "Person:")
assert.Contains(t, encoded, "Ali")
})
}
func TestApSO_EncodingWithAbsentField(t *testing.T) {
t.Run("omits optional field when absent", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "Person:Bob" },
)
// Create field codec
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test encoding with nickname absent
person := PersonWithNickname{Name: "Bob", Nickname: nil}
encoded := enhancedCodec.Encode(person)
// Should only have base encoding
assert.Equal(t, "Person:Bob", encoded)
})
}
func TestApSO_TypeChecking(t *testing.T) {
t.Run("preserves base type checker", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec with type checker
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "" },
)
// Create field codec
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test type checking with valid type
nickname := "Eve"
person := PersonWithNickname{Name: "Eve", Nickname: &nickname}
isResult := enhancedCodec.Is(person)
assert.True(t, either.IsRight(isResult), "Should accept PersonWithNickname type")
// Test type checking with invalid type
invalidResult := enhancedCodec.Is("not a person")
assert.True(t, either.IsLeft(invalidResult), "Should reject non-PersonWithNickname type")
})
}
func TestApSO_Naming(t *testing.T) {
t.Run("generates descriptive name", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.ToResult(validation.Success(p))
}
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected PersonWithNickname",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
if p, ok := i.(PersonWithNickname); ok {
return validation.Success(p)
}
return validation.FailureWithMessage[PersonWithNickname](i, "expected PersonWithNickname")(ctx)
}
},
func(p PersonWithNickname) string { return "" },
)
// Create field codec
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
if s, ok := i.(string); ok {
return validation.ToResult(validation.Success(s))
}
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "expected string",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
if s, ok := i.(string); ok {
return validation.Success(s)
}
return validation.FailureWithMessage[string](i, "expected string")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Check that the name includes ApS
name := enhancedCodec.Name()
assert.Contains(t, name, "ApS", "Name should contain 'ApS'")
})
}
func TestApSO_ErrorAccumulation(t *testing.T) {
t.Run("accumulates validation errors", func(t *testing.T) {
// Create an optional for PersonWithNickname.Nickname
nicknameOpt := optional.MakeOptional(
func(p PersonWithNickname) option.Option[string] {
if p.Nickname != nil {
return option.Some(*p.Nickname)
}
return option.None[string]()
},
func(p PersonWithNickname, nick string) PersonWithNickname {
p.Nickname = &nick
return p
},
)
// Create base codec that fails validation
baseCodec := MakeType(
"PersonWithNickname",
func(i any) validation.Result[PersonWithNickname] {
return validation.ToResult(validation.Failures[PersonWithNickname](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "base validation error",
},
}))
},
func(i any) Decode[Context, PersonWithNickname] {
return func(ctx Context) validation.Validation[PersonWithNickname] {
return validation.FailureWithMessage[PersonWithNickname](i, "base validation error")(ctx)
}
},
func(p PersonWithNickname) string { return "" },
)
// Create field codec that also fails
nicknameCodec := MakeType(
"Nickname",
func(i any) validation.Result[string] {
return validation.ToResult(validation.Failures[string](validation.Errors{
&validation.ValidationError{
Value: i,
Messsage: "nickname validation error",
},
}))
},
func(i any) Decode[Context, string] {
return func(ctx Context) validation.Validation[string] {
return validation.FailureWithMessage[string](i, "nickname validation error")(ctx)
}
},
F.Identity[string],
)
// Apply ApSO
operator := ApSO(S.Monoid, nicknameOpt, nicknameCodec)
enhancedCodec := operator(baseCodec)
// Test validation with present nickname - should accumulate errors
nickname := "Dave"
person := PersonWithNickname{Name: "Dave", Nickname: &nickname}
result := enhancedCodec.Decode(person)
// Should fail
assert.True(t, either.IsLeft(result), "Should fail validation")
// Extract errors
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(PersonWithNickname) validation.Errors { return nil },
)
// Should have errors from both base and field validation
assert.NotEmpty(t, errors, "Should have validation errors")
})
}

View File

@@ -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[A, B, O, I 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]) func(Type[A, O, I]) Type[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]))),
)
}

View File

@@ -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, int, int, any](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)
})
}

View File

@@ -159,7 +159,7 @@ func TestURL(t *testing.T) {
func TestDate(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, time.Time](time.Time{}))
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](time.Time{}))
t.Run("ISO 8601 date format", func(t *testing.T) {
dateCodec := Date("2006-01-02")

View File

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

View File

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

View File

@@ -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) {

View File

@@ -70,7 +70,7 @@ func TestEitherEncode(t *testing.T) {
// TestEitherDecode tests decoding/validation of Either values
func TestEitherDecode(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, either.Either[string, int]](either.Left[int]("")))
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](either.Left[int]("")))
// Create codecs that both work with string input
stringCodec := Id[string]()

View File

@@ -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"
@@ -10,12 +11,15 @@ import (
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/decoder"
"github.com/IBM/fp-go/v2/optics/encoder"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"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 (
@@ -338,4 +342,156 @@ type (
// - ApplicativeMonoid: Combines successful results using inner monoid
// - AlternativeMonoid: Combines applicative and alternative behaviors
Monoid[A any] = monoid.Monoid[A]
// Lens is an optic that focuses on a specific field within a product type S.
// It provides a way to get and set a field of type A within a structure of type S.
//
// A Lens[S, A] represents a relationship between a source type S and a focus type A,
// where the focus always exists (unlike Optional which may not exist).
//
// Lens operations:
// - Get: Extract the field value A from structure S
// - Set: Update the field value A in structure S, returning a new S
//
// Lens laws:
// 1. GetSet: If you get a value and then set it back, nothing changes
// Set(Get(s))(s) = s
// 2. SetGet: If you set a value, you can get it back
// Get(Set(a)(s)) = a
// 3. SetSet: Setting twice is the same as setting once with the final value
// Set(b)(Set(a)(s)) = Set(b)(s)
//
// In the codec context, lenses are used with ApSL to build codecs for struct fields:
// - Extract field values for encoding
// - Update field values during validation
// - Compose codec operations on nested structures
//
// Example:
// type Person struct { Name string; Age int }
//
// nameLens := lens.MakeLens(
// func(p Person) string { return p.Name },
// func(p Person, name string) Person { p.Name = name; return p },
// )
//
// // Use with ApSL to build a codec
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSL(S.Monoid, nameLens, codec.String),
// )
//
// See also:
// - ApSL: Applicative sequencing with lens
// - Optional: For fields that may not exist
Lens[S, A any] = lens.Lens[S, A]
// Optional is an optic that focuses on a field within a product type S that may not exist.
// It provides a way to get and set an optional field of type A within a structure of type S.
//
// An Optional[S, A] represents a relationship between a source type S and a focus type A,
// where the focus may or may not be present (unlike Lens where it always exists).
//
// Optional operations:
// - GetOption: Try to extract the field value, returning Option[A]
// - Set: Update the field value if it exists, returning a new S
//
// Optional laws:
// 1. GetSet (No-op on None): If GetOption returns None, Set has no effect
// GetOption(s) = None => Set(a)(s) = s
// 2. SetGet (Get what you Set): If GetOption returns Some, you can get back what you set
// GetOption(s) = Some(_) => GetOption(Set(a)(s)) = Some(a)
// 3. SetSet (Last Set Wins): Setting twice is the same as setting once with the final value
// Set(b)(Set(a)(s)) = Set(b)(s)
//
// In the codec context, optionals are used with ApSO to build codecs for optional fields:
// - Extract optional field values for encoding (only if present)
// - Update optional field values during validation
// - Handle nullable or pointer fields gracefully
// - Compose codec operations on structures with optional data
//
// Example:
// type Person struct {
// Name string
// Nickname *string // Optional field
// }
//
// nicknameOpt := optional.MakeOptional(
// func(p Person) option.Option[string] {
// if p.Nickname != nil {
// return option.Some(*p.Nickname)
// }
// return option.None[string]()
// },
// func(p Person, nick string) Person {
// p.Nickname = &nick
// return p
// },
// )
//
// // Use with ApSO to build a codec with optional field
// personCodec := F.Pipe1(
// codec.Struct[Person]("Person"),
// codec.ApSO(S.Monoid, nicknameOpt, codec.String),
// )
//
// // Encoding omits the field when absent
// p1 := Person{Name: "Alice", Nickname: nil}
// encoded := personCodec.Encode(p1) // No nickname in output
//
// See also:
// - 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
)

View File

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

View File

@@ -0,0 +1,135 @@
// 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 validate
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerresult"
)
// FromReaderResult converts a ReaderResult into a Validate.
//
// This function bridges the gap between simple error-based validation (ReaderResult)
// and the more sophisticated validation framework that supports error accumulation
// and detailed context tracking.
//
// # Type Parameters
//
// - I: The input type that the validator will receive
// - A: The output type that the validator will produce on success
//
// # Parameters
//
// - r: A ReaderResult[I, A] which is readerresult.ReaderResult[I, A]
// This represents a computation that:
// 1. Takes an input of type I
// 2. Returns Either[error, A] (success with A or failure with error)
//
// # Returns
//
// - Validate[I, A]: A validator that:
// 1. Takes an input of type I
// 2. Takes a validation Context (path through nested structures)
// 3. Returns Validation[A] (Either[Errors, A])
//
// # Behavior
//
// The conversion follows this logic:
//
// 1. Success case: If the ReaderResult succeeds with value A:
// - Wraps the value in validation.Success[A]
// - Returns a validator that always succeeds with that value
//
// 2. Failure case: If the ReaderResult fails with an error:
// - Creates a validation.ValidationError with:
// - The input value that caused the failure
// - The current validation context (path information)
// - A generic message "unable to decode"
// - The original error as the cause
// - Returns a validator that fails with this detailed error
//
// # Error Handling
//
// The function enhances simple error handling by:
// - Converting a single error into a structured validation.ValidationError
// - Preserving the original error as the cause (accessible via Unwrap())
// - Adding context information about where the error occurred
// - Making the error compatible with the validation framework's error accumulation
//
// # Example Usage
//
// Basic conversion:
//
// // A simple ReaderResult that parses an integer
// parseIntRR := result.Eitherize1(strconv.Atoi)
//
// // Convert to Validate
// validateInt := FromReaderResult[string, int](parseIntRR)
//
// // Use the validator
// result := validateInt("42")(nil) // Success(42)
// result := validateInt("abc")(nil) // Failure with ValidationError
//
// Integration with validation pipeline:
//
// // Combine with other validators
// validatePositiveInt := F.Pipe1(
// FromReaderResult[string, int](parseIntRR),
// Chain(func(n int) Validate[string, int] {
// if n > 0 {
// return Of[string](n)
// }
// return func(input string) Reader[Context, Validation[int]] {
// return validation.FailureWithMessage[int](n, "must be positive")
// }
// }),
// )
//
// # Implementation Details
//
// The function uses a functional composition approach:
//
// 1. readerresult.Map: Transforms successful results
// - Wraps the success value in validation.Success
// - Lifts it into a Reader context with reader.Of
//
// 2. readerresult.GetOrElse: Handles failures
// - Uses reader.Asks to access the validation context
// - Creates a validation.ValidationError with validation.FailureWithError
// - Uses reader.Local to adapt the context type
//
// # See Also
//
// - Validate: The target validation type
// - ReaderResult: The source type
// - validation.Success: Creates successful validations
// - validation.FailureWithError: Creates validation failures with cause
// - Context: Validation context for error reporting
func FromReaderResult[I, A any](r ReaderResult[I, A]) Validate[I, A] {
return F.Pipe2(
r,
readerresult.Map[I](F.Flow2(
validation.Success[A],
reader.Of[Context],
)),
readerresult.GetOrElse(F.Pipe1(
reader.Asks(F.Flip(F.Bind2nd(validation.FailureWithError[A], "unable to decode"))),
reader.Map[error](reader.Local[Decode[Context, A]](F.ToAny[I])),
)),
)
}

View File

@@ -0,0 +1,482 @@
// 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 validate
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestFromReaderResult_Success tests that FromReaderResult correctly converts
// a successful ReaderResult into a successful Validate
func TestFromReaderResult_Success(t *testing.T) {
t.Run("converts successful ReaderResult with integer", func(t *testing.T) {
// Create a ReaderResult that always succeeds
successRR := func(input int) result.Result[string] {
return result.Of(fmt.Sprintf("value: %d", input))
}
// Convert to Validate
validator := FromReaderResult(successRR)
// Execute the validator
validationResult := validator(42)(nil)
// Verify success
assert.Equal(t, validation.Success("value: 42"), validationResult)
})
t.Run("converts successful ReaderResult with string input", func(t *testing.T) {
// Create a ReaderResult that parses a string to int
parseIntRR := result.Eitherize1(strconv.Atoi)
// Convert to Validate
validator := FromReaderResult(parseIntRR)
// Execute with valid input
validationResult := validator("123")(nil)
// Verify success
assert.Equal(t, validation.Success(123), validationResult)
})
t.Run("converts successful ReaderResult with complex type", func(t *testing.T) {
type User struct {
Name string
Age int
}
// Create a ReaderResult that creates a User
createUserRR := func(input string) result.Result[User] {
return result.Of(User{Name: input, Age: 25})
}
// Convert to Validate
validator := FromReaderResult(createUserRR)
// Execute the validator
validationResult := validator("Alice")(nil)
// Verify success
assert.Equal(t, validation.Success(User{Name: "Alice", Age: 25}), validationResult)
})
t.Run("preserves success with empty context", func(t *testing.T) {
successRR := func(input int) result.Result[int] {
return result.Of(input * 2)
}
validator := FromReaderResult(successRR)
validationResult := validator(21)(Context{})
assert.Equal(t, validation.Success(42), validationResult)
})
t.Run("preserves success with non-empty context", func(t *testing.T) {
successRR := func(input string) result.Result[string] {
return result.Of(input + " processed")
}
validator := FromReaderResult(successRR)
ctx := Context{
{Key: "user", Type: "User"},
{Key: "name", Type: "string"},
}
validationResult := validator("test")(ctx)
assert.Equal(t, validation.Success("test processed"), validationResult)
})
}
// TestFromReaderResult_Failure tests that FromReaderResult correctly converts
// a failed ReaderResult into a failed Validate with proper error information
func TestFromReaderResult_Failure(t *testing.T) {
t.Run("converts failed ReaderResult to validation error", func(t *testing.T) {
expectedErr := errors.New("parse error")
// Create a ReaderResult that always fails
failureRR := func(input string) result.Result[int] {
return result.Left[int](expectedErr)
}
// Convert to Validate
validator := FromReaderResult(failureRR)
// Execute the validator
validationResult := validator("invalid")(nil)
// Verify failure
assert.True(t, either.IsLeft(validationResult))
errors := either.MonadFold(validationResult,
F.Identity[Errors],
func(int) Errors { return nil },
)
require.Len(t, errors, 1)
assert.Equal(t, "unable to decode", errors[0].Messsage)
assert.Equal(t, "invalid", errors[0].Value)
assert.Equal(t, expectedErr, errors[0].Cause)
})
t.Run("preserves original error as cause", func(t *testing.T) {
originalErr := fmt.Errorf("original error: %w", errors.New("root cause"))
failureRR := func(input int) result.Result[string] {
return result.Left[string](originalErr)
}
validator := FromReaderResult(failureRR)
validationResult := validator(42)(nil)
assert.True(t, either.IsLeft(validationResult))
errors := either.MonadFold(validationResult,
F.Identity[Errors],
func(string) Errors { return nil },
)
require.Len(t, errors, 1)
assert.Equal(t, originalErr, errors[0].Cause)
assert.ErrorIs(t, errors[0].Cause, originalErr)
})
t.Run("includes context in validation error", func(t *testing.T) {
failureRR := func(input string) result.Result[int] {
return result.Left[int](errors.New("conversion failed"))
}
validator := FromReaderResult(failureRR)
ctx := Context{
{Key: "user", Type: "User"},
{Key: "age", Type: "int"},
}
validationResult := validator("abc")(ctx)
assert.True(t, either.IsLeft(validationResult))
errors := either.MonadFold(validationResult,
F.Identity[Errors],
func(int) Errors { return nil },
)
require.Len(t, errors, 1)
assert.Equal(t, ctx, errors[0].Context)
assert.Equal(t, "abc", errors[0].Value)
})
t.Run("handles different error types", func(t *testing.T) {
testCases := []struct {
name string
err error
input string
}{
{
name: "simple error",
err: errors.New("simple error"),
input: "test1",
},
{
name: "formatted error",
err: fmt.Errorf("formatted error: %s", "details"),
input: "test2",
},
{
name: "wrapped error",
err: fmt.Errorf("wrapped: %w", errors.New("inner")),
input: "test3",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
failureRR := func(input string) result.Result[int] {
return result.Left[int](tc.err)
}
validator := FromReaderResult(failureRR)
validationResult := validator(tc.input)(nil)
assert.True(t, either.IsLeft(validationResult))
errors := either.MonadFold(validationResult,
F.Identity[Errors],
func(int) Errors { return nil },
)
require.Len(t, errors, 1)
assert.Equal(t, tc.err, errors[0].Cause)
assert.Equal(t, tc.input, errors[0].Value)
})
}
})
}
// TestFromReaderResult_Integration tests FromReaderResult in combination with
// other validation operations
func TestFromReaderResult_Integration(t *testing.T) {
t.Run("chains with other validators", func(t *testing.T) {
// Parse string to int
parseIntRR := result.Eitherize1(strconv.Atoi)
// Validate positive
validatePositive := func(n int) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
return func(ctx Context) Validation[int] {
if n > 0 {
return validation.Success(n)
}
return validation.FailureWithMessage[int](n, "must be positive")(ctx)
}
}
}
// Combine validators
validator := F.Pipe1(
FromReaderResult(parseIntRR),
Chain(validatePositive),
)
// Test with valid positive number
result1 := validator("42")(nil)
assert.True(t, either.IsRight(result1))
// Test with valid negative number (should fail positive check)
result2 := validator("-5")(nil)
assert.True(t, either.IsLeft(result2))
// Test with invalid string (should fail parsing)
result3 := validator("abc")(nil)
assert.True(t, either.IsLeft(result3))
})
t.Run("maps successful result", func(t *testing.T) {
parseIntRR := result.Eitherize1(strconv.Atoi)
// Convert and map to double the value
validator := F.Pipe1(
FromReaderResult(parseIntRR),
Map[string](func(n int) int { return n * 2 }),
)
validationResult := validator("21")(nil)
assert.Equal(t, validation.Success(42), validationResult)
})
t.Run("composes with Do and Bind", func(t *testing.T) {
type State struct {
parsed int
valid bool
}
parseIntRR := result.Eitherize1(strconv.Atoi)
validator := F.Pipe2(
Do[string](State{}),
Bind(func(p int) func(State) State {
return func(s State) State { s.parsed = p; return s }
}, func(s State) Validate[string, int] {
return FromReaderResult(parseIntRR)
}),
Let[string](func(v bool) func(State) State {
return func(s State) State { s.valid = v; return s }
}, func(s State) bool {
return s.parsed > 0
}),
)
result := validator("42")(nil)
assert.Equal(t, validation.Success(State{parsed: 42, valid: true}), result)
})
}
// TestFromReaderResult_EdgeCases tests edge cases and boundary conditions
func TestFromReaderResult_EdgeCases(t *testing.T) {
t.Run("handles nil context", func(t *testing.T) {
successRR := func(input int) result.Result[int] {
return result.Of(input)
}
validator := FromReaderResult(successRR)
validationResult := validator(42)(nil)
assert.True(t, either.IsRight(validationResult))
})
t.Run("handles empty input", func(t *testing.T) {
identityRR := func(input string) result.Result[string] {
return result.Of(input)
}
validator := FromReaderResult(identityRR)
validationResult := validator("")(nil)
assert.Equal(t, validation.Success(""), validationResult)
})
t.Run("handles zero values", func(t *testing.T) {
identityRR := func(input int) result.Result[int] {
return result.Of(input)
}
validator := FromReaderResult(identityRR)
validationResult := validator(0)(nil)
assert.Equal(t, validation.Success(0), validationResult)
})
t.Run("handles pointer types", func(t *testing.T) {
type Data struct {
Value int
}
createDataRR := func(input int) result.Result[*Data] {
return result.Of(&Data{Value: input})
}
validator := FromReaderResult(createDataRR)
validationResult := validator(42)(nil)
assert.True(t, either.IsRight(validationResult))
data := either.MonadFold(validationResult,
func(Errors) *Data { return nil },
F.Identity[*Data],
)
require.NotNil(t, data)
assert.Equal(t, 42, data.Value)
})
t.Run("handles slice types", func(t *testing.T) {
splitRR := func(input string) result.Result[[]string] {
if input == "" {
return result.Left[[]string](errors.New("empty input"))
}
return result.Of([]string{input, input})
}
validator := FromReaderResult(splitRR)
validationResult := validator("test")(nil)
assert.Equal(t, validation.Success([]string{"test", "test"}), validationResult)
})
t.Run("handles map types", func(t *testing.T) {
createMapRR := func(input string) result.Result[map[string]int] {
return result.Of(map[string]int{input: len(input)})
}
validator := FromReaderResult(createMapRR)
validationResult := validator("hello")(nil)
assert.Equal(t, validation.Success(map[string]int{"hello": 5}), validationResult)
})
}
// TestFromReaderResult_TypeSafety tests that the function maintains type safety
func TestFromReaderResult_TypeSafety(t *testing.T) {
t.Run("maintains input type", func(t *testing.T) {
// This test verifies that the input type is preserved
intToStringRR := func(input int) result.Result[string] {
return result.Of(fmt.Sprintf("%d", input))
}
validator := FromReaderResult(intToStringRR)
// This should compile and work correctly
validationResult := validator(42)(nil)
assert.Equal(t, validation.Success("42"), validationResult)
})
t.Run("maintains output type", func(t *testing.T) {
// This test verifies that the output type is preserved
stringToIntRR := result.Eitherize1(strconv.Atoi)
validator := FromReaderResult(stringToIntRR)
validationResult := validator("42")(nil)
// The result should be Validation[int]
assert.Equal(t, validation.Success(42), validationResult)
})
t.Run("works with different type combinations", func(t *testing.T) {
type Input struct{ Value string }
type Output struct{ Result int }
transformRR := result.Eitherize1(func(input Input) (Output, error) {
val, err := strconv.Atoi(input.Value)
if err != nil {
return Output{}, err
}
return Output{Result: val}, nil
})
validator := FromReaderResult(transformRR)
validationResult := validator(Input{Value: "42"})(nil)
assert.Equal(t, validation.Success(Output{Result: 42}), validationResult)
})
}
// BenchmarkFromReaderResult_Success benchmarks the success path
func BenchmarkFromReaderResult_Success(b *testing.B) {
successRR := func(input int) result.Result[int] {
return result.Of(input * 2)
}
validator := FromReaderResult(successRR)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(42)(nil)
}
}
// BenchmarkFromReaderResult_Failure benchmarks the failure path
func BenchmarkFromReaderResult_Failure(b *testing.B) {
failureRR := func(input int) result.Result[int] {
return result.Left[int](errors.New("error"))
}
validator := FromReaderResult(failureRR)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(42)(nil)
}
}
// BenchmarkFromReaderResult_WithContext benchmarks with context
func BenchmarkFromReaderResult_WithContext(b *testing.B) {
successRR := func(input int) result.Result[int] {
return result.Of(input * 2)
}
validator := FromReaderResult(successRR)
ctx := Context{
{Key: "user", Type: "User"},
{Key: "age", Type: "int"},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = validator(42)(ctx)
}
}

View File

@@ -26,7 +26,7 @@ func TestMonadChainLeft(t *testing.T) {
handler := func(errs Errors) Validate[string, int] {
for _, err := range errs {
if err.Messsage == "validation failed" {
return Of[string, int](0) // recover with default
return Of[string](0) // recover with default
}
}
return func(input string) Reader[Context, Validation[int]] {
@@ -43,7 +43,7 @@ func TestMonadChainLeft(t *testing.T) {
})
t.Run("preserves success values unchanged", func(t *testing.T) {
successValidator := Of[string, int](42)
successValidator := Of[string](42)
handler := func(errs Errors) Validate[string, int] {
return func(input string) Reader[Context, Validation[int]] {
@@ -145,7 +145,7 @@ func TestMonadChainLeft(t *testing.T) {
}
handler := func(errs Errors) Validate[Config, string] {
return Of[Config, string]("default-value")
return Of[Config]("default-value")
}
validator := MonadChainLeft(failingValidator, handler)
@@ -194,7 +194,7 @@ func TestMonadChainLeft(t *testing.T) {
}
handler := func(errs Errors) Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
// MonadChainLeft - direct application
@@ -229,7 +229,7 @@ func TestMonadChainLeft(t *testing.T) {
// Check if we can recover
for _, err := range errs {
if err.Messsage == "error1" {
return Of[string, int](100) // recover
return Of[string](100) // recover
}
}
return func(input string) Reader[Context, Validation[int]] {
@@ -248,12 +248,12 @@ func TestMonadChainLeft(t *testing.T) {
})
t.Run("does not call handler on success", func(t *testing.T) {
successValidator := Of[string, int](42)
successValidator := Of[string](42)
handlerCalled := false
handler := func(errs Errors) Validate[string, int] {
handlerCalled = true
return Of[string, int](0)
return Of[string](0)
}
validator := MonadChainLeft(successValidator, handler)
@@ -267,9 +267,9 @@ func TestMonadChainLeft(t *testing.T) {
// TestMonadAlt tests the MonadAlt function
func TestMonadAlt(t *testing.T) {
t.Run("returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
return Of[string](100)
}
result := MonadAlt(validator1, validator2)("input")(nil)
@@ -285,7 +285,7 @@ func TestMonadAlt(t *testing.T) {
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
result := MonadAlt(failing, fallback)("input")(nil)
@@ -328,11 +328,11 @@ func TestMonadAlt(t *testing.T) {
})
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
evaluated := false
validator2 := func() Validate[string, int] {
evaluated = true
return Of[string, int](100)
return Of[string](100)
}
result := MonadAlt(validator1, validator2)("input")(nil)
@@ -349,7 +349,7 @@ func TestMonadAlt(t *testing.T) {
}
}
fallback := func() Validate[string, string] {
return Of[string, string]("fallback")
return Of[string]("fallback")
}
result := MonadAlt(failing, fallback)("input")(nil)
@@ -374,7 +374,7 @@ func TestMonadAlt(t *testing.T) {
}
}
succeeding := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
// Chain: try failing1, then failing2, then succeeding
@@ -395,7 +395,7 @@ func TestMonadAlt(t *testing.T) {
}
}
fallback := func() Validate[Config, string] {
return Of[Config, string]("default")
return Of[Config]("default")
}
result := MonadAlt(failing, fallback)(Config{Port: 9999})(nil)
@@ -458,9 +458,9 @@ func TestMonadAlt(t *testing.T) {
// TestAlt tests the Alt function
func TestAlt(t *testing.T) {
t.Run("returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
return Of[string](100)
}
withAlt := Alt(validator2)
@@ -477,7 +477,7 @@ func TestAlt(t *testing.T) {
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
withAlt := Alt(fallback)
@@ -522,11 +522,11 @@ func TestAlt(t *testing.T) {
})
t.Run("does not evaluate second validator when first succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
evaluated := false
validator2 := func() Validate[string, int] {
evaluated = true
return Of[string, int](100)
return Of[string](100)
}
withAlt := Alt(validator2)
@@ -553,7 +553,7 @@ func TestAlt(t *testing.T) {
}
}
succeeding := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
// Use F.Pipe to chain alternatives
@@ -576,7 +576,7 @@ func TestAlt(t *testing.T) {
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
// Alt - curried for pipelines
@@ -592,9 +592,9 @@ func TestAlt(t *testing.T) {
// TestMonadAltAndAltEquivalence tests that MonadAlt and Alt are equivalent
func TestMonadAltAndAltEquivalence(t *testing.T) {
t.Run("both produce same results for success", func(t *testing.T) {
validator1 := Of[string, int](42)
validator1 := Of[string](42)
validator2 := func() Validate[string, int] {
return Of[string, int](100)
return Of[string](100)
}
resultMonadAlt := MonadAlt(validator1, validator2)("input")(nil)
@@ -612,7 +612,7 @@ func TestMonadAltAndAltEquivalence(t *testing.T) {
}
}
fallback := func() Validate[string, int] {
return Of[string, int](42)
return Of[string](42)
}
resultMonadAlt := MonadAlt(failing, fallback)("input")(nil)

View File

@@ -15,7 +15,7 @@ import (
// TestAlternativeMonoid tests the AlternativeMonoid function
func TestAlternativeMonoid(t *testing.T) {
t.Run("with string monoid", func(t *testing.T) {
m := AlternativeMonoid[string, string](S.Monoid)
m := AlternativeMonoid[string](S.Monoid)
t.Run("empty returns validator that succeeds with empty string", func(t *testing.T) {
empty := m.Empty()
@@ -25,8 +25,8 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("concat combines successful validators using monoid", func(t *testing.T) {
validator1 := Of[string, string]("Hello")
validator2 := Of[string, string](" World")
validator1 := Of[string]("Hello")
validator2 := Of[string](" World")
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
@@ -42,7 +42,7 @@ func TestAlternativeMonoid(t *testing.T) {
})
}
}
succeeding := Of[string, string]("fallback")
succeeding := Of[string]("fallback")
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
@@ -85,7 +85,7 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("concat with empty preserves validator", func(t *testing.T) {
validator := Of[string, string]("test")
validator := Of[string]("test")
empty := m.Empty()
result1 := m.Concat(validator, empty)("input")(nil)
@@ -110,7 +110,7 @@ func TestAlternativeMonoid(t *testing.T) {
func(a, b int) int { return a + b },
0,
)
m := AlternativeMonoid[string, int](intMonoid)
m := AlternativeMonoid[string](intMonoid)
t.Run("empty returns validator with zero", func(t *testing.T) {
empty := m.Empty()
@@ -124,8 +124,8 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("concat combines decoded values when both succeed", func(t *testing.T) {
validator1 := Of[string, int](10)
validator2 := Of[string, int](32)
validator1 := Of[string](10)
validator2 := Of[string](32)
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
@@ -145,7 +145,7 @@ func TestAlternativeMonoid(t *testing.T) {
})
}
}
succeeding := Of[string, int](42)
succeeding := Of[string](42)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
@@ -158,10 +158,10 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("multiple concat operations", func(t *testing.T) {
validator1 := Of[string, int](1)
validator2 := Of[string, int](2)
validator3 := Of[string, int](3)
validator4 := Of[string, int](4)
validator1 := Of[string](1)
validator2 := Of[string](2)
validator3 := Of[string](3)
validator4 := Of[string](4)
combined := m.Concat(m.Concat(m.Concat(validator1, validator2), validator3), validator4)
result := combined("input")(nil)
@@ -175,11 +175,11 @@ func TestAlternativeMonoid(t *testing.T) {
})
t.Run("satisfies monoid laws", func(t *testing.T) {
m := AlternativeMonoid[string, string](S.Monoid)
m := AlternativeMonoid[string](S.Monoid)
validator1 := Of[string, string]("a")
validator2 := Of[string, string]("b")
validator3 := Of[string, string]("c")
validator1 := Of[string]("a")
validator2 := Of[string]("b")
validator3 := Of[string]("c")
t.Run("left identity", func(t *testing.T) {
result := m.Concat(m.Empty(), validator1)("input")(nil)
@@ -222,7 +222,7 @@ func TestAlternativeMonoid(t *testing.T) {
func TestAltMonoid(t *testing.T) {
t.Run("with default value as zero", func(t *testing.T) {
m := AltMonoid(func() Validate[string, int] {
return Of[string, int](0)
return Of[string](0)
})
t.Run("empty returns the provided zero validator", func(t *testing.T) {
@@ -233,8 +233,8 @@ func TestAltMonoid(t *testing.T) {
})
t.Run("concat returns first validator when it succeeds", func(t *testing.T) {
validator1 := Of[string, int](42)
validator2 := Of[string, int](100)
validator1 := Of[string](42)
validator2 := Of[string](100)
combined := m.Concat(validator1, validator2)
result := combined("input")(nil)
@@ -250,7 +250,7 @@ func TestAltMonoid(t *testing.T) {
})
}
}
succeeding := Of[string, int](42)
succeeding := Of[string](42)
combined := m.Concat(failing, succeeding)
result := combined("input")(nil)
@@ -341,7 +341,7 @@ func TestAltMonoid(t *testing.T) {
t.Run("chaining multiple fallbacks", func(t *testing.T) {
m := AltMonoid(func() Validate[string, string] {
return Of[string, string]("default")
return Of[string]("default")
})
primary := func(input string) Reader[Context, Validation[string]] {
@@ -358,7 +358,7 @@ func TestAltMonoid(t *testing.T) {
})
}
}
tertiary := Of[string, string]("tertiary value")
tertiary := Of[string]("tertiary value")
combined := m.Concat(m.Concat(primary, secondary), tertiary)
result := combined("input")(nil)
@@ -369,14 +369,14 @@ func TestAltMonoid(t *testing.T) {
t.Run("difference from AlternativeMonoid", func(t *testing.T) {
// AltMonoid - first success wins
altM := AltMonoid(func() Validate[string, int] {
return Of[string, int](0)
return Of[string](0)
})
// AlternativeMonoid - combines successes
altMonoid := AlternativeMonoid[string, int](N.MonoidSum[int]())
altMonoid := AlternativeMonoid[string](N.MonoidSum[int]())
validator1 := Of[string, int](10)
validator2 := Of[string, int](32)
validator1 := Of[string](10)
validator2 := Of[string](32)
// AltMonoid: returns first success (10)
result1 := altM.Concat(validator1, validator2)("input")(nil)

View File

@@ -22,9 +22,11 @@ import (
"github.com/IBM/fp-go/v2/optics/codec/decode"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerresult"
)
type (
ReaderResult[R, A any] = readerresult.ReaderResult[R, A]
// Monoid represents an algebraic structure with an associative binary operation
// and an identity element. Used for combining values of type A.

View File

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

View File

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

View File

@@ -1405,7 +1405,7 @@ func TestFromResult(t *testing.T) {
t.Run("extract from successful result", func(t *testing.T) {
prism := FromResult[int]()
success := result.Of[int](42)
success := result.Of(42)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
@@ -1435,7 +1435,7 @@ func TestFromResult(t *testing.T) {
t.Run("works with string type", func(t *testing.T) {
prism := FromResult[string]()
success := result.Of[string]("hello")
success := result.Of("hello")
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
@@ -1451,7 +1451,7 @@ func TestFromResult(t *testing.T) {
prism := FromResult[Person]()
person := Person{Name: "Alice", Age: 30}
success := result.Of[Person](person)
success := result.Of(person)
extracted := prism.GetOption(success)
assert.True(t, O.IsSome(extracted))
@@ -1465,9 +1465,9 @@ func TestFromResult(t *testing.T) {
func TestFromResultWithSet(t *testing.T) {
t.Run("set on successful result", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
setter := Set[result.Result[int]](200)
success := result.Of[int](42)
success := result.Of(42)
updated := setter(prism)(success)
// Verify the value was updated
@@ -1478,7 +1478,7 @@ func TestFromResultWithSet(t *testing.T) {
t.Run("set on error result leaves it unchanged", func(t *testing.T) {
prism := FromResult[int]()
setter := Set[result.Result[int], int](200)
setter := Set[result.Result[int]](200)
failure := E.Left[int](errors.New("test error"))
updated := setter(prism)(failure)
@@ -1527,13 +1527,13 @@ func TestFromResultComposition(t *testing.T) {
composed := Compose[result.Result[int]](positivePrism)(FromResult[int]())
// Test with positive number
success := result.Of[int](42)
success := result.Of(42)
extracted := composed.GetOption(success)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 42, O.GetOrElse(F.Constant(-1))(extracted))
// Test with negative number
negativeSuccess := result.Of[int](-5)
negativeSuccess := result.Of(-5)
extracted = composed.GetOption(negativeSuccess)
assert.True(t, O.IsNone(extracted))
@@ -1705,7 +1705,7 @@ func TestParseJSONWithSet(t *testing.T) {
originalJSON := []byte(`{"name":"Alice","age":30}`)
newPerson := Person{Name: "Bob", Age: 25}
setter := Set[[]byte, Person](newPerson)
setter := Set[[]byte](newPerson)
updatedJSON := setter(prism)(originalJSON)
// Parse the updated JSON
@@ -1722,7 +1722,7 @@ func TestParseJSONWithSet(t *testing.T) {
invalidJSON := []byte(`{invalid}`)
newPerson := Person{Name: "Charlie", Age: 35}
setter := Set[[]byte, Person](newPerson)
setter := Set[[]byte](newPerson)
result := setter(prism)(invalidJSON)
// Should return original unchanged since it couldn't be parsed

View File

@@ -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).

View File

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

View File

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

View File

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

View File

@@ -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]
)

View File

@@ -630,7 +630,7 @@ func TestLocalIOK(t *testing.T) {
}
// Compose using LocalIOK
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(useConfig)
adapted := LocalIOK[string](loadConfig)(useConfig)
result := adapted("config.json")()
assert.Equal(t, "localhost:8080", result)
@@ -650,7 +650,7 @@ func TestLocalIOK(t *testing.T) {
return io.Of(fmt.Sprintf("Processed: %d", n))
}
adapted := LocalIOK[string, int, string](loadData)(processData)
adapted := LocalIOK[string](loadData)(processData)
result := adapted("test")()
assert.Equal(t, "Processed: 40", result)
@@ -679,8 +679,8 @@ func TestLocalIOK(t *testing.T) {
}
// Compose transformations
step1 := LocalIOK[string, UserEnv, int](loadUser)(formatUser)
step2 := LocalIOK[string, int, string](parseID)(step1)
step1 := LocalIOK[string](loadUser)(formatUser)
step2 := LocalIOK[string](parseID)(step1)
result := step2("42")()
assert.Equal(t, "User ID: 42", result)
@@ -704,7 +704,7 @@ func TestLocalIOK(t *testing.T) {
return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
}
adapted := LocalIOK[string, DatabaseConfig, AppConfig](extractDB)(connectDB)
adapted := LocalIOK[string](extractDB)(connectDB)
result := adapted(AppConfig{
Database: DatabaseConfig{Host: "", Port: 5432},
})()
@@ -735,8 +735,8 @@ func TestLocalIOK(t *testing.T) {
}
// Compose the pipeline
step1 := LocalIOK[string, SimpleConfig, string](parseConfig)(useConfig)
step2 := LocalIOK[string, string, ConfigFile](readFile)(step1)
step1 := LocalIOK[string](parseConfig)(useConfig)
step2 := LocalIOK[string](readFile)(step1)
result := step2(ConfigFile{Path: "app.json"})()
assert.Equal(t, "Using example.com:9000", result)

View File

@@ -149,7 +149,7 @@ func TestLocalIOK(t *testing.T) {
}
// Compose using LocalIOK
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(useConfig)
adapted := LocalIOK[string, string](loadConfig)(useConfig)
result := adapted("config.json")()
assert.Equal(t, E.Of[string]("Port: 8080"), result)
@@ -169,7 +169,7 @@ func TestLocalIOK(t *testing.T) {
return IOE.Of[string]("Processed: " + strconv.Itoa(n))
}
adapted := LocalIOK[string, string, int, string](loadData)(processData)
adapted := LocalIOK[string, string](loadData)(processData)
result := adapted("test")()
assert.Equal(t, E.Of[string]("Processed: 40"), result)
@@ -188,7 +188,7 @@ func TestLocalIOK(t *testing.T) {
return IOE.Left[string]("operation failed")
}
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(failingOperation)
adapted := LocalIOK[string, string](loadConfig)(failingOperation)
result := adapted("config.json")()
assert.Equal(t, E.Left[string]("operation failed"), result)
@@ -216,8 +216,8 @@ func TestLocalIOK(t *testing.T) {
}
// Compose transformations
step1 := LocalIOK[string, string, SimpleConfig, int](loadConfig)(formatConfig)
step2 := LocalIOK[string, string, int, string](parseID)(step1)
step1 := LocalIOK[string, string](loadConfig)(formatConfig)
step2 := LocalIOK[string, string](parseID)(step1)
result := step2("42")()
assert.Equal(t, E.Of[string]("Port: 8042"), result)
@@ -243,7 +243,7 @@ func TestLocalIOEitherK(t *testing.T) {
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
// Success case
result := adapted("config.json")()
@@ -265,7 +265,7 @@ func TestLocalIOEitherK(t *testing.T) {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
result := adapted("missing.json")()
// Error from loadConfig should propagate
@@ -282,7 +282,7 @@ func TestLocalIOEitherK(t *testing.T) {
return IOE.Left[string]("operation failed")
}
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(failingOperation)
adapted := LocalIOEitherK[string](loadConfig)(failingOperation)
result := adapted("config.json")()
// Error from ReaderIOEither should propagate
@@ -317,8 +317,8 @@ func TestLocalIOEitherK(t *testing.T) {
}
// Compose transformations
step1 := LocalIOEitherK[string, SimpleConfig, int, string](loadConfig)(formatConfig)
step2 := LocalIOEitherK[string, int, string, string](parseID)(step1)
step1 := LocalIOEitherK[string](loadConfig)(formatConfig)
step2 := LocalIOEitherK[string](parseID)(step1)
// Success case
result := step2("42")()
@@ -364,8 +364,8 @@ func TestLocalIOEitherK(t *testing.T) {
}
// Compose the pipeline
step1 := LocalIOEitherK[string, SimpleConfig, string, string](parseConfig)(useConfig)
step2 := LocalIOEitherK[string, string, ConfigFile, string](readFile)(step1)
step1 := LocalIOEitherK[string](parseConfig)(useConfig)
step2 := LocalIOEitherK[string](readFile)(step1)
// Success case
result := step2(ConfigFile{Path: "app.json"})()

View File

@@ -37,11 +37,46 @@ import (
"github.com/IBM/fp-go/v2/readeroption"
)
// FromReaderOption converts a ReaderOption to a Kleisli arrow that handles None cases.
// When the Option is None, the provided lazy error value is used.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The value type
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Kleisli arrow that converts ReaderOption to ReaderIOEither
//
//go:inline
func FromReaderOption[R, A, E any](onNone Lazy[E]) Kleisli[R, E, ReaderOption[R, A], A] {
return function.Bind2nd(function.Flow2[ReaderOption[R, A], IOE.Kleisli[E, Option[A], A]], IOE.FromOption[A](onNone))
}
// FromReaderIO lifts a ReaderIO into a ReaderIOEither, placing the result in the Right side.
// This is an alias for RightReaderIO, converting a computation that cannot fail into one
// that can fail but never does.
//
// # Type Parameters
//
// - E: The error type (will never actually contain an error)
// - R: The context/environment type
// - A: The value type
//
// # Parameters
//
// - ma: The ReaderIO to lift
//
// # Returns
//
// - ReaderIOEither with the ReaderIO result in the Right side
//
//go:inline
func FromReaderIO[E, R, A any](ma ReaderIO[R, A]) ReaderIOEither[R, E, A] {
return RightReaderIO[E](ma)
@@ -121,6 +156,26 @@ func MonadChainFirst[R, E, A, B any](fa ReaderIOEither[R, E, A], f Kleisli[R, E,
f)
}
// MonadTap is an alias for MonadChainFirst.
// It sequences two computations but keeps the result of the first, emphasizing the
// side-effect nature of the operation (like "tapping" into a pipeline).
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - fa: The ReaderIOEither computation
// - f: The side effect Kleisli arrow
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTap[R, E, A, B any](fa ReaderIOEither[R, E, A], f Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirst(fa, f)
@@ -165,6 +220,26 @@ func MonadChainFirstEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f either
)
}
// MonadTapEitherK is an alias for MonadChainFirstEitherK.
// It chains an Either-returning computation while preserving the original value,
// emphasizing the side-effect nature of the operation.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The Either-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f either.Kleisli[E, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstEitherK(ma, f)
@@ -183,6 +258,25 @@ func ChainFirstEitherK[R, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, E,
)
}
// TapEitherK is an alias for ChainFirstEitherK.
// It returns a function that chains an Either-returning side effect while preserving
// the original value, emphasizing the "tap" pattern for observing values.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The Either-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapEitherK[R, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, E, A, A] {
return ChainFirstEitherK[R](f)
@@ -213,6 +307,26 @@ func ChainReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E, A, B
)
}
// MonadChainFirstReaderK chains a Reader-returning computation while preserving the original value.
// Useful for performing Reader-based side effects (like logging with context) while keeping
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The Reader-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadChainFirstReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -223,6 +337,25 @@ func MonadChainFirstReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader
)
}
// MonadTapReaderK is an alias for MonadChainFirstReaderK.
// It chains a Reader-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The Reader-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstReaderK(ma, f)
@@ -240,11 +373,49 @@ func ChainFirstReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E,
)
}
// TapReaderK is an alias for ChainFirstReaderK.
// It returns a function that chains a Reader-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The Reader-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderK[E](f)
}
// MonadChainReaderIOK chains a ReaderIO-returning computation into a ReaderIOEither.
// The ReaderIO is automatically lifted into the ReaderIOEither context.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderIO-returning function
//
// # Returns
//
// - ReaderIOEither with the result of the ReaderIO computation
//
//go:inline
func MonadChainReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, B] {
return fromreader.MonadChainReaderK(
@@ -255,6 +426,24 @@ func MonadChainReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.
)
}
// ChainReaderIOK returns a function that chains a ReaderIO-returning function into ReaderIOEither.
// This is the curried version of MonadChainReaderIOK.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: The ReaderIO-returning function
//
// # Returns
//
// - Operator that chains the ReaderIO computation
//
//go:inline
func ChainReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, B] {
return fromreader.ChainReaderK(
@@ -264,6 +453,25 @@ func ChainReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E,
)
}
// MonadChainFirstReaderIOK chains a ReaderIO-returning computation while preserving the original value.
// Useful for performing ReaderIO-based side effects while keeping the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadChainFirstReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -274,11 +482,48 @@ func MonadChainFirstReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f read
)
}
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK.
// It chains a ReaderIO-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstReaderIOK(ma, f)
}
// ChainFirstReaderIOK returns a function that chains a ReaderIO-returning function while
// preserving the original value. This is the curried version of MonadChainFirstReaderIOK.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func ChainFirstReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, A] {
return fromreader.ChainFirstReaderK(
@@ -288,11 +533,49 @@ func ChainFirstReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R
)
}
// TapReaderIOK is an alias for ChainFirstReaderIOK.
// It returns a function that chains a ReaderIO-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderIOK[E](f)
}
// MonadChainReaderEitherK chains a ReaderEither-returning computation into a ReaderIOEither.
// The ReaderEither is automatically lifted into the ReaderIOEither context.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderEither-returning function
//
// # Returns
//
// - ReaderIOEither with the result of the ReaderEither computation
//
//go:inline
func MonadChainReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, B] {
return fromreader.MonadChainReaderK(
@@ -315,6 +598,25 @@ func ChainReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, E,
)
}
// MonadChainFirstReaderEitherK chains a ReaderEither-returning computation while preserving the original value.
// Useful for performing ReaderEither-based side effects while keeping the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderEither-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadChainFirstReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -325,6 +627,25 @@ func MonadChainFirstReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f
)
}
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK.
// It chains a ReaderEither-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderEither-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstReaderEitherK(ma, f)
@@ -342,11 +663,48 @@ func ChainFirstReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[
)
}
// TapReaderEitherK is an alias for ChainFirstReaderEitherK.
// It returns a function that chains a ReaderEither-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The ReaderEither-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderEitherK(f)
}
// ChainReaderOptionK returns a function that chains a ReaderOption-returning function into ReaderIOEither.
// When the Option is None, the provided error value is used.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The input value type
// - B: The output value type
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Function that takes a ReaderOption Kleisli and returns an Operator
//
//go:inline
func ChainReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, B] {
fro := FromReaderOption[R, B](onNone)
@@ -359,6 +717,24 @@ func ChainReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisl
}
}
// ChainFirstReaderOptionK returns a function that chains a ReaderOption-returning function
// while preserving the original value. When the Option is None, the provided error value is used.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Function that takes a ReaderOption Kleisli and returns an Operator
//
//go:inline
func ChainFirstReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
fro := FromReaderOption[R, B](onNone)
@@ -371,6 +747,25 @@ func ChainFirstReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.K
}
}
// TapReaderOptionK is an alias for ChainFirstReaderOptionK.
// It returns a function that chains a ReaderOption-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Function that takes a ReaderOption Kleisli and returns an Operator
//
//go:inline
func TapReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderOptionK[R, A, B](onNone)
@@ -401,6 +796,110 @@ func ChainIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, B]
)
}
// ChainFirstIOEitherK chains an IOEither computation while preserving the original value.
// This is useful for performing side effects that may fail (like logging, validation, or
// external API calls) while keeping the original value in the success case.
//
// The function executes the IOEither computation but discards its result, returning the
// original value if both computations succeed. If either computation fails, the error
// is propagated.
//
// This is the curried version that returns an Operator for use in function composition.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: IOEither.Kleisli function that performs the side effect
//
// # Returns
//
// - Operator that chains the side effect while preserving the original value
//
// # Example Usage
//
// type Config struct{ LogEnabled bool }
//
// logValue := func(v int) IOEither[error, string] {
// return IOE.Of[error](fmt.Sprintf("Value: %d", v))
// }
//
// pipeline := F.Pipe1(
// Of[Config, error](42),
// ChainFirstIOEitherK[Config](logValue),
// )
// result := pipeline(Config{LogEnabled: true})() // Right(42)
//
// # See Also
//
// - TapIOEitherK: Alias for ChainFirstIOEitherK
// - ChainIOEitherK: Chains IOEither and uses its result
// - ChainFirstEitherK: Similar but for Either computations
//
//go:inline
func ChainFirstIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, A] {
return fromioeither.ChainFirstIOEitherK(
Chain[R, E, A, A],
Map[R, E, B, A],
FromIOEither[R, E, B],
f,
)
}
// TapIOEitherK is an alias for ChainFirstIOEitherK.
// It executes an IOEither side effect while preserving the original value.
//
// The name "Tap" emphasizes the side-effect nature of the operation, similar to
// tapping into a pipeline to observe or log values without modifying the flow.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: IOEither.Kleisli function that performs the side effect
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
// # Example Usage
//
// type Config struct{}
//
// validatePositive := func(v int) IOEither[error, bool] {
// if v > 0 {
// return IOE.Of[error](true)
// }
// return IOE.Left[bool](errors.New("must be positive"))
// }
//
// pipeline := F.Pipe1(
// Of[Config, error](42),
// TapIOEitherK[Config](validatePositive),
// )
// result := pipeline(Config{})() // Right(42) if validation passes
//
// # See Also
//
// - ChainFirstIOEitherK: The underlying implementation
// - TapEitherK: Similar but for Either computations
// - TapIOK: Similar but for IO computations
//
//go:inline
func TapIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, A] {
return ChainFirstIOEitherK[R](f)
}
// MonadChainIOK chains an IO-returning computation into a ReaderIOEither.
// The IO is automatically lifted into the ReaderIOEither context (always succeeds).
//
@@ -440,6 +939,25 @@ func MonadChainFirstIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f io.Kleisli
)
}
// MonadTapIOK is an alias for MonadChainFirstIOK.
// It chains an IO-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The IO-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f io.Kleisli[A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstIOK(ma, f)
@@ -458,6 +976,25 @@ func ChainFirstIOK[R, E, A, B any](f io.Kleisli[A, B]) Operator[R, E, A, A] {
)
}
// TapIOK is an alias for ChainFirstIOK.
// It returns a function that chains an IO-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The IO-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapIOK[R, E, A, B any](f io.Kleisli[A, B]) Operator[R, E, A, A] {
return ChainFirstIOK[R, E](f)
@@ -540,6 +1077,25 @@ func ChainFirst[R, E, A, B any](f Kleisli[R, E, A, B]) Operator[R, E, A, A] {
f)
}
// Tap is an alias for ChainFirst.
// It returns a function that sequences computations but keeps the first result,
// emphasizing the side-effect nature of the operation.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The Kleisli arrow for the side effect
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func Tap[R, E, A, B any](f Kleisli[R, E, A, B]) Operator[R, E, A, A] {
return ChainFirst(f)
@@ -667,12 +1223,12 @@ func FromPredicate[R, E, A any](pred func(A) bool, onFalse func(A) E) func(A) Re
// This is useful for converting a ReaderIOEither into a ReaderIO by handling all cases.
//
//go:inline
func Fold[R, E, A, B any](onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOEither[R, E, A]) ReaderIO[R, B] {
func Fold[R, E, A, B any](onLeft readerio.Kleisli[R, E, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOEither[R, E, A]) ReaderIO[R, B] {
return eithert.MatchE(readerio.MonadChain[R, either.Either[E, A], B], onLeft, onRight)
}
//go:inline
func MonadFold[R, E, A, B any](ma ReaderIOEither[R, E, A], onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) ReaderIO[R, B] {
func MonadFold[R, E, A, B any](ma ReaderIOEither[R, E, A], onLeft readerio.Kleisli[R, E, B], onRight func(A) ReaderIO[R, B]) ReaderIO[R, B] {
return eithert.FoldE(readerio.MonadChain[R, either.Either[E, A], B], ma, onLeft, onRight)
}
@@ -680,7 +1236,7 @@ func MonadFold[R, E, A, B any](ma ReaderIOEither[R, E, A], onLeft func(E) Reader
// The default is computed lazily via a ReaderIO.
//
//go:inline
func GetOrElse[R, E, A any](onLeft func(E) ReaderIO[R, A]) func(ReaderIOEither[R, E, A]) ReaderIO[R, A] {
func GetOrElse[R, E, A any](onLeft readerio.Kleisli[R, E, A]) func(ReaderIOEither[R, E, A]) ReaderIO[R, A] {
return eithert.GetOrElse(readerio.MonadChain[R, either.Either[E, A], A], readerio.Of[R, A], onLeft)
}

Some files were not shown because too many files have changed in this diff Show More