mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-09 23:11:40 +02:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3007cbbfa | ||
|
|
5aa0e1ea2e | ||
|
|
d586428cb0 | ||
|
|
d2dbce6e8b | ||
|
|
6f7ec0768d | ||
|
|
ca813b673c | ||
|
|
af271e7d10 | ||
|
|
567315a31c | ||
|
|
311ed55f06 | ||
|
|
23333ce52c | ||
|
|
eb7fc9f77b | ||
|
|
fd0550e71b | ||
|
|
13063bbd88 |
240
v2/README.md
240
v2/README.md
@@ -2,25 +2,152 @@
|
||||
|
||||
[](https://pkg.go.dev/github.com/IBM/fp-go/v2)
|
||||
[](https://coveralls.io/github/IBM/fp-go?branch=main)
|
||||
[](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**
|
||||
@@ -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}}]
|
||||
|
||||
}
|
||||
|
||||
@@ -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
7
v2/assert/types.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package assert
|
||||
|
||||
import "github.com/IBM/fp-go/v2/result"
|
||||
|
||||
type (
|
||||
Result[T any] = result.Result[T]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
4
v2/bytes/coverage.out
Normal 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
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
417
v2/cli/lens.go
417
v2/cli/lens.go
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
8340
v2/coverage.out
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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))),
|
||||
)
|
||||
|
||||
40
v2/di/doc.go
40
v2/di/doc.go
@@ -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))
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
13
v2/di/erasure/types.go
Normal 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]
|
||||
)
|
||||
3181
v2/di/gen.go
3181
v2/di/gen.go
File diff suppressed because it is too large
Load Diff
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)())
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
15
v2/di/types.go
Normal 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]
|
||||
)
|
||||
@@ -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)),
|
||||
|
||||
@@ -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")))))
|
||||
}
|
||||
|
||||
@@ -648,5 +648,3 @@ func BenchmarkString_Left(b *testing.B) {
|
||||
benchString = left.String()
|
||||
}
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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]())
|
||||
}
|
||||
|
||||
@@ -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]]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
158
v2/eq/eq.go
158
v2/eq/eq.go
@@ -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)
|
||||
|
||||
120
v2/eq/monoid.go
120
v2/eq/monoid.go
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
87
v2/internal/fromoption/option.go
Normal file
87
v2/internal/fromoption/option.go
Normal 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))
|
||||
// }
|
||||
28
v2/internal/fromoption/types.go
Normal file
28
v2/internal/fromoption/types.go
Normal 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
|
||||
}
|
||||
)
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
267
v2/lazy/doc.go
Normal 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
|
||||
101
v2/lazy/lazy.go
101
v2/lazy/lazy.go
@@ -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
|
||||
|
||||
503
v2/lazy/lazy_extended_test.go
Normal file
503
v2/lazy/lazy_extended_test.go
Normal 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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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
233
v2/optics/iso/lens/doc.go
Normal 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
|
||||
@@ -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])))
|
||||
}
|
||||
|
||||
401
v2/optics/iso/lens/lens_test.go
Normal file
401
v2/optics/iso/lens/lens_test.go
Normal 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
|
||||
11
v2/optics/iso/lens/types.go
Normal file
11
v2/optics/iso/lens/types.go
Normal 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
305
v2/optics/iso/option/doc.go
Normal 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
|
||||
@@ -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
366
v2/optics/lens/iso/doc.go
Normal 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
|
||||
@@ -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],
|
||||
|
||||
14
v2/optics/lens/iso/types.go
Normal file
14
v2/optics/lens/iso/types.go
Normal 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]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
640
v2/optics/lens/lens_laws_test.go
Normal file
640
v2/optics/lens/lens_laws_test.go
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
481
v2/optics/lens/option/from_test.go
Normal file
481
v2/optics/lens/option/from_test.go
Normal 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
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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
479
v2/optics/optional/doc.go
Normal 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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
495
v2/optics/traversal/doc.go
Normal 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
|
||||
@@ -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 (
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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
120
v2/predicate/predicates.go
Normal 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]())
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -56,5 +56,3 @@ func TestApS(t *testing.T) {
|
||||
|
||||
assert.Equal(t, res(context.Background())(), result.Of("John Doe"))
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
88
v2/readeroption/array.go
Normal 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)
|
||||
}
|
||||
107
v2/readeroption/array_test.go
Normal file
107
v2/readeroption/array_test.go
Normal 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
303
v2/readeroption/bind.go
Normal 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)
|
||||
}
|
||||
99
v2/readeroption/bind_test.go
Normal file
99
v2/readeroption/bind_test.go
Normal 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
113
v2/readeroption/curry.go
Normal 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)
|
||||
}
|
||||
162
v2/readeroption/curry_test.go
Normal file
162
v2/readeroption/curry_test.go
Normal 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
Reference in New Issue
Block a user