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

Compare commits

...

8 Commits

Author SHA1 Message Date
Dr. Carsten Leue
ca813b673c fix: better tests and doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 16:24:12 +01:00
Dr. Carsten Leue
af271e7d10 fix: better endo and lens
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 15:03:55 +01:00
Dr. Carsten Leue
567315a31c fix: make a distinction between Chain and Compose for endomorphism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 13:51:00 +01:00
Dr. Carsten Leue
311ed55f06 fix: add Read method to Readers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 11:59:20 +01:00
Dr. Carsten Leue
23333ce52c doc: improve doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 11:08:18 +01:00
Dr. Carsten Leue
eb7fc9f77b fix: better tests for Lazy
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 10:46:07 +01:00
Dr. Carsten Leue
fd0550e71b fix: better test coverage
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 10:35:53 +01:00
Dr. Carsten Leue
13063bbd88 fix: doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 16:36:12 +01:00
102 changed files with 19572 additions and 3004 deletions

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

@@ -19,13 +19,17 @@ import (
"context"
"time"
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
)
const (
@@ -747,3 +751,93 @@ func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOResult[A]) Re
func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] {
return RIOR.OrLeft[A](onLeft)
}
//go:inline
func FromReaderEither[A any](ma ReaderEither[context.Context, error, A]) ReaderIOResult[A] {
return RIOR.FromReaderEither(ma)
}
//go:inline
func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
return RIOR.FromReaderEither(ma)
}
//go:inline
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
return RIOR.FromReaderOption[context.Context, A](onNone)
}
//go:inline
func MonadChainReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderK(ma, f)
}
//go:inline
func ChainReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderK(f)
}
//go:inline
func MonadChainFirstReaderK[A, B any](ma ReaderIOResult[A], f reader.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderK(ma, f)
}
//go:inline
func ChainFirstReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderK(f)
}
//go:inline
func MonadChainReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderResultK(ma, f)
}
//go:inline
func ChainReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainReaderResultK(f)
}
//go:inline
func MonadChainFirstReaderResultK[A, B any](ma ReaderIOResult[A], f readerresult.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderResultK(ma, f)
}
//go:inline
func ChainFirstReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderResultK(f)
}
//go:inline
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderIOK(ma, f)
}
//go:inline
func ChainReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderIOK(f)
}
//go:inline
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderIOK(f)
}
//go:inline
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.Read[A](r)
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -19,14 +19,17 @@ import (
"context"
"github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
)
@@ -119,4 +122,8 @@ type (
// // Apply the transformation
// result := toUpper(computation)
Operator[A, B any] = Kleisli[ReaderIOResult[A], B]
ReaderResult[A any] = readerresult.ReaderResult[A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
)

View File

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

View File

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

8340
v2/coverage.out Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,115 +17,290 @@ package endomorphism
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity"
)
// MonadAp applies an endomorphism to a value in a monadic context.
// MonadAp applies an endomorphism in a function to an endomorphism value.
//
// This function applies the endomorphism fab to the value fa, returning the result.
// It's the monadic application operation for endomorphisms.
// For endomorphisms, Ap composes two endomorphisms using RIGHT-TO-LEFT composition.
// This is the applicative functor operation for endomorphisms.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT (same as MonadCompose):
// - fa is applied first to the input
// - fab is applied to the result
//
// Parameters:
// - fab: An endomorphism to apply
// - fa: The value to apply the endomorphism to
// - fab: An endomorphism to apply (outer function)
// - fa: An endomorphism to apply first (inner function)
//
// Returns:
// - The result of applying fab to fa
//
// Example:
//
// double := func(x int) int { return x * 2 }
// result := endomorphism.MonadAp(double, 5) // Returns: 10
func MonadAp[A any](fab Endomorphism[A], fa A) A {
return identity.MonadAp(fab, fa)
}
// Ap returns a function that applies a value to an endomorphism.
//
// This is the curried version of MonadAp. It takes a value and returns a function
// that applies that value to any endomorphism.
//
// Parameters:
// - fa: The value to be applied
//
// Returns:
// - A function that takes an endomorphism and applies fa to it
//
// Example:
//
// applyFive := endomorphism.Ap(5)
// double := func(x int) int { return x * 2 }
// result := applyFive(double) // Returns: 10
func Ap[A any](fa A) func(Endomorphism[A]) A {
return identity.Ap[A](fa)
}
// Compose composes two endomorphisms into a single endomorphism.
//
// Given two endomorphisms f1 and f2, Compose returns a new endomorphism that
// applies f1 first, then applies f2 to the result. This is function composition:
// Compose(f1, f2)(x) = f2(f1(x))
//
// Composition is associative: Compose(Compose(f, g), h) = Compose(f, Compose(g, h))
//
// Parameters:
// - f1: The first endomorphism to apply
// - f2: The second endomorphism to apply
//
// Returns:
// - A new endomorphism that is the composition of f1 and f2
// - A new endomorphism that applies fa, then fab
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// doubleAndIncrement := endomorphism.Compose(double, increment)
// result := doubleAndIncrement(5) // (5 * 2) + 1 = 11
func Compose[A any](f1, f2 Endomorphism[A]) Endomorphism[A] {
return function.Flow2(f1, f2)
// result := endomorphism.MonadAp(double, increment) // Composes: double increment
// // result(5) = double(increment(5)) = double(6) = 12
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
return MonadCompose(fab, fa)
}
// MonadChain chains two endomorphisms together.
// Ap returns a function that applies an endomorphism to another endomorphism.
//
// This is the monadic bind operation for endomorphisms. It composes two endomorphisms
// ma and f, returning a new endomorphism that applies ma first, then f.
// MonadChain is equivalent to Compose.
// This is the curried version of MonadAp. It takes an endomorphism fa and returns
// a function that composes any endomorphism with fa using RIGHT-TO-LEFT composition.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - fa is applied first to the input
// - The endomorphism passed to the returned function is applied to the result
//
// Parameters:
// - ma: The first endomorphism in the chain
// - f: The second endomorphism in the chain
// - fa: The first endomorphism to apply (inner function)
//
// Returns:
// - A new endomorphism that chains ma and f
// - A function that takes an endomorphism and composes it with fa (right-to-left)
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// applyIncrement := endomorphism.Ap(increment)
// double := func(x int) int { return x * 2 }
// composed := applyIncrement(double) // double ∘ increment
// // composed(5) = double(increment(5)) = double(6) = 12
func Ap[A any](fa Endomorphism[A]) Operator[A] {
return Compose(fa)
}
// MonadCompose composes two endomorphisms, executing them from right to left.
//
// MonadCompose creates a new endomorphism that applies f2 first, then f1.
// This follows the mathematical notation of function composition: (f1 ∘ f2)(x) = f1(f2(x))
//
// IMPORTANT: The execution order is RIGHT-TO-LEFT:
// - f2 is applied first to the input
// - f1 is applied to the result of f2
//
// This is different from Chain/MonadChain which executes LEFT-TO-RIGHT.
//
// Parameters:
// - f1: The second function to apply (outer function)
// - f2: The first function to apply (inner function)
//
// Returns:
// - A new endomorphism that applies f2, then f1
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
//
// // MonadCompose executes RIGHT-TO-LEFT: increment first, then double
// composed := endomorphism.MonadCompose(double, increment)
// result := composed(5) // (5 + 1) * 2 = 12
//
// // Compare with Chain which executes LEFT-TO-RIGHT:
// chained := endomorphism.MonadChain(double, increment)
// result2 := chained(5) // (5 * 2) + 1 = 11
func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
return function.Flow2(g, f)
}
// MonadMap maps an endomorphism over another endomorphism using function composition.
//
// For endomorphisms, Map is equivalent to Compose (RIGHT-TO-LEFT composition).
// This is the functor map operation for endomorphisms.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
// - g is applied first to the input
// - f is applied to the result
//
// Parameters:
// - f: The function to map (outer function)
// - g: The endomorphism to map over (inner function)
//
// Returns:
// - A new endomorphism that applies g, then f
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
// mapped := endomorphism.MonadMap(double, increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
return MonadCompose(f, g)
}
// Compose returns a function that composes an endomorphism with another, executing right to left.
//
// This is the curried version of MonadCompose. It takes an endomorphism g and returns
// a function that composes any endomorphism with g, applying g first (inner function),
// then the input endomorphism (outer function).
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT (mathematical composition):
// - g is applied first to the input
// - The endomorphism passed to the returned function is applied to the result of g
//
// This follows the mathematical composition notation where Compose(g)(f) = f ∘ g
//
// Parameters:
// - g: The first endomorphism to apply (inner function)
//
// Returns:
// - A function that takes an endomorphism f and composes it with g (right-to-left)
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// composeWithIncrement := endomorphism.Compose(increment)
// double := func(x int) int { return x * 2 }
//
// // Composes double with increment (RIGHT-TO-LEFT: increment first, then double)
// composed := composeWithIncrement(double)
// result := composed(5) // (5 + 1) * 2 = 12
//
// // Compare with Chain which executes LEFT-TO-RIGHT:
// chainWithIncrement := endomorphism.Chain(increment)
// chained := chainWithIncrement(double)
// result2 := chained(5) // (5 * 2) + 1 = 11
func Compose[A any](g Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadCompose, g)
}
// Map returns a function that maps an endomorphism over another endomorphism.
//
// This is the curried version of MonadMap. It takes an endomorphism f and returns
// a function that maps f over any endomorphism using RIGHT-TO-LEFT composition.
//
// IMPORTANT: Execution order is RIGHT-TO-LEFT (same as Compose):
// - The endomorphism passed to the returned function is applied first
// - f is applied to the result
//
// For endomorphisms, Map is equivalent to Compose.
//
// Parameters:
// - f: The function to map (outer function)
//
// Returns:
// - A function that takes an endomorphism and maps f over it (right-to-left)
//
// Example:
//
// double := func(x int) int { return x * 2 }
// mapDouble := endomorphism.Map(double)
// increment := func(x int) int { return x + 1 }
// mapped := mapDouble(increment)
// // mapped(5) = double(increment(5)) = double(6) = 12
func Map[A any](f Endomorphism[A]) Operator[A] {
return Compose(f)
}
// MonadChain chains two endomorphisms together, executing them from left to right.
//
// This is the monadic bind operation for endomorphisms. For endomorphisms, bind is
// simply left-to-right function composition: ma is applied first, then f.
//
// IMPORTANT: The execution order is LEFT-TO-RIGHT:
// - ma is applied first to the input
// - f is applied to the result of ma
//
// This is different from MonadCompose which executes RIGHT-TO-LEFT.
//
// Parameters:
// - ma: The first endomorphism to apply
// - f: The second endomorphism to apply
//
// Returns:
// - A new endomorphism that applies ma, then f
//
// Example:
//
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
//
// // MonadChain executes LEFT-TO-RIGHT: double first, then increment
// chained := endomorphism.MonadChain(double, increment)
// result := chained(5) // (5 * 2) + 1 = 11
//
// // Compare with MonadCompose which executes RIGHT-TO-LEFT:
// composed := endomorphism.MonadCompose(increment, double)
// result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order)
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
return Compose(ma, f)
return function.Flow2(ma, f)
}
// Chain returns a function that chains an endomorphism with another.
// MonadChainFirst chains two endomorphisms but returns the result of the first.
//
// This is the curried version of MonadChain. It takes an endomorphism f and returns
// a function that chains any endomorphism with f.
// This applies ma first, then f, but discards the result of f and returns the result of ma.
// Useful for performing side-effects while preserving the original value.
//
// Parameters:
// - f: The endomorphism to chain with
// - ma: The endomorphism whose result to keep
// - f: The endomorphism to apply for its effect
//
// Returns:
// - A function that takes an endomorphism and chains it with f
// - A new endomorphism that applies both but returns ma's result
//
// Example:
//
// double := func(x int) int { return x * 2 }
// log := func(x int) int { fmt.Println(x); return x }
// chained := endomorphism.MonadChainFirst(double, log)
// result := chained(5) // Prints 10, returns 10
func MonadChainFirst[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
return func(a A) A {
result := ma(a)
f(result) // Apply f for its effect
return result // But return ma's result
}
}
// ChainFirst returns a function that chains for effect but preserves the original result.
//
// This is the curried version of MonadChainFirst.
//
// Parameters:
// - f: The endomorphism to apply for its effect
//
// Returns:
// - A function that takes an endomorphism and chains it with f, keeping the first result
//
// Example:
//
// log := func(x int) int { fmt.Println(x); return x }
// chainLog := endomorphism.ChainFirst(log)
// double := func(x int) int { return x * 2 }
// chained := chainLog(double)
// result := chained(5) // Prints 10, returns 10
func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadChainFirst, f)
}
// Chain returns a function that chains an endomorphism with another, executing left to right.
//
// This is the curried version of MonadChain. It takes an endomorphism f and returns
// a function that chains any endomorphism with f, applying the input endomorphism first,
// then f.
//
// IMPORTANT: Execution order is LEFT-TO-RIGHT:
// - The endomorphism passed to the returned function is applied first
// - f is applied to the result
//
// Parameters:
// - f: The second endomorphism to apply
//
// Returns:
// - A function that takes an endomorphism and chains it with f (left-to-right)
//
// Example:
//
// increment := func(x int) int { return x + 1 }
// chainWithIncrement := endomorphism.Chain(increment)
// double := func(x int) int { return x * 2 }
//
// // Chains double (first) with increment (second)
// chained := chainWithIncrement(double)
// result := chained(5) // (5 * 2) + 1 = 11
func Chain[A any](f Endomorphism[A]) Endomorphism[Endomorphism[A]] {
func Chain[A any](f Endomorphism[A]) Operator[A] {
return function.Bind2nd(MonadChain, f)
}

View File

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

View File

@@ -88,11 +88,15 @@ func Identity[A any]() Endomorphism[A] {
// For endomorphisms, this operation is composition (Compose). This means:
// - Concat(f, Concat(g, h)) = Concat(Concat(f, g), h)
//
// IMPORTANT: Concat uses Compose, which executes RIGHT-TO-LEFT:
// - Concat(f, g) applies g first, then f
// - This is equivalent to Compose(f, g)
//
// The returned semigroup can be used with semigroup operations to combine
// multiple endomorphisms.
//
// Returns:
// - A Semigroup[Endomorphism[A]] where concat is composition
// - A Semigroup[Endomorphism[A]] where concat is composition (right-to-left)
//
// Example:
//
@@ -102,11 +106,11 @@ func Identity[A any]() Endomorphism[A] {
// double := func(x int) int { return x * 2 }
// increment := func(x int) int { return x + 1 }
//
// // Combine using the semigroup
// // Combine using the semigroup (RIGHT-TO-LEFT execution)
// combined := sg.Concat(double, increment)
// result := combined(5) // (5 * 2) + 1 = 11
// result := combined(5) // (5 + 1) * 2 = 12 (increment first, then double)
func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
return S.MakeSemigroup(Compose[A])
return S.MakeSemigroup(MonadCompose[A])
}
// Monoid returns a Monoid for endomorphisms where concat is composition and empty is identity.
@@ -115,6 +119,10 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// - The binary operation is composition (Compose)
// - The identity element is the identity function (Identity)
//
// IMPORTANT: Concat uses Compose, which executes RIGHT-TO-LEFT:
// - Concat(f, g) applies g first, then f
// - ConcatAll applies functions from right to left
//
// This satisfies the monoid laws:
// - Right identity: Concat(x, Empty) = x
// - Left identity: Concat(Empty, x) = x
@@ -124,7 +132,7 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// combine multiple endomorphisms.
//
// Returns:
// - A Monoid[Endomorphism[A]] with composition and identity
// - A Monoid[Endomorphism[A]] with composition (right-to-left) and identity
//
// Example:
//
@@ -135,9 +143,9 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
// increment := func(x int) int { return x + 1 }
// square := func(x int) int { return x * x }
//
// // Combine multiple endomorphisms
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
// combined := M.ConcatAll(monoid)(double, increment, square)
// result := combined(5) // ((5 * 2) + 1) * ((5 * 2) + 1) = 121
// result := combined(5) // square(increment(double(5))) = square(increment(10)) = square(11) = 121
func Monoid[A any]() M.Monoid[Endomorphism[A]] {
return M.MakeMonoid(Compose[A], Identity[A]())
return M.MakeMonoid(MonadCompose[A], Identity[A]())
}

View File

@@ -37,6 +37,8 @@ type (
// var g endomorphism.Endomorphism[int] = increment
Endomorphism[A any] = func(A) A
Kleisli[A any] = func(A) Endomorphism[A]
// Operator represents a transformation from one endomorphism to another.
//
// An Operator takes an endomorphism on type A and produces an endomorphism on type B.
@@ -52,5 +54,5 @@ type (
// return strconv.Itoa(result)
// }
// }
Operator[A, B any] = func(Endomorphism[A]) Endomorphism[B]
Operator[A any] = Endomorphism[Endomorphism[A]]
)

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package fromoption
import (
"github.com/IBM/fp-go/v2/option"
)
type (
Option[T any] = option.Option[T]
FromOption[A, HKTA any] interface {
FromEither(Option[A]) HKTA
}
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
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"
O "github.com/IBM/fp-go/v2/option"
@@ -9,30 +8,23 @@ import (
// 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 +40,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 +93,5 @@ 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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,120 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package predicate
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/eq"
)
// IsEqual creates a Kleisli arrow that tests if two values are equal using a custom equality function.
//
// This function takes an Eq instance (which defines how to compare values of type A) and returns
// a curried function that can be used to create predicates for equality testing.
//
// Parameters:
// - pred: An Eq[A] instance that defines equality for type A
//
// Returns:
// - A Kleisli[A, A] that takes a value and returns a predicate testing equality with that value
//
// Example:
//
// type Person struct { Name string; Age int }
// personEq := eq.MakeEq(func(a, b Person) bool {
// return a.Name == b.Name && a.Age == b.Age
// })
// isEqualToPerson := IsEqual(personEq)
// alice := Person{Name: "Alice", Age: 30}
// isAlice := isEqualToPerson(alice)
// isAlice(Person{Name: "Alice", Age: 30}) // true
// isAlice(Person{Name: "Bob", Age: 30}) // false
func IsEqual[A any](pred eq.Eq[A]) Kleisli[A, A] {
return F.Curry2(pred.Equals)
}
// IsStrictEqual creates a Kleisli arrow that tests if two values are equal using Go's == operator.
//
// This is a convenience function for comparable types that uses strict equality (==) for comparison.
// It's equivalent to IsEqual with an Eq instance based on ==.
//
// Returns:
// - A Kleisli[A, A] that takes a value and returns a predicate testing strict equality
//
// Example:
//
// isEqualTo5 := IsStrictEqual[int]()(5)
// isEqualTo5(5) // true
// isEqualTo5(10) // false
//
// isEqualToHello := IsStrictEqual[string]()("hello")
// isEqualToHello("hello") // true
// isEqualToHello("world") // false
func IsStrictEqual[A comparable]() Kleisli[A, A] {
return IsEqual(eq.FromStrictEquals[A]())
}
// IsZero creates a predicate that tests if a value equals the zero value for its type.
//
// The zero value is the default value for a type in Go (e.g., 0 for int, "" for string,
// false for bool, nil for pointers, etc.).
//
// Returns:
// - A Predicate[A] that returns true if the value is the zero value for type A
//
// Example:
//
// isZeroInt := IsZero[int]()
// isZeroInt(0) // true
// isZeroInt(5) // false
//
// isZeroString := IsZero[string]()
// isZeroString("") // true
// isZeroString("hello") // false
//
// isZeroBool := IsZero[bool]()
// isZeroBool(false) // true
// isZeroBool(true) // false
func IsZero[A comparable]() Predicate[A] {
var zero A
return IsStrictEqual[A]()(zero)
}
// IsNonZero creates a predicate that tests if a value is not equal to the zero value for its type.
//
// This is the negation of IsZero, returning true for any non-zero value.
//
// Returns:
// - A Predicate[A] that returns true if the value is not the zero value for type A
//
// Example:
//
// isNonZeroInt := IsNonZero[int]()
// isNonZeroInt(0) // false
// isNonZeroInt(5) // true
// isNonZeroInt(-3) // true
//
// isNonZeroString := IsNonZero[string]()
// isNonZeroString("") // false
// isNonZeroString("hello") // true
//
// isNonZeroPtr := IsNonZero[*int]()
// isNonZeroPtr(nil) // false
// isNonZeroPtr(new(int)) // true
func IsNonZero[A comparable]() Predicate[A] {
return Not(IsZero[A]())
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

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

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

View File

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

98
v2/readeroption/from.go Normal file
View File

@@ -0,0 +1,98 @@
// 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"
)
// From functions convert Go functions that take a context as the first parameter
// and return (value, bool) into ReaderOption functions with uncurried parameters.
//
// Unlike Curry functions which return fully curried functions, From functions
// return functions that take multiple parameters at once (uncurried style).
//
// 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.
// From0 converts a function that takes only a context and returns (A, bool)
// into a function that returns a ReaderOption[R, A].
//
// Example:
//
// getConfig := func(ctx context.Context) (Config, bool) {
// cfg, ok := ctx.Value("config").(Config)
// return cfg, ok
// }
// roFunc := readeroption.From0(getConfig)
// ro := roFunc() // Returns a ReaderOption[context.Context, Config]
// result := ro(ctx) // Returns option.Some(config) or option.None()
func From0[R, A any](f func(R) (A, bool)) func() ReaderOption[R, A] {
return G.From0[ReaderOption[R, A]](f)
}
// From1 converts a function that takes a context and one argument, returning (A, bool),
// into a function that takes one argument and returns a ReaderOption.
//
// This is equivalent to Curry1 but provided for consistency with the From naming convention.
//
// Example:
//
// findUser := func(ctx context.Context, id int) (User, bool) {
// // Query database using context
// return user, found
// }
// roFunc := readeroption.From1(findUser)
// ro := roFunc(123) // Returns a ReaderOption[context.Context, User]
// result := ro(ctx) // Returns option.Some(user) or option.None()
func From1[R, T1, A any](f func(R, T1) (A, bool)) Kleisli[R, T1, A] {
return G.From1[ReaderOption[R, A]](f)
}
// From2 converts a function that takes a context and two arguments, returning (A, bool),
// into a function that takes two arguments (uncurried) and returns a ReaderOption.
//
// Example:
//
// query := func(ctx context.Context, table string, id int) (Record, bool) {
// // Query database using context
// return record, found
// }
// roFunc := readeroption.From2(query)
// ro := roFunc("users", 123) // Returns a ReaderOption[context.Context, Record]
// result := ro(ctx) // Returns option.Some(record) or option.None()
func From2[R, T1, T2, A any](f func(R, T1, T2) (A, bool)) func(T1, T2) ReaderOption[R, A] {
return G.From2[ReaderOption[R, A]](f)
}
// From3 converts a function that takes a context and three arguments, returning (A, bool),
// into a function that takes three arguments (uncurried) and returns a ReaderOption.
//
// Example:
//
// complexQuery := func(ctx context.Context, db string, table string, id int) (Record, bool) {
// // Query database using context
// return record, found
// }
// roFunc := readeroption.From3(complexQuery)
// ro := roFunc("mydb", "users", 123) // Returns a ReaderOption[context.Context, Record]
// result := ro(ctx) // Returns option.Some(record) or option.None()
func From3[R, T1, T2, T3, A any](f func(R, T1, T2, T3) (A, bool)) func(T1, T2, T3) ReaderOption[R, A] {
return G.From3[ReaderOption[R, A]](f)
}

View File

@@ -0,0 +1,112 @@
// 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 TestFrom0(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
}
roFunc := From0(getConfig)
ro := roFunc()
// 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 TestFrom1(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
}
roFunc := From1(lookup)
// Test with value in context
ctx1 := context.WithValue(context.Background(), "count", 42)
result1 := roFunc("count")(ctx1)
assert.Equal(t, O.Of(42), result1)
// Test without value in context
ctx2 := context.Background()
result2 := roFunc("count")(ctx2)
assert.Equal(t, O.None[int](), result2)
}
func TestFrom2(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
}
roFunc := From2(combine)
// Test with enabled context
ctx1 := context.WithValue(context.Background(), "enabled", true)
result1 := roFunc("test", 5)(ctx1)
assert.Equal(t, O.Of("test:5"), result1)
// Test with disabled context
ctx2 := context.Background()
result2 := roFunc("test", 5)(ctx2)
assert.Equal(t, O.None[string](), result2)
}
func TestFrom3(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
}
roFunc := From3(combine)
// Test with enabled context and true flag
ctx1 := context.WithValue(context.Background(), "enabled", true)
result1 := roFunc("test", 5, true)(ctx1)
assert.Equal(t, O.Of("test:5"), result1)
// Test with false flag
result2 := roFunc("test", 5, false)(ctx1)
assert.Equal(t, O.None[string](), result2)
}

View File

@@ -0,0 +1,60 @@
// 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 generic
import (
F "github.com/IBM/fp-go/v2/function"
RA "github.com/IBM/fp-go/v2/internal/array"
O "github.com/IBM/fp-go/v2/option"
)
// MonadTraverseArray transforms an array
func MonadTraverseArray[GB ~func(E) O.Option[B], GBS ~func(E) O.Option[BBS], AAS ~[]A, BBS ~[]B, E, A, B any](ma AAS, f func(A) GB) GBS {
return RA.MonadTraverse(
Of[GBS, E, BBS],
Map[GBS, func(E) O.Option[func(B) BBS], E, BBS, func(B) BBS],
Ap[GB, GBS, func(E) O.Option[func(B) BBS], E, B, BBS],
ma, f,
)
}
// TraverseArray transforms an array
func TraverseArray[GB ~func(E) O.Option[B], GBS ~func(E) O.Option[BBS], AAS ~[]A, BBS ~[]B, E, A, B any](f func(A) GB) func(AAS) GBS {
return RA.Traverse[AAS](
Of[GBS, E, BBS],
Map[GBS, func(E) O.Option[func(B) BBS], E, BBS, func(B) BBS],
Ap[GB, GBS, func(E) O.Option[func(B) BBS], E, B, BBS],
f,
)
}
// TraverseArrayWithIndex transforms an array
func TraverseArrayWithIndex[GB ~func(E) O.Option[B], GBS ~func(E) O.Option[BBS], AAS ~[]A, BBS ~[]B, E, A, B any](f func(int, A) GB) func(AAS) GBS {
return RA.TraverseWithIndex[AAS](
Of[GBS, E, BBS],
Map[GBS, func(E) O.Option[func(B) BBS], E, BBS, func(B) BBS],
Ap[GB, GBS, func(E) O.Option[func(B) BBS], E, B, BBS],
f,
)
}
// SequenceArray converts a homogeneous sequence of either into an either of sequence
func SequenceArray[GA ~func(E) O.Option[A], GAS ~func(E) O.Option[AAS], AAS ~[]A, GAAS ~[]GA, E, A any](ma GAAS) GAS {
return MonadTraverseArray[GA, GAS](ma, F.Identity[GA])
}

View File

@@ -0,0 +1,184 @@
// 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 generic
import (
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
O "github.com/IBM/fp-go/v2/option"
)
// 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 {
// Config Config
// User User
// }
// type Env struct {
// ConfigService ConfigService
// UserService UserService
// }
// result := generic.Do[ReaderEither[Env, error, State], Env, error, State](State{})
func Do[GS ~func(R) O.Option[S], R, S any](
empty S,
) GS {
return Of[GS](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 {
// Config Config
// User User
// }
// type Env struct {
// ConfigService ConfigService
// UserService UserService
// }
//
// result := F.Pipe2(
// generic.Do[ReaderEither[Env, error, State], Env, error, State](State{}),
// generic.Bind[ReaderEither[Env, error, State], ReaderEither[Env, error, State], ReaderEither[Env, error, Config], Env, error, State, State, Config](
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) ReaderEither[Env, error, Config] {
// return func(env Env) either.Either[error, Config] {
// return env.ConfigService.Load()
// }
// },
// ),
// generic.Bind[ReaderEither[Env, error, State], ReaderEither[Env, error, State], ReaderEither[Env, error, User], Env, error, State, State, User](
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) ReaderEither[Env, error, User] {
// // This can access s.Config from the previous step
// return func(env Env) either.Either[error, User] {
// return env.UserService.GetUserForConfig(s.Config)
// }
// },
// ),
// )
func Bind[GS1 ~func(R) O.Option[S1], GS2 ~func(R) O.Option[S2], GT ~func(R) O.Option[T], R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) GT,
) func(GS1) GS2 {
return C.Bind(
Chain[GS1, GS2, R, S1, S2],
Map[GT, GS2, R, T, S2],
setter,
f,
)
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
func Let[GS1 ~func(R) O.Option[S1], GS2 ~func(R) O.Option[S2], R, S1, S2, T any](
key func(T) func(S1) S2,
f func(S1) T,
) func(GS1) GS2 {
return F.Let(
Map[GS1, GS2, R, S1, S2],
key,
f,
)
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
func LetTo[GS1 ~func(R) O.Option[S1], GS2 ~func(R) O.Option[S2], R, S1, S2, B any](
key func(B) func(S1) S2,
b B,
) func(GS1) GS2 {
return F.LetTo(
Map[GS1, GS2, R, S1, S2],
key,
b,
)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[GS1 ~func(R) O.Option[S1], GT ~func(R) O.Option[T], R, S1, T any](
setter func(T) S1,
) func(GT) GS1 {
return C.BindTo(
Map[GT, GS1, R, T, S1],
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 {
// Config Config
// User User
// }
// type Env struct {
// ConfigService ConfigService
// UserService UserService
// }
//
// // These operations are independent and can be combined with ApS
// getConfig := func(env Env) either.Either[error, Config] {
// return env.ConfigService.Load()
// }
// getUser := func(env Env) either.Either[error, User] {
// return env.UserService.GetCurrent()
// }
//
// result := F.Pipe2(
// generic.Do[ReaderEither[Env, error, State], Env, error, State](State{}),
// generic.ApS[...](
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// getConfig,
// ),
// generic.ApS[...](
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// )
func ApS[GS1 ~func(R) O.Option[S1], GS2 ~func(R) O.Option[S2], GT ~func(R) O.Option[T], R, S1, S2, T any](
setter func(T) func(S1) S2,
fa GT,
) func(GS1) GS2 {
return A.ApS(
Ap[GT, GS2, func(R) O.Option[func(T) S2], R, T, S2],
Map[GS1, func(R) O.Option[func(T) S2], R, S1, func(T) S2],
setter,
fa,
)
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
O "github.com/IBM/fp-go/v2/option"
G "github.com/IBM/fp-go/v2/reader/generic"
)
// these functions curry a golang function with the context as the firsr parameter into a either reader with the context as the last parameter
// this goes back to the advice in https://pkg.go.dev/context to put the context as a first parameter as a convention
func Curry0[GEA ~func(R) O.Option[A], R, A any](f func(R) (A, bool)) GEA {
return G.Curry0[GEA](O.Optionize1(f))
}
func Curry1[GEA ~func(R) O.Option[A], R, T1, A any](f func(R, T1) (A, bool)) func(T1) GEA {
return G.Curry1[GEA](O.Optionize2(f))
}
func Curry2[GEA ~func(R) O.Option[A], R, T1, T2, A any](f func(R, T1, T2) (A, bool)) func(T1) func(T2) GEA {
return G.Curry2[GEA](O.Optionize3(f))
}
func Curry3[GEA ~func(R) O.Option[A], R, T1, T2, T3, A any](f func(R, T1, T2, T3) (A, bool)) func(T1) func(T2) func(T3) GEA {
return G.Curry3[GEA](O.Optionize4(f))
}
func Uncurry1[GEA ~func(R) O.Option[A], R, T1, A any](f func(T1) GEA) func(R, T1) (A, bool) {
return O.Unoptionize2(G.Uncurry1(f))
}
func Uncurry2[GEA ~func(R) O.Option[A], R, T1, T2, A any](f func(T1) func(T2) GEA) func(R, T1, T2) (A, bool) {
return O.Unoptionize3(G.Uncurry2(f))
}
func Uncurry3[GEA ~func(R) O.Option[A], R, T1, T2, T3, A any](f func(T1) func(T2) func(T3) GEA) func(R, T1, T2, T3) (A, bool) {
return O.Unoptionize4(G.Uncurry3(f))
}

View File

@@ -0,0 +1,40 @@
// 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 generic
import (
O "github.com/IBM/fp-go/v2/option"
G "github.com/IBM/fp-go/v2/reader/generic"
)
// these functions From a golang function with the context as the firsr parameter into a either reader with the context as the last parameter
// this goes back to the advice in https://pkg.go.dev/context to put the context as a first parameter as a convention
func From0[GEA ~func(R) O.Option[A], R, A any](f func(R) (A, bool)) func() GEA {
return G.From0[GEA](O.Optionize1(f))
}
func From1[GEA ~func(R) O.Option[A], R, T1, A any](f func(R, T1) (A, bool)) func(T1) GEA {
return G.From1[GEA](O.Optionize2(f))
}
func From2[GEA ~func(R) O.Option[A], R, T1, T2, A any](f func(R, T1, T2) (A, bool)) func(T1, T2) GEA {
return G.From2[GEA](O.Optionize3(f))
}
func From3[GEA ~func(R) O.Option[A], R, T1, T2, T3, A any](f func(R, T1, T2, T3) (A, bool)) func(T1, T2, T3) GEA {
return G.From3[GEA](O.Optionize4(f))
}

View File

@@ -0,0 +1,129 @@
// 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 generic
import (
F "github.com/IBM/fp-go/v2/function"
FO "github.com/IBM/fp-go/v2/internal/fromoption"
FR "github.com/IBM/fp-go/v2/internal/fromreader"
FC "github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/optiont"
"github.com/IBM/fp-go/v2/internal/readert"
O "github.com/IBM/fp-go/v2/option"
R "github.com/IBM/fp-go/v2/reader/generic"
)
//go:inline
func MakeReaderOption[GEA ~func(E) O.Option[A], E, A any](f func(E) O.Option[A]) GEA {
return f
}
//go:inline
func FromOption[GEA ~func(E) O.Option[A], E, A any](e O.Option[A]) GEA {
return R.Of[GEA](e)
}
func SomeReader[GA ~func(E) A, GEA ~func(E) O.Option[A], E, A any](r GA) GEA {
return optiont.SomeF(R.MonadMap[GA, GEA, E, A, O.Option[A]], r)
}
func Some[GEA ~func(E) O.Option[A], E, A any](r A) GEA {
return optiont.Of(R.Of[GEA, E, O.Option[A]], r)
}
//go:inline
func FromReader[GA ~func(E) A, GEA ~func(E) O.Option[A], E, A any](r GA) GEA {
return SomeReader[GA, GEA](r)
}
func MonadMap[GEA ~func(E) O.Option[A], GEB ~func(E) O.Option[B], E, A, B any](fa GEA, f func(A) B) GEB {
return readert.MonadMap[GEA, GEB](O.MonadMap[A, B], fa, f)
}
func Map[GEA ~func(E) O.Option[A], GEB ~func(E) O.Option[B], E, A, B any](f func(A) B) func(GEA) GEB {
return readert.Map[GEA, GEB](O.Map[A, B], f)
}
func MonadChain[GEA ~func(E) O.Option[A], GEB ~func(E) O.Option[B], E, A, B any](ma GEA, f func(A) GEB) GEB {
return readert.MonadChain(O.MonadChain[A, B], ma, f)
}
func Chain[GEA ~func(E) O.Option[A], GEB ~func(E) O.Option[B], E, A, B any](f func(A) GEB) func(GEA) GEB {
return F.Bind2nd(MonadChain[GEA, GEB, E, A, B], f)
}
func Of[GEA ~func(E) O.Option[A], E, A any](a A) GEA {
return readert.MonadOf[GEA](O.Of[A], a)
}
func MonadAp[GEA ~func(E) O.Option[A], GEB ~func(E) O.Option[B], GEFAB ~func(E) O.Option[func(A) B], E, A, B any](fab GEFAB, fa GEA) GEB {
return readert.MonadAp[GEA, GEB, GEFAB, E, A](O.MonadAp[B, A], fab, fa)
}
func Ap[GEA ~func(E) O.Option[A], GEB ~func(E) O.Option[B], GEFAB ~func(E) O.Option[func(A) B], E, A, B any](fa GEA) func(GEFAB) GEB {
return F.Bind2nd(MonadAp[GEA, GEB, GEFAB, E, A, B], fa)
}
func FromPredicate[GEA ~func(E) O.Option[A], E, A any](pred func(A) bool) func(A) GEA {
return FO.FromPredicate(FromOption[GEA, E, A], pred)
}
func Fold[GEA ~func(E) O.Option[A], GB ~func(E) B, E, A, B any](onNone func() GB, onRight func(A) GB) func(GEA) GB {
return optiont.MatchE(R.Chain[GEA, GB, E, O.Option[A], B], onNone, onRight)
}
func GetOrElse[GEA ~func(E) O.Option[A], GA ~func(E) A, E, A any](onNone func() GA) func(GEA) GA {
return optiont.GetOrElse(R.Chain[GEA, GA, E, O.Option[A], A], onNone, R.Of[GA, E, A])
}
func Ask[GEE ~func(E) O.Option[E], E, L any]() GEE {
return FR.Ask(FromReader[func(E) E, GEE, E, E])()
}
func Asks[GA ~func(E) A, GEA ~func(E) O.Option[A], E, A any](r GA) GEA {
return FR.Asks(FromReader[GA, GEA, E, A])(r)
}
func MonadChainOptionK[GEA ~func(E) O.Option[A], GEB ~func(E) O.Option[B], E, A, B any](ma GEA, f func(A) O.Option[B]) GEB {
return FO.MonadChainOptionK(
MonadChain[GEA, GEB, E, A, B],
FromOption[GEB, E, B],
ma,
f,
)
}
func ChainOptionK[GEA ~func(E) O.Option[A], GEB ~func(E) O.Option[B], E, A, B any](f func(A) O.Option[B]) func(ma GEA) GEB {
return F.Bind2nd(MonadChainOptionK[GEA, GEB, E, A, B], f)
}
func Flatten[GEA ~func(E) O.Option[A], GGA ~func(E) O.Option[GEA], E, A any](mma GGA) GEA {
return MonadChain(mma, F.Identity[GEA])
}
// Local changes the value of the local context during the execution of the action `ma` (similar to `Contravariant`'s
// `contramap`).
func Local[GA1 ~func(R1) O.Option[A], GA2 ~func(R2) O.Option[A], R2, R1, E, A any](f func(R2) R1) func(GA1) GA2 {
return R.Local[GA1, GA2](f)
}
func MonadFlap[GEFAB ~func(E) O.Option[func(A) B], GEB ~func(E) O.Option[B], E, A, B any](fab GEFAB, a A) GEB {
return FC.MonadFlap(MonadMap[GEFAB, GEB], fab, a)
}
func Flap[GEFAB ~func(E) O.Option[func(A) B], GEB ~func(E) O.Option[B], E, A, B any](a A) func(GEFAB) GEB {
return FC.Flap(Map[GEFAB, GEB], a)
}

View File

@@ -0,0 +1,80 @@
// 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 generic
import (
"github.com/IBM/fp-go/v2/internal/apply"
O "github.com/IBM/fp-go/v2/option"
T "github.com/IBM/fp-go/v2/tuple"
)
// SequenceT converts n inputs of higher kinded types into a higher kinded types of n strongly typed values, represented as a tuple
func SequenceT1[
GA ~func(E) O.Option[A],
GTA ~func(E) O.Option[T.Tuple1[A]],
E, A any](a GA) GTA {
return apply.SequenceT1(
Map[GA, GTA, E, A, T.Tuple1[A]],
a,
)
}
func SequenceT2[
GA ~func(E) O.Option[A],
GB ~func(E) O.Option[B],
GTAB ~func(E) O.Option[T.Tuple2[A, B]],
E, A, B any](a GA, b GB) GTAB {
return apply.SequenceT2(
Map[GA, func(E) O.Option[func(B) T.Tuple2[A, B]], E, A, func(B) T.Tuple2[A, B]],
Ap[GB, GTAB, func(E) O.Option[func(B) T.Tuple2[A, B]], E, B, T.Tuple2[A, B]],
a, b,
)
}
func SequenceT3[
GA ~func(E) O.Option[A],
GB ~func(E) O.Option[B],
GC ~func(E) O.Option[C],
GTABC ~func(E) O.Option[T.Tuple3[A, B, C]],
E, A, B, C any](a GA, b GB, c GC) GTABC {
return apply.SequenceT3(
Map[GA, func(E) O.Option[func(B) func(C) T.Tuple3[A, B, C]], E, A, func(B) func(C) T.Tuple3[A, B, C]],
Ap[GB, func(E) O.Option[func(C) T.Tuple3[A, B, C]], func(E) O.Option[func(B) func(C) T.Tuple3[A, B, C]], E, B, func(C) T.Tuple3[A, B, C]],
Ap[GC, GTABC, func(E) O.Option[func(C) T.Tuple3[A, B, C]], E, C, T.Tuple3[A, B, C]],
a, b, c,
)
}
func SequenceT4[
GA ~func(E) O.Option[A],
GB ~func(E) O.Option[B],
GC ~func(E) O.Option[C],
GD ~func(E) O.Option[D],
GTABCD ~func(E) O.Option[T.Tuple4[A, B, C, D]],
E, A, B, C, D any](a GA, b GB, c GC, d GD) GTABCD {
return apply.SequenceT4(
Map[GA, func(E) O.Option[func(B) func(C) func(D) T.Tuple4[A, B, C, D]], E, A, func(B) func(C) func(D) T.Tuple4[A, B, C, D]],
Ap[GB, func(E) O.Option[func(C) func(D) T.Tuple4[A, B, C, D]], func(E) O.Option[func(B) func(C) func(D) T.Tuple4[A, B, C, D]], E, B, func(C) func(D) T.Tuple4[A, B, C, D]],
Ap[GC, func(E) O.Option[func(D) T.Tuple4[A, B, C, D]], func(E) O.Option[func(C) func(D) T.Tuple4[A, B, C, D]], E, C, func(D) T.Tuple4[A, B, C, D]],
Ap[GD, GTABCD, func(E) O.Option[func(D) T.Tuple4[A, B, C, D]], E, D, T.Tuple4[A, B, C, D]],
a, b, c, d,
)
}

350
v2/readeroption/reader.go Normal file
View File

@@ -0,0 +1,350 @@
// 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 (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/fromoption"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/optiont"
"github.com/IBM/fp-go/v2/internal/readert"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
)
// FromOption lifts an Option[A] into a ReaderOption[E, A].
// The resulting computation ignores the environment and returns the given option.
//
//go:inline
func FromOption[E, A any](e Option[A]) ReaderOption[E, A] {
return reader.Of[E](e)
}
// Some wraps a value in a ReaderOption, representing a successful computation.
// This is equivalent to Of but more explicit about the Option semantics.
//
//go:inline
func Some[E, A any](r A) ReaderOption[E, A] {
return optiont.Of(reader.Of[E, Option[A]], r)
}
// FromReader lifts a Reader[E, A] into a ReaderOption[E, A].
// The resulting computation always succeeds (returns Some).
//
//go:inline
func FromReader[E, A any](r Reader[E, A]) ReaderOption[E, A] {
return SomeReader(r)
}
// SomeReader lifts a Reader[E, A] into a ReaderOption[E, A].
// The resulting computation always succeeds (returns Some).
//
//go:inline
func SomeReader[E, A any](r Reader[E, A]) ReaderOption[E, A] {
return optiont.SomeF(reader.MonadMap[E, A, Option[A]], r)
}
// MonadMap applies a function to the value inside a ReaderOption.
// If the ReaderOption contains None, the function is not applied.
//
// Example:
//
// ro := readeroption.Of[Config](42)
// doubled := readeroption.MonadMap(ro, func(x int) int { return x * 2 })
//
//go:inline
func MonadMap[E, A, B any](fa ReaderOption[E, A], f func(A) B) ReaderOption[E, B] {
return readert.MonadMap[ReaderOption[E, A], ReaderOption[E, B]](O.MonadMap[A, B], fa, f)
}
// Map returns a function that applies a transformation to the value inside a ReaderOption.
// This is the curried version of MonadMap, useful for composition with F.Pipe.
//
// Example:
//
// doubled := F.Pipe1(
// readeroption.Of[Config](42),
// readeroption.Map[Config](func(x int) int { return x * 2 }),
// )
//
//go:inline
func Map[E, A, B any](f func(A) B) Operator[E, A, B] {
return readert.Map[ReaderOption[E, A], ReaderOption[E, B]](O.Map[A, B], f)
}
// MonadChain sequences two ReaderOption computations, where the second depends on the result of the first.
// If the first computation returns None, the second is not executed.
//
// Example:
//
// findUser := func(id int) readeroption.ReaderOption[DB, User] { ... }
// loadProfile := func(user User) readeroption.ReaderOption[DB, Profile] { ... }
// result := readeroption.MonadChain(findUser(123), loadProfile)
//
//go:inline
func MonadChain[E, A, B any](ma ReaderOption[E, A], f Kleisli[E, A, B]) ReaderOption[E, B] {
return readert.MonadChain(O.MonadChain[A, B], ma, f)
}
// Chain returns a function that sequences ReaderOption computations.
// This is the curried version of MonadChain, useful for composition with F.Pipe.
//
// Example:
//
// result := F.Pipe1(
// findUser(123),
// readeroption.Chain(loadProfile),
// )
//
//go:inline
func Chain[E, A, B any](f Kleisli[E, A, B]) Operator[E, A, B] {
return readert.Chain[ReaderOption[E, A]](O.Chain[A, B], f)
}
// Of wraps a value in a ReaderOption, representing a successful computation.
// The resulting computation ignores the environment and returns Some(a).
//
// Example:
//
// ro := readeroption.Of[Config](42)
// result := ro(config) // Returns option.Some(42)
//
//go:inline
func Of[E, A any](a A) ReaderOption[E, A] {
return readert.MonadOf[ReaderOption[E, A]](O.Of[A], a)
}
// None creates a ReaderOption representing a failed computation.
// The resulting computation ignores the environment and returns None.
//
// Example:
//
// ro := readeroption.None[Config, int]()
// result := ro(config) // Returns option.None[int]()
//
//go:inline
func None[E, A any]() ReaderOption[E, A] {
return reader.Of[E](O.None[A]())
}
// MonadAp applies a function wrapped in a ReaderOption to a value wrapped in a ReaderOption.
// Both computations are executed with the same environment.
// If either computation returns None, the result is None.
//
//go:inline
func MonadAp[E, A, B any](fab ReaderOption[E, func(A) B], fa ReaderOption[E, A]) ReaderOption[E, B] {
return readert.MonadAp[ReaderOption[E, A], ReaderOption[E, B], ReaderOption[E, func(A) B], E, A](O.MonadAp[B, A], fab, fa)
}
// Ap returns a function that applies a function wrapped in a ReaderOption to a value.
// This is the curried version of MonadAp.
//
//go:inline
func Ap[B, E, A any](fa ReaderOption[E, A]) Operator[E, func(A) B, B] {
return readert.Ap[ReaderOption[E, A], ReaderOption[E, B], ReaderOption[E, func(A) B], E, A](O.Ap[B, A], fa)
}
// FromPredicate creates a Kleisli arrow that filters a value based on a predicate.
// If the predicate returns true, the value is wrapped in Some; otherwise, None is returned.
//
// Example:
//
// isPositive := readeroption.FromPredicate[Config](func(x int) bool { return x > 0 })
// result := F.Pipe1(
// readeroption.Of[Config](42),
// readeroption.Chain(isPositive),
// )
//
//go:inline
func FromPredicate[E, A any](pred func(A) bool) Kleisli[E, A, A] {
return fromoption.FromPredicate(FromOption[E, A], pred)
}
// Fold extracts the value from a ReaderOption by providing handlers for both cases.
// The onNone handler is called if the computation returns None.
// The onRight handler is called if the computation returns Some(a).
//
// Example:
//
// result := readeroption.Fold(
// func() reader.Reader[Config, string] { return reader.Of[Config]("not found") },
// func(user User) reader.Reader[Config, string] { return reader.Of[Config](user.Name) },
// )(findUser(123))
//
//go:inline
func Fold[E, A, B any](onNone Reader[E, B], onRight func(A) Reader[E, B]) func(ReaderOption[E, A]) Reader[E, B] {
return optiont.MatchE(reader.Chain[E, Option[A], B], function.Constant(onNone), onRight)
}
func MonadFold[E, A, B any](fa ReaderOption[E, A], onNone Reader[E, B], onRight func(A) Reader[E, B]) Reader[E, B] {
return optiont.MonadMatchE(fa, reader.MonadChain[E, Option[A], B], function.Constant(onNone), onRight)
}
// GetOrElse returns the value from a ReaderOption, or a default value if it's None.
//
// Example:
//
// result := readeroption.GetOrElse(
// func() reader.Reader[Config, User] { return reader.Of[Config](defaultUser) },
// )(findUser(123))
//
//go:inline
func GetOrElse[E, A any](onNone Reader[E, A]) func(ReaderOption[E, A]) Reader[E, A] {
return optiont.GetOrElse(reader.Chain[E, Option[A], A], function.Constant(onNone), reader.Of[E, A])
}
// Ask retrieves the current environment as a ReaderOption.
// This always succeeds and returns Some(environment).
//
// Example:
//
// getConfig := readeroption.Ask[Config, any]()
// result := getConfig(myConfig) // Returns option.Some(myConfig)
//
//go:inline
func Ask[E, L any]() ReaderOption[E, E] {
return fromreader.Ask(FromReader[E, E])()
}
// Asks creates a ReaderOption that applies a function to the environment.
// This always succeeds and returns Some(f(environment)).
//
// Example:
//
// getTimeout := readeroption.Asks(func(cfg Config) int { return cfg.Timeout })
// result := getTimeout(myConfig) // Returns option.Some(myConfig.Timeout)
//
//go:inline
func Asks[E, A any](r Reader[E, A]) ReaderOption[E, A] {
return fromreader.Asks(FromReader[E, A])(r)
}
// MonadChainOptionK chains a ReaderOption with a function that returns an Option.
// This is useful for integrating functions that return Option directly.
//
// Example:
//
// parseAge := func(s string) option.Option[int] { ... }
// result := readeroption.MonadChainOptionK(
// readeroption.Of[Config]("25"),
// parseAge,
// )
//
//go:inline
func MonadChainOptionK[E, A, B any](ma ReaderOption[E, A], f func(A) Option[B]) ReaderOption[E, B] {
return fromoption.MonadChainOptionK(
MonadChain[E, A, B],
FromOption[E, B],
ma,
f,
)
}
// ChainOptionK returns a function that chains a ReaderOption with a function returning an Option.
// This is the curried version of MonadChainOptionK.
//
// Example:
//
// parseAge := func(s string) option.Option[int] { ... }
// result := F.Pipe1(
// readeroption.Of[Config]("25"),
// readeroption.ChainOptionK[Config](parseAge),
// )
//
//go:inline
func ChainOptionK[E, A, B any](f func(A) Option[B]) Operator[E, A, B] {
return fromoption.ChainOptionK(
Chain[E, A, B],
FromOption[E, B],
f,
)
}
// Flatten removes one level of nesting from a ReaderOption.
// Converts ReaderOption[E, ReaderOption[E, A]] to ReaderOption[E, A].
//
// Example:
//
// nested := readeroption.Of[Config](readeroption.Of[Config](42))
// flattened := readeroption.Flatten(nested)
//
//go:inline
func Flatten[E, A any](mma ReaderOption[E, ReaderOption[E, A]]) ReaderOption[E, A] {
return MonadChain(mma, function.Identity[ReaderOption[E, A]])
}
// Local changes the value of the local context during the execution of the action `ma` (similar to `Contravariant`'s
// `contramap`).
//
// This allows you to transform the environment before passing it to a computation.
//
// Example:
//
// type GlobalConfig struct { DB DBConfig }
// type DBConfig struct { Host string }
//
// // A computation that needs DBConfig
// query := func(cfg DBConfig) option.Option[User] { ... }
//
// // Transform GlobalConfig to DBConfig
// result := readeroption.Local(func(g GlobalConfig) DBConfig { return g.DB })(
// readeroption.Asks(query),
// )
//
//go:inline
func Local[A, R2, R1 any](f func(R2) R1) func(ReaderOption[R1, A]) ReaderOption[R2, A] {
return reader.Local[Option[A]](f)
}
// Read applies a context to a reader to obtain its value.
// This executes the ReaderOption computation with the given environment.
//
// Example:
//
// ro := readeroption.Of[Config](42)
// result := readeroption.Read[int](myConfig)(ro) // Returns option.Some(42)
//
//go:inline
func Read[A, E any](e E) func(ReaderOption[E, A]) Option[A] {
return reader.Read[Option[A]](e)
}
// MonadFlap applies a value to a function wrapped in a ReaderOption.
// This is the reverse of MonadAp.
//
//go:inline
func MonadFlap[E, A, B any](fab ReaderOption[E, func(A) B], a A) ReaderOption[E, B] {
return functor.MonadFlap(MonadMap[E, func(A) B, B], fab, a)
}
// Flap returns a function that applies a value to a function wrapped in a ReaderOption.
// This is the curried version of MonadFlap.
//
//go:inline
func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
return functor.Flap(Map[E, func(A) B, B], a)
}
//go:inline
func MonadAlt[E, A any](fa ReaderOption[E, A], that ReaderOption[E, A]) ReaderOption[E, A] {
return MonadFold(fa, that, Of[E, A])
}
//go:inline
func Alt[E, A any](that ReaderOption[E, A]) Operator[E, A, A] {
return Fold(that, Of[E, A])
}

View File

@@ -0,0 +1,239 @@
// 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 (
"fmt"
"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/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
)
type MyContext string
const defaultContext MyContext = "default"
func TestMap(t *testing.T) {
g := F.Pipe1(
Of[MyContext](1),
Map[MyContext](utils.Double),
)
assert.Equal(t, O.Of(2), g(defaultContext))
}
func TestAp(t *testing.T) {
g := F.Pipe1(
Of[MyContext](utils.Double),
Ap[int](Of[MyContext](1)),
)
assert.Equal(t, O.Of(2), g(defaultContext))
}
func TestFlatten(t *testing.T) {
g := F.Pipe1(
Of[MyContext](Of[MyContext]("a")),
Flatten[MyContext, string],
)
assert.Equal(t, O.Of("a"), g(defaultContext))
}
func TestFromOption(t *testing.T) {
// Test with Some
opt1 := O.Of(42)
ro1 := FromOption[MyContext](opt1)
assert.Equal(t, O.Of(42), ro1(defaultContext))
// Test with None
opt2 := O.None[int]()
ro2 := FromOption[MyContext](opt2)
assert.Equal(t, O.None[int](), ro2(defaultContext))
}
func TestSome(t *testing.T) {
ro := Some[MyContext](42)
assert.Equal(t, O.Of(42), ro(defaultContext))
}
func TestFromReader(t *testing.T) {
reader := func(ctx MyContext) int {
return 42
}
ro := FromReader(reader)
assert.Equal(t, O.Of(42), ro(defaultContext))
}
func TestOf(t *testing.T) {
ro := Of[MyContext](42)
assert.Equal(t, O.Of(42), ro(defaultContext))
}
func TestNone(t *testing.T) {
ro := None[MyContext, int]()
assert.Equal(t, O.None[int](), ro(defaultContext))
}
func TestChain(t *testing.T) {
double := func(x int) ReaderOption[MyContext, int] {
return Of[MyContext](x * 2)
}
g := F.Pipe1(
Of[MyContext](21),
Chain(double),
)
assert.Equal(t, O.Of(42), g(defaultContext))
// Test with None
g2 := F.Pipe1(
None[MyContext, int](),
Chain(double),
)
assert.Equal(t, O.None[int](), g2(defaultContext))
}
func TestFromPredicate(t *testing.T) {
isPositive := FromPredicate[MyContext](func(x int) bool {
return x > 0
})
// Test with positive number
g1 := F.Pipe1(
Of[MyContext](42),
Chain(isPositive),
)
assert.Equal(t, O.Of(42), g1(defaultContext))
// Test with negative number
g2 := F.Pipe1(
Of[MyContext](-5),
Chain(isPositive),
)
assert.Equal(t, O.None[int](), g2(defaultContext))
}
func TestFold(t *testing.T) {
onNone := reader.Of[MyContext]("none")
onSome := func(x int) Reader[MyContext, string] {
return reader.Of[MyContext](fmt.Sprintf("%d", x))
}
// Test with Some
g1 := Fold(onNone, onSome)(Of[MyContext](42))
assert.Equal(t, "42", g1(defaultContext))
// Test with None
g2 := Fold(onNone, onSome)(None[MyContext, int]())
assert.Equal(t, "none", g2(defaultContext))
}
func TestGetOrElse(t *testing.T) {
defaultValue := reader.Of[MyContext](0)
// Test with Some
g1 := GetOrElse(defaultValue)(Of[MyContext](42))
assert.Equal(t, 42, g1(defaultContext))
// Test with None
g2 := GetOrElse(defaultValue)(None[MyContext, int]())
assert.Equal(t, 0, g2(defaultContext))
}
func TestAsk(t *testing.T) {
ro := Ask[MyContext, any]()
result := ro(defaultContext)
assert.Equal(t, O.Of(defaultContext), result)
}
func TestAsks(t *testing.T) {
reader := func(ctx MyContext) string {
return string(ctx)
}
ro := Asks(reader)
result := ro(defaultContext)
assert.Equal(t, O.Of("default"), result)
}
func TestChainOptionK(t *testing.T) {
parsePositive := func(x int) O.Option[int] {
if x > 0 {
return O.Of(x)
}
return O.None[int]()
}
// Test with positive number
g1 := F.Pipe1(
Of[MyContext](42),
ChainOptionK[MyContext](parsePositive),
)
assert.Equal(t, O.Of(42), g1(defaultContext))
// Test with negative number
g2 := F.Pipe1(
Of[MyContext](-5),
ChainOptionK[MyContext](parsePositive),
)
assert.Equal(t, O.None[int](), g2(defaultContext))
}
func TestLocal(t *testing.T) {
type GlobalContext struct {
Value string
}
// A computation that needs a string context
ro := Asks(func(s string) string {
return "Hello, " + s
})
// Transform GlobalContext to string
transformed := Local[string](func(g GlobalContext) string {
return g.Value
})(ro)
result := transformed(GlobalContext{Value: "World"})
assert.Equal(t, O.Of("Hello, World"), result)
}
func TestRead(t *testing.T) {
ro := Of[MyContext](42)
result := Read[int](defaultContext)(ro)
assert.Equal(t, O.Of(42), result)
}
func TestFlap(t *testing.T) {
addFunc := func(x int) int {
return x + 10
}
g := F.Pipe1(
Of[MyContext](addFunc),
Flap[MyContext, int](32),
)
assert.Equal(t, O.Of(42), g(defaultContext))
}

127
v2/readeroption/sequence.go Normal file
View File

@@ -0,0 +1,127 @@
// 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"
T "github.com/IBM/fp-go/v2/tuple"
)
// SequenceT functions convert multiple ReaderOption values into a single ReaderOption containing a tuple.
// If any input is None, the entire result is None.
// Otherwise, returns Some containing a tuple of all the unwrapped values.
//
// These functions are useful for combining multiple independent ReaderOption computations
// where you need to preserve the individual types of each result.
// SequenceT1 converts a single ReaderOption into a ReaderOption of a 1-tuple.
// This is mainly useful for consistency with the other SequenceT functions.
//
// Example:
//
// type Config struct { ... }
//
// user := readeroption.Of[Config](User{Name: "Alice"})
// result := readeroption.SequenceT1(user)
// // result(config) returns option.Some(tuple.MakeTuple1(User{Name: "Alice"}))
func SequenceT1[E, A any](a ReaderOption[E, A]) ReaderOption[E, T.Tuple1[A]] {
return G.SequenceT1[
ReaderOption[E, A],
ReaderOption[E, T.Tuple1[A]],
](a)
}
// SequenceT2 combines two ReaderOption values into a ReaderOption of a 2-tuple.
// If either input is None, the result is None.
//
// Example:
//
// type Config struct { ... }
//
// user := readeroption.Of[Config](User{Name: "Alice"})
// count := readeroption.Of[Config](42)
//
// result := readeroption.SequenceT2(user, count)
// // result(config) returns option.Some(tuple.MakeTuple2(User{Name: "Alice"}, 42))
//
// noneUser := readeroption.None[Config, User]()
// result2 := readeroption.SequenceT2(noneUser, count)
// // result2(config) returns option.None[tuple.Tuple2[User, int]]()
func SequenceT2[E, A, B any](
a ReaderOption[E, A],
b ReaderOption[E, B],
) ReaderOption[E, T.Tuple2[A, B]] {
return G.SequenceT2[
ReaderOption[E, A],
ReaderOption[E, B],
ReaderOption[E, T.Tuple2[A, B]],
](a, b)
}
// SequenceT3 combines three ReaderOption values into a ReaderOption of a 3-tuple.
// If any input is None, the result is None.
//
// Example:
//
// type Config struct { ... }
//
// user := readeroption.Of[Config](User{Name: "Alice"})
// count := readeroption.Of[Config](42)
// active := readeroption.Of[Config](true)
//
// result := readeroption.SequenceT3(user, count, active)
// // result(config) returns option.Some(tuple.MakeTuple3(User{Name: "Alice"}, 42, true))
func SequenceT3[E, A, B, C any](
a ReaderOption[E, A],
b ReaderOption[E, B],
c ReaderOption[E, C],
) ReaderOption[E, T.Tuple3[A, B, C]] {
return G.SequenceT3[
ReaderOption[E, A],
ReaderOption[E, B],
ReaderOption[E, C],
ReaderOption[E, T.Tuple3[A, B, C]],
](a, b, c)
}
// SequenceT4 combines four ReaderOption values into a ReaderOption of a 4-tuple.
// If any input is None, the result is None.
//
// Example:
//
// type Config struct { ... }
//
// user := readeroption.Of[Config](User{Name: "Alice"})
// count := readeroption.Of[Config](42)
// active := readeroption.Of[Config](true)
// score := readeroption.Of[Config](95.5)
//
// result := readeroption.SequenceT4(user, count, active, score)
// // result(config) returns option.Some(tuple.MakeTuple4(User{Name: "Alice"}, 42, true, 95.5))
func SequenceT4[E, A, B, C, D any](
a ReaderOption[E, A],
b ReaderOption[E, B],
c ReaderOption[E, C],
d ReaderOption[E, D],
) ReaderOption[E, T.Tuple4[A, B, C, D]] {
return G.SequenceT4[
ReaderOption[E, A],
ReaderOption[E, B],
ReaderOption[E, C],
ReaderOption[E, D],
ReaderOption[E, T.Tuple4[A, B, C, D]],
](a, b, c, d)
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readeroption
import (
"testing"
O "github.com/IBM/fp-go/v2/option"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
func TestSequenceT1(t *testing.T) {
t1 := Of[MyContext]("s1")
e1 := None[MyContext, string]()
res1 := SequenceT1(t1)
assert.Equal(t, O.Of(T.MakeTuple1("s1")), res1(defaultContext))
res2 := SequenceT1(e1)
assert.Equal(t, O.None[T.Tuple1[string]](), res2(defaultContext))
}
func TestSequenceT2(t *testing.T) {
t1 := Of[MyContext]("s1")
e1 := None[MyContext, string]()
t2 := Of[MyContext](2)
e2 := None[MyContext, int]()
res1 := SequenceT2(t1, t2)
assert.Equal(t, O.Of(T.MakeTuple2("s1", 2)), res1(defaultContext))
res2 := SequenceT2(e1, t2)
assert.Equal(t, O.None[T.Tuple2[string, int]](), res2(defaultContext))
res3 := SequenceT2(t1, e2)
assert.Equal(t, O.None[T.Tuple2[string, int]](), res3(defaultContext))
}
func TestSequenceT3(t *testing.T) {
t1 := Of[MyContext]("s1")
e1 := None[MyContext, string]()
t2 := Of[MyContext](2)
e2 := None[MyContext, int]()
t3 := Of[MyContext](true)
e3 := None[MyContext, bool]()
res1 := SequenceT3(t1, t2, t3)
assert.Equal(t, O.Of(T.MakeTuple3("s1", 2, true)), res1(defaultContext))
res2 := SequenceT3(e1, t2, t3)
assert.Equal(t, O.None[T.Tuple3[string, int, bool]](), res2(defaultContext))
res3 := SequenceT3(t1, e2, t3)
assert.Equal(t, O.None[T.Tuple3[string, int, bool]](), res3(defaultContext))
res4 := SequenceT3(t1, t2, e3)
assert.Equal(t, O.None[T.Tuple3[string, int, bool]](), res4(defaultContext))
}
func TestSequenceT4(t *testing.T) {
t1 := Of[MyContext]("s1")
t2 := Of[MyContext](2)
t3 := Of[MyContext](true)
t4 := Of[MyContext](1.0)
res := SequenceT4(t1, t2, t3, t4)
assert.Equal(t, O.Of(T.MakeTuple4("s1", 2, true, 1.0)), res(defaultContext))
}

92
v2/readeroption/types.go Normal file
View File

@@ -0,0 +1,92 @@
// 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 provides a monad transformer that combines the Reader and Option monads.
//
// ReaderOption[R, A] represents a computation that:
// - Depends on a shared environment of type R (Reader monad)
// - May fail to produce a value of type A (Option monad)
//
// This is useful for computations that need access to configuration, context, or dependencies
// while also being able to represent the absence of a value without using errors.
//
// The ReaderOption monad is defined as: Reader[R, Option[A]]
//
// Key operations:
// - Of: Wraps a value in a ReaderOption
// - None: Creates a ReaderOption representing no value
// - Map: Transforms the value inside a ReaderOption
// - Chain: Sequences ReaderOption computations
// - Ask/Asks: Accesses the environment
//
// Example:
//
// type Config struct {
// DatabaseURL string
// Timeout int
// }
//
// // A computation that may or may not find a user
// func findUser(id int) readeroption.ReaderOption[Config, User] {
// return readeroption.Asks(func(cfg Config) option.Option[User] {
// // Use cfg.DatabaseURL to query database
// // Return Some(user) if found, None() if not found
// })
// }
//
// // Chain multiple operations
// result := F.Pipe2(
// findUser(123),
// readeroption.Chain(func(user User) readeroption.ReaderOption[Config, Profile] {
// return loadProfile(user.ProfileID)
// }),
// readeroption.Map(func(profile Profile) string {
// return profile.DisplayName
// }),
// )
//
// // Execute with config
// config := Config{DatabaseURL: "localhost:5432", Timeout: 30}
// displayName := result(config) // Returns Option[string]
package readeroption
import (
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
)
type (
// Lazy represents a deferred computation
Lazy[A any] = lazy.Lazy[A]
// Option represents an optional value
Option[A any] = option.Option[A]
// Reader represents a computation that depends on an environment R and produces a value A
Reader[R, A any] = reader.Reader[R, A]
// ReaderOption represents a computation that depends on an environment R and may produce a value A.
// It combines the Reader monad (for dependency injection) with the Option monad (for optional values).
ReaderOption[R, A any] = Reader[R, Option[A]]
// Kleisli represents a function that takes a value A and returns a ReaderOption[R, B].
// This is the type of functions used with Chain/Bind operations.
Kleisli[R, A, B any] = Reader[A, ReaderOption[R, B]]
// Operator represents a function that transforms one ReaderOption into another.
// This is commonly used for lifting functions into the ReaderOption context.
Operator[R, A, B any] = Reader[ReaderOption[R, A], ReaderOption[R, B]]
)

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