1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-09 23:11:40 +02:00

Compare commits

...

23 Commits

Author SHA1 Message Date
Dr. Carsten Leue
8a2e9539b1 fix: add result
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-17 20:36:06 +01:00
Dr. Carsten Leue
03d9720a29 fix: optimize performance for option
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-17 12:19:24 +01:00
Dr. Carsten Leue
57794ccb34 fix: add idiomatic go options package
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-17 11:10:27 +01:00
Dr. Carsten Leue
404eb875d3 fix: add idiomatic version
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-16 17:27:16 +01:00
Dr. Carsten Leue
ed108812d6 fix: modernize codebase
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-15 17:00:22 +01:00
Dr. Carsten Leue
ab868315d4 fix: traverse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-15 12:13:37 +01:00
Dr. Carsten Leue
02d0be9dad fix: add traversal for sequences
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-14 14:12:44 +01:00
Dr. Carsten Leue
2c1d8196b4 fix: support go iterators and cleanup types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-14 12:56:12 +01:00
Dr. Carsten Leue
17eb8ae66f fix: add Chain...Left methods
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 16:51:15 +01:00
Dr. Carsten Leue
b70e481e7d fix: some minor improvements
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 12:56:51 +01:00
Dr. Carsten Leue
3c3bb7c166 fix: improve lens implementation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 12:15:52 +01:00
Dr. Carsten Leue
d3007cbbfa fix: improve lens generator
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 09:39:18 +01:00
Dr. Carsten Leue
5aa0e1ea2e fix: handle non comparable types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 09:35:56 +01:00
Dr. Carsten Leue
d586428cb0 fix: examples
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-13 09:05:57 +01:00
Dr. Carsten Leue
d2dbce6e8b fix: improve lens handling
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 18:23:57 +01:00
Dr. Carsten Leue
6f7ec0768d fix: improve lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 17:28:20 +01:00
Dr. Carsten Leue
ca813b673c fix: better tests and doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 16:24:12 +01:00
Dr. Carsten Leue
af271e7d10 fix: better endo and lens
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 15:03:55 +01:00
Dr. Carsten Leue
567315a31c fix: make a distinction between Chain and Compose for endomorphism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 13:51:00 +01:00
Dr. Carsten Leue
311ed55f06 fix: add Read method to Readers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 11:59:20 +01:00
Dr. Carsten Leue
23333ce52c doc: improve doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 11:08:18 +01:00
Dr. Carsten Leue
eb7fc9f77b fix: better tests for Lazy
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 10:46:07 +01:00
Dr. Carsten Leue
fd0550e71b fix: better test coverage
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 10:35:53 +01:00
304 changed files with 39150 additions and 4116 deletions

View File

@@ -0,0 +1,14 @@
{
"permissions": {
"allow": [
"Bash(go test:*)",
"Bash(go tool cover:*)",
"Bash(sort:*)",
"Bash(timeout 30 go test:*)",
"Bash(cut:*)",
"Bash(go build:*)"
],
"deny": [],
"ask": []
}
}

View File

@@ -0,0 +1,344 @@
# Deep Chaining Performance Analysis
## Executive Summary
The **only remaining performance gap** between `v2/option` and `idiomatic/option` is in **deep chaining operations** (multiple sequential transformations). This document demonstrates the problem, explains the root cause, and provides recommendations.
## Benchmark Results
### v2/option (Struct-based)
```
BenchmarkChain_3Steps 8.17 ns/op 0 allocs
BenchmarkChain_5Steps 16.57 ns/op 0 allocs
BenchmarkChain_10Steps 47.01 ns/op 0 allocs
BenchmarkMap_5Steps 0.28 ns/op 0 allocs ⚡
```
### idiomatic/option (Tuple-based)
```
BenchmarkChain_3Steps 0.22 ns/op 0 allocs ⚡
BenchmarkChain_5Steps 0.22 ns/op 0 allocs ⚡
BenchmarkChain_10Steps 0.21 ns/op 0 allocs ⚡
BenchmarkMap_5Steps 0.22 ns/op 0 allocs ⚡
```
### Performance Comparison
| Steps | v2/option | idiomatic/option | Slowdown |
|-------|-----------|------------------|----------|
| 3 | 8.17 ns | 0.22 ns | **37x slower** |
| 5 | 16.57 ns | 0.22 ns | **75x slower** |
| 10 | 47.01 ns | 0.21 ns | **224x slower** |
**Key Finding**: The performance gap **increases linearly** with chain depth!
---
## Visual Example: The Problem
### Scenario: Processing User Input
```go
// Process user input through multiple validation steps
input := "42"
// v2/option - Nested MonadChain
result := MonadChain(
MonadChain(
MonadChain(
Some(input),
validateNotEmpty, // Step 1
),
parseToInt, // Step 2
),
validateRange, // Step 3
)
```
### What Happens Under the Hood
#### v2/option (Struct Construction Overhead)
```go
// Step 0: Initial value
Some(input)
// Creates: Option[string]{value: "42", isSome: true}
// Memory: HEAP allocation
// Step 1: Validate not empty
MonadChain(opt, validateNotEmpty)
// Input: Option[string]{value: "42", isSome: true} ← Read from heap
// Output: Option[string]{value: "42", isSome: true} ← NEW heap allocation
// Memory: 2 heap allocations
// Step 2: Parse to int
MonadChain(opt, parseToInt)
// Input: Option[string]{value: "42", isSome: true} ← Read from heap
// Output: Option[int]{value: 42, isSome: true} ← NEW heap allocation
// Memory: 3 heap allocations
// Step 3: Validate range
MonadChain(opt, validateRange)
// Input: Option[int]{value: 42, isSome: true} ← Read from heap
// Output: Option[int]{value: 42, isSome: true} ← NEW heap allocation
// Memory: 4 heap allocations TOTAL
// Each step:
// 1. Reads Option struct from memory
// 2. Checks isSome field
// 3. Calls function
// 4. Creates NEW Option struct
// 5. Writes to memory
```
#### idiomatic/option (Zero Allocation)
```go
// Step 0: Initial value
s, ok := Some(input)
// Creates: ("42", true)
// Memory: STACK only (registers)
// Step 1: Validate not empty
v1, ok1 := Chain(validateNotEmpty)(s, ok)
// Input: ("42", true) ← Values in registers
// Output: ("42", true) ← Values in registers
// Memory: ZERO allocations
// Step 2: Parse to int
v2, ok2 := Chain(parseToInt)(v1, ok1)
// Input: ("42", true) ← Values in registers
// Output: (42, true) ← Values in registers
// Memory: ZERO allocations
// Step 3: Validate range
v3, ok3 := Chain(validateRange)(v2, ok2)
// Input: (42, true) ← Values in registers
// Output: (42, true) ← Values in registers
// Memory: ZERO allocations TOTAL
// Each step:
// 1. Reads values from registers (no memory access!)
// 2. Checks bool flag
// 3. Calls function
// 4. Returns new tuple (stays in registers)
// 5. Compiler optimizes everything away!
```
---
## Assembly-Level Difference
### v2/option - Struct Overhead
```asm
; Every chain step does:
MOV RAX, [heap_ptr] ; Load struct from heap
TEST BYTE [RAX+8], 1 ; Check isSome field
JZ none_case ; Branch if None
MOV RDI, [RAX] ; Load value from struct
CALL transform_func ; Call the function
CALL malloc ; Allocate new struct ⚠️
MOV [new_ptr], result ; Store result
MOV [new_ptr+8], 1 ; Set isSome = true
```
### idiomatic/option - Optimized Away
```asm
; All steps compiled to:
MOV EAX, 42 ; The final result!
; Everything else optimized away! ⚡
```
**Compiler insight**: With tuples, the Go compiler can:
1. **Inline everything** - No function call overhead
2. **Eliminate branches** - Constant propagation removes `if ok` checks
3. **Use registers only** - Values never touch memory
4. **Dead code elimination** - Removes unnecessary operations
---
## Real-World Example with Timings
### Example: User Registration Validation Chain
```go
// Validate: email → trim → lowercase → check format → check uniqueness
```
#### v2/option Performance
```go
func ValidateEmail_v2(email string) Option[string] {
return MonadChain(
MonadChain(
MonadChain(
MonadChain(
Some(email),
trimWhitespace, // ~2 ns
),
toLowerCase, // ~2 ns
),
validateFormat, // ~2 ns
),
checkUniqueness, // ~2 ns
)
}
// Total: ~8-16 ns (matches our 5-step benchmark: 16.57 ns)
```
#### idiomatic/option Performance
```go
func ValidateEmail_idiomatic(email string) (string, bool) {
v1, ok1 := Chain(trimWhitespace)(email, true)
v2, ok2 := Chain(toLowerCase)(v1, ok1)
v3, ok3 := Chain(validateFormat)(v2, ok2)
return Chain(checkUniqueness)(v3, ok3)
}
// Total: ~0.22 ns (entire chain optimized to single operation!)
```
**Impact**: For 1 million validations:
- v2/option: 16.57 ms
- idiomatic/option: 0.22 ms
- **Difference: 75x faster = saved 16.35 ms**
---
## Why Map is Fast in v2/option
Interestingly, `Map` (pure transformations) is **much faster** than `Chain`:
```
v2/option:
- BenchmarkChain_5Steps: 16.57 ns
- BenchmarkMap_5Steps: 0.28 ns ← 59x FASTER!
```
**Reason**: Map transformations can be **inlined and fused** by the compiler:
```go
// This:
Map(f5)(Map(f4)(Map(f3)(Map(f2)(Map(f1)(opt)))))
// Becomes (after compiler optimization):
Some(f5(f4(f3(f2(f1(value)))))) // Single struct construction!
// While Chain cannot be optimized the same way:
MonadChain(MonadChain(...)) // Must construct at each step
```
---
## When Does This Matter?
### ⚠️ **Rarely Critical** (99% of use cases)
Even 10-step chains only cost **47 nanoseconds**. For context:
- Database query: **~1,000,000 ns** (1 ms)
- HTTP request: **~10,000,000 ns** (10 ms)
- File I/O: **~100,000 ns** (0.1 ms)
**The 47 ns overhead is negligible compared to real I/O operations.**
### ⚡ **Can Matter** (High-throughput scenarios)
1. **In-memory data processing pipelines**
```go
// Processing 10 million records with 5-step validation
v2/option: 165 ms
idiomatic/option: 2 ms
Difference: 163 ms saved ⚡
```
2. **Real-time stream processing**
- Processing 100k events/second with chained transformations
- 16.57 ns × 100,000 = 1.66 ms vs 0.22 ns × 100,000 = 0.022 ms
- Can affect throughput for high-frequency trading, gaming, etc.
3. **Tight inner loops with chained logic**
```go
for i := 0; i < 1_000_000; i++ {
result := Chain(f1).Chain(f2).Chain(f3).Chain(f4)(data[i])
}
// v2/option: 16 ms
// idiomatic: 0.22 ms
```
---
## Root Cause Summary
| Aspect | v2/option | idiomatic/option | Why? |
|--------|-----------|------------------|------|
| **Intermediate values** | `Option[T]` struct | `(T, bool)` tuple | Struct requires memory, tuple can use registers |
| **Memory allocation** | 1 per step | 0 total | Heap vs stack |
| **Compiler optimization** | Limited | Aggressive | Structs block inlining |
| **Cache impact** | Heap reads | Register-only | Memory bandwidth saved |
| **Branch prediction** | Struct checks | Optimized away | Compiler removes branches |
---
## Recommendations
### ✅ **Use v2/option When:**
- I/O-bound operations (database, network, files)
- User-facing applications (latency dominated by I/O)
- Need JSON marshaling, TryCatch, SequenceArray
- Chain depth < 5 steps (overhead < 20 ns - negligible)
- Code clarity > microsecond performance
### ✅ **Use idiomatic/option When:**
- CPU-bound data processing
- High-throughput stream processing
- Tight inner loops with chaining
- In-memory analytics
- Performance-critical paths
- Chain depth > 5 steps
### ✅ **Mitigation for v2/option:**
If you need v2/option but want better chain performance:
1. **Use Map instead of Chain** when possible:
```go
// Bad (16.57 ns):
MonadChain(MonadChain(MonadChain(opt, f1), f2), f3)
// Good (0.28 ns):
Map(f3)(Map(f2)(Map(f1)(opt)))
```
2. **Batch operations**:
```go
// Instead of chaining many steps:
validate := func(x T) Option[T] {
// Combine multiple checks in one function
if check1(x) && check2(x) && check3(x) {
return Some(transform(x))
}
return None[T]()
}
```
3. **Profile first**:
- Only optimize hot paths
- 47 ns is often acceptable
- Don't premature optimize
---
## Conclusion
**The deep chaining performance gap is:**
- ✅ **Real and measurable** (37-224x slower)
- ✅ **Well understood** (struct construction overhead)
- ⚠️ **Rarely critical** (nanosecond differences usually don't matter)
- ✅ **Easy to work around** (use Map, batch operations)
- ✅ **Worth it for the API benefits** (JSON, methods, helpers)
**For 99% of applications, v2/option's performance is excellent.** The gap only matters in specialized high-throughput scenarios where you should probably use idiomatic/option anyway.
The optimizations already applied (`//go:inline`, direct field access) brought v2/option to **competitive parity** for all practical purposes. The remaining gap is a **fundamental design trade-off**, not a fixable bug.

View File

@@ -2,25 +2,152 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/IBM/fp-go/v2.svg)](https://pkg.go.dev/github.com/IBM/fp-go/v2)
[![Coverage Status](https://coveralls.io/repos/github/IBM/fp-go/badge.svg?branch=main&flag=v2)](https://coveralls.io/github/IBM/fp-go?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/IBM/fp-go/v2)](https://goreportcard.com/report/github.com/IBM/fp-go/v2)
Version 2 of fp-go leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
**fp-go** is a comprehensive functional programming library for Go, bringing type-safe functional patterns inspired by [fp-ts](https://gcanti.github.io/fp-ts/) to the Go ecosystem. Version 2 leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
## 📚 Table of Contents
- [Overview](#-overview)
- [Features](#-features)
- [Requirements](#-requirements)
- [Breaking Changes](#-breaking-changes)
- [Installation](#-installation)
- [Quick Start](#-quick-start)
- [Breaking Changes](#️-breaking-changes)
- [Key Improvements](#-key-improvements)
- [Migration Guide](#-migration-guide)
- [Installation](#-installation)
- [What's New](#-whats-new)
- [Documentation](#-documentation)
- [Contributing](#-contributing)
- [License](#-license)
## 🎯 Overview
fp-go brings the power of functional programming to Go with:
- **Type-safe abstractions** - Monads, Functors, Applicatives, and more
- **Composable operations** - Build complex logic from simple, reusable functions
- **Error handling** - Elegant error management with `Either`, `Result`, and `IOEither`
- **Lazy evaluation** - Control when and how computations execute
- **Optics** - Powerful lens, prism, and traversal operations for immutable data manipulation
## ✨ Features
- 🔒 **Type Safety** - Leverage Go's generics for compile-time guarantees
- 🧩 **Composability** - Chain operations naturally with functional composition
- 📦 **Rich Type System** - `Option`, `Either`, `Result`, `IO`, `Reader`, and more
- 🎯 **Practical** - Designed for real-world Go applications
- 🚀 **Performance** - Zero-cost abstractions where possible
- 📖 **Well-documented** - Comprehensive API documentation and examples
- 🧪 **Battle-tested** - Extensive test coverage
## 🔧 Requirements
- **Go 1.24 or later** (for generic type alias support)
## ⚠️ Breaking Changes
## 📦 Installation
### 1. Generic Type Aliases
```bash
go get github.com/IBM/fp-go/v2
```
## 🚀 Quick Start
### Working with Option
```go
package main
import (
"fmt"
"github.com/IBM/fp-go/v2/option"
)
func main() {
// Create an Option
some := option.Some(42)
none := option.None[int]()
// Map over values
doubled := option.Map(N.Mul(2))(some)
fmt.Println(option.GetOrElse(0)(doubled)) // Output: 84
// Chain operations
result := option.Chain(func(x int) option.Option[string] {
if x > 0 {
return option.Some(fmt.Sprintf("Positive: %d", x))
}
return option.None[string]()
})(some)
fmt.Println(option.GetOrElse("No value")(result)) // Output: Positive: 42
}
```
### Error Handling with Result
```go
package main
import (
"errors"
"fmt"
"github.com/IBM/fp-go/v2/result"
)
func divide(a, b int) result.Result[int] {
if b == 0 {
return result.Error[int](errors.New("division by zero"))
}
return result.Ok(a / b)
}
func main() {
res := divide(10, 2)
// Pattern match on the result
result.Fold(
func(err error) { fmt.Println("Error:", err) },
func(val int) { fmt.Println("Result:", val) },
)(res)
// Output: Result: 5
// Or use GetOrElse for a default value
value := result.GetOrElse(0)(divide(10, 0))
fmt.Println("Value:", value) // Output: Value: 0
}
```
### Composing IO Operations
```go
package main
import (
"fmt"
"github.com/IBM/fp-go/v2/io"
)
func main() {
// Define pure IO operations
readInput := io.MakeIO(func() string {
return "Hello, fp-go!"
})
// Transform the result
uppercase := io.Map(func(s string) string {
return fmt.Sprintf(">>> %s <<<", s)
})(readInput)
// Execute the IO operation
result := uppercase()
fmt.Println(result) // Output: >>> Hello, fp-go! <<<
}
```
### From V1 to V2
#### 1. Generic Type Aliases
V2 uses [generic type aliases](https://github.com/golang/go/issues/46477) which require Go 1.24+. This is the most significant change and enables cleaner type definitions.
@@ -34,7 +161,7 @@ type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
```
### 2. Generic Type Parameter Ordering
#### 2. Generic Type Parameter Ordering
Type parameters that **cannot** be inferred from function arguments now come first, improving type inference.
@@ -52,7 +179,7 @@ func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, fu
This change allows the Go compiler to infer more types automatically, reducing the need for explicit type parameters.
### 3. Pair Monad Semantics
#### 3. Pair Monad Semantics
Monadic operations for `Pair` now operate on the **second argument** to align with the [Haskell definition](https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Data-Pair.html).
@@ -60,7 +187,7 @@ Monadic operations for `Pair` now operate on the **second argument** to align wi
```go
// Operations on first element
pair := MakePair(1, "hello")
result := Map(func(x int) int { return x * 2 })(pair) // Pair(2, "hello")
result := Map(N.Mul(2))(pair) // Pair(2, "hello")
```
**V2:**
@@ -70,6 +197,36 @@ pair := MakePair(1, "hello")
result := Map(func(s string) string { return s + "!" })(pair) // Pair(1, "hello!")
```
#### 4. Endomorphism Compose Semantics
The `Compose` function for endomorphisms now follows **mathematical function composition** (right-to-left execution), aligning with standard functional programming conventions.
**V1:**
```go
// Compose executed left-to-right
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
composed := Compose(double, increment)
result := composed(5) // (5 * 2) + 1 = 11
```
**V2:**
```go
// Compose executes RIGHT-TO-LEFT (mathematical composition)
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
composed := Compose(double, increment)
result := composed(5) // (5 + 1) * 2 = 12
// Use MonadChain for LEFT-TO-RIGHT execution
chained := MonadChain(double, increment)
result2 := chained(5) // (5 * 2) + 1 = 11
```
**Key Difference:**
- `Compose(f, g)` now means `f ∘ g`, which applies `g` first, then `f` (right-to-left)
- `MonadChain(f, g)` applies `f` first, then `g` (left-to-right)
## ✨ Key Improvements
### 1. Simplified Type Declarations
@@ -91,16 +248,16 @@ func processData(input string) ET.Either[error, OPT.Option[int]] {
**V2 Approach:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/option"
)
// Define type aliases once
type Either[A any] = either.Either[error, A]
type Result[A any] = result.Result[A]
type Option[A any] = option.Option[A]
// Use them throughout your codebase
func processData(input string) Either[Option[int]] {
func processData(input string) Result[Option[int]] {
// implementation
}
```
@@ -211,7 +368,7 @@ If you're using `Pair`, update operations to work on the second element:
```go
pair := MakePair(42, "data")
// Map operates on first element
result := Map(func(x int) int { return x * 2 })(pair)
result := Map(N.Mul(2))(pair)
```
**After (V2):**
@@ -230,20 +387,14 @@ Create project-wide type aliases for common patterns:
package myapp
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
)
type Either[A any] = either.Either[error, A]
type Result[A any] = result.Result[A]
type Option[A any] = option.Option[A]
type IOEither[A any] = ioeither.IOEither[error, A]
```
## 📦 Installation
```bash
go get github.com/IBM/fp-go/v2
type IOResult[A any] = ioresult.IOResult[A]
```
## 🆕 What's New
@@ -277,25 +428,37 @@ func process() IOET.IOEither[error, string] {
**V2 Simplified Example:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/ioeither"
"strconv"
"github.com/IBM/fp-go/v2/ioresult"
)
type IOEither[A any] = ioeither.IOEither[error, A]
type IOResult[A any] = ioresult.IOResult[A]
func process() IOEither[string] {
return ioeither.Map(
func process() IOResult[string] {
return ioresult.Map(
strconv.Itoa,
)(fetchData())
}
```
## 📚 Additional Resources
## 📚 Documentation
- [Main README](../README.md) - Core concepts and design philosophy
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)
- [Code Samples](../samples/)
- [Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)
- **[API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)** - Complete API reference
- **[Code Samples](./samples/)** - Practical examples and use cases
- **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases
### Core Modules
- **Option** - Represent optional values without nil
- **Either** - Type-safe error handling with left/right values
- **Result** - Simplified Either with error as left type
- **IO** - Lazy evaluation and side effect management
- **IOEither** - Combine IO with error handling
- **Reader** - Dependency injection pattern
- **ReaderIOEither** - Combine Reader, IO, and Either for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
## 🤔 Should I Migrate?
@@ -310,10 +473,25 @@ func process() IOEither[string] {
- ⚠️ Migration effort outweighs benefits for your project
- ⚠️ You need stability in production (V2 is newer)
## 🤝 Contributing
Contributions are welcome! Here's how you can help:
1. **Report bugs** - Open an issue with a clear description and reproduction steps
2. **Suggest features** - Share your ideas for improvements
3. **Submit PRs** - Fix bugs or add features (please discuss major changes first)
4. **Improve docs** - Help make the documentation clearer and more comprehensive
Please read our contribution guidelines before submitting pull requests.
## 🐛 Issues and Feedback
Found a bug or have a suggestion? Please [open an issue](https://github.com/IBM/fp-go/issues) on GitHub.
## 📄 License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
This project is licensed under the Apache License 2.0. See the [LICENSE](https://github.com/IBM/fp-go/blob/main/LICENSE) file for details.
---
**Made with ❤️ by IBM**

View File

@@ -17,11 +17,10 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
)
@@ -50,16 +49,16 @@ func Replicate[A any](n int, a A) []A {
// This is the monadic version of Map that takes the array as the first parameter.
//
//go:inline
func MonadMap[A, B any](as []A, f func(a A) B) []B {
func MonadMap[A, B any](as []A, f func(A) B) []B {
return G.MonadMap[[]A, []B](as, f)
}
// MonadMapRef applies a function to a pointer to each element of an array, returning a new array with the results.
// This is useful when you need to access elements by reference without copying.
func MonadMapRef[A, B any](as []A, f func(a *A) B) []B {
func MonadMapRef[A, B any](as []A, f func(*A) B) []B {
count := len(as)
bs := make([]B, count)
for i := count - 1; i >= 0; i-- {
for i := range count {
bs[i] = f(&as[i])
}
return bs
@@ -68,7 +67,7 @@ func MonadMapRef[A, B any](as []A, f func(a *A) B) []B {
// MapWithIndex applies a function to each element and its index in an array, returning a new array with the results.
//
//go:inline
func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
func MapWithIndex[A, B any](f func(int, A) B) Operator[A, B] {
return G.MapWithIndex[[]A, []B](f)
}
@@ -77,39 +76,39 @@ func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
//
// Example:
//
// double := array.Map(func(x int) int { return x * 2 })
// double := array.Map(N.Mul(2))
// result := double([]int{1, 2, 3}) // [2, 4, 6]
//
//go:inline
func Map[A, B any](f func(a A) B) func([]A) []B {
func Map[A, B any](f func(A) B) Operator[A, B] {
return G.Map[[]A, []B](f)
}
// MapRef applies a function to a pointer to each element of an array, returning a new array with the results.
// This is the curried version that returns a function.
func MapRef[A, B any](f func(a *A) B) func([]A) []B {
func MapRef[A, B any](f func(*A) B) Operator[A, B] {
return F.Bind2nd(MonadMapRef[A, B], f)
}
func filterRef[A any](fa []A, pred func(a *A) bool) []A {
var result []A
func filterRef[A any](fa []A, pred func(*A) bool) []A {
count := len(fa)
for i := 0; i < count; i++ {
a := fa[i]
if pred(&a) {
result = append(result, a)
var result []A = make([]A, 0, count)
for i := range count {
a := &fa[i]
if pred(a) {
result = append(result, *a)
}
}
return result
}
func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
var result []B
func filterMapRef[A, B any](fa []A, pred func(*A) bool, f func(*A) B) []B {
count := len(fa)
for i := 0; i < count; i++ {
a := fa[i]
if pred(&a) {
result = append(result, f(&a))
var result []B = make([]B, 0, count)
for i := range count {
a := &fa[i]
if pred(a) {
result = append(result, f(a))
}
}
return result
@@ -118,19 +117,19 @@ func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
// Filter returns a new array with all elements from the original array that match a predicate
//
//go:inline
func Filter[A any](pred func(A) bool) EM.Endomorphism[[]A] {
func Filter[A any](pred func(A) bool) Operator[A, A] {
return G.Filter[[]A](pred)
}
// FilterWithIndex returns a new array with all elements from the original array that match a predicate
//
//go:inline
func FilterWithIndex[A any](pred func(int, A) bool) EM.Endomorphism[[]A] {
func FilterWithIndex[A any](pred func(int, A) bool) Operator[A, A] {
return G.FilterWithIndex[[]A](pred)
}
// FilterRef returns a new array with all elements from the original array that match a predicate operating on pointers.
func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
func FilterRef[A any](pred func(*A) bool) Operator[A, A] {
return F.Bind2nd(filterRef[A], pred)
}
@@ -138,7 +137,7 @@ func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
// This is the monadic version that takes the array as the first parameter.
//
//go:inline
func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B {
func MonadFilterMap[A, B any](fa []A, f option.Kleisli[A, B]) []B {
return G.MonadFilterMap[[]A, []B](fa, f)
}
@@ -146,33 +145,33 @@ func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B {
// keeping only the Some values. This is the monadic version that takes the array as the first parameter.
//
//go:inline
func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) O.Option[B]) []B {
func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) Option[B]) []B {
return G.MonadFilterMapWithIndex[[]A, []B](fa, f)
}
// FilterMap maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
// FilterMap maps an array with an iterating function that returns an [Option] and it keeps only the Some values discarding the Nones.
//
//go:inline
func FilterMap[A, B any](f func(A) O.Option[B]) func([]A) []B {
func FilterMap[A, B any](f option.Kleisli[A, B]) Operator[A, B] {
return G.FilterMap[[]A, []B](f)
}
// FilterMapWithIndex maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
// FilterMapWithIndex maps an array with an iterating function that returns an [Option] and it keeps only the Some values discarding the Nones.
//
//go:inline
func FilterMapWithIndex[A, B any](f func(int, A) O.Option[B]) func([]A) []B {
func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
return G.FilterMapWithIndex[[]A, []B](f)
}
// FilterChain maps an array with an iterating function that returns an [O.Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
//
//go:inline
func FilterChain[A, B any](f func(A) O.Option[[]B]) func([]A) []B {
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.FilterChain[[]A](f)
}
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B {
func FilterMapRef[A, B any](pred func(a *A) bool, f func(*A) B) Operator[A, B] {
return func(fa []A) []B {
return filterMapRef(fa, pred, f)
}
@@ -180,8 +179,7 @@ func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B
func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B {
current := initial
count := len(fa)
for i := 0; i < count; i++ {
for i := range len(fa) {
current = f(current, &fa[i])
}
return current
@@ -262,6 +260,8 @@ func Empty[A any]() []A {
}
// Zero returns an empty array of type A (alias for Empty).
//
//go:inline
func Zero[A any]() []A {
return Empty[A]()
}
@@ -277,7 +277,7 @@ func Of[A any](a A) []A {
// This is the monadic version that takes the array as the first parameter (also known as FlatMap).
//
//go:inline
func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
func MonadChain[A, B any](fa []A, f Kleisli[A, B]) []B {
return G.MonadChain(fa, f)
}
@@ -290,7 +290,7 @@ func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
// result := duplicate([]int{1, 2, 3}) // [1, 1, 2, 2, 3, 3]
//
//go:inline
func Chain[A, B any](f func(A) []B) func([]A) []B {
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return G.Chain[[]A](f)
}
@@ -306,7 +306,7 @@ func MonadAp[B, A any](fab []func(A) B, fa []A) []B {
// This is the curried version.
//
//go:inline
func Ap[B, A any](fa []A) func([]func(A) B) []B {
func Ap[B, A any](fa []A) Operator[func(A) B, B] {
return G.Ap[[]B, []func(A) B](fa)
}
@@ -328,7 +328,7 @@ func MatchLeft[A, B any](onEmpty func() B, onNonEmpty func(A, []A) B) func([]A)
// Returns None if the array is empty.
//
//go:inline
func Tail[A any](as []A) O.Option[[]A] {
func Tail[A any](as []A) Option[[]A] {
return G.Tail(as)
}
@@ -336,7 +336,7 @@ func Tail[A any](as []A) O.Option[[]A] {
// Returns None if the array is empty.
//
//go:inline
func Head[A any](as []A) O.Option[A] {
func Head[A any](as []A) Option[A] {
return G.Head(as)
}
@@ -344,7 +344,7 @@ func Head[A any](as []A) O.Option[A] {
// Returns None if the array is empty.
//
//go:inline
func First[A any](as []A) O.Option[A] {
func First[A any](as []A) Option[A] {
return G.First(as)
}
@@ -352,12 +352,12 @@ func First[A any](as []A) O.Option[A] {
// Returns None if the array is empty.
//
//go:inline
func Last[A any](as []A) O.Option[A] {
func Last[A any](as []A) Option[A] {
return G.Last(as)
}
// PrependAll inserts a separator before each element of an array.
func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
func PrependAll[A any](middle A) Operator[A, A] {
return func(as []A) []A {
count := len(as)
dst := count * 2
@@ -377,7 +377,7 @@ func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
// Example:
//
// result := array.Intersperse(0)([]int{1, 2, 3}) // [1, 0, 2, 0, 3]
func Intersperse[A any](middle A) EM.Endomorphism[[]A] {
func Intersperse[A any](middle A) Operator[A, A] {
prepend := PrependAll(middle)
return func(as []A) []A {
if IsEmpty(as) {
@@ -406,7 +406,7 @@ func Flatten[A any](mma [][]A) []A {
}
// Slice extracts a subarray from index low (inclusive) to high (exclusive).
func Slice[A any](low, high int) func(as []A) []A {
func Slice[A any](low, high int) Operator[A, A] {
return array.Slice[[]A](low, high)
}
@@ -414,7 +414,7 @@ func Slice[A any](low, high int) func(as []A) []A {
// Returns None if the index is out of bounds.
//
//go:inline
func Lookup[A any](idx int) func([]A) O.Option[A] {
func Lookup[A any](idx int) func([]A) Option[A] {
return G.Lookup[[]A](idx)
}
@@ -422,7 +422,7 @@ func Lookup[A any](idx int) func([]A) O.Option[A] {
// If the index is out of bounds, the element is appended.
//
//go:inline
func UpsertAt[A any](a A) EM.Endomorphism[[]A] {
func UpsertAt[A any](a A) Operator[A, A] {
return G.UpsertAt[[]A](a)
}
@@ -468,7 +468,7 @@ func ConstNil[A any]() []A {
// SliceRight extracts a subarray from the specified start index to the end.
//
//go:inline
func SliceRight[A any](start int) EM.Endomorphism[[]A] {
func SliceRight[A any](start int) Operator[A, A] {
return G.SliceRight[[]A](start)
}
@@ -482,7 +482,7 @@ func Copy[A any](b []A) []A {
// Clone creates a deep copy of the array using the provided endomorphism to clone the values
//
//go:inline
func Clone[A any](f func(A) A) func(as []A) []A {
func Clone[A any](f func(A) A) Operator[A, A] {
return G.Clone[[]A](f)
}
@@ -510,8 +510,8 @@ func Fold[A any](m M.Monoid[A]) func([]A) A {
// Push adds an element to the end of an array (alias for Append).
//
//go:inline
func Push[A any](a A) EM.Endomorphism[[]A] {
return G.Push[EM.Endomorphism[[]A]](a)
func Push[A any](a A) Operator[A, A] {
return G.Push[Operator[A, A]](a)
}
// MonadFlap applies a value to an array of functions, producing an array of results.
@@ -526,13 +526,13 @@ func MonadFlap[B, A any](fab []func(A) B, a A) []B {
// This is the curried version.
//
//go:inline
func Flap[B, A any](a A) func([]func(A) B) []B {
func Flap[B, A any](a A) Operator[func(A) B, B] {
return G.Flap[func(A) B, []func(A) B, []B](a)
}
// Prepend adds an element to the beginning of an array, returning a new array.
//
//go:inline
func Prepend[A any](head A) EM.Endomorphism[[]A] {
return G.Prepend[EM.Endomorphism[[]A]](head)
func Prepend[A any](head A) Operator[A, A] {
return G.Prepend[Operator[A, A]](head)
}

View File

@@ -56,8 +56,8 @@ func Do[S any](
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) []T,
) func([]S1) []S2 {
f Kleisli[S1, T],
) Operator[S1, S2] {
return G.Bind[[]S1, []S2](setter, f)
}
@@ -79,7 +79,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func([]S1) []S2 {
) Operator[S1, S2] {
return G.Let[[]S1, []S2](setter, f)
}
@@ -101,7 +101,7 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func([]S1) []S2 {
) Operator[S1, S2] {
return G.LetTo[[]S1, []S2](setter, b)
}
@@ -120,7 +120,7 @@ func LetTo[S1, S2, T any](
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) func([]T) []S1 {
) Operator[T, S1] {
return G.BindTo[[]S1, []T](setter)
}
@@ -143,6 +143,6 @@ func BindTo[S1, T any](
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa []T,
) func([]S1) []S2 {
) Operator[S1, S2] {
return G.ApS[[]S1, []S2](setter, fa)
}

View File

@@ -36,7 +36,7 @@
// generated := array.MakeBy(5, func(i int) int { return i * 2 })
//
// // Transforming arrays
// doubled := array.Map(func(x int) int { return x * 2 })(arr)
// doubled := array.Map(N.Mul(2))(arr)
// filtered := array.Filter(func(x int) bool { return x > 2 })(arr)
//
// // Combining arrays
@@ -50,7 +50,7 @@
// numbers := []int{1, 2, 3, 4, 5}
//
// // Map transforms each element
// doubled := array.Map(func(x int) int { return x * 2 })(numbers)
// doubled := array.Map(N.Mul(2))(numbers)
// // Result: [2, 4, 6, 8, 10]
//
// // Filter keeps elements matching a predicate

View File

@@ -17,7 +17,7 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/option"
)
// FindFirst finds the first element which satisfies a predicate function.
@@ -30,7 +30,7 @@ import (
// result2 := findGreaterThan3([]int{1, 2, 3}) // None
//
//go:inline
func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] {
func FindFirst[A any](pred func(A) bool) option.Kleisli[[]A, A] {
return G.FindFirst[[]A](pred)
}
@@ -45,7 +45,7 @@ func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] {
// result := findEvenAtEvenIndex([]int{1, 3, 4, 5}) // Some(4)
//
//go:inline
func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
func FindFirstWithIndex[A any](pred func(int, A) bool) option.Kleisli[[]A, A] {
return G.FindFirstWithIndex[[]A](pred)
}
@@ -65,7 +65,7 @@ func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
// result := parseFirst([]string{"a", "42", "b"}) // Some(42)
//
//go:inline
func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
func FindFirstMap[A, B any](sel option.Kleisli[A, B]) option.Kleisli[[]A, B] {
return G.FindFirstMap[[]A](sel)
}
@@ -73,7 +73,7 @@ func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
// The selector receives both the index and the element.
//
//go:inline
func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
func FindFirstMapWithIndex[A, B any](sel func(int, A) Option[B]) option.Kleisli[[]A, B] {
return G.FindFirstMapWithIndex[[]A](sel)
}
@@ -86,7 +86,7 @@ func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.O
// result := findGreaterThan3([]int{1, 4, 2, 5}) // Some(5)
//
//go:inline
func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] {
func FindLast[A any](pred func(A) bool) option.Kleisli[[]A, A] {
return G.FindLast[[]A](pred)
}
@@ -94,7 +94,7 @@ func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] {
// Returns Some(element) if found, None if no element matches.
//
//go:inline
func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
func FindLastWithIndex[A any](pred func(int, A) bool) option.Kleisli[[]A, A] {
return G.FindLastWithIndex[[]A](pred)
}
@@ -102,7 +102,7 @@ func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
// This combines finding and mapping in a single operation, searching from the end.
//
//go:inline
func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
func FindLastMap[A, B any](sel option.Kleisli[A, B]) option.Kleisli[[]A, B] {
return G.FindLastMap[[]A](sel)
}
@@ -110,6 +110,6 @@ func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
// The selector receives both the index and the element, searching from the end.
//
//go:inline
func FindLastMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
func FindLastMapWithIndex[A, B any](sel func(int, A) Option[B]) option.Kleisli[[]A, B] {
return G.FindLastMapWithIndex[[]A](sel)
}

View File

@@ -25,8 +25,10 @@ import (
)
// Of constructs a single element array
//
//go:inline
func Of[GA ~[]A, A any](value A) GA {
return GA{value}
return array.Of[GA](value)
}
func Reduce[GA ~[]A, A, B any](f func(B, A) B, initial B) func(GA) B {
@@ -82,7 +84,7 @@ func MakeBy[AS ~[]A, F ~func(int) A, A any](n int, f F) AS {
}
// run the generator function across the input
as := make(AS, n)
for i := n - 1; i >= 0; i-- {
for i := range n {
as[i] = f(i)
}
return as
@@ -165,10 +167,9 @@ func Size[GA ~[]A, A any](as GA) int {
func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
result := make(GB, 0, len(fa))
for _, a := range fa {
O.Map(func(b B) B {
if b, ok := O.Unwrap(f(a)); ok {
result = append(result, b)
return b
})(f(a))
}
}
return result
}
@@ -176,10 +177,9 @@ func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
func filterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(int, A) O.Option[B]) GB {
result := make(GB, 0, len(fa))
for i, a := range fa {
O.Map(func(b B) B {
if b, ok := O.Unwrap(f(i, a)); ok {
result = append(result, b)
return b
})(f(i, a))
}
}
return result
}

View File

@@ -42,8 +42,7 @@ func FindFirst[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) O.Option[
func FindFirstMapWithIndex[AS ~[]A, PRED ~func(int, A) O.Option[B], A, B any](pred PRED) func(AS) O.Option[B] {
none := O.None[B]()
return func(as AS) O.Option[B] {
count := len(as)
for i := 0; i < count; i++ {
for i := range len(as) {
out := pred(i, as[i])
if O.IsSome(out) {
return out

View File

@@ -0,0 +1,34 @@
package generic
import (
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup"
)
// Monoid returns a Monoid instance for arrays.
// The Monoid combines arrays through concatenation, with an empty array as the identity element.
//
// Example:
//
// m := array.Monoid[int]()
// result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
// empty := m.Empty() // []
//
//go:inline
func Monoid[GT ~[]T, T any]() M.Monoid[GT] {
return M.MakeMonoid(array.Concat[GT], Empty[GT]())
}
// Semigroup returns a Semigroup instance for arrays.
// The Semigroup combines arrays through concatenation.
//
// Example:
//
// s := array.Semigroup[int]()
// result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
//
//go:inline
func Semigroup[GT ~[]T, T any]() S.Semigroup[GT] {
return S.MakeSemigroup(array.Concat[GT])
}

View File

@@ -26,7 +26,7 @@ import (
func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, fb BS, f FCT) CS {
l := N.Min(len(fa), len(fb))
res := make(CS, l)
for i := l - 1; i >= 0; i-- {
for i := range l {
res[i] = f(fa[i], fb[i])
}
return res
@@ -43,7 +43,7 @@ func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS,
l := len(cs)
as := make(AS, l)
bs := make(BS, l)
for i := l - 1; i >= 0; i-- {
for i := range l {
t := cs[i]
as[i] = t.F1
bs[i] = t.F2

View File

@@ -18,7 +18,6 @@ package array
import (
"testing"
O "github.com/IBM/fp-go/v2/option"
OR "github.com/IBM/fp-go/v2/ord"
"github.com/stretchr/testify/assert"
)
@@ -103,39 +102,6 @@ func TestSortByKey(t *testing.T) {
assert.Equal(t, "Charlie", result[2].Name)
}
func TestMonadTraverse(t *testing.T) {
result := MonadTraverse(
O.Of[[]int],
O.Map[[]int, func(int) []int],
O.Ap[[]int, int],
[]int{1, 3, 5},
func(n int) O.Option[int] {
if n%2 == 1 {
return O.Some(n * 2)
}
return O.None[int]()
},
)
assert.Equal(t, O.Some([]int{2, 6, 10}), result)
// Test with None case
result2 := MonadTraverse(
O.Of[[]int],
O.Map[[]int, func(int) []int],
O.Ap[[]int, int],
[]int{1, 2, 3},
func(n int) O.Option[int] {
if n%2 == 1 {
return O.Some(n * 2)
}
return O.None[int]()
},
)
assert.Equal(t, O.None[[]int](), result2)
}
func TestUniqByKey(t *testing.T) {
type Person struct {
Name string

View File

@@ -16,27 +16,12 @@
package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup"
)
func concat[T any](left, right []T) []T {
// some performance checks
ll := len(left)
if ll == 0 {
return right
}
lr := len(right)
if lr == 0 {
return left
}
// need to copy
buf := make([]T, ll+lr)
copy(buf[copy(buf, left):], right)
return buf
}
// Monoid returns a Monoid instance for arrays.
// The Monoid combines arrays through concatenation, with an empty array as the identity element.
//
@@ -45,8 +30,10 @@ func concat[T any](left, right []T) []T {
// m := array.Monoid[int]()
// result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
// empty := m.Empty() // []
//
//go:inline
func Monoid[T any]() M.Monoid[[]T] {
return M.MakeMonoid(concat[T], Empty[T]())
return G.Monoid[[]T]()
}
// Semigroup returns a Semigroup instance for arrays.
@@ -56,8 +43,10 @@ func Monoid[T any]() M.Monoid[[]T] {
//
// s := array.Semigroup[int]()
// result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
//
//go:inline
func Semigroup[T any]() S.Semigroup[[]T] {
return S.MakeSemigroup(concat[T])
return G.Semigroup[[]T]()
}
func addLen[A any](count int, data []A) int {

View File

@@ -16,10 +16,18 @@
package array
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
)
func MonadSequence[HKTA, HKTRA any](
fof func(HKTA) HKTRA,
m M.Monoid[HKTRA],
ma []HKTA) HKTRA {
return array.MonadSequence(fof, m.Empty(), m.Concat, ma)
}
// Sequence takes an array where elements are HKT<A> (higher kinded type) and,
// using an applicative of that HKT, returns an HKT of []A.
//
@@ -55,16 +63,11 @@ import (
// option.MonadAp[[]int, int],
// )
// result := seq(opts) // Some([1, 2, 3])
func Sequence[A, HKTA, HKTRA, HKTFRA any](
_of func([]A) HKTRA,
_map func(HKTRA, func([]A) func(A) []A) HKTFRA,
_ap func(HKTFRA, HKTA) HKTRA,
func Sequence[HKTA, HKTRA any](
fof func(HKTA) HKTRA,
m M.Monoid[HKTRA],
) func([]HKTA) HKTRA {
ca := F.Curry2(Append[A])
empty := _of(Empty[A]())
return Reduce(func(fas HKTRA, fa HKTA) HKTRA {
return _ap(_map(fas, ca), fa)
}, empty)
return array.Sequence[[]HKTA](fof, m.Empty(), m.Concat)
}
// ArrayOption returns a function to convert a sequence of options into an option of a sequence.
@@ -86,10 +89,10 @@ func Sequence[A, HKTA, HKTRA, HKTFRA any](
// option.Some(3),
// }
// result2 := array.ArrayOption[int]()(opts2) // None
func ArrayOption[A any]() func([]O.Option[A]) O.Option[[]A] {
return Sequence(
O.Of[[]A],
O.MonadMap[[]A, func(A) []A],
O.MonadAp[[]A, A],
func ArrayOption[A any](ma []Option[A]) Option[[]A] {
return MonadSequence(
O.Map(Of[A]),
O.ApplicativeMonoid(Monoid[A]()),
ma,
)
}

View File

@@ -24,8 +24,7 @@ import (
)
func TestSequenceOption(t *testing.T) {
seq := ArrayOption[int]()
assert.Equal(t, O.Of([]int{1, 3}), seq([]O.Option[int]{O.Of(1), O.Of(3)}))
assert.Equal(t, O.None[[]int](), seq([]O.Option[int]{O.Of(1), O.None[int]()}))
assert.Equal(t, O.Of([]int{1, 3}), ArrayOption([]O.Option[int]{O.Of(1), O.Of(3)}))
assert.Equal(t, O.None[[]int](), ArrayOption([]O.Option[int]{O.Of(1), O.None[int]()}))
}

View File

@@ -18,6 +18,7 @@ package array
import (
"testing"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
@@ -243,7 +244,7 @@ func TestSliceComposition(t *testing.T) {
t.Run("slice then map", func(t *testing.T) {
sliced := Slice[int](2, 5)(data)
mapped := Map(func(x int) int { return x * 2 })(sliced)
mapped := Map(N.Mul(2))(sliced)
assert.Equal(t, []int{4, 6, 8}, mapped)
})

View File

@@ -32,7 +32,7 @@ import (
// // Result: [1, 1, 2, 3, 4, 5, 6, 9]
//
//go:inline
func Sort[T any](ord O.Ord[T]) func(ma []T) []T {
func Sort[T any](ord O.Ord[T]) Operator[T, T] {
return G.Sort[[]T](ord)
}
@@ -62,7 +62,7 @@ func Sort[T any](ord O.Ord[T]) func(ma []T) []T {
// // Result: [{"Bob", 25}, {"Alice", 30}, {"Charlie", 35}]
//
//go:inline
func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T {
func SortByKey[K, T any](ord O.Ord[K], f func(T) K) Operator[T, T] {
return G.SortByKey[[]T](ord, f)
}
@@ -93,6 +93,6 @@ func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T {
// // Result: [{"Jones", "Bob"}, {"Smith", "Alice"}, {"Smith", "John"}]
//
//go:inline
func SortBy[T any](ord []O.Ord[T]) func(ma []T) []T {
func SortBy[T any](ord []O.Ord[T]) Operator[T, T] {
return G.SortBy[[]T](ord)
}

View File

@@ -80,3 +80,25 @@ func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
return array.MonadTraverse(fof, fmap, fap, ta, f)
}
//go:inline
func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
f func(int, A) HKTB) func([]A) HKTRB {
return array.TraverseWithIndex[[]A](fof, fmap, fap, f)
}
//go:inline
func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
ta []A,
f func(int, A) HKTB) HKTRB {
return array.MonadTraverseWithIndex(fof, fmap, fap, ta, f)
}

9
v2/array/types.go Normal file
View File

@@ -0,0 +1,9 @@
package array
import "github.com/IBM/fp-go/v2/option"
type (
Kleisli[A, B any] = func(A) []B
Operator[A, B any] = Kleisli[[]A, B]
Option[A any] = option.Option[A]
)

View File

@@ -46,6 +46,6 @@ func StrictUniq[A comparable](as []A) []A {
// // Result: [{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}]
//
//go:inline
func Uniq[A any, K comparable](f func(A) K) func(as []A) []A {
func Uniq[A any, K comparable](f func(A) K) Operator[A, A] {
return G.Uniq[[]A](f)
}

View File

@@ -19,8 +19,8 @@ import (
"fmt"
"testing"
E "github.com/IBM/fp-go/v2/either"
EQ "github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -28,82 +28,82 @@ var (
errTest = fmt.Errorf("test failure")
// Eq is the equal predicate checking if objects are equal
Eq = EQ.FromEquals(assert.ObjectsAreEqual)
Eq = eq.FromEquals(assert.ObjectsAreEqual)
)
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, t *testing.T, expected T) func(actual T) E.Either[error, T] {
return func(actual T) E.Either[error, T] {
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, t *testing.T, expected T) result.Kleisli[T, T] {
return func(actual T) Result[T] {
ok := wrapped(t, expected, actual)
if ok {
return E.Of[error](actual)
return result.Of(actual)
}
return E.Left[T](errTest)
return result.Left[T](errTest)
}
}
// NotEqual tests if the expected and the actual values are not equal
func NotEqual[T any](t *testing.T, expected T) func(actual T) E.Either[error, T] {
func NotEqual[T any](t *testing.T, expected T) result.Kleisli[T, T] {
return wrap1(assert.NotEqual, t, expected)
}
// Equal tests if the expected and the actual values are equal
func Equal[T any](t *testing.T, expected T) func(actual T) E.Either[error, T] {
func Equal[T any](t *testing.T, expected T) result.Kleisli[T, T] {
return wrap1(assert.Equal, t, expected)
}
// Length tests if an array has the expected length
func Length[T any](t *testing.T, expected int) func(actual []T) E.Either[error, []T] {
return func(actual []T) E.Either[error, []T] {
func Length[T any](t *testing.T, expected int) result.Kleisli[[]T, []T] {
return func(actual []T) Result[[]T] {
ok := assert.Len(t, actual, expected)
if ok {
return E.Of[error](actual)
return result.Of(actual)
}
return E.Left[[]T](errTest)
return result.Left[[]T](errTest)
}
}
// NoError validates that there is no error
func NoError[T any](t *testing.T) func(actual E.Either[error, T]) E.Either[error, T] {
return func(actual E.Either[error, T]) E.Either[error, T] {
return E.MonadFold(actual, func(e error) E.Either[error, T] {
func NoError[T any](t *testing.T) result.Operator[T, T] {
return func(actual Result[T]) Result[T] {
return result.MonadFold(actual, func(e error) Result[T] {
assert.NoError(t, e)
return E.Left[T](e)
}, func(value T) E.Either[error, T] {
return result.Left[T](e)
}, func(value T) Result[T] {
assert.NoError(t, nil)
return E.Right[error](value)
return result.Of(value)
})
}
}
// ArrayContains tests if a value is contained in an array
func ArrayContains[T any](t *testing.T, expected T) func(actual []T) E.Either[error, []T] {
return func(actual []T) E.Either[error, []T] {
func ArrayContains[T any](t *testing.T, expected T) result.Kleisli[[]T, []T] {
return func(actual []T) Result[[]T] {
ok := assert.Contains(t, actual, expected)
if ok {
return E.Of[error](actual)
return result.Of(actual)
}
return E.Left[[]T](errTest)
return result.Left[[]T](errTest)
}
}
// ContainsKey tests if a key is contained in a map
func ContainsKey[T any, K comparable](t *testing.T, expected K) func(actual map[K]T) E.Either[error, map[K]T] {
return func(actual map[K]T) E.Either[error, map[K]T] {
func ContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
return func(actual map[K]T) Result[map[K]T] {
ok := assert.Contains(t, actual, expected)
if ok {
return E.Of[error](actual)
return result.Of(actual)
}
return E.Left[map[K]T](errTest)
return result.Left[map[K]T](errTest)
}
}
// NotContainsKey tests if a key is not contained in a map
func NotContainsKey[T any, K comparable](t *testing.T, expected K) func(actual map[K]T) E.Either[error, map[K]T] {
return func(actual map[K]T) E.Either[error, map[K]T] {
func NotContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
return func(actual map[K]T) Result[map[K]T] {
ok := assert.NotContains(t, actual, expected)
if ok {
return E.Of[error](actual)
return result.Of(actual)
}
return E.Left[map[K]T](errTest)
return result.Left[map[K]T](errTest)
}
}

7
v2/assert/types.go Normal file
View File

@@ -0,0 +1,7 @@
package assert
import "github.com/IBM/fp-go/v2/result"
type (
Result[T any] = result.Result[T]
)

View File

@@ -15,14 +15,163 @@
package bytes
// Empty returns an empty byte slice.
//
// This function returns the identity element for the byte slice Monoid,
// which is an empty byte slice. It's useful as a starting point for
// building byte slices or as a default value.
//
// Returns:
// - An empty byte slice ([]byte{})
//
// Properties:
// - Empty() is the identity element for Monoid.Concat
// - Monoid.Concat(Empty(), x) == x
// - Monoid.Concat(x, Empty()) == x
//
// Example - Basic usage:
//
// empty := Empty()
// fmt.Println(len(empty)) // 0
//
// Example - As identity element:
//
// data := []byte("hello")
// result1 := Monoid.Concat(Empty(), data) // []byte("hello")
// result2 := Monoid.Concat(data, Empty()) // []byte("hello")
//
// Example - Building byte slices:
//
// // Start with empty and build up
// buffer := Empty()
// buffer = Monoid.Concat(buffer, []byte("Hello"))
// buffer = Monoid.Concat(buffer, []byte(" "))
// buffer = Monoid.Concat(buffer, []byte("World"))
// // buffer: []byte("Hello World")
//
// See also:
// - Monoid.Empty(): Alternative way to get empty byte slice
// - ConcatAll(): For concatenating multiple byte slices
func Empty() []byte {
return Monoid.Empty()
}
// ToString converts a byte slice to a string.
//
// This function performs a direct conversion from []byte to string.
// The conversion creates a new string with a copy of the byte data.
//
// Parameters:
// - a: The byte slice to convert
//
// Returns:
// - A string containing the same data as the byte slice
//
// Performance Note:
//
// This conversion allocates a new string. For performance-critical code
// that needs to avoid allocations, consider using unsafe.String (Go 1.20+)
// or working directly with byte slices.
//
// Example - Basic conversion:
//
// bytes := []byte("hello")
// str := ToString(bytes)
// fmt.Println(str) // "hello"
//
// Example - Converting binary data:
//
// // ASCII codes for "Hello"
// data := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
// str := ToString(data)
// fmt.Println(str) // "Hello"
//
// Example - Empty byte slice:
//
// empty := Empty()
// str := ToString(empty)
// fmt.Println(str == "") // true
//
// Example - UTF-8 encoded text:
//
// utf8Bytes := []byte("Hello, 世界")
// str := ToString(utf8Bytes)
// fmt.Println(str) // "Hello, 世界"
//
// Example - Round-trip conversion:
//
// original := "test string"
// bytes := []byte(original)
// result := ToString(bytes)
// fmt.Println(original == result) // true
//
// See also:
// - []byte(string): For converting string to byte slice
// - Size(): For getting the length of a byte slice
func ToString(a []byte) string {
return string(a)
}
// Size returns the number of bytes in a byte slice.
//
// This function returns the length of the byte slice, which is the number
// of bytes it contains. This is equivalent to len(as) but provided as a
// named function for use in functional composition.
//
// Parameters:
// - as: The byte slice to measure
//
// Returns:
// - The number of bytes in the slice
//
// Example - Basic usage:
//
// data := []byte("hello")
// size := Size(data)
// fmt.Println(size) // 5
//
// Example - Empty slice:
//
// empty := Empty()
// size := Size(empty)
// fmt.Println(size) // 0
//
// Example - Binary data:
//
// binary := []byte{0x01, 0x02, 0x03, 0x04}
// size := Size(binary)
// fmt.Println(size) // 4
//
// Example - UTF-8 encoded text:
//
// // Note: Size returns byte count, not character count
// utf8 := []byte("Hello, 世界")
// byteCount := Size(utf8)
// fmt.Println(byteCount) // 13 (not 9 characters)
//
// Example - Using in functional composition:
//
// import "github.com/IBM/fp-go/v2/array"
//
// slices := [][]byte{
// []byte("a"),
// []byte("bb"),
// []byte("ccc"),
// }
//
// // Map to get sizes
// sizes := array.Map(Size)(slices)
// // sizes: []int{1, 2, 3}
//
// Example - Checking if slice is empty:
//
// data := []byte("test")
// isEmpty := Size(data) == 0
// fmt.Println(isEmpty) // false
//
// See also:
// - len(): Built-in function for getting slice length
// - ToString(): For converting byte slice to string
func Size(as []byte) int {
return len(as)
}

View File

@@ -187,6 +187,299 @@ func TestOrd(t *testing.T) {
})
}
// TestOrdProperties tests mathematical properties of Ord
func TestOrdProperties(t *testing.T) {
t.Run("reflexivity: x == x", func(t *testing.T) {
testCases := [][]byte{
[]byte{},
[]byte("a"),
[]byte("test"),
[]byte{0x01, 0x02, 0x03},
}
for _, tc := range testCases {
assert.Equal(t, 0, Ord.Compare(tc, tc),
"Compare(%v, %v) should be 0", tc, tc)
assert.True(t, Ord.Equals(tc, tc),
"Equals(%v, %v) should be true", tc, tc)
}
})
t.Run("antisymmetry: if x <= y and y <= x then x == y", func(t *testing.T) {
testCases := []struct {
a, b []byte
}{
{[]byte("abc"), []byte("abc")},
{[]byte{}, []byte{}},
{[]byte{0x01}, []byte{0x01}},
}
for _, tc := range testCases {
cmp1 := Ord.Compare(tc.a, tc.b)
cmp2 := Ord.Compare(tc.b, tc.a)
if cmp1 <= 0 && cmp2 <= 0 {
assert.True(t, Ord.Equals(tc.a, tc.b),
"If %v <= %v and %v <= %v, they should be equal", tc.a, tc.b, tc.b, tc.a)
}
}
})
t.Run("transitivity: if x <= y and y <= z then x <= z", func(t *testing.T) {
x := []byte("a")
y := []byte("b")
z := []byte("c")
cmpXY := Ord.Compare(x, y)
cmpYZ := Ord.Compare(y, z)
cmpXZ := Ord.Compare(x, z)
if cmpXY <= 0 && cmpYZ <= 0 {
assert.True(t, cmpXZ <= 0,
"If %v <= %v and %v <= %v, then %v <= %v", x, y, y, z, x, z)
}
})
t.Run("totality: either x <= y or y <= x", func(t *testing.T) {
testCases := []struct {
a, b []byte
}{
{[]byte("abc"), []byte("abd")},
{[]byte("xyz"), []byte("abc")},
{[]byte{}, []byte("a")},
{[]byte{0x01}, []byte{0x02}},
}
for _, tc := range testCases {
cmp1 := Ord.Compare(tc.a, tc.b)
cmp2 := Ord.Compare(tc.b, tc.a)
assert.True(t, cmp1 <= 0 || cmp2 <= 0,
"Either %v <= %v or %v <= %v must be true", tc.a, tc.b, tc.b, tc.a)
}
})
}
// TestEdgeCases tests edge cases and boundary conditions
func TestEdgeCases(t *testing.T) {
t.Run("very large byte slices", func(t *testing.T) {
large := make([]byte, 1000000)
for i := range large {
large[i] = byte(i % 256)
}
size := Size(large)
assert.Equal(t, 1000000, size)
str := ToString(large)
assert.Equal(t, 1000000, len(str))
})
t.Run("concatenating many slices", func(t *testing.T) {
slices := make([][]byte, 100)
for i := range slices {
slices[i] = []byte{byte(i)}
}
result := ConcatAll(slices...)
assert.Equal(t, 100, Size(result))
})
t.Run("null bytes in slice", func(t *testing.T) {
data := []byte{0x00, 0x01, 0x00, 0x02}
size := Size(data)
assert.Equal(t, 4, size)
str := ToString(data)
assert.Equal(t, 4, len(str))
})
t.Run("comparing slices with null bytes", func(t *testing.T) {
a := []byte{0x00, 0x01}
b := []byte{0x00, 0x02}
assert.Equal(t, -1, Ord.Compare(a, b))
})
}
// TestMonoidConcatPerformance tests concatenation performance characteristics
func TestMonoidConcatPerformance(t *testing.T) {
t.Run("ConcatAll vs repeated Concat", func(t *testing.T) {
slices := [][]byte{
[]byte("a"),
[]byte("b"),
[]byte("c"),
[]byte("d"),
[]byte("e"),
}
// Using ConcatAll
result1 := ConcatAll(slices...)
// Using repeated Concat
result2 := Monoid.Empty()
for _, s := range slices {
result2 = Monoid.Concat(result2, s)
}
assert.Equal(t, result1, result2)
assert.Equal(t, []byte("abcde"), result1)
})
}
// TestRoundTrip tests round-trip conversions
func TestRoundTrip(t *testing.T) {
t.Run("string to bytes to string", func(t *testing.T) {
original := "Hello, World! 世界"
bytes := []byte(original)
result := ToString(bytes)
assert.Equal(t, original, result)
})
t.Run("bytes to string to bytes", func(t *testing.T) {
original := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f}
str := ToString(original)
result := []byte(str)
assert.Equal(t, original, result)
})
}
// TestConcatAllVariadic tests ConcatAll with various argument counts
func TestConcatAllVariadic(t *testing.T) {
t.Run("zero arguments", func(t *testing.T) {
result := ConcatAll()
assert.Equal(t, []byte{}, result)
})
t.Run("one argument", func(t *testing.T) {
result := ConcatAll([]byte("test"))
assert.Equal(t, []byte("test"), result)
})
t.Run("two arguments", func(t *testing.T) {
result := ConcatAll([]byte("hello"), []byte("world"))
assert.Equal(t, []byte("helloworld"), result)
})
t.Run("many arguments", func(t *testing.T) {
result := ConcatAll(
[]byte("a"),
[]byte("b"),
[]byte("c"),
[]byte("d"),
[]byte("e"),
[]byte("f"),
[]byte("g"),
[]byte("h"),
[]byte("i"),
[]byte("j"),
)
assert.Equal(t, []byte("abcdefghij"), result)
})
}
// Benchmark tests
func BenchmarkToString(b *testing.B) {
data := []byte("Hello, World!")
b.Run("small", func(b *testing.B) {
for b.Loop() {
_ = ToString(data)
}
})
b.Run("large", func(b *testing.B) {
large := make([]byte, 10000)
for i := range large {
large[i] = byte(i % 256)
}
b.ResetTimer()
for b.Loop() {
_ = ToString(large)
}
})
}
func BenchmarkSize(b *testing.B) {
data := []byte("Hello, World!")
for b.Loop() {
_ = Size(data)
}
}
func BenchmarkMonoidConcat(b *testing.B) {
a := []byte("Hello")
c := []byte(" World")
b.Run("small slices", func(b *testing.B) {
for b.Loop() {
_ = Monoid.Concat(a, c)
}
})
b.Run("large slices", func(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
b.ResetTimer()
for b.Loop() {
_ = Monoid.Concat(large1, large2)
}
})
}
func BenchmarkConcatAll(b *testing.B) {
slices := [][]byte{
[]byte("Hello"),
[]byte(" "),
[]byte("World"),
[]byte("!"),
}
b.Run("few slices", func(b *testing.B) {
for b.Loop() {
_ = ConcatAll(slices...)
}
})
b.Run("many slices", func(b *testing.B) {
many := make([][]byte, 100)
for i := range many {
many[i] = []byte{byte(i)}
}
b.ResetTimer()
for b.Loop() {
_ = ConcatAll(many...)
}
})
}
func BenchmarkOrdCompare(b *testing.B) {
a := []byte("abc")
c := []byte("abd")
b.Run("equal", func(b *testing.B) {
for b.Loop() {
_ = Ord.Compare(a, a)
}
})
b.Run("different", func(b *testing.B) {
for b.Loop() {
_ = Ord.Compare(a, c)
}
})
b.Run("large slices", func(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
large2[9999] = 1
b.ResetTimer()
for b.Loop() {
_ = Ord.Compare(large1, large2)
}
})
}
// Example tests
func ExampleEmpty() {
empty := Empty()
@@ -219,3 +512,17 @@ func ExampleConcatAll() {
// Output:
}
func ExampleMonoid_concat() {
result := Monoid.Concat([]byte("Hello"), []byte(" World"))
println(string(result)) // Hello World
// Output:
}
func ExampleOrd_compare() {
cmp := Ord.Compare([]byte("abc"), []byte("abd"))
println(cmp) // -1 (abc < abd)
// Output:
}

4
v2/bytes/coverage.out Normal file
View File

@@ -0,0 +1,4 @@
mode: set
github.com/IBM/fp-go/v2/bytes/bytes.go:55.21,57.2 1 1
github.com/IBM/fp-go/v2/bytes/bytes.go:111.32,113.2 1 1
github.com/IBM/fp-go/v2/bytes/bytes.go:175.26,177.2 1 1

View File

@@ -23,12 +23,219 @@ import (
)
var (
// monoid for byte arrays
// Monoid is the Monoid instance for byte slices.
//
// This Monoid combines byte slices through concatenation, with an empty
// byte slice as the identity element. It satisfies the monoid laws:
//
// Identity laws:
// - Monoid.Concat(Monoid.Empty(), x) == x (left identity)
// - Monoid.Concat(x, Monoid.Empty()) == x (right identity)
//
// Associativity law:
// - Monoid.Concat(Monoid.Concat(a, b), c) == Monoid.Concat(a, Monoid.Concat(b, c))
//
// Operations:
// - Empty(): Returns an empty byte slice []byte{}
// - Concat(a, b []byte): Concatenates two byte slices
//
// Example - Basic concatenation:
//
// result := Monoid.Concat([]byte("Hello"), []byte(" World"))
// // result: []byte("Hello World")
//
// Example - Identity element:
//
// empty := Monoid.Empty()
// data := []byte("test")
// result1 := Monoid.Concat(empty, data) // []byte("test")
// result2 := Monoid.Concat(data, empty) // []byte("test")
//
// Example - Building byte buffers:
//
// buffer := Monoid.Empty()
// buffer = Monoid.Concat(buffer, []byte("Line 1\n"))
// buffer = Monoid.Concat(buffer, []byte("Line 2\n"))
// buffer = Monoid.Concat(buffer, []byte("Line 3\n"))
//
// Example - Associativity:
//
// a := []byte("a")
// b := []byte("b")
// c := []byte("c")
// left := Monoid.Concat(Monoid.Concat(a, b), c) // []byte("abc")
// right := Monoid.Concat(a, Monoid.Concat(b, c)) // []byte("abc")
// // left == right
//
// See also:
// - ConcatAll: For concatenating multiple byte slices at once
// - Empty(): Convenience function for getting empty byte slice
Monoid = A.Monoid[byte]()
// ConcatAll concatenates all bytes
// ConcatAll efficiently concatenates multiple byte slices into a single slice.
//
// This function takes a variadic number of byte slices and combines them
// into a single byte slice. It pre-allocates the exact amount of memory
// needed, making it more efficient than repeated concatenation.
//
// Parameters:
// - slices: Zero or more byte slices to concatenate
//
// Returns:
// - A new byte slice containing all input slices concatenated in order
//
// Performance:
//
// ConcatAll is more efficient than using Monoid.Concat repeatedly because
// it calculates the total size upfront and allocates memory once, avoiding
// multiple allocations and copies.
//
// Example - Basic usage:
//
// result := ConcatAll(
// []byte("Hello"),
// []byte(" "),
// []byte("World"),
// )
// // result: []byte("Hello World")
//
// Example - Empty input:
//
// result := ConcatAll()
// // result: []byte{}
//
// Example - Single slice:
//
// result := ConcatAll([]byte("test"))
// // result: []byte("test")
//
// Example - Building protocol messages:
//
// import "encoding/binary"
//
// header := []byte{0x01, 0x02}
// length := make([]byte, 4)
// binary.BigEndian.PutUint32(length, 100)
// payload := []byte("data")
// footer := []byte{0xFF}
//
// message := ConcatAll(header, length, payload, footer)
//
// Example - With empty slices:
//
// result := ConcatAll(
// []byte("a"),
// []byte{},
// []byte("b"),
// []byte{},
// []byte("c"),
// )
// // result: []byte("abc")
//
// Example - Building CSV line:
//
// fields := [][]byte{
// []byte("John"),
// []byte("Doe"),
// []byte("30"),
// }
// separator := []byte(",")
//
// // Interleave fields with separators
// parts := [][]byte{
// fields[0], separator,
// fields[1], separator,
// fields[2],
// }
// line := ConcatAll(parts...)
// // line: []byte("John,Doe,30")
//
// See also:
// - Monoid.Concat: For concatenating exactly two byte slices
// - bytes.Join: Standard library function for joining with separator
ConcatAll = A.ArrayConcatAll[byte]
// Ord implements the default ordering on bytes
// Ord is the Ord instance for byte slices providing lexicographic ordering.
//
// This Ord instance compares byte slices lexicographically (dictionary order),
// comparing bytes from left to right until a difference is found or one slice
// ends. It uses the standard library's bytes.Compare and bytes.Equal functions.
//
// Comparison rules:
// - Compares byte-by-byte from left to right
// - First differing byte determines the order
// - Shorter slice is less than longer slice if all bytes match
// - Empty slice is less than any non-empty slice
//
// Operations:
// - Compare(a, b []byte) int: Returns -1 if a < b, 0 if a == b, 1 if a > b
// - Equals(a, b []byte) bool: Returns true if slices are equal
//
// Example - Basic comparison:
//
// cmp := Ord.Compare([]byte("abc"), []byte("abd"))
// // cmp: -1 (abc < abd)
//
// cmp = Ord.Compare([]byte("xyz"), []byte("abc"))
// // cmp: 1 (xyz > abc)
//
// cmp = Ord.Compare([]byte("test"), []byte("test"))
// // cmp: 0 (equal)
//
// Example - Length differences:
//
// cmp := Ord.Compare([]byte("ab"), []byte("abc"))
// // cmp: -1 (shorter is less)
//
// cmp = Ord.Compare([]byte("abc"), []byte("ab"))
// // cmp: 1 (longer is greater)
//
// Example - Empty slices:
//
// cmp := Ord.Compare([]byte{}, []byte("a"))
// // cmp: -1 (empty is less)
//
// cmp = Ord.Compare([]byte{}, []byte{})
// // cmp: 0 (both empty)
//
// Example - Equality check:
//
// equal := Ord.Equals([]byte("test"), []byte("test"))
// // equal: true
//
// equal = Ord.Equals([]byte("test"), []byte("Test"))
// // equal: false (case-sensitive)
//
// Example - Sorting byte slices:
//
// import "github.com/IBM/fp-go/v2/array"
//
// data := [][]byte{
// []byte("zebra"),
// []byte("apple"),
// []byte("mango"),
// }
//
// sorted := array.Sort(Ord)(data)
// // sorted: [[]byte("apple"), []byte("mango"), []byte("zebra")]
//
// Example - Binary data comparison:
//
// cmp := Ord.Compare([]byte{0x01, 0x02}, []byte{0x01, 0x03})
// // cmp: -1 (0x02 < 0x03)
//
// Example - Finding minimum:
//
// import O "github.com/IBM/fp-go/v2/ord"
//
// a := []byte("xyz")
// b := []byte("abc")
// min := O.Min(Ord)(a, b)
// // min: []byte("abc")
//
// See also:
// - bytes.Compare: Standard library comparison function
// - bytes.Equal: Standard library equality function
// - array.Sort: For sorting slices using an Ord instance
Ord = O.MakeOrd(bytes.Compare, bytes.Equal)
)

View File

@@ -53,17 +53,20 @@ var (
// structInfo holds information about a struct that needs lens generation
type structInfo struct {
Name string
Fields []fieldInfo
Imports map[string]string // package path -> alias
Name string
TypeParams string // e.g., "[T any]" or "[K comparable, V any]" - for type declarations
TypeParamNames string // e.g., "[T]" or "[K, V]" - for type usage in function signatures
Fields []fieldInfo
Imports map[string]string // package path -> alias
}
// fieldInfo holds information about a struct field
type fieldInfo struct {
Name string
TypeName string
BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag
Name string
TypeName string
BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag
IsComparable bool // true if the type is comparable (can use ==)
}
// templateData holds data for template rendering
@@ -74,64 +77,95 @@ type templateData struct {
const lensStructTemplate = `
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
type {{.Name}}Lenses struct {
type {{.Name}}Lenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
{{.Name}} {{if .IsOptional}}LO.LensO[{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[{{$.Name}}, {{.TypeName}}]{{end}}
{{.Name}} L.Lens[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
// optional fields
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O LO.LensO[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
}
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
type {{.Name}}RefLenses struct {
type {{.Name}}RefLenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
{{.Name}} {{if .IsOptional}}LO.LensO[*{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[*{{$.Name}}, {{.TypeName}}]{{end}}
{{.Name}} L.Lens[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
// optional fields
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O LO.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
}
`
const lensConstructorTemplate = `
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
func Make{{.Name}}Lenses() {{.Name}}Lenses {
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
{{- if .IsOptional}}
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
lens{{.Name}} := L.MakeLens(
func(s {{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) {{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}}
{{- end}}
return {{.Name}}Lenses{
return {{.Name}}Lenses{{.TypeParamNames}}{
// mandatory lenses
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s {{$.Name}}, v O.Option[{{.TypeName}}]) {{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- else}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s },
),
{{.Name}}: lens{{.Name}},
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O: lens{{.Name}}O,
{{- end}}
{{- end}}
}
}
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
func Make{{.Name}}RefLenses() {{.Name}}RefLenses {
func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
{{- if .IsOptional}}
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
{{- end}}
{{- end}}
return {{.Name}}RefLenses{
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s *{{$.Name}}, v O.Option[{{.TypeName}}]) *{{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- if .IsComparable}}
lens{{.Name}} := L.MakeLensStrict(
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- else}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
),
lens{{.Name}} := L.MakeLensRef(
func(s *{{$.Name}}{{$.TypeParamNames}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}{{$.TypeParamNames}}, v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} { s.{{.Name}} = v; return s },
)
{{- end}}
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
lens{{.Name}}O := LO.FromIso[*{{$.Name}}{{$.TypeParamNames}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}})
{{- end}}
{{- end}}
return {{.Name}}RefLenses{{.TypeParamNames}}{
// mandatory lenses
{{- range .Fields}}
{{.Name}}: lens{{.Name}},
{{- end}}
// optional lenses
{{- range .Fields}}
{{- if .IsComparable}}
{{.Name}}O: lens{{.Name}}O,
{{- end}}
{{- end}}
}
@@ -257,6 +291,259 @@ func isPointerType(expr ast.Expr) bool {
return ok
}
// isComparableType checks if a type expression represents a comparable type.
// Comparable types in Go include:
// - Basic types (bool, numeric types, string)
// - Pointer types
// - Channel types
// - Interface types
// - Structs where all fields are comparable
// - Arrays where the element type is comparable
//
// Non-comparable types include:
// - Slices
// - Maps
// - Functions
//
// typeParams is a map of type parameter names to their constraints (e.g., "T" -> "any", "K" -> "comparable")
func isComparableType(expr ast.Expr, typeParams map[string]string) bool {
switch t := expr.(type) {
case *ast.Ident:
// Check if this is a type parameter
if constraint, isTypeParam := typeParams[t.Name]; isTypeParam {
// Type parameter - check its constraint
return constraint == "comparable"
}
// Basic types and named types
// We assume named types are comparable unless they're known non-comparable types
name := t.Name
// Known non-comparable built-in types
if name == "error" {
// error is an interface, which is comparable
return true
}
// Most basic types and named types are comparable
// We can't determine if a custom type is comparable without type checking,
// so we assume it is (conservative approach)
return true
case *ast.StarExpr:
// Pointer types are always comparable
return true
case *ast.ArrayType:
// Arrays are comparable if their element type is comparable
if t.Len == nil {
// This is a slice (no length), slices are not comparable
return false
}
// Fixed-size array, check element type
return isComparableType(t.Elt, typeParams)
case *ast.MapType:
// Maps are not comparable
return false
case *ast.FuncType:
// Functions are not comparable
return false
case *ast.InterfaceType:
// Interface types are comparable
return true
case *ast.StructType:
// Structs are comparable if all fields are comparable
// We can't easily determine this without full type information,
// so we conservatively return false for struct literals
return false
case *ast.SelectorExpr:
// Qualified identifier (e.g., pkg.Type)
// We can't determine comparability without type information
// Check for known non-comparable types from standard library
if ident, ok := t.X.(*ast.Ident); ok {
pkgName := ident.Name
typeName := t.Sel.Name
// Check for known non-comparable types
if pkgName == "context" && typeName == "Context" {
// context.Context is an interface, which is comparable
return true
}
// For other qualified types, we assume they're comparable
// This is a conservative approach
}
return true
case *ast.IndexExpr, *ast.IndexListExpr:
// Generic types - we can't determine comparability without type information
// For common generic types, we can make educated guesses
var baseExpr ast.Expr
if idx, ok := t.(*ast.IndexExpr); ok {
baseExpr = idx.X
} else if idxList, ok := t.(*ast.IndexListExpr); ok {
baseExpr = idxList.X
}
if sel, ok := baseExpr.(*ast.SelectorExpr); ok {
if ident, ok := sel.X.(*ast.Ident); ok {
pkgName := ident.Name
typeName := sel.Sel.Name
// Check for known non-comparable generic types
if pkgName == "option" && typeName == "Option" {
// Option types are not comparable (they contain a slice internally)
return false
}
if pkgName == "either" && typeName == "Either" {
// Either types are not comparable
return false
}
}
}
// For other generic types, conservatively assume not comparable
log.Printf("Not comparable type: %v\n", t)
return false
case *ast.ChanType:
// Channel types are comparable
return true
default:
// Unknown type, conservatively assume not comparable
return false
}
}
// embeddedFieldResult holds both the field info and its AST type for import extraction
type embeddedFieldResult struct {
fieldInfo fieldInfo
fieldType ast.Expr
}
// extractEmbeddedFields extracts fields from an embedded struct type
// It returns a slice of embeddedFieldResult for all exported fields in the embedded struct
// typeParamsMap contains the type parameters of the parent struct (for checking comparability)
func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, file *ast.File, typeParamsMap map[string]string) []embeddedFieldResult {
var results []embeddedFieldResult
// Get the type name of the embedded field
var typeName string
var typeIdent *ast.Ident
switch t := embedType.(type) {
case *ast.Ident:
// Direct embedded type: type MyStruct struct { EmbeddedType }
typeName = t.Name
typeIdent = t
case *ast.StarExpr:
// Pointer embedded type: type MyStruct struct { *EmbeddedType }
if ident, ok := t.X.(*ast.Ident); ok {
typeName = ident.Name
typeIdent = ident
}
case *ast.SelectorExpr:
// Qualified embedded type: type MyStruct struct { pkg.EmbeddedType }
// We can't easily resolve this without full type information
// For now, skip these
return results
}
if typeName == "" || typeIdent == nil {
return results
}
// Find the struct definition in the same file
var embeddedStructType *ast.StructType
ast.Inspect(file, func(n ast.Node) bool {
if ts, ok := n.(*ast.TypeSpec); ok {
if ts.Name.Name == typeName {
if st, ok := ts.Type.(*ast.StructType); ok {
embeddedStructType = st
return false
}
}
}
return true
})
if embeddedStructType == nil {
// Struct not found in this file, might be from another package
return results
}
// Extract fields from the embedded struct
for _, field := range embeddedStructType.Fields.List {
// Skip embedded fields within embedded structs (for now, to avoid infinite recursion)
if len(field.Names) == 0 {
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
fieldTypeName := getTypeName(field.Type)
isOptional := false
baseType := fieldTypeName
// Check if field is optional
if isPointerType(field.Type) {
isOptional = true
baseType = strings.TrimPrefix(fieldTypeName, "*")
} else if hasOmitEmpty(field.Tag) {
isOptional = true
}
// Check if the type is comparable
isComparable := isComparableType(field.Type, typeParamsMap)
results = append(results, embeddedFieldResult{
fieldInfo: fieldInfo{
Name: name.Name,
TypeName: fieldTypeName,
BaseType: baseType,
IsOptional: isOptional,
IsComparable: isComparable,
},
fieldType: field.Type,
})
}
}
}
return results
}
// extractTypeParams extracts type parameters from a type spec
// Returns two strings: full params like "[T any]" and names only like "[T]"
func extractTypeParams(typeSpec *ast.TypeSpec) (string, string) {
if typeSpec.TypeParams == nil || len(typeSpec.TypeParams.List) == 0 {
return "", ""
}
var params []string
var names []string
for _, field := range typeSpec.TypeParams.List {
for _, name := range field.Names {
constraint := getTypeName(field.Type)
params = append(params, name.Name+" "+constraint)
names = append(names, name.Name)
}
}
fullParams := "[" + strings.Join(params, ", ") + "]"
nameParams := "[" + strings.Join(names, ", ") + "]"
return fullParams, nameParams
}
// buildTypeParamsMap creates a map of type parameter names to their constraints
// e.g., for "type Box[T any, K comparable]", returns {"T": "any", "K": "comparable"}
func buildTypeParamsMap(typeSpec *ast.TypeSpec) map[string]string {
typeParamsMap := make(map[string]string)
if typeSpec.TypeParams == nil || len(typeSpec.TypeParams.List) == 0 {
return typeParamsMap
}
for _, field := range typeSpec.TypeParams.List {
constraint := getTypeName(field.Type)
for _, name := range field.Names {
typeParamsMap[name.Name] = constraint
}
}
return typeParamsMap
}
// parseFile parses a Go file and extracts structs with lens annotations
func parseFile(filename string) ([]structInfo, string, error) {
fset := token.NewFileSet()
@@ -320,9 +607,27 @@ func parseFile(filename string) ([]structInfo, string, error) {
var fields []fieldInfo
structImports := make(map[string]string)
// Build type parameters map for this struct
typeParamsMap := buildTypeParamsMap(typeSpec)
for _, field := range structType.Fields.List {
if len(field.Names) == 0 {
// Embedded field, skip for now
// Embedded field - promote its fields
embeddedResults := extractEmbeddedFields(field.Type, fileImports, node, typeParamsMap)
for _, embResult := range embeddedResults {
// Extract imports from embedded field's type
fieldImports := make(map[string]string)
extractImports(embResult.fieldType, fieldImports)
// Resolve package names to full import paths
for pkgName := range fieldImports {
if importPath, ok := fileImports[pkgName]; ok {
structImports[importPath] = pkgName
}
}
fields = append(fields, embResult.fieldInfo)
}
continue
}
for _, name := range field.Names {
@@ -331,6 +636,7 @@ func parseFile(filename string) ([]structInfo, string, error) {
typeName := getTypeName(field.Type)
isOptional := false
baseType := typeName
isComparable := false
// Check if field is optional:
// 1. Pointer types are always optional
@@ -344,6 +650,11 @@ func parseFile(filename string) ([]structInfo, string, error) {
isOptional = true
}
// Check if the type is comparable (for non-optional fields)
// For optional fields, we don't need to check since they use LensO
isComparable = isComparableType(field.Type, typeParamsMap)
// log.Printf("field %s, type: %v, isComparable: %b\n", name, field.Type, isComparable)
// Extract imports from this field's type
fieldImports := make(map[string]string)
extractImports(field.Type, fieldImports)
@@ -356,20 +667,24 @@ func parseFile(filename string) ([]structInfo, string, error) {
}
fields = append(fields, fieldInfo{
Name: name.Name,
TypeName: typeName,
BaseType: baseType,
IsOptional: isOptional,
Name: name.Name,
TypeName: typeName,
BaseType: baseType,
IsOptional: isOptional,
IsComparable: isComparable,
})
}
}
}
if len(fields) > 0 {
typeParams, typeParamNames := extractTypeParams(typeSpec)
structs = append(structs, structInfo{
Name: typeSpec.Name.Name,
Fields: fields,
Imports: structImports,
Name: typeSpec.Name.Name,
TypeParams: typeParams,
TypeParamNames: typeParamNames,
Fields: fields,
Imports: structImports,
})
}
@@ -469,8 +784,8 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
// Standard fp-go imports always needed
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
f.WriteString("\tI \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
f.WriteString("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// Add additional imports collected from field types
for importPath, alias := range allImports {

View File

@@ -168,6 +168,91 @@ func TestIsPointerType(t *testing.T) {
}
}
func TestIsComparableType(t *testing.T) {
tests := []struct {
name string
code string
expected bool
}{
{
name: "basic type - string",
code: "type T struct { F string }",
expected: true,
},
{
name: "basic type - int",
code: "type T struct { F int }",
expected: true,
},
{
name: "basic type - bool",
code: "type T struct { F bool }",
expected: true,
},
{
name: "pointer type",
code: "type T struct { F *string }",
expected: true,
},
{
name: "slice type - not comparable",
code: "type T struct { F []string }",
expected: false,
},
{
name: "map type - not comparable",
code: "type T struct { F map[string]int }",
expected: false,
},
{
name: "array type - comparable if element is",
code: "type T struct { F [5]int }",
expected: true,
},
{
name: "interface type",
code: "type T struct { F interface{} }",
expected: true,
},
{
name: "channel type",
code: "type T struct { F chan int }",
expected: true,
},
{
name: "function type - not comparable",
code: "type T struct { F func() }",
expected: false,
},
{
name: "struct literal - conservatively not comparable",
code: "type T struct { F struct{ X int } }",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
require.NoError(t, err)
var fieldType ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
fieldType = field.Type
return false
}
return true
})
require.NotNil(t, fieldType)
result := isComparableType(fieldType, map[string]string{})
assert.Equal(t, tt.expected, result)
})
}
}
func TestHasOmitEmpty(t *testing.T) {
tests := []struct {
name string
@@ -337,6 +422,167 @@ type Config struct {
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
}
func TestParseFileWithComparableTypes(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type TypeTest struct {
Name string
Age int
Pointer *string
Slice []string
Map map[string]int
Channel chan int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check TypeTest struct
typeTest := structs[0]
assert.Equal(t, "TypeTest", typeTest.Name)
assert.Len(t, typeTest.Fields, 6)
// Name - string is comparable
assert.Equal(t, "Name", typeTest.Fields[0].Name)
assert.Equal(t, "string", typeTest.Fields[0].TypeName)
assert.False(t, typeTest.Fields[0].IsOptional)
assert.True(t, typeTest.Fields[0].IsComparable, "string should be comparable")
// Age - int is comparable
assert.Equal(t, "Age", typeTest.Fields[1].Name)
assert.Equal(t, "int", typeTest.Fields[1].TypeName)
assert.False(t, typeTest.Fields[1].IsOptional)
assert.True(t, typeTest.Fields[1].IsComparable, "int should be comparable")
// Pointer - pointer is optional, IsComparable not checked for optional fields
assert.Equal(t, "Pointer", typeTest.Fields[2].Name)
assert.Equal(t, "*string", typeTest.Fields[2].TypeName)
assert.True(t, typeTest.Fields[2].IsOptional)
// Slice - not comparable
assert.Equal(t, "Slice", typeTest.Fields[3].Name)
assert.Equal(t, "[]string", typeTest.Fields[3].TypeName)
assert.False(t, typeTest.Fields[3].IsOptional)
assert.False(t, typeTest.Fields[3].IsComparable, "slice should not be comparable")
// Map - not comparable
assert.Equal(t, "Map", typeTest.Fields[4].Name)
assert.Equal(t, "map[string]int", typeTest.Fields[4].TypeName)
assert.False(t, typeTest.Fields[4].IsOptional)
assert.False(t, typeTest.Fields[4].IsComparable, "map should not be comparable")
// Channel - comparable (note: getTypeName returns "any" for channel types, but isComparableType correctly identifies them)
assert.Equal(t, "Channel", typeTest.Fields[5].Name)
assert.Equal(t, "any", typeTest.Fields[5].TypeName) // getTypeName doesn't handle chan types specifically
assert.False(t, typeTest.Fields[5].IsOptional)
assert.True(t, typeTest.Fields[5].IsComparable, "channel should be comparable")
}
func TestLensRefTemplatesWithComparable(t *testing.T) {
s := structInfo{
Name: "TestStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Age", TypeName: "int", IsOptional: false, IsComparable: true},
{Name: "Data", TypeName: "[]byte", IsOptional: false, IsComparable: false},
{Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: false},
},
}
// Test constructor template for RefLenses
var constructorBuf bytes.Buffer
err := constructorTmpl.Execute(&constructorBuf, s)
require.NoError(t, err)
constructorStr := constructorBuf.String()
// Check that MakeLensStrict is used for comparable types in RefLenses
assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses")
// Name field - comparable, should use MakeLensStrict
assert.Contains(t, constructorStr, "lensName := L.MakeLensStrict(",
"comparable field Name should use MakeLensStrict in RefLenses")
// Age field - comparable, should use MakeLensStrict
assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(",
"comparable field Age should use MakeLensStrict in RefLenses")
// Data field - not comparable, should use MakeLensRef
assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(",
"non-comparable field Data should use MakeLensRef in RefLenses")
}
func TestGenerateLensHelpersWithComparable(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type TestStruct struct {
Name string
Count int
Data []byte
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content in RefLenses
assert.Contains(t, contentStr, "MakeTestStructRefLenses")
// Name and Count are comparable, should use MakeLensStrict
assert.Contains(t, contentStr, "L.MakeLensStrict",
"comparable fields should use MakeLensStrict in RefLenses")
// Data is not comparable (slice), should use MakeLensRef
assert.Contains(t, contentStr, "L.MakeLensRef",
"non-comparable fields should use MakeLensRef in RefLenses")
// Verify the pattern appears for Name field (comparable)
namePattern := "lensName := L.MakeLensStrict("
assert.Contains(t, contentStr, namePattern,
"Name field should use MakeLensStrict")
// Verify the pattern appears for Data field (not comparable)
dataPattern := "lensData := L.MakeLensRef("
assert.Contains(t, contentStr, dataPattern,
"Data field should use MakeLensRef")
}
func TestGenerateLensHelpers(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
@@ -373,11 +619,11 @@ type TestStruct struct {
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "Code generated by go generate")
assert.Contains(t, contentStr, "TestStructLens")
assert.Contains(t, contentStr, "MakeTestStructLens")
assert.Contains(t, contentStr, "TestStructLenses")
assert.Contains(t, contentStr, "MakeTestStructLenses")
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
assert.Contains(t, contentStr, "I.FromZero")
assert.Contains(t, contentStr, "IO.FromZero")
}
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
@@ -411,8 +657,8 @@ func TestLensTemplates(t *testing.T) {
s := structInfo{
Name: "TestStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "*int", IsOptional: true},
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Value", TypeName: "*int", IsOptional: true, IsComparable: true},
},
}
@@ -424,7 +670,9 @@ func TestLensTemplates(t *testing.T) {
structStr := structBuf.String()
assert.Contains(t, structStr, "type TestStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[TestStruct, *int]")
assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]")
assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]")
assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]")
// Test constructor template
var constructorBuf bytes.Buffer
@@ -434,19 +682,21 @@ func TestLensTemplates(t *testing.T) {
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
assert.Contains(t, constructorStr, "return TestStructLenses{")
assert.Contains(t, constructorStr, "Name: L.MakeLens(")
assert.Contains(t, constructorStr, "Value: L.MakeLens(")
assert.Contains(t, constructorStr, "I.FromZero")
assert.Contains(t, constructorStr, "Name: lensName,")
assert.Contains(t, constructorStr, "NameO: lensNameO,")
assert.Contains(t, constructorStr, "Value: lensValue,")
assert.Contains(t, constructorStr, "ValueO: lensValueO,")
assert.Contains(t, constructorStr, "IO.FromZero")
}
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
s := structInfo{
Name: "ConfigStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "string", IsOptional: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true}, // pointer
{Name: "Name", TypeName: "string", IsOptional: false, IsComparable: true},
{Name: "Value", TypeName: "string", IsOptional: true, IsComparable: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true, IsComparable: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true, IsComparable: true}, // pointer
},
}
@@ -458,9 +708,13 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
structStr := structBuf.String()
assert.Contains(t, structStr, "type ConfigStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[ConfigStruct, string]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Count LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Pointer LO.LensO[ConfigStruct, *string]")
assert.Contains(t, structStr, "NameO LO.LensO[ConfigStruct, string]")
assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "comparable non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]")
assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "comparable non-pointer with omitempty should have optional lens")
assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]")
assert.Contains(t, structStr, "PointerO LO.LensO[ConfigStruct, *string]")
// Test constructor template
var constructorBuf bytes.Buffer
@@ -469,9 +723,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) {
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
assert.Contains(t, constructorStr, "isoValue := I.FromZero[string]()")
assert.Contains(t, constructorStr, "isoCount := I.FromZero[int]()")
assert.Contains(t, constructorStr, "isoPointer := I.FromZero[*string]()")
assert.Contains(t, constructorStr, "IO.FromZero[string]()")
assert.Contains(t, constructorStr, "IO.FromZero[int]()")
assert.Contains(t, constructorStr, "IO.FromZero[*string]()")
}
func TestLensCommandFlags(t *testing.T) {
@@ -480,7 +734,7 @@ func TestLensCommandFlags(t *testing.T) {
assert.Equal(t, "lens", cmd.Name)
assert.Equal(t, "generate lens code for annotated structs", cmd.Usage)
assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens")
assert.Contains(t, strings.ToLower(cmd.Description), "lenso")
assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses")
// Check flags
assert.Len(t, cmd.Flags, 3)
@@ -501,3 +755,330 @@ func TestLensCommandFlags(t *testing.T) {
assert.True(t, hasFilename, "should have filename flag")
assert.True(t, hasVerbose, "should have verbose flag")
}
func TestParseFileWithEmbeddedStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// Base struct to be embedded
type Base struct {
ID int
Name string
}
// fp-go:Lens
type Extended struct {
Base
Extra string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Extended struct
extended := structs[0]
assert.Equal(t, "Extended", extended.Name)
assert.Len(t, extended.Fields, 3, "Should have 3 fields: ID, Name (from Base), and Extra")
// Check that embedded fields are promoted
fieldNames := make(map[string]bool)
for _, field := range extended.Fields {
fieldNames[field.Name] = true
}
assert.True(t, fieldNames["ID"], "Should have promoted ID field from Base")
assert.True(t, fieldNames["Name"], "Should have promoted Name field from Base")
assert.True(t, fieldNames["Extra"], "Should have Extra field")
}
func TestGenerateLensHelpersWithEmbeddedStruct(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// Base struct to be embedded
type Address struct {
Street string
City string
}
// fp-go:Lens
type Person struct {
Address
Name string
Age int
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "PersonLenses")
assert.Contains(t, contentStr, "MakePersonLenses")
// Check that embedded fields are included
assert.Contains(t, contentStr, "Street L.Lens[Person, string]", "Should have lens for embedded Street field")
assert.Contains(t, contentStr, "City L.Lens[Person, string]", "Should have lens for embedded City field")
assert.Contains(t, contentStr, "Name L.Lens[Person, string]", "Should have lens for Name field")
assert.Contains(t, contentStr, "Age L.Lens[Person, int]", "Should have lens for Age field")
// Check that optional lenses are also generated for embedded fields
assert.Contains(t, contentStr, "StreetO LO.LensO[Person, string]")
assert.Contains(t, contentStr, "CityO LO.LensO[Person, string]")
}
func TestParseFileWithPointerEmbeddedStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// Base struct to be embedded
type Metadata struct {
CreatedAt string
UpdatedAt string
}
// fp-go:Lens
type Document struct {
*Metadata
Title string
Content string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Document struct
doc := structs[0]
assert.Equal(t, "Document", doc.Name)
assert.Len(t, doc.Fields, 4, "Should have 4 fields: CreatedAt, UpdatedAt (from *Metadata), Title, and Content")
// Check that embedded fields are promoted
fieldNames := make(map[string]bool)
for _, field := range doc.Fields {
fieldNames[field.Name] = true
}
assert.True(t, fieldNames["CreatedAt"], "Should have promoted CreatedAt field from *Metadata")
assert.True(t, fieldNames["UpdatedAt"], "Should have promoted UpdatedAt field from *Metadata")
assert.True(t, fieldNames["Title"], "Should have Title field")
assert.True(t, fieldNames["Content"], "Should have Content field")
}
func TestParseFileWithGenericStruct(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Container[T any] struct {
Value T
Count int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Container struct
container := structs[0]
assert.Equal(t, "Container", container.Name)
assert.Equal(t, "[T any]", container.TypeParams, "Should have type parameter [T any]")
assert.Len(t, container.Fields, 2)
assert.Equal(t, "Value", container.Fields[0].Name)
assert.Equal(t, "T", container.Fields[0].TypeName)
assert.Equal(t, "Count", container.Fields[1].Name)
assert.Equal(t, "int", container.Fields[1].TypeName)
}
func TestParseFileWithMultipleTypeParams(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Pair[K comparable, V any] struct {
Key K
Value V
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Pair struct
pair := structs[0]
assert.Equal(t, "Pair", pair.Name)
assert.Equal(t, "[K comparable, V any]", pair.TypeParams, "Should have type parameters [K comparable, V any]")
assert.Len(t, pair.Fields, 2)
assert.Equal(t, "Key", pair.Fields[0].Name)
assert.Equal(t, "K", pair.Fields[0].TypeName)
assert.Equal(t, "Value", pair.Fields[1].Name)
assert.Equal(t, "V", pair.Fields[1].TypeName)
}
func TestGenerateLensHelpersWithGenericStruct(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type Box[T any] struct {
Content T
Label string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content with type parameters
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "type BoxLenses[T any] struct", "Should have generic BoxLenses type")
assert.Contains(t, contentStr, "type BoxRefLenses[T any] struct", "Should have generic BoxRefLenses type")
assert.Contains(t, contentStr, "func MakeBoxLenses[T any]() BoxLenses[T]", "Should have generic constructor")
assert.Contains(t, contentStr, "func MakeBoxRefLenses[T any]() BoxRefLenses[T]", "Should have generic ref constructor")
// Check that fields use the generic type parameter
assert.Contains(t, contentStr, "Content L.Lens[Box[T], T]", "Should have lens for generic Content field")
assert.Contains(t, contentStr, "Label L.Lens[Box[T], string]", "Should have lens for Label field")
// Check optional lenses - only for comparable types
// T any is not comparable, so ContentO should NOT be generated
assert.NotContains(t, contentStr, "ContentO LO.LensO[Box[T], T]", "T any is not comparable, should not have optional lens")
// string is comparable, so LabelO should be generated
assert.Contains(t, contentStr, "LabelO LO.LensO[Box[T], string]", "string is comparable, should have optional lens")
}
func TestGenerateLensHelpersWithComparableTypeParam(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type ComparableBox[T comparable] struct {
Key T
Value string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content with type parameters
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "type ComparableBoxLenses[T comparable] struct", "Should have generic ComparableBoxLenses type")
assert.Contains(t, contentStr, "type ComparableBoxRefLenses[T comparable] struct", "Should have generic ComparableBoxRefLenses type")
// Check that Key field (with comparable constraint) uses MakeLensStrict in RefLenses
assert.Contains(t, contentStr, "lensKey := L.MakeLensStrict(", "Key field with comparable constraint should use MakeLensStrict")
// Check that Value field (string, always comparable) also uses MakeLensStrict
assert.Contains(t, contentStr, "lensValue := L.MakeLensStrict(", "Value field (string) should use MakeLensStrict")
// Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "L.MakeLensRef(", "Should not use MakeLensRef when all fields are comparable")
}

11
v2/constant/monoid.go Normal file
View File

@@ -0,0 +1,11 @@
package constant
import (
"github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
)
// Monoid returns a [M.Monoid] that returns a constant value in all operations
func Monoid[A any](a A) M.Monoid[A] {
return M.MakeMonoid(function.Constant2[A, A](a), a)
}

View File

@@ -53,12 +53,12 @@ import (
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
RIOEH "github.com/IBM/fp-go/v2/context/readerioresult/http"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/http/builder"
H "github.com/IBM/fp-go/v2/http/headers"
LZ "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
// Requester converts an http/builder.Builder into a ReaderIOResult that produces HTTP requests.
@@ -143,10 +143,10 @@ func Requester(builder *R.Builder) RIOEH.Requester {
return F.Pipe5(
builder.GetBody(),
O.Fold(LZ.Of(E.Of[error](withoutBody)), E.Map[error](withBody)),
E.Ap[func(string) RIOE.ReaderIOResult[*http.Request]](builder.GetTargetURL()),
E.Flap[error, RIOE.ReaderIOResult[*http.Request]](builder.GetMethod()),
E.GetOrElse(RIOE.Left[*http.Request]),
O.Fold(LZ.Of(result.Of(withoutBody)), result.Map(withBody)),
result.Ap[RIOE.Kleisli[string, *http.Request]](builder.GetTargetURL()),
result.Flap[RIOE.ReaderIOResult[*http.Request]](builder.GetMethod()),
result.GetOrElse(RIOE.Left[*http.Request]),
RIOE.Map(func(req *http.Request) *http.Request {
req.Header = H.Monoid.Concat(req.Header, builder.GetHeaders())
return req

View File

@@ -19,13 +19,17 @@ import (
"context"
"time"
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
)
const (
@@ -176,6 +180,11 @@ func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIORe
return RIOR.MonadChainFirst(ma, f)
}
//go:inline
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTap(ma, f)
}
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
// This is the curried version of [MonadChainFirst].
//
@@ -189,6 +198,11 @@ func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirst(f)
}
//go:inline
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.Tap(f)
}
// Of creates a [ReaderIOResult] that always succeeds with the given value.
// This is the same as [Right] and represents the monadic return operation.
//
@@ -399,6 +413,11 @@ func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B])
return RIOR.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
return RIOR.MonadTapEitherK(ma, f)
}
// ChainFirstEitherK chains a function that returns an [Either] but keeps the original value.
// This is the curried version of [MonadChainFirstEitherK].
//
@@ -412,6 +431,11 @@ func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return RIOR.ChainFirstEitherK[context.Context](f)
}
//go:inline
func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return RIOR.TapEitherK[context.Context](f)
}
// ChainOptionK chains a function that returns an [Option] into a [ReaderIOResult] computation.
// If the Option is None, the provided error function is called.
//
@@ -534,6 +558,11 @@ func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderI
return RIOR.MonadChainFirstIOK(ma, f)
}
//go:inline
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
return RIOR.MonadTapIOK(ma, f)
}
// ChainFirstIOK chains a function that returns an [IO] but keeps the original value.
// This is the curried version of [MonadChainFirstIOK].
//
@@ -547,6 +576,11 @@ func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIOR.ChainFirstIOK[context.Context](f)
}
//go:inline
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return RIOR.TapIOK[context.Context](f)
}
// ChainIOEitherK chains a function that returns an [IOResult] into a [ReaderIOResult] computation.
// This is useful for integrating IOResult-returning functions into ReaderIOResult workflows.
//
@@ -747,3 +781,180 @@ func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOResult[A]) Re
func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] {
return RIOR.OrLeft[A](onLeft)
}
//go:inline
func FromReaderEither[A any](ma ReaderEither[context.Context, error, A]) ReaderIOResult[A] {
return RIOR.FromReaderEither(ma)
}
//go:inline
func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
return RIOR.FromReaderEither(ma)
}
//go:inline
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
return RIOR.FromReaderOption[context.Context, A](onNone)
}
//go:inline
func MonadChainReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderK(ma, f)
}
//go:inline
func ChainReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderK(f)
}
//go:inline
func MonadChainFirstReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderK(ma, f)
}
//go:inline
func MonadTapReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderK(ma, f)
}
//go:inline
func ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderK(f)
}
//go:inline
func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderK(f)
}
//go:inline
func MonadChainReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderResultK(ma, f)
}
//go:inline
func ChainReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainReaderResultK(f)
}
//go:inline
func MonadChainFirstReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderResultK(ma, f)
}
//go:inline
func MonadTapReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderResultK(ma, f)
}
//go:inline
func ChainFirstReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderResultK(f)
}
//go:inline
func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.TapReaderResultK(f)
}
//go:inline
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderIOK(ma, f)
}
//go:inline
func ChainReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderIOK(f)
}
//go:inline
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderIOK(f)
}
//go:inline
func TapReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderIOK(f)
}
//go:inline
func ChainReaderOptionK[A, B any](onNone func() 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] {
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] {
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.Read[A](r)
}
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type. If the input is a Right value, it passes through unchanged.
//
//go:inline
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
return RIOR.MonadChainLeft(fa, f)
}
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
//
//go:inline
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] {
return RIOR.ChainLeft(f)
}
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
// If the input is a Left value, it applies the function f to the error and executes the resulting computation,
// but always returns the original Left error regardless of what f returns (Left or Right).
// If the input is a Right value, it passes through unchanged without calling f.
//
// This is useful for side effects on errors (like logging or metrics) where you want to perform an action
// when an error occurs but always propagate the original error, ensuring the error path is preserved.
//
//go:inline
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstLeft(ma, f)
}
//go:inline
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadTapLeft(ma, f)
}
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
// It returns a function that chains a computation on the left (error) side while always preserving the original error.
//
// This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
// in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
// ensuring the error path is preserved.
//
//go:inline
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.ChainFirstLeft[A](f)
}
//go:inline
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.TapLeft[A](f)
}

View File

@@ -24,6 +24,7 @@ import (
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither"
N "github.com/IBM/fp-go/v2/number"
)
var (
@@ -37,21 +38,21 @@ var (
// Benchmark core constructors
func BenchmarkLeft(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Left[int](benchErr)
}
}
func BenchmarkRight(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Right(42)
}
}
func BenchmarkOf(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Of(42)
}
}
@@ -60,7 +61,7 @@ func BenchmarkFromEither_Right(b *testing.B) {
either := E.Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromEither(either)
}
}
@@ -69,7 +70,7 @@ func BenchmarkFromEither_Left(b *testing.B) {
either := E.Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromEither(either)
}
}
@@ -77,7 +78,7 @@ func BenchmarkFromEither_Left(b *testing.B) {
func BenchmarkFromIO(b *testing.B) {
io := func() int { return 42 }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromIO(io)
}
}
@@ -85,7 +86,7 @@ func BenchmarkFromIO(b *testing.B) {
func BenchmarkFromIOEither_Right(b *testing.B) {
ioe := IOE.Of[error](42)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromIOEither(ioe)
}
}
@@ -93,7 +94,7 @@ func BenchmarkFromIOEither_Right(b *testing.B) {
func BenchmarkFromIOEither_Left(b *testing.B) {
ioe := IOE.Left[int](benchErr)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = FromIOEither(ioe)
}
}
@@ -103,7 +104,7 @@ func BenchmarkExecute_Right(b *testing.B) {
rioe := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -112,7 +113,7 @@ func BenchmarkExecute_Left(b *testing.B) {
rioe := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -123,7 +124,7 @@ func BenchmarkExecute_WithContext(b *testing.B) {
defer cancel()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(ctx)()
}
}
@@ -131,40 +132,40 @@ func BenchmarkExecute_WithContext(b *testing.B) {
// Benchmark functor operations
func BenchmarkMonadMap_Right(b *testing.B) {
rioe := Right(42)
mapper := func(a int) int { return a * 2 }
mapper := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadMap(rioe, mapper)
}
}
func BenchmarkMonadMap_Left(b *testing.B) {
rioe := Left[int](benchErr)
mapper := func(a int) int { return a * 2 }
mapper := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadMap(rioe, mapper)
}
}
func BenchmarkMap_Right(b *testing.B) {
rioe := Right(42)
mapper := Map(func(a int) int { return a * 2 })
mapper := Map(N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = mapper(rioe)
}
}
func BenchmarkMap_Left(b *testing.B) {
rioe := Left[int](benchErr)
mapper := Map(func(a int) int { return a * 2 })
mapper := Map(N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = mapper(rioe)
}
}
@@ -174,7 +175,7 @@ func BenchmarkMapTo_Right(b *testing.B) {
mapper := MapTo[int](99)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = mapper(rioe)
}
}
@@ -185,7 +186,7 @@ func BenchmarkMonadChain_Right(b *testing.B) {
chainer := func(a int) ReaderIOResult[int] { return Right(a * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadChain(rioe, chainer)
}
}
@@ -195,7 +196,7 @@ func BenchmarkMonadChain_Left(b *testing.B) {
chainer := func(a int) ReaderIOResult[int] { return Right(a * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadChain(rioe, chainer)
}
}
@@ -205,7 +206,7 @@ func BenchmarkChain_Right(b *testing.B) {
chainer := Chain(func(a int) ReaderIOResult[int] { return Right(a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -215,7 +216,7 @@ func BenchmarkChain_Left(b *testing.B) {
chainer := Chain(func(a int) ReaderIOResult[int] { return Right(a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -225,7 +226,7 @@ func BenchmarkChainFirst_Right(b *testing.B) {
chainer := ChainFirst(func(a int) ReaderIOResult[string] { return Right("logged") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -235,7 +236,7 @@ func BenchmarkChainFirst_Left(b *testing.B) {
chainer := ChainFirst(func(a int) ReaderIOResult[string] { return Right("logged") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -244,7 +245,7 @@ func BenchmarkFlatten_Right(b *testing.B) {
nested := Right(Right(42))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Flatten(nested)
}
}
@@ -253,28 +254,28 @@ func BenchmarkFlatten_Left(b *testing.B) {
nested := Left[ReaderIOResult[int]](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Flatten(nested)
}
}
// Benchmark applicative operations
func BenchmarkMonadApSeq_RightRight(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApSeq(fab, fa)
}
}
func BenchmarkMonadApSeq_RightLeft(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApSeq(fab, fa)
}
}
@@ -284,27 +285,27 @@ func BenchmarkMonadApSeq_LeftRight(b *testing.B) {
fa := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApSeq(fab, fa)
}
}
func BenchmarkMonadApPar_RightRight(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApPar(fab, fa)
}
}
func BenchmarkMonadApPar_RightLeft(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApPar(fab, fa)
}
}
@@ -314,30 +315,30 @@ func BenchmarkMonadApPar_LeftRight(b *testing.B) {
fa := Right(42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = MonadApPar(fab, fa)
}
}
// Benchmark execution of applicative operations
func BenchmarkExecuteApSeq_RightRight(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
rioe := MonadApSeq(fab, fa)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
func BenchmarkExecuteApPar_RightRight(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
rioe := MonadApPar(fab, fa)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -348,7 +349,7 @@ func BenchmarkAlt_RightRight(b *testing.B) {
alternative := Alt(func() ReaderIOResult[int] { return Right(99) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = alternative(rioe)
}
}
@@ -358,7 +359,7 @@ func BenchmarkAlt_LeftRight(b *testing.B) {
alternative := Alt(func() ReaderIOResult[int] { return Right(99) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = alternative(rioe)
}
}
@@ -368,7 +369,7 @@ func BenchmarkOrElse_Right(b *testing.B) {
recover := OrElse(func(e error) ReaderIOResult[int] { return Right(0) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = recover(rioe)
}
}
@@ -378,7 +379,7 @@ func BenchmarkOrElse_Left(b *testing.B) {
recover := OrElse(func(e error) ReaderIOResult[int] { return Right(0) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = recover(rioe)
}
}
@@ -389,7 +390,7 @@ func BenchmarkChainEitherK_Right(b *testing.B) {
chainer := ChainEitherK(func(a int) Either[int] { return E.Right[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -399,7 +400,7 @@ func BenchmarkChainEitherK_Left(b *testing.B) {
chainer := ChainEitherK(func(a int) Either[int] { return E.Right[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -409,7 +410,7 @@ func BenchmarkChainIOK_Right(b *testing.B) {
chainer := ChainIOK(func(a int) func() int { return func() int { return a * 2 } })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -419,7 +420,7 @@ func BenchmarkChainIOK_Left(b *testing.B) {
chainer := ChainIOK(func(a int) func() int { return func() int { return a * 2 } })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -429,7 +430,7 @@ func BenchmarkChainIOEitherK_Right(b *testing.B) {
chainer := ChainIOEitherK(func(a int) IOEither[int] { return IOE.Of[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -439,7 +440,7 @@ func BenchmarkChainIOEitherK_Left(b *testing.B) {
chainer := ChainIOEitherK(func(a int) IOEither[int] { return IOE.Of[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = chainer(rioe)
}
}
@@ -447,7 +448,7 @@ func BenchmarkChainIOEitherK_Left(b *testing.B) {
// Benchmark context operations
func BenchmarkAsk(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Ask()
}
}
@@ -455,7 +456,7 @@ func BenchmarkAsk(b *testing.B) {
func BenchmarkDefer(b *testing.B) {
gen := func() ReaderIOResult[int] { return Right(42) }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Defer(gen)
}
}
@@ -463,7 +464,7 @@ func BenchmarkDefer(b *testing.B) {
func BenchmarkMemoize(b *testing.B) {
rioe := Right(42)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Memoize(rioe)
}
}
@@ -472,14 +473,14 @@ func BenchmarkMemoize(b *testing.B) {
func BenchmarkDelay_Construction(b *testing.B) {
rioe := Right(42)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = Delay[int](time.Millisecond)(rioe)
}
}
func BenchmarkTimer_Construction(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Timer(time.Millisecond)
}
}
@@ -490,7 +491,7 @@ func BenchmarkTryCatch_Success(b *testing.B) {
return func() (int, error) { return 42, nil }
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = TryCatch(f)
}
}
@@ -500,7 +501,7 @@ func BenchmarkTryCatch_Error(b *testing.B) {
return func() (int, error) { return 0, benchErr }
}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = TryCatch(f)
}
}
@@ -512,7 +513,7 @@ func BenchmarkExecuteTryCatch_Success(b *testing.B) {
rioe := TryCatch(f)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -524,7 +525,7 @@ func BenchmarkExecuteTryCatch_Error(b *testing.B) {
rioe := TryCatch(f)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -534,10 +535,10 @@ func BenchmarkPipeline_Map_Right(b *testing.B) {
rioe := Right(21)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe1(
rioe,
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
}
}
@@ -546,10 +547,10 @@ func BenchmarkPipeline_Map_Left(b *testing.B) {
rioe := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe1(
rioe,
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
}
}
@@ -558,7 +559,7 @@ func BenchmarkPipeline_Chain_Right(b *testing.B) {
rioe := Right(21)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe1(
rioe,
Chain(func(x int) ReaderIOResult[int] { return Right(x * 2) }),
@@ -570,7 +571,7 @@ func BenchmarkPipeline_Chain_Left(b *testing.B) {
rioe := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe1(
rioe,
Chain(func(x int) ReaderIOResult[int] { return Right(x * 2) }),
@@ -582,12 +583,12 @@ func BenchmarkPipeline_Complex_Right(b *testing.B) {
rioe := Right(10)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe3(
rioe,
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
Chain(func(x int) ReaderIOResult[int] { return Right(x + 1) }),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
}
}
@@ -596,12 +597,12 @@ func BenchmarkPipeline_Complex_Left(b *testing.B) {
rioe := Left[int](benchErr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchRIOE = F.Pipe3(
rioe,
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
Chain(func(x int) ReaderIOResult[int] { return Right(x + 1) }),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
}
}
@@ -609,13 +610,13 @@ func BenchmarkPipeline_Complex_Left(b *testing.B) {
func BenchmarkExecutePipeline_Complex_Right(b *testing.B) {
rioe := F.Pipe3(
Right(10),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
Chain(func(x int) ReaderIOResult[int] { return Right(x + 1) }),
Map(func(x int) int { return x * 2 }),
Map(N.Mul(2)),
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -624,7 +625,7 @@ func BenchmarkExecutePipeline_Complex_Right(b *testing.B) {
func BenchmarkDo(b *testing.B) {
type State struct{ value int }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Do(State{})
}
}
@@ -642,7 +643,7 @@ func BenchmarkBind_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = binder(initial)
}
}
@@ -658,7 +659,7 @@ func BenchmarkLet_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = letter(initial)
}
}
@@ -674,7 +675,7 @@ func BenchmarkApS_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = aps(initial)
}
}
@@ -687,7 +688,7 @@ func BenchmarkTraverseArray_Empty(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -699,7 +700,7 @@ func BenchmarkTraverseArray_Small(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -714,7 +715,7 @@ func BenchmarkTraverseArray_Medium(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -726,7 +727,7 @@ func BenchmarkTraverseArraySeq_Small(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -738,7 +739,7 @@ func BenchmarkTraverseArrayPar_Small(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(arr)
}
}
@@ -751,7 +752,7 @@ func BenchmarkSequenceArray_Small(b *testing.B) {
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = SequenceArray(arr)
}
}
@@ -763,7 +764,7 @@ func BenchmarkExecuteTraverseArray_Small(b *testing.B) {
})(arr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = rioe(benchCtx)()
}
}
@@ -775,7 +776,7 @@ func BenchmarkExecuteTraverseArraySeq_Small(b *testing.B) {
})(arr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = rioe(benchCtx)()
}
}
@@ -787,7 +788,7 @@ func BenchmarkExecuteTraverseArrayPar_Small(b *testing.B) {
})(arr)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = rioe(benchCtx)()
}
}
@@ -800,7 +801,7 @@ func BenchmarkTraverseRecord_Small(b *testing.B) {
})
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = traverser(rec)
}
}
@@ -813,7 +814,7 @@ func BenchmarkSequenceRecord_Small(b *testing.B) {
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = SequenceRecord(rec)
}
}
@@ -826,7 +827,7 @@ func BenchmarkWithResource_Success(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = WithResource[int](acquire, release)(body)
}
}
@@ -839,7 +840,7 @@ func BenchmarkExecuteWithResource_Success(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -852,7 +853,7 @@ func BenchmarkExecuteWithResource_ErrorInBody(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(benchCtx)()
}
}
@@ -865,13 +866,13 @@ func BenchmarkExecute_CanceledContext(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(ctx)()
}
}
func BenchmarkExecuteApPar_CanceledContext(b *testing.B) {
fab := Right(func(a int) int { return a * 2 })
fab := Right(N.Mul(2))
fa := Right(42)
rioe := MonadApPar(fab, fa)
ctx, cancel := context.WithCancel(benchCtx)
@@ -879,9 +880,7 @@ func BenchmarkExecuteApPar_CanceledContext(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = rioe(ctx)()
}
}
// Made with Bob

File diff suppressed because it is too large Load Diff

View File

@@ -284,3 +284,160 @@ func TestWithResourceErrorInRelease(t *testing.T) {
assert.Equal(t, 0, countRelease)
assert.Equal(t, E.Left[int](err), res)
}
func TestMonadChainFirstLeft(t *testing.T) {
ctx := context.Background()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
sideEffectCalled := false
originalErr := fmt.Errorf("original error")
result := MonadChainFirstLeft(
Left[int](originalErr),
func(e error) ReaderIOResult[int] {
sideEffectCalled = true
return Left[int](fmt.Errorf("new error")) // This error is ignored
},
)
actualResult := result(ctx)()
assert.True(t, sideEffectCalled)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var capturedError error
originalErr := fmt.Errorf("validation failed")
result := MonadChainFirstLeft(
Left[int](originalErr),
func(e error) ReaderIOResult[int] {
capturedError = e
return Right(999) // This Right value is ignored
},
)
actualResult := result(ctx)()
assert.Equal(t, originalErr, capturedError)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
sideEffectCalled := false
result := MonadChainFirstLeft(
Right(42),
func(e error) ReaderIOResult[int] {
sideEffectCalled = true
return Left[int](fmt.Errorf("should not be called"))
},
)
assert.False(t, sideEffectCalled)
assert.Equal(t, E.Right[error](42), result(ctx)())
})
// Test that side effects are executed but original error is always preserved
t.Run("Side effects executed but original error preserved", func(t *testing.T) {
effectCount := 0
originalErr := fmt.Errorf("original error")
result := MonadChainFirstLeft(
Left[int](originalErr),
func(e error) ReaderIOResult[int] {
effectCount++
// Try to return Right, but original Left should still be returned
return Right(999)
},
)
actualResult := result(ctx)()
assert.Equal(t, 1, effectCount)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
}
func TestChainFirstLeft(t *testing.T) {
ctx := context.Background()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {
var captured error
originalErr := fmt.Errorf("test error")
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
captured = e
return Left[int](fmt.Errorf("ignored error"))
})
result := F.Pipe1(
Left[int](originalErr),
chainFn,
)
actualResult := result(ctx)()
assert.Equal(t, originalErr, captured)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Left value - function returns Right, still returns original Left
t.Run("Left value with function returning Right still returns original Left", func(t *testing.T) {
var captured error
originalErr := fmt.Errorf("test error")
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
captured = e
return Right(42) // This Right is ignored
})
result := F.Pipe1(
Left[int](originalErr),
chainFn,
)
actualResult := result(ctx)()
assert.Equal(t, originalErr, captured)
assert.Equal(t, E.Left[int](originalErr), actualResult)
})
// Test with Right value - should pass through without calling function
t.Run("Right value passes through", func(t *testing.T) {
called := false
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
called = true
return Right(0)
})
result := F.Pipe1(
Right(100),
chainFn,
)
assert.False(t, called)
assert.Equal(t, E.Right[error](100), result(ctx)())
})
// Test that original error is always preserved regardless of what f returns
t.Run("Original error always preserved", func(t *testing.T) {
originalErr := fmt.Errorf("original")
chainFn := ChainFirstLeft[int](func(e error) ReaderIOResult[int] {
// Try to return Right, but original Left should still be returned
return Right(999)
})
result := F.Pipe1(
Left[int](originalErr),
chainFn,
)
assert.Equal(t, E.Left[int](originalErr), result(ctx)())
})
// Test logging with Left preservation
t.Run("Logging with Left preservation", func(t *testing.T) {
errorLog := []string{}
originalErr := fmt.Errorf("step1")
logError := ChainFirstLeft[string](func(e error) ReaderIOResult[string] {
errorLog = append(errorLog, "Logged: "+e.Error())
return Left[string](fmt.Errorf("log entry")) // This is ignored
})
result := F.Pipe2(
Left[string](originalErr),
logError,
ChainLeft(func(e error) ReaderIOResult[string] {
return Left[string](fmt.Errorf("step2"))
}),
)
actualResult := result(ctx)()
assert.Equal(t, []string{"Logged: step1"}, errorLog)
assert.Equal(t, E.Left[string](fmt.Errorf("step2")), actualResult)
})
}

View File

@@ -16,8 +16,8 @@
package readerioresult
import (
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/internal/record"
)
@@ -29,7 +29,7 @@ import (
//
// Returns a function that transforms an array into a ReaderIOResult of an array.
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
return array.Traverse(
Of[[]B],
Map[[]B, func(B) []B],
Ap[[]B, B],
@@ -46,7 +46,7 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
//
// Returns a function that transforms an array into a ReaderIOResult of an array.
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderIOResult[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
return array.TraverseWithIndex(
Of[[]B],
Map[[]B, func(B) []B],
Ap[[]B, B],
@@ -135,22 +135,20 @@ func MonadTraverseArraySeq[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
//
// Returns a function that transforms an array into a ReaderIOResult of an array.
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
return array.Traverse(
Of[[]B],
Map[[]B, func(B) []B],
ApSeq[[]B, B],
f,
)
}
// TraverseArrayWithIndexSeq uses transforms an array [[]A] into [[]ReaderIOResult[B]] and then resolves that into a [ReaderIOResult[[]B]]
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) ReaderIOResult[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
return array.TraverseWithIndex(
Of[[]B],
Map[[]B, func(B) []B],
ApSeq[[]B, B],
f,
)
}
@@ -230,22 +228,20 @@ func MonadTraverseArrayPar[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
//
// Returns a function that transforms an array into a ReaderIOResult of an array.
func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
return array.Traverse(
Of[[]B],
Map[[]B, func(B) []B],
ApPar[[]B, B],
f,
)
}
// TraverseArrayWithIndexPar uses transforms an array [[]A] into [[]ReaderIOResult[B]] and then resolves that into a [ReaderIOResult[[]B]]
func TraverseArrayWithIndexPar[A, B any](f func(int, A) ReaderIOResult[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
return array.TraverseWithIndex(
Of[[]B],
Map[[]B, func(B) []B],
ApPar[[]B, B],
f,
)
}

View File

@@ -19,14 +19,17 @@ import (
"context"
"github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
)
@@ -119,4 +122,8 @@ type (
// // Apply the transformation
// 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]
)

View File

@@ -92,3 +92,8 @@ func MonadFlap[B, A any](fab ReaderResult[func(A) B], a A) ReaderResult[B] {
func Flap[B, A any](a A) Operator[func(A) B, B] {
return readereither.Flap[context.Context, error, B](a)
}
//go:inline
func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
return readereither.Read[error, A](r)
}

View File

@@ -23,11 +23,13 @@ import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/result"
)
type (
Option[A any] = option.Option[A]
Either[A any] = either.Either[error, A]
Result[A any] = result.Result[A]
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]

8340
v2/coverage.out Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ import (
DIE "github.com/IBM/fp-go/v2/di/erasure"
F "github.com/IBM/fp-go/v2/function"
IO "github.com/IBM/fp-go/v2/io"
IOE "github.com/IBM/fp-go/v2/ioeither"
IOR "github.com/IBM/fp-go/v2/ioresult"
)
var (
@@ -34,5 +34,5 @@ var (
var RunMain = F.Flow3(
DIE.MakeInjector,
Main,
IOE.Fold(IO.Of[error], F.Constant1[any](IO.Of[error](nil))),
IOR.Fold(IO.Of[error], F.Constant1[any](IO.Of[error](nil))),
)

View File

@@ -64,8 +64,8 @@ Creating and using dependencies:
dbProvider := di.MakeProvider1(
DBToken,
ConfigToken.Identity(),
func(cfg Config) IOE.IOEither[error, Database] {
return IOE.Of[error](NewDatabase(cfg))
func(cfg Config) IOResult[Database] {
return ioresult.Of(NewDatabase(cfg))
},
)
@@ -73,8 +73,8 @@ Creating and using dependencies:
APIToken,
ConfigToken.Identity(),
DBToken.Identity(),
func(cfg Config, db Database) IOE.IOEither[error, APIService] {
return IOE.Of[error](NewAPIService(cfg, db))
func(cfg Config, db Database) IOResult[APIService] {
return ioresult.Of(NewAPIService(cfg, db))
},
)
@@ -116,7 +116,7 @@ MakeProvider0 - No dependencies:
provider := di.MakeProvider0(
token,
IOE.Of[error](value),
ioresult.Of(value),
)
MakeProvider1 - One dependency:
@@ -124,8 +124,8 @@ MakeProvider1 - One dependency:
provider := di.MakeProvider1(
resultToken,
dep1Token.Identity(),
func(dep1 Dep1Type) IOE.IOEither[error, ResultType] {
return IOE.Of[error](createResult(dep1))
func(dep1 Dep1Type) IOResult[ResultType] {
return ioresult.Of(createResult(dep1))
},
)
@@ -135,8 +135,8 @@ MakeProvider2 - Two dependencies:
resultToken,
dep1Token.Identity(),
dep2Token.Identity(),
func(dep1 Dep1Type, dep2 Dep2Type) IOE.IOEither[error, ResultType] {
return IOE.Of[error](createResult(dep1, dep2))
func(dep1 Dep1Type, dep2 Dep2Type) IOResult[ResultType] {
return ioresult.Of(createResult(dep1, dep2))
},
)
@@ -153,7 +153,7 @@ provider is registered:
token := di.MakeTokenWithDefault0(
"ServiceName",
IOE.Of[error](defaultImplementation),
ioresult.Of(defaultImplementation),
)
// Or with dependencies
@@ -161,8 +161,8 @@ provider is registered:
"ServiceName",
dep1Token.Identity(),
dep2Token.Identity(),
func(dep1 Dep1Type, dep2 Dep2Type) IOE.IOEither[error, ResultType] {
return IOE.Of[error](createDefault(dep1, dep2))
func(dep1 Dep1Type, dep2 Dep2Type) IOResult[ResultType] {
return ioresult.Of(createDefault(dep1, dep2))
},
)
@@ -208,8 +208,8 @@ The framework provides a convenient pattern for running applications:
mainProvider := di.MakeProvider1(
di.InjMain,
APIToken.Identity(),
func(api APIService) IOE.IOEither[error, any] {
return IOE.Of[error](api.Start())
func(api APIService) IOResult[any] {
return ioresult.Of(api.Start())
},
)
@@ -247,8 +247,8 @@ Example 1: Configuration-based Service
clientProvider := di.MakeProvider1(
ClientToken,
ConfigToken.Identity(),
func(cfg Config) IOE.IOEither[error, HTTPClient] {
return IOE.Of[error](HTTPClient{config: cfg})
func(cfg Config) IOResult[HTTPClient] {
return ioresult.Of(HTTPClient{config: cfg})
},
)
@@ -263,8 +263,8 @@ Example 2: Optional Dependencies
serviceProvider := di.MakeProvider1(
ServiceToken,
CacheToken.Option(), // Optional dependency
func(cache O.Option[Cache]) IOE.IOEither[error, Service] {
return IOE.Of[error](NewService(cache))
func(cache Option[Cache]) IOResult[Service] {
return ioresult.Of(NewService(cache))
},
)
@@ -279,8 +279,8 @@ Example 3: Lazy Dependencies
reporterProvider := di.MakeProvider1(
ReporterToken,
DBToken.IOEither(), // Lazy dependency
func(dbIO IOE.IOEither[error, Database]) IOE.IOEither[error, Reporter] {
return IOE.Of[error](NewReporter(dbIO))
func(dbIO IOResult[Database]) IOResult[Reporter] {
return ioresult.Of(NewReporter(dbIO))
},
)

View File

@@ -20,7 +20,7 @@ import (
"github.com/IBM/fp-go/v2/errors"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/identity"
IOE "github.com/IBM/fp-go/v2/ioeither"
IOR "github.com/IBM/fp-go/v2/ioresult"
L "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/record"
@@ -42,8 +42,8 @@ var (
missingProviderError = F.Flow4(
Dependency.String,
errors.OnSome[string]("no provider for dependency [%s]"),
IOE.Left[any, error],
F.Constant1[InjectableFactory, IOE.IOEither[error, any]],
IOR.Left[any],
F.Constant1[InjectableFactory, IOResult[any]],
)
// missingProviderErrorOrDefault returns the default [ProviderFactory] or an error
@@ -56,7 +56,7 @@ var (
emptyMulti any = A.Empty[any]()
// emptyMultiDependency returns a [ProviderFactory] for an empty, multi dependency
emptyMultiDependency = F.Constant1[Dependency](F.Constant1[InjectableFactory](IOE.Of[error](emptyMulti)))
emptyMultiDependency = F.Constant1[Dependency](F.Constant1[InjectableFactory](IOR.Of(emptyMulti)))
// handleMissingProvider covers the case of a missing provider. It either
// returns an error or an empty multi value provider
@@ -93,21 +93,21 @@ var (
// isMultiDependency tests if a dependency is a container dependency
func isMultiDependency(dep Dependency) bool {
return dep.Flag()&Multi == Multi
return dep.Flag()&MULTI == MULTI
}
// isItemProvider tests if a provivder provides a single item
func isItemProvider(provider Provider) bool {
return provider.Provides().Flag()&Item == Item
return provider.Provides().Flag()&ITEM == ITEM
}
// itemProviderFactory combines multiple factories into one, returning an array
func itemProviderFactory(fcts []ProviderFactory) ProviderFactory {
return func(inj InjectableFactory) IOE.IOEither[error, any] {
return func(inj InjectableFactory) IOResult[any] {
return F.Pipe2(
fcts,
IOE.TraverseArray(I.Flap[IOE.IOEither[error, any]](inj)),
IOE.Map[error](F.ToAny[[]any]),
IOR.TraverseArray(I.Flap[IOResult[any]](inj)),
IOR.Map(F.ToAny[[]any]),
)
}
}
@@ -118,7 +118,7 @@ func itemProviderFactory(fcts []ProviderFactory) ProviderFactory {
// makes sure to transitively resolve the required dependencies.
func MakeInjector(providers []Provider) InjectableFactory {
type Result = IOE.IOEither[error, any]
type Result = IOResult[any]
type LazyResult = L.Lazy[Result]
// resolved stores the values resolved so far, key is the string ID
@@ -148,11 +148,11 @@ func MakeInjector(providers []Provider) InjectableFactory {
T.Map2(F.Flow3(
Dependency.Id,
R.Lookup[ProviderFactory, string],
I.Ap[O.Option[ProviderFactory]](factoryByID),
I.Ap[Option[ProviderFactory]](factoryByID),
), handleMissingProvider),
T.Tupled2(O.MonadGetOrElse[ProviderFactory]),
I.Ap[IOE.IOEither[error, any]](injFct),
IOE.Memoize[error, any],
I.Ap[IOResult[any]](injFct),
IOR.Memoize[any],
)
}

View File

@@ -19,25 +19,23 @@ import (
"fmt"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/identity"
IO "github.com/IBM/fp-go/v2/io"
IOE "github.com/IBM/fp-go/v2/ioeither"
IOO "github.com/IBM/fp-go/v2/iooption"
Int "github.com/IBM/fp-go/v2/number/integer"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/record"
"github.com/IBM/fp-go/v2/result"
)
type (
// InjectableFactory is a factory function that can create an untyped instance of a service based on its [Dependency] identifier
InjectableFactory = func(Dependency) IOE.IOEither[error, any]
ProviderFactory = func(InjectableFactory) IOE.IOEither[error, any]
InjectableFactory = func(Dependency) IOResult[any]
ProviderFactory = func(InjectableFactory) IOResult[any]
paramIndex = map[int]int
paramValue = map[int]any
handler = func(paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue]
handler = func(paramIndex) func([]IOResult[any]) IOResult[paramValue]
mapping = map[int]paramIndex
Provider interface {
@@ -83,50 +81,50 @@ var (
mergeMaps = R.UnionLastMonoid[int, any]()
collectParams = R.CollectOrd[any, any](Int.Ord)(F.SK[int, any])
mapDeps = F.Curry2(A.MonadMap[Dependency, IOE.IOEither[error, any]])
mapDeps = F.Curry2(A.MonadMap[Dependency, IOResult[any]])
handlers = map[int]handler{
Identity: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
IDENTITY: func(mp paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return func(res []IOResult[any]) IOResult[paramValue] {
return F.Pipe1(
mp,
IOE.TraverseRecord[int](getAt(res)),
)
}
},
Option: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
OPTION: func(mp paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return func(res []IOResult[any]) IOResult[paramValue] {
return F.Pipe3(
mp,
IO.TraverseRecord[int](getAt(res)),
IO.Map(R.Map[int](F.Flow2(
E.ToOption[error, any],
F.ToAny[O.Option[any]],
result.ToOption[any],
F.ToAny[Option[any]],
))),
IOE.FromIO[error, paramValue],
)
}
},
IOEither: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
IOEITHER: func(mp paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return func(res []IOResult[any]) IOResult[paramValue] {
return F.Pipe2(
mp,
R.Map[int](F.Flow2(
getAt(res),
F.ToAny[IOE.IOEither[error, any]],
F.ToAny[IOResult[any]],
)),
IOE.Of[error, paramValue],
)
}
},
IOOption: func(mp paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
IOOPTION: func(mp paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return func(res []IOResult[any]) IOResult[paramValue] {
return F.Pipe2(
mp,
R.Map[int](F.Flow3(
getAt(res),
IOE.ToIOOption[error, any],
F.ToAny[IOO.IOOption[any]],
F.ToAny[IOOption[any]],
)),
IOE.Of[error, paramValue],
)
@@ -141,23 +139,23 @@ func getAt[T any](ar []T) func(idx int) T {
}
}
func handleMapping(mp mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither[error, []any] {
func handleMapping(mp mapping) func(res []IOResult[any]) IOResult[[]any] {
preFct := F.Pipe1(
mp,
R.Collect(func(idx int, p paramIndex) func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue] {
R.Collect(func(idx int, p paramIndex) func([]IOResult[any]) IOResult[paramValue] {
return handlers[idx](p)
}),
)
doFct := F.Flow2(
I.Flap[IOE.IOEither[error, paramValue], []IOE.IOEither[error, any]],
IOE.TraverseArray[error, func([]IOE.IOEither[error, any]) IOE.IOEither[error, paramValue], paramValue],
I.Flap[IOResult[paramValue], []IOResult[any]],
IOE.TraverseArray[error, func([]IOResult[any]) IOResult[paramValue], paramValue],
)
postFct := IOE.Map[error](F.Flow2(
A.Fold(mergeMaps),
collectParams,
))
return func(res []IOE.IOEither[error, any]) IOE.IOEither[error, []any] {
return func(res []IOResult[any]) IOResult[[]any] {
return F.Pipe2(
preFct,
doFct(res),
@@ -170,7 +168,7 @@ func handleMapping(mp mapping) func(res []IOE.IOEither[error, any]) IOE.IOEither
// a function that accepts the resolved dependencies to return a result
func MakeProviderFactory(
deps []Dependency,
fct func(param ...any) IOE.IOEither[error, any]) ProviderFactory {
fct func(param ...any) IOResult[any]) ProviderFactory {
return F.Flow3(
mapDeps(deps),

View File

@@ -17,20 +17,18 @@ package erasure
import (
"fmt"
O "github.com/IBM/fp-go/v2/option"
)
const (
BehaviourMask = 0x0f
Identity = 0 // required dependency
Option = 1 // optional dependency
IOEither = 2 // lazy and required
IOOption = 3 // lazy and optional
IDENTITY = 0 // required dependency
OPTION = 1 // optional dependency
IOEITHER = 2 // lazy and required
IOOPTION = 3 // lazy and optional
TypeMask = 0xf0
Multi = 1 << 4 // array of implementations
Item = 2 << 4 // item of a multi token
MULTI = 1 << 4 // array of implementations
ITEM = 2 << 4 // item of a multi token
)
// Dependency describes the relationship to a service
@@ -41,5 +39,5 @@ type Dependency interface {
// Flag returns a tag that identifies the behaviour of the dependency
Flag() int
// ProviderFactory optionally returns an attached [ProviderFactory] that represents the default for this dependency
ProviderFactory() O.Option[ProviderFactory]
ProviderFactory() Option[ProviderFactory]
}

13
v2/di/erasure/types.go Normal file
View File

@@ -0,0 +1,13 @@
package erasure
import (
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
)
type (
Option[T any] = option.Option[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
)

File diff suppressed because it is too large Load Diff

View File

@@ -19,14 +19,14 @@ import (
DIE "github.com/IBM/fp-go/v2/di/erasure"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity"
IOE "github.com/IBM/fp-go/v2/ioeither"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
IOR "github.com/IBM/fp-go/v2/ioresult"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
// Resolve performs a type safe resolution of a dependency
func Resolve[T any](token InjectionToken[T]) RIOE.ReaderIOEither[DIE.InjectableFactory, error, T] {
func Resolve[T any](token InjectionToken[T]) RIOR.ReaderIOResult[DIE.InjectableFactory, T] {
return F.Flow2(
identity.Ap[IOE.IOEither[error, any]](asDependency(token)),
IOE.ChainEitherK(token.Unerase),
identity.Ap[IOResult[any]](asDependency(token)),
IOR.ChainResultK(token.Unerase),
)
}

View File

@@ -22,9 +22,10 @@ import (
"github.com/IBM/fp-go/v2/errors"
F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
)
func lookupAt[T any](idx int, token Dependency[T]) func(params []any) E.Either[error, T] {
func lookupAt[T any](idx int, token Dependency[T]) func(params []any) Result[T] {
return F.Flow3(
A.Lookup[any](idx),
E.FromOption[any](errors.OnNone("No parameter at position %d", idx)),
@@ -32,7 +33,7 @@ func lookupAt[T any](idx int, token Dependency[T]) func(params []any) E.Either[e
)
}
func eraseTuple[A, R any](f func(A) IOE.IOEither[error, R]) func(E.Either[error, A]) IOE.IOEither[error, any] {
func eraseTuple[A, R any](f func(A) IOResult[R]) func(Result[A]) IOResult[any] {
return F.Flow3(
IOE.FromEither[error, A],
IOE.Chain(f),
@@ -40,8 +41,8 @@ func eraseTuple[A, R any](f func(A) IOE.IOEither[error, R]) func(E.Either[error,
)
}
func eraseProviderFactory0[R any](f IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] {
return func(_ ...any) IOE.IOEither[error, any] {
func eraseProviderFactory0[R any](f IOResult[R]) func(params ...any) IOResult[any] {
return func(_ ...any) IOResult[any] {
return F.Pipe1(
f,
IOE.Map[error](F.ToAny[R]),
@@ -50,7 +51,7 @@ func eraseProviderFactory0[R any](f IOE.IOEither[error, R]) func(params ...any)
}
func MakeProviderFactory0[R any](
fct IOE.IOEither[error, R],
fct IOResult[R],
) DIE.ProviderFactory {
return DIE.MakeProviderFactory(
A.Empty[DIE.Dependency](),
@@ -59,13 +60,13 @@ func MakeProviderFactory0[R any](
}
// MakeTokenWithDefault0 creates a unique [InjectionToken] for a specific type with an attached default [DIE.Provider]
func MakeTokenWithDefault0[R any](name string, fct IOE.IOEither[error, R]) InjectionToken[R] {
func MakeTokenWithDefault0[R any](name string, fct IOResult[R]) InjectionToken[R] {
return MakeTokenWithDefault[R](name, MakeProviderFactory0(fct))
}
func MakeProvider0[R any](
token InjectionToken[R],
fct IOE.IOEither[error, R],
fct IOResult[R],
) DIE.Provider {
return DIE.MakeProvider(
token,
@@ -75,5 +76,5 @@ func MakeProvider0[R any](
// ConstProvider simple implementation for a provider with a constant value
func ConstProvider[R any](token InjectionToken[R], value R) DIE.Provider {
return MakeProvider0(token, IOE.Of[error](value))
return MakeProvider0(token, ioresult.Of(value))
}

View File

@@ -25,7 +25,8 @@ import (
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -39,19 +40,19 @@ func TestSimpleProvider(t *testing.T) {
var staticCount int
staticValue := func(value string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
staticValue := func(value string) IOResult[string] {
return func() Result[string] {
staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
return result.Of(fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
}
}
var dynamicCount int
dynamicValue := func(value string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
dynamicValue := func(value string) IOResult[string] {
return func() Result[string] {
dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
}
}
@@ -81,19 +82,19 @@ func TestOptionalProvider(t *testing.T) {
var staticCount int
staticValue := func(value string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
staticValue := func(value string) IOResult[string] {
return func() Result[string] {
staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
return result.Of(fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
}
}
var dynamicCount int
dynamicValue := func(value O.Option[string]) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
dynamicValue := func(value Option[string]) IOResult[string] {
return func() Result[string] {
dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
}
}
@@ -123,10 +124,10 @@ func TestOptionalProviderMissingDependency(t *testing.T) {
var dynamicCount int
dynamicValue := func(value O.Option[string]) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
dynamicValue := func(value Option[string]) IOResult[string] {
return func() Result[string] {
dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
}
}
@@ -151,10 +152,10 @@ func TestProviderMissingDependency(t *testing.T) {
var dynamicCount int
dynamicValue := func(value string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
dynamicValue := func(value string) IOResult[string] {
return func() Result[string] {
dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
}
}
@@ -179,31 +180,31 @@ func TestEagerAndLazyProvider(t *testing.T) {
var staticCount int
staticValue := func(value string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
staticValue := func(value string) IOResult[string] {
return func() Result[string] {
staticCount++
return E.Of[error](fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
return result.Of(fmt.Sprintf("Static based on [%s], at [%s]", value, time.Now()))
}
}
var dynamicCount int
dynamicValue := func(value string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
dynamicValue := func(value string) IOResult[string] {
return func() Result[string] {
dynamicCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
return result.Of(fmt.Sprintf("Dynamic based on [%s] at [%s]", value, time.Now()))
}
}
var lazyEagerCount int
lazyEager := func(laz IOE.IOEither[error, string], eager string) IOE.IOEither[error, string] {
lazyEager := func(laz IOResult[string], eager string) IOResult[string] {
return F.Pipe1(
laz,
IOE.Chain(func(lazValue string) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
IOE.Chain(func(lazValue string) IOResult[string] {
return func() Result[string] {
lazyEagerCount++
return E.Of[error](fmt.Sprintf("Dynamic based on [%s], [%s] at [%s]", lazValue, eager, time.Now()))
return result.Of(fmt.Sprintf("Dynamic based on [%s], [%s] at [%s]", lazValue, eager, time.Now()))
}
}),
)
@@ -248,7 +249,7 @@ func TestItemProvider(t *testing.T) {
value := multiInj()
assert.Equal(t, E.Of[error](A.From("Value1", "Value2")), value)
assert.Equal(t, result.Of(A.From("Value1", "Value2")), value)
}
func TestEmptyItemProvider(t *testing.T) {
@@ -269,7 +270,7 @@ func TestEmptyItemProvider(t *testing.T) {
value := multiInj()
assert.Equal(t, E.Of[error](A.Empty[string]()), value)
assert.Equal(t, result.Of(A.Empty[string]()), value)
}
func TestDependencyOnMultiProvider(t *testing.T) {
@@ -283,8 +284,8 @@ func TestDependencyOnMultiProvider(t *testing.T) {
p1 := ConstProvider(INJ_KEY1, "Value3")
p2 := ConstProvider(INJ_KEY2, "Value4")
fromMulti := func(val string, multi []string) IOE.IOEither[error, string] {
return IOE.Of[error](fmt.Sprintf("Val: %s, Multi: %s", val, multi))
fromMulti := func(val string, multi []string) IOResult[string] {
return ioresult.Of(fmt.Sprintf("Val: %s, Multi: %s", val, multi))
}
p3 := MakeProvider2(INJ_KEY3, INJ_KEY1.Identity(), injMulti.Container().Identity(), fromMulti)
@@ -295,19 +296,19 @@ func TestDependencyOnMultiProvider(t *testing.T) {
v := r3(inj)()
assert.Equal(t, E.Of[error]("Val: Value3, Multi: [Value1 Value2]"), v)
assert.Equal(t, result.Of("Val: Value3, Multi: [Value1 Value2]"), v)
}
func TestTokenWithDefaultProvider(t *testing.T) {
// token without a default
injToken1 := MakeToken[string]("Token1")
// token with a default
injToken2 := MakeTokenWithDefault0("Token2", IOE.Of[error]("Carsten"))
injToken2 := MakeTokenWithDefault0("Token2", ioresult.Of("Carsten"))
// dependency
injToken3 := MakeToken[string]("Token3")
p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOE.IOEither[error, string] {
return IOE.Of[error](fmt.Sprintf("Token: %s", data))
p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOResult[string] {
return ioresult.Of(fmt.Sprintf("Token: %s", data))
})
// populate the injector
@@ -320,19 +321,19 @@ func TestTokenWithDefaultProvider(t *testing.T) {
// inj1 should not be available
assert.True(t, E.IsLeft(r1(inj)()))
// r3 should work
assert.Equal(t, E.Of[error]("Token: Carsten"), r3(inj)())
assert.Equal(t, result.Of("Token: Carsten"), r3(inj)())
}
func TestTokenWithDefaultProviderAndOverride(t *testing.T) {
// token with a default
injToken2 := MakeTokenWithDefault0("Token2", IOE.Of[error]("Carsten"))
injToken2 := MakeTokenWithDefault0("Token2", ioresult.Of("Carsten"))
// dependency
injToken3 := MakeToken[string]("Token3")
p2 := ConstProvider(injToken2, "Override")
p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOE.IOEither[error, string] {
return IOE.Of[error](fmt.Sprintf("Token: %s", data))
p3 := MakeProvider1(injToken3, injToken2.Identity(), func(data string) IOResult[string] {
return ioresult.Of(fmt.Sprintf("Token: %s", data))
})
// populate the injector
@@ -342,5 +343,5 @@ func TestTokenWithDefaultProviderAndOverride(t *testing.T) {
r3 := Resolve(injToken3)
// r3 should work
assert.Equal(t, E.Of[error]("Token: Override"), r3(inj)())
assert.Equal(t, result.Of("Token: Override"), r3(inj)())
}

View File

@@ -21,10 +21,7 @@ import (
"sync/atomic"
DIE "github.com/IBM/fp-go/v2/di/erasure"
E "github.com/IBM/fp-go/v2/either"
IO "github.com/IBM/fp-go/v2/io"
IOE "github.com/IBM/fp-go/v2/ioeither"
IOO "github.com/IBM/fp-go/v2/iooption"
O "github.com/IBM/fp-go/v2/option"
)
@@ -33,7 +30,7 @@ import (
type Dependency[T any] interface {
DIE.Dependency
// Unerase converts a value with erased type signature into a strongly typed value
Unerase(val any) E.Either[error, T]
Unerase(val any) Result[T]
}
// InjectionToken uniquely identifies a dependency by giving it an Id, Type and name
@@ -42,17 +39,17 @@ type InjectionToken[T any] interface {
// Identity idenifies this dependency as a mandatory, required dependency, it will be resolved eagerly and injected as `T`.
// If the dependency cannot be resolved, the resolution process fails
Identity() Dependency[T]
// Option identifies this dependency as optional, it will be resolved eagerly and injected as [O.Option[T]].
// Option identifies this dependency as optional, it will be resolved eagerly and injected as [Option[T]].
// If the dependency cannot be resolved, the resolution process continues and the dependency is represented as [O.None[T]]
Option() Dependency[O.Option[T]]
// IOEither identifies this dependency as mandatory but it will be resolved lazily as a [IOE.IOEither[error, T]]. This
Option() Dependency[Option[T]]
// IOEither identifies this dependency as mandatory but it will be resolved lazily as a [IOResult[T]]. This
// value is memoized to make sure the dependency is a singleton.
// If the dependency cannot be resolved, the resolution process fails
IOEither() Dependency[IOE.IOEither[error, T]]
// IOOption identifies this dependency as optional but it will be resolved lazily as a [IOO.IOOption[T]]. This
IOEither() Dependency[IOResult[T]]
// IOOption identifies this dependency as optional but it will be resolved lazily as a [IOOption[T]]. This
// value is memoized to make sure the dependency is a singleton.
// If the dependency cannot be resolved, the resolution process continues and the dependency is represented as the none value.
IOOption() Dependency[IOO.IOOption[T]]
IOOption() Dependency[IOOption[T]]
}
// MultiInjectionToken uniquely identifies a dependency by giving it an Id, Type and name that can have multiple implementations.
@@ -79,12 +76,12 @@ type tokenBase struct {
name string
id string
flag int
providerFactory O.Option[DIE.ProviderFactory]
providerFactory Option[DIE.ProviderFactory]
}
type token[T any] struct {
base *tokenBase
toType func(val any) E.Either[error, T]
toType func(val any) Result[T]
}
func (t *token[T]) Id() string {
@@ -99,26 +96,26 @@ func (t *token[T]) String() string {
return t.base.name
}
func (t *token[T]) Unerase(val any) E.Either[error, T] {
func (t *token[T]) Unerase(val any) Result[T] {
return t.toType(val)
}
func (t *token[T]) ProviderFactory() O.Option[DIE.ProviderFactory] {
func (t *token[T]) ProviderFactory() Option[DIE.ProviderFactory] {
return t.base.providerFactory
}
func makeTokenBase(name string, id string, typ int, providerFactory O.Option[DIE.ProviderFactory]) *tokenBase {
func makeTokenBase(name string, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase {
return &tokenBase{name, id, typ, providerFactory}
}
func makeToken[T any](name string, id string, typ int, unerase func(val any) E.Either[error, T], providerFactory O.Option[DIE.ProviderFactory]) Dependency[T] {
func makeToken[T any](name string, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] {
return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase}
}
type injectionToken[T any] struct {
token[T]
option Dependency[O.Option[T]]
ioeither Dependency[IOE.IOEither[error, T]]
iooption Dependency[IOO.IOOption[T]]
option Dependency[Option[T]]
ioeither Dependency[IOResult[T]]
iooption Dependency[IOOption[T]]
}
type multiInjectionToken[T any] struct {
@@ -130,19 +127,19 @@ func (i *injectionToken[T]) Identity() Dependency[T] {
return i
}
func (i *injectionToken[T]) Option() Dependency[O.Option[T]] {
func (i *injectionToken[T]) Option() Dependency[Option[T]] {
return i.option
}
func (i *injectionToken[T]) IOEither() Dependency[IOE.IOEither[error, T]] {
func (i *injectionToken[T]) IOEither() Dependency[IOResult[T]] {
return i.ioeither
}
func (i *injectionToken[T]) IOOption() Dependency[IOO.IOOption[T]] {
func (i *injectionToken[T]) IOOption() Dependency[IOOption[T]] {
return i.iooption
}
func (i *injectionToken[T]) ProviderFactory() O.Option[DIE.ProviderFactory] {
func (i *injectionToken[T]) ProviderFactory() Option[DIE.ProviderFactory] {
return i.base.providerFactory
}
@@ -155,14 +152,14 @@ func (m *multiInjectionToken[T]) Item() InjectionToken[T] {
}
// makeToken create a unique [InjectionToken] for a specific type
func makeInjectionToken[T any](name string, providerFactory O.Option[DIE.ProviderFactory]) InjectionToken[T] {
func makeInjectionToken[T any](name string, providerFactory Option[DIE.ProviderFactory]) InjectionToken[T] {
id := genID()
toIdentity := toType[T]()
return &injectionToken[T]{
token[T]{makeTokenBase(name, id, DIE.Identity, providerFactory), toIdentity},
makeToken(fmt.Sprintf("Option[%s]", name), id, DIE.Option, toOptionType(toIdentity), providerFactory),
makeToken(fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEither, toIOEitherType(toIdentity), providerFactory),
makeToken(fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOption, toIOOptionType(toIdentity), providerFactory),
token[T]{makeTokenBase(name, id, DIE.IDENTITY, providerFactory), toIdentity},
makeToken(fmt.Sprintf("Option[%s]", name), id, DIE.OPTION, toOptionType(toIdentity), providerFactory),
makeToken(fmt.Sprintf("IOEither[%s]", name), id, DIE.IOEITHER, toIOEitherType(toIdentity), providerFactory),
makeToken(fmt.Sprintf("IOOption[%s]", name), id, DIE.IOOPTION, toIOOptionType(toIdentity), providerFactory),
}
}
@@ -187,17 +184,17 @@ func MakeMultiToken[T any](name string) MultiInjectionToken[T] {
providerFactory := O.None[DIE.ProviderFactory]()
// container
container := &injectionToken[[]T]{
token[[]T]{makeTokenBase(containerName, id, DIE.Multi|DIE.Identity, providerFactory), toContainer},
makeToken(fmt.Sprintf("Option[%s]", containerName), id, DIE.Multi|DIE.Option, toOptionType(toContainer), providerFactory),
makeToken(fmt.Sprintf("IOEither[%s]", containerName), id, DIE.Multi|DIE.IOEither, toIOEitherType(toContainer), providerFactory),
makeToken(fmt.Sprintf("IOOption[%s]", containerName), id, DIE.Multi|DIE.IOOption, toIOOptionType(toContainer), providerFactory),
token[[]T]{makeTokenBase(containerName, id, DIE.MULTI|DIE.IDENTITY, providerFactory), toContainer},
makeToken(fmt.Sprintf("Option[%s]", containerName), id, DIE.MULTI|DIE.OPTION, toOptionType(toContainer), providerFactory),
makeToken(fmt.Sprintf("IOEither[%s]", containerName), id, DIE.OPTION|DIE.IOEITHER, toIOEitherType(toContainer), providerFactory),
makeToken(fmt.Sprintf("IOOption[%s]", containerName), id, DIE.OPTION|DIE.IOOPTION, toIOOptionType(toContainer), providerFactory),
}
// item
item := &injectionToken[T]{
token[T]{makeTokenBase(itemName, id, DIE.Item|DIE.Identity, providerFactory), toItem},
makeToken(fmt.Sprintf("Option[%s]", itemName), id, DIE.Item|DIE.Option, toOptionType(toItem), providerFactory),
makeToken(fmt.Sprintf("IOEither[%s]", itemName), id, DIE.Item|DIE.IOEither, toIOEitherType(toItem), providerFactory),
makeToken(fmt.Sprintf("IOOption[%s]", itemName), id, DIE.Item|DIE.IOOption, toIOOptionType(toItem), providerFactory),
token[T]{makeTokenBase(itemName, id, DIE.ITEM|DIE.IDENTITY, providerFactory), toItem},
makeToken(fmt.Sprintf("Option[%s]", itemName), id, DIE.ITEM|DIE.OPTION, toOptionType(toItem), providerFactory),
makeToken(fmt.Sprintf("IOEither[%s]", itemName), id, DIE.ITEM|DIE.IOEITHER, toIOEitherType(toItem), providerFactory),
makeToken(fmt.Sprintf("IOOption[%s]", itemName), id, DIE.ITEM|DIE.IOOPTION, toIOOptionType(toItem), providerFactory),
}
// returns the token
return &multiInjectionToken[T]{container, item}

View File

@@ -23,7 +23,9 @@ import (
F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither"
IOO "github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -75,9 +77,9 @@ func TestTokenUnerase(t *testing.T) {
token := MakeToken[int]("IntToken")
// Test successful unerase
result := token.Unerase(42)
assert.True(t, E.IsRight(result))
assert.Equal(t, E.Of[error](42), result)
res := token.Unerase(42)
assert.True(t, E.IsRight(res))
assert.Equal(t, result.Of(42), res)
// Test failed unerase (wrong type)
result2 := token.Unerase("not an int")
@@ -104,7 +106,7 @@ func TestTokenProviderFactory(t *testing.T) {
assert.True(t, O.IsNone(token1.ProviderFactory()))
// Token with default
token2 := MakeTokenWithDefault0("Token2", IOE.Of[error](42))
token2 := MakeTokenWithDefault0("Token2", ioresult.Of(42))
assert.True(t, O.IsSome(token2.ProviderFactory()))
}
@@ -148,13 +150,13 @@ func TestOptionTokenUnerase(t *testing.T) {
optionToken := token.Option()
// Test successful unerase with Some
result := optionToken.Unerase(O.Of[any](42))
assert.True(t, E.IsRight(result))
res := optionToken.Unerase(O.Of[any](42))
assert.True(t, E.IsRight(res))
// Test successful unerase with None
noneResult := optionToken.Unerase(O.None[any]())
assert.True(t, E.IsRight(noneResult))
assert.Equal(t, E.Of[error](O.None[int]()), noneResult)
assert.Equal(t, result.Of(O.None[int]()), noneResult)
// Test failed unerase (wrong type)
badResult := optionToken.Unerase(42) // Not an Option
@@ -166,7 +168,7 @@ func TestIOEitherTokenUnerase(t *testing.T) {
ioeitherToken := token.IOEither()
// Test successful unerase
ioValue := IOE.Of[error](any(42))
ioValue := ioresult.Of(any(42))
result := ioeitherToken.Unerase(ioValue)
assert.True(t, E.IsRight(result))
@@ -222,7 +224,7 @@ func TestMultiTokenContainerUnerase(t *testing.T) {
}
func TestMakeTokenWithDefault(t *testing.T) {
factory := MakeProviderFactory0(IOE.Of[error](42))
factory := MakeProviderFactory0(ioresult.Of(42))
token := MakeTokenWithDefault[int]("TokenWithDefault", factory)
assert.NotNil(t, token)
@@ -247,7 +249,7 @@ func TestMultiTokenStringRepresentation(t *testing.T) {
// Benchmark tests
func BenchmarkMakeToken(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
MakeToken[int]("BenchToken")
}
}
@@ -257,13 +259,13 @@ func BenchmarkTokenUnerase(b *testing.B) {
value := any(42)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
token.Unerase(value)
}
}
func BenchmarkMakeMultiToken(b *testing.B) {
for i := 0; i < b.N; i++ {
for b.Loop() {
MakeMultiToken[int]("BenchMulti")
}
}

15
v2/di/types.go Normal file
View File

@@ -0,0 +1,15 @@
package di
import (
"github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
type (
Option[T any] = option.Option[T]
Result[T any] = result.Result[T]
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
)

View File

@@ -23,12 +23,13 @@ import (
IOE "github.com/IBM/fp-go/v2/ioeither"
IOO "github.com/IBM/fp-go/v2/iooption"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
var (
toOptionAny = toType[O.Option[any]]()
toIOEitherAny = toType[IOE.IOEither[error, any]]()
toIOOptionAny = toType[IOO.IOOption[any]]()
toOptionAny = toType[Option[any]]()
toIOEitherAny = toType[IOResult[any]]()
toIOOptionAny = toType[IOOption[any]]()
toArrayAny = toType[[]any]()
)
@@ -38,45 +39,45 @@ func asDependency[T DIE.Dependency](t T) DIE.Dependency {
}
// toType converts an any to a T
func toType[T any]() func(t any) E.Either[error, T] {
func toType[T any]() result.Kleisli[any, T] {
return E.ToType[T](errors.OnSome[any]("Value of type [%T] cannot be converted."))
}
// toOptionType converts an any to an Option[any] and then to an Option[T]
func toOptionType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, O.Option[T]] {
func toOptionType[T any](item result.Kleisli[any, T]) result.Kleisli[any, Option[T]] {
return F.Flow2(
toOptionAny,
E.Chain(O.Fold(
F.Nullary2(O.None[T], E.Of[error, O.Option[T]]),
F.Nullary2(O.None[T], E.Of[error, Option[T]]),
F.Flow2(
item,
E.Map[error](O.Of[T]),
result.Map(O.Of[T]),
),
)),
)
}
// toIOEitherType converts an any to an IOEither[error, any] and then to an IOEither[error, T]
func toIOEitherType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, IOE.IOEither[error, T]] {
func toIOEitherType[T any](item result.Kleisli[any, T]) result.Kleisli[any, IOResult[T]] {
return F.Flow2(
toIOEitherAny,
E.Map[error](IOE.ChainEitherK(item)),
result.Map(IOE.ChainEitherK(item)),
)
}
// toIOOptionType converts an any to an IOOption[any] and then to an IOOption[T]
func toIOOptionType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, IOO.IOOption[T]] {
func toIOOptionType[T any](item result.Kleisli[any, T]) result.Kleisli[any, IOOption[T]] {
return F.Flow2(
toIOOptionAny,
E.Map[error](IOO.ChainOptionK(F.Flow2(
result.Map(IOO.ChainOptionK(F.Flow2(
item,
E.ToOption[error, T],
result.ToOption[T],
))),
)
}
// toArrayType converts an any to a []T
func toArrayType[T any](item func(any) E.Either[error, T]) func(t any) E.Either[error, []T] {
func toArrayType[T any](item result.Kleisli[any, T]) result.Kleisli[any, []T] {
return F.Flow2(
toArrayAny,
E.Chain(E.TraverseArray(item)),

View File

@@ -21,8 +21,9 @@ import (
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
IOE "github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -33,13 +34,13 @@ var (
func TestToType(t *testing.T) {
// good cases
assert.Equal(t, E.Of[error](10), toInt(any(10)))
assert.Equal(t, E.Of[error]("Carsten"), toString(any("Carsten")))
assert.Equal(t, E.Of[error](O.Of("Carsten")), toType[O.Option[string]]()(any(O.Of("Carsten"))))
assert.Equal(t, E.Of[error](O.Of(any("Carsten"))), toType[O.Option[any]]()(any(O.Of(any("Carsten")))))
assert.Equal(t, result.Of(10), toInt(any(10)))
assert.Equal(t, result.Of("Carsten"), toString(any("Carsten")))
assert.Equal(t, result.Of(O.Of("Carsten")), toType[Option[string]]()(any(O.Of("Carsten"))))
assert.Equal(t, result.Of(O.Of(any("Carsten"))), toType[Option[any]]()(any(O.Of(any("Carsten")))))
// failure
assert.False(t, E.IsRight(toInt(any("Carsten"))))
assert.False(t, E.IsRight(toType[O.Option[string]]()(O.Of(any("Carsten")))))
assert.False(t, E.IsRight(toType[Option[string]]()(O.Of(any("Carsten")))))
}
func TestToOptionType(t *testing.T) {
@@ -47,17 +48,17 @@ func TestToOptionType(t *testing.T) {
toOptInt := toOptionType(toInt)
toOptString := toOptionType(toString)
// good cases
assert.Equal(t, E.Of[error](O.Of(10)), toOptInt(any(O.Of(any(10)))))
assert.Equal(t, E.Of[error](O.Of("Carsten")), toOptString(any(O.Of(any("Carsten")))))
assert.Equal(t, result.Of(O.Of(10)), toOptInt(any(O.Of(any(10)))))
assert.Equal(t, result.Of(O.Of("Carsten")), toOptString(any(O.Of(any("Carsten")))))
// bad cases
assert.False(t, E.IsRight(toOptInt(any(10))))
assert.False(t, E.IsRight(toOptInt(any(O.Of(10)))))
}
func invokeIOEither[T any](e E.Either[error, IOE.IOEither[error, T]]) E.Either[error, T] {
func invokeIOEither[T any](e Result[IOResult[T]]) Result[T] {
return F.Pipe1(
e,
E.Chain(func(ioe IOE.IOEither[error, T]) E.Either[error, T] {
E.Chain(func(ioe IOResult[T]) Result[T] {
return ioe()
}),
)
@@ -68,11 +69,11 @@ func TestToIOEitherType(t *testing.T) {
toIOEitherInt := toIOEitherType(toInt)
toIOEitherString := toIOEitherType(toString)
// good cases
assert.Equal(t, E.Of[error](10), invokeIOEither(toIOEitherInt(any(IOE.Of[error](any(10))))))
assert.Equal(t, E.Of[error]("Carsten"), invokeIOEither(toIOEitherString(any(IOE.Of[error](any("Carsten"))))))
assert.Equal(t, result.Of(10), invokeIOEither(toIOEitherInt(any(ioresult.Of(any(10))))))
assert.Equal(t, result.Of("Carsten"), invokeIOEither(toIOEitherString(any(ioresult.Of(any("Carsten"))))))
// bad cases
assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(IOE.Of[error](any(10)))))))
assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(IOE.Of[error]("Carsten"))))))
assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(ioresult.Of(any(10)))))))
assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any(ioresult.Of("Carsten"))))))
assert.False(t, E.IsRight(invokeIOEither(toIOEitherString(any("Carsten")))))
}
@@ -80,5 +81,5 @@ func TestToArrayType(t *testing.T) {
// shortcuts
toArrayString := toArrayType(toString)
// good cases
assert.Equal(t, E.Of[error](A.From("a", "b")), toArrayString(any(A.From(any("a"), any("b")))))
assert.Equal(t, result.Of(A.From("a", "b")), toArrayString(any(A.From(any("a"), any("b")))))
}

View File

@@ -4,16 +4,17 @@ import (
"fmt"
"testing"
A "github.com/IBM/fp-go/v2/array"
TST "github.com/IBM/fp-go/v2/internal/testing"
"github.com/stretchr/testify/assert"
)
func TestCompactArray(t *testing.T) {
ar := []Either[string, string]{
ar := A.From(
Of[string]("ok"),
Left[string]("err"),
Of[string]("ok"),
}
)
res := CompactArray(ar)
assert.Equal(t, 2, len(res))

View File

@@ -20,6 +20,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
@@ -203,7 +204,7 @@ func TestLetL(t *testing.T) {
)
t.Run("LetL with pure transformation", func(t *testing.T) {
double := func(v int) int { return v * 2 }
double := N.Mul(2)
result := F.Pipe1(
Right[error](Counter{Value: 21}),
@@ -215,7 +216,7 @@ func TestLetL(t *testing.T) {
})
t.Run("LetL with Left input", func(t *testing.T) {
double := func(v int) int { return v * 2 }
double := N.Mul(2)
result := F.Pipe1(
Left[Counter](assert.AnError),
@@ -227,8 +228,8 @@ func TestLetL(t *testing.T) {
})
t.Run("LetL with multiple transformations", func(t *testing.T) {
double := func(v int) int { return v * 2 }
addTen := func(v int) int { return v + 10 }
double := N.Mul(2)
addTen := N.Add(10)
result := F.Pipe2(
Right[error](Counter{Value: 5}),
@@ -241,7 +242,7 @@ func TestLetL(t *testing.T) {
})
t.Run("LetL with identity transformation", func(t *testing.T) {
identity := func(v int) int { return v }
identity := F.Identity[int]
result := F.Pipe1(
Right[error](Counter{Value: 42}),
@@ -315,7 +316,7 @@ func TestLensOperationsCombined(t *testing.T) {
)
t.Run("Combine LetToL and LetL", func(t *testing.T) {
double := func(v int) int { return v * 2 }
double := N.Mul(2)
result := F.Pipe2(
Right[error](Counter{Value: 100}),
@@ -328,7 +329,7 @@ func TestLensOperationsCombined(t *testing.T) {
})
t.Run("Combine LetL and BindL", func(t *testing.T) {
double := func(v int) int { return v * 2 }
double := N.Mul(2)
validate := func(v int) Either[error, int] {
if v > 100 {
return Left[int](assert.AnError)

View File

@@ -58,7 +58,7 @@ func FromIO[E any, IO ~func() A, A any](f IO) Either[E, A] {
//
// Example:
//
// fab := either.Right[error](func(x int) int { return x * 2 })
// fab := either.Right[error](N.Mul(2))
// fa := either.Right[error](21)
// result := either.MonadAp(fab, fa) // Right(42)
func MonadAp[B, E, A any](fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] {
@@ -81,7 +81,7 @@ func Ap[B, E, A any](fa Either[E, A]) Operator[E, func(A) B, B] {
//
// result := either.MonadMap(
// either.Right[error](21),
// func(x int) int { return x * 2 },
// N.Mul(2),
// ) // Right(42)
//
//go:inline

View File

@@ -20,6 +20,7 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
var (
@@ -33,21 +34,21 @@ var (
// Benchmark core constructors
func BenchmarkLeft(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Left[int](errBench)
}
}
func BenchmarkRight(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Right[error](42)
}
}
func BenchmarkOf(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Of[error](42)
}
}
@@ -57,7 +58,7 @@ func BenchmarkIsLeft(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchBool = IsLeft(left)
}
}
@@ -66,7 +67,7 @@ func BenchmarkIsRight(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchBool = IsRight(right)
}
}
@@ -75,10 +76,10 @@ func BenchmarkIsRight(b *testing.B) {
func BenchmarkMonadFold_Right(b *testing.B) {
right := Right[error](42)
onLeft := func(e error) int { return 0 }
onRight := func(a int) int { return a * 2 }
onRight := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = MonadFold(right, onLeft, onRight)
}
}
@@ -86,10 +87,10 @@ func BenchmarkMonadFold_Right(b *testing.B) {
func BenchmarkMonadFold_Left(b *testing.B) {
left := Left[int](errBench)
onLeft := func(e error) int { return 0 }
onRight := func(a int) int { return a * 2 }
onRight := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = MonadFold(left, onLeft, onRight)
}
}
@@ -98,11 +99,11 @@ func BenchmarkFold_Right(b *testing.B) {
right := Right[error](42)
folder := Fold(
func(e error) int { return 0 },
func(a int) int { return a * 2 },
N.Mul(2),
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = folder(right)
}
}
@@ -111,11 +112,11 @@ func BenchmarkFold_Left(b *testing.B) {
left := Left[int](errBench)
folder := Fold(
func(e error) int { return 0 },
func(a int) int { return a * 2 },
N.Mul(2),
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = folder(left)
}
}
@@ -125,7 +126,7 @@ func BenchmarkUnwrap_Right(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt, _ = Unwrap(right)
}
}
@@ -134,7 +135,7 @@ func BenchmarkUnwrap_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt, _ = Unwrap(left)
}
}
@@ -143,7 +144,7 @@ func BenchmarkUnwrapError_Right(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt, _ = UnwrapError(right)
}
}
@@ -152,7 +153,7 @@ func BenchmarkUnwrapError_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt, _ = UnwrapError(left)
}
}
@@ -160,40 +161,40 @@ func BenchmarkUnwrapError_Left(b *testing.B) {
// Benchmark functor operations
func BenchmarkMonadMap_Right(b *testing.B) {
right := Right[error](42)
mapper := func(a int) int { return a * 2 }
mapper := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadMap(right, mapper)
}
}
func BenchmarkMonadMap_Left(b *testing.B) {
left := Left[int](errBench)
mapper := func(a int) int { return a * 2 }
mapper := N.Mul(2)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadMap(left, mapper)
}
}
func BenchmarkMap_Right(b *testing.B) {
right := Right[error](42)
mapper := Map[error](func(a int) int { return a * 2 })
mapper := Map[error](N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = mapper(right)
}
}
func BenchmarkMap_Left(b *testing.B) {
left := Left[int](errBench)
mapper := Map[error](func(a int) int { return a * 2 })
mapper := Map[error](N.Mul(2))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = mapper(left)
}
}
@@ -203,7 +204,7 @@ func BenchmarkMapLeft_Right(b *testing.B) {
mapper := MapLeft[int](error.Error)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = mapper(right)
}
}
@@ -213,7 +214,7 @@ func BenchmarkMapLeft_Left(b *testing.B) {
mapper := MapLeft[int](error.Error)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = mapper(left)
}
}
@@ -226,7 +227,7 @@ func BenchmarkBiMap_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = mapper(right)
}
}
@@ -239,7 +240,7 @@ func BenchmarkBiMap_Left(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = mapper(left)
}
}
@@ -250,7 +251,7 @@ func BenchmarkMonadChain_Right(b *testing.B) {
chainer := func(a int) Either[error, int] { return Right[error](a * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadChain(right, chainer)
}
}
@@ -260,7 +261,7 @@ func BenchmarkMonadChain_Left(b *testing.B) {
chainer := func(a int) Either[error, int] { return Right[error](a * 2) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadChain(left, chainer)
}
}
@@ -270,7 +271,7 @@ func BenchmarkChain_Right(b *testing.B) {
chainer := Chain(func(a int) Either[error, int] { return Right[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = chainer(right)
}
}
@@ -280,7 +281,7 @@ func BenchmarkChain_Left(b *testing.B) {
chainer := Chain(func(a int) Either[error, int] { return Right[error](a * 2) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = chainer(left)
}
}
@@ -290,7 +291,7 @@ func BenchmarkChainFirst_Right(b *testing.B) {
chainer := ChainFirst(func(a int) Either[error, string] { return Right[error]("logged") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = chainer(right)
}
}
@@ -300,7 +301,7 @@ func BenchmarkChainFirst_Left(b *testing.B) {
chainer := ChainFirst(func(a int) Either[error, string] { return Right[error]("logged") })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = chainer(left)
}
}
@@ -309,7 +310,7 @@ func BenchmarkFlatten_Right(b *testing.B) {
nested := Right[error](Right[error](42))
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Flatten(nested)
}
}
@@ -318,28 +319,28 @@ func BenchmarkFlatten_Left(b *testing.B) {
nested := Left[Either[error, int]](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = Flatten(nested)
}
}
// Benchmark applicative operations
func BenchmarkMonadAp_RightRight(b *testing.B) {
fab := Right[error](func(a int) int { return a * 2 })
fab := Right[error](N.Mul(2))
fa := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadAp(fab, fa)
}
}
func BenchmarkMonadAp_RightLeft(b *testing.B) {
fab := Right[error](func(a int) int { return a * 2 })
fab := Right[error](N.Mul(2))
fa := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadAp(fab, fa)
}
}
@@ -349,18 +350,18 @@ func BenchmarkMonadAp_LeftRight(b *testing.B) {
fa := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadAp(fab, fa)
}
}
func BenchmarkAp_RightRight(b *testing.B) {
fab := Right[error](func(a int) int { return a * 2 })
fab := Right[error](N.Mul(2))
fa := Right[error](42)
ap := Ap[int](fa)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = ap(fab)
}
}
@@ -371,7 +372,7 @@ func BenchmarkAlt_RightRight(b *testing.B) {
alternative := Alt(func() Either[error, int] { return Right[error](99) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = alternative(right)
}
}
@@ -381,7 +382,7 @@ func BenchmarkAlt_LeftRight(b *testing.B) {
alternative := Alt(func() Either[error, int] { return Right[error](99) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = alternative(left)
}
}
@@ -391,7 +392,7 @@ func BenchmarkOrElse_Right(b *testing.B) {
recover := OrElse(func(e error) Either[error, int] { return Right[error](0) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = recover(right)
}
}
@@ -401,7 +402,7 @@ func BenchmarkOrElse_Left(b *testing.B) {
recover := OrElse(func(e error) Either[error, int] { return Right[error](0) })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = recover(left)
}
}
@@ -410,7 +411,7 @@ func BenchmarkOrElse_Left(b *testing.B) {
func BenchmarkTryCatch_Success(b *testing.B) {
onThrow := func(err error) error { return err }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = TryCatch(42, nil, onThrow)
}
}
@@ -418,21 +419,21 @@ func BenchmarkTryCatch_Success(b *testing.B) {
func BenchmarkTryCatch_Error(b *testing.B) {
onThrow := func(err error) error { return err }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = TryCatch(0, errBench, onThrow)
}
}
func BenchmarkTryCatchError_Success(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = TryCatchError(42, nil)
}
}
func BenchmarkTryCatchError_Error(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = TryCatchError(0, errBench)
}
}
@@ -441,7 +442,7 @@ func BenchmarkSwap_Right(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Swap(right)
}
}
@@ -450,7 +451,7 @@ func BenchmarkSwap_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Swap(left)
}
}
@@ -460,7 +461,7 @@ func BenchmarkGetOrElse_Right(b *testing.B) {
getter := GetOrElse(func(e error) int { return 0 })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = getter(right)
}
}
@@ -470,7 +471,7 @@ func BenchmarkGetOrElse_Left(b *testing.B) {
getter := GetOrElse(func(e error) int { return 0 })
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchInt = getter(left)
}
}
@@ -480,10 +481,10 @@ func BenchmarkPipeline_Map_Right(b *testing.B) {
right := Right[error](21)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe1(
right,
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
)
}
}
@@ -492,10 +493,10 @@ func BenchmarkPipeline_Map_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe1(
left,
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
)
}
}
@@ -504,7 +505,7 @@ func BenchmarkPipeline_Chain_Right(b *testing.B) {
right := Right[error](21)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe1(
right,
Chain(func(x int) Either[error, int] { return Right[error](x * 2) }),
@@ -516,7 +517,7 @@ func BenchmarkPipeline_Chain_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe1(
left,
Chain(func(x int) Either[error, int] { return Right[error](x * 2) }),
@@ -528,12 +529,12 @@ func BenchmarkPipeline_Complex_Right(b *testing.B) {
right := Right[error](10)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe3(
right,
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
Chain(func(x int) Either[error, int] { return Right[error](x + 1) }),
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
)
}
}
@@ -542,12 +543,12 @@ func BenchmarkPipeline_Complex_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = F.Pipe3(
left,
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
Chain(func(x int) Either[error, int] { return Right[error](x + 1) }),
Map[error](func(x int) int { return x * 2 }),
Map[error](N.Mul(2)),
)
}
}
@@ -559,7 +560,7 @@ func BenchmarkMonadSequence2_RightRight(b *testing.B) {
f := func(a, b int) Either[error, int] { return Right[error](a + b) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadSequence2(e1, e2, f)
}
}
@@ -570,7 +571,7 @@ func BenchmarkMonadSequence2_LeftRight(b *testing.B) {
f := func(a, b int) Either[error, int] { return Right[error](a + b) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadSequence2(e1, e2, f)
}
}
@@ -582,7 +583,7 @@ func BenchmarkMonadSequence3_RightRightRight(b *testing.B) {
f := func(a, b, c int) Either[error, int] { return Right[error](a + b + c) }
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchResult = MonadSequence3(e1, e2, e3, f)
}
}
@@ -591,7 +592,7 @@ func BenchmarkMonadSequence3_RightRightRight(b *testing.B) {
func BenchmarkDo(b *testing.B) {
type State struct{ value int }
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = Do[error](State{})
}
}
@@ -609,7 +610,7 @@ func BenchmarkBind_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = binder(initial)
}
}
@@ -625,7 +626,7 @@ func BenchmarkLet_Right(b *testing.B) {
)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = letter(initial)
}
}
@@ -635,7 +636,7 @@ func BenchmarkString_Right(b *testing.B) {
right := Right[error](42)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchString = right.String()
}
}
@@ -644,9 +645,7 @@ func BenchmarkString_Left(b *testing.B) {
left := Left[int](errBench)
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for b.Loop() {
benchString = left.String()
}
}
// Made with Bob

View File

@@ -66,7 +66,7 @@ func TestUnwrapError(t *testing.T) {
func TestReduce(t *testing.T) {
s := S.Semigroup()
s := S.Semigroup
assert.Equal(t, "foobar", F.Pipe1(Right[string]("bar"), Reduce[string](s.Concat, "foo")))
assert.Equal(t, "foo", F.Pipe1(Left[string]("bar"), Reduce[string](s.Concat, "foo")))

View File

@@ -46,7 +46,7 @@ func _log[E, A any](left func(string, ...any), right func(string, ...any), prefi
// result := F.Pipe2(
// either.Right[error](42),
// logger("Processing"),
// either.Map(func(x int) int { return x * 2 }),
// either.Map(N.Mul(2)),
// )
// // Logs: "Processing: 42"
// // result is Right(84)

142
v2/either/validation.go Normal file
View File

@@ -0,0 +1,142 @@
// Copyright (c) 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 either
import (
F "github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
// MonadApV is the applicative validation functor that combines errors using a semigroup.
//
// Unlike the standard [MonadAp] which short-circuits on the first Left (error),
// MonadApV accumulates all errors using the provided semigroup's Concat operation.
// This is particularly useful for validation scenarios where you want to collect
// all validation errors rather than stopping at the first one.
//
// The function takes a semigroup for combining errors and returns a function that
// applies a wrapped function to a wrapped value, accumulating errors if both are Left.
//
// Behavior:
// - If both fab and fa are Left, combines their errors using sg.Concat
// - If only fab is Left, returns Left with fab's error
// - If only fa is Left, returns Left with fa's error
// - If both are Right, applies the function and returns Right with the result
//
// Type Parameters:
// - B: The result type after applying the function
// - E: The error type (must support the semigroup operation)
// - A: The input type to the function
//
// Parameters:
// - sg: A semigroup that defines how to combine two error values
//
// Returns:
// - A function that takes a wrapped function and a wrapped value, returning
// Either[E, B] with accumulated errors or the computed result
//
// Example:
//
// // Define a semigroup that concatenates error messages
// errorSemigroup := semigroup.MakeSemigroup(func(e1, e2 string) string {
// return e1 + "; " + e2
// })
//
// // Create the validation applicative
// applyV := either.MonadApV[int](errorSemigroup)
//
// // Both are errors - errors get combined
// fab := either.Left[func(int) int]("error1")
// fa := either.Left[int]("error2")
// result := applyV(fab, fa) // Left("error1; error2")
//
// // One error - returns that error
// fab2 := either.Right[string](N.Mul(2))
// fa2 := either.Left[int]("validation failed")
// result2 := applyV(fab2, fa2) // Left("validation failed")
//
// // Both success - applies function
// fab3 := either.Right[string](N.Mul(2))
// fa3 := either.Right[string](21)
// result3 := applyV(fab3, fa3) // Right(42)
func MonadApV[B, E, A any](sg S.Semigroup[E]) func(fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] {
c := F.Bind2of2(sg.Concat)
return func(fab Either[E, func(a A) B], fa Either[E, A]) Either[E, B] {
return MonadFold(fab, func(eab E) Either[E, B] {
return MonadFold(fa, F.Flow2(c(eab), Left[B]), F.Constant1[A](Left[B](eab)))
}, func(ab func(A) B) Either[E, B] {
return MonadFold(fa, Left[B, E], F.Flow2(ab, Right[E, B]))
})
}
}
// ApV is the curried version of [MonadApV] that combines errors using a semigroup.
//
// This function provides a more convenient API for validation scenarios by currying
// the arguments. It first takes the value to validate, then returns a function that
// takes the validation function. This allows for a more natural composition style.
//
// Like [MonadApV], this accumulates all errors using the provided semigroup instead
// of short-circuiting on the first error. This is the key difference from the
// standard [Ap] function.
//
// Type Parameters:
// - B: The result type after applying the function
// - E: The error type (must support the semigroup operation)
// - A: The input type to the function
//
// Parameters:
// - sg: A semigroup that defines how to combine two error values
//
// Returns:
// - A function that takes a value Either[E, A] and returns an Operator that
// applies validation functions while accumulating errors
//
// Example:
//
// // Define a semigroup for combining validation errors
// type ValidationError struct {
// Errors []string
// }
// errorSemigroup := semigroup.MakeSemigroup(func(e1, e2 ValidationError) ValidationError {
// return ValidationError{Errors: append(e1.Errors, e2.Errors...)}
// })
//
// // Create validators
// validatePositive := func(x int) either.Either[ValidationError, int] {
// if x > 0 {
// return either.Right[ValidationError](x)
// }
// return either.Left[int](ValidationError{Errors: []string{"must be positive"}})
// }
//
// // Use ApV for validation
// applyValidation := either.ApV[int](errorSemigroup)
// value := either.Left[int](ValidationError{Errors: []string{"invalid input"}})
// validator := either.Left[func(int) int](ValidationError{Errors: []string{"invalid validator"}})
//
// result := applyValidation(value)(validator)
// // Left(ValidationError{Errors: []string{"invalid validator", "invalid input"}})
func ApV[B, E, A any](sg S.Semigroup[E]) func(fa Either[E, A]) Operator[E, func(A) B, B] {
c := F.Bind2of2(sg.Concat)
return func(fa Either[E, A]) Operator[E, func(A) B, B] {
return Fold(func(eab E) Either[E, B] {
return MonadFold(fa, F.Flow2(c(eab), Left[B]), F.Constant1[A](Left[B](eab)))
}, func(ab func(A) B) Either[E, B] {
return MonadFold(fa, Left[B, E], F.Flow2(ab, Right[E, B]))
})
}
}

View File

@@ -0,0 +1,381 @@
// Copyright (c) 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 either
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
// TestMonadApV_BothRight tests MonadApV when both function and value are Right
func TestMonadApV_BothRight(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
// Create the validation applicative
applyV := MonadApV[int, string, int](sg)
// Both are Right - should apply function
fab := Right[string](N.Mul(2))
fa := Right[string](21)
result := applyV(fab, fa)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string](42), result)
}
// TestMonadApV_BothLeft tests MonadApV when both function and value are Left
func TestMonadApV_BothLeft(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
// Create the validation applicative
applyV := MonadApV[int, string, int](sg)
// Both are Left - should combine errors
fab := Left[func(int) int]("error1")
fa := Left[int]("error2")
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa error + fab error
assert.Equal(t, Left[int]("error2; error1"), result)
}
// TestMonadApV_LeftFunction tests MonadApV when function is Left and value is Right
func TestMonadApV_LeftFunction(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
// Create the validation applicative
applyV := MonadApV[int, string, int](sg)
// Function is Left, value is Right - should return function's error
fab := Left[func(int) int]("function error")
fa := Right[string](21)
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("function error"), result)
}
// TestMonadApV_LeftValue tests MonadApV when function is Right and value is Left
func TestMonadApV_LeftValue(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
// Create the validation applicative
applyV := MonadApV[int, string, int](sg)
// Function is Right, value is Left - should return value's error
fab := Right[string](N.Mul(2))
fa := Left[int]("value error")
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("value error"), result)
}
// TestMonadApV_WithSliceSemigroup tests MonadApV with a slice-based semigroup
func TestMonadApV_WithSliceSemigroup(t *testing.T) {
// Create a semigroup that concatenates slices
sg := S.MakeSemigroup(func(a, b []string) []string {
return append(a, b...)
})
// Create the validation applicative
applyV := MonadApV[string, []string, string](sg)
// Both are Left with slice errors
fab := Left[func(string) string]([]string{"error1", "error2"})
fa := Left[string]([]string{"error3", "error4"})
result := applyV(fab, fa)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa errors + fab errors
expected := Left[string]([]string{"error3", "error4", "error1", "error2"})
assert.Equal(t, expected, result)
}
// TestMonadApV_ComplexFunction tests MonadApV with a more complex function
func TestMonadApV_ComplexFunction(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + " | " + b
})
// Create the validation applicative
applyV := MonadApV[string, string, int](sg)
// Test with a function that transforms the value
fab := Right[string](func(x int) string {
if x > 0 {
return "positive"
}
return "non-positive"
})
fa := Right[string](42)
result := applyV(fab, fa)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string]("positive"), result)
}
// TestApV_BothRight tests ApV when both function and value are Right
func TestApV_BothRight(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
// Create the validation applicative
applyV := ApV[int, string, int](sg)
// Both are Right - should apply function
fa := Right[string](21)
fab := Right[string](N.Mul(2))
result := applyV(fa)(fab)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string](42), result)
}
// TestApV_BothLeft tests ApV when both function and value are Left
func TestApV_BothLeft(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
// Create the validation applicative
applyV := ApV[int, string, int](sg)
// Both are Left - should combine errors
fa := Left[int]("error2")
fab := Left[func(int) int]("error1")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa error + fab error
assert.Equal(t, Left[int]("error2; error1"), result)
}
// TestApV_LeftFunction tests ApV when function is Left and value is Right
func TestApV_LeftFunction(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
// Create the validation applicative
applyV := ApV[int, string, int](sg)
// Function is Left, value is Right - should return function's error
fa := Right[string](21)
fab := Left[func(int) int]("function error")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("function error"), result)
}
// TestApV_LeftValue tests ApV when function is Right and value is Left
func TestApV_LeftValue(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + "; " + b
})
// Create the validation applicative
applyV := ApV[int, string, int](sg)
// Function is Right, value is Left - should return value's error
fa := Left[int]("value error")
fab := Right[string](N.Mul(2))
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
assert.Equal(t, Left[int]("value error"), result)
}
// TestApV_Composition tests ApV with function composition
func TestApV_Composition(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + " & " + b
})
// Create the validation applicative
applyV := ApV[string, string, int](sg)
// Test composition with pipe
fa := Right[string](10)
fab := Right[string](func(x int) string {
return F.Pipe1(x, func(n int) string {
if n >= 10 {
return "large"
}
return "small"
})
})
result := F.Pipe1(fa, applyV)(fab)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string]("large"), result)
}
// TestApV_WithStructSemigroup tests ApV with a custom struct semigroup
func TestApV_WithStructSemigroup(t *testing.T) {
type ValidationErrors struct {
Errors []string
}
// Create a semigroup that combines validation errors
sg := S.MakeSemigroup(func(a, b ValidationErrors) ValidationErrors {
return ValidationErrors{
Errors: append(append([]string{}, a.Errors...), b.Errors...),
}
})
// Create the validation applicative
applyV := ApV[int, ValidationErrors, int](sg)
// Both are Left with validation errors
fa := Left[int](ValidationErrors{Errors: []string{"field1: required"}})
fab := Left[func(int) int](ValidationErrors{Errors: []string{"field2: invalid"}})
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: fa errors + fab errors
expected := Left[int](ValidationErrors{
Errors: []string{"field1: required", "field2: invalid"},
})
assert.Equal(t, expected, result)
}
// TestApV_MultipleValidations tests ApV with multiple validation steps
func TestApV_MultipleValidations(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + ", " + b
})
// Create the validation applicative
applyV := ApV[int, string, int](sg)
// Simulate multiple validation failures
validation1 := Left[int]("age must be positive")
validation2 := Left[func(int) int]("name is required")
result := applyV(validation1)(validation2)
assert.True(t, IsLeft(result))
// When both are Left, errors are combined as: validation1 error + validation2 error
assert.Equal(t, Left[int]("age must be positive, name is required"), result)
}
// TestMonadApV_DifferentTypes tests MonadApV with different input and output types
func TestMonadApV_DifferentTypes(t *testing.T) {
// Create a semigroup for string concatenation
sg := S.MakeSemigroup(func(a, b string) string {
return a + " + " + b
})
// Create the validation applicative
applyV := MonadApV[string, string, int](sg)
// Function converts int to string
fab := Right[string](func(x int) string {
return F.Pipe1(x, func(n int) string {
if n == 0 {
return "zero"
} else if n > 0 {
return "positive"
}
return "negative"
})
})
fa := Right[string](-5)
result := applyV(fab, fa)
assert.True(t, IsRight(result))
assert.Equal(t, Right[string]("negative"), result)
}
// TestApV_FirstSemigroup tests ApV with First semigroup (always returns first error)
func TestApV_FirstSemigroup(t *testing.T) {
// Use First semigroup which always returns the first value
sg := S.First[string]()
// Create the validation applicative
applyV := ApV[int, string, int](sg)
// Both are Left - should return first error
fa := Left[int]("error2")
fab := Left[func(int) int]("error1")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// First semigroup returns the first value, which is fa's error
assert.Equal(t, Left[int]("error2"), result)
}
// TestApV_LastSemigroup tests ApV with Last semigroup (always returns last error)
func TestApV_LastSemigroup(t *testing.T) {
// Use Last semigroup which always returns the last value
sg := S.Last[string]()
// Create the validation applicative
applyV := ApV[int, string, int](sg)
// Both are Left - should return last error
fa := Left[int]("error2")
fab := Left[func(int) int]("error1")
result := applyV(fa)(fab)
assert.True(t, IsLeft(result))
// Last semigroup returns the last value, which is fab's error
assert.Equal(t, Left[int]("error1"), result)
}
// Made with Bob

View File

@@ -36,16 +36,21 @@
// )
//
// // Define some endomorphisms
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
//
// // Compose them
// doubleAndIncrement := endomorphism.Compose(double, increment)
// result := doubleAndIncrement(5) // (5 * 2) + 1 = 11
// // Compose them (RIGHT-TO-LEFT execution)
// composed := endomorphism.Compose(double, increment)
// result := composed(5) // increment(5) then double: (5 + 1) * 2 = 12
//
// // Chain them (LEFT-TO-RIGHT execution)
// chained := endomorphism.MonadChain(double, increment)
// result2 := chained(5) // double(5) then increment: (5 * 2) + 1 = 11
//
// # Monoid Operations
//
// Endomorphisms form a monoid, which means you can combine multiple endomorphisms:
// Endomorphisms form a monoid, which means you can combine multiple endomorphisms.
// The monoid uses Compose, which executes RIGHT-TO-LEFT:
//
// import (
// "github.com/IBM/fp-go/v2/endomorphism"
@@ -55,22 +60,39 @@
// // Get the monoid for int endomorphisms
// monoid := endomorphism.Monoid[int]()
//
// // Combine multiple endomorphisms
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(
// func(x int) int { return x * 2 },
// func(x int) int { return x + 1 },
// func(x int) int { return x * 3 },
// N.Mul(2), // applied third
// func(x int) int { return x + 1 }, // applied second
// func(x int) int { return x * 3 }, // applied first
// )
// result := combined(5) // ((5 * 2) + 1) * 3 = 33
// result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
//
// # Monad Operations
//
// The package also provides monadic operations for endomorphisms:
// The package also provides monadic operations for endomorphisms.
// MonadChain executes LEFT-TO-RIGHT, unlike Compose:
//
// // Chain allows sequencing of endomorphisms
// f := func(x int) int { return x * 2 }
// // Chain allows sequencing of endomorphisms (LEFT-TO-RIGHT)
// f := N.Mul(2)
// g := func(x int) int { return x + 1 }
// chained := endomorphism.MonadChain(f, g)
// chained := endomorphism.MonadChain(f, g) // f first, then g
// result := chained(5) // (5 * 2) + 1 = 11
//
// # Compose vs Chain
//
// The key difference between Compose and Chain/MonadChain is execution order:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
//
// // Compose: RIGHT-TO-LEFT (mathematical composition)
// composed := endomorphism.Compose(double, increment)
// result1 := composed(5) // increment(5) * 2 = (5 + 1) * 2 = 12
//
// // MonadChain: LEFT-TO-RIGHT (sequential application)
// chained := endomorphism.MonadChain(double, increment)
// result2 := chained(5) // double(5) + 1 = (5 * 2) + 1 = 11
//
// # Type Safety
//

View File

@@ -17,115 +17,372 @@ package endomorphism
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity"
)
// MonadAp applies an endomorphism to a value in a monadic context.
// MonadAp applies an endomorphism in a function to an endomorphism value.
//
// This function applies the endomorphism fab to the value fa, returning the result.
// It's the monadic application operation for endomorphisms.
// For endomorphisms, Ap composes two endomorphisms using RIGHT-TO-LEFT composition.
// This is the applicative functor operation for endomorphisms.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT (same as MonadCompose):
// - fa is applied first to the input
// - fab is applied to the result
//
// Parameters:
// - fab: An endomorphism to apply
// - fa: The value to apply the endomorphism to
// - fab: An endomorphism to apply (outer function)
// - fa: An endomorphism to apply first (inner function)
//
// Returns:
// - The result of applying fab to fa
// - A new endomorphism that applies fa, then fab
//
// Example:
//
// double := func(x int) int { return x * 2 }
// result := endomorphism.MonadAp(double, 5) // Returns: 10
func MonadAp[A any](fab Endomorphism[A], fa A) A {
return identity.MonadAp(fab, fa)
}
// Ap returns a function that applies a value to an endomorphism.
//
// This is the curried version of MonadAp. It takes a value and returns a function
// that applies that value to any endomorphism.
//
// Parameters:
// - fa: The value to be applied
//
// Returns:
// - A function that takes an endomorphism and applies fa to it
//
// Example:
//
// applyFive := endomorphism.Ap(5)
// double := func(x int) int { return x * 2 }
// result := applyFive(double) // Returns: 10
func Ap[A any](fa A) func(Endomorphism[A]) A {
return identity.Ap[A](fa)
}
// Compose composes two endomorphisms into a single endomorphism.
//
// Given two endomorphisms f1 and f2, Compose returns a new endomorphism that
// applies f1 first, then applies f2 to the result. This is function composition:
// Compose(f1, f2)(x) = f2(f1(x))
//
// Composition is associative: Compose(Compose(f, g), h) = Compose(f, Compose(g, h))
//
// Parameters:
// - f1: The first endomorphism to apply
// - f2: The second endomorphism to apply
//
// Returns:
// - A new endomorphism that is the composition of f1 and f2
//
// Example:
//
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// doubleAndIncrement := endomorphism.Compose(double, increment)
// result := doubleAndIncrement(5) // (5 * 2) + 1 = 11
func Compose[A any](f1, f2 Endomorphism[A]) Endomorphism[A] {
return function.Flow2(f1, f2)
// result := endomorphism.MonadAp(double, increment) // Composes: double increment
// // result(5) = double(increment(5)) = double(6) = 12
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
return MonadCompose(fab, fa)
}
// MonadChain chains two endomorphisms together.
// Ap returns a function that applies an endomorphism to another endomorphism.
//
// This is the monadic bind operation for endomorphisms. It composes two endomorphisms
// ma and f, returning a new endomorphism that applies ma first, then f.
// MonadChain is equivalent to Compose.
// This is the curried version of MonadAp. It takes an endomorphism fa and returns
// a function that composes any endomorphism with fa using RIGHT-TO-LEFT composition.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - fa is applied first to the input
// - The endomorphism passed to the returned function is applied to the result
//
// Parameters:
// - ma: The first endomorphism in the chain
// - f: The second endomorphism in the chain
// - fa: The first endomorphism to apply (inner function)
//
// Returns:
// - A new endomorphism that chains ma and f
// - A function that takes an endomorphism and composes it with fa (right-to-left)
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// applyIncrement := endomorphism.Ap(increment)
// double := N.Mul(2)
// composed := applyIncrement(double) // double ∘ increment
// // composed(5) = double(increment(5)) = double(6) = 12
func Ap[A any](fa Endomorphism[A]) Operator[A] {
return Compose(fa)
}
// MonadCompose composes two endomorphisms, executing them from right to left.
//
// MonadCompose creates a new endomorphism that applies f2 first, then f1.
// This follows the mathematical notation of function composition: (f1 ∘ f2)(x) = f1(f2(x))
//
// IMPORTANT: The execution order is RIGHT-TO-LEFT:
// - f2 is applied first to the input
// - f1 is applied to the result of f2
//
// This is different from Chain/MonadChain which executes LEFT-TO-RIGHT.
//
// Parameters:
// - f1: The second function to apply (outer function)
// - f2: The first function to apply (inner function)
//
// Returns:
// - A new endomorphism that applies f2, then f1
//
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
//
// // MonadCompose executes RIGHT-TO-LEFT: increment first, then double
// composed := endomorphism.MonadCompose(double, increment)
// result := composed(5) // (5 + 1) * 2 = 12
//
// // Compare with Chain which executes LEFT-TO-RIGHT:
// chained := endomorphism.MonadChain(double, increment)
// result2 := chained(5) // (5 * 2) + 1 = 11
func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
return function.Flow2(g, f)
}
// MonadMap maps an endomorphism over another endomorphism using function composition.
//
// For endomorphisms, Map is equivalent to Compose (RIGHT-TO-LEFT composition).
// This is the functor map operation for endomorphisms.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - g is applied first to the input
// - f is applied to the result
//
// Parameters:
// - f: The function to map (outer function)
// - g: The endomorphism to map over (inner function)
//
// Returns:
// - A new endomorphism that applies g, then f
//
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// mapped := endomorphism.MonadMap(double, increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
return MonadCompose(f, g)
}
// Compose returns a function that composes an endomorphism with another, executing right to left.
//
// This is the curried version of MonadCompose. It takes an endomorphism g and returns
// a function that composes any endomorphism with g, applying g first (inner function),
// then the input endomorphism (outer function).
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT (mathematical composition):
// - g is applied first to the input
// - The endomorphism passed to the returned function is applied to the result of g
//
// This follows the mathematical composition notation where Compose(g)(f) = f ∘ g
//
// Parameters:
// - g: The first endomorphism to apply (inner function)
//
// Returns:
// - A function that takes an endomorphism f and composes it with g (right-to-left)
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// composeWithIncrement := endomorphism.Compose(increment)
// double := N.Mul(2)
//
// // Composes double with increment (RIGHT-TO-LEFT: increment first, then double)
// composed := composeWithIncrement(double)
// result := composed(5) // (5 + 1) * 2 = 12
//
// // Compare with Chain which executes LEFT-TO-RIGHT:
// chainWithIncrement := endomorphism.Chain(increment)
// chained := chainWithIncrement(double)
// result2 := chained(5) // (5 * 2) + 1 = 11
func Compose[A any](g Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadCompose, g)
}
// Map returns a function that maps an endomorphism over another endomorphism.
//
// This is the curried version of MonadMap. It takes an endomorphism f and returns
// a function that maps f over any endomorphism using RIGHT-TO-LEFT composition.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT (same as Compose):
// - The endomorphism passed to the returned function is applied first
// - f is applied to the result
//
// For endomorphisms, Map is equivalent to Compose.
//
// Parameters:
// - f: The function to map (outer function)
//
// Returns:
// - A function that takes an endomorphism and maps f over it (right-to-left)
//
// Example:
//
// double := N.Mul(2)
// mapDouble := endomorphism.Map(double)
// increment := func(x int) int { return x + 1 }
// mapped := mapDouble(increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func Map[A any](f Endomorphism[A]) Operator[A] {
return Compose(f)
}
// MonadChain chains two endomorphisms together, executing them from left to right.
//
// This is the monadic bind operation for endomorphisms. For endomorphisms, bind is
// simply left-to-right function composition: ma is applied first, then f.
//
// IMPORTANT: The execution order is LEFT-TO-RIGHT:
// - ma is applied first to the input
// - f is applied to the result of ma
//
// This is different from MonadCompose which executes RIGHT-TO-LEFT.
//
// Parameters:
// - ma: The first endomorphism to apply
// - f: The second endomorphism to apply
//
// Returns:
// - A new endomorphism that applies ma, then f
//
// Example:
//
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
//
// // MonadChain executes LEFT-TO-RIGHT: double first, then increment
// chained := endomorphism.MonadChain(double, increment)
// result := chained(5) // (5 * 2) + 1 = 11
//
// // Compare with MonadCompose which executes RIGHT-TO-LEFT:
// composed := endomorphism.MonadCompose(increment, double)
// result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order)
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
return Compose(ma, f)
return function.Flow2(ma, f)
}
// Chain returns a function that chains an endomorphism with another.
// MonadChainFirst chains two endomorphisms but returns the result of the first.
//
// This is the curried version of MonadChain. It takes an endomorphism f and returns
// a function that chains any endomorphism with f.
// This applies ma first, then f, but discards the result of f and returns the result of ma.
// Useful for performing side-effects while preserving the original value.
//
// Parameters:
// - f: The endomorphism to chain with
// - ma: The endomorphism whose result to keep
// - f: The endomorphism to apply for its effect
//
// Returns:
// - A function that takes an endomorphism and chains it with f
// - A new endomorphism that applies both but returns ma's result
//
// Example:
//
// double := N.Mul(2)
// log := func(x int) int { fmt.Println(x); return x }
// chained := endomorphism.MonadChainFirst(double, log)
// result := chained(5) // Prints 10, returns 10
func MonadChainFirst[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
return func(a A) A {
result := ma(a)
f(result) // Apply f for its effect
return result // But return ma's result
}
}
// ChainFirst returns a function that chains for effect but preserves the original result.
//
// This is the curried version of MonadChainFirst.
//
// Parameters:
// - f: The endomorphism to apply for its effect
//
// Returns:
// - A function that takes an endomorphism and chains it with f, keeping the first result
//
// Example:
//
// log := func(x int) int { fmt.Println(x); return x }
// chainLog := endomorphism.ChainFirst(log)
// double := N.Mul(2)
// chained := chainLog(double)
// result := chained(5) // Prints 10, returns 10
func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadChainFirst, f)
}
// Chain returns a function that chains an endomorphism with another, executing left to right.
//
// This is the curried version of MonadChain. It takes an endomorphism f and returns
// a function that chains any endomorphism with f, applying the input endomorphism first,
// then f.
//
// IMPORTANT: Execution order is LEFT-TO-RIGHT:
// - The endomorphism passed to the returned function is applied first
// - f is applied to the result
//
// Parameters:
// - f: The second endomorphism to apply
//
// Returns:
// - A function that takes an endomorphism and chains it with f (left-to-right)
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// chainWithIncrement := endomorphism.Chain(increment)
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
//
// // Chains double (first) with increment (second)
// chained := chainWithIncrement(double)
// result := chained(5) // (5 * 2) + 1 = 11
func Chain[A any](f Endomorphism[A]) Endomorphism[Endomorphism[A]] {
func Chain[A any](f Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadChain, f)
}
// Flatten collapses a nested endomorphism into a single endomorphism.
//
// Given an endomorphism that transforms endomorphisms (Endomorphism[Endomorphism[A]]),
// Flatten produces a simple endomorphism by applying the outer transformation to the
// identity function. This is the monadic join operation for the Endomorphism monad.
//
// The function applies the nested endomorphism to Identity[A] to extract the inner
// endomorphism, effectively "flattening" the two layers into one.
//
// Type Parameters:
// - A: The type being transformed by the endomorphisms
//
// Parameters:
// - mma: A nested endomorphism that transforms endomorphisms
//
// Returns:
// - An endomorphism that applies the transformation directly to values of type A
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// // An endomorphism that wraps another endomorphism
// addThenDouble := func(endo Endomorphism[Counter]) Endomorphism[Counter] {
// return func(c Counter) Counter {
// c = endo(c) // Apply the input endomorphism
// c.Value = c.Value * 2 // Then double
// return c
// }
// }
//
// flattened := Flatten(addThenDouble)
// result := flattened(Counter{Value: 5}) // Counter{Value: 10}
func Flatten[A any](mma Endomorphism[Endomorphism[A]]) Endomorphism[A] {
return mma(function.Identity[A])
}
// Join performs self-application of a function that produces endomorphisms.
//
// Given a function that takes a value and returns an endomorphism of that same type,
// Join creates an endomorphism that applies the value to itself through the function.
// This operation is also known as the W combinator (warbler) in combinatory logic,
// or diagonal application.
//
// The resulting endomorphism evaluates f(a)(a), applying the same value a to both
// the function f and the resulting endomorphism.
//
// Type Parameters:
// - A: The type being transformed
//
// Parameters:
// - f: A function that takes a value and returns an endomorphism of that type
//
// Returns:
// - An endomorphism that performs self-application: f(a)(a)
//
// Example:
//
// type Point struct {
// X, Y int
// }
//
// // Create an endomorphism based on the input point
// scaleBy := func(p Point) Endomorphism[Point] {
// return func(p2 Point) Point {
// return Point{
// X: p2.X * p.X,
// Y: p2.Y * p.Y,
// }
// }
// }
//
// selfScale := Join(scaleBy)
// result := selfScale(Point{X: 3, Y: 4}) // Point{X: 9, Y: 16}
func Join[A any](f Kleisli[A]) Endomorphism[A] {
return func(a A) A {
return f(a)(a)
}
}

View File

@@ -19,6 +19,7 @@ import (
"testing"
M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
@@ -76,84 +77,152 @@ func TestCurry3(t *testing.T) {
// TestMonadAp tests the MonadAp function
func TestMonadAp(t *testing.T) {
result := MonadAp(double, 5)
assert.Equal(t, 10, result, "MonadAp should apply endomorphism to value")
// MonadAp composes two endomorphisms (RIGHT-TO-LEFT)
// MonadAp(double, increment) means: increment first, then double
composed := MonadAp(double, increment)
result := composed(5)
assert.Equal(t, 12, result, "MonadAp should compose right-to-left: (5 + 1) * 2 = 12")
result2 := MonadAp(increment, 10)
assert.Equal(t, 11, result2, "MonadAp should work with different endomorphisms")
// Test with different order
composed2 := MonadAp(increment, double)
result2 := composed2(5)
assert.Equal(t, 11, result2, "MonadAp should compose right-to-left: (5 * 2) + 1 = 11")
result3 := MonadAp(square, 4)
assert.Equal(t, 16, result3, "MonadAp should work with square function")
// Test with square
composed3 := MonadAp(square, increment)
result3 := composed3(5)
assert.Equal(t, 36, result3, "MonadAp should compose right-to-left: (5 + 1) ^ 2 = 36")
}
// TestAp tests the Ap function
func TestAp(t *testing.T) {
applyFive := Ap(5)
// Ap is the curried version of MonadAp
// Ap(increment) returns a function that composes with increment (RIGHT-TO-LEFT)
applyIncrement := Ap(increment)
result := applyFive(double)
assert.Equal(t, 10, result, "Ap should apply value to endomorphism")
composed := applyIncrement(double)
result := composed(5)
assert.Equal(t, 12, result, "Ap should compose right-to-left: (5 + 1) * 2 = 12")
result2 := applyFive(increment)
assert.Equal(t, 6, result2, "Ap should work with different endomorphisms")
// Test with different endomorphism
composed2 := applyIncrement(square)
result2 := composed2(5)
assert.Equal(t, 36, result2, "Ap should compose right-to-left: (5 + 1) ^ 2 = 36")
applyTen := Ap(10)
result3 := applyTen(square)
assert.Equal(t, 100, result3, "Ap should work with different values")
// Test with different base endomorphism
applyDouble := Ap(double)
composed3 := applyDouble(increment)
result3 := composed3(5)
assert.Equal(t, 11, result3, "Ap should compose right-to-left: (5 * 2) + 1 = 11")
}
// TestCompose tests the Compose function
func TestCompose(t *testing.T) {
// Test basic composition: (5 * 2) + 1 = 11
doubleAndIncrement := Compose(double, increment)
result := doubleAndIncrement(5)
assert.Equal(t, 11, result, "Compose should compose endomorphisms correctly")
// TestMonadCompose tests the MonadCompose function
func TestMonadCompose(t *testing.T) {
// Test basic composition: RIGHT-TO-LEFT execution
// MonadCompose(double, increment) means: increment first, then double
composed := MonadCompose(double, increment)
result := composed(5)
assert.Equal(t, 12, result, "MonadCompose should execute right-to-left: (5 + 1) * 2 = 12")
// Test composition order: (5 + 1) * 2 = 12
incrementAndDouble := Compose(increment, double)
result2 := incrementAndDouble(5)
assert.Equal(t, 12, result2, "Compose should respect order of composition")
// Test composition order: RIGHT-TO-LEFT execution
// MonadCompose(increment, double) means: double first, then increment
composed2 := MonadCompose(increment, double)
result2 := composed2(5)
assert.Equal(t, 11, result2, "MonadCompose should execute right-to-left: (5 * 2) + 1 = 11")
// Test with three compositions: ((5 * 2) + 1) * ((5 * 2) + 1) = 121
complex := Compose(Compose(double, increment), square)
// Test with three compositions: RIGHT-TO-LEFT execution
// MonadCompose(MonadCompose(double, increment), square) means: square, then increment, then double
complex := MonadCompose(MonadCompose(double, increment), square)
result3 := complex(5)
assert.Equal(t, 121, result3, "Compose should work with nested compositions")
// 5 -> square -> 25 -> increment -> 26 -> double -> 52
assert.Equal(t, 52, result3, "MonadCompose should work with nested compositions: square(5)=25, +1=26, *2=52")
}
// TestMonadChain tests the MonadChain function
func TestMonadChain(t *testing.T) {
// MonadChain should behave like Compose
// MonadChain executes LEFT-TO-RIGHT (first arg first, second arg second)
chained := MonadChain(double, increment)
result := chained(5)
assert.Equal(t, 11, result, "MonadChain should chain endomorphisms correctly")
assert.Equal(t, 11, result, "MonadChain should execute left-to-right: (5 * 2) + 1 = 11")
chained2 := MonadChain(increment, double)
result2 := chained2(5)
assert.Equal(t, 12, result2, "MonadChain should respect order")
assert.Equal(t, 12, result2, "MonadChain should execute left-to-right: (5 + 1) * 2 = 12")
// Test with negative values
chained3 := MonadChain(negate, increment)
result3 := chained3(5)
assert.Equal(t, -4, result3, "MonadChain should work with negative values")
assert.Equal(t, -4, result3, "MonadChain should execute left-to-right: -(5) + 1 = -4")
}
// TestChain tests the Chain function
func TestChain(t *testing.T) {
// Chain(f) returns a function that applies its argument first, then f
chainWithIncrement := Chain(increment)
// chainWithIncrement(double) means: double first, then increment
chained := chainWithIncrement(double)
result := chained(5)
assert.Equal(t, 11, result, "Chain should create chaining function correctly")
assert.Equal(t, 11, result, "Chain should execute left-to-right: (5 * 2) + 1 = 11")
chainWithDouble := Chain(double)
// chainWithDouble(increment) means: increment first, then double
chained2 := chainWithDouble(increment)
result2 := chained2(5)
assert.Equal(t, 12, result2, "Chain should work with different endomorphisms")
assert.Equal(t, 12, result2, "Chain should execute left-to-right: (5 + 1) * 2 = 12")
// Test chaining with square
chainWithSquare := Chain(square)
// chainWithSquare(double) means: double first, then square
chained3 := chainWithSquare(double)
result3 := chained3(3)
assert.Equal(t, 36, result3, "Chain should work with square function")
assert.Equal(t, 36, result3, "Chain should execute left-to-right: (3 * 2) ^ 2 = 36")
}
// TestCompose tests the curried Compose function
func TestCompose(t *testing.T) {
// Compose(g) returns a function that applies g first, then its argument
composeWithIncrement := Compose(increment)
// composeWithIncrement(double) means: increment first, then double
composed := composeWithIncrement(double)
result := composed(5)
assert.Equal(t, 12, result, "Compose should execute right-to-left: (5 + 1) * 2 = 12")
composeWithDouble := Compose(double)
// composeWithDouble(increment) means: double first, then increment
composed2 := composeWithDouble(increment)
result2 := composed2(5)
assert.Equal(t, 11, result2, "Compose should execute right-to-left: (5 * 2) + 1 = 11")
// Test composing with square
composeWithSquare := Compose(square)
// composeWithSquare(double) means: square first, then double
composed3 := composeWithSquare(double)
result3 := composed3(3)
assert.Equal(t, 18, result3, "Compose should execute right-to-left: (3 ^ 2) * 2 = 18")
}
// TestMonadComposeVsCompose demonstrates the relationship between MonadCompose and Compose
func TestMonadComposeVsCompose(t *testing.T) {
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
// MonadCompose takes both functions at once
monadComposed := MonadCompose(double, increment)
result1 := monadComposed(5) // (5 + 1) * 2 = 12
// Compose is the curried version - takes one function, returns a function
curriedCompose := Compose(increment)
composed := curriedCompose(double)
result2 := composed(5) // (5 + 1) * 2 = 12
assert.Equal(t, result1, result2, "MonadCompose and Compose should produce the same result")
assert.Equal(t, 12, result1, "Both should execute right-to-left: (5 + 1) * 2 = 12")
// Demonstrate that Compose(g)(f) is equivalent to MonadCompose(f, g)
assert.Equal(t, MonadCompose(double, increment)(5), Compose(increment)(double)(5),
"Compose(g)(f) should equal MonadCompose(f, g)")
}
// TestOf tests the Of function
@@ -191,12 +260,14 @@ func TestIdentity(t *testing.T) {
assert.Equal(t, 0, id(0), "Identity should work with zero")
assert.Equal(t, -10, id(-10), "Identity should work with negative values")
// Identity should be neutral for composition
composed1 := Compose(id, double)
assert.Equal(t, 10, composed1(5), "Identity should be right neutral for composition")
// Identity should be neutral for composition (RIGHT-TO-LEFT)
// Compose(id, double) means: double first, then id
composed1 := MonadCompose(id, double)
assert.Equal(t, 10, composed1(5), "Identity should be left neutral: double(5) = 10")
composed2 := Compose(double, id)
assert.Equal(t, 10, composed2(5), "Identity should be left neutral for composition")
// Compose(double, id) means: id first, then double
composed2 := MonadCompose(double, id)
assert.Equal(t, 10, composed2(5), "Identity should be right neutral: id(5) then double = 10")
// Test with strings
idStr := Identity[string]()
@@ -207,10 +278,11 @@ func TestIdentity(t *testing.T) {
func TestSemigroup(t *testing.T) {
sg := Semigroup[int]()
// Test basic concat
// Test basic concat (RIGHT-TO-LEFT execution via Compose)
// Concat(double, increment) means: increment first, then double
combined := sg.Concat(double, increment)
result := combined(5)
assert.Equal(t, 11, result, "Semigroup concat should compose endomorphisms")
assert.Equal(t, 12, result, "Semigroup concat should execute right-to-left: (5 + 1) * 2 = 12")
// Test associativity: (f . g) . h = f . (g . h)
f := double
@@ -223,10 +295,12 @@ func TestSemigroup(t *testing.T) {
testValue := 3
assert.Equal(t, left(testValue), right(testValue), "Semigroup should be associative")
// Test with ConcatAll from semigroup package
// Test with ConcatAll from semigroup package (RIGHT-TO-LEFT)
// ConcatAll(double)(increment, square) means: square, then increment, then double
combined2 := S.ConcatAll(sg)(double)([]Endomorphism[int]{increment, square})
result2 := combined2(5)
assert.Equal(t, 121, result2, "Semigroup should work with ConcatAll")
// 5 -> square -> 25 -> increment -> 26 -> double -> 52
assert.Equal(t, 52, result2, "Semigroup ConcatAll should execute right-to-left: square(5)=25, +1=26, *2=52")
}
// TestMonoid tests the Monoid function
@@ -237,19 +311,21 @@ func TestMonoid(t *testing.T) {
empty := monoid.Empty()
assert.Equal(t, 42, empty(42), "Monoid empty should be identity")
// Test right identity: x . empty = x
// Test right identity: x . empty = x (RIGHT-TO-LEFT: empty first, then x)
// Concat(double, empty) means: empty first, then double
rightIdentity := monoid.Concat(double, empty)
assert.Equal(t, 10, rightIdentity(5), "Monoid should satisfy right identity")
assert.Equal(t, 10, rightIdentity(5), "Monoid should satisfy right identity: empty(5) then double = 10")
// Test left identity: empty . x = x
// Test left identity: empty . x = x (RIGHT-TO-LEFT: x first, then empty)
// Concat(empty, double) means: double first, then empty
leftIdentity := monoid.Concat(empty, double)
assert.Equal(t, 10, leftIdentity(5), "Monoid should satisfy left identity")
assert.Equal(t, 10, leftIdentity(5), "Monoid should satisfy left identity: double(5) then empty = 10")
// Test ConcatAll with multiple endomorphisms
// Test ConcatAll with multiple endomorphisms (RIGHT-TO-LEFT execution)
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
result := combined(5)
// (5 * 2) = 10, (10 + 1) = 11, (11 * 11) = 121
assert.Equal(t, 121, result, "Monoid should work with ConcatAll")
// RIGHT-TO-LEFT: square(5) = 25, increment(25) = 26, double(26) = 52
assert.Equal(t, 52, result, "Monoid ConcatAll should execute right-to-left: square(5)=25, +1=26, *2=52")
// Test ConcatAll with empty list should return identity
emptyResult := M.ConcatAll(monoid)([]Endomorphism[int]{})
@@ -294,19 +370,20 @@ func TestMonoidLaws(t *testing.T) {
// TestEndomorphismWithDifferentTypes tests endomorphisms with different types
func TestEndomorphismWithDifferentTypes(t *testing.T) {
// Test with strings
toUpper := func(s string) string {
// Test with strings (RIGHT-TO-LEFT execution)
addExclamation := func(s string) string {
return s + "!"
}
addPrefix := func(s string) string {
return "Hello, " + s
}
strComposed := Compose(toUpper, addPrefix)
// Compose(addExclamation, addPrefix) means: addPrefix first, then addExclamation
strComposed := MonadCompose(addExclamation, addPrefix)
result := strComposed("World")
assert.Equal(t, "Hello, World!", result, "Endomorphism should work with strings")
assert.Equal(t, "Hello, World!", result, "Compose should execute right-to-left with strings")
// Test with float64
// Test with float64 (RIGHT-TO-LEFT execution)
doubleFloat := func(x float64) float64 {
return x * 2.0
}
@@ -314,76 +391,116 @@ func TestEndomorphismWithDifferentTypes(t *testing.T) {
return x + 1.0
}
floatComposed := Compose(doubleFloat, addOne)
// Compose(doubleFloat, addOne) means: addOne first, then doubleFloat
floatComposed := MonadCompose(doubleFloat, addOne)
resultFloat := floatComposed(5.5)
assert.Equal(t, 12.0, resultFloat, "Endomorphism should work with float64")
// 5.5 + 1.0 = 6.5, 6.5 * 2.0 = 13.0
assert.Equal(t, 13.0, resultFloat, "Compose should execute right-to-left: (5.5 + 1.0) * 2.0 = 13.0")
}
// TestComplexCompositions tests more complex composition scenarios
func TestComplexCompositions(t *testing.T) {
// Create a pipeline of transformations
pipeline := Compose(
Compose(
Compose(double, increment),
// Create a pipeline of transformations (RIGHT-TO-LEFT execution)
// Innermost Compose is evaluated first in the composition chain
pipeline := MonadCompose(
MonadCompose(
MonadCompose(double, increment),
square,
),
negate,
)
// (5 * 2) = 10, (10 + 1) = 11, (11 * 11) = 121, -(121) = -121
// RIGHT-TO-LEFT: negate(5) = -5, square(-5) = 25, increment(25) = 26, double(26) = 52
result := pipeline(5)
assert.Equal(t, -121, result, "Complex composition should work correctly")
assert.Equal(t, 52, result, "Complex composition should execute right-to-left")
// Test using monoid to build the same pipeline
// Test using monoid to build the same pipeline (RIGHT-TO-LEFT)
monoid := Monoid[int]()
pipelineMonoid := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square, negate})
resultMonoid := pipelineMonoid(5)
assert.Equal(t, -121, resultMonoid, "Monoid-based pipeline should match composition")
// RIGHT-TO-LEFT: negate(5) = -5, square(-5) = 25, increment(25) = 26, double(26) = 52
assert.Equal(t, 52, resultMonoid, "Monoid-based pipeline should match composition (right-to-left)")
}
// TestOperatorType tests the Operator type
func TestOperatorType(t *testing.T) {
// Create an operator that lifts an int endomorphism to work on the length of strings
lengthOperator := func(f Endomorphism[int]) Endomorphism[string] {
return func(s string) string {
newLen := f(len(s))
if newLen > len(s) {
// Pad with spaces
for i := len(s); i < newLen; i++ {
s += " "
}
} else if newLen < len(s) {
// Truncate
s = s[:newLen]
}
return s
// Create an operator that transforms int endomorphisms
// This operator takes an endomorphism and returns a new one that applies it twice
applyTwice := func(f Endomorphism[int]) Endomorphism[int] {
return func(x int) int {
return f(f(x))
}
}
// Use the operator
var op Operator[int, string] = lengthOperator
doubleLength := op(double)
var op Operator[int] = applyTwice
doubleDouble := op(double)
result := doubleLength("hello") // len("hello") = 5, 5 * 2 = 10
assert.Equal(t, 10, len(result), "Operator should transform endomorphisms correctly")
assert.Equal(t, "hello ", result, "Operator should pad string correctly")
result := doubleDouble(5) // double(double(5)) = double(10) = 20
assert.Equal(t, 20, result, "Operator should transform endomorphisms correctly")
// Test with increment
incrementTwice := op(increment)
result2 := incrementTwice(5) // increment(increment(5)) = increment(6) = 7
assert.Equal(t, 7, result2, "Operator should work with different endomorphisms")
}
// BenchmarkCompose benchmarks the Compose function
func BenchmarkCompose(b *testing.B) {
composed := Compose(double, increment)
composed := MonadCompose(double, increment)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = composed(5)
}
}
// BenchmarkMonoidConcatAll benchmarks ConcatAll with monoid
// TestComposeVsChain demonstrates the key difference between Compose and Chain
func TestComposeVsChain(t *testing.T) {
double := N.Mul(2)
increment := func(x int) int { return x + 1 }
// Compose executes RIGHT-TO-LEFT
// Compose(double, increment) means: increment first, then double
composed := MonadCompose(double, increment)
composedResult := composed(5) // (5 + 1) * 2 = 12
// MonadChain executes LEFT-TO-RIGHT
// MonadChain(double, increment) means: double first, then increment
chained := MonadChain(double, increment)
chainedResult := chained(5) // (5 * 2) + 1 = 11
assert.Equal(t, 12, composedResult, "Compose should execute right-to-left")
assert.Equal(t, 11, chainedResult, "MonadChain should execute left-to-right")
assert.NotEqual(t, composedResult, chainedResult, "Compose and Chain should produce different results with non-commutative operations")
// To get the same result with Compose, we need to reverse the order
composedReversed := MonadCompose(increment, double)
assert.Equal(t, chainedResult, composedReversed(5), "Compose with reversed args should match Chain")
// Demonstrate with a more complex example
square := func(x int) int { return x * x }
// Compose: RIGHT-TO-LEFT
composed3 := MonadCompose(MonadCompose(square, increment), double)
// double(5) = 10, increment(10) = 11, square(11) = 121
result1 := composed3(5)
// MonadChain: LEFT-TO-RIGHT
chained3 := MonadChain(MonadChain(double, increment), square)
// double(5) = 10, increment(10) = 11, square(11) = 121
result2 := chained3(5)
assert.Equal(t, 121, result1, "Compose should execute right-to-left")
assert.Equal(t, 121, result2, "MonadChain should execute left-to-right")
assert.Equal(t, result1, result2, "Both should produce same result when operations are in correct order")
}
func BenchmarkMonoidConcatAll(b *testing.B) {
monoid := Monoid[int]()
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = combined(5)
}
}
@@ -393,7 +510,215 @@ func BenchmarkChain(b *testing.B) {
chainWithIncrement := Chain(increment)
chained := chainWithIncrement(double)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
_ = chained(5)
}
}
// TestFunctorLaws tests that endomorphisms satisfy the functor laws
func TestFunctorLaws(t *testing.T) {
// Functor Law 1: Identity
// map(id) = id
t.Run("Identity", func(t *testing.T) {
id := Identity[int]()
endo := double
// map(id)(endo) should equal endo
mapped := MonadMap(id, endo)
testValue := 5
assert.Equal(t, endo(testValue), mapped(testValue), "map(id) should equal id")
})
// Functor Law 2: Composition
// map(f . g) = map(f) . map(g)
t.Run("Composition", func(t *testing.T) {
f := double
g := increment
endo := square
// Left side: map(f . g)(endo)
composed := MonadCompose(f, g)
left := MonadMap(composed, endo)
// Right side: map(f)(map(g)(endo))
mappedG := MonadMap(g, endo)
right := MonadMap(f, mappedG)
testValue := 3
assert.Equal(t, left(testValue), right(testValue), "map(f . g) should equal map(f) . map(g)")
})
}
// TestApplicativeLaws tests that endomorphisms satisfy the applicative functor laws
func TestApplicativeLaws(t *testing.T) {
// Applicative Law 1: Identity
// ap(id, v) = v
t.Run("Identity", func(t *testing.T) {
id := Identity[int]()
v := double
applied := MonadAp(id, v)
testValue := 5
assert.Equal(t, v(testValue), applied(testValue), "ap(id, v) should equal v")
})
// Applicative Law 2: Composition
// ap(ap(ap(compose, u), v), w) = ap(u, ap(v, w))
t.Run("Composition", func(t *testing.T) {
u := double
v := increment
w := square
// For endomorphisms, ap is just composition
// Left side: ap(ap(ap(compose, u), v), w) = compose(compose(u, v), w)
left := MonadCompose(MonadCompose(u, v), w)
// Right side: ap(u, ap(v, w)) = compose(u, compose(v, w))
right := MonadCompose(u, MonadCompose(v, w))
testValue := 3
assert.Equal(t, left(testValue), right(testValue), "Applicative composition law")
})
// Applicative Law 3: Homomorphism
// ap(pure(f), pure(x)) = pure(f(x))
t.Run("Homomorphism", func(t *testing.T) {
// For endomorphisms, "pure" is just the identity function that returns a constant
// This law is trivially satisfied for endomorphisms
f := double
x := 5
// ap(f, id) applied to x should equal f(x)
id := Identity[int]()
applied := MonadAp(f, id)
assert.Equal(t, f(x), applied(x), "Homomorphism law")
})
}
// TestMonadLaws tests that endomorphisms satisfy the monad laws
func TestMonadLaws(t *testing.T) {
// Monad Law 1: Left Identity
// chain(pure(a), f) = f(a)
t.Run("LeftIdentity", func(t *testing.T) {
// For endomorphisms, "pure" is the identity function
// chain(id, f) = f
id := Identity[int]()
f := double
chained := MonadChain(id, f)
testValue := 5
assert.Equal(t, f(testValue), chained(testValue), "chain(id, f) should equal f")
})
// Monad Law 2: Right Identity
// chain(m, pure) = m
t.Run("RightIdentity", func(t *testing.T) {
m := double
id := Identity[int]()
chained := MonadChain(m, id)
testValue := 5
assert.Equal(t, m(testValue), chained(testValue), "chain(m, id) should equal m")
})
// Monad Law 3: Associativity
// chain(chain(m, f), g) = chain(m, x => chain(f(x), g))
t.Run("Associativity", func(t *testing.T) {
m := square
f := double
g := increment
// Left side: chain(chain(m, f), g)
left := MonadChain(MonadChain(m, f), g)
// Right side: chain(m, chain(f, g))
// For simple endomorphisms (not Kleisli arrows), this simplifies to:
right := MonadChain(m, MonadChain(f, g))
testValue := 3
assert.Equal(t, left(testValue), right(testValue), "Monad associativity law")
})
}
// TestMonadComposeVsMonadChain verifies the relationship between Compose and Chain
func TestMonadComposeVsMonadChain(t *testing.T) {
f := double
g := increment
// MonadCompose(f, g) should equal MonadChain(g, f)
// Because Compose is right-to-left and Chain is left-to-right
composed := MonadCompose(f, g)
chained := MonadChain(g, f)
testValue := 5
assert.Equal(t, composed(testValue), chained(testValue),
"MonadCompose(f, g) should equal MonadChain(g, f)")
}
// TestMapEqualsCompose verifies that Map is equivalent to Compose for endomorphisms
func TestMapEqualsCompose(t *testing.T) {
f := double
g := increment
// MonadMap(f, g) should equal MonadCompose(f, g)
mapped := MonadMap(f, g)
composed := MonadCompose(f, g)
testValue := 5
assert.Equal(t, composed(testValue), mapped(testValue),
"MonadMap should equal MonadCompose for endomorphisms")
// Curried versions
mapF := Map(f)
composeF := Compose(f)
mappedG := mapF(g)
composedG := composeF(g)
assert.Equal(t, composedG(testValue), mappedG(testValue),
"Map should equal Compose for endomorphisms (curried)")
}
// TestApEqualsCompose verifies that Ap is equivalent to Compose for endomorphisms
func TestApEqualsCompose(t *testing.T) {
f := double
g := increment
// MonadAp(f, g) should equal MonadCompose(f, g)
applied := MonadAp(f, g)
composed := MonadCompose(f, g)
testValue := 5
assert.Equal(t, composed(testValue), applied(testValue),
"MonadAp should equal MonadCompose for endomorphisms")
// Curried versions
apG := Ap(g)
composeG := Compose(g)
appliedF := apG(f)
composedF := composeG(f)
assert.Equal(t, composedF(testValue), appliedF(testValue),
"Ap should equal Compose for endomorphisms (curried)")
}
// TestChainFirst tests the ChainFirst operation
func TestChainFirst(t *testing.T) {
double := N.Mul(2)
// Track side effect
var sideEffect int
logEffect := func(x int) int {
sideEffect = x
return x + 100 // This result should be discarded
}
chained := MonadChainFirst(double, logEffect)
result := chained(5)
// Should return double's result (10), not logEffect's result
assert.Equal(t, 10, result, "ChainFirst should return first result")
// But side effect should have been executed with double's result
assert.Equal(t, 10, sideEffect, "ChainFirst should execute second function for effect")
}

10
v2/endomorphism/from.go Normal file
View File

@@ -0,0 +1,10 @@
package endomorphism
import (
"github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/semigroup"
)
func FromSemigroup[A any](s S.Semigroup[A]) Kleisli[A] {
return function.Bind2of2(s.Concat)
}

View File

@@ -35,7 +35,7 @@ import (
//
// Example:
//
// myFunc := func(x int) int { return x * 2 }
// myFunc := N.Mul(2)
// endo := endomorphism.Of(myFunc)
func Of[F ~func(A) A, A any](f F) Endomorphism[A] {
return f
@@ -75,7 +75,7 @@ func Unwrap[F ~func(A) A, A any](f Endomorphism[A]) F {
// result := id(42) // Returns: 42
//
// // Identity is neutral for composition
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// composed := endomorphism.Compose(id, double)
// // composed behaves exactly like double
func Identity[A any]() Endomorphism[A] {
@@ -88,25 +88,29 @@ func Identity[A any]() Endomorphism[A] {
// For endomorphisms, this operation is composition (Compose). This means:
// - Concat(f, Concat(g, h)) = Concat(Concat(f, g), h)
//
// IMPORTANT: Concat uses Compose, which executes RIGHT-TO-LEFT:
// - Concat(f, g) applies g first, then f
// - This is equivalent to Compose(f, g)
//
// The returned semigroup can be used with semigroup operations to combine
// multiple endomorphisms.
//
// Returns:
// - A Semigroup[Endomorphism[A]] where concat is composition
// - A Semigroup[Endomorphism[A]] where concat is composition (right-to-left)
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/semigroup"
//
// sg := endomorphism.Semigroup[int]()
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
//
// // Combine using the semigroup
// // Combine using the semigroup (RIGHT-TO-LEFT execution)
// combined := sg.Concat(double, increment)
// result := combined(5) // (5 * 2) + 1 = 11
// result := combined(5) // (5 + 1) * 2 = 12 (increment first, then double)
func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
return S.MakeSemigroup(Compose[A])
return S.MakeSemigroup(MonadCompose[A])
}
// Monoid returns a Monoid for endomorphisms where concat is composition and empty is identity.
@@ -115,6 +119,10 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// - The binary operation is composition (Compose)
// - The identity element is the identity function (Identity)
//
// IMPORTANT: Concat uses Compose, which executes RIGHT-TO-LEFT:
// - Concat(f, g) applies g first, then f
// - ConcatAll applies functions from right to left
//
// This satisfies the monoid laws:
// - Right identity: Concat(x, Empty) = x
// - Left identity: Concat(Empty, x) = x
@@ -124,20 +132,20 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// combine multiple endomorphisms.
//
// Returns:
// - A Monoid[Endomorphism[A]] with composition and identity
// - A Monoid[Endomorphism[A]] with composition (right-to-left) and identity
//
// Example:
//
// import M "github.com/IBM/fp-go/v2/monoid"
//
// monoid := endomorphism.Monoid[int]()
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
// square := func(x int) int { return x * x }
//
// // Combine multiple endomorphisms
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(double, increment, square)
// result := combined(5) // ((5 * 2) + 1) * ((5 * 2) + 1) = 121
// result := combined(5) // square(increment(double(5))) = square(increment(10)) = square(11) = 121
func Monoid[A any]() M.Monoid[Endomorphism[A]] {
return M.MakeMonoid(Compose[A], Identity[A]())
return M.MakeMonoid(MonadCompose[A], Identity[A]())
}

View File

@@ -29,7 +29,7 @@ type (
// Example:
//
// // Simple endomorphisms on integers
// double := func(x int) int { return x * 2 }
// double := N.Mul(2)
// increment := func(x int) int { return x + 1 }
//
// // Both are endomorphisms of type Endomorphism[int]
@@ -37,6 +37,8 @@ type (
// var g endomorphism.Endomorphism[int] = increment
Endomorphism[A any] = func(A) A
Kleisli[A any] = func(A) Endomorphism[A]
// Operator represents a transformation from one endomorphism to another.
//
// An Operator takes an endomorphism on type A and produces an endomorphism on type B.
@@ -52,5 +54,5 @@ type (
// return strconv.Itoa(result)
// }
// }
Operator[A, B any] = func(Endomorphism[A]) Endomorphism[B]
Operator[A any] = Endomorphism[Endomorphism[A]]
)

View File

@@ -15,7 +15,84 @@
package eq
// Contramap implements an Equals predicate based on a mapping
// Contramap creates an Eq[B] from an Eq[A] by providing a function that maps B to A.
// This is a contravariant functor operation that allows you to transform equality predicates
// by mapping the input type. It's particularly useful for comparing complex types by
// extracting comparable fields.
//
// The name "contramap" comes from category theory, where it represents a contravariant
// functor. Unlike regular map (covariant), which transforms the output, contramap
// transforms the input in the opposite direction.
//
// Type Parameters:
// - A: The type that has an existing Eq instance
// - B: The type for which we want to create an Eq instance
//
// Parameters:
// - f: A function that extracts or converts a value of type B to type A
//
// Returns:
// - A function that takes an Eq[A] and returns an Eq[B]
//
// The resulting Eq[B] compares two B values by:
// 1. Applying f to both values to get A values
// 2. Using the original Eq[A] to compare those A values
//
// Example - Compare structs by a single field:
//
// type Person struct {
// ID int
// Name string
// Age int
// }
//
// // Compare persons by ID only
// personEqByID := eq.Contramap(func(p Person) int {
// return p.ID
// })(eq.FromStrictEquals[int]())
//
// p1 := Person{ID: 1, Name: "Alice", Age: 30}
// p2 := Person{ID: 1, Name: "Bob", Age: 25}
// assert.True(t, personEqByID.Equals(p1, p2)) // Same ID, different names
//
// Example - Case-insensitive string comparison:
//
// type User struct {
// Username string
// Email string
// }
//
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
//
// userEqByUsername := eq.Contramap(func(u User) string {
// return u.Username
// })(caseInsensitiveEq)
//
// u1 := User{Username: "Alice", Email: "alice@example.com"}
// u2 := User{Username: "ALICE", Email: "different@example.com"}
// assert.True(t, userEqByUsername.Equals(u1, u2)) // Case-insensitive match
//
// Example - Nested field access:
//
// type Address struct {
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// // Compare persons by city
// personEqByCity := eq.Contramap(func(p Person) string {
// return p.Address.City
// })(eq.FromStrictEquals[string]())
//
// Contramap Law:
// Contramap must satisfy: Contramap(f)(Contramap(g)(eq)) = Contramap(g ∘ f)(eq)
// This means contramapping twice is the same as contramapping with the composed function.
func Contramap[A, B any](f func(b B) A) func(Eq[A]) Eq[B] {
return func(fa Eq[A]) Eq[B] {
equals := fa.Equals

View File

@@ -19,38 +19,188 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
// Eq represents an equality type class for type T.
// It provides a way to define custom equality semantics for any type,
// not just those that are comparable with Go's == operator.
//
// Type Parameters:
// - T: The type for which equality is defined
//
// Methods:
// - Equals(x, y T) bool: Returns true if x and y are considered equal
//
// Laws:
// An Eq instance must satisfy the equivalence relation laws:
// 1. Reflexivity: Equals(x, x) = true for all x
// 2. Symmetry: Equals(x, y) = Equals(y, x) for all x, y
// 3. Transitivity: If Equals(x, y) and Equals(y, z), then Equals(x, z)
//
// Example:
//
// // Create an equality predicate for integers
// intEq := eq.FromStrictEquals[int]()
// assert.True(t, intEq.Equals(42, 42))
// assert.False(t, intEq.Equals(42, 43))
//
// // Create a custom equality predicate
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
// assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
type Eq[T any] interface {
// Equals returns true if x and y are considered equal according to this equality predicate.
//
// Parameters:
// - x: The first value to compare
// - y: The second value to compare
//
// Returns:
// - true if x and y are equal, false otherwise
Equals(x, y T) bool
}
// eq is the internal implementation of the Eq interface.
// It wraps a comparison function to provide the Eq interface.
type eq[T any] struct {
c func(x, y T) bool
}
// Equals implements the Eq interface by delegating to the wrapped comparison function.
func (e eq[T]) Equals(x, y T) bool {
return e.c(x, y)
}
// strictEq is a helper function that uses Go's built-in == operator for comparison.
// It can only be used with comparable types.
func strictEq[A comparable](a, b A) bool {
return a == b
}
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
// FromStrictEquals constructs an Eq instance using Go's built-in == operator.
// This is the most common way to create an Eq for types that support ==.
//
// Type Parameters:
// - T: Must be a comparable type (supports ==)
//
// Returns:
// - An Eq[T] that uses == for equality comparison
//
// Example:
//
// intEq := eq.FromStrictEquals[int]()
// assert.True(t, intEq.Equals(42, 42))
// assert.False(t, intEq.Equals(42, 43))
//
// stringEq := eq.FromStrictEquals[string]()
// assert.True(t, stringEq.Equals("hello", "hello"))
// assert.False(t, stringEq.Equals("hello", "world"))
//
// Note: For types that are not comparable or require custom equality logic,
// use FromEquals instead.
func FromStrictEquals[T comparable]() Eq[T] {
return FromEquals(strictEq[T])
}
// FromEquals constructs an [EQ.Eq] from the comparison function
// FromEquals constructs an Eq instance from a custom comparison function.
// This allows defining equality for any type, including non-comparable types
// or types that need custom equality semantics.
//
// Type Parameters:
// - T: The type for which equality is being defined (can be any type)
//
// Parameters:
// - c: A function that takes two values of type T and returns true if they are equal
//
// Returns:
// - An Eq[T] that uses the provided function for equality comparison
//
// Example:
//
// // Case-insensitive string equality
// caseInsensitiveEq := eq.FromEquals(func(a, b string) bool {
// return strings.EqualFold(a, b)
// })
// assert.True(t, caseInsensitiveEq.Equals("Hello", "HELLO"))
//
// // Approximate float equality
// approxEq := eq.FromEquals(func(a, b float64) bool {
// return math.Abs(a-b) < 0.0001
// })
// assert.True(t, approxEq.Equals(1.0, 1.00009))
//
// // Custom struct equality (compare by specific fields)
// type Person struct { ID int; Name string }
// personEq := eq.FromEquals(func(a, b Person) bool {
// return a.ID == b.ID // Compare only by ID
// })
//
// Note: The provided function should satisfy the equivalence relation laws
// (reflexivity, symmetry, transitivity) for correct behavior.
func FromEquals[T any](c func(x, y T) bool) Eq[T] {
return eq[T]{c: c}
}
// Empty returns the equals predicate that is always true
// Empty returns an Eq instance that always returns true for any comparison.
// This is the identity element for the Eq Monoid and is useful when you need
// an equality predicate that accepts everything.
//
// Type Parameters:
// - T: The type for which the always-true equality is defined
//
// Returns:
// - An Eq[T] where Equals(x, y) always returns true
//
// Example:
//
// alwaysTrue := eq.Empty[int]()
// assert.True(t, alwaysTrue.Equals(1, 2))
// assert.True(t, alwaysTrue.Equals(42, 100))
//
// // Useful as identity in monoid operations
// monoid := eq.Monoid[string]()
// combined := monoid.Concat(eq.FromStrictEquals[string](), monoid.Empty())
// // combined behaves the same as FromStrictEquals
//
// Use cases:
// - As the identity element in Monoid operations
// - When you need a placeholder equality that accepts everything
// - In generic code that requires an Eq but doesn't need actual comparison
func Empty[T any]() Eq[T] {
return FromEquals(F.Constant2[T, T](true))
}
// Equals returns a predicate to test if one value equals the other under an equals predicate
// Equals returns a curried equality checking function.
// This is useful for partial application and functional composition.
//
// Type Parameters:
// - T: The type being compared
//
// Parameters:
// - eq: The Eq instance to use for comparison
//
// Returns:
// - A function that takes a value and returns another function that checks equality with that value
//
// Example:
//
// intEq := eq.FromStrictEquals[int]()
// equals42 := eq.Equals(intEq)(42)
//
// assert.True(t, equals42(42))
// assert.False(t, equals42(43))
//
// // Use in higher-order functions
// numbers := []int{40, 41, 42, 43, 44}
// filtered := array.Filter(equals42)(numbers)
// // filtered = [42]
//
// // Partial application
// equalsFunc := eq.Equals(intEq)
// equals10 := equalsFunc(10)
// equals20 := equalsFunc(20)
//
// This is particularly useful when working with functional programming patterns
// like map, filter, and other higher-order functions.
func Equals[T any](eq Eq[T]) func(T) func(T) bool {
return func(other T) func(T) bool {
return F.Bind2nd(eq.Equals, other)

View File

@@ -20,6 +20,65 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// Semigroup returns a Semigroup instance for Eq[A].
// A Semigroup provides a way to combine two values of the same type.
// For Eq, the combination uses logical AND - two values are equal only if
// they are equal according to BOTH equality predicates.
//
// Type Parameters:
// - A: The type for which equality predicates are being combined
//
// Returns:
// - A Semigroup[Eq[A]] that combines equality predicates with logical AND
//
// The Concat operation satisfies:
// - Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Example - Combine multiple equality checks:
//
// type User struct {
// Username string
// Email string
// }
//
// usernameEq := eq.Contramap(func(u User) string {
// return u.Username
// })(eq.FromStrictEquals[string]())
//
// emailEq := eq.Contramap(func(u User) string {
// return u.Email
// })(eq.FromStrictEquals[string]())
//
// // Users are equal only if BOTH username AND email match
// userEq := eq.Semigroup[User]().Concat(usernameEq, emailEq)
//
// u1 := User{Username: "alice", Email: "alice@example.com"}
// u2 := User{Username: "alice", Email: "alice@example.com"}
// u3 := User{Username: "alice", Email: "different@example.com"}
//
// assert.True(t, userEq.Equals(u1, u2)) // Both match
// assert.False(t, userEq.Equals(u1, u3)) // Email differs
//
// Example - Combine multiple field checks:
//
// type Product struct {
// ID int
// Name string
// Price float64
// }
//
// idEq := eq.Contramap(func(p Product) int { return p.ID })(eq.FromStrictEquals[int]())
// nameEq := eq.Contramap(func(p Product) string { return p.Name })(eq.FromStrictEquals[string]())
// priceEq := eq.Contramap(func(p Product) float64 { return p.Price })(eq.FromStrictEquals[float64]())
//
// sg := eq.Semigroup[Product]()
// // All three fields must match
// productEq := sg.Concat(sg.Concat(idEq, nameEq), priceEq)
//
// Use cases:
// - Combining multiple field comparisons for struct equality
// - Building complex equality predicates from simpler ones
// - Ensuring all conditions are met (logical AND of predicates)
func Semigroup[A any]() S.Semigroup[Eq[A]] {
return S.MakeSemigroup(func(x, y Eq[A]) Eq[A] {
return FromEquals(func(a, b A) bool {
@@ -28,6 +87,67 @@ func Semigroup[A any]() S.Semigroup[Eq[A]] {
})
}
// Monoid returns a Monoid instance for Eq[A].
// A Monoid extends Semigroup with an identity element (Empty).
// For Eq, the identity is an equality predicate that always returns true.
//
// Type Parameters:
// - A: The type for which the equality monoid is defined
//
// Returns:
// - A Monoid[Eq[A]] with:
// - Concat: Combines equality predicates with logical AND (from Semigroup)
// - Empty: An equality predicate that always returns true (identity element)
//
// Monoid Laws:
// 1. Left Identity: Concat(Empty(), x) = x
// 2. Right Identity: Concat(x, Empty()) = x
// 3. Associativity: Concat(Concat(x, y), z) = Concat(x, Concat(y, z))
//
// Example - Using the identity element:
//
// monoid := eq.Monoid[int]()
// intEq := eq.FromStrictEquals[int]()
//
// // Empty is the identity - combining with it doesn't change behavior
// leftIdentity := monoid.Concat(monoid.Empty(), intEq)
// rightIdentity := monoid.Concat(intEq, monoid.Empty())
//
// assert.True(t, leftIdentity.Equals(42, 42))
// assert.False(t, leftIdentity.Equals(42, 43))
// assert.True(t, rightIdentity.Equals(42, 42))
// assert.False(t, rightIdentity.Equals(42, 43))
//
// Example - Empty always returns true:
//
// monoid := eq.Monoid[string]()
// alwaysTrue := monoid.Empty()
//
// assert.True(t, alwaysTrue.Equals("hello", "world"))
// assert.True(t, alwaysTrue.Equals("same", "same"))
// assert.True(t, alwaysTrue.Equals("", "anything"))
//
// Example - Building complex equality with fold:
//
// type Person struct {
// FirstName string
// LastName string
// Age int
// }
//
// firstNameEq := eq.Contramap(func(p Person) string { return p.FirstName })(eq.FromStrictEquals[string]())
// lastNameEq := eq.Contramap(func(p Person) string { return p.LastName })(eq.FromStrictEquals[string]())
// ageEq := eq.Contramap(func(p Person) int { return p.Age })(eq.FromStrictEquals[int]())
//
// monoid := eq.Monoid[Person]()
// // Combine all predicates - all fields must match
// personEq := monoid.Concat(monoid.Concat(firstNameEq, lastNameEq), ageEq)
//
// Use cases:
// - Providing a neutral element for equality combinations
// - Generic algorithms that require a Monoid instance
// - Folding multiple equality predicates into one
// - Default "accept everything" equality predicate
func Monoid[A any]() M.Monoid[Eq[A]] {
return M.MakeMonoid(Semigroup[A]().Concat, Empty[A]())
}

View File

@@ -23,6 +23,7 @@ import (
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
@@ -266,7 +267,7 @@ func TestEither(t *testing.T) {
erased := Erase(42)
result := F.Pipe1(
SafeUnerase[int](erased),
E.Map[error](func(x int) int { return x * 2 }),
E.Map[error](N.Mul(2)),
)
assert.True(t, E.IsRight(result))

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,105 @@
package function
// Flip reverses the order of parameters of a curried function
// Flip reverses the order of parameters of a curried function.
//
// Given a curried function f that takes T1 then T2 and returns R,
// Flip returns a new curried function that takes T2 then T1 and returns R.
// This is useful when you have a curried function but need to apply its
// arguments in a different order.
//
// Mathematical notation:
// - Given: f: T1 → T2 → R
// - Returns: g: T2 → T1 → R where g(t2)(t1) = f(t1)(t2)
//
// Type Parameters:
// - T1: The type of the first parameter (becomes second after flip)
// - T2: The type of the second parameter (becomes first after flip)
// - R: The return type
//
// Parameters:
// - f: A curried function taking T1 then T2 and returning R
//
// Returns:
// - A new curried function taking T2 then T1 and returning R
//
// Relationship to Swap:
//
// Flip is the curried version of Swap. While Swap works with binary functions,
// Flip works with curried functions:
// - Swap: func(T1, T2) R → func(T2, T1) R
// - Flip: func(T1) func(T2) R → func(T2) func(T1) R
//
// Example - Basic usage:
//
// // Create a curried division function
// divide := Curry2(func(a, b float64) float64 { return a / b })
// // divide(10)(2) = 5.0 (10 / 2)
//
// // Flip the parameter order
// divideFlipped := Flip(divide)
// // divideFlipped(10)(2) = 0.2 (2 / 10)
//
// Example - String formatting:
//
// // Curried string formatter: format(template)(value)
// format := Curry2(func(template, value string) string {
// return fmt.Sprintf(template, value)
// })
//
// // Normal order: template first, then value
// result1 := format("Hello, %s!")("World") // "Hello, World!"
//
// // Flipped order: value first, then template
// formatFlipped := Flip(format)
// result2 := formatFlipped("Hello, %s!")("World") // "Hello, World!"
//
// // Useful for partial application in different order
// greetWorld := format("Hello, %s!")
// greetWorld("Alice") // "Hello, Alice!"
//
// formatAlice := formatFlipped("Alice")
// formatAlice("Hello, %s!") // "Hello, Alice!"
//
// Example - Practical use case with map operations:
//
// // Curried map lookup: getFrom(map)(key)
// getFrom := Curry2(func(m map[string]int, key string) int {
// return m[key]
// })
//
// data := map[string]int{"a": 1, "b": 2, "c": 3}
//
// // Create a getter for this specific map
// getValue := getFrom(data)
// getValue("a") // 1
//
// // Flip to create key-first version: get(key)(map)
// get := Flip(getFrom)
// getA := get("a")
// getA(data) // 1
//
// Example - Combining with other functional patterns:
//
// // Curried append: append(slice)(element)
// appendTo := Curry2(func(slice []int, elem int) []int {
// return append(slice, elem)
// })
//
// // Flip to get: prepend(element)(slice)
// prepend := Flip(appendTo)
//
// nums := []int{1, 2, 3}
// add4 := appendTo(nums)
// result1 := add4(4) // [1, 2, 3, 4]
//
// prependZero := prepend(0)
// result2 := prependZero(nums) // [1, 2, 3, 0]
//
// See also:
// - Swap: For flipping parameters of non-curried binary functions
// - Curry2: For converting binary functions to curried form
// - Uncurry2: For converting curried functions back to binary form
func Flip[T1, T2, R any](f func(T1) func(T2) R) func(T2) func(T1) R {
return func(t2 T2) func(T1) R {
return func(t1 T1) R {

View File

@@ -22,15 +22,265 @@ import (
"github.com/stretchr/testify/assert"
)
// TestFlip tests the Flip function with various scenarios
func TestFlip(t *testing.T) {
t.Run("flips string concatenation", func(t *testing.T) {
// Create a curried function that formats strings
format := Curry2(func(a, b string) string {
return fmt.Sprintf("%s:%s", a, b)
})
x := Curry2(func(a, b string) string {
return fmt.Sprintf("%s:%s", a, b)
// Original order: a then b
assert.Equal(t, "a:b", format("a")("b"))
assert.Equal(t, "hello:world", format("hello")("world"))
// Flipped order: b then a
flipped := Flip(format)
assert.Equal(t, "b:a", flipped("a")("b"))
assert.Equal(t, "world:hello", flipped("hello")("world"))
})
assert.Equal(t, "a:b", x("a")("b"))
t.Run("flips numeric operations", func(t *testing.T) {
// Curried subtraction: subtract(a)(b) = a - b
subtract := Curry2(func(a, b int) int {
return a - b
})
y := Flip(x)
// Original: 10 - 3 = 7
assert.Equal(t, 7, subtract(10)(3))
assert.Equal(t, "b:a", y("a")("b"))
// Flipped: 3 - 10 = -7
flipped := Flip(subtract)
assert.Equal(t, -7, flipped(10)(3))
})
t.Run("flips division", func(t *testing.T) {
// Curried division: divide(a)(b) = a / b
divide := Curry2(func(a, b float64) float64 {
return a / b
})
// Original: 10 / 2 = 5.0
assert.Equal(t, 5.0, divide(10)(2))
// Flipped: 2 / 10 = 0.2
flipped := Flip(divide)
assert.Equal(t, 0.2, flipped(10)(2))
})
t.Run("flips with partial application", func(t *testing.T) {
// Curried append-like operation
prepend := Curry2(func(prefix, text string) string {
return prefix + text
})
// Create specialized functions with original order
addHello := prepend("Hello, ")
assert.Equal(t, "Hello, World", addHello("World"))
assert.Equal(t, "Hello, Go", addHello("Go"))
// Flip and create specialized functions with reversed order
flipped := Flip(prepend)
addToWorld := flipped("World")
assert.Equal(t, "Hello, World", addToWorld("Hello, "))
assert.Equal(t, "Goodbye, World", addToWorld("Goodbye, "))
})
t.Run("flips with different types", func(t *testing.T) {
// Curried function with different input types
repeat := Curry2(func(s string, n int) string {
result := ""
for i := 0; i < n; i++ {
result += s
}
return result
})
// Original: repeat("x")(3) = "xxx"
assert.Equal(t, "xxx", repeat("x")(3))
assert.Equal(t, "abab", repeat("ab")(2))
// Flipped: repeat(3)("x") = "xxx"
flipped := Flip(repeat)
assert.Equal(t, "xxx", flipped(3)("x"))
assert.Equal(t, "abab", flipped(2)("ab"))
})
t.Run("double flip returns to original", func(t *testing.T) {
// Flipping twice should return to original behavior
original := Curry2(func(a, b string) string {
return a + "-" + b
})
flipped := Flip(original)
doubleFlipped := Flip(flipped)
// Original and double-flipped should behave the same
assert.Equal(t, original("a")("b"), doubleFlipped("a")("b"))
assert.Equal(t, "a-b", doubleFlipped("a")("b"))
})
t.Run("flips with complex types", func(t *testing.T) {
type Person struct {
Name string
Age int
}
// Curried function creating a person
makePerson := Curry2(func(name string, age int) Person {
return Person{Name: name, Age: age}
})
// Original order: name then age
alice := makePerson("Alice")(30)
assert.Equal(t, "Alice", alice.Name)
assert.Equal(t, 30, alice.Age)
// Flipped order: age then name
flipped := Flip(makePerson)
bob := flipped(25)("Bob")
assert.Equal(t, "Bob", bob.Name)
assert.Equal(t, 25, bob.Age)
})
t.Run("flips map operations", func(t *testing.T) {
// Curried map getter: get(map)(key)
get := Curry2(func(m map[string]int, key string) int {
return m[key]
})
data := map[string]int{"a": 1, "b": 2, "c": 3}
// Original: provide map first, then key
getValue := get(data)
assert.Equal(t, 1, getValue("a"))
assert.Equal(t, 2, getValue("b"))
// Flipped: provide key first, then map
flipped := Flip(get)
getA := flipped("a")
assert.Equal(t, 1, getA(data))
data2 := map[string]int{"a": 10, "b": 20}
assert.Equal(t, 10, getA(data2))
})
t.Run("flips boolean operations", func(t *testing.T) {
// Curried logical operation
implies := Curry2(func(a, b bool) bool {
return !a || b
})
// Test truth table for implication
assert.True(t, implies(true)(true)) // T → T = T
assert.False(t, implies(true)(false)) // T → F = F
assert.True(t, implies(false)(true)) // F → T = T
assert.True(t, implies(false)(false)) // F → F = T
// Flipped version (reverse implication)
flipped := Flip(implies)
assert.True(t, flipped(true)(true)) // T ← T = T
assert.True(t, flipped(true)(false)) // T ← F = T
assert.False(t, flipped(false)(true)) // F ← T = F
assert.True(t, flipped(false)(false)) // F ← F = T
})
t.Run("flips with slice operations", func(t *testing.T) {
// Curried slice append
appendTo := Curry2(func(slice []int, elem int) []int {
return append(slice, elem)
})
nums := []int{1, 2, 3}
// Original: provide slice first, then element
add4 := appendTo(nums)
result1 := add4(4)
assert.Equal(t, []int{1, 2, 3, 4}, result1)
// Flipped: provide element first, then slice
flipped := Flip(appendTo)
appendFive := flipped(5)
result2 := appendFive(nums)
assert.Equal(t, []int{1, 2, 3, 5}, result2)
})
}
// TestFlipProperties tests mathematical properties of Flip
func TestFlipProperties(t *testing.T) {
t.Run("flip is involutive (flip . flip = id)", func(t *testing.T) {
// Flipping twice should give back the original function behavior
original := Curry2(func(a, b int) int {
return a*10 + b
})
flipped := Flip(original)
doubleFlipped := Flip(flipped)
// Test with multiple inputs
testCases := []struct{ a, b int }{
{1, 2},
{5, 7},
{0, 0},
{-1, 3},
}
for _, tc := range testCases {
assert.Equal(t,
original(tc.a)(tc.b),
doubleFlipped(tc.a)(tc.b),
"flip(flip(f)) should equal f for inputs (%d, %d)", tc.a, tc.b)
}
})
t.Run("flip preserves function composition", func(t *testing.T) {
// If we have f: A → B → C and g: C → D
// then g ∘ f(a)(b) = g(f(a)(b))
// and g ∘ flip(f)(b)(a) = g(flip(f)(b)(a))
f := Curry2(func(a, b int) int {
return a + b
})
g := func(n int) int {
return n * 2
}
flippedF := Flip(f)
// Compose g with f
composed1 := func(a, b int) int {
return g(f(a)(b))
}
// Compose g with flipped f
composed2 := func(a, b int) int {
return g(flippedF(b)(a))
}
// Both should give the same result
assert.Equal(t, composed1(3, 5), composed2(3, 5))
assert.Equal(t, 16, composed1(3, 5)) // (3 + 5) * 2 = 16
})
}
// BenchmarkFlip benchmarks the Flip function
func BenchmarkFlip(b *testing.B) {
add := Curry2(func(a, b int) int {
return a + b
})
flipped := Flip(add)
b.Run("original", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = add(i)(i + 1)
}
})
b.Run("flipped", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = flipped(i)(i + 1)
}
})
}

View File

@@ -80,7 +80,6 @@ import (
A "github.com/IBM/fp-go/v2/array"
B "github.com/IBM/fp-go/v2/bytes"
E "github.com/IBM/fp-go/v2/either"
ENDO "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
C "github.com/IBM/fp-go/v2/http/content"
@@ -91,16 +90,17 @@ import (
L "github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/record"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
)
type (
Builder struct {
method O.Option[string]
method Option[string]
url string
headers http.Header
body O.Option[E.Either[error, []byte]]
body Option[Result[[]byte]]
query url.Values
}
@@ -117,19 +117,19 @@ var (
// Monoid is the [M.Monoid] for the [Endomorphism]
Monoid = ENDO.Monoid[*Builder]()
// Url is a [L.Lens] for the URL
// Url is a [Lens] for the URL
//
// Deprecated: use [URL] instead
Url = L.MakeLensRef((*Builder).GetURL, (*Builder).SetURL)
// URL is a [L.Lens] for the URL
// URL is a [Lens] for the URL
URL = L.MakeLensRef((*Builder).GetURL, (*Builder).SetURL)
// Method is a [L.Lens] for the HTTP method
// Method is a [Lens] for the HTTP method
Method = L.MakeLensRef((*Builder).GetMethod, (*Builder).SetMethod)
// Body is a [L.Lens] for the request body
// Body is a [Lens] for the request body
Body = L.MakeLensRef((*Builder).GetBody, (*Builder).SetBody)
// Headers is a [L.Lens] for the complete set of request headers
// Headers is a [Lens] for the complete set of request headers
Headers = L.MakeLensRef((*Builder).GetHeaders, (*Builder).SetHeaders)
// Query is a [L.Lens] for the set of query parameters
// Query is a [Lens] for the set of query parameters
Query = L.MakeLensRef((*Builder).GetQuery, (*Builder).SetQuery)
rawQuery = L.MakeLensRef(getRawQuery, setRawQuery)
@@ -139,11 +139,11 @@ var (
setHeader = F.Bind2of3((*Builder).SetHeader)
noHeader = O.None[string]()
noBody = O.None[E.Either[error, []byte]]()
noBody = O.None[Result[[]byte]]()
noQueryArg = O.None[string]()
parseURL = E.Eitherize1(url.Parse)
parseQuery = E.Eitherize1(url.ParseQuery)
parseURL = result.Eitherize1(url.Parse)
parseQuery = result.Eitherize1(url.ParseQuery)
// WithQuery creates a [Endomorphism] for a complete set of query parameters
WithQuery = Query.Set
@@ -159,12 +159,12 @@ var (
WithHeaders = Headers.Set
// WithBody creates a [Endomorphism] for a request body
WithBody = F.Flow2(
O.Of[E.Either[error, []byte]],
O.Of[Result[[]byte]],
Body.Set,
)
// WithBytes creates a [Endomorphism] for a request body using bytes
WithBytes = F.Flow2(
E.Of[error, []byte],
result.Of[[]byte],
WithBody,
)
// WithContentType adds the [H.ContentType] header
@@ -202,7 +202,7 @@ var (
)
// bodyAsBytes returns a []byte with a fallback to the empty array
bodyAsBytes = O.Fold(B.Empty, E.Fold(F.Ignore1of1[error](B.Empty), F.Identity[[]byte]))
bodyAsBytes = O.Fold(B.Empty, result.Fold(F.Ignore1of1[error](B.Empty), F.Identity[[]byte]))
)
func setRawQuery(u *url.URL, raw string) *url.URL {
@@ -223,35 +223,35 @@ func (builder *Builder) clone() *Builder {
// GetTargetUrl constructs a full URL with query parameters on top of the provided URL string
//
// Deprecated: use [GetTargetURL] instead
func (builder *Builder) GetTargetUrl() E.Either[error, string] {
func (builder *Builder) GetTargetUrl() Result[string] {
return builder.GetTargetURL()
}
// GetTargetURL constructs a full URL with query parameters on top of the provided URL string
func (builder *Builder) GetTargetURL() E.Either[error, string] {
func (builder *Builder) GetTargetURL() Result[string] {
// construct the final URL
return F.Pipe3(
builder,
Url.Get,
parseURL,
E.Chain(F.Flow4(
result.Chain(F.Flow4(
T.Replicate2[*url.URL],
T.Map2(
F.Flow2(
F.Curry2(setRawQuery),
E.Of[error, func(string) *url.URL],
result.Of[func(string) *url.URL],
),
F.Flow3(
rawQuery.Get,
parseQuery,
E.Map[error](F.Flow2(
result.Map(F.Flow2(
F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()),
(url.Values).Encode,
)),
),
),
T.Tupled2(E.MonadAp[*url.URL, error, string]),
E.Map[error]((*url.URL).String),
T.Tupled2(result.MonadAp[*url.URL, string]),
result.Map((*url.URL).String),
)),
)
}
@@ -285,7 +285,7 @@ func (builder *Builder) SetQuery(query url.Values) *Builder {
return builder
}
func (builder *Builder) GetBody() O.Option[E.Either[error, []byte]] {
func (builder *Builder) GetBody() Option[Result[[]byte]] {
return builder.body
}
@@ -310,7 +310,7 @@ func (builder *Builder) SetHeaders(headers http.Header) *Builder {
return builder
}
func (builder *Builder) SetBody(body O.Option[E.Either[error, []byte]]) *Builder {
func (builder *Builder) SetBody(body Option[Result[[]byte]]) *Builder {
builder.body = body
return builder
}
@@ -325,7 +325,7 @@ func (builder *Builder) DelHeader(name string) *Builder {
return builder
}
func (builder *Builder) GetHeader(name string) O.Option[string] {
func (builder *Builder) GetHeader(name string) Option[string] {
return F.Pipe2(
name,
builder.headers.Get,
@@ -342,8 +342,8 @@ func (builder *Builder) GetHash() string {
return MakeHash(builder)
}
// Header returns a [L.Lens] for a single header
func Header(name string) L.Lens[*Builder, O.Option[string]] {
// Header returns a [Lens] for a single header
func Header(name string) Lens[*Builder, Option[string]] {
get := getHeader(name)
set := F.Bind1of2(setHeader(name))
del := F.Flow2(
@@ -351,7 +351,7 @@ func Header(name string) L.Lens[*Builder, O.Option[string]] {
LZ.Map(delHeader(name)),
)
return L.MakeLens(get, func(b *Builder, value O.Option[string]) *Builder {
return L.MakeLens(get, func(b *Builder, value Option[string]) *Builder {
cpy := b.clone()
return F.Pipe1(
value,
@@ -392,8 +392,8 @@ func WithJSON[T any](data T) Endomorphism {
)
}
// QueryArg is a [L.Lens] for the first value of a query argument
func QueryArg(name string) L.Lens[*Builder, O.Option[string]] {
// QueryArg is a [Lens] for the first value of a query argument
func QueryArg(name string) Lens[*Builder, Option[string]] {
return F.Pipe1(
Query,
L.Compose[*Builder](FM.AtValue(name)),

13
v2/http/builder/type.go Normal file
View File

@@ -0,0 +1,13 @@
package builder
import (
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
type (
Option[T any] = option.Option[T]
Result[T any] = result.Result[T]
Lens[S, T any] = lens.Lens[S, T]
)

View File

@@ -0,0 +1,49 @@
// Copyright (c) 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 option
import (
M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup"
)
// ApplySemigroup lifts a Semigroup over a type A to a Semigroup over Option[A].
// The resulting semigroup combines two Options using the applicative functor pattern.
//
// Example:
//
// intSemigroup := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
// optSemigroup := ApplySemigroup(intSemigroup)
// result := optSemigroup.Concat(Some(2), Some(3)) // Some(5)
// result := optSemigroup.Concat(Some(2), None[int]()) // None
func ApplySemigroup[A any](s S.Semigroup[A]) S.Semigroup[Option[A]] {
return S.ApplySemigroup(MonadMap[A, func(A) A], MonadAp[A, A], s)
}
// ApplicativeMonoid returns a Monoid that concatenates Option instances via their applicative functor.
// This combines the monoid structure of the underlying type with the Option structure.
//
// Example:
//
// intMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
// optMonoid := ApplicativeMonoid(intMonoid)
// result := optMonoid.Concat(Some(2), Some(3)) // Some(5)
// result := optMonoid.Empty() // Some(0)
//
//go:inline
func ApplicativeMonoid[A any](m M.Monoid[A]) M.Monoid[Option[A]] {
return M.ApplicativeMonoid(Of[A], MonadMap[A, func(A) A], MonadAp[A, A], m)
}

View File

@@ -0,0 +1,96 @@
// Copyright (c) 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 option
// TraverseArrayG transforms an array by applying a function that returns an Option to each element.
// Returns Some containing the array of results if all operations succeed, None if any fails.
// This is the generic version that works with custom slice types.
//
// Example:
//
// parse := func(s string) Option[int] {
// n, err := strconv.Atoi(s)
// if err != nil { return None[int]() }
// return Some(n)
// }
// result := TraverseArrayG[[]string, []int](parse)([]string{"1", "2", "3"}) // Some([1, 2, 3])
// result := TraverseArrayG[[]string, []int](parse)([]string{"1", "x", "3"}) // None
func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f Kleisli[A, B]) Kleisli[GA, GB] {
return func(g GA) (GB, bool) {
bs := make(GB, len(g))
for i, a := range g {
b, bok := f(a)
if !bok {
return bs, false
}
bs[i] = b
}
return bs, true
}
}
// TraverseArray transforms an array by applying a function that returns an Option to each element.
// Returns Some containing the array of results if all operations succeed, None if any fails.
//
// Example:
//
// validate := func(x int) Option[int] {
// if x > 0 { return Some(x * 2) }
// return None[int]()
// }
// result := TraverseArray(validate)([]int{1, 2, 3}) // Some([2, 4, 6])
// result := TraverseArray(validate)([]int{1, -1, 3}) // None
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return TraverseArrayG[[]A, []B](f)
}
// TraverseArrayWithIndexG transforms an array by applying an indexed function that returns an Option.
// The function receives both the index and the element.
// This is the generic version that works with custom slice types.
//
// Example:
//
// f := func(i int, s string) Option[string] {
// return Some(fmt.Sprintf("%d:%s", i, s))
// }
// result := TraverseArrayWithIndexG[[]string, []string](f)([]string{"a", "b"}) // Some(["0:a", "1:b"])
func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) (B, bool)) Kleisli[GA, GB] {
return func(g GA) (GB, bool) {
bs := make(GB, len(g))
for i, a := range g {
b, bok := f(i, a)
if !bok {
return bs, false
}
bs[i] = b
}
return bs, true
}
}
// TraverseArrayWithIndex transforms an array by applying an indexed function that returns an Option.
// The function receives both the index and the element.
//
// Example:
//
// f := func(i int, x int) Option[int] {
// if x > i { return Some(x) }
// return None[int]()
// }
// result := TraverseArrayWithIndex(f)([]int{1, 2, 3}) // Some([1, 2, 3])
func TraverseArrayWithIndex[A, B any](f func(int, A) (B, bool)) Kleisli[[]A, []B] {
return TraverseArrayWithIndexG[[]A, []B](f)
}

View File

@@ -0,0 +1,18 @@
package option
import (
"testing"
"github.com/stretchr/testify/assert"
)
func AssertEq[A any](l A, lok bool) func(A, bool) func(*testing.T) {
return func(r A, rok bool) func(*testing.T) {
return func(t *testing.T) {
assert.Equal(t, lok, rok)
if lok && rok {
assert.Equal(t, l, r)
}
}
}
}

View File

@@ -0,0 +1,181 @@
// Copyright (c) 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 option
import (
"testing"
)
// Benchmark basic construction
func BenchmarkSome(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = Some(42)
}
}
func BenchmarkNone(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = None[int]()
}
}
// Benchmark basic operations
func BenchmarkIsSome(b *testing.B) {
v, ok := Some(42)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = IsSome(v, ok)
}
}
func BenchmarkMap(b *testing.B) {
v, ok := Some(21)
mapper := Map(func(x int) int { return x * 2 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = mapper(v, ok)
}
}
func BenchmarkChain(b *testing.B) {
v, ok := Some(21)
chainer := Chain(func(x int) (int, bool) {
if x > 0 {
return x * 2, true
}
return 0, false
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = chainer(v, ok)
}
}
func BenchmarkFilter(b *testing.B) {
v, ok := Some(42)
filter := Filter(func(x int) bool { return x > 0 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = filter(v, ok)
}
}
func BenchmarkGetOrElse(b *testing.B) {
v, ok := Some(42)
getter := GetOrElse(func() int { return 0 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = getter(v, ok)
}
}
// Benchmark collection operations
func BenchmarkTraverseArray_Small(b *testing.B) {
data := []int{1, 2, 3, 4, 5}
traverser := TraverseArray(func(x int) (int, bool) {
return x * 2, true
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = traverser(data)
}
}
func BenchmarkTraverseArray_Large(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
traverser := TraverseArray(func(x int) (int, bool) {
return x * 2, true
})
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = traverser(data)
}
}
// Benchmark do-notation
func BenchmarkDoBind(b *testing.B) {
type State struct {
x int
y int
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
s1, ok1 := Do(State{})
s2, ok2 := Bind(
func(x int) func(State) State {
return func(s State) State {
s.x = x
return s
}
},
func(s State) (int, bool) { return 10, true },
)(s1, ok1)
_, _ = Bind(
func(y int) func(State) State {
return func(s State) State {
s.y = y
return s
}
},
func(s State) (int, bool) { return 20, true },
)(s2, ok2)
}
}
// Benchmark conversions
func BenchmarkFromPredicate(b *testing.B) {
pred := FromPredicate(func(x int) bool { return x > 0 })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = pred(42)
}
}
func BenchmarkFromNillable(b *testing.B) {
val := 42
ptr := &val
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = FromNillable(ptr)
}
}
// Benchmark complex chains
func BenchmarkComplexChain(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Some(1)
v2, ok2 := Chain(func(x int) (int, bool) { return x + 1, true })(v1, ok1)
v3, ok3 := Chain(func(x int) (int, bool) { return x * 2, true })(v2, ok2)
_, _ = Chain(func(x int) (int, bool) { return x - 5, true })(v3, ok3)
}
}

356
v2/idiomatic/option/bind.go Normal file
View File

@@ -0,0 +1,356 @@
// Copyright (c) 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 option
import (
"github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type S to be used with the Bind operation.
// This is the starting point for building up a context using do-notation style.
//
// Parameters:
// - empty: The initial empty context value
//
// Example:
//
// type Result struct {
// x int
// y string
// }
// result := Do(Result{})
func Do[S any](
empty S,
) (S, bool) {
return Of(empty)
}
// Bind attaches the result of a computation to a context S1 to produce a context S2.
// This is used in do-notation style to sequentially build up a context.
//
// Parameters:
// - setter: A function that takes a value and returns a function to update the context
// - f: A function that computes an Option value from the current context
//
// Example:
//
// type State struct { x int; y int }
// result := F.Pipe2(
// Do(State{}),
// Bind(func(x int) func(State) State {
// return func(s State) State { s.x = x; return s }
// }, func(s State) (int, bool) { return 42, true }),
// )
func Bind[S1, S2, A any](
setter func(A) func(S1) S2,
f Kleisli[S1, A],
) Operator[S1, S2] {
return func(s1 S1, s1ok bool) (s2 S2, s2ok bool) {
if s1ok {
a, aok := f(s1)
if aok {
return Of(setter(a)(s1))
}
}
return
}
}
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
// Unlike Bind, the computation function returns a plain value, not an Option.
//
// Parameters:
// - key: A function that takes a value and returns a function to update the context
// - f: A pure function that computes a value from the current context
//
// Example:
//
// type State struct { x int; computed int }
// result := F.Pipe2(
// Do(State{x: 5}),
// Let(func(c int) func(State) State {
// return func(s State) State { s.computed = c; return s }
// }, func(s State) int { return s.x * 2 }),
// )
func Let[S1, S2, B any](
key func(B) func(S1) S2,
f func(S1) B,
) Operator[S1, S2] {
return func(s1 S1, s1ok bool) (s2 S2, s2ok bool) {
if s1ok {
return Of(key(f(s1))(s1))
}
return
}
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
//
// Parameters:
// - key: A function that takes a value and returns a function to update the context
// - b: The constant value to attach to the context
//
// Example:
//
// type State struct { x int; name string }
// result := F.Pipe2(
// Do(State{x: 5}),
// LetTo(func(n string) func(State) State {
// return func(s State) State { s.name = n; return s }
// }, "example"),
// )
func LetTo[S1, S2, B any](
key func(B) func(S1) S2,
b B,
) Operator[S1, S2] {
kb := key(b)
return func(s1 S1, s1ok bool) (s2 S2, s2ok bool) {
if s1ok {
return Of(kb(s1))
}
return
}
}
// BindTo initializes a new state S1 from a value T.
// This is typically used as the first operation after creating an Option value.
//
// Parameters:
// - setter: A function that creates the initial context from a value
//
// Example:
//
// type State struct { value int }
// result := F.Pipe1(
// Some(42),
// BindTo(func(x int) State { return State{value: x} }),
// )
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
return func(t T, tok bool) (s1 S1, s1ok bool) {
if tok {
return Of(setter(t))
}
return
}
}
// ApS attaches a value to a context S1 to produce a context S2 by considering the context and the value concurrently.
// This uses the applicative functor pattern, allowing parallel composition.
//
// Parameters:
// - setter: A function that takes a value and returns a function to update the context
//
// Returns a function that takes an Option (value, bool) and returns an Operator.
//
// Example:
//
// type State struct { x int; y int }
// result := F.Pipe2(
// Do(State{}),
// ApS(func(x int) func(State) State {
// return func(s State) State { s.x = x; return s }
// }, Some(42)),
// )
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
) func(T, bool) Operator[S1, S2] {
return func(t T, tok bool) Operator[S1, S2] {
if tok {
st := setter(t)
return func(s1 S1, s1ok bool) (s2 S2, s2ok bool) {
if s1ok {
return Of(st(s1))
}
return
}
}
return func(_ S1, _ bool) (s2 S2, s2ok bool) {
return
}
}
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type Address struct {
// Street string
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// // Create a lens for the Address field
// addressLens := lens.MakeLens(
// func(p Person) Address { return p.Address },
// func(p Person, a Address) Person { p.Address = a; return p },
// )
//
// // Use ApSL to update the address
// result := F.Pipe2(
// option.Some(Person{Name: "Alice"}),
// option.ApSL(
// addressLens,
// option.Some(Address{Street: "Main St", City: "NYC"}),
// ),
// )
//
// Parameters:
// - lens: A lens that focuses on a field within the structure S
//
// Returns a function that takes an Option (value, bool) and returns an Operator.
func ApSL[S, T any](
lens L.Lens[S, T],
) func(T, bool) Operator[S, S] {
return ApS(lens.Set)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// an Option that produces the new value.
//
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
// the current value of the focused field.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Increment the counter, but return None if it would exceed 100
// increment := func(v int) option.Option[int] {
// if v >= 100 {
// return option.None[int]()
// }
// return option.Some(v + 1)
// }
//
// result := F.Pipe1(
// option.Some(Counter{Value: 42}),
// option.BindL(valueLens, increment),
// ) // Some(Counter{Value: 43})
//
// Parameters:
// - lens: A lens that focuses on a field within the structure S
// - f: A function that computes an Option value from the current field value
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return Bind(lens.Set, func(s S) (T, bool) {
return f(lens.Get(s))
})
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in Option).
//
// This is useful for pure transformations that cannot fail, such as mathematical operations,
// string manipulations, or other deterministic updates.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Double the counter value
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// option.Some(Counter{Value: 21}),
// option.LetL(valueLens, double),
// ) // Some(Counter{Value: 42})
//
// Parameters:
// - lens: A lens that focuses on a field within the structure S
// - f: A pure transformation function for the field value
func LetL[S, T any](
lens L.Lens[S, T],
f Endomorphism[T],
) Operator[S, S] {
return Let(lens.Set, function.Flow2(lens.Get, f))
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// This is useful for resetting fields, initializing values, or setting fields to
// predetermined constants.
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// option.Some(Config{Debug: true, Timeout: 30}),
// option.LetToL(debugLens, false),
// ) // Some(Config{Debug: false, Timeout: 30})
//
// Parameters:
// - lens: A lens that focuses on a field within the structure S
// - b: The constant value to set the field to
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Operator[S, S] {
return LetTo(lens.Set, b)
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) 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 option
import (
"testing"
"github.com/IBM/fp-go/v2/internal/utils"
)
func getLastName(s utils.Initial) (string, bool) {
return Of("Doe")
}
func getGivenName(s utils.WithLastName) (string, bool) {
return Of("John")
}
func TestBind(t *testing.T) {
res, resok := Flow3(
Bind(utils.SetLastName, getLastName),
Bind(utils.SetGivenName, getGivenName),
Map(utils.GetFullName),
)(Do(utils.Empty))
AssertEq(Of("John Doe"))(res, resok)(t)
}
func TestApS(t *testing.T) {
res, resok := Flow3(
ApS(utils.SetLastName)(Of("Doe")),
ApS(utils.SetGivenName)(Of("John")),
Map(utils.GetFullName),
)(Do(utils.Empty))
AssertEq(Of("John Doe"))(res, resok)(t)
}

View File

@@ -0,0 +1,123 @@
// Copyright (c) 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 option
import (
"testing"
)
// Benchmark shallow chain (1 step)
func BenchmarkChain_1Step(b *testing.B) {
v, ok := Some(1)
chainer := Chain(func(x int) (int, bool) { return x + 1, true })
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = chainer(v, ok)
}
}
// Benchmark moderate chain (3 steps)
func BenchmarkChain_3Steps(b *testing.B) {
v, ok := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Chain(func(x int) (int, bool) { return x + 1, true })(v, ok)
v2, ok2 := Chain(func(x int) (int, bool) { return x * 2, true })(v1, ok1)
_, _ = Chain(func(x int) (int, bool) { return x - 5, true })(v2, ok2)
}
}
// Benchmark deep chain (5 steps)
func BenchmarkChain_5Steps(b *testing.B) {
v, ok := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Chain(func(x int) (int, bool) { return x + 1, true })(v, ok)
v2, ok2 := Chain(func(x int) (int, bool) { return x * 2, true })(v1, ok1)
v3, ok3 := Chain(func(x int) (int, bool) { return x - 5, true })(v2, ok2)
v4, ok4 := Chain(func(x int) (int, bool) { return x * 10, true })(v3, ok3)
_, _ = Chain(func(x int) (int, bool) { return x + 100, true })(v4, ok4)
}
}
// Benchmark very deep chain (10 steps)
func BenchmarkChain_10Steps(b *testing.B) {
v, ok := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Chain(func(x int) (int, bool) { return x + 1, true })(v, ok)
v2, ok2 := Chain(func(x int) (int, bool) { return x * 2, true })(v1, ok1)
v3, ok3 := Chain(func(x int) (int, bool) { return x - 5, true })(v2, ok2)
v4, ok4 := Chain(func(x int) (int, bool) { return x * 10, true })(v3, ok3)
v5, ok5 := Chain(func(x int) (int, bool) { return x + 100, true })(v4, ok4)
v6, ok6 := Chain(func(x int) (int, bool) { return x - 50, true })(v5, ok5)
v7, ok7 := Chain(func(x int) (int, bool) { return x * 3, true })(v6, ok6)
v8, ok8 := Chain(func(x int) (int, bool) { return x + 20, true })(v7, ok7)
v9, ok9 := Chain(func(x int) (int, bool) { return x / 2, true })(v8, ok8)
_, _ = Chain(func(x int) (int, bool) { return x - 10, true })(v9, ok9)
}
}
// Benchmark Map-based chain (should be faster due to inlining)
func BenchmarkMap_5Steps(b *testing.B) {
v, ok := Some(1)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v1, ok1 := Map(func(x int) int { return x + 1 })(v, ok)
v2, ok2 := Map(func(x int) int { return x * 3 })(v1, ok1)
v3, ok3 := Map(func(x int) int { return x + 20 })(v2, ok2)
v4, ok4 := Map(func(x int) int { return x / 2 })(v3, ok3)
_, _ = Map(func(x int) int { return x - 10 })(v4, ok4)
}
}
// Real-world example: parsing and validating user input
func BenchmarkChain_RealWorld_Validation(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
s, sok := Some("42")
// Step 1: Validate not empty
v1, ok1 := Chain(func(s string) (string, bool) {
if len(s) > 0 {
return s, true
}
return "", false
})(s, sok)
// Step 2: Parse to int (simulated)
v2, ok2 := Chain(func(s string) (int, bool) {
if s == "42" {
return 42, true
}
return 0, false
})(v1, ok1)
// Step 3: Validate range
_, _ = Chain(func(n int) (int, bool) {
if n > 0 && n < 100 {
return n, true
}
return 0, false
})(v2, ok2)
}
}

113
v2/idiomatic/option/core.go Normal file
View File

@@ -0,0 +1,113 @@
// Copyright (c) 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 option
import "fmt"
type (
Operator[A, B any] = func(A, bool) (B, bool)
Kleisli[A, B any] = func(A) (B, bool)
)
// IsSome checks if an Option contains a value.
//
// Parameters:
// - t: The value of the Option
// - tok: Whether the Option contains a value (true for Some, false for None)
//
// Example:
//
// opt := Some(42)
// IsSome(opt) // true
// opt := None[int]()
// IsSome(opt) // false
//
//go:inline
func IsSome[T any](t T, tok bool) bool {
return tok
}
// IsNone checks if an Option is None (contains no value).
//
// Parameters:
// - t: The value of the Option
// - tok: Whether the Option contains a value (true for Some, false for None)
//
// Example:
//
// opt := None[int]()
// IsNone(opt) // true
// opt := Some(42)
// IsNone(opt) // false
//
//go:inline
func IsNone[T any](t T, tok bool) bool {
return !tok
}
// Some creates an Option that contains a value.
//
// Parameters:
// - value: The value to wrap in Some
//
// Example:
//
// opt := Some(42) // Option containing 42
// opt := Some("hello") // Option containing "hello"
//
//go:inline
func Some[T any](value T) (T, bool) {
return value, true
}
// Of creates an Option that contains a value.
// This is an alias for Some and is used in monadic contexts.
//
// Parameters:
// - value: The value to wrap in Some
//
// Example:
//
// opt := Of(42) // Option containing 42
//
//go:inline
func Of[T any](value T) (T, bool) {
return Some(value)
}
// None creates an Option that contains no value.
//
// Example:
//
// opt := None[int]() // Empty Option of type int
// opt := None[string]() // Empty Option of type string
//
//go:inline
func None[T any]() (t T, tok bool) {
return
}
// ToString converts an Option to a string representation for debugging.
//
// Parameters:
// - t: The value of the Option
// - tok: Whether the Option contains a value (true for Some, false for None)
func ToString[T any](t T, tok bool) string {
if tok {
return fmt.Sprintf("Some[%T](%v)", t, t)
}
return fmt.Sprintf("None[%T]", t)
}

View File

@@ -0,0 +1,257 @@
mode: set
github.com/IBM/fp-go/v2/idiomatic/option/array.go:31.82,32.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:32.31,34.23 2 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:34.23,36.12 2 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:36.12,38.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:39.4,39.22 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:41.3,41.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:56.65,58.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:70.100,71.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:71.31,73.23 2 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:73.23,75.12 2 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:75.12,77.5 1 0
github.com/IBM/fp-go/v2/idiomatic/option/array.go:78.4,78.22 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:80.3,80.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/array.go:94.83,96.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:38.13,40.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:61.20,62.51 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:62.51,63.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:63.11,65.11 2 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:65.11,67.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:69.3,69.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:92.20,93.51 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:93.51,94.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:94.11,96.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:97.3,97.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:119.20,121.51 2 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:121.51,122.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:122.11,124.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:125.3,125.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:144.19,145.48 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:145.48,146.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:146.10,148.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:149.3,149.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:172.34,173.46 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:173.46,174.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:174.10,176.53 2 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:176.53,177.13 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:177.13,179.6 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:180.5,180.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:183.3,183.48 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:183.48,185.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:229.32,231.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:274.18,275.44 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:275.44,277.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:316.18,318.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/bind.go:354.18,356.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:39.40,41.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:57.40,59.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:72.37,74.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:87.35,89.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:99.36,101.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:108.44,109.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:109.9,111.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/core.go:112.2,112.35 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:49.62,50.50 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:50.50,51.37 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:51.37,52.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:52.12,53.13 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:53.13,55.6 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:56.5,56.17 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:58.4,58.16 1 1
github.com/IBM/fp-go/v2/idiomatic/option/eq.go:82.72,84.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:7.74,9.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:15.88,17.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:23.116,25.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:31.130,32.43 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:32.43,34.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:41.158,43.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:49.172,50.43 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:50.43,52.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:59.200,61.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:67.214,68.43 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:68.43,70.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:77.242,79.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:85.256,86.43 1 1
github.com/IBM/fp-go/v2/idiomatic/option/function.go:86.43,88.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/functor.go:26.62,28.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/functor.go:39.44,41.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:8.81,9.38 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:9.38,10.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:10.27,12.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:13.3,13.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:18.125,19.52 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:19.52,20.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:20.27,21.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:21.28,23.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:25.3,25.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:30.169,31.66 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:31.66,32.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:32.27,33.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:33.28,34.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:34.29,36.6 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:39.3,39.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:44.213,45.80 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:45.80,46.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:46.27,47.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:47.28,48.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:48.29,49.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:49.30,51.7 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:55.3,55.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:60.257,61.94 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:61.94,62.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:62.27,63.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:63.28,64.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:64.29,65.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:65.30,66.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:66.31,68.8 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:73.3,73.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:78.301,79.108 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:79.108,80.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:80.27,81.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:81.28,82.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:82.29,83.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:83.30,84.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:84.31,85.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:85.32,87.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:93.3,93.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:98.345,99.122 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:99.122,100.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:100.27,101.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:101.28,102.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:102.29,103.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:103.30,104.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:104.31,105.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:105.32,106.33 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:106.33,108.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:115.3,115.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:120.389,121.136 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:121.136,122.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:122.27,123.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:123.28,124.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:124.29,125.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:125.30,126.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:126.31,127.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:127.32,128.33 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:128.33,129.34 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:129.34,131.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:139.3,139.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:144.433,145.150 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:145.150,146.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:146.27,147.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:147.28,148.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:148.29,149.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:149.30,150.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:150.31,151.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:151.32,152.33 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:152.33,153.34 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:153.34,154.35 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:154.35,156.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:165.3,165.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:170.487,171.168 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:171.168,172.27 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:172.27,173.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:173.28,174.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:174.29,175.30 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:175.30,176.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:176.31,177.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:177.32,178.33 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:178.33,179.34 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:179.34,180.35 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:180.35,181.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:181.39,183.13 1 1
github.com/IBM/fp-go/v2/idiomatic/option/gen.go:193.3,193.9 1 0
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:57.70,58.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:58.39,60.20 2 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:60.20,62.12 2 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:62.12,64.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:65.4,65.22 1 1
github.com/IBM/fp-go/v2/idiomatic/option/iter.go:67.3,67.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:24.103,25.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:25.39,26.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:26.10,28.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:28.9,30.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:31.3,31.16 1 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:56.72,58.44 2 1
github.com/IBM/fp-go/v2/idiomatic/option/logger.go:58.44,60.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:53.60,54.29 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:54.29,56.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:60.45,62.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:65.48,67.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:70.57,72.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:86.43,88.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:103.59,104.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:104.10,105.58 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:105.58,106.13 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:106.13,108.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:109.4,109.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:112.2,112.51 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:112.51,114.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:128.50,129.47 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:129.47,130.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:130.11,132.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:133.3,133.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:146.42,147.40 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:147.40,149.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:167.72,168.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:168.31,169.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:169.10,171.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:172.3,172.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:186.56,187.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:187.31,188.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:188.10,190.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:191.3,191.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:208.54,209.45 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:209.45,210.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:210.10,212.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:213.3,213.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:227.54,228.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:228.39,230.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:245.59,246.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:246.39,247.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:247.10,250.4 2 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:251.3,251.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:265.55,266.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:266.39,267.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:267.10,269.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:270.3,270.16 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:286.66,287.31 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:287.31,288.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:288.10,290.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:291.3,291.17 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:306.54,307.39 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:307.39,309.3 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:322.49,323.55 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:323.55,324.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:324.12,326.4 1 1
github.com/IBM/fp-go/v2/idiomatic/option/option.go:327.3,327.9 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:37.63,38.47 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:38.47,39.10 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:39.10,40.35 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:40.35,41.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:41.12,43.6 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:44.5,44.14 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:47.3,47.34 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:47.34,48.11 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:48.11,50.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:51.4,51.12 1 1
github.com/IBM/fp-go/v2/idiomatic/option/ord.go:64.71,66.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/pointed.go:26.45,28.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/pointed.go:37.38,39.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:30.105,31.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:31.32,33.24 2 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:33.24,34.25 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:34.25,36.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:36.10,38.5 1 0
github.com/IBM/fp-go/v2/idiomatic/option/record.go:41.3,41.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:56.88,58.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:71.121,72.32 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:72.32,74.24 2 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:74.24,75.28 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:75.28,77.5 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:77.10,79.5 1 0
github.com/IBM/fp-go/v2/idiomatic/option/record.go:82.3,82.18 1 1
github.com/IBM/fp-go/v2/idiomatic/option/record.go:97.104,99.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/type.go:18.37,21.2 2 1
github.com/IBM/fp-go/v2/idiomatic/option/type.go:35.39,37.2 1 1
github.com/IBM/fp-go/v2/idiomatic/option/type.go:48.38,50.2 1 1

236
v2/idiomatic/option/doc.go Normal file
View File

@@ -0,0 +1,236 @@
// Copyright (c) 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 option implements the Option monad using idiomatic Go tuple signatures.
//
// Unlike the standard option package which uses wrapper structs, this package represents
// Options as tuples (value, bool) where the boolean indicates presence (true) or absence (false).
// This approach is more idiomatic in Go and has better performance characteristics.
//
// # Type Signatures
//
// The core types used in this package are:
//
// Operator[A, B any] = func(A, bool) (B, bool) // Transforms an Option[A] to Option[B]
// Kleisli[A, B any] = func(A) (B, bool) // Monadic function from A to Option[B]
//
// # Basic Usage
//
// Create an Option with Some or None:
//
// some := Some(42) // (42, true)
// none := None[int]() // (0, false)
// opt := Of(42) // Alternative to Some: (42, true)
//
// Check if an Option contains a value:
//
// value, ok := Some(42)
// if ok {
// // value == 42
// }
//
// if IsSome(Some(42)) {
// // Option contains a value
// }
// if IsNone(None[int]()) {
// // Option is empty
// }
//
// Extract values:
//
// value, ok := Some(42) // Direct tuple unpacking: value == 42, ok == true
// value := GetOrElse(func() int { return 0 })(Some(42)) // Returns 42
// value := GetOrElse(func() int { return 0 })(None[int]()) // Returns 0
//
// # Transformations
//
// Map transforms the contained value:
//
// double := Map(func(x int) int { return x * 2 })
// result := double(Some(21)) // (42, true)
// result := double(None[int]()) // (0, false)
//
// Chain sequences operations that may fail:
//
// validate := Chain(func(x int) (int, bool) {
// if x > 0 { return x * 2, true }
// return 0, false
// })
// result := validate(Some(5)) // (10, true)
// result := validate(Some(-1)) // (0, false)
//
// Filter keeps values that satisfy a predicate:
//
// isPositive := Filter(func(x int) bool { return x > 0 })
// result := isPositive(Some(5)) // (5, true)
// result := isPositive(Some(-1)) // (0, false)
//
// # Working with Collections
//
// Transform arrays using TraverseArray:
//
// doublePositive := func(x int) (int, bool) {
// if x > 0 { return x * 2, true }
// return 0, false
// }
// result := TraverseArray(doublePositive)([]int{1, 2, 3}) // ([2, 4, 6], true)
// result := TraverseArray(doublePositive)([]int{1, -2, 3}) // ([], false)
//
// Transform with indexes:
//
// f := func(i int, x int) (int, bool) {
// if x > i { return x, true }
// return 0, false
// }
// result := TraverseArrayWithIndex(f)([]int{1, 2, 3}) // ([1, 2, 3], true)
//
// Transform records (maps):
//
// double := func(x int) (int, bool) { return x * 2, true }
// result := TraverseRecord(double)(map[string]int{"a": 1, "b": 2})
// // (map[string]int{"a": 2, "b": 4}, true)
//
// # Algebraic Operations
//
// Option supports various algebraic structures:
//
// - Functor: Map operations for transforming values
// - Applicative: Ap operations for applying wrapped functions
// - Monad: Chain operations for sequencing computations
// - Alternative: Alt operations for providing fallbacks
//
// Applicative example:
//
// fab := Some(func(x int) int { return x * 2 })
// fa := Some(21)
// result := Ap[int](fa)(fab) // (42, true)
//
// Alternative example:
//
// withDefault := Alt(func() (int, bool) { return 100, true })
// result := withDefault(Some(42)) // (42, true)
// result := withDefault(None[int]()) // (100, true)
//
// # Conversion Functions
//
// Convert predicates to Options:
//
// isPositive := FromPredicate(func(n int) bool { return n > 0 })
// result := isPositive(5) // (5, true)
// result := isPositive(-1) // (0, false)
//
// Convert nullable pointers to Options:
//
// var ptr *int = nil
// result := FromNillable(ptr) // (nil, false)
// val := 42
// result := FromNillable(&val) // (&val, true)
//
// Convert zero/non-zero values to Options:
//
// result := FromZero[int]()(0) // (0, true)
// result := FromZero[int]()(5) // (0, false)
// result := FromNonZero[int]()(5) // (5, true)
// result := FromNonZero[int]()(0) // (0, false)
//
// Use equality-based conversion:
//
// import "github.com/IBM/fp-go/v2/eq"
// equals42 := FromEq(eq.FromStrictEquals[int]())(42)
// result := equals42(42) // (42, true)
// result := equals42(10) // (0, false)
//
// # Do-Notation Style
//
// Build complex computations using do-notation:
//
// type Result struct {
// x int
// y int
// sum int
// }
//
// result := F.Pipe3(
// Do(Result{}),
// Bind(func(x int) func(Result) Result {
// return func(r Result) Result { r.x = x; return r }
// }, func(r Result) (int, bool) { return Some(10) }),
// Bind(func(y int) func(Result) Result {
// return func(r Result) Result { r.y = y; return r }
// }, func(r Result) (int, bool) { return Some(20) }),
// Let(func(sum int) func(Result) Result {
// return func(r Result) Result { r.sum = sum; return r }
// }, func(r Result) int { return r.x + r.y }),
// ) // (Result{x: 10, y: 20, sum: 30}, true)
//
// # Lens-Based Operations
//
// Use lenses for cleaner field updates:
//
// type Person struct {
// Name string
// Age int
// }
//
// ageLens := lens.MakeLens(
// func(p Person) int { return p.Age },
// func(p Person, age int) Person { p.Age = age; return p },
// )
//
// // Update using a lens
// incrementAge := BindL(ageLens, func(age int) (int, bool) {
// if age < 120 { return age + 1, true }
// return 0, false
// })
// result := incrementAge(Some(Person{Name: "Alice", Age: 30}))
// // (Person{Name: "Alice", Age: 31}, true)
//
// // Set using a lens
// setAge := LetToL(ageLens, 25)
// result := setAge(Some(Person{Name: "Bob", Age: 30}))
// // (Person{Name: "Bob", Age: 25}, true)
//
// # Folding and Reducing
//
// Fold provides a way to handle both Some and None cases:
//
// handler := Fold(
// func() string { return "no value" },
// func(x int) string { return fmt.Sprintf("value: %d", x) },
// )
// result := handler(Some(42)) // "value: 42"
// result := handler(None[int]()) // "no value"
//
// Reduce folds an Option into a single value:
//
// sum := Reduce(func(acc, val int) int { return acc + val }, 0)
// result := sum(Some(5)) // 5
// result := sum(None[int]()) // 0
//
// # Debugging
//
// Convert Options to strings for debugging:
//
// str := ToString(Some(42)) // "Some[int](42)"
// str := ToString(None[int]()) // "None[int]"
//
// # Subpackages
//
// - option/number: Number conversion utilities for working with Options
package option
//go:generate go run .. option --count 10 --filename gen.go
// Made with Bob

84
v2/idiomatic/option/eq.go Normal file
View File

@@ -0,0 +1,84 @@
// Copyright (c) 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 option
import (
EQ "github.com/IBM/fp-go/v2/eq"
)
// Eq constructs an equality predicate for Option[A] given an equality predicate for A.
// Two Options are equal if:
// - Both are None, or
// - Both are Some and their contained values are equal according to the provided Eq
//
// Parameters:
// - eq: An equality predicate for the contained type A
//
// Returns a curried function that takes two Options (as tuples) and returns true if they are equal.
//
// Example:
//
// intEq := eq.FromStrictEquals[int]()
// optEq := Eq(intEq)
//
// opt1 := Some(42) // (42, true)
// opt2 := Some(42) // (42, true)
// optEq(opt1)(opt2) // true
//
// opt3 := Some(43) // (43, true)
// optEq(opt1)(opt3) // false
//
// none1 := None[int]() // (0, false)
// none2 := None[int]() // (0, false)
// optEq(none1)(none2) // true
//
// optEq(opt1)(none1) // false
func Eq[A any](eq EQ.Eq[A]) func(A, bool) func(A, bool) bool {
return func(a1 A, a1ok bool) func(A, bool) bool {
return func(a2 A, a2ok bool) bool {
if a1ok {
if a2ok {
return eq.Equals(a1, a2)
}
return false
}
return !a2ok
}
}
}
// FromStrictEquals constructs an Eq for Option[A] using Go's built-in equality (==) for type A.
// This is a convenience function for comparable types.
//
// Returns a curried function that takes two Options (as tuples) and returns true if they are equal.
//
// Example:
//
// optEq := FromStrictEquals[int]()
//
// opt1 := Some(42) // (42, true)
// opt2 := Some(42) // (42, true)
// optEq(opt1)(opt2) // true
//
// none1 := None[int]() // (0, false)
// none2 := None[int]() // (0, false)
// optEq(none1)(none2) // true
//
// opt3 := Some(43) // (43, true)
// optEq(opt1)(opt3) // false
func FromStrictEquals[A comparable]() func(A, bool) func(A, bool) bool {
return Eq(EQ.FromStrictEquals[A]())
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 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 option
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEq(t *testing.T) {
r1, r1ok := Of(1)
r2, r2ok := Of(1)
r3, r3ok := Of(2)
n1, n1ok := None[int]()
eq := FromStrictEquals[int]()
assert.True(t, eq(r1, r1ok)(r1, r1ok))
assert.True(t, eq(r1, r1ok)(r2, r2ok))
assert.False(t, eq(r1, r1ok)(r3, r3ok))
assert.False(t, eq(r1, r1ok)(n1, n1ok))
assert.True(t, eq(n1, n1ok)(n1, n1ok))
assert.False(t, eq(n1, n1ok)(r2, r2ok))
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) 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 option
import "fmt"
func ExampleSome_creation() {
// Build an Option
none1, none1ok := None[int]()
some1, some1ok := Some("value")
// Build from a value
fromNillable := FromNillable[string]
nonFromNil, nonFromNilok := fromNillable(nil) // None[*string]
value := "value"
someFromPointer, someFromPointerok := fromNillable(&value) // Some[*string](xxx)
// some predicate
isEven := func(num int) bool {
return num%2 == 0
}
fromEven := FromPredicate(isEven)
noneFromPred, noneFromPredok := fromEven(3) // None[int]
someFromPred, someFromPredok := fromEven(4) // Some[int](4)
fmt.Println(ToString(none1, none1ok))
fmt.Println(ToString(some1, some1ok))
fmt.Println(ToString(nonFromNil, nonFromNilok))
fmt.Println(IsSome(someFromPointer, someFromPointerok))
fmt.Println(ToString(noneFromPred, noneFromPredok))
fmt.Println(ToString(someFromPred, someFromPredok))
// Output:
// None[int]
// Some[string](value)
// None[*string]
// true
// None[int]
// Some[int](4)
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) 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 option
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
func ExampleSome_extraction() {
noneValue, okFromNone := None[int]()
someValue, okFromSome := Of(42)
// Convert Option[T] with a default value
noneWithDefault := GetOrElse(F.Constant(0))(noneValue, okFromNone) // 0
someWithDefault := GetOrElse(F.Constant(0))(someValue, okFromSome) // 42
// Apply a different function on None/Some(...)
doubleOrZero := Fold(
F.Constant(0), // none case
N.Mul(2), // some case
) // func(ma Option[int]) int
doubleFromNone := doubleOrZero(noneValue, okFromNone) // 0
doubleFromSome := doubleOrZero(someValue, okFromSome) // 84
fmt.Printf("%d, %t\n", noneValue, okFromNone)
fmt.Printf("%d, %t\n", someValue, okFromSome)
fmt.Println(noneWithDefault)
fmt.Println(someWithDefault)
fmt.Println(doubleFromNone)
fmt.Println(doubleFromSome)
// Output:
// 0, false
// 42, true
// 0
// 42
// 0
// 84
}

View File

@@ -0,0 +1,89 @@
package option
// Pipe1 takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe1[F1 ~func(T0) (T1, bool), T0, T1 any](t0 T0, f1 F1) (T1, bool) {
return f1(t0)
}
// Flow1 creates a function that takes an initial value t0 and successively applies 1 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow1[F1 ~func(T0, bool) (T1, bool), T0, T1 any](f1 F1) func(T0, bool) (T1, bool) {
return f1
}
// Pipe2 takes an initial value t0 and successively applies 2 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe2[F1 ~func(T0) (T1, bool), F2 ~func(T1, bool) (T2, bool), T0, T1, T2 any](t0 T0, f1 F1, f2 F2) (T2, bool) {
return f2(f1(t0))
}
// Flow2 creates a function that takes an initial value t0 and successively applies 2 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow2[F1 ~func(T0, bool) (T1, bool), F2 ~func(T1, bool) (T2, bool), T0, T1, T2 any](f1 F1, f2 F2) func(T0, bool) (T2, bool) {
return func(t0 T0, t0ok bool) (T2, bool) {
return f2(f1(t0, t0ok))
}
}
// Pipe3 takes an initial value t0 and successively applies 3 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe3[F1 ~func(T0) (T1, bool), F2 ~func(T1, bool) (T2, bool), F3 ~func(T2, bool) (T3, bool), T0, T1, T2, T3 any](t0 T0, f1 F1, f2 F2, f3 F3) (T3, bool) {
return f3(f2(f1(t0)))
}
// Flow3 creates a function that takes an initial value t0 and successively applies 3 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow3[F1 ~func(T0, bool) (T1, bool), F2 ~func(T1, bool) (T2, bool), F3 ~func(T2, bool) (T3, bool), T0, T1, T2, T3 any](f1 F1, f2 F2, f3 F3) func(T0, bool) (T3, bool) {
return func(t0 T0, t0ok bool) (T3, bool) {
return f3(f2(f1(t0, t0ok)))
}
}
// Pipe4 takes an initial value t0 and successively applies 4 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe4[F1 ~func(T0) (T1, bool), F2 ~func(T1, bool) (T2, bool), F3 ~func(T2, bool) (T3, bool), F4 ~func(T3, bool) (T4, bool), T0, T1, T2, T3, T4 any](t0 T0, f1 F1, f2 F2, f3 F3, f4 F4) (T4, bool) {
return f4(f3(f2(f1(t0))))
}
// Flow4 creates a function that takes an initial value t0 and successively applies 4 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow4[F1 ~func(T0, bool) (T1, bool), F2 ~func(T1, bool) (T2, bool), F3 ~func(T2, bool) (T3, bool), F4 ~func(T3, bool) (T4, bool), T0, T1, T2, T3, T4 any](f1 F1, f2 F2, f3 F3, f4 F4) func(T0, bool) (T4, bool) {
return func(t0 T0, t0ok bool) (T4, bool) {
return f4(f3(f2(f1(t0, t0ok))))
}
}
// Pipe5 takes an initial value t0 and successively applies 5 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Pipe5[F1 ~func(T0) (T1, bool), F2 ~func(T1, bool) (T2, bool), F3 ~func(T2, bool) (T3, bool), F4 ~func(T3, bool) (T4, bool), F5 ~func(T4, bool) (T5, bool), T0, T1, T2, T3, T4, T5 any](t0 T0, f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) (T5, bool) {
return f5(f4(f3(f2(f1(t0)))))
}
// Flow5 creates a function that takes an initial value t0 and successively applies 5 functions where the input of a function is the return value of the previous function
// The final return value is the result of the last function application
//
//go:inline
func Flow5[F1 ~func(T0, bool) (T1, bool), F2 ~func(T1, bool) (T2, bool), F3 ~func(T2, bool) (T3, bool), F4 ~func(T3, bool) (T4, bool), F5 ~func(T4, bool) (T5, bool), T0, T1, T2, T3, T4, T5 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) func(T0, bool) (T5, bool) {
return func(t0 T0, t0ok bool) (T5, bool) {
return f5(f4(f3(f2(f1(t0, t0ok)))))
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2024 - 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 option
type (
optionFunctor[A, B any] struct{}
Functor[A, B any] interface {
Map(func(A) B) func(A, bool) (B, bool)
}
)
func (o optionFunctor[A, B]) Map(f func(A) B) Operator[A, B] {
return Map(f)
}
// Functor implements the functoric operations for Option.
// A functor is a type that can be mapped over, transforming the contained value
// while preserving the structure.
//
// Example:
//
// f := Functor[int, string]()
// mapper := f.Map(strconv.Itoa)
// result := mapper(Some(42)) // Some("42")
func MakeFunctor[A, B any]() Functor[A, B] {
return optionFunctor[A, B]{}
}

195
v2/idiomatic/option/gen.go Normal file
View File

@@ -0,0 +1,195 @@
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2025-03-09 23:53:08.2750287 +0100 CET m=+0.001545801
package option
// TraverseTuple1 converts a [Tuple1] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple1]].
func TraverseTuple1[F1 ~Kleisli[A1, T1], A1, T1 any](f1 F1) func(A1) (T1, bool) {
return func(a1 A1) (t1 T1, ok bool) {
if t1, ok := f1(a1); ok {
return t1, true
}
return
}
}
// TraverseTuple2 converts a [Tuple2] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple2]].
func TraverseTuple2[F1 ~Kleisli[A1, T1], F2 ~Kleisli[A2, T2], A1, T1, A2, T2 any](f1 F1, f2 F2) func(A1, A2) (T1, T2, bool) {
return func(a1 A1, a2 A2) (t1 T1, t2 T2, ok bool) {
if t1, ok := f1(a1); ok {
if t2, ok := f2(a2); ok {
return t1, t2, true
}
}
return
}
}
// TraverseTuple3 converts a [Tuple3] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple3]].
func TraverseTuple3[F1 ~Kleisli[A1, T1], F2 ~Kleisli[A2, T2], F3 ~Kleisli[A3, T3], A1, T1, A2, T2, A3, T3 any](f1 F1, f2 F2, f3 F3) func(A1, A2, A3) (T1, T2, T3, bool) {
return func(a1 A1, a2 A2, a3 A3) (t1 T1, t2 T2, t3 T3, ok bool) {
if t1, ok := f1(a1); ok {
if t2, ok := f2(a2); ok {
if t3, ok := f3(a3); ok {
return t1, t2, t3, true
}
}
}
return
}
}
// TraverseTuple4 converts a [Tuple4] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple4]].
func TraverseTuple4[F1 ~Kleisli[A1, T1], F2 ~Kleisli[A2, T2], F3 ~Kleisli[A3, T3], F4 ~Kleisli[A4, T4], A1, T1, A2, T2, A3, T3, A4, T4 any](f1 F1, f2 F2, f3 F3, f4 F4) func(A1, A2, A3, A4) (T1, T2, T3, T4, bool) {
return func(a1 A1, a2 A2, a3 A3, a4 A4) (t1 T1, t2 T2, t3 T3, t4 T4, ok bool) {
if t1, ok := f1(a1); ok {
if t2, ok := f2(a2); ok {
if t3, ok := f3(a3); ok {
if t4, ok := f4(a4); ok {
return t1, t2, t3, t4, true
}
}
}
}
return
}
}
// TraverseTuple5 converts a [Tuple5] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple5]].
func TraverseTuple5[F1 ~Kleisli[A1, T1], F2 ~Kleisli[A2, T2], F3 ~Kleisli[A3, T3], F4 ~Kleisli[A4, T4], F5 ~Kleisli[A5, T5], A1, T1, A2, T2, A3, T3, A4, T4, A5, T5 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5) func(A1, A2, A3, A4, A5) (T1, T2, T3, T4, T5, bool) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, ok bool) {
if t1, ok := f1(a1); ok {
if t2, ok := f2(a2); ok {
if t3, ok := f3(a3); ok {
if t4, ok := f4(a4); ok {
if t5, ok := f5(a5); ok {
return t1, t2, t3, t4, t5, true
}
}
}
}
}
return
}
}
// TraverseTuple6 converts a [Tuple6] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple6]].
func TraverseTuple6[F1 ~Kleisli[A1, T1], F2 ~Kleisli[A2, T2], F3 ~Kleisli[A3, T3], F4 ~Kleisli[A4, T4], F5 ~Kleisli[A5, T5], F6 ~Kleisli[A6, T6], A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6) func(A1, A2, A3, A4, A5, A6) (T1, T2, T3, T4, T5, T6, bool) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, ok bool) {
if t1, ok := f1(a1); ok {
if t2, ok := f2(a2); ok {
if t3, ok := f3(a3); ok {
if t4, ok := f4(a4); ok {
if t5, ok := f5(a5); ok {
if t6, ok := f6(a6); ok {
return t1, t2, t3, t4, t5, t6, true
}
}
}
}
}
}
return
}
}
// TraverseTuple7 converts a [Tuple7] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple7]].
func TraverseTuple7[F1 ~Kleisli[A1, T1], F2 ~Kleisli[A2, T2], F3 ~Kleisli[A3, T3], F4 ~Kleisli[A4, T4], F5 ~Kleisli[A5, T5], F6 ~Kleisli[A6, T6], F7 ~Kleisli[A7, T7], A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7) func(A1, A2, A3, A4, A5, A6, A7) (T1, T2, T3, T4, T5, T6, T7, bool) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, ok bool) {
if t1, ok := f1(a1); ok {
if t2, ok := f2(a2); ok {
if t3, ok := f3(a3); ok {
if t4, ok := f4(a4); ok {
if t5, ok := f5(a5); ok {
if t6, ok := f6(a6); ok {
if t7, ok := f7(a7); ok {
return t1, t2, t3, t4, t5, t6, t7, true
}
}
}
}
}
}
}
return
}
}
// TraverseTuple8 converts a [Tuple8] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple8]].
func TraverseTuple8[F1 ~Kleisli[A1, T1], F2 ~Kleisli[A2, T2], F3 ~Kleisli[A3, T3], F4 ~Kleisli[A4, T4], F5 ~Kleisli[A5, T5], F6 ~Kleisli[A6, T6], F7 ~Kleisli[A7, T7], F8 ~Kleisli[A8, T8], A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8) func(A1, A2, A3, A4, A5, A6, A7, A8) (T1, T2, T3, T4, T5, T6, T7, T8, bool) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7, a8 A8) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, t8 T8, ok bool) {
if t1, ok := f1(a1); ok {
if t2, ok := f2(a2); ok {
if t3, ok := f3(a3); ok {
if t4, ok := f4(a4); ok {
if t5, ok := f5(a5); ok {
if t6, ok := f6(a6); ok {
if t7, ok := f7(a7); ok {
if t8, ok := f8(a8); ok {
return t1, t2, t3, t4, t5, t6, t7, t8, true
}
}
}
}
}
}
}
}
return
}
}
// TraverseTuple9 converts a [Tuple9] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple9]].
func TraverseTuple9[F1 ~Kleisli[A1, T1], F2 ~Kleisli[A2, T2], F3 ~Kleisli[A3, T3], F4 ~Kleisli[A4, T4], F5 ~Kleisli[A5, T5], F6 ~Kleisli[A6, T6], F7 ~Kleisli[A7, T7], F8 ~Kleisli[A8, T8], F9 ~Kleisli[A9, T9], A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8, A9, T9 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9) func(A1, A2, A3, A4, A5, A6, A7, A8, A9) (T1, T2, T3, T4, T5, T6, T7, T8, T9, bool) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7, a8 A8, a9 A9) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, t8 T8, t9 T9, ok bool) {
if t1, ok := f1(a1); ok {
if t2, ok := f2(a2); ok {
if t3, ok := f3(a3); ok {
if t4, ok := f4(a4); ok {
if t5, ok := f5(a5); ok {
if t6, ok := f6(a6); ok {
if t7, ok := f7(a7); ok {
if t8, ok := f8(a8); ok {
if t9, ok := f9(a9); ok {
return t1, t2, t3, t4, t5, t6, t7, t8, t9, true
}
}
}
}
}
}
}
}
}
return
}
}
// TraverseTuple10 converts a [Tuple10] of [A] via transformation functions transforming [A] to [Option[A]] into a [Option[Tuple10]].
func TraverseTuple10[F1 ~Kleisli[A1, T1], F2 ~Kleisli[A2, T2], F3 ~Kleisli[A3, T3], F4 ~Kleisli[A4, T4], F5 ~Kleisli[A5, T5], F6 ~Kleisli[A6, T6], F7 ~Kleisli[A7, T7], F8 ~Kleisli[A8, T8], F9 ~Kleisli[A9, T9], F10 ~Kleisli[A10, T10], A1, T1, A2, T2, A3, T3, A4, T4, A5, T5, A6, T6, A7, T7, A8, T8, A9, T9, A10, T10 any](f1 F1, f2 F2, f3 F3, f4 F4, f5 F5, f6 F6, f7 F7, f8 F8, f9 F9, f10 F10) func(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10) (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, bool) {
return func(a1 A1, a2 A2, a3 A3, a4 A4, a5 A5, a6 A6, a7 A7, a8 A8, a9 A9, a10 A10) (t1 T1, t2 T2, t3 T3, t4 T4, t5 T5, t6 T6, t7 T7, t8 T8, t9 T9, t10 T10, ok bool) {
if t1, ok := f1(a1); ok {
if t2, ok := f2(a2); ok {
if t3, ok := f3(a3); ok {
if t4, ok := f4(a4); ok {
if t5, ok := f5(a5); ok {
if t6, ok := f6(a6); ok {
if t7, ok := f7(a7); ok {
if t8, ok := f8(a8); ok {
if t9, ok := f9(a9); ok {
if t10, ok := f10(a10); ok {
return t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, true
}
}
}
}
}
}
}
}
}
}
return
}
}

View File

@@ -0,0 +1,69 @@
// Copyright (c) 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 option
import (
I "github.com/IBM/fp-go/v2/iterator/iter"
)
// TraverseIter transforms a sequence by applying a function that returns an Option to each element.
// Returns Some containing a sequence of results if all operations succeed, None if any fails.
// This function is useful for processing sequences where each element may fail validation or transformation.
//
// The traversal short-circuits on the first None encountered, making it efficient for validation pipelines.
// The resulting sequence is lazy and will only be evaluated when iterated.
//
// Example:
//
// // Parse a sequence of strings to integers
// parse := func(s string) Option[int] {
// n, err := strconv.Atoi(s)
// if err != nil { return None[int]() }
// return Some(n)
// }
//
// // Create a sequence of strings
// strings := func(yield func(string) bool) {
// for _, s := range []string{"1", "2", "3"} {
// if !yield(s) { return }
// }
// }
//
// result := TraverseIter(parse)(strings)
// // result is Some(sequence of [1, 2, 3])
//
// // With invalid input
// invalidStrings := func(yield func(string) bool) {
// for _, s := range []string{"1", "invalid", "3"} {
// if !yield(s) { return }
// }
// }
//
// result := TraverseIter(parse)(invalidStrings)
// // result is None because "invalid" cannot be parsed
func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
return func(s Seq[A]) (Seq[B], bool) {
var bs []B
for a := range s {
b, bok := f(a)
if !bok {
return nil, false
}
bs = append(bs, b)
}
return I.From(bs...), true
}
}

View File

@@ -0,0 +1,325 @@
// Copyright (c) 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 option
import (
"fmt"
"slices"
"strconv"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/iterator/iter"
"github.com/stretchr/testify/assert"
)
// Helper function to create a sequence from a slice
func seqFromSlice[T any](items []T) Seq[T] {
return I.From(items...)
}
// Helper function to collect a sequence into a slice
func collectSeq[T any](seq Seq[T]) []T {
return slices.Collect(seq)
}
func TestTraverseIter_AllSome(t *testing.T) {
// Test case where all transformations succeed
parse := func(s string) (int, bool) {
n, err := strconv.Atoi(s)
if err != nil {
return None[int]()
}
return Some(n)
}
input := I.From("1", "2", "3", "4", "5")
result, resultok := TraverseIter(parse)(input)
assert.True(t, IsSome(result, resultok), "Expected Some result when all transformations succeed")
collected := collectSeq(result)
expected := A.From(1, 2, 3, 4, 5)
assert.Equal(t, expected, collected)
}
func TestTraverseIter_ContainsNone(t *testing.T) {
// Test case where one transformation fails
parse := func(s string) (int, bool) {
n, err := strconv.Atoi(s)
if err != nil {
return None[int]()
}
return Some(n)
}
input := seqFromSlice([]string{"1", "invalid", "3"})
result, resultok := TraverseIter(parse)(input)
assert.True(t, IsNone(result, resultok), "Expected None when any transformation fails")
}
func TestTraverseIter_EmptySequence(t *testing.T) {
// Test with empty sequence
double := func(x int) (int, bool) {
return Some(x * 2)
}
input := seqFromSlice([]int{})
result, resultok := TraverseIter(double)(input)
assert.True(t, IsSome(result, resultok), "Expected Some for empty sequence")
collected := collectSeq(result)
assert.Empty(t, collected)
}
func TestTraverseIter_SingleElement(t *testing.T) {
// Test with single element - success case
validate := func(x int) (int, bool) {
if x > 0 {
return Some(x * 2)
}
return None[int]()
}
input := seqFromSlice([]int{5})
result, resultok := TraverseIter(validate)(input)
assert.True(t, IsSome(result, resultok))
collected := collectSeq(result)
assert.Equal(t, []int{10}, collected)
}
func TestTraverseIter_SingleElementFails(t *testing.T) {
// Test with single element - failure case
validate := func(x int) (int, bool) {
if x > 0 {
return Some(x * 2)
}
return None[int]()
}
input := seqFromSlice([]int{-5})
result, resultok := TraverseIter(validate)(input)
assert.True(t, IsNone(result, resultok))
}
func TestTraverseIter_Validation(t *testing.T) {
// Test validation use case
validatePositive := func(x int) (int, bool) {
if x > 0 {
return Some(x)
}
return None[int]()
}
// All positive
input1 := seqFromSlice([]int{1, 2, 3, 4})
result1, result1ok := TraverseIter(validatePositive)(input1)
assert.True(t, IsSome(result1, result1ok))
// Contains negative
input2 := seqFromSlice([]int{1, -2, 3})
result2, result2ok := TraverseIter(validatePositive)(input2)
assert.True(t, IsNone(result2, result2ok))
// Contains zero
input3 := seqFromSlice([]int{1, 0, 3})
result3, result3ok := TraverseIter(validatePositive)(input3)
assert.True(t, IsNone(result3, result3ok))
}
func TestTraverseIter_Transformation(t *testing.T) {
// Test transformation use case
safeDivide := func(x int) (float64, bool) {
if x != 0 {
return Some(100.0 / float64(x))
}
return None[float64]()
}
// All non-zero
input1 := seqFromSlice([]int{1, 2, 4, 5})
result1, result1ok := TraverseIter(safeDivide)(input1)
assert.True(t, IsSome(result1, result1ok))
collected := collectSeq(result1)
expected := []float64{100.0, 50.0, 25.0, 20.0}
assert.Equal(t, expected, collected)
// Contains zero
input2 := seqFromSlice([]int{1, 0, 4})
result2, result2ok := TraverseIter(safeDivide)(input2)
assert.True(t, IsNone(result2, result2ok))
}
func TestTraverseIter_ShortCircuit(t *testing.T) {
// Test that traversal short-circuits on first None
callCount := 0
countingFunc := func(x int) (int, bool) {
callCount++
if x < 0 {
return None[int]()
}
return Some(x * 2)
}
// First element fails
input := seqFromSlice([]int{-1, 2, 3, 4, 5})
result, resultok := TraverseIter(countingFunc)(input)
assert.True(t, IsNone(result, resultok))
// Should have called the function for elements until the first failure
// Note: The exact count depends on implementation details of the traverse function
assert.Greater(t, callCount, 0, "Function should be called at least once")
}
func TestTraverseIter_LazyEvaluation(t *testing.T) {
// Test that the result sequence is lazy
transform := func(x int) (int, bool) {
return Some(x * 2)
}
input := seqFromSlice([]int{1, 2, 3, 4, 5})
result, resultok := TraverseIter(transform)(input)
assert.True(t, IsSome(result, resultok))
// Partially consume the sequence
callCount := 0
Fold(func() int { return 0 }, func(seq Seq[int]) int {
for val := range seq {
callCount++
_ = val
if callCount == 2 {
break
}
}
return callCount
})(result, resultok)
assert.Equal(t, 2, callCount, "Should only evaluate consumed elements")
}
func TestTraverseIter_ComplexTransformation(t *testing.T) {
// Test with more complex transformation
type Person struct {
Name string
Age int
}
validatePerson := func(name string) (Person, bool) {
if name == "" {
return None[Person]()
}
return Some(Person{Name: name, Age: len(name)})
}
input := seqFromSlice([]string{"Alice", "Bob", "Charlie"})
result, resultok := TraverseIter(validatePerson)(input)
assert.True(t, IsSome(result, resultok))
collected := collectSeq((result))
expected := []Person{
{Name: "Alice", Age: 5},
{Name: "Bob", Age: 3},
{Name: "Charlie", Age: 7},
}
assert.Equal(t, expected, collected)
}
func TestTraverseIter_WithPipeline(t *testing.T) {
// Test TraverseIter in a functional pipeline
parse := func(s string) (int, bool) {
n, err := strconv.Atoi(s)
if err != nil {
return None[int]()
}
return Some(n)
}
input := seqFromSlice([]string{"1", "2", "3", "4", "5"})
collected := Fold(func() []int { return nil }, F.Identity[[]int])(Map(collectSeq[int])(TraverseIter(parse)(input)))
expected := []int{1, 2, 3, 4, 5}
assert.Equal(t, expected, collected)
}
func TestTraverseIter_ChainedTransformations(t *testing.T) {
// Test chaining multiple transformations
parseAndValidate := func(s string) (int, bool) {
n, err := strconv.Atoi(s)
if err != nil {
return None[int]()
}
if n > 0 {
return Some(n)
}
return None[int]()
}
// All valid
input1 := seqFromSlice([]string{"1", "2", "3"})
result1, result1ok := TraverseIter(parseAndValidate)(input1)
assert.True(t, IsSome(result1, result1ok))
// Contains invalid number
input2 := seqFromSlice([]string{"1", "invalid", "3"})
result2, result2ok := TraverseIter(parseAndValidate)(input2)
assert.True(t, IsNone(result2, result2ok))
// Contains non-positive number
input3 := seqFromSlice([]string{"1", "0", "3"})
result3, result3ok := TraverseIter(parseAndValidate)(input3)
assert.True(t, IsNone(result3, result3ok))
}
// Example test demonstrating usage
func ExampleTraverseIter() {
// Parse a sequence of strings to integers
parse := func(s string) (int, bool) {
n, err := strconv.Atoi(s)
if err != nil {
return None[int]()
}
return Some(n)
}
// Create a sequence of valid strings
validStrings := seqFromSlice([]string{"1", "2", "3"})
result, resultok := TraverseIter(parse)(validStrings)
if IsSome(result, resultok) {
numbers := collectSeq(result)
fmt.Println(numbers)
}
// Create a sequence with invalid string
invalidStrings := seqFromSlice([]string{"1", "invalid", "3"})
result2, result2ok := TraverseIter(parse)(invalidStrings)
if IsNone(result2, result2ok) {
fmt.Println("Parsing failed")
}
// Output:
// [1 2 3]
// Parsing failed
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) 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 option
import (
"log"
L "github.com/IBM/fp-go/v2/logging"
)
func _log[A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[A, A] {
return func(a A, aok bool) (A, bool) {
if aok {
right("%s: %v", prefix, a)
} else {
left("%s", prefix)
}
return a, aok
}
}
// Logger creates a logging function for Options that logs the state (None or Some with value)
// and returns the original Option unchanged. This is useful for debugging pipelines.
//
// Parameters:
// - loggers: optional log.Logger instances to use for logging (defaults to standard logger)
//
// Returns a function that takes a prefix string and returns a function that logs and passes through an Option.
//
// Example:
//
// logger := Logger[int]()
// result := F.Pipe2(
// Some(42),
// logger("step1"), // logs "step1: 42"
// Map(N.Mul(2)),
// ) // Some(84)
//
// result := F.Pipe1(
// None[int](),
// logger("step1"), // logs "step1"
// ) // None
func Logger[A any](loggers ...*log.Logger) func(string) Operator[A, A] {
left, right := L.LoggingCallbacks(loggers...)
return func(prefix string) Operator[A, A] {
return _log[A](left, right, prefix)
}
}

View File

@@ -0,0 +1,435 @@
// Copyright (c) 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 option
import (
"testing"
"github.com/IBM/fp-go/v2/eq"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
// Test Alt function
func TestAlt(t *testing.T) {
t.Run("Some value - returns original", func(t *testing.T) {
withDefault := Alt(func() (int, bool) { return 100, true })
AssertEq(Some(42))(withDefault(Some(42)))(t)
})
t.Run("None value - returns alternative Some", func(t *testing.T) {
withDefault := Alt(func() (int, bool) { return 100, true })
AssertEq(Some(100))(withDefault(None[int]()))(t)
})
t.Run("None value - alternative is also None", func(t *testing.T) {
withDefault := Alt(func() (int, bool) { return None[int]() })
AssertEq(None[int]())(withDefault(None[int]()))(t)
})
}
// Test Reduce function
func TestReduce(t *testing.T) {
t.Run("Some value - applies reducer", func(t *testing.T) {
sum := Reduce(func(acc, val int) int { return acc + val }, 10)
result := sum(Some(5))
assert.Equal(t, 15, result)
})
t.Run("None value - returns initial", func(t *testing.T) {
sum := Reduce(func(acc, val int) int { return acc + val }, 10)
result := sum(None[int]())
assert.Equal(t, 10, result)
})
t.Run("string concatenation", func(t *testing.T) {
concat := Reduce(func(acc, val string) string { return acc + val }, "prefix:")
result := concat(Some("test"))
assert.Equal(t, "prefix:test", result)
})
}
// Test FromZero function
func TestFromZero(t *testing.T) {
t.Run("zero value - returns Some", func(t *testing.T) {
AssertEq(Some(0))(FromZero[int]()(0))(t)
})
t.Run("non-zero value - returns None", func(t *testing.T) {
AssertEq(None[int]())(FromZero[int]()(5))(t)
})
t.Run("empty string - returns Some", func(t *testing.T) {
AssertEq(Some(""))(FromZero[string]()(""))(t)
})
t.Run("non-empty string - returns None", func(t *testing.T) {
AssertEq(None[string]())(FromZero[string]()("hello"))(t)
})
}
// Test FromNonZero function
func TestFromNonZero(t *testing.T) {
t.Run("non-zero value - returns Some", func(t *testing.T) {
AssertEq(Some(5))(FromNonZero[int]()(5))(t)
})
t.Run("zero value - returns None", func(t *testing.T) {
AssertEq(None[int]())(FromNonZero[int]()(0))(t)
})
t.Run("non-empty string - returns Some", func(t *testing.T) {
AssertEq(Some("hello"))(FromNonZero[string]()("hello"))(t)
})
t.Run("empty string - returns None", func(t *testing.T) {
AssertEq(None[string]())(FromNonZero[string]()(""))(t)
})
}
// Test FromEq function
func TestFromEq(t *testing.T) {
t.Run("matching value - returns Some", func(t *testing.T) {
equals42 := FromEq(eq.FromStrictEquals[int]())(42)
AssertEq(Some(42))(equals42(42))(t)
})
t.Run("non-matching value - returns None", func(t *testing.T) {
equals42 := FromEq(eq.FromStrictEquals[int]())(42)
AssertEq(None[int]())(equals42(10))(t)
})
t.Run("string equality", func(t *testing.T) {
equalsHello := FromEq(eq.FromStrictEquals[string]())("hello")
assert.True(t, IsSome(equalsHello("hello")))
assert.True(t, IsNone(equalsHello("world")))
})
}
// Test Pipe and Flow functions
func TestPipe1(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
AssertEq(Some(10))(Pipe1(5, double))(t)
}
func TestFlow1(t *testing.T) {
double := func(x int, ok bool) (int, bool) { return x * 2, ok }
flow := Flow1(double)
AssertEq(Some(10))(flow(Some(5)))(t)
}
func TestFlow2(t *testing.T) {
double := func(x int, ok bool) (int, bool) { return x * 2, ok }
add10 := func(x int, ok bool) (int, bool) {
if ok {
return x + 10, true
}
return 0, false
}
flow := Flow2(double, add10)
AssertEq(Some(20))(flow(Some(5)))(t)
}
func TestPipe3(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
add10 := func(x int, ok bool) (int, bool) {
if ok {
return x + 10, true
}
return 0, false
}
mul3 := func(x int, ok bool) (int, bool) {
if ok {
return x * 3, true
}
return 0, false
}
AssertEq(Some(60))(Pipe3(5, double, add10, mul3))(t) // (5 * 2 + 10) * 3 = 60
}
func TestPipe4(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
add10 := func(x int, ok bool) (int, bool) {
if ok {
return x + 10, true
}
return 0, false
}
mul3 := func(x int, ok bool) (int, bool) {
if ok {
return x * 3, true
}
return 0, false
}
sub5 := func(x int, ok bool) (int, bool) {
if ok {
return x - 5, true
}
return 0, false
}
AssertEq(Some(55))(Pipe4(5, double, add10, mul3, sub5))(t) // ((5 * 2 + 10) * 3) - 5 = 55
}
func TestFlow4(t *testing.T) {
f1 := func(x int, ok bool) (int, bool) { return x + 1, ok }
f2 := func(x int, ok bool) (int, bool) { return x * 2, ok }
f3 := func(x int, ok bool) (int, bool) { return x - 5, ok }
f4 := func(x int, ok bool) (int, bool) { return x * 10, ok }
flow := Flow4(f1, f2, f3, f4)
AssertEq(Some(70))(flow(Some(5)))(t) // ((5 + 1) * 2 - 5) * 10 = 70
}
func TestFlow5(t *testing.T) {
f1 := func(x int, ok bool) (int, bool) { return x + 1, ok }
f2 := func(x int, ok bool) (int, bool) { return x * 2, ok }
f3 := func(x int, ok bool) (int, bool) { return x - 5, ok }
f4 := func(x int, ok bool) (int, bool) { return x * 10, ok }
f5 := func(x int, ok bool) (int, bool) { return x + 100, ok }
flow := Flow5(f1, f2, f3, f4, f5)
AssertEq(Some(170))(flow(Some(5)))(t) // (((5 + 1) * 2 - 5) * 10) + 100 = 170
}
// Test Functor and Pointed
func TestMakeFunctor(t *testing.T) {
t.Run("Map with functor", func(t *testing.T) {
f := MakeFunctor[int, int]()
double := f.Map(func(x int) int { return x * 2 })
AssertEq(Some(42))(double(Some(21)))(t)
})
t.Run("Map with None", func(t *testing.T) {
f := MakeFunctor[int, int]()
double := f.Map(func(x int) int { return x * 2 })
AssertEq(None[int]())(double(None[int]()))(t)
})
}
func TestMakePointed(t *testing.T) {
t.Run("Of with value", func(t *testing.T) {
p := MakePointed[int]()
AssertEq(Some(42))(p.Of(42))(t)
})
t.Run("Of with string", func(t *testing.T) {
p := MakePointed[string]()
AssertEq(Some("hello"))(p.Of("hello"))(t)
})
}
// Test lens-based operations
type TestStruct struct {
Value int
Name string
}
func TestApSL(t *testing.T) {
valueLens := L.MakeLens(
func(s TestStruct) int { return s.Value },
func(s TestStruct, v int) TestStruct { s.Value = v; return s },
)
t.Run("Some struct, Some value", func(t *testing.T) {
applyValue := ApSL(valueLens)
v, ok := applyValue(Some(42))(Some(TestStruct{Value: 0, Name: "test"}))
assert.True(t, ok)
assert.Equal(t, 42, v.Value)
assert.Equal(t, "test", v.Name)
})
t.Run("Some struct, None value", func(t *testing.T) {
applyValue := ApSL(valueLens)
AssertEq(None[TestStruct]())(applyValue(None[int]())(Some(TestStruct{Value: 10, Name: "test"})))(t)
})
t.Run("None struct, Some value", func(t *testing.T) {
applyValue := ApSL(valueLens)
AssertEq(None[TestStruct]())(applyValue(Some(42))(None[TestStruct]()))(t)
})
}
func TestBindL(t *testing.T) {
valueLens := L.MakeLens(
func(s TestStruct) int { return s.Value },
func(s TestStruct, v int) TestStruct { s.Value = v; return s },
)
t.Run("increment value with validation", func(t *testing.T) {
increment := func(v int) (int, bool) {
if v < 100 {
return v + 1, true
}
return 0, false
}
bindIncrement := BindL(valueLens, increment)
v, ok := bindIncrement(Some(TestStruct{Value: 42, Name: "test"}))
assert.True(t, ok)
assert.Equal(t, 43, v.Value)
assert.Equal(t, "test", v.Name)
})
t.Run("validation fails", func(t *testing.T) {
increment := func(v int) (int, bool) {
if v < 100 {
return v + 1, true
}
return 0, false
}
bindIncrement := BindL(valueLens, increment)
AssertEq(None[TestStruct]())(bindIncrement(Some(TestStruct{Value: 100, Name: "test"})))(t)
})
t.Run("None input", func(t *testing.T) {
increment := func(v int) (int, bool) { return v + 1, true }
bindIncrement := BindL(valueLens, increment)
AssertEq(None[TestStruct]())(bindIncrement(None[TestStruct]()))(t)
})
}
func TestLetL(t *testing.T) {
valueLens := L.MakeLens(
func(s TestStruct) int { return s.Value },
func(s TestStruct, v int) TestStruct { s.Value = v; return s },
)
t.Run("double value", func(t *testing.T) {
double := func(v int) int { return v * 2 }
letDouble := LetL(valueLens, double)
v, ok := letDouble(Some(TestStruct{Value: 21, Name: "test"}))
assert.True(t, ok)
assert.Equal(t, 42, v.Value)
assert.Equal(t, "test", v.Name)
})
t.Run("None input", func(t *testing.T) {
double := func(v int) int { return v * 2 }
letDouble := LetL(valueLens, double)
AssertEq(None[TestStruct]())(letDouble(None[TestStruct]()))(t)
})
}
func TestLetToL(t *testing.T) {
valueLens := L.MakeLens(
func(s TestStruct) int { return s.Value },
func(s TestStruct, v int) TestStruct { s.Value = v; return s },
)
t.Run("set constant value", func(t *testing.T) {
setValue := LetToL(valueLens, 100)
v, ok := setValue(Some(TestStruct{Value: 42, Name: "test"}))
assert.True(t, ok)
assert.Equal(t, 100, v.Value)
assert.Equal(t, "test", v.Name)
})
t.Run("None input", func(t *testing.T) {
setValue := LetToL(valueLens, 100)
AssertEq(None[TestStruct]())(setValue(None[TestStruct]()))(t)
})
}
// Test tuple traversals
func TestTraverseTuple5(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, ok := TraverseTuple5(double, double, double, double, double)(1, 2, 3, 4, 5)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
}
func TestTraverseTuple6(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, ok := TraverseTuple6(double, double, double, double, double, double)(1, 2, 3, 4, 5, 6)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
}
func TestTraverseTuple7(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, v7, ok := TraverseTuple7(double, double, double, double, double, double, double)(1, 2, 3, 4, 5, 6, 7)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
assert.Equal(t, 14, v7)
}
func TestTraverseTuple8(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, v7, v8, ok := TraverseTuple8(double, double, double, double, double, double, double, double)(1, 2, 3, 4, 5, 6, 7, 8)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
assert.Equal(t, 14, v7)
assert.Equal(t, 16, v8)
}
func TestTraverseTuple9(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, v7, v8, v9, ok := TraverseTuple9(double, double, double, double, double, double, double, double, double)(1, 2, 3, 4, 5, 6, 7, 8, 9)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
assert.Equal(t, 14, v7)
assert.Equal(t, 16, v8)
assert.Equal(t, 18, v9)
}
func TestTraverseTuple10(t *testing.T) {
double := func(x int) (int, bool) { return x * 2, true }
v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, ok := TraverseTuple10(double, double, double, double, double, double, double, double, double, double)(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
assert.True(t, ok)
assert.Equal(t, 2, v1)
assert.Equal(t, 4, v2)
assert.Equal(t, 6, v3)
assert.Equal(t, 8, v4)
assert.Equal(t, 10, v5)
assert.Equal(t, 12, v6)
assert.Equal(t, 14, v7)
assert.Equal(t, 16, v8)
assert.Equal(t, 18, v9)
assert.Equal(t, 20, v10)
}
// Test tuple traversals with failure cases
func TestTraverseTuple5_Failure(t *testing.T) {
validate := func(x int) (int, bool) {
if x > 0 {
return x, true
}
return 0, false
}
_, _, _, _, _, ok := TraverseTuple5(validate, validate, validate, validate, validate)(1, -2, 3, 4, 5)
assert.False(t, ok)
}

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2024 - 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 option
import (
"github.com/IBM/fp-go/v2/internal/monad"
)
type (
optionMonad[A, B any] struct{}
)
func (o *optionMonad[A, B]) Of(a A) Option[A] {
return Of(a)
}
func (o *optionMonad[A, B]) Map(f func(A) B) Operator[A, B] {
return Map(f)
}
func (o *optionMonad[A, B]) Chain(f Kleisli[A, B]) Operator[A, B] {
return Chain(f)
}
func (o *optionMonad[A, B]) Ap(fa Option[A]) func(Option[func(A) B]) Option[B] {
return Ap[B](fa)
}
// Monad implements the monadic operations for Option.
// A monad provides a way to chain computations that may fail, handling the
// None case automatically.
//
// The monad interface includes:
// - Of: wraps a value in an Option
// - Map: transforms the contained value
// - Chain: sequences Option-returning operations
// - Ap: applies an Option-wrapped function to an Option-wrapped value
//
// Example:
//
// m := Monad[int, string]()
// result := m.Chain(func(x int) Option[string] {
// if x > 0 { return Some(fmt.Sprintf("%d", x)) }
// return None[string]()
// })(Some(42)) // Some("42")
func Monad[A, B any]() monad.Monad[A, B, Option[A], Option[B], Option[func(A) B]] {
return &optionMonad[A, B]{}
}

View File

@@ -0,0 +1,111 @@
// Copyright (c) 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 option
import (
F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup"
)
// Semigroup returns a function that lifts a Semigroup over type A to a Semigroup over Option[A].
// The resulting semigroup combines two Options according to these rules:
// - If both are Some, concatenates their values using the provided Semigroup
// - If one is None, returns the other
// - If both are None, returns None
//
// Example:
//
// intSemigroup := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
// optSemigroup := Semigroup[int]()(intSemigroup)
// optSemigroup.Concat(Some(2), Some(3)) // Some(5)
// optSemigroup.Concat(Some(2), None[int]()) // Some(2)
// optSemigroup.Concat(None[int](), Some(3)) // Some(3)
func Semigroup[A any]() func(S.Semigroup[A]) S.Semigroup[Option[A]] {
return func(s S.Semigroup[A]) S.Semigroup[Option[A]] {
concat := s.Concat
return S.MakeSemigroup(
func(x, y Option[A]) Option[A] {
return MonadFold(x, F.Constant(y), func(left A) Option[A] {
return MonadFold(y, F.Constant(x), func(right A) Option[A] {
return Some(concat(left, right))
})
})
},
)
}
}
// Monoid returns a function that lifts a Semigroup over type A to a Monoid over Option[A].
// The monoid returns the left-most non-None value. If both operands are Some, their inner
// values are concatenated using the provided Semigroup. The empty value is None.
//
// Truth table:
//
// | x | y | concat(x, y) |
// | ------- | ------- | ------------------ |
// | none | none | none |
// | some(a) | none | some(a) |
// | none | some(b) | some(b) |
// | some(a) | some(b) | some(concat(a, b)) |
//
// Example:
//
// intSemigroup := semigroup.MakeSemigroup(func(a, b int) int { return a + b })
// optMonoid := Monoid[int]()(intSemigroup)
// optMonoid.Concat(Some(2), Some(3)) // Some(5)
// optMonoid.Empty() // None
func Monoid[A any]() func(S.Semigroup[A]) M.Monoid[Option[A]] {
sg := Semigroup[A]()
return func(s S.Semigroup[A]) M.Monoid[Option[A]] {
return M.MakeMonoid(sg(s).Concat, None[A]())
}
}
// AlternativeMonoid creates a Monoid for Option[A] using the alternative semantics.
// This combines the applicative functor structure with the alternative (Alt) operation.
//
// Example:
//
// intMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
// optMonoid := AlternativeMonoid(intMonoid)
// result := optMonoid.Concat(Some(2), Some(3)) // Some(5)
func AlternativeMonoid[A any](m M.Monoid[A]) M.Monoid[Option[A]] {
return M.AlternativeMonoid(
Of[A],
MonadMap[A, func(A) A],
MonadAp[A, A],
MonadAlt[A],
m,
)
}
// AltMonoid creates a Monoid for Option[A] using the Alt operation.
// This monoid returns the first Some value, or None if both are None.
// The empty value is None.
//
// Example:
//
// optMonoid := AltMonoid[int]()
// optMonoid.Concat(Some(2), Some(3)) // Some(2) - returns first Some
// optMonoid.Concat(None[int](), Some(3)) // Some(3)
// optMonoid.Empty() // None
func AltMonoid[A any]() M.Monoid[Option[A]] {
return M.AltMonoid(
None[A],
MonadAlt[A],
)
}

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