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

Compare commits

...

13 Commits

Author SHA1 Message Date
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
Dr. Carsten Leue
13063bbd88 fix: doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 16:36:12 +01:00
118 changed files with 23666 additions and 3220 deletions

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(func(x int) int { return x * 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).
@@ -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 := func(x int) int { return x * 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 := func(x int) int { return x * 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
}
```
@@ -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

@@ -87,6 +87,6 @@ func Example_sort() {
// [abc klm zyx]
// [zyx klm abc]
// [None[int] Some[int](42) Some[int](1337)]
// [{c {false 0}} {b {true 10}} {d {true 10}} {a {true 30}}]
// [{c {0 false}} {b {10 true}} {d {10 true}} {a {30 true}}]
}

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 i := 0; i < b.N; i++ {
_ = 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 i := 0; i < b.N; i++ {
_ = ToString(large)
}
})
}
func BenchmarkSize(b *testing.B) {
data := []byte("Hello, World!")
for i := 0; i < b.N; i++ {
_ = Size(data)
}
}
func BenchmarkMonoidConcat(b *testing.B) {
a := []byte("Hello")
c := []byte(" World")
b.Run("small slices", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Monoid.Concat(a, c)
}
})
b.Run("large slices", func(b *testing.B) {
large1 := make([]byte, 10000)
large2 := make([]byte, 10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = 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 i := 0; i < b.N; i++ {
_ = 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 i := 0; i < b.N; i++ {
_ = ConcatAll(many...)
}
})
}
func BenchmarkOrdCompare(b *testing.B) {
a := []byte("abc")
c := []byte("abd")
b.Run("equal", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = Ord.Compare(a, a)
}
})
b.Run("different", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = 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 i := 0; i < b.N; i++ {
_ = 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")
}

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 (
@@ -747,3 +751,93 @@ 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 ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderK(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 ChainFirstReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderResultK(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 ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderIOK(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 Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.Read[A](r)
}

View File

@@ -883,5 +883,3 @@ func BenchmarkExecuteApPar_CanceledContext(b *testing.B) {
benchResult = rioe(ctx)()
}
}
// Made with Bob

File diff suppressed because it is too large Load Diff

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)

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

@@ -648,5 +648,3 @@ func BenchmarkString_Left(b *testing.B) {
benchString = left.String()
}
}
// Made with Bob

View File

@@ -76,8 +76,8 @@ func TestAp(t *testing.T) {
f := S.Size
assert.Equal(t, Right[string](3), F.Pipe1(Right[string](f), Ap[int](Right[string]("abc"))))
assert.Equal(t, Left[int]("maError"), F.Pipe1(Right[string](f), Ap[int](Left[string, string]("maError"))))
assert.Equal(t, Left[int]("mabError"), F.Pipe1(Left[func(string) int]("mabError"), Ap[int](Left[string, string]("maError"))))
assert.Equal(t, Left[int]("maError"), F.Pipe1(Right[string](f), Ap[int](Left[string]("maError"))))
assert.Equal(t, Left[int]("mabError"), F.Pipe1(Left[func(string) int]("mabError"), Ap[int](Left[string]("maError"))))
}
func TestAlt(t *testing.T) {

View File

@@ -39,13 +39,18 @@
// double := func(x int) int { return x * 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 },
// func(x int) int { return x * 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
// // Chain allows sequencing of endomorphisms (LEFT-TO-RIGHT)
// f := func(x int) int { return x * 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 := func(x int) int { return x * 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,290 @@ 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
//
// 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
// - A new endomorphism that applies fa, then fab
//
// Example:
//
// double := func(x int) int { return x * 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:
//
// increment := func(x int) int { return x + 1 }
// applyIncrement := endomorphism.Ap(increment)
// double := func(x int) int { return x * 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 := func(x int) int { return x * 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 := func(x int) int { return x * 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 := func(x int) int { return x * 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 := func(x int) int { return x * 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 := func(x int) int { return x * 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 := func(x int) int { return x * 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 := func(x int) int { return x * 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 }
//
// // 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)
}

View File

@@ -76,84 +76,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 := func(x int) int { return x * 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 +259,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 +277,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 +294,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 +310,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 +369,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,64 +390,63 @@ 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++ {
_ = composed(5)
@@ -379,6 +454,47 @@ func BenchmarkCompose(b *testing.B) {
}
// BenchmarkMonoidConcatAll benchmarks ConcatAll with monoid
// TestComposeVsChain demonstrates the key difference between Compose and Chain
func TestComposeVsChain(t *testing.T) {
double := func(x int) int { return x * 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})
@@ -397,3 +513,211 @@ func BenchmarkChain(b *testing.B) {
_ = 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 := func(x int) int { return x * 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")
}

View File

@@ -88,11 +88,15 @@ 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:
//
@@ -102,11 +106,11 @@ func Identity[A any]() Endomorphism[A] {
// double := func(x int) int { return x * 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,7 +132,7 @@ 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:
//
@@ -135,9 +143,9 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// 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

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

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

@@ -0,0 +1,87 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fromoption
import (
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
)
func FromPredicate[A, HKTEA any](fromOption func(Option[A]) HKTEA, pred func(A) bool) func(A) HKTEA {
return F.Flow2(O.FromPredicate(pred), fromOption)
}
// func MonadFromOption[E, A, HKTEA any](
// fromEither func(ET.Either[E, A]) HKTEA,
// onNone func() E,
// ma O.Option[A],
// ) HKTEA {
// return F.Pipe1(
// O.MonadFold(
// ma,
// F.Nullary2(onNone, ET.Left[A, E]),
// ET.Right[E, A],
// ),
// fromEither,
// )
// }
// func FromOptionK[A, E, B, HKTEB any](
// fromEither func(ET.Either[E, B]) HKTEB,
// onNone func() E) func(f func(A) O.Option[B]) func(A) HKTEB {
// // helper
// return F.Bind2nd(F.Flow2[func(A) O.Option[B], func(O.Option[B]) HKTEB, A, O.Option[B], HKTEB], FromOption(fromEither, onNone))
// }
func MonadChainOptionK[A, B, HKTEA, HKTEB any](
mchain func(HKTEA, func(A) HKTEB) HKTEB,
fromOption func(Option[B]) HKTEB,
ma HKTEA,
f func(A) Option[B]) HKTEB {
return mchain(ma, F.Flow2(f, fromOption))
}
func ChainOptionK[A, B, HKTEA, HKTEB any](
mchain func(func(A) HKTEB) func(HKTEA) HKTEB,
fromOption func(Option[B]) HKTEB,
f func(A) Option[B]) func(HKTEA) HKTEB {
return mchain(F.Flow2(f, fromOption))
}
// func ChainOptionK[A, E, B, HKTEA, HKTEB any](
// mchain func(HKTEA, func(A) HKTEB) HKTEB,
// fromEither func(ET.Either[E, B]) HKTEB,
// onNone func() E,
// ) func(f func(A) O.Option[B]) func(ma HKTEA) HKTEB {
// return F.Flow2(FromOptionK[A](fromEither, onNone), F.Bind1st(F.Bind2nd[HKTEA, func(A) HKTEB, HKTEB], mchain))
// }
// func MonadChainFirstEitherK[A, E, B, HKTEA, HKTEB any](
// mchain func(HKTEA, func(A) HKTEA) HKTEA,
// mmap func(HKTEB, func(B) A) HKTEA,
// fromEither func(ET.Either[E, B]) HKTEB,
// ma HKTEA,
// f func(A) ET.Either[E, B]) HKTEA {
// return C.MonadChainFirst(mchain, mmap, ma, F.Flow2(f, fromEither))
// }
// func ChainFirstEitherK[A, E, B, HKTEA, HKTEB any](
// mchain func(func(A) HKTEA) func(HKTEA) HKTEA,
// mmap func(func(B) A) func(HKTEB) HKTEA,
// fromEither func(ET.Either[E, B]) HKTEB,
// f func(A) ET.Either[E, B]) func(HKTEA) HKTEA {
// return C.ChainFirst(mchain, mmap, F.Flow2(f, fromEither))
// }

View File

@@ -0,0 +1,28 @@
// 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 fromoption
import (
"github.com/IBM/fp-go/v2/option"
)
type (
Option[T any] = option.Option[T]
FromOption[A, HKTA any] interface {
FromEither(Option[A]) HKTA
}
)

View File

@@ -46,9 +46,26 @@ func MonadChainReaderK[GB ~func(R) B, R, A, B, HKTRA, HKTRB any](
}
func ChainReaderK[GB ~func(R) B, R, A, B, HKTRA, HKTRB any](
mchain func(HKTRA, func(A) HKTRB) HKTRB,
mchain func(func(A) HKTRB) func(HKTRA) HKTRB,
fromReader func(GB) HKTRB,
f func(A) GB,
) func(HKTRA) HKTRB {
return F.Bind2nd(mchain, FromReaderK(fromReader, f))
return mchain(FromReaderK(fromReader, f))
}
func MonadChainFirstReaderK[GB ~func(R) B, R, A, B, HKTRA, HKTRB any](
mchain func(HKTRA, func(A) HKTRB) HKTRA,
fromReader func(GB) HKTRB,
ma HKTRA,
f func(A) GB,
) HKTRA {
return mchain(ma, FromReaderK(fromReader, f))
}
func ChainFirstReaderK[GB ~func(R) B, R, A, B, HKTRA, HKTRB any](
mchain func(func(A) HKTRB) func(HKTRA) HKTRA,
fromReader func(GB) HKTRB,
f func(A) GB,
) func(HKTRA) HKTRA {
return mchain(FromReaderK(fromReader, f))
}

View File

@@ -78,8 +78,27 @@ func Ap[A, B, HKTFAB, HKTFGAB, HKTFA, HKTFB any](
return apply.Ap(fap, fmap, O.Ap[B, A], fa)
}
func MatchE[A, HKTEA, HKTB any](mchain func(HKTEA, func(O.Option[A]) HKTB) HKTB, onNone func() HKTB, onSome func(A) HKTB) func(HKTEA) HKTB {
return F.Bind2nd(mchain, O.Fold(onNone, onSome))
func MonadMatchE[A, HKTEA, HKTB any](
fa HKTEA,
mchain func(HKTEA, func(O.Option[A]) HKTB) HKTB,
onNone func() HKTB,
onSome func(A) HKTB) HKTB {
return mchain(fa, O.Fold(onNone, onSome))
}
func MatchE[A, HKTEA, HKTB any](
mchain func(func(O.Option[A]) HKTB) func(HKTEA) HKTB,
onNone func() HKTB,
onSome func(A) HKTB) func(HKTEA) HKTB {
return mchain(O.Fold(onNone, onSome))
}
//go:inline
func GetOrElse[A, HKTEA, HKTB any](
mchain func(func(O.Option[A]) HKTB) func(HKTEA) HKTB,
onNone func() HKTB,
onSome func(A) HKTB) func(HKTEA) HKTB {
return MatchE(mchain, onNone, onSome)
}
func FromOptionK[A, B, HKTB any](
@@ -123,3 +142,7 @@ func Alt[LAZY ~func() HKTFA, A, HKTFA any](
return fchain(O.Fold(second, F.Flow2(O.Of[A], fof)))
}
func SomeF[A, HKTA, HKTEA any](fmap func(HKTA, func(A) O.Option[A]) HKTEA, fa HKTA) HKTEA {
return fmap(fa, O.Some[A])
}

View File

@@ -233,7 +233,7 @@ func After[GA ~func() O.Option[A], A any](timestamp time.Time) func(GA) GA {
// Fold convers an IOOption into an IO
func Fold[GA ~func() O.Option[A], GB ~func() B, A, B any](onNone func() GB, onSome func(A) GB) func(GA) GB {
return optiont.MatchE(IO.MonadChain[GA, GB, O.Option[A], B], onNone, onSome)
return optiont.MatchE(IO.Chain[GA, GB, O.Option[A], B], onNone, onSome)
}
// Defer creates an IO by creating a brand new IO via a generator function, each time

View File

@@ -24,6 +24,7 @@ import (
"github.com/IBM/fp-go/v2/internal/fromio"
"github.com/IBM/fp-go/v2/internal/optiont"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
)
@@ -47,7 +48,7 @@ func FromOption[A any](o Option[A]) IOOption[A] {
return io.Of(o)
}
func ChainOptionK[A, B any](f func(A) Option[B]) func(IOOption[A]) IOOption[B] {
func ChainOptionK[A, B any](f func(A) Option[B]) Operator[A, B] {
return optiont.ChainOptionK(
io.Chain[Option[A], Option[B]],
FromOption[B],
@@ -55,7 +56,7 @@ func ChainOptionK[A, B any](f func(A) Option[B]) func(IOOption[A]) IOOption[B] {
)
}
func MonadChainIOK[A, B any](ma IOOption[A], f func(A) IO[B]) IOOption[B] {
func MonadChainIOK[A, B any](ma IOOption[A], f io.Kleisli[A, B]) IOOption[B] {
return fromio.MonadChainIOK(
MonadChain[A, B],
FromIO[B],
@@ -64,7 +65,7 @@ func MonadChainIOK[A, B any](ma IOOption[A], f func(A) IO[B]) IOOption[B] {
)
}
func ChainIOK[A, B any](f func(A) IO[B]) func(IOOption[A]) IOOption[B] {
func ChainIOK[A, B any](f io.Kleisli[A, B]) Operator[A, B] {
return fromio.ChainIOK(
Chain[A, B],
FromIO[B],
@@ -80,15 +81,15 @@ func MonadMap[A, B any](fa IOOption[A], f func(A) B) IOOption[B] {
return optiont.MonadMap(io.MonadMap[Option[A], Option[B]], fa, f)
}
func Map[A, B any](f func(A) B) func(IOOption[A]) IOOption[B] {
func Map[A, B any](f func(A) B) Operator[A, B] {
return optiont.Map(io.Map[Option[A], Option[B]], f)
}
func MonadChain[A, B any](fa IOOption[A], f func(A) IOOption[B]) IOOption[B] {
func MonadChain[A, B any](fa IOOption[A], f Kleisli[A, B]) IOOption[B] {
return optiont.MonadChain(io.MonadChain[Option[A], Option[B]], io.MonadOf[Option[B]], fa, f)
}
func Chain[A, B any](f func(A) IOOption[B]) func(IOOption[A]) IOOption[B] {
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return optiont.Chain(io.Chain[Option[A], Option[B]], io.Of[Option[B]], f)
}
@@ -99,21 +100,21 @@ func MonadAp[B, A any](mab IOOption[func(A) B], ma IOOption[A]) IOOption[B] {
mab, ma)
}
func Ap[B, A any](ma IOOption[A]) func(IOOption[func(A) B]) IOOption[B] {
func Ap[B, A any](ma IOOption[A]) Operator[func(A) B, B] {
return optiont.Ap(
io.Ap[Option[B], Option[A]],
io.Map[Option[func(A) B], func(Option[A]) Option[B]],
ma)
}
func ApSeq[B, A any](ma IOOption[A]) func(IOOption[func(A) B]) IOOption[B] {
func ApSeq[B, A any](ma IOOption[A]) Operator[func(A) B, B] {
return optiont.Ap(
io.ApSeq[Option[B], Option[A]],
io.Map[Option[func(A) B], func(Option[A]) Option[B]],
ma)
}
func ApPar[B, A any](ma IOOption[A]) func(IOOption[func(A) B]) IOOption[B] {
func ApPar[B, A any](ma IOOption[A]) Operator[func(A) B, B] {
return optiont.Ap(
io.ApPar[Option[B], Option[A]],
io.Map[Option[func(A) B], func(Option[A]) Option[B]],
@@ -124,14 +125,14 @@ func Flatten[A any](mma IOOption[IOOption[A]]) IOOption[A] {
return MonadChain(mma, function.Identity[IOOption[A]])
}
func Optionize0[A any](f func() (A, bool)) func() IOOption[A] {
func Optionize0[A any](f func() (A, bool)) Lazy[IOOption[A]] {
ef := option.Optionize0(f)
return func() IOOption[A] {
return ef
}
}
func Optionize1[T1, A any](f func(t1 T1) (A, bool)) func(T1) IOOption[A] {
func Optionize1[T1, A any](f func(t1 T1) (A, bool)) Kleisli[T1, A] {
ef := option.Optionize1(f)
return func(t1 T1) IOOption[A] {
return func() Option[A] {
@@ -172,8 +173,8 @@ func Memoize[A any](ma IOOption[A]) IOOption[A] {
}
// Fold convers an [IOOption] into an [IO]
func Fold[A, B any](onNone func() IO[B], onSome func(A) IO[B]) func(IOOption[A]) IO[B] {
return optiont.MatchE(io.MonadChain[Option[A], B], onNone, onSome)
func Fold[A, B any](onNone IO[B], onSome io.Kleisli[A, B]) func(IOOption[A]) IO[B] {
return optiont.MatchE(io.Chain[Option[A], B], function.Constant(onNone), onSome)
}
// Defer creates an IO by creating a brand new IO via a generator function, each time
@@ -191,28 +192,28 @@ func FromEither[E, A any](e Either[E, A]) IOOption[A] {
}
// MonadAlt identifies an associative operation on a type constructor
func MonadAlt[A any](first IOOption[A], second Lazy[IOOption[A]]) IOOption[A] {
func MonadAlt[A any](first IOOption[A], second IOOption[A]) IOOption[A] {
return optiont.MonadAlt(
io.MonadOf[Option[A]],
io.MonadChain[Option[A], Option[A]],
first,
second,
lazy.Of(second),
)
}
// Alt identifies an associative operation on a type constructor
func Alt[A any](second Lazy[IOOption[A]]) func(IOOption[A]) IOOption[A] {
func Alt[A any](second IOOption[A]) Operator[A, A] {
return optiont.Alt(
io.Of[Option[A]],
io.Chain[Option[A], Option[A]],
second,
lazy.Of(second),
)
}
// MonadChainFirst runs the monad returned by the function but returns the result of the original monad
func MonadChainFirst[A, B any](ma IOOption[A], f func(A) IOOption[B]) IOOption[A] {
func MonadChainFirst[A, B any](ma IOOption[A], f Kleisli[A, B]) IOOption[A] {
return chain.MonadChainFirst(
MonadChain[A, A],
MonadMap[B, A],
@@ -222,7 +223,7 @@ func MonadChainFirst[A, B any](ma IOOption[A], f func(A) IOOption[B]) IOOption[A
}
// ChainFirst runs the monad returned by the function but returns the result of the original monad
func ChainFirst[A, B any](f func(A) IOOption[B]) func(IOOption[A]) IOOption[A] {
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return chain.ChainFirst(
Chain[A, A],
Map[B, A],
@@ -231,7 +232,7 @@ func ChainFirst[A, B any](f func(A) IOOption[B]) func(IOOption[A]) IOOption[A] {
}
// MonadChainFirstIOK runs the monad returned by the function but returns the result of the original monad
func MonadChainFirstIOK[A, B any](first IOOption[A], f func(A) IO[B]) IOOption[A] {
func MonadChainFirstIOK[A, B any](first IOOption[A], f io.Kleisli[A, B]) IOOption[A] {
return fromio.MonadChainFirstIOK(
MonadChain[A, A],
MonadMap[B, A],
@@ -242,7 +243,7 @@ func MonadChainFirstIOK[A, B any](first IOOption[A], f func(A) IO[B]) IOOption[A
}
// ChainFirstIOK runs the monad returned by the function but returns the result of the original monad
func ChainFirstIOK[A, B any](f func(A) IO[B]) func(IOOption[A]) IOOption[A] {
func ChainFirstIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
return fromio.ChainFirstIOK(
Chain[A, A],
Map[B, A],
@@ -252,11 +253,11 @@ func ChainFirstIOK[A, B any](f func(A) IO[B]) func(IOOption[A]) IOOption[A] {
}
// Delay creates an operation that passes in the value after some delay
func Delay[A any](delay time.Duration) func(IOOption[A]) IOOption[A] {
func Delay[A any](delay time.Duration) Operator[A, A] {
return io.Delay[Option[A]](delay)
}
// After creates an operation that passes after the given [time.Time]
func After[A any](timestamp time.Time) func(IOOption[A]) IOOption[A] {
func After[A any](timestamp time.Time) Operator[A, A] {
return io.After[Option[A]](timestamp)
}

View File

@@ -19,7 +19,7 @@ import "github.com/IBM/fp-go/v2/function"
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource
func WithResource[
R, A, ANY any](onCreate IOOption[R], onRelease func(R) IOOption[ANY]) Kleisli[Kleisli[R, A], A] {
R, A, ANY any](onCreate IOOption[R], onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] {
// simply map to implementation of bracket
return function.Bind13of3(Bracket[R, A, ANY])(onCreate, function.Ignore2of2[Option[A]](onRelease))
}

View File

@@ -21,10 +21,74 @@ import (
S "github.com/IBM/fp-go/v2/semigroup"
)
// ApplySemigroup lifts a Semigroup[A] to a Semigroup[Lazy[A]].
// This allows you to combine lazy computations using the semigroup operation
// on their underlying values.
//
// The resulting semigroup's Concat operation will evaluate both lazy computations
// and combine their results using the original semigroup's operation.
//
// Parameters:
// - s: A semigroup for values of type A
//
// Returns:
// - A semigroup for lazy computations of type A
//
// Example:
//
// import (
// M "github.com/IBM/fp-go/v2/monoid"
// "github.com/IBM/fp-go/v2/lazy"
// )
//
// // Create a semigroup for lazy integers using addition
// intAddSemigroup := lazy.ApplySemigroup(M.MonoidSum[int]())
//
// lazy1 := lazy.Of(5)
// lazy2 := lazy.Of(10)
//
// // Combine the lazy computations
// result := intAddSemigroup.Concat(lazy1, lazy2)() // 15
func ApplySemigroup[A any](s S.Semigroup[A]) S.Semigroup[Lazy[A]] {
return IO.ApplySemigroup(s)
}
// ApplicativeMonoid lifts a Monoid[A] to a Monoid[Lazy[A]].
// This allows you to combine lazy computations using the monoid operation
// on their underlying values, with an identity element.
//
// The resulting monoid's Concat operation will evaluate both lazy computations
// and combine their results using the original monoid's operation. The Empty
// operation returns a lazy computation that produces the monoid's identity element.
//
// Parameters:
// - m: A monoid for values of type A
//
// Returns:
// - A monoid for lazy computations of type A
//
// Example:
//
// import (
// M "github.com/IBM/fp-go/v2/monoid"
// "github.com/IBM/fp-go/v2/lazy"
// )
//
// // Create a monoid for lazy integers using addition
// intAddMonoid := lazy.ApplicativeMonoid(M.MonoidSum[int]())
//
// // Get the identity element (0 wrapped in lazy)
// empty := intAddMonoid.Empty()() // 0
//
// lazy1 := lazy.Of(5)
// lazy2 := lazy.Of(10)
//
// // Combine the lazy computations
// result := intAddMonoid.Concat(lazy1, lazy2)() // 15
//
// // Identity laws hold:
// // Concat(Empty(), x) == x
// // Concat(x, Empty()) == x
func ApplicativeMonoid[A any](m M.Monoid[A]) M.Monoid[Lazy[A]] {
return IO.ApplicativeMonoid(m)
}

267
v2/lazy/doc.go Normal file
View File

@@ -0,0 +1,267 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package lazy provides a functional programming abstraction for synchronous computations
// without side effects. It represents deferred computations that are evaluated only when
// their result is needed.
//
// # Overview
//
// A Lazy[A] is simply a function that takes no arguments and returns a value of type A:
//
// type Lazy[A any] = func() A
//
// This allows you to defer the evaluation of a computation until it's actually needed,
// which is useful for:
// - Avoiding unnecessary computations
// - Creating infinite data structures
// - Implementing memoization
// - Composing computations in a pure functional style
//
// # Core Concepts
//
// The lazy package implements several functional programming patterns:
//
// **Functor**: Transform values inside a Lazy context using Map
//
// **Applicative**: Combine multiple Lazy computations using Ap and ApS
//
// **Monad**: Chain dependent computations using Chain and Bind
//
// **Memoization**: Cache computation results using Memoize
//
// # Basic Usage
//
// Creating and evaluating lazy computations:
//
// import (
// "fmt"
// "github.com/IBM/fp-go/v2/lazy"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Create a lazy computation
// computation := lazy.Of(42)
//
// // Transform it
// doubled := F.Pipe1(
// computation,
// lazy.Map(func(x int) int { return x * 2 }),
// )
//
// // Evaluate when needed
// result := doubled() // 84
//
// # Memoization
//
// Lazy computations can be memoized to ensure they're evaluated only once:
//
// import "math/rand"
//
// // Without memoization - generates different values each time
// random := lazy.FromLazy(rand.Int)
// value1 := random() // e.g., 12345
// value2 := random() // e.g., 67890 (different)
//
// // With memoization - caches the first result
// memoized := lazy.Memoize(rand.Int)
// value1 := memoized() // e.g., 12345
// value2 := memoized() // 12345 (same as value1)
//
// # Chaining Computations
//
// Use Chain to compose dependent computations:
//
// getUserId := lazy.Of(123)
//
// getUser := F.Pipe1(
// getUserId,
// lazy.Chain(func(id int) lazy.Lazy[User] {
// return lazy.Of(fetchUser(id))
// }),
// )
//
// user := getUser()
//
// # Do-Notation Style
//
// The package supports do-notation style composition using Bind and ApS:
//
// type Config struct {
// Host string
// Port int
// }
//
// result := F.Pipe2(
// lazy.Do(Config{}),
// lazy.Bind(
// func(host string) func(Config) Config {
// return func(c Config) Config { c.Host = host; return c }
// },
// func(c Config) lazy.Lazy[string] {
// return lazy.Of("localhost")
// },
// ),
// lazy.Bind(
// func(port int) func(Config) Config {
// return func(c Config) Config { c.Port = port; return c }
// },
// func(c Config) lazy.Lazy[int] {
// return lazy.Of(8080)
// },
// ),
// )
//
// config := result() // Config{Host: "localhost", Port: 8080}
//
// # Traverse and Sequence
//
// Transform collections of values into lazy computations:
//
// // Transform array elements
// numbers := []int{1, 2, 3}
// doubled := F.Pipe1(
// numbers,
// lazy.TraverseArray(func(x int) lazy.Lazy[int] {
// return lazy.Of(x * 2)
// }),
// )
// result := doubled() // []int{2, 4, 6}
//
// // Sequence array of lazy computations
// computations := []lazy.Lazy[int]{
// lazy.Of(1),
// lazy.Of(2),
// lazy.Of(3),
// }
// result := lazy.SequenceArray(computations)() // []int{1, 2, 3}
//
// # Retry Logic
//
// The package includes retry functionality for computations that may fail:
//
// import (
// R "github.com/IBM/fp-go/v2/retry"
// "time"
// )
//
// policy := R.CapDelay(
// 2*time.Second,
// R.Monoid.Concat(
// R.ExponentialBackoff(10),
// R.LimitRetries(5),
// ),
// )
//
// action := func(status R.RetryStatus) lazy.Lazy[string] {
// return lazy.Of(fetchData())
// }
//
// check := func(value string) bool {
// return value == "" // retry if empty
// }
//
// result := lazy.Retrying(policy, action, check)()
//
// # Algebraic Structures
//
// The package provides algebraic structures for combining lazy computations:
//
// **Semigroup**: Combine two lazy values using a semigroup operation
//
// import M "github.com/IBM/fp-go/v2/monoid"
//
// intAddSemigroup := lazy.ApplySemigroup(M.MonoidSum[int]())
// result := intAddSemigroup.Concat(lazy.Of(5), lazy.Of(10))() // 15
//
// **Monoid**: Combine lazy values with an identity element
//
// intAddMonoid := lazy.ApplicativeMonoid(M.MonoidSum[int]())
// empty := intAddMonoid.Empty()() // 0
// result := intAddMonoid.Concat(lazy.Of(5), lazy.Of(10))() // 15
//
// # Comparison
//
// Compare lazy computations by evaluating and comparing their results:
//
// import EQ "github.com/IBM/fp-go/v2/eq"
//
// eq := lazy.Eq(EQ.FromEquals[int]())
// result := eq.Equals(lazy.Of(42), lazy.Of(42)) // true
//
// # Key Functions
//
// **Creation**:
// - Of: Create a lazy computation from a value
// - FromLazy: Create a lazy computation from another lazy computation
// - FromImpure: Convert a side effect into a lazy computation
// - Defer: Create a lazy computation from a generator function
//
// **Transformation**:
// - Map: Transform the value inside a lazy computation
// - MapTo: Replace the value with a constant
// - Chain: Chain dependent computations
// - ChainFirst: Chain computations but keep the first result
// - Flatten: Flatten nested lazy computations
//
// **Combination**:
// - Ap: Apply a lazy function to a lazy value
// - ApFirst: Combine two computations, keeping the first result
// - ApSecond: Combine two computations, keeping the second result
//
// **Memoization**:
// - Memoize: Cache the result of a computation
//
// **Do-Notation**:
// - Do: Start a do-notation context
// - Bind: Bind a computation result to a context
// - Let: Attach a pure value to a context
// - LetTo: Attach a constant to a context
// - BindTo: Initialize a context from a value
// - ApS: Attach a value using applicative style
//
// **Lens-Based Operations**:
// - BindL: Bind using a lens
// - LetL: Let using a lens
// - LetToL: LetTo using a lens
// - ApSL: ApS using a lens
//
// **Collections**:
// - TraverseArray: Transform array elements into lazy computations
// - SequenceArray: Convert array of lazy computations to lazy array
// - TraverseRecord: Transform record values into lazy computations
// - SequenceRecord: Convert record of lazy computations to lazy record
//
// **Tuples**:
// - SequenceT1, SequenceT2, SequenceT3, SequenceT4: Combine lazy computations into tuples
//
// **Retry**:
// - Retrying: Retry a computation according to a policy
//
// **Algebraic**:
// - ApplySemigroup: Create a semigroup for lazy values
// - ApplicativeMonoid: Create a monoid for lazy values
// - Eq: Create an equality predicate for lazy values
//
// # Relationship to IO
//
// The lazy package is built on top of the io package and shares the same underlying
// implementation. The key difference is conceptual:
// - lazy.Lazy[A] represents a pure, synchronous computation without side effects
// - io.IO[A] represents a computation that may have side effects
//
// In practice, they are the same type, but the lazy package provides a more focused
// API for pure computations.
package lazy

View File

@@ -21,10 +21,28 @@ import (
"github.com/IBM/fp-go/v2/io"
)
// Of creates a lazy computation that returns the given value.
// This is the most basic way to lift a value into the Lazy context.
//
// The computation is pure and will always return the same value when evaluated.
//
// Example:
//
// computation := lazy.Of(42)
// result := computation() // 42
func Of[A any](a A) Lazy[A] {
return io.Of(a)
}
// FromLazy creates a lazy computation from another lazy computation.
// This is an identity function that can be useful for type conversions or
// making the intent explicit in code.
//
// Example:
//
// original := func() int { return 42 }
// wrapped := lazy.FromLazy(original)
// result := wrapped() // 42
func FromLazy[A any](a Lazy[A]) Lazy[A] {
return io.FromIO(a)
}
@@ -34,22 +52,73 @@ func FromImpure(f func()) Lazy[any] {
return io.FromImpure(f)
}
// MonadOf creates a lazy computation that returns the given value.
// This is an alias for Of, provided for consistency with monadic naming conventions.
//
// Example:
//
// computation := lazy.MonadOf(42)
// result := computation() // 42
func MonadOf[A any](a A) Lazy[A] {
return io.MonadOf(a)
}
// MonadMap transforms the value inside a lazy computation using the provided function.
// The transformation is not applied until the lazy computation is evaluated.
//
// This is the monadic version of Map, taking the lazy computation as the first parameter.
//
// Example:
//
// computation := lazy.Of(5)
// doubled := lazy.MonadMap(computation, func(x int) int { return x * 2 })
// result := doubled() // 10
func MonadMap[A, B any](fa Lazy[A], f func(A) B) Lazy[B] {
return io.MonadMap(fa, f)
}
// Map transforms the value inside a lazy computation using the provided function.
// Returns a function that can be applied to a lazy computation.
//
// This is the curried version of MonadMap, useful for function composition.
//
// Example:
//
// double := lazy.Map(func(x int) int { return x * 2 })
// computation := lazy.Of(5)
// result := double(computation)() // 10
//
// // Or with pipe:
// result := F.Pipe1(lazy.Of(5), double)() // 10
func Map[A, B any](f func(A) B) func(fa Lazy[A]) Lazy[B] {
return io.Map(f)
}
// MonadMapTo replaces the value inside a lazy computation with a constant value.
// The original computation is still evaluated, but its result is discarded.
//
// This is useful when you want to sequence computations but only care about
// the side effects (though Lazy should represent pure computations).
//
// Example:
//
// computation := lazy.Of("ignored")
// replaced := lazy.MonadMapTo(computation, 42)
// result := replaced() // 42
func MonadMapTo[A, B any](fa Lazy[A], b B) Lazy[B] {
return io.MonadMapTo(fa, b)
}
// MapTo replaces the value inside a lazy computation with a constant value.
// Returns a function that can be applied to a lazy computation.
//
// This is the curried version of MonadMapTo.
//
// Example:
//
// replaceWith42 := lazy.MapTo[string](42)
// computation := lazy.Of("ignored")
// result := replaceWith42(computation)() // 42
func MapTo[A, B any](b B) Kleisli[Lazy[A], B] {
return io.MapTo[A](b)
}
@@ -64,10 +133,32 @@ func Chain[A, B any](f Kleisli[A, B]) Kleisli[Lazy[A], B] {
return io.Chain(f)
}
// MonadAp applies a lazy function to a lazy value.
// Both the function and the value are evaluated when the result is evaluated.
//
// This is the applicative functor operation, allowing you to apply functions
// that are themselves wrapped in a lazy context.
//
// Example:
//
// lazyFunc := lazy.Of(func(x int) int { return x * 2 })
// lazyValue := lazy.Of(5)
// result := lazy.MonadAp(lazyFunc, lazyValue)() // 10
func MonadAp[B, A any](mab Lazy[func(A) B], ma Lazy[A]) Lazy[B] {
return io.MonadApSeq(mab, ma)
}
// Ap applies a lazy function to a lazy value.
// Returns a function that takes a lazy function and returns a lazy result.
//
// This is the curried version of MonadAp, useful for function composition.
//
// Example:
//
// lazyValue := lazy.Of(5)
// applyTo5 := lazy.Ap[int](lazyValue)
// lazyFunc := lazy.Of(func(x int) int { return x * 2 })
// result := applyTo5(lazyFunc)() // 10
func Ap[B, A any](ma Lazy[A]) func(Lazy[func(A) B]) Lazy[B] {
return io.ApSeq[B](ma)
}
@@ -123,7 +214,15 @@ func ChainTo[A, B any](fb Lazy[B]) Kleisli[Lazy[A], B] {
return io.ChainTo[A](fb)
}
// Now returns the current timestamp
// Now is a lazy computation that returns the current timestamp when evaluated.
// Each evaluation will return the current time at the moment of evaluation.
//
// Example:
//
// time1 := lazy.Now()
// // ... some time passes ...
// time2 := lazy.Now()
// // time1 and time2 will be different
var Now Lazy[time.Time] = io.Now
// Defer creates an IO by creating a brand new IO via a generator function, each time

View File

@@ -0,0 +1,503 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lazy
import (
"testing"
"time"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
M "github.com/IBM/fp-go/v2/monoid"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
func TestOf(t *testing.T) {
result := Of(42)
assert.Equal(t, 42, result())
}
func TestFromLazy(t *testing.T) {
original := func() int { return 42 }
wrapped := FromLazy(original)
assert.Equal(t, 42, wrapped())
}
func TestFromImpure(t *testing.T) {
counter := 0
impure := func() {
counter++
}
lazy := FromImpure(impure)
lazy()
assert.Equal(t, 1, counter)
}
func TestMonadOf(t *testing.T) {
result := MonadOf(42)
assert.Equal(t, 42, result())
}
func TestMonadMap(t *testing.T) {
result := MonadMap(Of(5), func(x int) int { return x * 2 })
assert.Equal(t, 10, result())
}
func TestMonadMapTo(t *testing.T) {
result := MonadMapTo(Of("ignored"), 42)
assert.Equal(t, 42, result())
}
func TestMapTo(t *testing.T) {
mapper := MapTo[string](42)
result := mapper(Of("ignored"))
assert.Equal(t, 42, result())
}
func TestMonadChain(t *testing.T) {
result := MonadChain(Of(5), func(x int) Lazy[int] {
return Of(x * 2)
})
assert.Equal(t, 10, result())
}
func TestMonadChainFirst(t *testing.T) {
result := MonadChainFirst(Of(5), func(x int) Lazy[string] {
return Of("ignored")
})
assert.Equal(t, 5, result())
}
func TestChainFirst(t *testing.T) {
chainer := ChainFirst(func(x int) Lazy[string] {
return Of("ignored")
})
result := chainer(Of(5))
assert.Equal(t, 5, result())
}
func TestMonadChainTo(t *testing.T) {
result := MonadChainTo(Of(5), Of(10))
assert.Equal(t, 10, result())
}
func TestChainTo(t *testing.T) {
chainer := ChainTo[int](Of(10))
result := chainer(Of(5))
assert.Equal(t, 10, result())
}
func TestMonadAp(t *testing.T) {
lazyFunc := Of(func(x int) int { return x * 2 })
lazyValue := Of(5)
result := MonadAp(lazyFunc, lazyValue)
assert.Equal(t, 10, result())
}
func TestMonadApFirst(t *testing.T) {
result := MonadApFirst(Of(5), Of(10))
assert.Equal(t, 5, result())
}
func TestMonadApSecond(t *testing.T) {
result := MonadApSecond(Of(5), Of(10))
assert.Equal(t, 10, result())
}
func TestNow(t *testing.T) {
before := time.Now()
result := Now()
after := time.Now()
assert.True(t, result.After(before) || result.Equal(before))
assert.True(t, result.Before(after) || result.Equal(after))
}
func TestDefer(t *testing.T) {
counter := 0
deferred := Defer(func() Lazy[int] {
counter++
return Of(counter)
})
// First execution
result1 := deferred()
assert.Equal(t, 1, result1)
// Second execution should generate a new computation
result2 := deferred()
assert.Equal(t, 2, result2)
}
func TestDo(t *testing.T) {
type State struct {
Value int
}
result := Do(State{Value: 42})
assert.Equal(t, State{Value: 42}, result())
}
func TestLet(t *testing.T) {
type State struct {
Value int
}
result := F.Pipe2(
Do(State{}),
Let(
func(v int) func(State) State {
return func(s State) State { s.Value = v; return s }
},
func(s State) int { return 42 },
),
Map(func(s State) int { return s.Value }),
)
assert.Equal(t, 42, result())
}
func TestLetTo(t *testing.T) {
type State struct {
Value int
}
result := F.Pipe2(
Do(State{}),
LetTo(
func(v int) func(State) State {
return func(s State) State { s.Value = v; return s }
},
42,
),
Map(func(s State) int { return s.Value }),
)
assert.Equal(t, 42, result())
}
func TestBindTo(t *testing.T) {
type State struct {
Value int
}
result := F.Pipe2(
Of(42),
BindTo(func(v int) State { return State{Value: v} }),
Map(func(s State) int { return s.Value }),
)
assert.Equal(t, 42, result())
}
func TestBindL(t *testing.T) {
type Config struct {
Port int
}
type State struct {
Config Config
}
// Create a lens manually
configLens := L.MakeLens(
func(s State) Config { return s.Config },
func(s State, cfg Config) State { s.Config = cfg; return s },
)
result := F.Pipe2(
Do(State{Config: Config{Port: 8080}}),
BindL(configLens, func(cfg Config) Lazy[Config] {
return Of(Config{Port: cfg.Port + 1})
}),
Map(func(s State) int { return s.Config.Port }),
)
assert.Equal(t, 8081, result())
}
func TestLetL(t *testing.T) {
type Config struct {
Port int
}
type State struct {
Config Config
}
// Create a lens manually
configLens := L.MakeLens(
func(s State) Config { return s.Config },
func(s State, cfg Config) State { s.Config = cfg; return s },
)
result := F.Pipe2(
Do(State{Config: Config{Port: 8080}}),
LetL(configLens, func(cfg Config) Config {
return Config{Port: cfg.Port + 1}
}),
Map(func(s State) int { return s.Config.Port }),
)
assert.Equal(t, 8081, result())
}
func TestLetToL(t *testing.T) {
type Config struct {
Port int
}
type State struct {
Config Config
}
// Create a lens manually
configLens := L.MakeLens(
func(s State) Config { return s.Config },
func(s State, cfg Config) State { s.Config = cfg; return s },
)
result := F.Pipe2(
Do(State{}),
LetToL(configLens, Config{Port: 8080}),
Map(func(s State) int { return s.Config.Port }),
)
assert.Equal(t, 8080, result())
}
func TestApSL(t *testing.T) {
type Config struct {
Port int
}
type State struct {
Config Config
}
// Create a lens manually
configLens := L.MakeLens(
func(s State) Config { return s.Config },
func(s State, cfg Config) State { s.Config = cfg; return s },
)
result := F.Pipe2(
Do(State{}),
ApSL(configLens, Of(Config{Port: 8080})),
Map(func(s State) int { return s.Config.Port }),
)
assert.Equal(t, 8080, result())
}
func TestSequenceT1(t *testing.T) {
result := SequenceT1(Of(42))
tuple := result()
assert.Equal(t, 42, tuple.F1)
}
func TestSequenceT2(t *testing.T) {
result := SequenceT2(Of(42), Of("hello"))
tuple := result()
assert.Equal(t, 42, tuple.F1)
assert.Equal(t, "hello", tuple.F2)
}
func TestSequenceT3(t *testing.T) {
result := SequenceT3(Of(42), Of("hello"), Of(true))
tuple := result()
assert.Equal(t, 42, tuple.F1)
assert.Equal(t, "hello", tuple.F2)
assert.Equal(t, true, tuple.F3)
}
func TestSequenceT4(t *testing.T) {
result := SequenceT4(Of(42), Of("hello"), Of(true), Of(3.14))
tuple := result()
assert.Equal(t, 42, tuple.F1)
assert.Equal(t, "hello", tuple.F2)
assert.Equal(t, true, tuple.F3)
assert.Equal(t, 3.14, tuple.F4)
}
func TestTraverseArray(t *testing.T) {
numbers := []int{1, 2, 3}
result := F.Pipe1(
numbers,
TraverseArray(func(x int) Lazy[int] {
return Of(x * 2)
}),
)
assert.Equal(t, []int{2, 4, 6}, result())
}
func TestTraverseArrayWithIndex(t *testing.T) {
numbers := []int{10, 20, 30}
result := F.Pipe1(
numbers,
TraverseArrayWithIndex(func(i int, x int) Lazy[int] {
return Of(x + i)
}),
)
assert.Equal(t, []int{10, 21, 32}, result())
}
func TestSequenceArray(t *testing.T) {
lazies := []Lazy[int]{Of(1), Of(2), Of(3)}
result := SequenceArray(lazies)
assert.Equal(t, []int{1, 2, 3}, result())
}
func TestMonadTraverseArray(t *testing.T) {
numbers := []int{1, 2, 3}
result := MonadTraverseArray(numbers, func(x int) Lazy[int] {
return Of(x * 2)
})
assert.Equal(t, []int{2, 4, 6}, result())
}
func TestTraverseRecord(t *testing.T) {
record := map[string]int{"a": 1, "b": 2}
result := F.Pipe1(
record,
TraverseRecord[string](func(x int) Lazy[int] {
return Of(x * 2)
}),
)
resultMap := result()
assert.Equal(t, 2, resultMap["a"])
assert.Equal(t, 4, resultMap["b"])
}
func TestTraverseRecordWithIndex(t *testing.T) {
record := map[string]int{"a": 10, "b": 20}
result := F.Pipe1(
record,
TraverseRecordWithIndex(func(k string, x int) Lazy[int] {
if k == "a" {
return Of(x + 1)
}
return Of(x + 2)
}),
)
resultMap := result()
assert.Equal(t, 11, resultMap["a"])
assert.Equal(t, 22, resultMap["b"])
}
func TestSequenceRecord(t *testing.T) {
record := map[string]Lazy[int]{
"a": Of(1),
"b": Of(2),
}
result := SequenceRecord(record)
resultMap := result()
assert.Equal(t, 1, resultMap["a"])
assert.Equal(t, 2, resultMap["b"])
}
func TestMonadTraverseRecord(t *testing.T) {
record := map[string]int{"a": 1, "b": 2}
result := MonadTraverseRecord(record, func(x int) Lazy[int] {
return Of(x * 2)
})
resultMap := result()
assert.Equal(t, 2, resultMap["a"])
assert.Equal(t, 4, resultMap["b"])
}
func TestApplySemigroup(t *testing.T) {
sg := ApplySemigroup(M.MakeMonoid(
func(a, b int) int { return a + b },
0,
))
result := sg.Concat(Of(5), Of(10))
assert.Equal(t, 15, result())
}
func TestApplicativeMonoid(t *testing.T) {
mon := ApplicativeMonoid(M.MakeMonoid(
func(a, b int) int { return a + b },
0,
))
// Test Empty
empty := mon.Empty()
assert.Equal(t, 0, empty())
// Test Concat
result := mon.Concat(Of(5), Of(10))
assert.Equal(t, 15, result())
// Test identity laws
left := mon.Concat(mon.Empty(), Of(5))
assert.Equal(t, 5, left())
right := mon.Concat(Of(5), mon.Empty())
assert.Equal(t, 5, right())
}
func TestEq(t *testing.T) {
eq := Eq(EQ.FromEquals(func(a, b int) bool { return a == b }))
assert.True(t, eq.Equals(Of(42), Of(42)))
assert.False(t, eq.Equals(Of(42), Of(43)))
}
func TestComplexDoNotation(t *testing.T) {
// Test a more complex do-notation scenario
result := F.Pipe3(
Do(utils.Empty),
Bind(utils.SetLastName, func(s utils.Initial) Lazy[string] {
return Of("Doe")
}),
Bind(utils.SetGivenName, func(s utils.WithLastName) Lazy[string] {
return Of("John")
}),
Map(utils.GetFullName),
)
assert.Equal(t, "John Doe", result())
}
func TestChainComposition(t *testing.T) {
// Test chaining multiple operations
double := func(x int) Lazy[int] {
return Of(x * 2)
}
addTen := func(x int) Lazy[int] {
return Of(x + 10)
}
result := F.Pipe2(
Of(5),
Chain(double),
Chain(addTen),
)
assert.Equal(t, 20, result())
}
func TestMapComposition(t *testing.T) {
// Test mapping multiple transformations
result := F.Pipe3(
Of(5),
Map(func(x int) int { return x * 2 }),
Map(func(x int) int { return x + 10 }),
Map(func(x int) int { return x }),
)
assert.Equal(t, 20, result())
}

View File

@@ -22,18 +22,56 @@ import (
// SequenceT converts n inputs of higher kinded types into a higher kinded types of n strongly typed values, represented as a tuple
// SequenceT1 combines a single lazy computation into a lazy tuple.
// This is mainly useful for consistency with the other SequenceT functions.
//
// Example:
//
// lazy1 := lazy.Of(42)
// result := lazy.SequenceT1(lazy1)()
// // result is tuple.Tuple1[int]{F1: 42}
func SequenceT1[A any](a Lazy[A]) Lazy[tuple.Tuple1[A]] {
return io.SequenceT1(a)
}
// SequenceT2 combines two lazy computations into a lazy tuple of two elements.
// Both computations are evaluated when the result is evaluated.
//
// Example:
//
// lazy1 := lazy.Of(42)
// lazy2 := lazy.Of("hello")
// result := lazy.SequenceT2(lazy1, lazy2)()
// // result is tuple.Tuple2[int, string]{F1: 42, F2: "hello"}
func SequenceT2[A, B any](a Lazy[A], b Lazy[B]) Lazy[tuple.Tuple2[A, B]] {
return io.SequenceT2(a, b)
}
// SequenceT3 combines three lazy computations into a lazy tuple of three elements.
// All computations are evaluated when the result is evaluated.
//
// Example:
//
// lazy1 := lazy.Of(42)
// lazy2 := lazy.Of("hello")
// lazy3 := lazy.Of(true)
// result := lazy.SequenceT3(lazy1, lazy2, lazy3)()
// // result is tuple.Tuple3[int, string, bool]{F1: 42, F2: "hello", F3: true}
func SequenceT3[A, B, C any](a Lazy[A], b Lazy[B], c Lazy[C]) Lazy[tuple.Tuple3[A, B, C]] {
return io.SequenceT3(a, b, c)
}
// SequenceT4 combines four lazy computations into a lazy tuple of four elements.
// All computations are evaluated when the result is evaluated.
//
// Example:
//
// lazy1 := lazy.Of(42)
// lazy2 := lazy.Of("hello")
// lazy3 := lazy.Of(true)
// lazy4 := lazy.Of(3.14)
// result := lazy.SequenceT4(lazy1, lazy2, lazy3, lazy4)()
// // result is tuple.Tuple4[int, string, bool, float64]{F1: 42, F2: "hello", F3: true, F4: 3.14}
func SequenceT4[A, B, C, D any](a Lazy[A], b Lazy[B], c Lazy[C], d Lazy[D]) Lazy[tuple.Tuple4[A, B, C, D]] {
return io.SequenceT4(a, b, c, d)
}

View File

@@ -17,6 +17,18 @@ package lazy
import "github.com/IBM/fp-go/v2/io"
// MonadTraverseArray applies a function returning a lazy computation to all elements
// in an array and transforms this into a lazy computation of that array.
//
// This is the monadic version of TraverseArray, taking the array as the first parameter.
//
// Example:
//
// numbers := []int{1, 2, 3}
// result := lazy.MonadTraverseArray(numbers, func(x int) lazy.Lazy[int] {
// return lazy.Of(x * 2)
// })()
// // result is []int{2, 4, 6}
func MonadTraverseArray[A, B any](tas []A, f Kleisli[A, B]) Lazy[[]B] {
return io.MonadTraverseArray(tas, f)
}
@@ -38,6 +50,18 @@ func SequenceArray[A any](tas []Lazy[A]) Lazy[[]A] {
return io.SequenceArray(tas)
}
// MonadTraverseRecord applies a function returning a lazy computation to all values
// in a record (map) and transforms this into a lazy computation of that record.
//
// This is the monadic version of TraverseRecord, taking the record as the first parameter.
//
// Example:
//
// record := map[string]int{"a": 1, "b": 2}
// result := lazy.MonadTraverseRecord(record, func(x int) lazy.Lazy[int] {
// return lazy.Of(x * 2)
// })()
// // result is map[string]int{"a": 2, "b": 4}
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) Lazy[map[K]B] {
return io.MonadTraverseRecord(tas, f)
}

View File

@@ -1,9 +1,60 @@
package lazy
type (
// Lazy represents a synchronous computation without side effects
// Lazy represents a synchronous computation without side effects.
// It is a function that takes no arguments and returns a value of type A.
//
// Lazy computations are evaluated only when their result is needed (lazy evaluation).
// This allows for:
// - Deferring expensive computations until they're actually required
// - Creating infinite data structures
// - Implementing memoization patterns
// - Composing pure computations in a functional style
//
// Example:
//
// // Create a lazy computation
// computation := lazy.Of(42)
//
// // Transform it (not evaluated yet)
// doubled := lazy.Map(func(x int) int { return x * 2 })(computation)
//
// // Evaluate when needed
// result := doubled() // 84
//
// Note: Lazy is an alias for io.IO[A] but represents pure computations
// without side effects, whereas IO represents computations that may have side effects.
Lazy[A any] = func() A
Kleisli[A, B any] = func(A) Lazy[B]
// Kleisli represents a function that takes a value of type A and returns
// a lazy computation producing a value of type B.
//
// Kleisli arrows are used for composing monadic computations. They allow
// you to chain operations where each step depends on the result of the previous step.
//
// Example:
//
// // A Kleisli arrow that doubles a number lazily
// double := func(x int) lazy.Lazy[int] {
// return lazy.Of(x * 2)
// }
//
// // Chain it with another operation
// result := lazy.Chain(double)(lazy.Of(5))() // 10
Kleisli[A, B any] = func(A) Lazy[B]
// Operator represents a function that takes a lazy computation of type A
// and returns a lazy computation of type B.
//
// Operators are used to transform lazy computations. They are essentially
// Kleisli arrows where the input is already wrapped in a Lazy context.
//
// Example:
//
// // An operator that doubles the value in a lazy computation
// doubleOp := lazy.Map(func(x int) int { return x * 2 })
//
// // Apply it to a lazy computation
// result := doubleOp(lazy.Of(5))() // 10
Operator[A, B any] = Kleisli[Lazy[A], B]
)

View File

@@ -1,4 +1,234 @@
# Optics
Refer to [Introduction to optics: lenses and prisms](https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe) for an introduction about functional optics.
Functional optics for composable data access and manipulation in Go.
## Overview
Optics are first-class, composable references to parts of data structures. They provide a uniform interface for reading, writing, and transforming nested immutable data without verbose boilerplate code.
## Quick Start
```go
import (
"github.com/IBM/fp-go/v2/optics/lens"
F "github.com/IBM/fp-go/v2/function"
)
type Person struct {
Name string
Age int
}
// Create a lens for the Name field
nameLens := lens.MakeLens(
func(p Person) string { return p.Name },
func(p Person, name string) Person {
p.Name = name
return p
},
)
person := Person{Name: "Alice", Age: 30}
// Get the name
name := nameLens.Get(person) // "Alice"
// Set a new name (returns a new Person)
updated := nameLens.Set("Bob")(person)
// person.Name is still "Alice", updated.Name is "Bob"
```
## Core Optics Types
### Lens - Product Types (Structs)
Focus on a single field within a struct. Provides get and set operations.
**Use when:** Working with struct fields that always exist.
```go
ageLens := lens.MakeLens(
func(p Person) int { return p.Age },
func(p Person, age int) Person {
p.Age = age
return p
},
)
```
### Prism - Sum Types (Variants)
Focus on one variant of a sum type. Provides optional get and definite set.
**Use when:** Working with Either, Result, or custom sum types.
```go
import "github.com/IBM/fp-go/v2/optics/prism"
successPrism := prism.MakePrism(
func(r Result) option.Option[int] {
if s, ok := r.(Success); ok {
return option.Some(s.Value)
}
return option.None[int]()
},
func(v int) Result { return Success{Value: v} },
)
```
### Iso - Isomorphisms
Bidirectional transformation between equivalent types with no information loss.
**Use when:** Converting between equivalent representations (e.g., Celsius ↔ Fahrenheit).
```go
import "github.com/IBM/fp-go/v2/optics/iso"
celsiusToFahrenheit := iso.MakeIso(
func(c float64) float64 { return c*9/5 + 32 },
func(f float64) float64 { return (f - 32) * 5 / 9 },
)
```
### Optional - Maybe Values
Focus on a value that may or may not exist.
**Use when:** Working with nullable fields or values that may be absent.
```go
import "github.com/IBM/fp-go/v2/optics/optional"
timeoutOptional := optional.MakeOptional(
func(c Config) option.Option[*int] {
return option.FromNillable(c.Timeout)
},
func(c Config, t *int) Config {
c.Timeout = t
return c
},
)
```
### Traversal - Multiple Values
Focus on multiple values simultaneously, allowing batch operations.
**Use when:** Working with collections or updating multiple fields at once.
```go
import (
"github.com/IBM/fp-go/v2/optics/traversal"
TA "github.com/IBM/fp-go/v2/optics/traversal/array"
)
numbers := []int{1, 2, 3, 4, 5}
// Double all elements
doubled := F.Pipe2(
numbers,
TA.Traversal[int](),
traversal.Modify[[]int, int](func(n int) int { return n * 2 }),
)
// Result: [2, 4, 6, 8, 10]
```
## Composition
The real power of optics comes from composition:
```go
type Company struct {
Name string
Address Address
}
type Address struct {
Street string
City string
}
// Individual lenses
addressLens := lens.MakeLens(
func(c Company) Address { return c.Address },
func(c Company, a Address) Company {
c.Address = a
return c
},
)
cityLens := lens.MakeLens(
func(a Address) string { return a.City },
func(a Address, city string) Address {
a.City = city
return a
},
)
// Compose to access city directly from company
companyCityLens := F.Pipe1(
addressLens,
lens.Compose[Company](cityLens),
)
company := Company{
Name: "Acme Corp",
Address: Address{Street: "Main St", City: "NYC"},
}
city := companyCityLens.Get(company) // "NYC"
updated := companyCityLens.Set("Boston")(company)
```
## Optics Hierarchy
```
Iso[S, A]
Lens[S, A]
Optional[S, A]
Traversal[S, A]
Prism[S, A]
Optional[S, A]
Traversal[S, A]
```
More specific optics can be converted to more general ones.
## Package Structure
- **optics/lens**: Lenses for product types (structs)
- **optics/prism**: Prisms for sum types (Either, Result, etc.)
- **optics/iso**: Isomorphisms for equivalent types
- **optics/optional**: Optional optics for maybe values
- **optics/traversal**: Traversals for multiple values
Each package includes specialized sub-packages for common patterns:
- **array**: Optics for arrays/slices
- **either**: Optics for Either types
- **option**: Optics for Option types
- **record**: Optics for maps
## Documentation
For detailed documentation on each optic type, see:
- [Main Package Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics)
- [Lens Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens)
- [Prism Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism)
- [Iso Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso)
- [Optional Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)
- [Traversal Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)
## Further Reading
For an introduction to functional optics concepts:
- [Introduction to optics: lenses and prisms](https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe) by Giulio Canti
## Examples
See the [samples/lens](../samples/lens) directory for complete working examples.
## License
Apache License 2.0 - See LICENSE file for details.

233
v2/optics/iso/lens/doc.go Normal file
View File

@@ -0,0 +1,233 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package lens provides conversions from isomorphisms to lenses.
# Overview
This package bridges the gap between isomorphisms (bidirectional transformations)
and lenses (focused accessors). Since every isomorphism can be viewed as a lens,
this package provides functions to perform that conversion.
An isomorphism Iso[S, A] represents a lossless bidirectional transformation between
types S and A. A lens Lens[S, A] provides focused access to a part A within a
structure S. Since an isomorphism can transform the entire structure S to A and back,
it naturally forms a lens that focuses on the "whole as a part".
# Mathematical Foundation
Given an Iso[S, A] with:
- Get: S → A (forward transformation)
- ReverseGet: A → S (reverse transformation)
We can construct a Lens[S, A] with:
- Get: S → A (same as iso's Get)
- Set: A → S → S (implemented as: a => s => ReverseGet(a))
The lens laws are automatically satisfied because the isomorphism laws guarantee:
1. GetSet: Set(Get(s))(s) == s (from iso's round-trip law)
2. SetGet: Get(Set(a)(s)) == a (from iso's inverse law)
3. SetSet: Set(a2)(Set(a1)(s)) == Set(a2)(s) (trivially true)
# Basic Usage
Converting an isomorphism to a lens:
type Celsius float64
type Kelvin float64
// Create an isomorphism between Celsius and Kelvin
celsiusKelvinIso := iso.MakeIso(
func(c Celsius) Kelvin { return Kelvin(c + 273.15) },
func(k Kelvin) Celsius { return Celsius(k - 273.15) },
)
// Convert to a lens
celsiusKelvinLens := lens.IsoAsLens(celsiusKelvinIso)
// Use as a lens
celsius := Celsius(20.0)
kelvin := celsiusKelvinLens.Get(celsius) // 293.15 K
updated := celsiusKelvinLens.Set(Kelvin(300))(celsius) // 26.85°C
# Working with Pointers
For pointer-based structures, use IsoAsLensRef:
type UserId int
type User struct {
id UserId
name string
}
// Isomorphism between User pointer and UserId
userIdIso := iso.MakeIso(
func(u *User) UserId { return u.id },
func(id UserId) *User { return &User{id: id, name: "Unknown"} },
)
// Convert to a reference lens
userIdLens := lens.IsoAsLensRef(userIdIso)
user := &User{id: 42, name: "Alice"}
id := userIdLens.Get(user) // 42
updated := userIdLens.Set(UserId(100))(user) // New user with id 100
# Use Cases
1. Type Wrappers: Convert between newtype wrappers and their underlying types
type Email string
type ValidatedEmail struct{ value Email }
emailIso := iso.MakeIso(
func(ve ValidatedEmail) Email { return ve.value },
func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
)
emailLens := lens.IsoAsLens(emailIso)
2. Unit Conversions: Work with different units of measurement
type Meters float64
type Feet float64
metersFeetIso := iso.MakeIso(
func(m Meters) Feet { return Feet(m * 3.28084) },
func(f Feet) Meters { return Meters(f / 3.28084) },
)
distanceLens := lens.IsoAsLens(metersFeetIso)
3. Encoding/Decoding: Transform between different representations
type JSON string
type Config struct {
Host string
Port int
}
// Assuming encode/decode functions exist
configIso := iso.MakeIso(encode, decode)
configLens := lens.IsoAsLens(configIso)
# Composition
Lenses created from isomorphisms can be composed with other lenses:
type Temperature struct {
celsius Celsius
}
// Lens to access celsius field
celsiusFieldLens := L.MakeLens(
func(t Temperature) Celsius { return t.celsius },
func(t Temperature, c Celsius) Temperature {
t.celsius = c
return t
},
)
// Compose with iso-based lens to work with Kelvin
tempKelvinLens := F.Pipe1(
celsiusFieldLens,
L.Compose[Temperature](celsiusKelvinLens),
)
temp := Temperature{celsius: 20}
kelvin := tempKelvinLens.Get(temp) // 293.15 K
updated := tempKelvinLens.Set(Kelvin(300))(temp) // 26.85°C
# Comparison with Direct Lenses
While you can create a lens directly, using an isomorphism provides benefits:
1. Reusability: The isomorphism can be used in multiple contexts
2. Bidirectionality: The inverse transformation is explicitly available
3. Type Safety: Isomorphism laws ensure correctness
4. Composability: Isomorphisms compose naturally
Direct lens approach requires defining both get and set operations separately,
while the isomorphism approach defines the bidirectional transformation once
and converts it to a lens when needed.
# Performance Considerations
Converting an isomorphism to a lens has minimal overhead. The resulting lens
simply delegates to the isomorphism's Get and ReverseGet functions. However,
keep in mind:
1. Each Set operation performs a full transformation via ReverseGet
2. For pointer types, use IsoAsLensRef to ensure proper copying
3. The lens ignores the original structure in Set, using only the new value
# Function Reference
Conversion Functions:
- IsoAsLens: Convert Iso[S, A] to Lens[S, A] for value types
- IsoAsLensRef: Convert Iso[*S, A] to Lens[*S, A] for pointer types
# Related Packages
- github.com/IBM/fp-go/v2/optics/iso: Isomorphisms (bidirectional transformations)
- github.com/IBM/fp-go/v2/optics/lens: Lenses (focused accessors)
- github.com/IBM/fp-go/v2/optics/lens/iso: Convert lenses to isomorphisms (inverse operation)
- github.com/IBM/fp-go/v2/endomorphism: Endomorphisms (A → A functions)
- github.com/IBM/fp-go/v2/function: Function composition utilities
# Examples
Complete example with type wrappers:
type UserId int
type Username string
type User struct {
id UserId
name Username
}
// Isomorphism for UserId
userIdIso := iso.MakeIso(
func(u User) UserId { return u.id },
func(id UserId) User { return User{id: id, name: "Unknown"} },
)
// Isomorphism for Username
usernameIso := iso.MakeIso(
func(u User) Username { return u.name },
func(name Username) User { return User{id: 0, name: name} },
)
// Convert to lenses
idLens := lens.IsoAsLens(userIdIso)
nameLens := lens.IsoAsLens(usernameIso)
user := User{id: 42, name: "Alice"}
// Access and modify through lenses
id := idLens.Get(user) // 42
name := nameLens.Get(user) // "Alice"
renamed := nameLens.Set("Bob")(user) // User{id: 0, name: "Bob"}
reidentified := idLens.Set(UserId(100))(user) // User{id: 100, name: "Unknown"}
Note: When using Set with iso-based lenses, the entire structure is replaced
via ReverseGet, so other fields may be reset to default values. For partial
updates, use regular lenses instead.
*/
package lens
// Made with Bob

View File

@@ -18,16 +18,15 @@ package lens
import (
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// IsoAsLens converts an `Iso` to a `Lens`
func IsoAsLens[S, A any](sa I.Iso[S, A]) L.Lens[S, A] {
func IsoAsLens[S, A any](sa Iso[S, A]) Lens[S, A] {
return L.MakeLensCurried(sa.Get, F.Flow2(sa.ReverseGet, F.Flow2(F.Constant1[S, S], EM.Of[func(S) S])))
}
// IsoAsLensRef converts an `Iso` to a `Lens`
func IsoAsLensRef[S, A any](sa I.Iso[*S, A]) L.Lens[*S, A] {
func IsoAsLensRef[S, A any](sa Iso[*S, A]) Lens[*S, A] {
return L.MakeLensRefCurried(sa.Get, F.Flow2(sa.ReverseGet, F.Flow2(F.Constant1[*S, *S], EM.Of[func(*S) *S])))
}

View File

@@ -0,0 +1,401 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lens
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
ISO "github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
// Test types
type Celsius float64
type Fahrenheit float64
type UserId int
type User struct {
id UserId
name string
}
type Meters float64
type Feet float64
// TestIsoAsLensBasic tests basic functionality of IsoAsLens
func TestIsoAsLensBasic(t *testing.T) {
// Create an isomorphism between Celsius and Fahrenheit
celsiusToFahrenheit := func(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
fahrenheitToCelsius := func(f Fahrenheit) Celsius {
return Celsius((f - 32) * 5 / 9)
}
tempIso := ISO.MakeIso(celsiusToFahrenheit, fahrenheitToCelsius)
tempLens := IsoAsLens(tempIso)
t.Run("Get", func(t *testing.T) {
celsius := Celsius(20.0)
fahrenheit := tempLens.Get(celsius)
assert.InDelta(t, 68.0, float64(fahrenheit), 0.001)
})
t.Run("Set", func(t *testing.T) {
celsius := Celsius(20.0)
newFahrenheit := Fahrenheit(86.0)
updated := tempLens.Set(newFahrenheit)(celsius)
assert.InDelta(t, 30.0, float64(updated), 0.001)
})
t.Run("SetPreservesOriginal", func(t *testing.T) {
original := Celsius(20.0)
newFahrenheit := Fahrenheit(86.0)
_ = tempLens.Set(newFahrenheit)(original)
// Original should be unchanged
assert.Equal(t, Celsius(20.0), original)
})
}
// TestIsoAsLensRefBasic tests basic functionality of IsoAsLensRef
func TestIsoAsLensRefBasic(t *testing.T) {
// Create an isomorphism for User pointer and UserId
userToId := func(u *User) UserId {
return u.id
}
idToUser := func(id UserId) *User {
return &User{id: id, name: "Unknown"}
}
userIdIso := ISO.MakeIso(userToId, idToUser)
userIdLens := IsoAsLensRef(userIdIso)
t.Run("Get", func(t *testing.T) {
user := &User{id: 42, name: "Alice"}
id := userIdLens.Get(user)
assert.Equal(t, UserId(42), id)
})
t.Run("Set", func(t *testing.T) {
user := &User{id: 42, name: "Alice"}
newId := UserId(100)
updated := userIdLens.Set(newId)(user)
assert.Equal(t, UserId(100), updated.id)
assert.Equal(t, "Unknown", updated.name) // ReverseGet creates new user
})
t.Run("SetCreatesNewPointer", func(t *testing.T) {
user := &User{id: 42, name: "Alice"}
newId := UserId(100)
updated := userIdLens.Set(newId)(user)
// Should be different pointers
assert.NotSame(t, user, updated)
// Original should be unchanged
assert.Equal(t, UserId(42), user.id)
assert.Equal(t, "Alice", user.name)
})
}
// TestIsoAsLensLaws verifies that IsoAsLens satisfies lens laws
func TestIsoAsLensLaws(t *testing.T) {
// Create a simple isomorphism
type Wrapper struct{ value int }
wrapperIso := ISO.MakeIso(
func(w Wrapper) int { return w.value },
func(i int) Wrapper { return Wrapper{value: i} },
)
lens := IsoAsLens(wrapperIso)
wrapper := Wrapper{value: 42}
newValue := 100
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSetLaw", func(t *testing.T) {
result := lens.Set(lens.Get(wrapper))(wrapper)
assert.Equal(t, wrapper, result)
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGetLaw", func(t *testing.T) {
result := lens.Get(lens.Set(newValue)(wrapper))
assert.Equal(t, newValue, result)
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSetLaw", func(t *testing.T) {
result1 := lens.Set(200)(lens.Set(newValue)(wrapper))
result2 := lens.Set(200)(wrapper)
assert.Equal(t, result2, result1)
})
}
// TestIsoAsLensRefLaws verifies that IsoAsLensRef satisfies lens laws
func TestIsoAsLensRefLaws(t *testing.T) {
type Wrapper struct{ value int }
wrapperIso := ISO.MakeIso(
func(w *Wrapper) int { return w.value },
func(i int) *Wrapper { return &Wrapper{value: i} },
)
lens := IsoAsLensRef(wrapperIso)
wrapper := &Wrapper{value: 42}
newValue := 100
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSetLaw", func(t *testing.T) {
result := lens.Set(lens.Get(wrapper))(wrapper)
assert.Equal(t, wrapper.value, result.value)
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGetLaw", func(t *testing.T) {
result := lens.Get(lens.Set(newValue)(wrapper))
assert.Equal(t, newValue, result)
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSetLaw", func(t *testing.T) {
result1 := lens.Set(200)(lens.Set(newValue)(wrapper))
result2 := lens.Set(200)(wrapper)
assert.Equal(t, result2.value, result1.value)
})
}
// TestIsoAsLensComposition tests composing iso-based lenses with other lenses
func TestIsoAsLensComposition(t *testing.T) {
type Temperature struct {
celsius Celsius
}
// Lens to access celsius field
celsiusFieldLens := L.MakeLens(
func(t Temperature) Celsius { return t.celsius },
func(t Temperature, c Celsius) Temperature {
t.celsius = c
return t
},
)
// Isomorphism between Celsius and Fahrenheit
celsiusToFahrenheit := func(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
fahrenheitToCelsius := func(f Fahrenheit) Celsius {
return Celsius((f - 32) * 5 / 9)
}
tempIso := ISO.MakeIso(celsiusToFahrenheit, fahrenheitToCelsius)
tempLens := IsoAsLens(tempIso)
// Compose to work with Fahrenheit directly from Temperature
composedLens := F.Pipe1(
celsiusFieldLens,
L.Compose[Temperature](tempLens),
)
temp := Temperature{celsius: 20}
t.Run("ComposedGet", func(t *testing.T) {
fahrenheit := composedLens.Get(temp)
assert.InDelta(t, 68.0, float64(fahrenheit), 0.001)
})
t.Run("ComposedSet", func(t *testing.T) {
newFahrenheit := Fahrenheit(86.0)
updated := composedLens.Set(newFahrenheit)(temp)
assert.InDelta(t, 30.0, float64(updated.celsius), 0.001)
})
}
// TestIsoAsLensModify tests using Modify with iso-based lenses
func TestIsoAsLensModify(t *testing.T) {
// Isomorphism between Meters and Feet
metersToFeet := func(m Meters) Feet {
return Feet(m * 3.28084)
}
feetToMeters := func(f Feet) Meters {
return Meters(f / 3.28084)
}
distanceIso := ISO.MakeIso(metersToFeet, feetToMeters)
distanceLens := IsoAsLens(distanceIso)
meters := Meters(10.0)
t.Run("ModifyDouble", func(t *testing.T) {
// Double the distance in feet, result in meters
doubleFeet := func(f Feet) Feet { return f * 2 }
modified := L.Modify[Meters](doubleFeet)(distanceLens)(meters)
assert.InDelta(t, 20.0, float64(modified), 0.001)
})
t.Run("ModifyIdentity", func(t *testing.T) {
// Identity modification should return same value
identity := func(f Feet) Feet { return f }
modified := L.Modify[Meters](identity)(distanceLens)(meters)
assert.InDelta(t, float64(meters), float64(modified), 0.001)
})
}
// TestIsoAsLensWithIdentityIso tests that identity iso creates identity lens
func TestIsoAsLensWithIdentityIso(t *testing.T) {
type Value int
idIso := ISO.Id[Value]()
idLens := IsoAsLens(idIso)
value := Value(42)
t.Run("IdentityGet", func(t *testing.T) {
result := idLens.Get(value)
assert.Equal(t, value, result)
})
t.Run("IdentitySet", func(t *testing.T) {
newValue := Value(100)
result := idLens.Set(newValue)(value)
assert.Equal(t, newValue, result)
})
}
// TestIsoAsLensRefWithIdentityIso tests identity iso with references
func TestIsoAsLensRefWithIdentityIso(t *testing.T) {
type Value struct{ n int }
idIso := ISO.Id[*Value]()
idLens := IsoAsLensRef(idIso)
value := &Value{n: 42}
t.Run("IdentityGet", func(t *testing.T) {
result := idLens.Get(value)
assert.Equal(t, value, result)
})
t.Run("IdentitySet", func(t *testing.T) {
newValue := &Value{n: 100}
result := idLens.Set(newValue)(value)
assert.Equal(t, newValue, result)
})
}
// TestIsoAsLensRoundTrip tests round-trip conversions
func TestIsoAsLensRoundTrip(t *testing.T) {
type Email string
type ValidatedEmail struct{ value Email }
emailIso := ISO.MakeIso(
func(ve ValidatedEmail) Email { return ve.value },
func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
)
emailLens := IsoAsLens(emailIso)
validated := ValidatedEmail{value: "user@example.com"}
t.Run("RoundTripThroughGet", func(t *testing.T) {
// Get the email, then Set it back
email := emailLens.Get(validated)
restored := emailLens.Set(email)(validated)
assert.Equal(t, validated, restored)
})
t.Run("RoundTripThroughSet", func(t *testing.T) {
// Set a new email, then Get it
newEmail := Email("admin@example.com")
updated := emailLens.Set(newEmail)(validated)
retrieved := emailLens.Get(updated)
assert.Equal(t, newEmail, retrieved)
})
}
// TestIsoAsLensWithComplexTypes tests with more complex type transformations
func TestIsoAsLensWithComplexTypes(t *testing.T) {
type Point struct {
x, y float64
}
type PolarCoord struct {
r, theta float64
}
// Isomorphism between Cartesian and Polar coordinates (simplified for testing)
cartesianToPolar := func(p Point) PolarCoord {
r := p.x*p.x + p.y*p.y
theta := 0.0 // Simplified
return PolarCoord{r: r, theta: theta}
}
polarToCartesian := func(pc PolarCoord) Point {
return Point{x: pc.r, y: pc.theta} // Simplified
}
coordIso := ISO.MakeIso(cartesianToPolar, polarToCartesian)
coordLens := IsoAsLens(coordIso)
point := Point{x: 3.0, y: 4.0}
t.Run("ComplexGet", func(t *testing.T) {
polar := coordLens.Get(point)
assert.NotNil(t, polar)
})
t.Run("ComplexSet", func(t *testing.T) {
newPolar := PolarCoord{r: 5.0, theta: 0.927}
updated := coordLens.Set(newPolar)(point)
assert.NotNil(t, updated)
})
}
// TestIsoAsLensTypeConversion tests type conversion scenarios
func TestIsoAsLensTypeConversion(t *testing.T) {
type StringWrapper string
type IntWrapper int
// Isomorphism that converts string length to int
strLenIso := ISO.MakeIso(
func(s StringWrapper) IntWrapper { return IntWrapper(len(s)) },
func(i IntWrapper) StringWrapper {
// Create a string of given length (simplified)
result := ""
for j := 0; j < int(i); j++ {
result += "x"
}
return StringWrapper(result)
},
)
strLenLens := IsoAsLens(strLenIso)
t.Run("StringToLength", func(t *testing.T) {
str := StringWrapper("hello")
length := strLenLens.Get(str)
assert.Equal(t, IntWrapper(5), length)
})
t.Run("LengthToString", func(t *testing.T) {
str := StringWrapper("hello")
newLength := IntWrapper(3)
updated := strLenLens.Set(newLength)(str)
assert.Equal(t, 3, len(updated))
})
}
// Made with Bob

View File

@@ -0,0 +1,11 @@
package lens
import (
"github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
)
type (
Lens[S, A any] = L.Lens[S, A]
Iso[S, A any] = iso.Iso[S, A]
)

305
v2/optics/iso/option/doc.go Normal file
View File

@@ -0,0 +1,305 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package option provides isomorphisms for working with Option types.
# Overview
This package offers utilities to convert between regular values and Option-wrapped values,
particularly useful for handling zero values and optional data. It provides isomorphisms
that treat certain values (like zero values) as representing absence, mapping them to None,
while other values map to Some.
# Core Functionality
The main function in this package is FromZero, which creates an isomorphism between a
comparable type T and Option[T], treating the zero value as None.
# FromZero Isomorphism
FromZero creates a bidirectional transformation where:
- Forward (Get): T → Option[T]
- Zero value → None
- Non-zero value → Some(value)
- Reverse (ReverseGet): Option[T] → T
- None → Zero value
- Some(value) → value
# Basic Usage
Working with integers:
import (
"github.com/IBM/fp-go/v2/optics/iso/option"
O "github.com/IBM/fp-go/v2/option"
)
isoInt := option.FromZero[int]()
// Convert zero to None
opt := isoInt.Get(0) // None[int]
// Convert non-zero to Some
opt = isoInt.Get(42) // Some(42)
// Convert None to zero
val := isoInt.ReverseGet(O.None[int]()) // 0
// Convert Some to value
val = isoInt.ReverseGet(O.Some(42)) // 42
# Use Cases
## Database Nullable Columns
Convert between database NULL and Go zero values:
type User struct {
ID int
Name string
Age *int // NULL in database
Email *string
}
ageIso := option.FromZero[*int]()
// Reading from database
var dbAge *int = nil
optAge := ageIso.Get(dbAge) // None[*int]
// Writing to database
userAge := 25
dbAge = ageIso.ReverseGet(O.Some(&userAge)) // &25
## Configuration with Defaults
Handle optional configuration values:
type Config struct {
Port int
Timeout int
MaxConn int
}
portIso := option.FromZero[int]()
// Use zero as "not configured"
config := Config{Port: 0, Timeout: 30, MaxConn: 100}
portOpt := portIso.Get(config.Port) // None[int] (use default)
// Set explicit value
config.Port = portIso.ReverseGet(O.Some(8080)) // 8080
## API Response Handling
Work with APIs that use zero values to indicate absence:
type APIResponse struct {
UserID int // 0 means not set
Score float64 // 0.0 means not available
Message string // "" means no message
}
userIDIso := option.FromZero[int]()
scoreIso := option.FromZero[float64]()
messageIso := option.FromZero[string]()
response := APIResponse{UserID: 0, Score: 0.0, Message: ""}
userID := userIDIso.Get(response.UserID) // None[int]
score := scoreIso.Get(response.Score) // None[float64]
message := messageIso.Get(response.Message) // None[string]
## Validation Logic
Simplify required vs optional field validation:
type FormData struct {
Name string // Required
Email string // Required
Phone string // Optional (empty = not provided)
Comments string // Optional
}
phoneIso := option.FromZero[string]()
commentsIso := option.FromZero[string]()
form := FormData{
Name: "Alice",
Email: "alice@example.com",
Phone: "",
Comments: "",
}
// Check optional fields
phone := phoneIso.Get(form.Phone) // None[string]
comments := commentsIso.Get(form.Comments) // None[string]
// Validate: required fields must be non-empty
if form.Name == "" || form.Email == "" {
// Validation error
}
# Working with Different Types
## Strings
strIso := option.FromZero[string]()
opt := strIso.Get("") // None[string]
opt = strIso.Get("hello") // Some("hello")
val := strIso.ReverseGet(O.None[string]()) // ""
val = strIso.ReverseGet(O.Some("world")) // "world"
## Pointers
ptrIso := option.FromZero[*int]()
opt := ptrIso.Get(nil) // None[*int]
num := 42
opt = ptrIso.Get(&num) // Some(&num)
val := ptrIso.ReverseGet(O.None[*int]()) // nil
val = ptrIso.ReverseGet(O.Some(&num)) // &num
## Floating Point Numbers
floatIso := option.FromZero[float64]()
opt := floatIso.Get(0.0) // None[float64]
opt = floatIso.Get(3.14) // Some(3.14)
val := floatIso.ReverseGet(O.None[float64]()) // 0.0
val = floatIso.ReverseGet(O.Some(2.71)) // 2.71
## Booleans
boolIso := option.FromZero[bool]()
opt := boolIso.Get(false) // None[bool]
opt = boolIso.Get(true) // Some(true)
val := boolIso.ReverseGet(O.None[bool]()) // false
val = boolIso.ReverseGet(O.Some(true)) // true
# Composition with Other Optics
Combine with lenses for nested structures:
import (
L "github.com/IBM/fp-go/v2/optics/lens"
I "github.com/IBM/fp-go/v2/optics/iso"
)
type Settings struct {
Volume int // 0 means muted
}
volumeLens := L.MakeLens(
func(s Settings) int { return s.Volume },
func(s Settings, v int) Settings {
s.Volume = v
return s
},
)
volumeIso := option.FromZero[int]()
// Compose lens with iso
volumeOptLens := F.Pipe1(
volumeLens,
L.IMap[Settings](volumeIso.Get, volumeIso.ReverseGet),
)
settings := Settings{Volume: 0}
vol := volumeOptLens.Get(settings) // None[int] (muted)
// Set volume
updated := volumeOptLens.Set(O.Some(75))(settings)
// updated.Volume == 75
# Isomorphism Laws
FromZero satisfies the isomorphism round-trip laws:
1. **ReverseGet(Get(t)) == t** for all t: T
isoInt := option.FromZero[int]()
value := 42
result := isoInt.ReverseGet(isoInt.Get(value))
// result == 42
2. **Get(ReverseGet(opt)) == opt** for all opt: Option[T]
isoInt := option.FromZero[int]()
opt := O.Some(42)
result := isoInt.Get(isoInt.ReverseGet(opt))
// result == Some(42)
These laws ensure that the transformation is truly reversible with no information loss.
# Performance Considerations
The FromZero isomorphism is very efficient:
- No allocations for the iso structure itself
- Simple equality comparison for zero check
- Direct value unwrapping for ReverseGet
- No reflection or runtime type assertions
# Type Safety
The isomorphism is fully type-safe:
- Compile-time type checking ensures T is comparable
- Generic type parameters prevent type mismatches
- No runtime type assertions needed
- The compiler enforces correct usage
# Limitations
The FromZero isomorphism has some limitations to be aware of:
1. **Zero Value Ambiguity**: Cannot distinguish between "intentionally zero" and "absent"
- For int: 0 always maps to None, even if 0 is a valid value
- For string: "" always maps to None, even if empty string is valid
- Solution: Use a different representation (e.g., pointers) if zero is meaningful
2. **Comparable Constraint**: Only works with comparable types
- Cannot use with slices, maps, or functions
- Cannot use with structs containing non-comparable fields
- Solution: Use pointers to such types, or custom isomorphisms
3. **Boolean Limitation**: false always maps to None
- Cannot represent "explicitly false" vs "not set"
- Solution: Use *bool or a custom type if this distinction matters
# Related Packages
- github.com/IBM/fp-go/v2/optics/iso: Core isomorphism functionality
- github.com/IBM/fp-go/v2/option: Option type and operations
- github.com/IBM/fp-go/v2/optics/lens: Lenses for focused access
- github.com/IBM/fp-go/v2/optics/lens/option: Lenses for optional values
# See Also
For more information on isomorphisms and optics:
- optics/iso package documentation
- optics package overview
- option package documentation
*/
package option
// Made with Bob

View File

@@ -453,6 +453,8 @@ Core Lens Creation:
- MakeLensCurried: Create a lens with curried setter
- MakeLensRef: Create a lens for pointer-based structures
- MakeLensRefCurried: Create a lens for pointers with curried setter
- MakeLensWithEq: Create a lens with equality optimization for pointer structures
- MakeLensStrict: Create a lens with strict equality optimization for pointer structures
- Id: Create an identity lens
- IdRef: Create an identity lens for pointers

366
v2/optics/lens/iso/doc.go Normal file
View File

@@ -0,0 +1,366 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package iso provides utilities for composing lenses with isomorphisms.
# Overview
This package bridges lenses and isomorphisms, allowing you to transform the focus type
of a lens using an isomorphism. It provides functions to compose lenses with isomorphisms
and to create isomorphisms for common patterns like nullable pointers.
The key insight is that if you have a Lens[S, A] and an Iso[A, B], you can create a
Lens[S, B] by composing them. This allows you to work with transformed views of your
data without changing the underlying structure.
# Core Functions
## FromNillable
Creates an isomorphism between a nullable pointer and an Option type:
type Config struct {
Timeout *int
}
// Create isomorphism: *int ↔ Option[int]
timeoutIso := iso.FromNillable[int]()
// nil → None, &value → Some(value)
opt := timeoutIso.Get(nil) // None[int]
num := 42
opt = timeoutIso.Get(&num) // Some(42)
// None → nil, Some(value) → &value
ptr := timeoutIso.ReverseGet(O.None[int]()) // nil
ptr = timeoutIso.ReverseGet(O.Some(42)) // &42
## Compose
Composes a lens with an isomorphism to transform the focus type:
type Person struct {
Name string
Age int
}
type Celsius float64
type Fahrenheit float64
type Weather struct {
Temperature Celsius
}
// Lens to access temperature
tempLens := L.MakeLens(
func(w Weather) Celsius { return w.Temperature },
func(w Weather, t Celsius) Weather {
w.Temperature = t
return w
},
)
// Isomorphism: Celsius ↔ Fahrenheit
celsiusToFahrenheit := I.MakeIso(
func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) },
func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) },
)
// Compose to work with Fahrenheit
tempFahrenheitLens := F.Pipe1(
tempLens,
iso.Compose[Weather, Celsius, Fahrenheit](celsiusToFahrenheit),
)
weather := Weather{Temperature: 20} // 20°C
tempF := tempFahrenheitLens.Get(weather) // 68°F
updated := tempFahrenheitLens.Set(86)(weather) // Set to 86°F (30°C)
# Use Cases
## Working with Nullable Fields
Convert between nullable pointers and Option types:
type DatabaseConfig struct {
Host string
Port int
Username string
Password *string // Nullable
}
type AppConfig struct {
Database *DatabaseConfig
}
// Lens to database config
dbLens := L.MakeLens(
func(c AppConfig) *DatabaseConfig { return c.Database },
func(c AppConfig, db *DatabaseConfig) AppConfig {
c.Database = db
return c
},
)
// Isomorphism for nullable pointer
dbIso := iso.FromNillable[DatabaseConfig]()
// Compose to work with Option
dbOptLens := F.Pipe1(
dbLens,
iso.Compose[AppConfig, *DatabaseConfig, O.Option[DatabaseConfig]](dbIso),
)
config := AppConfig{Database: nil}
dbOpt := dbOptLens.Get(config) // None[DatabaseConfig]
// Set with Some
newDB := DatabaseConfig{Host: "localhost", Port: 5432}
updated := dbOptLens.Set(O.Some(newDB))(config)
## Unit Conversions
Work with different units of measurement:
type Distance struct {
Meters float64
}
type Kilometers float64
type Miles float64
// Lens to meters
metersLens := L.MakeLens(
func(d Distance) float64 { return d.Meters },
func(d Distance, m float64) Distance {
d.Meters = m
return d
},
)
// Isomorphism: meters ↔ kilometers
metersToKm := I.MakeIso(
func(m float64) Kilometers { return Kilometers(m / 1000) },
func(km Kilometers) float64 { return float64(km * 1000) },
)
// Compose to work with kilometers
kmLens := F.Pipe1(
metersLens,
iso.Compose[Distance, float64, Kilometers](metersToKm),
)
distance := Distance{Meters: 5000}
km := kmLens.Get(distance) // 5 km
updated := kmLens.Set(Kilometers(10))(distance) // 10000 meters
## Type Wrappers
Work with newtype wrappers:
type UserId int
type User struct {
ID UserId
Name string
}
// Lens to user ID
idLens := L.MakeLens(
func(u User) UserId { return u.ID },
func(u User, id UserId) User {
u.ID = id
return u
},
)
// Isomorphism: UserId ↔ int
userIdIso := I.MakeIso(
func(id UserId) int { return int(id) },
func(i int) UserId { return UserId(i) },
)
// Compose to work with raw int
idIntLens := F.Pipe1(
idLens,
iso.Compose[User, UserId, int](userIdIso),
)
user := User{ID: 42, Name: "Alice"}
rawId := idIntLens.Get(user) // 42 (int)
updated := idIntLens.Set(100)(user) // UserId(100)
## Nested Nullable Fields
Safely navigate through nullable nested structures:
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address *Address
}
type Company struct {
Name string
CEO *Person
}
// Lens to CEO
ceoLens := L.MakeLens(
func(c Company) *Person { return c.CEO },
func(c Company, p *Person) Company {
c.CEO = p
return c
},
)
// Isomorphism for nullable person
personIso := iso.FromNillable[Person]()
// Compose to work with Option[Person]
ceoOptLens := F.Pipe1(
ceoLens,
iso.Compose[Company, *Person, O.Option[Person]](personIso),
)
company := Company{Name: "Acme Corp", CEO: nil}
ceo := ceoOptLens.Get(company) // None[Person]
// Set CEO
newCEO := Person{Name: "Alice", Address: nil}
updated := ceoOptLens.Set(O.Some(newCEO))(company)
# Composition Patterns
## Chaining Multiple Isomorphisms
type Meters float64
type Kilometers float64
type Miles float64
type Journey struct {
Distance Meters
}
// Lens to distance
distLens := L.MakeLens(
func(j Journey) Meters { return j.Distance },
func(j Journey, d Meters) Journey {
j.Distance = d
return j
},
)
// Isomorphisms
metersToKm := I.MakeIso(
func(m Meters) Kilometers { return Kilometers(m / 1000) },
func(km Kilometers) Meters { return Meters(km * 1000) },
)
kmToMiles := I.MakeIso(
func(km Kilometers) Miles { return Miles(km * 0.621371) },
func(mi Miles) Kilometers { return Kilometers(mi / 0.621371) },
)
// Compose lens with chained isomorphisms
milesLens := F.Pipe2(
distLens,
iso.Compose[Journey, Meters, Kilometers](metersToKm),
iso.Compose[Journey, Kilometers, Miles](kmToMiles),
)
journey := Journey{Distance: 5000} // 5000 meters
miles := milesLens.Get(journey) // ~3.11 miles
## Combining with Optional Lenses
type Config struct {
Database *DatabaseConfig
}
type DatabaseConfig struct {
Port int
}
// Lens to database (nullable)
dbLens := L.MakeLens(
func(c Config) *DatabaseConfig { return c.Database },
func(c Config, db *DatabaseConfig) Config {
c.Database = db
return c
},
)
// Convert to Option lens
dbIso := iso.FromNillable[DatabaseConfig]()
dbOptLens := F.Pipe1(
dbLens,
iso.Compose[Config, *DatabaseConfig, O.Option[DatabaseConfig]](dbIso),
)
// Now compose with lens to port
portLens := L.MakeLens(
func(db DatabaseConfig) int { return db.Port },
func(db DatabaseConfig, port int) DatabaseConfig {
db.Port = port
return db
},
)
// Use ComposeOption to handle the Option
defaultDB := DatabaseConfig{Port: 5432}
configPortLens := F.Pipe1(
dbOptLens,
L.ComposeOption[Config, int](defaultDB)(portLens),
)
# Performance Considerations
Composing lenses with isomorphisms is efficient:
- No additional allocations beyond the lens and iso structures
- Composition creates function closures but is still performant
- The isomorphism transformations are applied on-demand
- Consider caching composed lenses for frequently used paths
# Type Safety
All operations are fully type-safe:
- Compile-time type checking ensures correct composition
- Generic type parameters prevent type mismatches
- No runtime type assertions needed
- The compiler enforces that isomorphisms are properly reversible
# Related Packages
- github.com/IBM/fp-go/v2/optics/lens: Core lens functionality
- github.com/IBM/fp-go/v2/optics/iso: Core isomorphism functionality
- github.com/IBM/fp-go/v2/optics/iso/lens: Convert isomorphisms to lenses
- github.com/IBM/fp-go/v2/option: Option type and operations
- github.com/IBM/fp-go/v2/function: Function composition utilities
# See Also
For more information on lenses and isomorphisms:
- optics/lens package documentation
- optics/iso package documentation
- optics package overview
*/
package iso
// Made with Bob

View File

@@ -24,18 +24,18 @@ import (
)
// FromNillable converts a nillable value to an option and back
func FromNillable[T any]() I.Iso[*T, O.Option[T]] {
func FromNillable[T any]() Iso[*T, Option[T]] {
return I.MakeIso(F.Flow2(
O.FromPredicate(F.IsNonNil[T]),
O.Map(F.Deref[T]),
),
O.Fold(F.Constant((*T)(nil)), F.Ref[T]),
O.Fold(F.ConstNil[T], F.Ref[T]),
)
}
// Compose converts a Lens to a property of `A` into a lens to a property of type `B`
// the transformation is done via an ISO
func Compose[S, A, B any](ab I.Iso[A, B]) func(sa L.Lens[S, A]) L.Lens[S, B] {
func Compose[S, A, B any](ab Iso[A, B]) Operator[S, A, B] {
return F.Pipe2(
ab,
IL.IsoAsLens[A, B],

View File

@@ -0,0 +1,14 @@
package iso
import (
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
)
type (
Option[A any] = option.Option[A]
Iso[S, A any] = iso.Iso[S, A]
Lens[S, A any] = lens.Lens[S, A]
Operator[S, A, B any] = lens.Operator[S, A, B]
)

View File

@@ -435,7 +435,7 @@ func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GE
// person := Person{Name: "Alice", Address: Address{Street: "Main St"}}
// street := personStreetLens.Get(person) // "Main St"
// updated := personStreetLens.Set("Oak Ave")(person)
func Compose[S, A, B any](ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
func Compose[S, A, B any](ab Lens[A, B]) Operator[S, A, B] {
return compose(MakeLens[func(S) B, func(S, B) S], ab)
}
@@ -477,7 +477,7 @@ func Compose[S, A, B any](ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
// )
//
// personStreetLens := F.Pipe1(addressLens, lens.ComposeRef[Person](streetLens))
func ComposeRef[S, A, B any](ab Lens[A, B]) func(Lens[*S, A]) Lens[*S, B] {
func ComposeRef[S, A, B any](ab Lens[A, B]) Operator[*S, A, B] {
return compose(MakeLensRef[func(*S) B, func(*S, B) *S], ab)
}

View File

@@ -0,0 +1,640 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package lens
import (
"testing"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// TestModify tests the Modify function
func TestModify(t *testing.T) {
type Counter struct {
Value int
}
valueLens := MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter {
c.Value = v
return c
},
)
counter := Counter{Value: 5}
// Test increment
increment := func(v int) int { return v + 1 }
modifyIncrement := Modify[Counter](increment)(valueLens)
incremented := modifyIncrement(counter)
assert.Equal(t, 6, incremented.Value)
assert.Equal(t, 5, counter.Value) // Original unchanged
// Test double
double := func(v int) int { return v * 2 }
modifyDouble := Modify[Counter](double)(valueLens)
doubled := modifyDouble(counter)
assert.Equal(t, 10, doubled.Value)
assert.Equal(t, 5, counter.Value) // Original unchanged
// Test identity (no change)
identity := func(v int) int { return v }
modifyIdentity := Modify[Counter](identity)(valueLens)
unchanged := modifyIdentity(counter)
assert.Equal(t, counter, unchanged)
}
func TestModifyRef(t *testing.T) {
valueLens := MakeLensRef(
func(s *Street) int { return s.num },
func(s *Street, num int) *Street {
s.num = num
return s
},
)
street := &Street{num: 10, name: "Main"}
// Test increment
increment := func(v int) int { return v + 1 }
modifyIncrement := Modify[*Street](increment)(valueLens)
incremented := modifyIncrement(street)
assert.Equal(t, 11, incremented.num)
assert.Equal(t, 10, street.num) // Original unchanged
}
// Lens Laws Tests
func TestMakeLensLaws(t *testing.T) {
nameLens := MakeLens(
func(s Street) string { return s.name },
func(s Street, name string) Street {
s.name = name
return s
},
)
street := Street{num: 1, name: "Main"}
newName := "Oak"
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSet", func(t *testing.T) {
result := nameLens.Set(nameLens.Get(street))(street)
assert.Equal(t, street, result)
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGet", func(t *testing.T) {
result := nameLens.Get(nameLens.Set(newName)(street))
assert.Equal(t, newName, result)
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSet", func(t *testing.T) {
result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street))
result2 := nameLens.Set("Elm")(street)
assert.Equal(t, result2, result1)
})
}
func TestMakeLensRefLaws(t *testing.T) {
nameLens := MakeLensRef(
(*Street).GetName,
(*Street).SetName,
)
street := &Street{num: 1, name: "Main"}
newName := "Oak"
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSet", func(t *testing.T) {
result := nameLens.Set(nameLens.Get(street))(street)
assert.Equal(t, street.name, result.name)
assert.Equal(t, street.num, result.num)
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGet", func(t *testing.T) {
result := nameLens.Get(nameLens.Set(newName)(street))
assert.Equal(t, newName, result)
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSet", func(t *testing.T) {
result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street))
result2 := nameLens.Set("Elm")(street)
assert.Equal(t, result2.name, result1.name)
assert.Equal(t, result2.num, result1.num)
})
}
func TestMakeLensCurriedLaws(t *testing.T) {
nameLens := MakeLensCurried(
func(s Street) string { return s.name },
func(name string) func(Street) Street {
return func(s Street) Street {
s.name = name
return s
}
},
)
street := Street{num: 1, name: "Main"}
newName := "Oak"
// Law 1: GetSet
t.Run("GetSet", func(t *testing.T) {
result := nameLens.Set(nameLens.Get(street))(street)
assert.Equal(t, street, result)
})
// Law 2: SetGet
t.Run("SetGet", func(t *testing.T) {
result := nameLens.Get(nameLens.Set(newName)(street))
assert.Equal(t, newName, result)
})
// Law 3: SetSet
t.Run("SetSet", func(t *testing.T) {
result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street))
result2 := nameLens.Set("Elm")(street)
assert.Equal(t, result2, result1)
})
}
func TestMakeLensRefCurriedLaws(t *testing.T) {
nameLens := MakeLensRefCurried(
func(s *Street) string { return s.name },
func(name string) func(*Street) *Street {
return func(s *Street) *Street {
s.name = name
return s
}
},
)
street := &Street{num: 1, name: "Main"}
newName := "Oak"
// Law 1: GetSet
t.Run("GetSet", func(t *testing.T) {
result := nameLens.Set(nameLens.Get(street))(street)
assert.Equal(t, street.name, result.name)
assert.Equal(t, street.num, result.num)
})
// Law 2: SetGet
t.Run("SetGet", func(t *testing.T) {
result := nameLens.Get(nameLens.Set(newName)(street))
assert.Equal(t, newName, result)
})
// Law 3: SetSet
t.Run("SetSet", func(t *testing.T) {
result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street))
result2 := nameLens.Set("Elm")(street)
assert.Equal(t, result2.name, result1.name)
assert.Equal(t, result2.num, result1.num)
})
}
func TestMakeLensWithEqLaws(t *testing.T) {
nameLens := MakeLensWithEq(
EQ.FromStrictEquals[string](),
func(s *Street) string { return s.name },
func(s *Street, name string) *Street {
s.name = name
return s
},
)
street := &Street{num: 1, name: "Main"}
newName := "Oak"
// Law 1: GetSet
t.Run("GetSet", func(t *testing.T) {
result := nameLens.Set(nameLens.Get(street))(street)
assert.Equal(t, street.name, result.name)
assert.Equal(t, street.num, result.num)
// With Eq optimization, should return same pointer
assert.Same(t, street, result)
})
// Law 2: SetGet
t.Run("SetGet", func(t *testing.T) {
result := nameLens.Get(nameLens.Set(newName)(street))
assert.Equal(t, newName, result)
})
// Law 3: SetSet
t.Run("SetSet", func(t *testing.T) {
result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street))
result2 := nameLens.Set("Elm")(street)
assert.Equal(t, result2.name, result1.name)
assert.Equal(t, result2.num, result1.num)
})
}
func TestMakeLensStrictLaws(t *testing.T) {
nameLens := MakeLensStrict(
func(s *Street) string { return s.name },
func(s *Street, name string) *Street {
s.name = name
return s
},
)
street := &Street{num: 1, name: "Main"}
newName := "Oak"
// Law 1: GetSet
t.Run("GetSet", func(t *testing.T) {
result := nameLens.Set(nameLens.Get(street))(street)
assert.Equal(t, street.name, result.name)
assert.Equal(t, street.num, result.num)
// With strict equality optimization, should return same pointer
assert.Same(t, street, result)
})
// Law 2: SetGet
t.Run("SetGet", func(t *testing.T) {
result := nameLens.Get(nameLens.Set(newName)(street))
assert.Equal(t, newName, result)
})
// Law 3: SetSet
t.Run("SetSet", func(t *testing.T) {
result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street))
result2 := nameLens.Set("Elm")(street)
assert.Equal(t, result2.name, result1.name)
assert.Equal(t, result2.num, result1.num)
})
}
func TestIdLaws(t *testing.T) {
idLens := Id[Street]()
street := Street{num: 1, name: "Main"}
newStreet := Street{num: 2, name: "Oak"}
// Law 1: GetSet
t.Run("GetSet", func(t *testing.T) {
result := idLens.Set(idLens.Get(street))(street)
assert.Equal(t, street, result)
})
// Law 2: SetGet
t.Run("SetGet", func(t *testing.T) {
result := idLens.Get(idLens.Set(newStreet)(street))
assert.Equal(t, newStreet, result)
})
// Law 3: SetSet
t.Run("SetSet", func(t *testing.T) {
anotherStreet := Street{num: 3, name: "Elm"}
result1 := idLens.Set(anotherStreet)(idLens.Set(newStreet)(street))
result2 := idLens.Set(anotherStreet)(street)
assert.Equal(t, result2, result1)
})
}
func TestIdRefLaws(t *testing.T) {
idLens := IdRef[Street]()
street := &Street{num: 1, name: "Main"}
newStreet := &Street{num: 2, name: "Oak"}
// Law 1: GetSet
t.Run("GetSet", func(t *testing.T) {
result := idLens.Set(idLens.Get(street))(street)
assert.Equal(t, street.name, result.name)
assert.Equal(t, street.num, result.num)
})
// Law 2: SetGet
t.Run("SetGet", func(t *testing.T) {
result := idLens.Get(idLens.Set(newStreet)(street))
assert.Equal(t, newStreet, result)
})
// Law 3: SetSet
t.Run("SetSet", func(t *testing.T) {
anotherStreet := &Street{num: 3, name: "Elm"}
result1 := idLens.Set(anotherStreet)(idLens.Set(newStreet)(street))
result2 := idLens.Set(anotherStreet)(street)
assert.Equal(t, result2, result1)
})
}
func TestComposeLaws(t *testing.T) {
streetLens := MakeLensRef((*Street).GetName, (*Street).SetName)
addrLens := MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
// Compose to get street name from address
streetNameLens := Compose[*Address](streetLens)(addrLens)
sampleStreet := Street{num: 220, name: "Schönaicherstr"}
sampleAddress := Address{city: "Böblingen", street: &sampleStreet}
newName := "Böblingerstr"
// Law 1: GetSet
t.Run("GetSet", func(t *testing.T) {
result := streetNameLens.Set(streetNameLens.Get(&sampleAddress))(&sampleAddress)
assert.Equal(t, sampleAddress.street.name, result.street.name)
assert.Equal(t, sampleAddress.street.num, result.street.num)
})
// Law 2: SetGet
t.Run("SetGet", func(t *testing.T) {
result := streetNameLens.Get(streetNameLens.Set(newName)(&sampleAddress))
assert.Equal(t, newName, result)
})
// Law 3: SetSet
t.Run("SetSet", func(t *testing.T) {
result1 := streetNameLens.Set("Elm St")(streetNameLens.Set(newName)(&sampleAddress))
result2 := streetNameLens.Set("Elm St")(&sampleAddress)
assert.Equal(t, result2.street.name, result1.street.name)
})
}
func TestComposeRefLaws(t *testing.T) {
streetLens := MakeLensRef((*Street).GetName, (*Street).SetName)
addrLens := MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
// Compose using ComposeRef
streetNameLens := ComposeRef[Address](streetLens)(addrLens)
sampleStreet := Street{num: 220, name: "Schönaicherstr"}
sampleAddress := Address{city: "Böblingen", street: &sampleStreet}
newName := "Böblingerstr"
// Law 1: GetSet
t.Run("GetSet", func(t *testing.T) {
result := streetNameLens.Set(streetNameLens.Get(&sampleAddress))(&sampleAddress)
assert.Equal(t, sampleAddress.street.name, result.street.name)
assert.Equal(t, sampleAddress.street.num, result.street.num)
})
// Law 2: SetGet
t.Run("SetGet", func(t *testing.T) {
result := streetNameLens.Get(streetNameLens.Set(newName)(&sampleAddress))
assert.Equal(t, newName, result)
})
// Law 3: SetSet
t.Run("SetSet", func(t *testing.T) {
result1 := streetNameLens.Set("Elm St")(streetNameLens.Set(newName)(&sampleAddress))
result2 := streetNameLens.Set("Elm St")(&sampleAddress)
assert.Equal(t, result2.street.name, result1.street.name)
})
}
func TestIMapLaws(t *testing.T) {
type Celsius float64
type Fahrenheit float64
celsiusToFahrenheit := func(c Celsius) Fahrenheit {
return Fahrenheit(c*9/5 + 32)
}
fahrenheitToCelsius := func(f Fahrenheit) Celsius {
return Celsius((f - 32) * 5 / 9)
}
type Weather struct {
Temperature Celsius
}
tempCelsiusLens := MakeLens(
func(w Weather) Celsius { return w.Temperature },
func(w Weather, t Celsius) Weather {
w.Temperature = t
return w
},
)
// Create a lens that works with Fahrenheit
tempFahrenheitLens := F.Pipe1(
tempCelsiusLens,
IMap[Weather](celsiusToFahrenheit, fahrenheitToCelsius),
)
weather := Weather{Temperature: 20} // 20°C
newTempF := Fahrenheit(86) // 86°F (30°C)
// Law 1: GetSet
t.Run("GetSet", func(t *testing.T) {
result := tempFahrenheitLens.Set(tempFahrenheitLens.Get(weather))(weather)
// Allow small floating point differences
assert.InDelta(t, float64(weather.Temperature), float64(result.Temperature), 0.0001)
})
// Law 2: SetGet
t.Run("SetGet", func(t *testing.T) {
result := tempFahrenheitLens.Get(tempFahrenheitLens.Set(newTempF)(weather))
assert.InDelta(t, float64(newTempF), float64(result), 0.0001)
})
// Law 3: SetSet
t.Run("SetSet", func(t *testing.T) {
anotherTempF := Fahrenheit(95) // 95°F (35°C)
result1 := tempFahrenheitLens.Set(anotherTempF)(tempFahrenheitLens.Set(newTempF)(weather))
result2 := tempFahrenheitLens.Set(anotherTempF)(weather)
assert.InDelta(t, float64(result2.Temperature), float64(result1.Temperature), 0.0001)
})
}
func TestIMapIdentity(t *testing.T) {
// IMap with identity functions should behave like the original lens
type S struct {
a int
}
originalLens := MakeLens(
func(s S) int { return s.a },
func(s S, a int) S {
s.a = a
return s
},
)
// Apply IMap with identity functions
identityMappedLens := F.Pipe1(
originalLens,
IMap[S](F.Identity[int], F.Identity[int]),
)
s := S{a: 42}
// Both lenses should behave identically
assert.Equal(t, originalLens.Get(s), identityMappedLens.Get(s))
assert.Equal(t, originalLens.Set(100)(s), identityMappedLens.Set(100)(s))
}
func TestIMapComposition(t *testing.T) {
// IMap(f, g) ∘ IMap(h, k) = IMap(f ∘ h, k ∘ g)
type S struct {
value int
}
baseLens := MakeLens(
func(s S) int { return s.value },
func(s S, v int) S {
s.value = v
return s
},
)
// First transformation: int -> float64
intToFloat := func(i int) float64 { return float64(i) }
floatToInt := func(f float64) int { return int(f) }
// Second transformation: float64 -> string
floatToString := func(f float64) string { return F.Pipe1(f, func(x float64) string { return "value" }) }
stringToFloat := func(s string) float64 { return 42.0 }
// Compose IMap twice
lens1 := F.Pipe1(baseLens, IMap[S](intToFloat, floatToInt))
lens2 := F.Pipe1(lens1, IMap[S](floatToString, stringToFloat))
// Direct composition
lens3 := F.Pipe1(
baseLens,
IMap[S](
F.Flow2(intToFloat, floatToString),
F.Flow2(stringToFloat, floatToInt),
),
)
s := S{value: 10}
// Both should produce the same results
assert.Equal(t, lens2.Get(s), lens3.Get(s))
assert.Equal(t, lens2.Set("test")(s), lens3.Set("test")(s))
}
func TestModifyLaws(t *testing.T) {
// Modify should satisfy: Modify(id) = id
// and: Modify(f ∘ g) = Modify(f) ∘ Modify(g)
type S struct {
value int
}
lens := MakeLens(
func(s S) int { return s.value },
func(s S, v int) S {
s.value = v
return s
},
)
s := S{value: 10}
// Modify with identity should return the same value
t.Run("ModifyIdentity", func(t *testing.T) {
modifyIdentity := Modify[S](F.Identity[int])(lens)
result := modifyIdentity(s)
assert.Equal(t, s, result)
})
// Modify composition: Modify(f ∘ g) = Modify(f) ∘ Modify(g)
t.Run("ModifyComposition", func(t *testing.T) {
f := func(x int) int { return x * 2 }
g := func(x int) int { return x + 3 }
// Modify(f ∘ g)
composed := F.Flow2(g, f)
modifyComposed := Modify[S](composed)(lens)
result1 := modifyComposed(s)
// Modify(f) ∘ Modify(g)
modifyG := Modify[S](g)(lens)
intermediate := modifyG(s)
modifyF := Modify[S](f)(lens)
result2 := modifyF(intermediate)
assert.Equal(t, result1, result2)
})
}
func TestComposeAssociativity(t *testing.T) {
// Test that lens composition is associative:
// (l1 ∘ l2) ∘ l3 = l1 ∘ (l2 ∘ l3)
type Level3 struct {
value string
}
type Level2 struct {
level3 Level3
}
type Level1 struct {
level2 Level2
}
lens12 := MakeLens(
func(l1 Level1) Level2 { return l1.level2 },
func(l1 Level1, l2 Level2) Level1 {
l1.level2 = l2
return l1
},
)
lens23 := MakeLens(
func(l2 Level2) Level3 { return l2.level3 },
func(l2 Level2, l3 Level3) Level2 {
l2.level3 = l3
return l2
},
)
lens3Value := MakeLens(
func(l3 Level3) string { return l3.value },
func(l3 Level3, v string) Level3 {
l3.value = v
return l3
},
)
// (lens12 ∘ lens23) ∘ lens3Value
composed1 := F.Pipe2(
lens12,
Compose[Level1](lens23),
Compose[Level1](lens3Value),
)
// lens12 ∘ (lens23 ∘ lens3Value)
composed2 := F.Pipe1(
lens12,
Compose[Level1](F.Pipe1(lens23, Compose[Level2](lens3Value))),
)
l1 := Level1{
level2: Level2{
level3: Level3{value: "test"},
},
}
// Both compositions should behave identically
assert.Equal(t, composed1.Get(l1), composed2.Get(l1))
assert.Equal(t, composed1.Set("new")(l1), composed2.Set("new")(l1))
}
// Made with Bob

View File

@@ -1,38 +1,31 @@
package option
import (
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/lens"
LI "github.com/IBM/fp-go/v2/optics/lens/iso"
O "github.com/IBM/fp-go/v2/option"
)
// fromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the nil value will be set instead
func fromPredicate[GET ~func(S) Option[A], SET ~func(S, Option[A]) S, S, A any](creator func(get GET, set SET) LensO[S, A], pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
func fromPredicate[GET ~func(S) Option[A], SET ~func(Option[A]) Endomorphism[S], S, A any](creator func(get GET, set SET) LensO[S, A], pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
fromPred := O.FromPredicate(pred)
return func(sa Lens[S, A]) LensO[S, A] {
fold := O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set)
return creator(F.Flow2(sa.Get, fromPred), func(s S, a Option[A]) S {
return F.Pipe2(
a,
fold,
EM.Ap(s),
)
})
return creator(F.Flow2(sa.Get, fromPred), O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set))
}
}
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the nil value will be set instead
func FromPredicate[S, A any](pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
return fromPredicate(lens.MakeLens[func(S) Option[A], func(S, Option[A]) S], pred, nilValue)
return fromPredicate(lens.MakeLensCurried[func(S) Option[A], func(Option[A]) Endomorphism[S]], pred, nilValue)
}
// FromPredicateRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the nil value will be set instead
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) Lens[*S, Option[A]] {
return fromPredicate(lens.MakeLensRef[func(*S) Option[A], func(*S, Option[A]) *S], pred, nilValue)
return fromPredicate(lens.MakeLensRefCurried[S, Option[A]], pred, nilValue)
}
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
@@ -48,45 +41,41 @@ func FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, Option[*A]] {
}
// fromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
func fromNullableProp[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
func fromNullableProp[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
orElse := O.GetOrElse(F.Constant(defaultValue))
return func(sa Lens[S, A]) Lens[S, A] {
return creator(F.Flow3(
sa.Get,
isNullable,
O.GetOrElse(F.Constant(defaultValue)),
), func(s S, a A) S {
return sa.Set(a)(s)
},
)
orElse,
), sa.Set)
}
}
// FromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
func FromNullableProp[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
return fromNullableProp(lens.MakeLens[func(S) A, func(S, A) S], isNullable, defaultValue)
return fromNullableProp(lens.MakeLensCurried[func(S) A, func(A) Endomorphism[S]], isNullable, defaultValue)
}
// FromNullablePropRef returns a `Lens` from a property that may be optional. The getter returns a default value for these items
func FromNullablePropRef[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[*S, A]) Lens[*S, A] {
return fromNullableProp(lens.MakeLensRef[func(*S) A, func(*S, A) *S], isNullable, defaultValue)
return fromNullableProp(lens.MakeLensRefCurried[S, A], isNullable, defaultValue)
}
// fromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
func fromOption[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
func fromOption[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
orElse := O.GetOrElse(F.Constant(defaultValue))
return func(sa LensO[S, A]) Lens[S, A] {
return creator(F.Flow2(
sa.Get,
O.GetOrElse(F.Constant(defaultValue)),
), func(s S, a A) S {
return sa.Set(O.Some(a))(s)
},
)
orElse,
), F.Flow2(O.Of[A], sa.Set))
}
}
// FromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
return fromOption(lens.MakeLens[func(S) A, func(S, A) S], defaultValue)
return fromOption(lens.MakeLensCurried[func(S) A, func(A) Endomorphism[S]], defaultValue)
}
// FromOptionRef creates a lens from an Option property with a default value for pointer structures.
@@ -105,5 +94,71 @@ func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
// Returns:
// - A function that takes a Lens[*S, Option[A]] and returns a Lens[*S, A]
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, Option[A]]) Lens[*S, A] {
return fromOption(lens.MakeLensRef[func(*S) A, func(*S, A) *S], defaultValue)
return fromOption(lens.MakeLensRefCurried[S, A], defaultValue)
}
// FromIso converts a Lens[S, A] to a LensO[S, A] using an isomorphism.
//
// This function takes an isomorphism between A and Option[A] and uses it to
// transform a regular lens into an optional lens. It's particularly useful when
// you have a custom isomorphism that defines how to convert between a value
// and its optional representation.
//
// The isomorphism must satisfy the round-trip laws:
// 1. iso.ReverseGet(iso.Get(a)) == a for all a: A
// 2. iso.Get(iso.ReverseGet(opt)) == opt for all opt: Option[A]
//
// Type Parameters:
// - S: The structure type containing the field
// - A: The type of the field being focused on
//
// Parameters:
// - iso: An isomorphism between A and Option[A] that defines the conversion
//
// Returns:
// - A function that takes a Lens[S, A] and returns a LensO[S, A]
//
// Example:
//
// type Config struct {
// timeout int
// }
//
// // Create a lens to the timeout field
// timeoutLens := lens.MakeLens(
// func(c Config) int { return c.timeout },
// func(c Config, t int) Config { c.timeout = t; return c },
// )
//
// // Create an isomorphism that treats 0 as None
// zeroAsNone := iso.MakeIso(
// func(t int) option.Option[int] {
// if t == 0 {
// return option.None[int]()
// }
// return option.Some(t)
// },
// func(opt option.Option[int]) int {
// return option.GetOrElse(func() int { return 0 })(opt)
// },
// )
//
// // Convert to optional lens
// optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
//
// config := Config{timeout: 0}
// opt := optTimeoutLens.Get(config) // None[int]()
// updated := optTimeoutLens.Set(option.Some(30))(config) // Config{timeout: 30}
//
// Common Use Cases:
// - Converting between sentinel values (like 0, -1, "") and Option
// - Applying custom validation logic when converting to/from Option
// - Integrating with existing isomorphisms like FromNillable
//
// See also:
// - FromPredicate: For predicate-based optional conversion
// - FromNillable: For pointer-based optional conversion
// - FromOption: For converting from optional to non-optional with defaults
func FromIso[S, A any](iso Iso[A, Option[A]]) func(Lens[S, A]) LensO[S, A] {
return LI.Compose[S](iso)
}

View File

@@ -0,0 +1,481 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package option
import (
"testing"
EQT "github.com/IBM/fp-go/v2/eq/testing"
F "github.com/IBM/fp-go/v2/function"
ISO "github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// Test types
type Config struct {
timeout int
retries int
}
type Settings struct {
maxConnections int
bufferSize int
}
// TestFromIsoBasic tests basic functionality of FromIso
func TestFromIsoBasic(t *testing.T) {
// Create an isomorphism that treats 0 as None
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
// Create a lens to the timeout field
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
// Convert to optional lens using FromIso
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
t.Run("GetNone", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
result := optTimeoutLens.Get(config)
assert.True(t, O.IsNone(result))
})
t.Run("GetSome", func(t *testing.T) {
config := Config{timeout: 30, retries: 3}
result := optTimeoutLens.Get(config)
assert.True(t, O.IsSome(result))
assert.Equal(t, 30, O.GetOrElse(F.Constant(0))(result))
})
t.Run("SetNone", func(t *testing.T) {
config := Config{timeout: 30, retries: 3}
updated := optTimeoutLens.Set(O.None[int]())(config)
assert.Equal(t, 0, updated.timeout)
assert.Equal(t, 3, updated.retries) // Other fields unchanged
})
t.Run("SetSome", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
updated := optTimeoutLens.Set(O.Some(60))(config)
assert.Equal(t, 60, updated.timeout)
assert.Equal(t, 3, updated.retries) // Other fields unchanged
})
t.Run("SetPreservesOriginal", func(t *testing.T) {
original := Config{timeout: 30, retries: 3}
_ = optTimeoutLens.Set(O.Some(60))(original)
// Original should be unchanged
assert.Equal(t, 30, original.timeout)
assert.Equal(t, 3, original.retries)
})
}
// TestFromIsoWithNegativeSentinel tests using -1 as a sentinel value
func TestFromIsoWithNegativeSentinel(t *testing.T) {
// Create an isomorphism that treats -1 as None
negativeOneAsNone := ISO.MakeIso(
func(n int) O.Option[int] {
if n == -1 {
return O.None[int]()
}
return O.Some(n)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(-1))(opt)
},
)
retriesLens := L.MakeLens(
func(c Config) int { return c.retries },
func(c Config, r int) Config { c.retries = r; return c },
)
optRetriesLens := FromIso[Config, int](negativeOneAsNone)(retriesLens)
t.Run("GetNoneForNegativeOne", func(t *testing.T) {
config := Config{timeout: 30, retries: -1}
result := optRetriesLens.Get(config)
assert.True(t, O.IsNone(result))
})
t.Run("GetSomeForZero", func(t *testing.T) {
config := Config{timeout: 30, retries: 0}
result := optRetriesLens.Get(config)
assert.True(t, O.IsSome(result))
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("SetNoneToNegativeOne", func(t *testing.T) {
config := Config{timeout: 30, retries: 5}
updated := optRetriesLens.Set(O.None[int]())(config)
assert.Equal(t, -1, updated.retries)
})
}
// TestFromIsoLaws verifies that FromIso satisfies lens laws
func TestFromIsoLaws(t *testing.T) {
// Create an isomorphism
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
eqOptInt := O.Eq(EQT.Eq[int]())
eqConfig := EQT.Eq[Config]()
config := Config{timeout: 30, retries: 3}
newValue := O.Some(60)
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSetLaw", func(t *testing.T) {
result := optTimeoutLens.Set(optTimeoutLens.Get(config))(config)
assert.True(t, eqConfig.Equals(config, result))
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGetLaw", func(t *testing.T) {
result := optTimeoutLens.Get(optTimeoutLens.Set(newValue)(config))
assert.True(t, eqOptInt.Equals(newValue, result))
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSetLaw", func(t *testing.T) {
a1 := O.Some(60)
a2 := O.None[int]()
result1 := optTimeoutLens.Set(a2)(optTimeoutLens.Set(a1)(config))
result2 := optTimeoutLens.Set(a2)(config)
assert.True(t, eqConfig.Equals(result1, result2))
})
// Use the testing helper to verify all laws
t.Run("AllLaws", func(t *testing.T) {
laws := LT.AssertLaws(t, eqOptInt, eqConfig)(optTimeoutLens)
assert.True(t, laws(config, O.Some(100)))
assert.True(t, laws(Config{timeout: 0, retries: 5}, O.None[int]()))
})
}
// TestFromIsoComposition tests composing FromIso with other lenses
func TestFromIsoComposition(t *testing.T) {
type Application struct {
config Config
}
// Isomorphism for zero as none
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
// Lens to config field
configLens := L.MakeLens(
func(a Application) Config { return a.config },
func(a Application, c Config) Application { a.config = c; return a },
)
// Lens to timeout field
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
// Compose: Application -> Config -> timeout (as Option)
optTimeoutFromConfig := FromIso[Config, int](zeroAsNone)(timeoutLens)
optTimeoutFromApp := F.Pipe1(
configLens,
L.Compose[Application](optTimeoutFromConfig),
)
app := Application{config: Config{timeout: 0, retries: 3}}
t.Run("ComposedGet", func(t *testing.T) {
result := optTimeoutFromApp.Get(app)
assert.True(t, O.IsNone(result))
})
t.Run("ComposedSet", func(t *testing.T) {
updated := optTimeoutFromApp.Set(O.Some(45))(app)
assert.Equal(t, 45, updated.config.timeout)
assert.Equal(t, 3, updated.config.retries)
})
}
// TestFromIsoModify tests using Modify with FromIso-based lenses
func TestFromIsoModify(t *testing.T) {
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
t.Run("ModifyNoneToSome", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
// Map None to Some(10)
modified := L.Modify[Config](O.Map(func(x int) int { return x + 10 }))(optTimeoutLens)(config)
// Since it was None, Map doesn't apply, stays None (0)
assert.Equal(t, 0, modified.timeout)
})
t.Run("ModifySomeValue", func(t *testing.T) {
config := Config{timeout: 30, retries: 3}
// Double the timeout value
modified := L.Modify[Config](O.Map(func(x int) int { return x * 2 }))(optTimeoutLens)(config)
assert.Equal(t, 60, modified.timeout)
})
t.Run("ModifyWithAlt", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
// Use Alt to provide a default
modified := L.Modify[Config](func(opt O.Option[int]) O.Option[int] {
return O.Alt(F.Constant(O.Some(10)))(opt)
})(optTimeoutLens)(config)
assert.Equal(t, 10, modified.timeout)
})
}
// TestFromIsoWithStringEmpty tests using empty string as None
func TestFromIsoWithStringEmpty(t *testing.T) {
type User struct {
name string
email string
}
// Isomorphism that treats empty string as None
emptyAsNone := ISO.MakeIso(
func(s string) O.Option[string] {
if s == "" {
return O.None[string]()
}
return O.Some(s)
},
func(opt O.Option[string]) string {
return O.GetOrElse(F.Constant(""))(opt)
},
)
emailLens := L.MakeLens(
func(u User) string { return u.email },
func(u User, e string) User { u.email = e; return u },
)
optEmailLens := FromIso[User, string](emptyAsNone)(emailLens)
t.Run("EmptyStringAsNone", func(t *testing.T) {
user := User{name: "Alice", email: ""}
result := optEmailLens.Get(user)
assert.True(t, O.IsNone(result))
})
t.Run("NonEmptyStringAsSome", func(t *testing.T) {
user := User{name: "Alice", email: "alice@example.com"}
result := optEmailLens.Get(user)
assert.True(t, O.IsSome(result))
assert.Equal(t, "alice@example.com", O.GetOrElse(F.Constant(""))(result))
})
t.Run("SetNoneToEmpty", func(t *testing.T) {
user := User{name: "Alice", email: "alice@example.com"}
updated := optEmailLens.Set(O.None[string]())(user)
assert.Equal(t, "", updated.email)
})
}
// TestFromIsoRoundTrip tests round-trip conversions
func TestFromIsoRoundTrip(t *testing.T) {
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
maxConnectionsLens := L.MakeLens(
func(s Settings) int { return s.maxConnections },
func(s Settings, m int) Settings { s.maxConnections = m; return s },
)
optMaxConnectionsLens := FromIso[Settings, int](zeroAsNone)(maxConnectionsLens)
t.Run("RoundTripThroughGet", func(t *testing.T) {
settings := Settings{maxConnections: 100, bufferSize: 1024}
// Get the value, then Set it back
opt := optMaxConnectionsLens.Get(settings)
restored := optMaxConnectionsLens.Set(opt)(settings)
assert.Equal(t, settings, restored)
})
t.Run("RoundTripThroughSet", func(t *testing.T) {
settings := Settings{maxConnections: 0, bufferSize: 1024}
// Set a new value, then Get it
newOpt := O.Some(200)
updated := optMaxConnectionsLens.Set(newOpt)(settings)
retrieved := optMaxConnectionsLens.Get(updated)
assert.True(t, O.Eq(EQT.Eq[int]()).Equals(newOpt, retrieved))
})
t.Run("RoundTripWithNone", func(t *testing.T) {
settings := Settings{maxConnections: 100, bufferSize: 1024}
// Set None, then get it back
updated := optMaxConnectionsLens.Set(O.None[int]())(settings)
retrieved := optMaxConnectionsLens.Get(updated)
assert.True(t, O.IsNone(retrieved))
})
}
// TestFromIsoChaining tests chaining multiple FromIso transformations
func TestFromIsoChaining(t *testing.T) {
// Create two different isomorphisms
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
config := Config{timeout: 30, retries: 3}
t.Run("ChainedOperations", func(t *testing.T) {
// Chain multiple operations
result := F.Pipe2(
config,
optTimeoutLens.Set(O.Some(60)),
optTimeoutLens.Set(O.None[int]()),
)
assert.Equal(t, 0, result.timeout)
})
}
// TestFromIsoMultipleFields tests using FromIso on multiple fields
func TestFromIsoMultipleFields(t *testing.T) {
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
retriesLens := L.MakeLens(
func(c Config) int { return c.retries },
func(c Config, r int) Config { c.retries = r; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
optRetriesLens := FromIso[Config, int](zeroAsNone)(retriesLens)
t.Run("IndependentFields", func(t *testing.T) {
config := Config{timeout: 0, retries: 5}
// Get both fields
timeoutOpt := optTimeoutLens.Get(config)
retriesOpt := optRetriesLens.Get(config)
assert.True(t, O.IsNone(timeoutOpt))
assert.True(t, O.IsSome(retriesOpt))
assert.Equal(t, 5, O.GetOrElse(F.Constant(0))(retriesOpt))
})
t.Run("SetBothFields", func(t *testing.T) {
config := Config{timeout: 0, retries: 0}
// Set both fields
updated := F.Pipe2(
config,
optTimeoutLens.Set(O.Some(30)),
optRetriesLens.Set(O.Some(3)),
)
assert.Equal(t, 30, updated.timeout)
assert.Equal(t, 3, updated.retries)
})
}
// Made with Bob

View File

@@ -17,6 +17,7 @@ package option
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
)
@@ -91,4 +92,6 @@ type (
// optLens := lens.FromNillableRef(timeoutLens)
// // optLens is a LensO[*Config, *int]
LensO[S, A any] = Lens[S, Option[A]]
Iso[S, A any] = iso.Iso[S, A]
)

View File

@@ -80,4 +80,7 @@ type (
// with the focused value updated to a. The original structure is never modified.
Set func(a A) Endomorphism[S]
}
Kleisli[S, A, B any] = func(A) Lens[S, B]
Operator[S, A, B any] = Kleisli[S, Lens[S, A], B]
)

479
v2/optics/optional/doc.go Normal file
View File

@@ -0,0 +1,479 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package optional provides optional optics for focusing on values that may not exist.
# Overview
An Optional is an optic that focuses on a subpart of a data structure that may or may not
be present. Unlike lenses which always focus on an existing field, optionals handle cases
where the target value might be absent, returning Option[A] instead of A.
Optionals are the bridge between lenses (which always succeed) and prisms (which may fail
to match). They combine aspects of both:
- Like lenses: Focus on a specific location in a structure
- Like prisms: The value at that location may not exist
Optionals are essential for:
- Working with nullable fields (pointers that may be nil)
- Accessing nested optional values
- Conditional updates based on value presence
- Safe navigation through potentially missing data
# Mathematical Foundation
An Optional[S, A] consists of two operations:
- GetOption: S → Option[A] (try to extract A from S, may return None)
- Set: A → S → S (update A in S, may be a no-op if value doesn't exist)
Optionals must satisfy the optional laws:
1. GetOptionSet: if GetOption(s) == Some(a), then GetOption(Set(a)(s)) == Some(a)
2. SetGetOption: if GetOption(s) == Some(a), then Set(a)(s) preserves other parts of s
3. SetSet: Set(a2)(Set(a1)(s)) == Set(a2)(s)
# Basic Usage
Creating an optional for a nullable field:
type Config struct {
Timeout *int
MaxSize *int
}
timeoutOptional := optional.MakeOptional(
func(c Config) option.Option[*int] {
return option.FromNillable(c.Timeout)
},
func(c Config, t *int) Config {
c.Timeout = t
return c
},
)
config := Config{Timeout: nil, MaxSize: ptr(100)}
// Get returns None for nil
timeout := timeoutOptional.GetOption(config) // None[*int]
// Set updates the value
newTimeout := 30
updated := timeoutOptional.Set(&newTimeout)(config)
// updated.Timeout points to 30
# Working with Pointers
For pointer-based structures, use MakeOptionalRef which handles copying automatically:
type Database struct {
Host string
Port int
}
type Config struct {
Database *Database
}
dbOptional := optional.MakeOptionalRef(
func(c *Config) option.Option[*Database] {
return option.FromNillable(c.Database)
},
func(c *Config, db *Database) *Config {
c.Database = db
return c
},
)
config := &Config{Database: nil}
// Get returns None when database is nil
db := dbOptional.GetOption(config) // None[*Database]
// Set creates a new config with the database
newDB := &Database{Host: "localhost", Port: 5432}
updated := dbOptional.Set(newDB)(config)
// config.Database is still nil, updated.Database points to newDB
# Identity Optional
The identity optional focuses on the entire structure:
idOpt := optional.Id[Config]()
config := Config{Timeout: ptr(30)}
value := idOpt.GetOption(config) // Some(config)
updated := idOpt.Set(Config{Timeout: ptr(60)})(config)
# Composing Optionals
Optionals can be composed to navigate through nested optional structures:
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address *Address
}
addressOpt := optional.MakeOptional(
func(p Person) option.Option[*Address] {
return option.FromNillable(p.Address)
},
func(p Person, a *Address) Person {
p.Address = a
return p
},
)
cityOpt := optional.MakeOptionalRef(
func(a *Address) option.Option[string] {
if a == nil {
return option.None[string]()
}
return option.Some(a.City)
},
func(a *Address, city string) *Address {
a.City = city
return a
},
)
// Compose to access city from person
personCityOpt := F.Pipe1(
addressOpt,
optional.Compose[Person, *Address, string](cityOpt),
)
person := Person{Name: "Alice", Address: nil}
// Get returns None when address is nil
city := personCityOpt.GetOption(person) // None[string]
// Set updates the city if address exists
withAddress := Person{
Name: "Alice",
Address: &Address{Street: "Main St", City: "NYC"},
}
updated := personCityOpt.Set("Boston")(withAddress)
// updated.Address.City == "Boston"
# From Predicate
Create an optional that only focuses on values satisfying a predicate:
type User struct {
Age int
}
ageOpt := optional.FromPredicate[User, int](
func(age int) bool { return age >= 18 },
)(
func(u User) int { return u.Age },
func(u User, age int) User {
u.Age = age
return u
},
)
adult := User{Age: 25}
age := ageOpt.GetOption(adult) // Some(25)
minor := User{Age: 15}
minorAge := ageOpt.GetOption(minor) // None[int]
// Set only works if predicate is satisfied
updated := ageOpt.Set(30)(adult) // Age becomes 30
unchanged := ageOpt.Set(30)(minor) // Age stays 15 (predicate fails)
# Modifying Values
Use ModifyOption to transform values that exist:
type Counter struct {
Value *int
}
valueOpt := optional.MakeOptional(
func(c Counter) option.Option[*int] {
return option.FromNillable(c.Value)
},
func(c Counter, v *int) Counter {
c.Value = v
return c
},
)
counter := Counter{Value: ptr(5)}
// Increment if value exists
incremented := F.Pipe3(
counter,
valueOpt,
optional.ModifyOption[Counter, *int](func(v *int) *int {
newVal := *v + 1
return &newVal
}),
option.GetOrElse(F.Constant(counter)),
)
// incremented.Value points to 6
// No change if value is nil
nilCounter := Counter{Value: nil}
result := F.Pipe3(
nilCounter,
valueOpt,
optional.ModifyOption[Counter, *int](func(v *int) *int {
newVal := *v + 1
return &newVal
}),
option.GetOrElse(F.Constant(nilCounter)),
)
// result.Value is still nil
# Bidirectional Mapping
Transform the focus type of an optional:
type Celsius float64
type Fahrenheit float64
type Weather struct {
Temperature *Celsius
}
tempCelsiusOpt := optional.MakeOptional(
func(w Weather) option.Option[*Celsius] {
return option.FromNillable(w.Temperature)
},
func(w Weather, t *Celsius) Weather {
w.Temperature = t
return w
},
)
// Create optional that works with Fahrenheit
tempFahrenheitOpt := F.Pipe1(
tempCelsiusOpt,
optional.IMap[Weather, *Celsius, *Fahrenheit](
func(c *Celsius) *Fahrenheit {
f := Fahrenheit(*c*9/5 + 32)
return &f
},
func(f *Fahrenheit) *Celsius {
c := Celsius((*f - 32) * 5 / 9)
return &c
},
),
)
celsius := Celsius(20)
weather := Weather{Temperature: &celsius}
tempF := tempFahrenheitOpt.GetOption(weather) // Some(68°F)
# Real-World Example: Configuration with Defaults
type DatabaseConfig struct {
Host string
Port int
Username string
Password string
}
type AppConfig struct {
Database *DatabaseConfig
Debug bool
}
dbOpt := optional.MakeOptional(
func(c AppConfig) option.Option[*DatabaseConfig] {
return option.FromNillable(c.Database)
},
func(c AppConfig, db *DatabaseConfig) AppConfig {
c.Database = db
return c
},
)
dbHostOpt := optional.MakeOptionalRef(
func(db *DatabaseConfig) option.Option[string] {
if db == nil {
return option.None[string]()
}
return option.Some(db.Host)
},
func(db *DatabaseConfig, host string) *DatabaseConfig {
db.Host = host
return db
},
)
// Compose to access database host
appDbHostOpt := F.Pipe1(
dbOpt,
optional.Compose[AppConfig, *DatabaseConfig, string](dbHostOpt),
)
config := AppConfig{Database: nil, Debug: true}
// Get returns None when database is not configured
host := appDbHostOpt.GetOption(config) // None[string]
// Set creates database if needed
withDB := AppConfig{
Database: &DatabaseConfig{Host: "localhost", Port: 5432},
Debug: true,
}
updated := appDbHostOpt.Set("prod.example.com")(withDB)
// updated.Database.Host == "prod.example.com"
# Real-World Example: Safe Navigation
type Company struct {
Name string
CEO *Person
}
type Person struct {
Name string
Address *Address
}
type Address struct {
City string
}
ceoOpt := optional.MakeOptional(
func(c Company) option.Option[*Person] {
return option.FromNillable(c.CEO)
},
func(c Company, p *Person) Company {
c.CEO = p
return c
},
)
addressOpt := optional.MakeOptionalRef(
func(p *Person) option.Option[*Address] {
return option.FromNillable(p.Address)
},
func(p *Person, a *Address) *Person {
p.Address = a
return p
},
)
cityOpt := optional.MakeOptionalRef(
func(a *Address) option.Option[string] {
if a == nil {
return option.None[string]()
}
return option.Some(a.City)
},
func(a *Address, city string) *Address {
a.City = city
return a
},
)
// Compose all optionals for safe navigation
ceoCityOpt := F.Pipe2(
ceoOpt,
optional.Compose[Company, *Person, *Address](addressOpt),
optional.Compose[Company, *Address, string](cityOpt),
)
company := Company{Name: "Acme Corp", CEO: nil}
// Safe navigation returns None at any missing level
city := ceoCityOpt.GetOption(company) // None[string]
# Optionals in the Optics Hierarchy
Optionals sit between lenses and traversals in the optics hierarchy:
Lens[S, A]
Optional[S, A]
Traversal[S, A]
Prism[S, A]
Optional[S, A]
This means:
- Every Lens can be converted to an Optional (value always exists)
- Every Prism can be converted to an Optional (variant may not match)
- Every Optional can be converted to a Traversal (0 or 1 values)
# Performance Considerations
Optionals are efficient:
- No reflection - all operations are type-safe at compile time
- Minimal allocations - optionals themselves are lightweight
- GetOption short-circuits on None
- Set operations create new copies (immutability)
For best performance:
- Use MakeOptionalRef for pointer structures to ensure proper copying
- Cache composed optionals rather than recomposing
- Consider batch operations when updating multiple optional values
# Type Safety
Optionals are fully type-safe:
- Compile-time type checking
- No runtime type assertions
- Generic type parameters ensure correctness
- Composition maintains type relationships
# Function Reference
Core Optional Creation:
- MakeOptional: Create an optional from getter and setter functions
- MakeOptionalRef: Create an optional for pointer-based structures
- Id: Create an identity optional
- IdRef: Create an identity optional for pointers
Composition:
- Compose: Compose two optionals
- ComposeRef: Compose optionals for pointer structures
Transformation:
- ModifyOption: Transform a value through an optional (returns Option[S])
- SetOption: Set a value through an optional (returns Option[S])
- IMap: Bidirectionally map an optional
- IChain: Bidirectionally map with optional results
- IChainAny: Map to/from any type
Predicate-Based:
- FromPredicate: Create optional from predicate
- FromPredicateRef: Create optional from predicate (ref version)
# Related Packages
- github.com/IBM/fp-go/v2/optics/lens: Lenses for fields that always exist
- github.com/IBM/fp-go/v2/optics/prism: Prisms for sum types
- github.com/IBM/fp-go/v2/optics/traversal: Traversals for multiple values
- github.com/IBM/fp-go/v2/option: Optional values
- github.com/IBM/fp-go/v2/endomorphism: Endomorphisms (A → A functions)
*/
package optional
// Made with Bob

View File

@@ -352,8 +352,11 @@ func FromEither[E, T any]() Prism[Either[E, T], T] {
// - Working with optional fields that use zero as "not set"
// - Replacing zero values with defaults
func FromZero[T comparable]() Prism[T, T] {
var zero T
return MakePrism(option.FromPredicate(func(t T) bool { return t == zero }), F.Identity[T])
return MakePrism(option.FromZero[T](), F.Identity[T])
}
func FromNonZero[T comparable]() Prism[T, T] {
return MakePrism(option.FromNonZero[T](), F.Identity[T])
}
// Match represents a regex match result with full reconstruction capability.

View File

@@ -18,6 +18,7 @@ package prism
import (
"github.com/IBM/fp-go/v2/either"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
)
type (
@@ -93,4 +94,6 @@ type (
// - FromEither for creating prisms that work with Either types
// - Prism composition for building complex error-handling pipelines
Either[E, T any] = either.Either[E, T]
Reader[R, T any] = reader.Reader[R, T]
)

495
v2/optics/traversal/doc.go Normal file
View File

@@ -0,0 +1,495 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package traversal provides traversals - optics for focusing on multiple values simultaneously.
# Overview
A Traversal is an optic that focuses on zero or more values within a data structure,
allowing you to view, modify, or fold over multiple elements at once. Unlike lenses
which focus on a single field, or prisms which focus on one variant, traversals can
target collections, multiple fields, or any number of values.
Traversals are the most general optic and sit at the bottom of the optics hierarchy.
They are essential for:
- Working with collections (arrays, slices, maps)
- Batch operations on multiple fields
- Filtering and transforming multiple values
- Aggregating data from multiple sources
- Applying the same operation to all matching elements
# Mathematical Foundation
A Traversal[S, A] is defined using higher-kinded types and applicative functors.
In practical terms, it provides operations to:
- Modify: Apply a function to all focused values
- Set: Replace all focused values with a constant
- FoldMap: Map each value to a monoid and combine results
- GetAll: Collect all focused values into a list
Traversals must satisfy the traversal laws:
1. Identity: traverse(Identity, id) == Identity
2. Composition: traverse(Compose(F, G), f) == Compose(traverse(F, traverse(G, f)))
These laws ensure that traversals compose properly and behave consistently.
# Basic Usage
Creating a traversal for array elements:
import (
A "github.com/IBM/fp-go/v2/array"
T "github.com/IBM/fp-go/v2/optics/traversal"
TA "github.com/IBM/fp-go/v2/optics/traversal/array"
)
numbers := []int{1, 2, 3, 4, 5}
// Get all elements
all := T.GetAll(numbers)(TA.Traversal[int]())
// Result: [1, 2, 3, 4, 5]
// Modify all elements
doubled := F.Pipe2(
numbers,
TA.Traversal[int](),
T.Modify[[]int, int](func(n int) int { return n * 2 }),
)
// Result: [2, 4, 6, 8, 10]
// Set all elements to a constant
allTens := F.Pipe2(
numbers,
TA.Traversal[int](),
T.Set[[]int, int](10),
)
// Result: [10, 10, 10, 10, 10]
# Identity Traversal
The identity traversal focuses on the entire structure:
idTrav := T.Id[int, int]()
value := 42
result := T.Modify[int, int](func(n int) int { return n * 2 })(idTrav)(value)
// Result: 84
# Folding with Traversals
Aggregate values using monoids:
import (
M "github.com/IBM/fp-go/v2/monoid"
N "github.com/IBM/fp-go/v2/number"
)
numbers := []int{1, 2, 3, 4, 5}
// Sum all elements
sum := F.Pipe2(
numbers,
TA.Traversal[int](),
T.FoldMap[int, []int, int](F.Identity[int]),
)(N.MonoidSum[int]())
// Result: 15
// Product of all elements
product := F.Pipe2(
numbers,
TA.Traversal[int](),
T.FoldMap[int, []int, int](F.Identity[int]),
)(N.MonoidProduct[int]())
// Result: 120
# Composing Traversals
Traversals can be composed to focus on nested collections:
type Person struct {
Name string
Friends []string
}
people := []Person{
{Name: "Alice", Friends: []string{"Bob", "Charlie"}},
{Name: "Bob", Friends: []string{"Alice", "David"}},
}
// Traversal for people array
peopleTrav := TA.Traversal[Person]()
// Traversal for friends array within a person
friendsTrav := T.MakeTraversal(func(p Person) []string {
return p.Friends
})
// Compose to access all friends of all people
allFriendsTrav := F.Pipe1(
peopleTrav,
T.Compose[[]Person, Person, string, ...](friendsTrav),
)
// Get all friends
allFriends := T.GetAll(people)(allFriendsTrav)
// Result: ["Bob", "Charlie", "Alice", "David"]
# Working with Records (Maps)
Traverse over map values:
import TR "github.com/IBM/fp-go/v2/optics/traversal/record"
scores := map[string]int{
"Alice": 85,
"Bob": 92,
"Charlie": 78,
}
// Get all scores
allScores := F.Pipe2(
scores,
TR.Traversal[string, int](),
T.GetAll[map[string]int, int],
)
// Result: [85, 92, 78] (order may vary)
// Increase all scores by 5
boosted := F.Pipe2(
scores,
TR.Traversal[string, int](),
T.Modify[map[string]int, int](func(score int) int {
return score + 5
}),
)
// Result: {"Alice": 90, "Bob": 97, "Charlie": 83}
# Working with Either Types
Traverse over the Right values:
import (
E "github.com/IBM/fp-go/v2/either"
TE "github.com/IBM/fp-go/v2/optics/traversal/either"
)
results := []E.Either[string, int]{
E.Right[string](10),
E.Left[int]("error"),
E.Right[string](20),
}
// Traversal for array of Either
arrayTrav := TA.Traversal[E.Either[string, int]]()
// Traversal for Right values
rightTrav := TE.Traversal[string, int]()
// Compose to access all Right values
allRightsTrav := F.Pipe1(
arrayTrav,
T.Compose[[]E.Either[string, int], E.Either[string, int], int, ...](rightTrav),
)
// Get all Right values
rights := T.GetAll(results)(allRightsTrav)
// Result: [10, 20]
// Double all Right values
doubled := F.Pipe2(
results,
allRightsTrav,
T.Modify[[]E.Either[string, int], int](func(n int) int { return n * 2 }),
)
// Result: [Right(20), Left("error"), Right(40)]
# Working with Option Types
Traverse over Some values:
import (
O "github.com/IBM/fp-go/v2/option"
TO "github.com/IBM/fp-go/v2/optics/traversal/option"
)
values := []O.Option[int]{
O.Some(1),
O.None[int](),
O.Some(2),
O.None[int](),
O.Some(3),
}
// Compose array and option traversals
allSomesTrav := F.Pipe1(
TA.Traversal[O.Option[int]](),
T.Compose[[]O.Option[int], O.Option[int], int, ...](TO.Traversal[int]()),
)
// Get all Some values
somes := T.GetAll(values)(allSomesTrav)
// Result: [1, 2, 3]
// Increment all Some values
incremented := F.Pipe2(
values,
allSomesTrav,
T.Modify[[]O.Option[int], int](func(n int) int { return n + 1 }),
)
// Result: [Some(2), None, Some(3), None, Some(4)]
# Real-World Example: Nested Data Structures
type Department struct {
Name string
Employees []Employee
}
type Employee struct {
Name string
Salary int
}
company := []Department{
{
Name: "Engineering",
Employees: []Employee{
{Name: "Alice", Salary: 100000},
{Name: "Bob", Salary: 95000},
},
},
{
Name: "Sales",
Employees: []Employee{
{Name: "Charlie", Salary: 80000},
{Name: "David", Salary: 85000},
},
},
}
// Traversal for departments
deptTrav := TA.Traversal[Department]()
// Traversal for employees within a department
empTrav := T.MakeTraversal(func(d Department) []Employee {
return d.Employees
})
// Traversal for employee array
empArrayTrav := TA.Traversal[Employee]()
// Compose to access all employees
allEmpTrav := F.Pipe2(
deptTrav,
T.Compose[[]Department, Department, []Employee, ...](empTrav),
T.Compose[[]Department, []Employee, Employee, ...](empArrayTrav),
)
// Get all employee names
names := F.Pipe2(
company,
allEmpTrav,
T.FoldMap[[]string, []Department, Employee](func(e Employee) []string {
return []string{e.Name}
}),
)(A.Monoid[string]())
// Result: ["Alice", "Bob", "Charlie", "David"]
// Give everyone a 10% raise
withRaises := F.Pipe2(
company,
allEmpTrav,
T.Modify[[]Department, Employee](func(e Employee) Employee {
e.Salary = int(float64(e.Salary) * 1.1)
return e
}),
)
# Real-World Example: Filtering with Traversals
type Product struct {
Name string
Price float64
InStock bool
}
products := []Product{
{Name: "Laptop", Price: 999.99, InStock: true},
{Name: "Mouse", Price: 29.99, InStock: false},
{Name: "Keyboard", Price: 79.99, InStock: true},
}
// Create a traversal that only focuses on in-stock products
inStockTrav := T.MakeTraversal(func(ps []Product) []Product {
return A.Filter(func(p Product) bool {
return p.InStock
})(ps)
})
// Apply discount to in-stock items
discounted := F.Pipe2(
products,
inStockTrav,
T.Modify[[]Product, Product](func(p Product) Product {
p.Price = p.Price * 0.9
return p
}),
)
// Only Laptop and Keyboard prices are reduced
# Real-World Example: Data Aggregation
type Order struct {
ID string
Items []OrderItem
Status string
}
type OrderItem struct {
Product string
Quantity int
Price float64
}
orders := []Order{
{
ID: "001",
Items: []OrderItem{
{Product: "Widget", Quantity: 2, Price: 10.0},
{Product: "Gadget", Quantity: 1, Price: 25.0},
},
Status: "completed",
},
{
ID: "002",
Items: []OrderItem{
{Product: "Widget", Quantity: 5, Price: 10.0},
},
Status: "completed",
},
}
// Traversal for orders
orderTrav := TA.Traversal[Order]()
// Traversal for items within an order
itemsTrav := T.MakeTraversal(func(o Order) []OrderItem {
return o.Items
})
// Traversal for item array
itemArrayTrav := TA.Traversal[OrderItem]()
// Compose to access all items
allItemsTrav := F.Pipe2(
orderTrav,
T.Compose[[]Order, Order, []OrderItem, ...](itemsTrav),
T.Compose[[]Order, []OrderItem, OrderItem, ...](itemArrayTrav),
)
// Calculate total revenue
totalRevenue := F.Pipe2(
orders,
allItemsTrav,
T.FoldMap[float64, []Order, OrderItem](func(item OrderItem) float64 {
return float64(item.Quantity) * item.Price
}),
)(N.MonoidSum[float64]())
// Result: 95.0 (2*10 + 1*25 + 5*10)
# Traversals in the Optics Hierarchy
Traversals are the most general optic:
Iso[S, A]
Lens[S, A]
Optional[S, A]
Traversal[S, A]
Prism[S, A]
Optional[S, A]
Traversal[S, A]
This means:
- Every Iso, Lens, Prism, and Optional can be converted to a Traversal
- Traversals are the most flexible but least specific optic
- Use more specific optics when possible for better type safety
# Performance Considerations
Traversals can be efficient but consider:
- Each traversal operation may iterate over all elements
- Composition creates nested iterations
- FoldMap is often more efficient than GetAll followed by reduction
- Modify creates new copies (immutability)
For best performance:
- Use specialized traversals (array, record, etc.) when available
- Avoid unnecessary composition
- Consider batch operations
- Cache composed traversals
# Type Safety
Traversals are fully type-safe:
- Compile-time type checking
- Generic type parameters ensure correctness
- Composition maintains type relationships
- No runtime type assertions
# Function Reference
Core Functions:
- Id: Create an identity traversal
- Modify: Apply a function to all focused values
- Set: Replace all focused values with a constant
- Compose: Compose two traversals
Aggregation:
- FoldMap: Map each value to a monoid and combine
- Fold: Fold over all values using a monoid
- GetAll: Collect all focused values into a list
# Specialized Traversals
The package includes specialized sub-packages for common patterns:
- array: Traversals for arrays and slices
- record: Traversals for maps
- either: Traversals for Either types
- option: Traversals for Option types
Each specialized package provides optimized implementations for its data type.
# Related Packages
- github.com/IBM/fp-go/v2/optics/lens: Lenses for single fields
- github.com/IBM/fp-go/v2/optics/prism: Prisms for sum types
- github.com/IBM/fp-go/v2/optics/optional: Optionals for maybe values
- github.com/IBM/fp-go/v2/optics/traversal/array: Array traversals
- github.com/IBM/fp-go/v2/optics/traversal/record: Record/map traversals
- github.com/IBM/fp-go/v2/optics/traversal/either: Either traversals
- github.com/IBM/fp-go/v2/optics/traversal/option: Option traversals
- github.com/IBM/fp-go/v2/array: Array utilities
- github.com/IBM/fp-go/v2/monoid: Monoid type class
*/
package traversal
// Made with Bob

View File

@@ -39,8 +39,8 @@ var (
// var opt Option[int] = Some(42) // Contains a value
// var opt Option[int] = None[int]() // Contains no value
type Option[A any] struct {
isSome bool
value A
isSome bool
}
type (

View File

@@ -17,9 +17,11 @@
package option
import (
"github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
C "github.com/IBM/fp-go/v2/internal/chain"
FC "github.com/IBM/fp-go/v2/internal/functor"
P "github.com/IBM/fp-go/v2/predicate"
)
// fromPredicate creates an Option based on a predicate function.
@@ -43,6 +45,21 @@ func FromPredicate[A any](pred func(A) bool) Kleisli[A, A] {
return F.Bind2nd(fromPredicate[A], pred)
}
//go:inline
func FromZero[A comparable]() Kleisli[A, A] {
return FromPredicate(P.IsZero[A]())
}
//go:inline
func FromNonZero[A comparable]() Kleisli[A, A] {
return FromPredicate(P.IsNonZero[A]())
}
//go:inline
func FromEq[A any](pred eq.Eq[A]) func(A) Kleisli[A, A] {
return F.Flow2(P.IsEqual(pred), FromPredicate[A])
}
// FromNillable converts a pointer to an Option.
// Returns Some if the pointer is non-nil, None otherwise.
//

View File

@@ -18,6 +18,7 @@ package predicate
import (
"testing"
"github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
@@ -408,3 +409,272 @@ func TestComplexScenarios(t *testing.T) {
assert.False(t, canBuy(Item{Price: 150, Stock: 0}))
})
}
// TestIsEqual tests the IsEqual function
func TestIsEqual(t *testing.T) {
t.Run("works with custom equality", func(t *testing.T) {
type Person struct {
Name string
Age int
}
// Custom equality that only compares names
nameEq := eq.FromEquals(func(a, b Person) bool {
return a.Name == b.Name
})
isEqualToPerson := IsEqual(nameEq)
alice := Person{Name: "Alice", Age: 30}
isAlice := isEqualToPerson(alice)
assert.True(t, isAlice(Person{Name: "Alice", Age: 30}))
assert.True(t, isAlice(Person{Name: "Alice", Age: 25})) // Different age, same name
assert.False(t, isAlice(Person{Name: "Bob", Age: 30}))
})
t.Run("works with struct equality", func(t *testing.T) {
type Point struct {
X, Y int
}
pointEq := eq.FromStrictEquals[Point]()
isEqualToPoint := IsEqual(pointEq)
origin := Point{X: 0, Y: 0}
isOrigin := isEqualToPoint(origin)
assert.True(t, isOrigin(Point{X: 0, Y: 0}))
assert.False(t, isOrigin(Point{X: 1, Y: 0}))
assert.False(t, isOrigin(Point{X: 0, Y: 1}))
})
t.Run("can be used with And/Or", func(t *testing.T) {
intEq := eq.FromStrictEquals[int]()
isEqualTo5 := IsEqual(intEq)(5)
isEqualTo10 := IsEqual(intEq)(10)
is5Or10 := F.Pipe1(isEqualTo5, Or(isEqualTo10))
assert.True(t, is5Or10(5))
assert.True(t, is5Or10(10))
assert.False(t, is5Or10(7))
})
}
// TestIsStrictEqual tests the IsStrictEqual function
func TestIsStrictEqual(t *testing.T) {
t.Run("works with integers", func(t *testing.T) {
isEqualTo42 := IsStrictEqual[int]()(42)
assert.True(t, isEqualTo42(42))
assert.False(t, isEqualTo42(0))
assert.False(t, isEqualTo42(-42))
})
t.Run("works with strings", func(t *testing.T) {
isEqualToHello := IsStrictEqual[string]()("hello")
assert.True(t, isEqualToHello("hello"))
assert.False(t, isEqualToHello("Hello"))
assert.False(t, isEqualToHello("world"))
assert.False(t, isEqualToHello(""))
})
t.Run("works with booleans", func(t *testing.T) {
isEqualToTrue := IsStrictEqual[bool]()(true)
assert.True(t, isEqualToTrue(true))
assert.False(t, isEqualToTrue(false))
isEqualToFalse := IsStrictEqual[bool]()(false)
assert.True(t, isEqualToFalse(false))
assert.False(t, isEqualToFalse(true))
})
t.Run("works with floats", func(t *testing.T) {
isEqualTo3Point14 := IsStrictEqual[float64]()(3.14)
assert.True(t, isEqualTo3Point14(3.14))
assert.False(t, isEqualTo3Point14(3.15))
assert.False(t, isEqualTo3Point14(0.0))
})
t.Run("can be combined with other predicates", func(t *testing.T) {
isEqualTo5 := IsStrictEqual[int]()(5)
isNotEqualTo5 := Not(isEqualTo5)
assert.False(t, isNotEqualTo5(5))
assert.True(t, isNotEqualTo5(10))
assert.True(t, isNotEqualTo5(0))
})
}
// TestIsZero tests the IsZero function
func TestIsZero(t *testing.T) {
t.Run("works with integers", func(t *testing.T) {
isZeroInt := IsZero[int]()
assert.True(t, isZeroInt(0))
assert.False(t, isZeroInt(1))
assert.False(t, isZeroInt(-1))
assert.False(t, isZeroInt(100))
})
t.Run("works with strings", func(t *testing.T) {
isZeroString := IsZero[string]()
assert.True(t, isZeroString(""))
assert.False(t, isZeroString("hello"))
assert.False(t, isZeroString(" "))
assert.False(t, isZeroString("0"))
})
t.Run("works with booleans", func(t *testing.T) {
isZeroBool := IsZero[bool]()
assert.True(t, isZeroBool(false))
assert.False(t, isZeroBool(true))
})
t.Run("works with floats", func(t *testing.T) {
isZeroFloat := IsZero[float64]()
assert.True(t, isZeroFloat(0.0))
assert.False(t, isZeroFloat(0.1))
assert.False(t, isZeroFloat(-0.1))
})
t.Run("works with pointers", func(t *testing.T) {
isZeroPtr := IsZero[*int]()
assert.True(t, isZeroPtr(nil))
x := 42
assert.False(t, isZeroPtr(&x))
})
t.Run("works with structs", func(t *testing.T) {
type Point struct {
X, Y int
}
isZeroPoint := IsZero[Point]()
assert.True(t, isZeroPoint(Point{X: 0, Y: 0}))
assert.False(t, isZeroPoint(Point{X: 1, Y: 0}))
assert.False(t, isZeroPoint(Point{X: 0, Y: 1}))
})
t.Run("can be combined with other predicates", func(t *testing.T) {
isZeroInt := IsZero[int]()
isPositiveOrZero := F.Pipe1(isPositive, Or(isZeroInt))
assert.True(t, isPositiveOrZero(5))
assert.True(t, isPositiveOrZero(0))
assert.False(t, isPositiveOrZero(-5))
})
}
// TestIsNonZero tests the IsNonZero function
func TestIsNonZero(t *testing.T) {
t.Run("works with integers", func(t *testing.T) {
isNonZeroInt := IsNonZero[int]()
assert.False(t, isNonZeroInt(0))
assert.True(t, isNonZeroInt(1))
assert.True(t, isNonZeroInt(-1))
assert.True(t, isNonZeroInt(100))
})
t.Run("works with strings", func(t *testing.T) {
isNonZeroString := IsNonZero[string]()
assert.False(t, isNonZeroString(""))
assert.True(t, isNonZeroString("hello"))
assert.True(t, isNonZeroString(" "))
assert.True(t, isNonZeroString("0"))
})
t.Run("works with booleans", func(t *testing.T) {
isNonZeroBool := IsNonZero[bool]()
assert.False(t, isNonZeroBool(false))
assert.True(t, isNonZeroBool(true))
})
t.Run("works with floats", func(t *testing.T) {
isNonZeroFloat := IsNonZero[float64]()
assert.False(t, isNonZeroFloat(0.0))
assert.True(t, isNonZeroFloat(0.1))
assert.True(t, isNonZeroFloat(-0.1))
})
t.Run("works with pointers", func(t *testing.T) {
isNonZeroPtr := IsNonZero[*int]()
assert.False(t, isNonZeroPtr(nil))
x := 42
assert.True(t, isNonZeroPtr(&x))
y := 0
assert.True(t, isNonZeroPtr(&y)) // Pointer itself is non-nil
})
t.Run("is opposite of IsZero", func(t *testing.T) {
isZeroInt := IsZero[int]()
isNonZeroInt := IsNonZero[int]()
testValues := []int{-100, -1, 0, 1, 100}
for _, v := range testValues {
assert.Equal(t, !isZeroInt(v), isNonZeroInt(v), "IsNonZero should be opposite of IsZero for value %d", v)
}
})
t.Run("can be combined with other predicates", func(t *testing.T) {
isNonZeroInt := IsNonZero[int]()
isNonZeroAndPositive := F.Pipe1(isNonZeroInt, And(isPositive))
assert.True(t, isNonZeroAndPositive(5))
assert.False(t, isNonZeroAndPositive(0))
assert.False(t, isNonZeroAndPositive(-5))
})
}
// TestPredicatesIntegration tests integration of predicates.go functions with other predicate operations
func TestPredicatesIntegration(t *testing.T) {
t.Run("filter with IsZero", func(t *testing.T) {
numbers := []int{0, 1, 0, 2, 0, 3}
isZeroInt := IsZero[int]()
var nonZeros []int
for _, n := range numbers {
if !isZeroInt(n) {
nonZeros = append(nonZeros, n)
}
}
assert.Equal(t, []int{1, 2, 3}, nonZeros)
})
t.Run("validation with IsNonZero", func(t *testing.T) {
type Config struct {
Host string
Port int
}
isNonZeroString := IsNonZero[string]()
isNonZeroInt := IsNonZero[int]()
getHost := func(c Config) string { return c.Host }
getPort := func(c Config) int { return c.Port }
hasHost := F.Pipe1(isNonZeroString, ContraMap(getHost))
hasPort := F.Pipe1(isNonZeroInt, ContraMap(getPort))
isValid := F.Pipe1(hasHost, And(hasPort))
assert.True(t, isValid(Config{Host: "localhost", Port: 8080}))
assert.False(t, isValid(Config{Host: "", Port: 8080}))
assert.False(t, isValid(Config{Host: "localhost", Port: 0}))
assert.False(t, isValid(Config{Host: "", Port: 0}))
})
t.Run("equality with monoid", func(t *testing.T) {
m := MonoidAny[int]()
isEqualTo1 := IsStrictEqual[int]()(1)
isEqualTo2 := IsStrictEqual[int]()(2)
isEqualTo3 := IsStrictEqual[int]()(3)
is1Or2Or3 := m.Concat(m.Concat(isEqualTo1, isEqualTo2), isEqualTo3)
assert.True(t, is1Or2Or3(1))
assert.True(t, is1Or2Or3(2))
assert.True(t, is1Or2Or3(3))
assert.False(t, is1Or2Or3(4))
assert.False(t, is1Or2Or3(0))
})
}

120
v2/predicate/predicates.go Normal file
View File

@@ -0,0 +1,120 @@
// 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 predicate
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/eq"
)
// IsEqual creates a Kleisli arrow that tests if two values are equal using a custom equality function.
//
// This function takes an Eq instance (which defines how to compare values of type A) and returns
// a curried function that can be used to create predicates for equality testing.
//
// Parameters:
// - pred: An Eq[A] instance that defines equality for type A
//
// Returns:
// - A Kleisli[A, A] that takes a value and returns a predicate testing equality with that value
//
// Example:
//
// type Person struct { Name string; Age int }
// personEq := eq.MakeEq(func(a, b Person) bool {
// return a.Name == b.Name && a.Age == b.Age
// })
// isEqualToPerson := IsEqual(personEq)
// alice := Person{Name: "Alice", Age: 30}
// isAlice := isEqualToPerson(alice)
// isAlice(Person{Name: "Alice", Age: 30}) // true
// isAlice(Person{Name: "Bob", Age: 30}) // false
func IsEqual[A any](pred eq.Eq[A]) Kleisli[A, A] {
return F.Curry2(pred.Equals)
}
// IsStrictEqual creates a Kleisli arrow that tests if two values are equal using Go's == operator.
//
// This is a convenience function for comparable types that uses strict equality (==) for comparison.
// It's equivalent to IsEqual with an Eq instance based on ==.
//
// Returns:
// - A Kleisli[A, A] that takes a value and returns a predicate testing strict equality
//
// Example:
//
// isEqualTo5 := IsStrictEqual[int]()(5)
// isEqualTo5(5) // true
// isEqualTo5(10) // false
//
// isEqualToHello := IsStrictEqual[string]()("hello")
// isEqualToHello("hello") // true
// isEqualToHello("world") // false
func IsStrictEqual[A comparable]() Kleisli[A, A] {
return IsEqual(eq.FromStrictEquals[A]())
}
// IsZero creates a predicate that tests if a value equals the zero value for its type.
//
// The zero value is the default value for a type in Go (e.g., 0 for int, "" for string,
// false for bool, nil for pointers, etc.).
//
// Returns:
// - A Predicate[A] that returns true if the value is the zero value for type A
//
// Example:
//
// isZeroInt := IsZero[int]()
// isZeroInt(0) // true
// isZeroInt(5) // false
//
// isZeroString := IsZero[string]()
// isZeroString("") // true
// isZeroString("hello") // false
//
// isZeroBool := IsZero[bool]()
// isZeroBool(false) // true
// isZeroBool(true) // false
func IsZero[A comparable]() Predicate[A] {
var zero A
return IsStrictEqual[A]()(zero)
}
// IsNonZero creates a predicate that tests if a value is not equal to the zero value for its type.
//
// This is the negation of IsZero, returning true for any non-zero value.
//
// Returns:
// - A Predicate[A] that returns true if the value is not the zero value for type A
//
// Example:
//
// isNonZeroInt := IsNonZero[int]()
// isNonZeroInt(0) // false
// isNonZeroInt(5) // true
// isNonZeroInt(-3) // true
//
// isNonZeroString := IsNonZero[string]()
// isNonZeroString("") // false
// isNonZeroString("hello") // true
//
// isNonZeroPtr := IsNonZero[*int]()
// isNonZeroPtr(nil) // false
// isNonZeroPtr(new(int)) // true
func IsNonZero[A comparable]() Predicate[A] {
return Not(IsZero[A]())
}

View File

@@ -45,7 +45,9 @@ type (
// It is commonly used for filtering, validation, and conditional logic.
Predicate[A any] = func(A) bool
Kleisli[A, B any] = func(A) Predicate[B]
// Operator represents a function that transforms a Predicate[A] into a Predicate[B].
// This is useful for composing and transforming predicates.
Operator[A, B any] = func(Predicate[A]) Predicate[B]
Operator[A, B any] = Kleisli[Predicate[A], B]
)

View File

@@ -278,7 +278,7 @@ func Local[A, R2, R1 any](f func(R2) R1) func(Reader[R1, A]) Reader[R2, A] {
// getPort := reader.Asks(func(c Config) int { return c.Port })
// run := reader.Read(Config{Port: 8080})
// port := run(getPort) // 8080
func Read[E, A any](e E) func(Reader[E, A]) A {
func Read[A, E any](e E) func(Reader[E, A]) A {
return I.Ap[A](e)
}

View File

@@ -175,7 +175,7 @@ func TestLocal(t *testing.T) {
func TestRead(t *testing.T) {
config := Config{Port: 8080}
getPort := Asks(func(c Config) int { return c.Port })
run := Read[Config, int](config)
run := Read[int](config)
port := run(getPort)
assert.Equal(t, 8080, port)
}

View File

@@ -153,7 +153,7 @@ func Local[E, A, R2, R1 any](f func(R2) R1) func(ReaderEither[R1, E, A]) ReaderE
// Read applies a context to a reader to obtain its value
func Read[E1, A, E any](e E) func(ReaderEither[E, E1, A]) Either[E1, A] {
return reader.Read[E, Either[E1, A]](e)
return reader.Read[Either[E1, A]](e)
}
func MonadFlap[L, E, A, B any](fab ReaderEither[L, E, func(A) B], a A) ReaderEither[L, E, B] {

View File

@@ -22,8 +22,12 @@ import (
)
type (
Option[A any] = option.Option[A]
Either[E, A any] = either.Either[E, A]
Reader[R, A any] = reader.Reader[R, A]
Option[A any] = option.Option[A]
Either[E, A any] = either.Either[E, A]
Reader[R, A any] = reader.Reader[R, A]
ReaderEither[R, E, A any] = Reader[R, Either[E, A]]
Kleisli[R, E, A, B any] = Reader[A, ReaderEither[R, E, B]]
Operator[R, E, A, B any] = Kleisli[R, E, ReaderEither[R, E, A], B]
)

View File

@@ -161,7 +161,7 @@ func MonadChainReaderK[GEA ~func(R) GIOA, GEB ~func(R) GIOB, GIOA ~func() either
// Deprecated:
func ChainReaderK[GEA ~func(R) GIOA, GEB ~func(R) GIOB, GIOA ~func() either.Either[E, A], GIOB ~func() either.Either[E, B], GB ~func(R) B, R, E, A, B any](f func(A) GB) func(GEA) GEB {
return FR.ChainReaderK(
MonadChain[GEA, GEB, GIOA, GIOB, R, E, A, B],
Chain[GEA, GEB, GIOA, GIOB, R, E, A, B],
FromReader[GB, GEB, GIOB, R, E, B],
f,
)
@@ -180,7 +180,7 @@ func MonadChainReaderIOK[GEA ~func(R) GIOEA, GEB ~func(R) GIOEB, GIOEA ~func() e
// Deprecated:
func ChainReaderIOK[GEA ~func(R) GIOEA, GEB ~func(R) GIOEB, GIOEA ~func() either.Either[E, A], GIOEB ~func() either.Either[E, B], GIOB ~func() B, GB ~func(R) GIOB, R, E, A, B any](f func(A) GB) func(GEA) GEB {
return FR.ChainReaderK(
MonadChain[GEA, GEB, GIOEA, GIOEB, R, E, A, B],
Chain[GEA, GEB, GIOEA, GIOEB, R, E, A, B],
RightReaderIO[GEB, GIOEB, GB, GIOB, R, E, B],
f,
)

View File

@@ -28,12 +28,18 @@ import (
"github.com/IBM/fp-go/v2/io"
IOE "github.com/IBM/fp-go/v2/ioeither"
L "github.com/IBM/fp-go/v2/lazy"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
RE "github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/readeroption"
)
//go:inline
func FromReaderOption[R, A, E any](onNone func() E) Kleisli[R, E, ReaderOption[R, A], A] {
return function.Bind2nd(function.Flow2[ReaderOption[R, A], IOE.Kleisli[E, Option[A], A]], IOE.FromOption[A](onNone))
}
//go:inline
func FromReaderIO[E, R, A any](ma ReaderIO[R, A]) ReaderIOEither[R, E, A] {
return RightReaderIO[E](ma)
}
@@ -116,7 +122,7 @@ func MonadChainFirst[R, E, A, B any](fa ReaderIOEither[R, E, A], f func(A) Reade
// The Either is automatically lifted into the ReaderIOEither context.
//
//go:inline
func MonadChainEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) either.Either[E, B]) ReaderIOEither[R, E, B] {
func MonadChainEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f either.Kleisli[E, A, B]) ReaderIOEither[R, E, B] {
return fromeither.MonadChainEitherK(
MonadChain[R, E, A, B],
FromEither[R, E, B],
@@ -129,7 +135,7 @@ func MonadChainEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) eit
// This is the curried version of MonadChainEitherK.
//
//go:inline
func ChainEitherK[R, E, A, B any](f func(A) either.Either[E, B]) Operator[R, E, A, B] {
func ChainEitherK[R, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, E, A, B] {
return fromeither.ChainEitherK(
Chain[R, E, A, B],
FromEither[R, E, B],
@@ -141,7 +147,7 @@ func ChainEitherK[R, E, A, B any](f func(A) either.Either[E, B]) Operator[R, E,
// Useful for validation or side effects that return Either.
//
//go:inline
func MonadChainFirstEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) either.Either[E, B]) ReaderIOEither[R, E, A] {
func MonadChainFirstEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f either.Kleisli[E, A, B]) ReaderIOEither[R, E, A] {
return fromeither.MonadChainFirstEitherK(
MonadChain[R, E, A, A],
MonadMap[R, E, B, A],
@@ -155,7 +161,7 @@ func MonadChainFirstEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A
// This is the curried version of MonadChainFirstEitherK.
//
//go:inline
func ChainFirstEitherK[R, E, A, B any](f func(A) either.Either[E, B]) Operator[R, E, A, A] {
func ChainFirstEitherK[R, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, E, A, A] {
return fromeither.ChainFirstEitherK(
Chain[R, E, A, A],
Map[R, E, B, A],
@@ -168,7 +174,7 @@ func ChainFirstEitherK[R, E, A, B any](f func(A) either.Either[E, B]) Operator[R
// The Reader is automatically lifted into the ReaderIOEither context.
//
//go:inline
func MonadChainReaderK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) Reader[R, B]) ReaderIOEither[R, E, B] {
func MonadChainReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader.Kleisli[R, A, B]) ReaderIOEither[R, E, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, E, A, B],
FromReader[E, R, B],
@@ -181,19 +187,147 @@ func MonadChainReaderK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) Rea
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainReaderK[E, R, A, B any](f func(A) Reader[R, B]) Operator[R, E, A, B] {
func ChainReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E, A, B] {
return fromreader.ChainReaderK(
MonadChain[R, E, A, B],
Chain[R, E, A, B],
FromReader[E, R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, E, A, B],
FromReader[E, R, B],
ma,
f,
)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOEither.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainFirstReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, E, A, B],
FromReader[E, R, B],
f,
)
}
//go:inline
func MonadChainReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, E, A, B],
FromReaderIO[E, R, B],
ma,
f,
)
}
//go:inline
func ChainReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, B] {
return fromreader.ChainReaderK(
Chain[R, E, A, B],
FromReaderIO[E, R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, E, A, B],
FromReaderIO[E, R, B],
ma,
f,
)
}
//go:inline
func ChainFirstReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, E, A, B],
FromReaderIO[E, R, B],
f,
)
}
//go:inline
func MonadChainReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, E, A, B],
FromReaderEither[R, E, B],
ma,
f,
)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOEither.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, E, A, B] {
return fromreader.ChainReaderK(
Chain[R, E, A, B],
FromReaderEither[R, E, B],
f,
)
}
//go:inline
func MonadChainFirstReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, E, A, B],
FromReaderEither[R, E, B],
ma,
f,
)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOEither.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainFirstReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, E, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, E, A, B],
FromReaderEither[R, E, B],
f,
)
}
//go:inline
func ChainReaderOptionK[R, A, B, E any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, B] {
fro := FromReaderOption[R, B](onNone)
return func(f readeroption.Kleisli[R, A, B]) Operator[R, E, A, B] {
return fromreader.ChainReaderK(
Chain[R, E, A, B],
fro,
f,
)
}
}
//go:inline
func ChainFirstReaderOptionK[R, A, B, E any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
fro := FromReaderOption[R, B](onNone)
return func(f readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, E, A, B],
fro,
f,
)
}
}
// MonadChainIOEitherK chains an IOEither-returning computation into a ReaderIOEither.
// The IOEither is automatically lifted into the ReaderIOEither context.
//
//go:inline
func MonadChainIOEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) IOE.IOEither[E, B]) ReaderIOEither[R, E, B] {
func MonadChainIOEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f IOE.Kleisli[E, A, B]) ReaderIOEither[R, E, B] {
return fromioeither.MonadChainIOEitherK(
MonadChain[R, E, A, B],
FromIOEither[R, E, B],
@@ -206,7 +340,7 @@ func MonadChainIOEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) I
// This is the curried version of MonadChainIOEitherK.
//
//go:inline
func ChainIOEitherK[R, E, A, B any](f func(A) IOE.IOEither[E, B]) Operator[R, E, A, B] {
func ChainIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, B] {
return fromioeither.ChainIOEitherK(
Chain[R, E, A, B],
FromIOEither[R, E, B],
@@ -218,7 +352,7 @@ func ChainIOEitherK[R, E, A, B any](f func(A) IOE.IOEither[E, B]) Operator[R, E,
// The IO is automatically lifted into the ReaderIOEither context (always succeeds).
//
//go:inline
func MonadChainIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) io.IO[B]) ReaderIOEither[R, E, B] {
func MonadChainIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f io.Kleisli[A, B]) ReaderIOEither[R, E, B] {
return fromio.MonadChainIOK(
MonadChain[R, E, A, B],
FromIO[R, E, B],
@@ -231,7 +365,7 @@ func MonadChainIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) io.IO[B
// This is the curried version of MonadChainIOK.
//
//go:inline
func ChainIOK[R, E, A, B any](f func(A) io.IO[B]) Operator[R, E, A, B] {
func ChainIOK[R, E, A, B any](f io.Kleisli[A, B]) Operator[R, E, A, B] {
return fromio.ChainIOK(
Chain[R, E, A, B],
FromIO[R, E, B],
@@ -243,7 +377,7 @@ func ChainIOK[R, E, A, B any](f func(A) io.IO[B]) Operator[R, E, A, B] {
// Useful for performing IO side effects while preserving the original value.
//
//go:inline
func MonadChainFirstIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) io.IO[B]) ReaderIOEither[R, E, A] {
func MonadChainFirstIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f io.Kleisli[A, B]) ReaderIOEither[R, E, A] {
return fromio.MonadChainFirstIOK(
MonadChain[R, E, A, A],
MonadMap[R, E, B, A],
@@ -257,7 +391,7 @@ func MonadChainFirstIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f func(A) io
// This is the curried version of MonadChainFirstIOK.
//
//go:inline
func ChainFirstIOK[R, E, A, B any](f func(A) io.IO[B]) Operator[R, E, A, A] {
func ChainFirstIOK[R, E, A, B any](f io.Kleisli[A, B]) Operator[R, E, A, A] {
return fromio.ChainFirstIOK(
Chain[R, E, A, A],
Map[R, E, B, A],
@@ -270,7 +404,7 @@ func ChainFirstIOK[R, E, A, B any](f func(A) io.IO[B]) Operator[R, E, A, A] {
// If the Option is None, the provided error function is called to produce the error value.
//
//go:inline
func ChainOptionK[R, A, B, E any](onNone func() E) func(func(A) O.Option[B]) Operator[R, E, A, B] {
func ChainOptionK[R, A, B, E any](onNone func() E) func(func(A) Option[B]) Operator[R, E, A, B] {
return fromeither.ChainOptionK(
MonadChain[R, E, A, B],
FromEither[R, E, B],
@@ -400,18 +534,18 @@ func FromReader[E, R, A any](ma Reader[R, A]) ReaderIOEither[R, E, A] {
}
// RightIO lifts an IO into a ReaderIOEither, placing the result in the Right side.
func RightIO[R, E, A any](ma io.IO[A]) ReaderIOEither[R, E, A] {
func RightIO[R, E, A any](ma IO[A]) ReaderIOEither[R, E, A] {
return function.Pipe2(ma, IOE.RightIO[E, A], FromIOEither[R, E, A])
}
// LeftIO lifts an IO into a ReaderIOEither, placing the result in the Left (error) side.
func LeftIO[R, A, E any](ma io.IO[E]) ReaderIOEither[R, E, A] {
func LeftIO[R, A, E any](ma IO[E]) ReaderIOEither[R, E, A] {
return function.Pipe2(ma, IOE.LeftIO[A, E], FromIOEither[R, E, A])
}
// FromIO lifts an IO into a ReaderIOEither context.
// The IO result is placed in the Right side (success).
func FromIO[R, E, A any](ma io.IO[A]) ReaderIOEither[R, E, A] {
func FromIO[R, E, A any](ma IO[A]) ReaderIOEither[R, E, A] {
return RightIO[R, E](ma)
}
@@ -419,7 +553,7 @@ func FromIO[R, E, A any](ma io.IO[A]) ReaderIOEither[R, E, A] {
// The computation becomes independent of any reader context.
//
//go:inline
func FromIOEither[R, E, A any](ma IOE.IOEither[E, A]) ReaderIOEither[R, E, A] {
func FromIOEither[R, E, A any](ma IOEither[E, A]) ReaderIOEither[R, E, A] {
return reader.Of[R](ma)
}
@@ -449,7 +583,7 @@ func Asks[E, R, A any](r Reader[R, A]) ReaderIOEither[R, E, A] {
// If the Option is None, the provided function is called to produce the error.
//
//go:inline
func FromOption[R, A, E any](onNone func() E) func(O.Option[A]) ReaderIOEither[R, E, A] {
func FromOption[R, A, E any](onNone func() E) func(Option[A]) ReaderIOEither[R, E, A] {
return fromeither.FromOption(FromEither[R, E, A], onNone)
}
@@ -616,3 +750,8 @@ func MapLeft[R, A, E1, E2 any](f func(E1) E2) func(ReaderIOEither[R, E1, A]) Rea
func Local[E, A, R1, R2 any](f func(R2) R1) func(ReaderIOEither[R1, E, A]) ReaderIOEither[R2, E, A] {
return reader.Local[IOEither[E, A]](f)
}
//go:inline
func Read[E, A, R any](r R) func(ReaderIOEither[R, E, A]) IOEither[E, A] {
return reader.Read[IOEither[E, A]](r)
}

View File

@@ -19,8 +19,10 @@ import (
"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/optics/lens/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/readeroption"
)
type (
@@ -84,4 +86,7 @@ type (
// Example:
// var doubleOp Operator[Config, error, int, int] = Map(func(x int) int { return x * 2 })
Operator[R, E, A, B any] = Kleisli[R, E, ReaderIOEither[R, E, A], B]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
Option[A any] = option.Option[A]
)

View File

@@ -56,5 +56,3 @@ func TestApS(t *testing.T) {
assert.Equal(t, res(context.Background())(), result.Of("John Doe"))
}
// Made with Bob

View File

@@ -16,12 +16,22 @@
package readerioresult
import (
"github.com/IBM/fp-go/v2/reader"
RE "github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
"github.com/IBM/fp-go/v2/readeroption"
)
//go:inline
func FromReaderOption[R, A any](onNone func() error) Kleisli[R, ReaderOption[R, A], A] {
return RIOE.FromReaderOption[R, A](onNone)
}
// FromReaderIO creates a function that lifts a ReaderIO-producing function into ReaderIOResult.
// The ReaderIO result is placed in the Right side of the Either.
//
//go:inline
func FromReaderIO[R, A any](ma ReaderIO[R, A]) ReaderIOResult[R, A] {
return RIOE.FromReaderIO[error](ma)
}
@@ -158,7 +168,7 @@ func ChainFirstResultK[R, A, B any](f func(A) Result[B]) Operator[R, A, A] {
// The Reader is automatically lifted into the ReaderIOResult context.
//
//go:inline
func MonadChainReaderK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Reader[R, B]) ReaderIOResult[R, B] {
func MonadChainReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainReaderK(ma, f)
}
@@ -166,10 +176,99 @@ func MonadChainReaderK[R, A, B any](ma ReaderIOResult[R, A], f func(A) Reader[R,
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainReaderK[R, A, B any](f func(A) Reader[R, B]) Operator[R, A, B] {
func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderK[error](f)
}
//go:inline
func MonadChainFirstReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderK(ma, f)
}
//go:inline
func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderK[error](f)
}
//go:inline
func ChainReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderOptionK[R, A, B](onNone)
}
//go:inline
func ChainFirstReaderOptionK[R, A, B any](onNone func() error) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderOptionK[R, A, B](onNone)
}
// MonadChainReaderK chains a Reader-returning computation into a ReaderIOResult.
// The Reader is automatically lifted into the ReaderIOResult context.
//
//go:inline
func MonadChainReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainReaderEitherK(ma, f)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderEitherK(f)
}
//go:inline
func MonadChainFirstReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderEitherK(ma, f)
}
//go:inline
func ChainFirstReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderEitherK(f)
}
//go:inline
func MonadChainReaderResultK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainReaderEitherK(ma, f)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainReaderResultK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderEitherK(f)
}
//go:inline
func MonadChainFirstReaderResultK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderEitherK(ma, f)
}
//go:inline
func ChainFirstReaderResultK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderEitherK(f)
}
//go:inline
func MonadChainReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainReaderIOK(ma, f)
}
//go:inline
func ChainReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderIOK[error](f)
}
//go:inline
func MonadChainFirstReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderIOK[error](f)
}
// MonadChainIOEitherK chains an IOEither-returning computation into a ReaderIOResult.
// The IOEither is automatically lifted into the ReaderIOResult context.
//
@@ -578,3 +677,8 @@ func MapLeft[R, A, E any](f func(error) E) func(ReaderIOResult[R, A]) RIOE.Reade
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, A] {
return RIOE.Local[error, A](f)
}
//go:inline
func Read[A, R any](r R) func(ReaderIOResult[R, A]) IOResult[A] {
return RIOE.Read[error, A](r)
}

View File

@@ -24,6 +24,7 @@ import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
)
@@ -42,6 +43,8 @@ type (
// side effects to produce a value of type A.
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// IOEither represents a computation that performs side effects and can either
// fail with an error of type E or succeed with a value of type A.
IOEither[E, A any] = ioeither.IOEither[E, A]

88
v2/readeroption/array.go Normal file
View File

@@ -0,0 +1,88 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeroption
import (
G "github.com/IBM/fp-go/v2/readeroption/generic"
)
// TraverseArray transforms an array by applying a function that returns a ReaderOption to each element.
// If any element results in None, the entire result is None.
// Otherwise, returns Some containing an array of all the unwrapped values.
//
// This is useful for performing a sequence of operations that may fail on each element of an array,
// where you want all operations to succeed or the entire computation to fail.
//
// Example:
//
// type DB struct { ... }
//
// findUser := func(id int) readeroption.ReaderOption[DB, User] { ... }
//
// userIDs := []int{1, 2, 3}
// result := F.Pipe1(
// readeroption.Of[DB](userIDs),
// readeroption.Chain(readeroption.TraverseArray[DB](findUser)),
// )
// // result will be Some([]User) if all users are found, None otherwise
func TraverseArray[E, A, B any](f func(A) ReaderOption[E, B]) Kleisli[E, []A, []B] {
return G.TraverseArray[ReaderOption[E, B], ReaderOption[E, []B], []A](f)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the index of each element.
//
// Example:
//
// type DB struct { ... }
//
// processWithIndex := func(idx int, value string) readeroption.ReaderOption[DB, Result] {
// // Use idx in processing
// return readeroption.Asks(func(db DB) option.Option[Result] { ... })
// }
//
// values := []string{"a", "b", "c"}
// result := readeroption.TraverseArrayWithIndex[DB](processWithIndex)(values)
func TraverseArrayWithIndex[E, A, B any](f func(int, A) ReaderOption[E, B]) func([]A) ReaderOption[E, []B] {
return G.TraverseArrayWithIndex[ReaderOption[E, B], ReaderOption[E, []B], []A](f)
}
// SequenceArray converts an array of ReaderOption values into a ReaderOption of an array.
// If any element is None, the entire result is None.
// Otherwise, returns Some containing an array of all the unwrapped values.
//
// This is useful when you have multiple independent ReaderOption computations and want to
// combine their results into a single array.
//
// Example:
//
// type Config struct { ... }
//
// user1 := readeroption.Of[Config](User{ID: 1, Name: "Alice"})
// user2 := readeroption.Of[Config](User{ID: 2, Name: "Bob"})
// user3 := readeroption.None[Config, User]()
//
// result := readeroption.SequenceArray([]readeroption.ReaderOption[Config, User]{
// user1, user2, user3,
// })
// // result(config) will be option.None[[]User]() because user3 is None
//
// result2 := readeroption.SequenceArray([]readeroption.ReaderOption[Config, User]{
// user1, user2,
// })
// // result2(config) will be option.Some([]User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}})
func SequenceArray[E, A any](ma []ReaderOption[E, A]) ReaderOption[E, []A] {
return G.SequenceArray[ReaderOption[E, A], ReaderOption[E, []A]](ma)
}

View File

@@ -0,0 +1,107 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeroption
import (
"context"
"testing"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestSequenceArray(t *testing.T) {
n := 10
readers := A.MakeBy(n, Of[context.Context, int])
exp := O.Of(A.MakeBy(n, F.Identity[int]))
g := F.Pipe1(
readers,
SequenceArray[context.Context, int],
)
assert.Equal(t, exp, g(context.Background()))
}
func TestTraverseArray(t *testing.T) {
// Function that doubles a number if it's positive
doubleIfPositive := func(x int) ReaderOption[context.Context, int] {
if x > 0 {
return Of[context.Context](x * 2)
}
return None[context.Context, int]()
}
// Test with all positive numbers
input1 := []int{1, 2, 3}
g1 := F.Pipe1(
Of[context.Context](input1),
Chain(TraverseArray(doubleIfPositive)),
)
assert.Equal(t, O.Of([]int{2, 4, 6}), g1(context.Background()))
// Test with a negative number (should return None)
input2 := []int{1, -2, 3}
g2 := F.Pipe1(
Of[context.Context](input2),
Chain(TraverseArray(doubleIfPositive)),
)
assert.Equal(t, O.None[[]int](), g2(context.Background()))
// Test with empty array
input3 := []int{}
g3 := F.Pipe1(
Of[context.Context](input3),
Chain(TraverseArray(doubleIfPositive)),
)
assert.Equal(t, O.Of([]int{}), g3(context.Background()))
}
func TestTraverseArrayWithIndex(t *testing.T) {
// Function that multiplies value by its index if index is even
multiplyByIndexIfEven := func(idx int, x int) ReaderOption[context.Context, int] {
if idx%2 == 0 {
return Of[context.Context](x * idx)
}
return Of[context.Context](x)
}
input := []int{10, 20, 30, 40}
g := TraverseArrayWithIndex(multiplyByIndexIfEven)(input)
// Expected: [10*0, 20, 30*2, 40] = [0, 20, 60, 40]
assert.Equal(t, O.Of([]int{0, 20, 60, 40}), g(context.Background()))
}
func TestTraverseArrayWithIndexNone(t *testing.T) {
// Function that returns None for odd indices
noneForOdd := func(idx int, x int) ReaderOption[context.Context, int] {
if idx%2 == 0 {
return Of[context.Context](x)
}
return None[context.Context, int]()
}
input := []int{10, 20, 30}
g := TraverseArrayWithIndex(noneForOdd)(input)
// Should return None because index 1 returns None
assert.Equal(t, O.None[[]int](), g(context.Background()))
}

303
v2/readeroption/bind.go Normal file
View File

@@ -0,0 +1,303 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeroption
import (
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
G "github.com/IBM/fp-go/v2/readeroption/generic"
)
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
// result := readereither.Do[Env, error](State{})
func Do[R, S any](
empty S,
) ReaderOption[R, S] {
return G.Do[ReaderOption[R, S]](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access the shared environment.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readereither.ReaderOption[Env, error, User] {
// return readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// },
// ),
// readereither.Bind(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) readereither.ReaderOption[Env, error, Config] {
// // This can access s.User from the previous step
// return readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfigForUser(s.User.ID)
// })
// },
// ),
// )
func Bind[R, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[R, S1, T],
) func(ReaderOption[R, S1]) ReaderOption[R, S2] {
return G.Bind[ReaderOption[R, S1], ReaderOption[R, S2]](setter, f)
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
func Let[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(ReaderOption[R, S1]) ReaderOption[R, S2] {
return G.Let[ReaderOption[R, S1], ReaderOption[R, S2]](setter, f)
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
func LetTo[R, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(ReaderOption[R, S1]) ReaderOption[R, S2] {
return G.LetTo[ReaderOption[R, S1], ReaderOption[R, S2]](setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[R, S1, T any](
setter func(T) S1,
) func(ReaderOption[R, T]) ReaderOption[R, S1] {
return G.BindTo[ReaderOption[R, S1], ReaderOption[R, T]](setter)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
// the context and the value concurrently (using Applicative rather than Monad).
// This allows independent computations to be combined without one depending on the result of the other.
//
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
// and can conceptually run in parallel.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// // These operations are independent and can be combined with ApS
// getUser := readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfig()
// })
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.ApS(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// readereither.ApS(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// getConfig,
// ),
// )
func ApS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderOption[R, T],
) func(ReaderOption[R, S1]) ReaderOption[R, S2] {
return G.ApS[ReaderOption[R, S1], ReaderOption[R, S2]](setter, fa)
}
// 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 State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// configLens := lens.MakeLens(
// func(s State) Config { return s.Config },
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfig()
// })
// result := F.Pipe2(
// readereither.Of[Env, error](State{}),
// readereither.ApSL(configLens, getConfig),
// )
func ApSL[R, S, T any](
lens L.Lens[S, T],
fa ReaderOption[R, T],
) func(ReaderOption[R, S]) ReaderOption[R, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a ReaderOption computation that produces an updated value.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.BindL(userLens, func(user User) readereither.ReaderOption[Env, error, User] {
// return readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// }),
// )
func BindL[R, S, T any](
lens L.Lens[S, T],
f Kleisli[R, T, T],
) func(ReaderOption[R, S]) ReaderOption[R, S] {
return Bind(lens.Set, F.Flow2(lens.Get, f))
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new value (without wrapping in a ReaderOption).
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// configLens := lens.MakeLens(
// func(s State) Config { return s.Config },
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// result := F.Pipe2(
// readereither.Do[any, error](State{Config: Config{Host: "localhost"}}),
// readereither.LetL(configLens, func(cfg Config) Config {
// cfg.Port = 8080
// return cfg
// }),
// )
func LetL[R, S, T any](
lens L.Lens[S, T],
f func(T) T,
) func(ReaderOption[R, S]) ReaderOption[R, S] {
return Let[R](lens.Set, F.Flow2(lens.Get, f))
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The value b is set directly to the focused field.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// configLens := lens.MakeLens(
// func(s State) Config { return s.Config },
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// newConfig := Config{Host: "localhost", Port: 8080}
// result := F.Pipe2(
// readereither.Do[any, error](State{}),
// readereither.LetToL(configLens, newConfig),
// )
func LetToL[R, S, T any](
lens L.Lens[S, T],
b T,
) func(ReaderOption[R, S]) ReaderOption[R, S] {
return LetTo[R](lens.Set, b)
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeroption
import (
"context"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func getLastName(s utils.Initial) ReaderOption[context.Context, string] {
return Of[context.Context]("Doe")
}
func getGivenName(s utils.WithLastName) ReaderOption[context.Context, string] {
return Of[context.Context]("John")
}
func TestBind(t *testing.T) {
res := F.Pipe3(
Do[context.Context](utils.Empty),
Bind(utils.SetLastName, getLastName),
Bind(utils.SetGivenName, getGivenName),
Map[context.Context](utils.GetFullName),
)
assert.Equal(t, res(context.Background()), O.Of("John Doe"))
}
func TestApS(t *testing.T) {
res := F.Pipe3(
Do[context.Context](utils.Empty),
ApS(utils.SetLastName, Of[context.Context]("Doe")),
ApS(utils.SetGivenName, Of[context.Context]("John")),
Map[context.Context](utils.GetFullName),
)
assert.Equal(t, res(context.Background()), O.Of("John Doe"))
}
func TestLet(t *testing.T) {
res := F.Pipe3(
Do[context.Context](utils.Empty),
Let[context.Context](utils.SetLastName, func(s utils.Initial) string {
return "Doe"
}),
Let[context.Context](utils.SetGivenName, func(s utils.WithLastName) string {
return "John"
}),
Map[context.Context](utils.GetFullName),
)
assert.Equal(t, res(context.Background()), O.Of("John Doe"))
}
func TestLetTo(t *testing.T) {
res := F.Pipe3(
Do[context.Context](utils.Empty),
LetTo[context.Context](utils.SetLastName, "Doe"),
LetTo[context.Context](utils.SetGivenName, "John"),
Map[context.Context](utils.GetFullName),
)
assert.Equal(t, res(context.Background()), O.Of("John Doe"))
}
func TestBindTo(t *testing.T) {
type State struct {
Value int
}
res := F.Pipe1(
Of[context.Context](42),
BindTo[context.Context](func(v int) State {
return State{Value: v}
}),
)
assert.Equal(t, res(context.Background()), O.Of(State{Value: 42}))
}

113
v2/readeroption/curry.go Normal file
View File

@@ -0,0 +1,113 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeroption
import (
G "github.com/IBM/fp-go/v2/readeroption/generic"
)
// Curry functions convert Go functions that take a context as the first parameter
// and return (value, bool) into curried ReaderOption functions.
//
// This follows the Go convention of passing context as the first parameter
// (see https://pkg.go.dev/context), while providing a functional programming interface.
//
// The bool return value indicates success (true) or failure (false), which maps to
// Some or None in the Option monad.
// Curry0 converts a function that takes only a context and returns (A, bool)
// into a ReaderOption[R, A].
//
// Example:
//
// getConfig := func(ctx context.Context) (Config, bool) {
// cfg, ok := ctx.Value("config").(Config)
// return cfg, ok
// }
// ro := readeroption.Curry0(getConfig)
// result := ro(ctx) // Returns option.Some(config) or option.None()
func Curry0[R, A any](f func(R) (A, bool)) ReaderOption[R, A] {
return G.Curry0[ReaderOption[R, A]](f)
}
// Curry1 converts a function that takes a context and one argument, returning (A, bool),
// into a curried function that returns a ReaderOption.
//
// Example:
//
// findUser := func(ctx context.Context, id int) (User, bool) {
// // Query database using context
// return user, found
// }
// ro := readeroption.Curry1(findUser)
// result := ro(123)(ctx) // Returns option.Some(user) or option.None()
func Curry1[R, T1, A any](f func(R, T1) (A, bool)) Kleisli[R, T1, A] {
return G.Curry1[ReaderOption[R, A]](f)
}
// Curry2 converts a function that takes a context and two arguments, returning (A, bool),
// into a curried function that returns a ReaderOption.
//
// Example:
//
// query := func(ctx context.Context, table string, id int) (Record, bool) {
// // Query database using context
// return record, found
// }
// ro := readeroption.Curry2(query)
// result := ro("users")(123)(ctx) // Returns option.Some(record) or option.None()
func Curry2[R, T1, T2, A any](f func(R, T1, T2) (A, bool)) func(T1) func(T2) ReaderOption[R, A] {
return G.Curry2[ReaderOption[R, A]](f)
}
// Curry3 converts a function that takes a context and three arguments, returning (A, bool),
// into a curried function that returns a ReaderOption.
//
// Example:
//
// complexQuery := func(ctx context.Context, db string, table string, id int) (Record, bool) {
// // Query database using context
// return record, found
// }
// ro := readeroption.Curry3(complexQuery)
// result := ro("mydb")("users")(123)(ctx)
func Curry3[R, T1, T2, T3, A any](f func(R, T1, T2, T3) (A, bool)) func(T1) func(T2) func(T3) ReaderOption[R, A] {
return G.Curry3[ReaderOption[R, A]](f)
}
// Uncurry1 converts a curried ReaderOption function back to a Go function
// that takes a context and one argument, returning (A, bool).
//
// Example:
//
// ro := func(id int) readeroption.ReaderOption[context.Context, User] { ... }
// findUser := readeroption.Uncurry1(ro)
// user, found := findUser(ctx, 123)
func Uncurry1[R, T1, A any](f func(T1) ReaderOption[R, A]) func(R, T1) (A, bool) {
return G.Uncurry1(f)
}
// Uncurry2 converts a curried ReaderOption function back to a Go function
// that takes a context and two arguments, returning (A, bool).
func Uncurry2[R, T1, T2, A any](f func(T1) func(T2) ReaderOption[R, A]) func(R, T1, T2) (A, bool) {
return G.Uncurry2(f)
}
// Uncurry3 converts a curried ReaderOption function back to a Go function
// that takes a context and three arguments, returning (A, bool).
func Uncurry3[R, T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderOption[R, A]) func(R, T1, T2, T3) (A, bool) {
return G.Uncurry3(f)
}

View File

@@ -0,0 +1,162 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeroption
import (
"context"
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestCurry0(t *testing.T) {
// Function that returns a value from context
getConfig := func(ctx context.Context) (string, bool) {
if val := ctx.Value("config"); val != nil {
return val.(string), true
}
return "", false
}
ro := Curry0(getConfig)
// Test with value in context
ctx1 := context.WithValue(context.Background(), "config", "test-config")
result1 := ro(ctx1)
assert.Equal(t, O.Of("test-config"), result1)
// Test without value in context
ctx2 := context.Background()
result2 := ro(ctx2)
assert.Equal(t, O.None[string](), result2)
}
func TestCurry1(t *testing.T) {
// Function that looks up a value by key
lookup := func(ctx context.Context, key string) (int, bool) {
if val := ctx.Value(key); val != nil {
return val.(int), true
}
return 0, false
}
ro := Curry1(lookup)
// Test with value in context
ctx1 := context.WithValue(context.Background(), "count", 42)
result1 := ro("count")(ctx1)
assert.Equal(t, O.Of(42), result1)
// Test without value in context
ctx2 := context.Background()
result2 := ro("count")(ctx2)
assert.Equal(t, O.None[int](), result2)
}
func TestCurry2(t *testing.T) {
// Function that combines two parameters with context
combine := func(ctx context.Context, a string, b int) (string, bool) {
if ctx.Value("enabled") == true {
return a + ":" + string(rune('0'+b)), true
}
return "", false
}
ro := Curry2(combine)
// Test with enabled context
ctx1 := context.WithValue(context.Background(), "enabled", true)
result1 := ro("test")(5)(ctx1)
assert.Equal(t, O.Of("test:5"), result1)
// Test with disabled context
ctx2 := context.Background()
result2 := ro("test")(5)(ctx2)
assert.Equal(t, O.None[string](), result2)
}
func TestCurry3(t *testing.T) {
// Function that combines three parameters with context
combine := func(ctx context.Context, a string, b int, c bool) (string, bool) {
if ctx.Value("enabled") == true && c {
return a + ":" + string(rune('0'+b)), true
}
return "", false
}
ro := Curry3(combine)
// Test with enabled context and true flag
ctx1 := context.WithValue(context.Background(), "enabled", true)
result1 := ro("test")(5)(true)(ctx1)
assert.Equal(t, O.Of("test:5"), result1)
// Test with false flag
result2 := ro("test")(5)(false)(ctx1)
assert.Equal(t, O.None[string](), result2)
}
func TestUncurry1(t *testing.T) {
// Create a curried function
curried := func(x int) ReaderOption[context.Context, int] {
return Of[context.Context](x * 2)
}
// Uncurry it
uncurried := Uncurry1(curried)
// Test the uncurried function
result, ok := uncurried(context.Background(), 21)
assert.True(t, ok)
assert.Equal(t, 42, result)
}
func TestUncurry2(t *testing.T) {
// Create a curried function
curried := func(x int) func(y int) ReaderOption[context.Context, int] {
return func(y int) ReaderOption[context.Context, int] {
return Of[context.Context](x + y)
}
}
// Uncurry it
uncurried := Uncurry2(curried)
// Test the uncurried function
result, ok := uncurried(context.Background(), 10, 32)
assert.True(t, ok)
assert.Equal(t, 42, result)
}
func TestUncurry3(t *testing.T) {
// Create a curried function
curried := func(x int) func(y int) func(z int) ReaderOption[context.Context, int] {
return func(y int) func(z int) ReaderOption[context.Context, int] {
return func(z int) ReaderOption[context.Context, int] {
return Of[context.Context](x + y + z)
}
}
}
// Uncurry it
uncurried := Uncurry3(curried)
// Test the uncurried function
result, ok := uncurried(context.Background(), 10, 20, 12)
assert.True(t, ok)
assert.Equal(t, 42, result)
}

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