1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-07 23:03:15 +02:00

Compare commits

...

10 Commits

Author SHA1 Message Date
Dr. Carsten Leue
54d5dbd04a fix: more tests for iso and prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 17:31:27 +01:00
Dr. Carsten Leue
51adce0c95 fix: better package import
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 16:15:16 +01:00
Dr. Carsten Leue
aa5e908810 fix: introduce Kleisli type
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 14:35:46 +01:00
Dr. Carsten Leue
b3bd5e9ad3 fix: bind docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 16:18:15 +01:00
Dr. Carsten Leue
178df09ff7 fix: document ApS
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 16:08:35 +01:00
Dr. Carsten Leue
92eb9715bd fix: implement some useful prisms
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 13:53:02 +01:00
Dr. Carsten Leue
41ebb04ae0 fix: upload coverage to coverall
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 11:45:01 +01:00
Dr. Carsten Leue
b2705e3adf fix: disable to version updates
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 11:39:17 +01:00
renovate[bot]
b232183e47 chore(config): migrate config renovate.json (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 11:26:23 +01:00
Dr. Carsten Leue
0f9f89f16d doc: improve documentation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 11:20:50 +01:00
136 changed files with 19804 additions and 5445 deletions

View File

@@ -41,6 +41,14 @@ jobs:
go mod tidy
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: ./coverage.txt
flag-name: v1-go-${{ matrix.go-version }}
parallel: true
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v5
# with:
@@ -72,6 +80,14 @@ jobs:
go mod tidy
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: ./v2/coverage.txt
flag-name: v2-go-${{ matrix.go-version }}
parallel: true
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v5
# with:
@@ -82,9 +98,22 @@ jobs:
# env:
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
coveralls-finish:
name: Finish Coveralls
needs:
- build-v1
- build-v2
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
release:
name: Release
needs:
needs:
- build-v1
- build-v2
if: github.repository == 'IBM/fp-go' && github.event_name != 'pull_request'

347
README.md
View File

@@ -1,207 +1,312 @@
# Functional programming library for golang
# fp-go: Functional Programming Library for Go
**🚧 Work in progress! 🚧** Despite major version 1 because of <https://github.com/semantic-release/semantic-release/issues/1507>. Trying to not make breaking changes, but devil is in the details.
[![Go Reference](https://pkg.go.dev/badge/github.com/IBM/fp-go.svg)](https://pkg.go.dev/github.com/IBM/fp-go)
[![Coverage Status](https://coveralls.io/repos/github/IBM/fp-go/badge.svg?branch=main)](https://coveralls.io/github/IBM/fp-go?branch=main)
**🚧 Work in progress! 🚧** Despite major version 1 (due to [semantic-release limitations](https://github.com/semantic-release/semantic-release/issues/1507)), we're working to minimize breaking changes.
![logo](resources/images/logo.png)
This library is strongly influenced by the awesome [fp-ts](https://github.com/gcanti/fp-ts).
A comprehensive functional programming library for Go, strongly influenced by the excellent [fp-ts](https://github.com/gcanti/fp-ts) library for TypeScript.
## Getting started
## 📚 Table of Contents
- [Getting Started](#-getting-started)
- [Design Goals](#-design-goals)
- [Core Concepts](#-core-concepts)
- [Comparison to Idiomatic Go](#comparison-to-idiomatic-go)
- [Implementation Notes](#implementation-notes)
- [Common Operations](#common-operations)
- [Resources](#-resources)
## 🚀 Getting Started
### Installation
```bash
go get github.com/IBM/fp-go
```
Refer to the [samples](./samples/).
### Quick Example
Find API documentation [here](https://pkg.go.dev/github.com/IBM/fp-go)
```go
import (
"errors"
"github.com/IBM/fp-go/either"
"github.com/IBM/fp-go/function"
)
## Design Goal
// Pure function that can fail
func divide(a, b int) either.Either[error, int] {
if b == 0 {
return either.Left[int](errors.New("division by zero"))
}
return either.Right[error](a / b)
}
This library aims to provide a set of data types and functions that make it easy and fun to write maintainable and testable code in golang. It encourages the following patterns:
// Compose operations safely
result := function.Pipe2(
divide(10, 2),
either.Map(func(x int) int { return x * 2 }),
either.GetOrElse(func() int { return 0 }),
)
// result = 10
```
- write many small, testable and pure functions, i.e. functions that produce output only depending on their input and that do not execute side effects
- offer helpers to isolate side effects into lazily executed functions (IO)
- expose a consistent set of composition to create new functions from existing ones
- for each data type there exists a small set of composition functions
- these functions are called the same across all data types, so you only have to learn a small number of function names
- the semantic of functions of the same name is consistent across all data types
### Resources
### How does this play with the [🧘🏽 Zen Of Go](https://the-zen-of-go.netlify.app/)?
- 📖 [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go)
- 💡 [Code Samples](./samples/)
- 🆕 [V2 Documentation](./v2/README.md) (requires Go 1.24+)
#### 🧘🏽 Each package fulfills a single purpose
## 🎯 Design Goals
✔️ Each of the top level packages (e.g. Option, Either, ReaderIOEither, ...) fulfills the purpose of defining the respective data type and implementing the set of common operations for this data type.
This library aims to provide a set of data types and functions that make it easy and fun to write maintainable and testable code in Go. It encourages the following patterns:
#### 🧘🏽 Handle errors explicitly
### Core Principles
✔️ The library makes a clear distinction between that operations that cannot fail by design and operations that can fail. Failure is represented via the `Either` type and errors are handled explicitly by using `Either`'s monadic set of operations.
- **Pure Functions**: Write many small, testable, and pure functions that produce output only depending on their input and execute no side effects
- **Side Effect Isolation**: Isolate side effects into lazily executed functions using the `IO` monad
- **Consistent Composition**: Expose a consistent set of composition functions across all data types
- Each data type has a small set of composition functions
- Functions are named consistently across all data types
- Semantics of same-named functions are consistent across data types
#### 🧘🏽 Return early rather than nesting deeply
### 🧘🏽 Alignment with the Zen of Go
✔️ We recommend to implement simple, small functions that implement one feature and that would typically not invoke other functions. Interaction with other functions is done by function composition and the composition makes sure to run one function after the other. In the error case the `Either` monad makes sure to skip the error path.
This library respects and aligns with [The Zen of Go](https://the-zen-of-go.netlify.app/):
#### 🧘🏽 Leave concurrency to the caller
| Principle | Alignment | Explanation |
|-----------|-----------|-------------|
| 🧘🏽 Each package fulfills a single purpose | ✔️ | Each top-level package (Option, Either, ReaderIOEither, etc.) defines one data type and its operations |
| 🧘🏽 Handle errors explicitly | ✔️ | Clear distinction between operations that can/cannot fail; failures represented via `Either` type |
| 🧘🏽 Return early rather than nesting deeply | ✔️ | Small, focused functions composed together; `Either` monad handles error paths automatically |
| 🧘🏽 Leave concurrency to the caller | ✔️ | Pure functions are synchronous; I/O operations are asynchronous by default |
| 🧘🏽 Before you launch a goroutine, know when it will stop | 🤷🏽 | Library doesn't start goroutines; Task monad supports cancellation via context |
| 🧘🏽 Avoid package level state | ✔️ | No package-level state anywhere |
| 🧘🏽 Simplicity matters | ✔️ | Small, consistent interface across data types; focus on business logic |
| 🧘🏽 Write tests to lock in behaviour | 🟡 | Programming pattern encourages testing; library has growing test coverage |
| 🧘🏽 If you think it's slow, first prove it with a benchmark | ✔️ | Performance claims should be backed by benchmarks |
| 🧘🏽 Moderation is a virtue | ✔️ | No custom goroutines or expensive synchronization; atomic counters for coordination |
| 🧘🏽 Maintainability counts | ✔️ | Small, concise operations; pure functions with clear type signatures |
✔️ All pure are synchronous by default. The I/O operations are asynchronous per default.
## 💡 Core Concepts
#### 🧘🏽 Before you launch a goroutine, know when it will stop
### Data Types
🤷🏽 This is left to the user of the library since the library itself will not start goroutines on its own. The Task monad offers support for cancellation via the golang context, though.
The library provides several key functional data types:
#### 🧘🏽 Avoid package level state
- **`Option[A]`**: Represents an optional value (Some or None)
- **`Either[E, A]`**: Represents a value that can be one of two types (Left for errors, Right for success)
- **`IO[A]`**: Represents a lazy computation that produces a value
- **`IOEither[E, A]`**: Represents a lazy computation that can fail
- **`Reader[R, A]`**: Represents a computation that depends on an environment
- **`ReaderIOEither[R, E, A]`**: Combines Reader, IO, and Either for effectful computations with dependencies
- **`Task[A]`**: Represents an asynchronous computation
- **`State[S, A]`**: Represents a stateful computation
✔️ No package level state anywhere, this would be a significant anti-pattern
### Monadic Operations
#### 🧘🏽 Simplicity matters
All data types support common monadic operations:
✔️ The library is simple in the sense that it offers a small, consistent interface to a variety of data types. Users can concentrate on implementing business logic rather than dealing with low level data structures.
#### 🧘🏽 Write tests to lock in the behaviour of your package’s API
🟡 The programming pattern suggested by this library encourages writing test cases. The library itself also has a growing number of tests, but not enough, yet. TBD
#### 🧘🏽 If you think it’s slow, first prove it with a benchmark
✔️ Absolutely. If you think the function composition offered by this library is too slow, please provide a benchmark.
#### 🧘🏽 Moderation is a virtue
✔️ The library does not implement its own goroutines and also does not require any expensive synchronization primitives. Coordination of IO operations is implemented via atomic counters without additional primitives.
#### 🧘🏽 Maintainability counts
✔️ Code that consumes this library is easy to maintain because of the small and concise set of operations exposed. Also the suggested programming paradigm to decompose an application into small functions increases maintainability, because these functions are easy to understand and if they are pure, it's often sufficient to look at the type signature to understand the purpose.
The library itself also comprises many small functions, but it's admittedly harder to maintain than code that uses it. However this asymmetry is intended because it offloads complexity from users into a central component.
- **`Map`**: Transform the value inside a context
- **`Chain`** (FlatMap): Transform and flatten nested contexts
- **`Ap`**: Apply a function in a context to a value in a context
- **`Of`**: Wrap a value in a context
- **`Fold`**: Extract a value from a context
## Comparison to Idiomatic Go
In this section we discuss how the functional APIs differ from idiomatic go function signatures and how to convert back and forth.
This section explains how functional APIs differ from idiomatic Go and how to convert between them.
### Pure functions
### Pure Functions
Pure functions are functions that take input parameters and that compute an output without changing any global state and without mutating the input parameters. They will always return the same output for the same input.
Pure functions take input parameters and compute output without changing global state or mutating inputs. They always return the same output for the same input.
#### Without Errors
If your pure function does not return an error, the idiomatic signature is just fine and no changes are required.
If your pure function doesn't return an error, the idiomatic signature works as-is:
```go
func add(a, b int) int {
return a + b
}
```
#### With Errors
If your pure function can return an error, then it will have a `(T, error)` return value in idiomatic go. In functional style the return value is [Either[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/either) because function composition is easier with such a return type. Use the `EitherizeXXX` methods in ["github.com/IBM/fp-go/either"](https://pkg.go.dev/github.com/IBM/fp-go/either) to convert from idiomatic to functional style and `UneitherizeXXX` to convert from functional to idiomatic style.
**Idiomatic Go:**
```go
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
```
### Effectful functions
**Functional Style:**
```go
func divide(a, b int) either.Either[error, int] {
if b == 0 {
return either.Left[int](errors.New("division by zero"))
}
return either.Right[error](a / b)
}
```
An effectful function (or function with a side effect) is one that changes data outside the scope of the function or that does not always produce the same output for the same input (because it depends on some external, mutable state). There is no special way in idiomatic go to identify such a function other than documentation. In functional style we represent them as functions that do not take an input but that produce an output. The base type for these functions is [IO[T]](https://pkg.go.dev/github.com/IBM/fp-go/io) because in many cases such functions represent `I/O` operations.
**Conversion:**
- Use `either.EitherizeXXX` to convert from idiomatic to functional style
- Use `either.UneitherizeXXX` to convert from functional to idiomatic style
### Effectful Functions
An effectful function changes data outside its scope or doesn't always produce the same output for the same input.
#### Without Errors
If your effectful function does not return an error, the functional signature is [IO[T]](https://pkg.go.dev/github.com/IBM/fp-go/io)
**Functional signature:** `IO[T]`
```go
func getCurrentTime() io.IO[time.Time] {
return func() time.Time {
return time.Now()
}
}
```
#### With Errors
If your effectful function can return an error, the functional signature is [IOEither[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/ioeither). Use `EitherizeXXX` from ["github.com/IBM/fp-go/ioeither"](https://pkg.go.dev/github.com/IBM/fp-go/ioeither) to convert an idiomatic go function to functional style.
**Functional signature:** `IOEither[error, T]`
```go
func readFile(path string) ioeither.IOEither[error, []byte] {
return func() either.Either[error, []byte] {
data, err := os.ReadFile(path)
if err != nil {
return either.Left[[]byte](err)
}
return either.Right[error](data)
}
}
```
**Conversion:**
- Use `ioeither.EitherizeXXX` to convert idiomatic Go functions to functional style
### Go Context
Functions that take a [context](https://pkg.go.dev/context) are per definition effectful because they depend on the context parameter that is designed to be mutable (it can e.g. be used to cancel a running operation). Furthermore in idiomatic go the parameter is typically passed as the first parameter to a function.
Functions that take a `context.Context` are effectful because they depend on mutable context.
In functional style we isolate the [context](https://pkg.go.dev/context) and represent the nature of the effectful function as an [IOEither[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/ioeither). The resulting type is [ReaderIOEither[T]](https://pkg.go.dev/github.com/IBM/fp-go/context/readerioeither), a function taking a [context](https://pkg.go.dev/context) that returns a function without parameters returning an [Either[error, T]](https://pkg.go.dev/github.com/IBM/fp-go/either). Use the `EitherizeXXX` methods from ["github.com/IBM/fp-go/context/readerioeither"](https://pkg.go.dev/github.com/IBM/fp-go/context/readerioeither) to convert an idiomatic go function with a [context](https://pkg.go.dev/context) to functional style.
**Idiomatic Go:**
```go
func fetchData(ctx context.Context, url string) ([]byte, error) {
// implementation
}
```
**Functional Style:**
```go
func fetchData(url string) readerioeither.ReaderIOEither[context.Context, error, []byte] {
return func(ctx context.Context) ioeither.IOEither[error, []byte] {
return func() either.Either[error, []byte] {
// implementation
}
}
}
```
**Conversion:**
- Use `readerioeither.EitherizeXXX` to convert idiomatic Go functions with context to functional style
## Implementation Notes
### Generics
All monadic operations are implemented via generics, i.e. they offer a type safe way to compose operations. This allows for convenient IDE support and also gives confidence about the correctness of the composition at compile time.
All monadic operations use Go generics for type safety:
Downside is that this will result in different versions of each operation per type, these versions are generated by the golang compiler at build time (unlike type erasure in languages such as Java of TypeScript). This might lead to large binaries for codebases with many different types. If this is a concern, you can always implement type erasure on top, i.e. use the monadic operations with the `any` type as if generics were not supported. You loose type safety, but this might result in smaller binaries.
-**Pros**: Type-safe composition, IDE support, compile-time correctness
- ⚠️ **Cons**: May result in larger binaries (different versions per type)
- 💡 **Tip**: For binary size concerns, use type erasure with `any` type
### Ordering of Generic Type Parameters
In go we need to specify all type parameters of a function on the global function definition, even if the function returns a higher order function and some of the type parameters are only applicable to the higher order function. So the following is not possible:
Go requires all type parameters on the global function definition. Parameters that cannot be auto-detected come first:
```go
func Map[A, B any](f func(A) B) [R, E any]func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
// Map: B cannot be auto-detected, so it comes first
func Map[R, E, A, B any](f func(A) B) func(ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
// Ap: B cannot be auto-detected from the argument
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
```
Note that the parameters `R` and `E` are not needed by the first level of `Map` but only by the resulting higher order function. Instead we need to specify the following:
This ordering maximizes type inference where possible.
```go
func Map[R, E, A, B any](f func(A) B) func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
```
### Use of the ~ Operator
which overspecifies `Map` on the global scope. As a result the go compiler will not be able to auto-detect these parameters, it can only auto detect `A` and `B` since they appear in the argument of `Map`. We need to explicitly pass values for these type parameters when `Map` is being used.
Because of this limitation the order of parameters on a function matters. We want to make sure that we define those parameters that cannot be auto-detected, first, and the parameters that can be auto-detected, last. This can lead to inconsistencies in parameter ordering, but we believe that the gain in convenience is worth it. The parameter order of `Ap` is e.g. different from that of `Map`:
```go
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(fab ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
```
because `R`, `E` and `A` can be determined from the argument to `Ap` but `B` cannot.
### Use of the [~ Operator](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md)
The FP library attempts to be easy to consume and one aspect of this is the definition of higher level type definitions instead of having to use their low level equivalent. It is e.g. more convenient and readable to use
```go
ReaderIOEither[R, E, A]
```
than
```go
func(R) func() Either.Either[E, A]
```
although both are logically equivalent. At the time of this writing the go type system does not support generic type aliases, only generic type definition, i.e. it is not possible to write:
```go
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
```
only
Go doesn't support generic type aliases (until Go 1.24), only type definitions. The `~` operator allows generic implementations to work with compatible types:
```go
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
```
This makes a big difference, because in the second case the type `ReaderIOEither[R, E, A any]` is considered a completely new type, not compatible to its right hand side, so it's not just a shortcut but a fully new type.
**Generic Subpackages:**
- Each higher-level type has a `generic` subpackage with fully generic implementations
- These are for library extensions, not end-users
- Main packages specialize generic implementations for convenience
From the implementation perspective however there is no reason to restrict the implementation to the new type, it can be generic for all compatible types. The way to express this in go is the [~](https://go.googlesource.com/proposal/+/master/design/47781-parameterized-go-ast.md) operator. This comes with some quite complicated type declarations in some cases, which undermines the goal of the library to be easy to use.
### Higher Kinded Types (HKT)
For that reason there exist sub-packages called `Generic` for all higher level types. These packages contain the fully generic implementation of the operations, preferring abstraction over usability. These packages are not meant to be used by end-users but are meant to be used by library extensions. The implementation for the convenient higher level types specializes the generic implementation for the particular higher level type, i.e. this layer does not contain any business logic but only *type magic*.
Go doesn't support HKT natively. This library addresses this by:
### Higher Kinded Types
- Introducing HKTs as individual types (e.g., `HKTA` for `HKT[A]`)
- Implementing generic algorithms in the `internal` package
- Keeping complexity hidden from end-users
Go does not support higher kinded types (HKT). Such types occur if a generic type itself is parametrized by another generic type. Example:
## Common Operations
The `Map` operation for `ReaderIOEither` is defined as:
### Map/Chain/Ap/Flap
| Operator | Parameter | Monad | Result | Use Case |
| -------- | ---------------- | --------------- | -------- | -------- |
| Map | `func(A) B` | `HKT[A]` | `HKT[B]` | Transform value in context |
| Chain | `func(A) HKT[B]` | `HKT[A]` | `HKT[B]` | Transform and flatten |
| Ap | `HKT[A]` | `HKT[func(A)B]` | `HKT[B]` | Apply function in context |
| Flap | `A` | `HKT[func(A)B]` | `HKT[B]` | Apply value to function in context |
### Example: Chaining Operations
```go
func Map[R, E, A, B any](f func(A) B) func(fa ReaderIOEither[R, E, A]) ReaderIOEither[R, E, B]
import (
"github.com/IBM/fp-go/either"
"github.com/IBM/fp-go/function"
)
result := function.Pipe3(
either.Right[error](10),
either.Map(func(x int) int { return x * 2 }),
either.Chain(func(x int) either.Either[error, int] {
if x > 15 {
return either.Right[error](x)
}
return either.Left[int](errors.New("too small"))
}),
either.GetOrElse(func() int { return 0 }),
)
```
and in fact the equivalent operations for all other monads follow the same pattern, we could try to introduce a new type for `ReaderIOEither` (without a parameter) as a HKT, e.g. like so (made-up syntax, does not work in go):
## 📚 Resources
```go
func Map[HKT, R, E, A, B any](f func(A) B) func(HKT[R, E, A]) HKT[R, E, B]
```
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go)
- [Code Samples](./samples/)
- [V2 Documentation](./v2/README.md) - New features in Go 1.24+
- [fp-ts](https://github.com/gcanti/fp-ts) - Original TypeScript inspiration
this would be the completely generic method signature for all possible monads. In particular in many cases it is possible to compose functions independent of the concrete knowledge of the actual `HKT`. From the perspective of a library this is the ideal situation because then a particular algorithm only has to be implemented and tested once.
## 🤝 Contributing
This FP library addresses this by introducing the HKTs as individual types, e.g. `HKT[A]` would be represented as a new generic type `HKTA`. This loses the correlation to the type `A` but allows to implement generic algorithms, at the price of readability.
Contributions are welcome! Please feel free to submit issues or pull requests.
For that reason these implementations are kept in the `internal` package. These are meant to be used by the library itself or by extensions, not by end users.
## 📄 License
## Map/Ap/Flap
The following table lists the relationship between some selected operators
| Operator | Parameter | Monad | Result |
| -------- | ---------------- | --------------- | -------- |
| Map | `func(A) B` | `HKT[A]` | `HKT[B]` |
| Chain | `func(A) HKT[B]` | `HKT[A]` | `HKT[B]` |
| Ap | `HKT[A]` | `HKT[func(A)B]` | `HKT[B]` |
| Flap | `A` | `HKT[func(A)B]` | `HKT[B]` |
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

View File

@@ -1,11 +1,20 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base",
"config:recommended",
":dependencyDashboard"
],
"rangeStrategy": "bump",
"packageRules": [
{
"matchDatasources": [
"golang-version"
],
"matchPackageNames": [
"go"
],
"enabled": false
},
{
"matchManagers": [
"gomod"
@@ -25,15 +34,6 @@
],
"automerge": true,
"groupName": "go dependencies"
},
{
"matchPackageNames": [
"conventional-changelog-conventionalcommits"
],
"matchUpdateTypes": [
"major"
],
"enabled": false
}
]
}

View File

@@ -1,43 +1,319 @@
# Functional programming library for golang V2
# fp-go V2: Enhanced Functional Programming for Go 1.24+
Go 1.24 introduces [generic type aliases](https://github.com/golang/go/issues/46477) which are leveraged by V2.
[![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)
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.
## 📚 Table of Contents
- [Requirements](#-requirements)
- [Breaking Changes](#-breaking-changes)
- [Key Improvements](#-key-improvements)
- [Migration Guide](#-migration-guide)
- [Installation](#-installation)
- [What's New](#-whats-new)
## 🔧 Requirements
- **Go 1.24 or later** (for generic type alias support)
## ⚠️ Breaking Changes
- use of [generic type aliases](https://github.com/golang/go/issues/46477) which requires [go1.24](https://tip.golang.org/doc/go1.24)
- order of generic type arguments adjusted such that types that _cannot_ be inferred by the method argument come first, e.g. in the `Ap` methods
- monadic operations for `Pair` operate on the second argument, to be compatible with the [Haskell](https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Data-Pair.html) definition
### 1. Generic Type Aliases
## Simplifications
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.
- use type aliases to get rid of namespace imports for type declarations, e.g. instead of
**V1:**
```go
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
```
**V2:**
```go
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
```
### 2. Generic Type Parameter Ordering
Type parameters that **cannot** be inferred from function arguments now come first, improving type inference.
**V1:**
```go
// Ap in V1 - less intuitive ordering
func Ap[R, E, A, B any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
```
**V2:**
```go
// Ap in V2 - B comes first as it cannot be inferred
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
```
This change allows the Go compiler to infer more types automatically, reducing the need for explicit type parameters.
### 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).
**V1:**
```go
// Operations on first element
pair := MakePair(1, "hello")
result := Map(func(x int) int { return x * 2 })(pair) // Pair(2, "hello")
```
**V2:**
```go
// Operations on second element (Haskell-compatible)
pair := MakePair(1, "hello")
result := Map(func(s string) string { return s + "!" })(pair) // Pair(1, "hello!")
```
## ✨ Key Improvements
### 1. Simplified Type Declarations
Generic type aliases eliminate the need for namespace imports in type declarations.
**V1 Approach:**
```go
import (
ET "github.com/IBM/fp-go/v2/either"
ET "github.com/IBM/fp-go/either"
OPT "github.com/IBM/fp-go/option"
)
func doSth() ET.Either[error, string] {
...
func processData(input string) ET.Either[error, OPT.Option[int]] {
// implementation
}
```
you can declare your type once
**V2 Approach:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
// Define type aliases once
type Either[A any] = either.Either[error, A]
type Option[A any] = option.Option[A]
// Use them throughout your codebase
func processData(input string) Either[Option[int]] {
// implementation
}
```
### 2. No More `generic` Subpackages
The library implementation no longer requires separate `generic` subpackages, making the codebase simpler and easier to understand.
**V1 Structure:**
```
either/
either.go
generic/
either.go // Generic implementation
```
**V2 Structure:**
```
either/
either.go // Single, clean implementation
```
### 3. Better Type Inference
The reordered type parameters allow the Go compiler to infer more types automatically:
**V1:**
```go
// Often need explicit type parameters
result := Map[Context, error, int, string](transform)(value)
```
**V2:**
```go
// Compiler can infer more types
result := Map(transform)(value) // Cleaner!
```
## 🚀 Migration Guide
### Step 1: Update Go Version
Ensure you're using Go 1.24 or later:
```bash
go version # Should show go1.24 or higher
```
### Step 2: Update Import Paths
Change all import paths from `github.com/IBM/fp-go` to `github.com/IBM/fp-go/v2`:
**Before:**
```go
import (
"github.com/IBM/fp-go/either"
"github.com/IBM/fp-go/option"
)
```
**After:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
```
### Step 3: Remove `generic` Subpackage Imports
If you were using generic subpackages, remove them:
**Before:**
```go
import (
E "github.com/IBM/fp-go/either/generic"
)
```
**After:**
```go
import (
"github.com/IBM/fp-go/v2/either"
)
type Either[A any] = either.Either[error, A]
```
and then use it across your codebase
### Step 4: Update Type Parameter Order
Review functions like `Ap` where type parameter order has changed. The compiler will help identify these:
**Before:**
```go
result := Ap[Context, error, int, string](value)(funcInContext)
```
**After:**
```go
result := Ap[string, Context, error, int](value)(funcInContext)
// Or better yet, let the compiler infer:
result := Ap(value)(funcInContext)
```
### Step 5: Update Pair Operations
If you're using `Pair`, update operations to work on the second element:
**Before (V1):**
```go
pair := MakePair(42, "data")
// Map operates on first element
result := Map(func(x int) int { return x * 2 })(pair)
```
**After (V2):**
```go
pair := MakePair(42, "data")
// Map operates on second element
result := Map(func(s string) string { return s + "!" })(pair)
```
### Step 6: Simplify Type Aliases
Create project-wide type aliases for common patterns:
```go
func doSth() Either[string] {
...
// types.go - Define once, use everywhere
package myapp
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ioeither"
)
type Either[A any] = either.Either[error, 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
```
## 🆕 What's New
### Cleaner API Surface
The elimination of `generic` subpackages means:
- Fewer imports to manage
- Simpler package structure
- Easier to navigate documentation
- More intuitive API
### Example: Before and After
**V1 Complex Example:**
```go
import (
ET "github.com/IBM/fp-go/either"
EG "github.com/IBM/fp-go/either/generic"
IOET "github.com/IBM/fp-go/ioeither"
IOEG "github.com/IBM/fp-go/ioeither/generic"
)
func process() IOET.IOEither[error, string] {
return IOEG.Map[error, int, string](
func(x int) string { return fmt.Sprintf("%d", x) },
)(fetchData())
}
```
- library implementation does no longer need to use the `generic` subpackage, this simplifies reading and understanding of the code
**V2 Simplified Example:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/ioeither"
)
type IOEither[A any] = ioeither.IOEither[error, A]
func process() IOEither[string] {
return ioeither.Map(
func(x int) string { return fmt.Sprintf("%d", x) },
)(fetchData())
}
```
## 📚 Additional Resources
- [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)
## 🤔 Should I Migrate?
**Migrate to V2 if:**
- ✅ You can use Go 1.24+
- ✅ You want cleaner, more maintainable code
- ✅ You want better type inference
- ✅ You're starting a new project
**Stay on V1 if:**
- ⚠️ You're locked to Go < 1.24
- ⚠️ Migration effort outweighs benefits for your project
- ⚠️ You need stability in production (V2 is newer)
## 🐛 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.

View File

@@ -21,14 +21,56 @@ import (
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// 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 {
// X int
// Y int
// }
// result := generic.Do[[]State, State](State{})
func Do[GS ~[]S, 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]
// 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.
// For arrays, this produces the cartesian product where later steps can use values from earlier steps.
//
// 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 {
// X int
// Y int
// }
//
// result := F.Pipe2(
// generic.Do[[]State, State](State{}),
// generic.Bind[[]State, []State, []int, State, State, int](
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// func(s State) []int {
// return []int{1, 2, 3}
// },
// ),
// generic.Bind[[]State, []State, []int, State, State, int](
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) []int {
// // This can access s.X from the previous step
// return []int{s.X * 10, s.X * 20}
// },
// ),
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
func Bind[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) GT,
@@ -75,7 +117,39 @@ func BindTo[GS1 ~[]S1, GT ~[]T, S1, T any](
)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
// 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. For arrays, this produces the cartesian product.
//
// Example:
//
// type State struct {
// X int
// Y string
// }
//
// // These operations are independent and can be combined with ApS
// xValues := []int{1, 2}
// yValues := []string{"a", "b"}
//
// result := F.Pipe2(
// generic.Do[[]State, State](State{}),
// generic.ApS[[]State, []State, []int, State, State, int](
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// xValues,
// ),
// generic.ApS[[]State, []State, []string, State, State, string](
// func(y string) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// yValues,
// ),
// ) // [{1,"a"}, {1,"b"}, {2,"a"}, {2,"b"}]
func ApS[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
setter func(T) func(S1) S2,
fa GT,

View File

@@ -403,5 +403,3 @@ func TestSlicePropertyBased(t *testing.T) {
}
})
}
// Made with Bob

View File

@@ -35,5 +35,6 @@ func Commands() []*C.Command {
IOCommand(),
IOOptionCommand(),
DICommand(),
LensCommand(),
}
}

524
v2/cli/lens.go Normal file
View File

@@ -0,0 +1,524 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"bytes"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"reflect"
"strings"
"text/template"
C "github.com/urfave/cli/v2"
)
const (
keyLensDir = "dir"
keyVerbose = "verbose"
lensAnnotation = "fp-go:Lens"
)
var (
flagLensDir = &C.StringFlag{
Name: keyLensDir,
Value: ".",
Usage: "Directory to scan for Go files",
}
flagVerbose = &C.BoolFlag{
Name: keyVerbose,
Aliases: []string{"v"},
Value: false,
Usage: "Enable verbose output",
}
)
// structInfo holds information about a struct that needs lens generation
type structInfo struct {
Name string
Fields []fieldInfo
Imports map[string]string // package path -> alias
}
// fieldInfo holds information about a struct field
type fieldInfo struct {
Name string
TypeName string
BaseType string // TypeName without leading * for pointer types
IsOptional bool // true if field is a pointer or has json omitempty tag
}
// templateData holds data for template rendering
type templateData struct {
PackageName string
Structs []structInfo
}
const lensStructTemplate = `
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
type {{.Name}}Lenses struct {
{{- range .Fields}}
{{.Name}} {{if .IsOptional}}LO.LensO[{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[{{$.Name}}, {{.TypeName}}]{{end}}
{{- end}}
}
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
type {{.Name}}RefLenses struct {
{{- range .Fields}}
{{.Name}} {{if .IsOptional}}LO.LensO[*{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[*{{$.Name}}, {{.TypeName}}]{{end}}
{{- end}}
}
`
const lensConstructorTemplate = `
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
func Make{{.Name}}Lenses() {{.Name}}Lenses {
{{- range .Fields}}
{{- if .IsOptional}}
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
{{- end}}
{{- end}}
return {{.Name}}Lenses{
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s {{$.Name}}, v O.Option[{{.TypeName}}]) {{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- else}}
{{.Name}}: L.MakeLens(
func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s },
),
{{- end}}
{{- end}}
}
}
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
func Make{{.Name}}RefLenses() {{.Name}}RefLenses {
{{- range .Fields}}
{{- if .IsOptional}}
iso{{.Name}} := I.FromZero[{{.TypeName}}]()
{{- end}}
{{- end}}
return {{.Name}}RefLenses{
{{- range .Fields}}
{{- if .IsOptional}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) O.Option[{{.TypeName}}] { return iso{{.Name}}.Get(s.{{.Name}}) },
func(s *{{$.Name}}, v O.Option[{{.TypeName}}]) *{{$.Name}} { s.{{.Name}} = iso{{.Name}}.ReverseGet(v); return s },
),
{{- else}}
{{.Name}}: L.MakeLensRef(
func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} },
func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s },
),
{{- end}}
{{- end}}
}
}
`
var (
structTmpl *template.Template
constructorTmpl *template.Template
)
func init() {
var err error
structTmpl, err = template.New("struct").Parse(lensStructTemplate)
if err != nil {
panic(err)
}
constructorTmpl, err = template.New("constructor").Parse(lensConstructorTemplate)
if err != nil {
panic(err)
}
}
// hasLensAnnotation checks if a comment group contains the lens annotation
func hasLensAnnotation(doc *ast.CommentGroup) bool {
if doc == nil {
return false
}
for _, comment := range doc.List {
if strings.Contains(comment.Text, lensAnnotation) {
return true
}
}
return false
}
// getTypeName extracts the type name from a field type expression
func getTypeName(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
return "*" + getTypeName(t.X)
case *ast.ArrayType:
return "[]" + getTypeName(t.Elt)
case *ast.MapType:
return "map[" + getTypeName(t.Key) + "]" + getTypeName(t.Value)
case *ast.SelectorExpr:
return getTypeName(t.X) + "." + t.Sel.Name
case *ast.InterfaceType:
return "interface{}"
case *ast.IndexExpr:
// Generic type with single type parameter (Go 1.18+)
// e.g., Option[string]
return getTypeName(t.X) + "[" + getTypeName(t.Index) + "]"
case *ast.IndexListExpr:
// Generic type with multiple type parameters (Go 1.18+)
// e.g., Map[string, int]
var params []string
for _, index := range t.Indices {
params = append(params, getTypeName(index))
}
return getTypeName(t.X) + "[" + strings.Join(params, ", ") + "]"
default:
return "any"
}
}
// extractImports extracts package imports from a type expression
// Returns a map of package path -> package name
func extractImports(expr ast.Expr, imports map[string]string) {
switch t := expr.(type) {
case *ast.StarExpr:
extractImports(t.X, imports)
case *ast.ArrayType:
extractImports(t.Elt, imports)
case *ast.MapType:
extractImports(t.Key, imports)
extractImports(t.Value, imports)
case *ast.SelectorExpr:
// This is a qualified identifier like "option.Option"
if ident, ok := t.X.(*ast.Ident); ok {
// ident.Name is the package name (e.g., "option")
// We need to track this for import resolution
imports[ident.Name] = ident.Name
}
case *ast.IndexExpr:
// Generic type with single type parameter
extractImports(t.X, imports)
extractImports(t.Index, imports)
case *ast.IndexListExpr:
// Generic type with multiple type parameters
extractImports(t.X, imports)
for _, index := range t.Indices {
extractImports(index, imports)
}
}
}
// hasOmitEmpty checks if a struct tag contains json omitempty
func hasOmitEmpty(tag *ast.BasicLit) bool {
if tag == nil {
return false
}
// Parse the struct tag
tagValue := strings.Trim(tag.Value, "`")
structTag := reflect.StructTag(tagValue)
jsonTag := structTag.Get("json")
// Check if omitempty is present
parts := strings.Split(jsonTag, ",")
for _, part := range parts {
if strings.TrimSpace(part) == "omitempty" {
return true
}
}
return false
}
// isPointerType checks if a type expression is a pointer
func isPointerType(expr ast.Expr) bool {
_, ok := expr.(*ast.StarExpr)
return ok
}
// parseFile parses a Go file and extracts structs with lens annotations
func parseFile(filename string) ([]structInfo, string, error) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
return nil, "", err
}
var structs []structInfo
packageName := node.Name.Name
// Build import map: package name -> import path
fileImports := make(map[string]string)
for _, imp := range node.Imports {
path := strings.Trim(imp.Path.Value, `"`)
var name string
if imp.Name != nil {
name = imp.Name.Name
} else {
// Extract package name from path (last component)
parts := strings.Split(path, "/")
name = parts[len(parts)-1]
}
fileImports[name] = path
}
// First pass: collect all GenDecls with their doc comments
declMap := make(map[*ast.TypeSpec]*ast.CommentGroup)
ast.Inspect(node, func(n ast.Node) bool {
if gd, ok := n.(*ast.GenDecl); ok {
for _, spec := range gd.Specs {
if ts, ok := spec.(*ast.TypeSpec); ok {
declMap[ts] = gd.Doc
}
}
}
return true
})
// Second pass: process type specs
ast.Inspect(node, func(n ast.Node) bool {
// Look for type declarations
typeSpec, ok := n.(*ast.TypeSpec)
if !ok {
return true
}
// Check if it's a struct type
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
return true
}
// Get the doc comment from our map
doc := declMap[typeSpec]
if !hasLensAnnotation(doc) {
return true
}
// Extract field information and collect imports
var fields []fieldInfo
structImports := make(map[string]string)
for _, field := range structType.Fields.List {
if len(field.Names) == 0 {
// Embedded field, skip for now
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
typeName := getTypeName(field.Type)
isOptional := false
baseType := typeName
// Check if field is optional:
// 1. Pointer types are always optional
// 2. Non-pointer types with json omitempty tag are optional
if isPointerType(field.Type) {
isOptional = true
// Strip leading * for base type
baseType = strings.TrimPrefix(typeName, "*")
} else if hasOmitEmpty(field.Tag) {
// Non-pointer type with omitempty is also optional
isOptional = true
}
// Extract imports from this field's type
fieldImports := make(map[string]string)
extractImports(field.Type, fieldImports)
// Resolve package names to full import paths
for pkgName := range fieldImports {
if importPath, ok := fileImports[pkgName]; ok {
structImports[importPath] = pkgName
}
}
fields = append(fields, fieldInfo{
Name: name.Name,
TypeName: typeName,
BaseType: baseType,
IsOptional: isOptional,
})
}
}
}
if len(fields) > 0 {
structs = append(structs, structInfo{
Name: typeSpec.Name.Name,
Fields: fields,
Imports: structImports,
})
}
return true
})
return structs, packageName, nil
}
// generateLensHelpers scans a directory for Go files and generates lens code
func generateLensHelpers(dir, filename string, verbose bool) error {
// Get absolute path
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
if verbose {
log.Printf("Scanning directory: %s", absDir)
}
// Find all Go files in the directory
files, err := filepath.Glob(filepath.Join(absDir, "*.go"))
if err != nil {
return err
}
if verbose {
log.Printf("Found %d Go files", len(files))
}
// Parse all files and collect structs
var allStructs []structInfo
var packageName string
for _, file := range files {
// Skip generated files and test files
if strings.HasSuffix(file, "_test.go") || strings.Contains(file, "gen.go") {
if verbose {
log.Printf("Skipping file: %s", filepath.Base(file))
}
continue
}
if verbose {
log.Printf("Parsing file: %s", filepath.Base(file))
}
structs, pkg, err := parseFile(file)
if err != nil {
log.Printf("Warning: failed to parse %s: %v", file, err)
continue
}
if verbose && len(structs) > 0 {
log.Printf("Found %d annotated struct(s) in %s", len(structs), filepath.Base(file))
for _, s := range structs {
log.Printf(" - %s (%d fields)", s.Name, len(s.Fields))
}
}
if packageName == "" {
packageName = pkg
}
allStructs = append(allStructs, structs...)
}
if len(allStructs) == 0 {
log.Printf("No structs with %s annotation found in %s", lensAnnotation, absDir)
return nil
}
// Collect all unique imports from all structs
allImports := make(map[string]string) // import path -> alias
for _, s := range allStructs {
for importPath, alias := range s.Imports {
allImports[importPath] = alias
}
}
// Create output file
outPath := filepath.Join(absDir, filename)
f, err := os.Create(filepath.Clean(outPath))
if err != nil {
return err
}
defer f.Close()
log.Printf("Generating lens code in [%s] for package [%s] with [%d] structs ...", outPath, packageName, len(allStructs))
// Write header
writePackage(f, packageName)
// Write imports
f.WriteString("import (\n")
// Standard fp-go imports always needed
f.WriteString("\tL \"github.com/IBM/fp-go/v2/optics/lens\"\n")
f.WriteString("\tLO \"github.com/IBM/fp-go/v2/optics/lens/option\"\n")
f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n")
f.WriteString("\tI \"github.com/IBM/fp-go/v2/optics/iso/option\"\n")
// Add additional imports collected from field types
for importPath, alias := range allImports {
f.WriteString("\t" + alias + " \"" + importPath + "\"\n")
}
f.WriteString(")\n")
// Generate lens code for each struct using templates
for _, s := range allStructs {
var buf bytes.Buffer
// Generate struct type
if err := structTmpl.Execute(&buf, s); err != nil {
return err
}
// Generate constructor
if err := constructorTmpl.Execute(&buf, s); err != nil {
return err
}
// Write to file
if _, err := f.Write(buf.Bytes()); err != nil {
return err
}
}
return nil
}
// LensCommand creates the CLI command for lens generation
func LensCommand() *C.Command {
return &C.Command{
Name: "lens",
Usage: "generate lens code for annotated structs",
Description: "Scans Go files for structs annotated with 'fp-go:Lens' and generates lens types. Pointer types and non-pointer types with json omitempty tag generate LensO (optional lens).",
Flags: []C.Flag{
flagLensDir,
flagFilename,
flagVerbose,
},
Action: func(ctx *C.Context) error {
return generateLensHelpers(
ctx.String(keyLensDir),
ctx.String(keyFilename),
ctx.Bool(keyVerbose),
)
},
}
}

503
v2/cli/lens_test.go Normal file
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 cli
import (
"bytes"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHasLensAnnotation(t *testing.T) {
tests := []struct {
name string
comment string
expected bool
}{
{
name: "has annotation",
comment: "// fp-go:Lens",
expected: true,
},
{
name: "has annotation with other text",
comment: "// This is a struct with fp-go:Lens annotation",
expected: true,
},
{
name: "no annotation",
comment: "// This is just a regular comment",
expected: false,
},
{
name: "nil comment",
comment: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var doc *ast.CommentGroup
if tt.comment != "" {
doc = &ast.CommentGroup{
List: []*ast.Comment{
{Text: tt.comment},
},
}
}
result := hasLensAnnotation(doc)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetTypeName(t *testing.T) {
tests := []struct {
name string
code string
expected string
}{
{
name: "simple type",
code: "type T struct { F string }",
expected: "string",
},
{
name: "pointer type",
code: "type T struct { F *string }",
expected: "*string",
},
{
name: "slice type",
code: "type T struct { F []int }",
expected: "[]int",
},
{
name: "map type",
code: "type T struct { F map[string]int }",
expected: "map[string]int",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
require.NoError(t, err)
var fieldType ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
fieldType = field.Type
return false
}
return true
})
require.NotNil(t, fieldType)
result := getTypeName(fieldType)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsPointerType(t *testing.T) {
tests := []struct {
name string
code string
expected bool
}{
{
name: "pointer type",
code: "type T struct { F *string }",
expected: true,
},
{
name: "non-pointer type",
code: "type T struct { F string }",
expected: false,
},
{
name: "slice type",
code: "type T struct { F []string }",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
require.NoError(t, err)
var fieldType ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
fieldType = field.Type
return false
}
return true
})
require.NotNil(t, fieldType)
result := isPointerType(fieldType)
assert.Equal(t, tt.expected, result)
})
}
}
func TestHasOmitEmpty(t *testing.T) {
tests := []struct {
name string
tag string
expected bool
}{
{
name: "has omitempty",
tag: "`json:\"field,omitempty\"`",
expected: true,
},
{
name: "has omitempty with other options",
tag: "`json:\"field,omitempty,string\"`",
expected: true,
},
{
name: "no omitempty",
tag: "`json:\"field\"`",
expected: false,
},
{
name: "no tag",
tag: "",
expected: false,
},
{
name: "different tag",
tag: "`xml:\"field\"`",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var tag *ast.BasicLit
if tt.tag != "" {
tag = &ast.BasicLit{
Value: tt.tag,
}
}
result := hasOmitEmpty(tag)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseFile(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Person struct {
Name string
Age int
Phone *string
}
// fp-go:Lens
type Address struct {
Street string
City string
}
// Not annotated
type Other struct {
Field string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 2)
// Check Person struct
person := structs[0]
assert.Equal(t, "Person", person.Name)
assert.Len(t, person.Fields, 3)
assert.Equal(t, "Name", person.Fields[0].Name)
assert.Equal(t, "string", person.Fields[0].TypeName)
assert.False(t, person.Fields[0].IsOptional)
assert.Equal(t, "Age", person.Fields[1].Name)
assert.Equal(t, "int", person.Fields[1].TypeName)
assert.False(t, person.Fields[1].IsOptional)
assert.Equal(t, "Phone", person.Fields[2].Name)
assert.Equal(t, "*string", person.Fields[2].TypeName)
assert.True(t, person.Fields[2].IsOptional)
// Check Address struct
address := structs[1]
assert.Equal(t, "Address", address.Name)
assert.Len(t, address.Fields, 2)
assert.Equal(t, "Street", address.Fields[0].Name)
assert.Equal(t, "City", address.Fields[1].Name)
}
func TestParseFileWithOmitEmpty(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Config struct {
Name string
Value string ` + "`json:\"value,omitempty\"`" + `
Count int ` + "`json:\",omitempty\"`" + `
Optional *string ` + "`json:\"optional,omitempty\"`" + `
Required int ` + "`json:\"required\"`" + `
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Config struct
config := structs[0]
assert.Equal(t, "Config", config.Name)
assert.Len(t, config.Fields, 5)
// Name - no tag, not optional
assert.Equal(t, "Name", config.Fields[0].Name)
assert.Equal(t, "string", config.Fields[0].TypeName)
assert.False(t, config.Fields[0].IsOptional)
// Value - has omitempty, should be optional
assert.Equal(t, "Value", config.Fields[1].Name)
assert.Equal(t, "string", config.Fields[1].TypeName)
assert.True(t, config.Fields[1].IsOptional, "Value field with omitempty should be optional")
// Count - has omitempty (no field name in tag), should be optional
assert.Equal(t, "Count", config.Fields[2].Name)
assert.Equal(t, "int", config.Fields[2].TypeName)
assert.True(t, config.Fields[2].IsOptional, "Count field with omitempty should be optional")
// Optional - pointer with omitempty, should be optional
assert.Equal(t, "Optional", config.Fields[3].Name)
assert.Equal(t, "*string", config.Fields[3].TypeName)
assert.True(t, config.Fields[3].IsOptional)
// Required - has json tag but no omitempty, not optional
assert.Equal(t, "Required", config.Fields[4].Name)
assert.Equal(t, "int", config.Fields[4].TypeName)
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
}
func TestGenerateLensHelpers(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type TestStruct struct {
Name string
Value *int
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "Code generated by go generate")
assert.Contains(t, contentStr, "TestStructLens")
assert.Contains(t, contentStr, "MakeTestStructLens")
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
assert.Contains(t, contentStr, "I.FromZero")
}
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// No annotation
type TestStruct struct {
Name string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code (should not create file)
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file does not exist
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
assert.True(t, os.IsNotExist(err))
}
func TestLensTemplates(t *testing.T) {
s := structInfo{
Name: "TestStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "*int", IsOptional: true},
},
}
// Test struct template
var structBuf bytes.Buffer
err := structTmpl.Execute(&structBuf, s)
require.NoError(t, err)
structStr := structBuf.String()
assert.Contains(t, structStr, "type TestStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[TestStruct, *int]")
// Test constructor template
var constructorBuf bytes.Buffer
err = constructorTmpl.Execute(&constructorBuf, s)
require.NoError(t, err)
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
assert.Contains(t, constructorStr, "return TestStructLenses{")
assert.Contains(t, constructorStr, "Name: L.MakeLens(")
assert.Contains(t, constructorStr, "Value: L.MakeLens(")
assert.Contains(t, constructorStr, "I.FromZero")
}
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
s := structInfo{
Name: "ConfigStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "string", IsOptional: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true}, // pointer
},
}
// Test struct template
var structBuf bytes.Buffer
err := structTmpl.Execute(&structBuf, s)
require.NoError(t, err)
structStr := structBuf.String()
assert.Contains(t, structStr, "type ConfigStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[ConfigStruct, string]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Count LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Pointer LO.LensO[ConfigStruct, *string]")
// Test constructor template
var constructorBuf bytes.Buffer
err = constructorTmpl.Execute(&constructorBuf, s)
require.NoError(t, err)
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
assert.Contains(t, constructorStr, "isoValue := I.FromZero[string]()")
assert.Contains(t, constructorStr, "isoCount := I.FromZero[int]()")
assert.Contains(t, constructorStr, "isoPointer := I.FromZero[*string]()")
}
func TestLensCommandFlags(t *testing.T) {
cmd := LensCommand()
assert.Equal(t, "lens", cmd.Name)
assert.Equal(t, "generate lens code for annotated structs", cmd.Usage)
assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens")
assert.Contains(t, strings.ToLower(cmd.Description), "lenso")
// Check flags
assert.Len(t, cmd.Flags, 3)
var hasDir, hasFilename, hasVerbose bool
for _, flag := range cmd.Flags {
switch flag.Names()[0] {
case "dir":
hasDir = true
case "filename":
hasFilename = true
case "verbose":
hasVerbose = true
}
}
assert.True(t, hasDir, "should have dir flag")
assert.True(t, hasFilename, "should have filename flag")
assert.True(t, hasVerbose, "should have verbose flag")
}

View File

@@ -18,12 +18,12 @@ package readereither
import "github.com/IBM/fp-go/v2/readereither"
// TraverseArray transforms an array
func TraverseArray[A, B any](f func(A) ReaderEither[B]) func([]A) ReaderEither[[]B] {
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return readereither.TraverseArray(f)
}
// TraverseArrayWithIndex transforms an array
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderEither[B]) func([]A) ReaderEither[[]B] {
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderEither[B]) Kleisli[[]A, []B] {
return readereither.TraverseArrayWithIndex(f)
}

View File

@@ -18,21 +18,71 @@ package readereither
import (
"context"
L "github.com/IBM/fp-go/v2/optics/lens"
G "github.com/IBM/fp-go/v2/readereither/generic"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// 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 {
// UserID string
// TenantID string
// }
// result := readereither.Do(State{})
func Do[S any](
empty S,
) ReaderEither[S] {
return G.Do[ReaderEither[S], context.Context, error, S](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// 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 context.Context from the 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 {
// UserID string
// TenantID string
// }
//
// result := F.Pipe2(
// readereither.Do(State{}),
// readereither.Bind(
// func(uid string) func(State) State {
// return func(s State) State { s.UserID = uid; return s }
// },
// func(s State) readereither.ReaderEither[string] {
// return func(ctx context.Context) either.Either[error, string] {
// if uid, ok := ctx.Value("userID").(string); ok {
// return either.Right[error](uid)
// }
// return either.Left[string](errors.New("no userID"))
// }
// },
// ),
// readereither.Bind(
// func(tid string) func(State) State {
// return func(s State) State { s.TenantID = tid; return s }
// },
// func(s State) readereither.ReaderEither[string] {
// // This can access s.UserID from the previous step
// return func(ctx context.Context) either.Either[error, string] {
// return either.Right[error]("tenant-" + s.UserID)
// }
// },
// ),
// )
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderEither[T],
) func(ReaderEither[S1]) ReaderEither[S2] {
f Kleisli[S1, T],
) Kleisli[ReaderEither[S1], S2] {
return G.Bind[ReaderEither[S1], ReaderEither[S2], ReaderEither[T], context.Context, error, S1, S2, T](setter, f)
}
@@ -40,7 +90,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(ReaderEither[S1]) ReaderEither[S2] {
) Kleisli[ReaderEither[S1], S2] {
return G.Let[ReaderEither[S1], ReaderEither[S2], context.Context, error, S1, S2, T](setter, f)
}
@@ -48,21 +98,212 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(ReaderEither[S1]) ReaderEither[S2] {
) Kleisli[ReaderEither[S1], S2] {
return G.LetTo[ReaderEither[S1], ReaderEither[S2], context.Context, error, S1, S2, T](setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[S1, T any](
setter func(T) S1,
) func(ReaderEither[T]) ReaderEither[S1] {
) Kleisli[ReaderEither[T], S1] {
return G.BindTo[ReaderEither[S1], ReaderEither[T], context.Context, error, S1, T](setter)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
// 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 {
// UserID string
// TenantID string
// }
//
// // These operations are independent and can be combined with ApS
// getUserID := func(ctx context.Context) either.Either[error, string] {
// return either.Right[error](ctx.Value("userID").(string))
// }
// getTenantID := func(ctx context.Context) either.Either[error, string] {
// return either.Right[error](ctx.Value("tenantID").(string))
// }
//
// result := F.Pipe2(
// readereither.Do(State{}),
// readereither.ApS(
// func(uid string) func(State) State {
// return func(s State) State { s.UserID = uid; return s }
// },
// getUserID,
// ),
// readereither.ApS(
// func(tid string) func(State) State {
// return func(s State) State { s.TenantID = tid; return s }
// },
// getTenantID,
// ),
// )
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderEither[T],
) func(ReaderEither[S1]) ReaderEither[S2] {
) Kleisli[ReaderEither[S1], S2] {
return G.ApS[ReaderEither[S1], ReaderEither[S2], ReaderEither[T], context.Context, error, S1, S2, T](setter, fa)
}
// ApSL is a variant of ApS that uses a lens to focus on a specific field in the state.
// Instead of providing a setter function, you provide a lens that knows how to get and set
// the field. This is more convenient when working with nested structures.
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - fa: A ReaderEither computation that produces a value of type T
//
// Returns:
// - A function that transforms ReaderEither[S] to ReaderEither[S] by setting the focused field
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// ageLens := lens.MakeLens(
// func(p Person) int { return p.Age },
// func(p Person, a int) Person { p.Age = a; return p },
// )
//
// getAge := func(ctx context.Context) either.Either[error, int] {
// return either.Right[error](30)
// }
//
// result := F.Pipe1(
// readereither.Do(Person{Name: "Alice", Age: 25}),
// readereither.ApSL(ageLens, getAge),
// )
func ApSL[S, T any](
lens L.Lens[S, T],
fa ReaderEither[T],
) Kleisli[ReaderEither[S], S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific field in the state.
// It combines the lens-based field access with monadic composition, allowing you to:
// 1. Extract a field value using the lens
// 2. Use that value in a computation that may fail
// 3. Update the field with the result
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - f: A function that takes the current field value and returns a ReaderEither computation
//
// Returns:
// - A function that transforms ReaderEither[S] to ReaderEither[S]
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// increment := func(v int) readereither.ReaderEither[int] {
// return func(ctx context.Context) either.Either[error, int] {
// if v >= 100 {
// return either.Left[int](errors.New("value too large"))
// }
// return either.Right[error](v + 1)
// }
// }
//
// result := F.Pipe1(
// readereither.Of[error](Counter{Value: 42}),
// readereither.BindL(valueLens, increment),
// )
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Kleisli[ReaderEither[S], S] {
return Bind[S, S, T](lens.Set, func(s S) ReaderEither[T] {
return f(lens.Get(s))
})
}
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
// It applies a pure transformation to the focused field without any effects.
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - f: A pure function that transforms the field value
//
// Returns:
// - A function that transforms ReaderEither[S] to ReaderEither[S]
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// readereither.Of[error](Counter{Value: 21}),
// readereither.LetL(valueLens, double),
// )
// // result when executed will be Right(Counter{Value: 42})
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Kleisli[ReaderEither[S], S] {
return Let[S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific field in the state.
// It sets the focused field to a constant value.
//
// Parameters:
// - lens: A lens that focuses on a field of type T within state S
// - b: The constant value to set
//
// Returns:
// - A function that transforms ReaderEither[S] to ReaderEither[S]
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// readereither.Of[error](Config{Debug: true, Timeout: 30}),
// readereither.LetToL(debugLens, false),
// )
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Kleisli[ReaderEither[S], S] {
return LetTo[S, S, T](lens.Set, b)
}

View File

@@ -28,26 +28,26 @@ func Curry0[A any](f func(context.Context) (A, error)) ReaderEither[A] {
return readereither.Curry0(f)
}
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderEither[A] {
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) Kleisli[T1, A] {
return readereither.Curry1(f)
}
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) func(T2) ReaderEither[A] {
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) Kleisli[T2, A] {
return readereither.Curry2(f)
}
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderEither[A] {
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) Kleisli[T3, A] {
return readereither.Curry3(f)
}
func Uncurry1[T1, A any](f func(T1) ReaderEither[A]) func(context.Context, T1) (A, error) {
func Uncurry1[T1, A any](f Kleisli[T1, A]) func(context.Context, T1) (A, error) {
return readereither.Uncurry1(f)
}
func Uncurry2[T1, T2, A any](f func(T1) func(T2) ReaderEither[A]) func(context.Context, T1, T2) (A, error) {
func Uncurry2[T1, T2, A any](f func(T1) Kleisli[T2, A]) func(context.Context, T1, T2) (A, error) {
return readereither.Uncurry2(f)
}
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderEither[A]) func(context.Context, T1, T2, T3) (A, error) {
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) Kleisli[T3, A]) func(context.Context, T1, T2, T3) (A, error) {
return readereither.Uncurry3(f)
}

View File

@@ -28,7 +28,7 @@ func From0[A any](f func(context.Context) (A, error)) func() ReaderEither[A] {
return readereither.From0(f)
}
func From1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderEither[A] {
func From1[T1, A any](f func(context.Context, T1) (A, error)) Kleisli[T1, A] {
return readereither.From1(f)
}

View File

@@ -41,11 +41,11 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
return readereither.Map[context.Context, error](f)
}
func MonadChain[A, B any](ma ReaderEither[A], f func(A) ReaderEither[B]) ReaderEither[B] {
func MonadChain[A, B any](ma ReaderEither[A], f Kleisli[A, B]) ReaderEither[B] {
return readereither.MonadChain(ma, f)
}
func Chain[A, B any](f func(A) ReaderEither[B]) Operator[A, B] {
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return readereither.Chain(f)
}
@@ -61,11 +61,11 @@ func Ap[A, B any](fa ReaderEither[A]) func(ReaderEither[func(A) B]) ReaderEither
return readereither.Ap[B](fa)
}
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) ReaderEither[A] {
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
return readereither.FromPredicate[context.Context](pred, onFalse)
}
func OrElse[A any](onLeft func(error) ReaderEither[A]) func(ReaderEither[A]) ReaderEither[A] {
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderEither[A], A] {
return readereither.OrElse(onLeft)
}

View File

@@ -31,5 +31,6 @@ type (
// ReaderEither is a specialization of the Reader monad for the typical golang scenario
ReaderEither[A any] = readereither.ReaderEither[context.Context, error, A]
Operator[A, B any] = reader.Reader[ReaderEither[A], ReaderEither[B]]
Kleisli[A, B any] = reader.Reader[A, ReaderEither[B]]
Operator[A, B any] = Kleisli[ReaderEither[A], B]
)

View File

@@ -19,20 +19,75 @@ import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// 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
// }
// result := readerioeither.Do(State{})
//
//go:inline
func Do[S any](
empty S,
) ReaderIOEither[S] {
return Of(empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// 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 context.Context from the 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
// }
//
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerioeither.ReaderIOEither[User] {
// return func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// },
// ),
// readerioeither.Bind(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) readerioeither.ReaderIOEither[Config] {
// // This can access s.User from the previous step
// return func(ctx context.Context) ioeither.IOEither[error, Config] {
// return ioeither.TryCatch(func() (Config, error) {
// return fetchConfigForUser(ctx, s.User.ID)
// })
// }
// },
// ),
// )
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderIOEither[T],
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
f Kleisli[S1, T],
) Operator[S1, S2] {
return chain.Bind(
Chain[S1, S2],
Map[T, S2],
@@ -42,10 +97,12 @@ func Bind[S1, S2, T any](
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
//
//go:inline
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
) Operator[S1, S2] {
return functor.Let(
Map[S1, S2],
setter,
@@ -54,10 +111,12 @@ func Let[S1, S2, T any](
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
//
//go:inline
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
) Operator[S1, S2] {
return functor.LetTo(
Map[S1, S2],
setter,
@@ -66,6 +125,8 @@ func LetTo[S1, S2, T any](
}
// BindTo initializes a new state [S1] from a value [T]
//
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
@@ -75,11 +136,53 @@ func BindTo[S1, T any](
)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
// 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
// }
//
// // These operations are independent and can be combined with ApS
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// getConfig := func(ctx context.Context) ioeither.IOEither[error, Config] {
// return ioeither.TryCatch(func() (Config, error) {
// return fetchConfig(ctx)
// })
// }
//
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.ApS(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// readerioeither.ApS(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// getConfig,
// ),
// )
//
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIOEither[T],
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
) Operator[S1, S2] {
return apply.ApS(
Ap[S2, T],
Map[S1, func(T) S2],
@@ -87,3 +190,152 @@ func ApS[S1, S2, T any](
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
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// result := F.Pipe2(
// readerioeither.Of(State{}),
// readerioeither.ApSL(userLens, getUser),
// )
//
//go:inline
func ApSL[S, T any](
lens L.Lens[S, T],
fa ReaderIOEither[T],
) Operator[S, 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 ReaderIOEither computation that produces an updated value.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.BindL(userLens, func(user User) readerioeither.ReaderIOEither[User] {
// return func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// }),
// )
//
//go:inline
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return Bind[S, S, T](lens.Set, func(s S) ReaderIOEither[T] {
return f(lens.Get(s))
})
}
// 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 ReaderIOEither).
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readerioeither.Do(State{User: User{Name: "Alice"}}),
// readerioeither.LetL(userLens, func(user User) User {
// user.Name = "Bob"
// return user
// }),
// )
//
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Operator[S, S] {
return Let[S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// 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
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// newUser := User{Name: "Bob", ID: 123}
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.LetToL(userLens, newUser),
// )
//
//go:inline
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Operator[S, S] {
return LetTo[S, S, T](lens.Set, b)
}

View File

@@ -22,6 +22,7 @@ import (
E "github.com/IBM/fp-go/v2/either"
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"
)
@@ -42,7 +43,7 @@ func TestBind(t *testing.T) {
Map(utils.GetFullName),
)
assert.Equal(t, res(context.Background())(), E.Of[error]("John Doe"))
assert.Equal(t, res(t.Context())(), E.Of[error]("John Doe"))
}
func TestApS(t *testing.T) {
@@ -54,5 +55,221 @@ func TestApS(t *testing.T) {
Map(utils.GetFullName),
)
assert.Equal(t, res(context.Background())(), E.Of[error]("John Doe"))
assert.Equal(t, res(t.Context())(), E.Of[error]("John Doe"))
}
func TestApS_WithError(t *testing.T) {
// Test that ApS propagates errors correctly
testErr := assert.AnError
res := F.Pipe3(
Do(utils.Empty),
ApS(utils.SetLastName, Left[string](testErr)),
ApS(utils.SetGivenName, Of("John")),
Map(utils.GetFullName),
)
result := res(t.Context())()
assert.True(t, E.IsLeft(result))
assert.Equal(t, testErr, E.ToError(result))
}
func TestApS_WithSecondError(t *testing.T) {
// Test that ApS propagates errors from the second operation
testErr := assert.AnError
res := F.Pipe3(
Do(utils.Empty),
ApS(utils.SetLastName, Of("Doe")),
ApS(utils.SetGivenName, Left[string](testErr)),
Map(utils.GetFullName),
)
result := res(t.Context())()
assert.True(t, E.IsLeft(result))
assert.Equal(t, testErr, E.ToError(result))
}
func TestApS_MultipleFields(t *testing.T) {
// Test ApS with more than two fields
type Person struct {
FirstName string
MiddleName string
LastName string
Age int
}
setFirstName := func(s string) func(Person) Person {
return func(p Person) Person {
p.FirstName = s
return p
}
}
setMiddleName := func(s string) func(Person) Person {
return func(p Person) Person {
p.MiddleName = s
return p
}
}
setLastName := func(s string) func(Person) Person {
return func(p Person) Person {
p.LastName = s
return p
}
}
setAge := func(a int) func(Person) Person {
return func(p Person) Person {
p.Age = a
return p
}
}
res := F.Pipe5(
Do(Person{}),
ApS(setFirstName, Of("John")),
ApS(setMiddleName, Of("Q")),
ApS(setLastName, Of("Doe")),
ApS(setAge, Of(42)),
Map(func(p Person) Person { return p }),
)
result := res(t.Context())()
assert.True(t, E.IsRight(result))
person := E.ToOption(result)
assert.True(t, O.IsSome(person))
p, _ := O.Unwrap(person)
assert.Equal(t, "John", p.FirstName)
assert.Equal(t, "Q", p.MiddleName)
assert.Equal(t, "Doe", p.LastName)
assert.Equal(t, 42, p.Age)
}
func TestApS_WithDifferentTypes(t *testing.T) {
// Test ApS with different value types
type State struct {
Name string
Count int
Flag bool
}
setName := func(s string) func(State) State {
return func(st State) State {
st.Name = s
return st
}
}
setCount := func(c int) func(State) State {
return func(st State) State {
st.Count = c
return st
}
}
setFlag := func(f bool) func(State) State {
return func(st State) State {
st.Flag = f
return st
}
}
res := F.Pipe4(
Do(State{}),
ApS(setName, Of("test")),
ApS(setCount, Of(100)),
ApS(setFlag, Of(true)),
Map(func(s State) State { return s }),
)
result := res(t.Context())()
assert.True(t, E.IsRight(result))
stateOpt := E.ToOption(result)
assert.True(t, O.IsSome(stateOpt))
state, _ := O.Unwrap(stateOpt)
assert.Equal(t, "test", state.Name)
assert.Equal(t, 100, state.Count)
assert.True(t, state.Flag)
}
func TestApS_EmptyState(t *testing.T) {
// Test ApS starting with an empty state
type Empty struct{}
res := Do(Empty{})
result := res(t.Context())()
assert.True(t, E.IsRight(result))
emptyOpt := E.ToOption(result)
assert.True(t, O.IsSome(emptyOpt))
empty, _ := O.Unwrap(emptyOpt)
assert.Equal(t, Empty{}, empty)
}
func TestApS_ChainedWithBind(t *testing.T) {
// Test mixing ApS with Bind operations
type State struct {
Independent string
Dependent string
}
setIndependent := func(s string) func(State) State {
return func(st State) State {
st.Independent = s
return st
}
}
setDependent := func(s string) func(State) State {
return func(st State) State {
st.Dependent = s
return st
}
}
getDependentValue := func(s State) ReaderIOEither[string] {
// This depends on the Independent field
return Of(s.Independent + "-dependent")
}
res := F.Pipe3(
Do(State{}),
ApS(setIndependent, Of("value")),
Bind(setDependent, getDependentValue),
Map(func(s State) State { return s }),
)
result := res(t.Context())()
assert.True(t, E.IsRight(result))
stateOpt := E.ToOption(result)
assert.True(t, O.IsSome(stateOpt))
state, _ := O.Unwrap(stateOpt)
assert.Equal(t, "value", state.Independent)
assert.Equal(t, "value-dependent", state.Dependent)
}
func TestApS_WithContextCancellation(t *testing.T) {
// Test that ApS respects context cancellation
type State struct {
Value string
}
setValue := func(s string) func(State) State {
return func(st State) State {
st.Value = s
return st
}
}
// Create a computation that would succeed
computation := ApS(setValue, Of("test"))(Do(State{}))
// Create a cancelled context
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := computation(ctx)()
assert.True(t, E.IsLeft(result))
}

View File

@@ -28,7 +28,7 @@ func Bracket[
A, B, ANY any](
acquire ReaderIOEither[A],
use func(A) ReaderIOEither[B],
use Kleisli[A, B],
release func(A, Either[B]) ReaderIOEither[ANY],
) ReaderIOEither[B] {
return bracket.Bracket[ReaderIOEither[A], ReaderIOEither[B], ReaderIOEither[ANY], Either[B], A, B](

View File

@@ -39,6 +39,8 @@ import (
// eqRIE := Eq(eqInt)
// ctx := context.Background()
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
//
//go:inline
func Eq[A any](eq eq.Eq[Either[A]]) func(context.Context) eq.Eq[ReaderIOEither[A]] {
return RIOE.Eq[context.Context](eq)
}

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,36 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package builder provides utilities for building HTTP requests in a functional way
// using the ReaderIOEither monad. It integrates with the http/builder package to
// create composable, type-safe HTTP request builders with proper error handling
// and context support.
//
// The main function, Requester, converts a Builder from the http/builder package
// into a ReaderIOEither that produces HTTP requests. This allows for:
// - Immutable request building with method chaining
// - Automatic header management including Content-Length
// - Support for requests with and without bodies
// - Proper error handling wrapped in Either
// - Context propagation for cancellation and timeouts
//
// Example usage:
//
// import (
// "context"
// B "github.com/IBM/fp-go/v2/http/builder"
// RB "github.com/IBM/fp-go/v2/context/readerioeither/http/builder"
// )
//
// builder := F.Pipe3(
// B.Default,
// B.WithURL("https://api.example.com/users"),
// B.WithMethod("POST"),
// B.WithJSONBody(userData),
// )
//
// requester := RB.Requester(builder)
// result := requester(context.Background())()
package builder
import (
@@ -31,6 +61,59 @@ import (
O "github.com/IBM/fp-go/v2/option"
)
// Requester converts an http/builder.Builder into a ReaderIOEither that produces HTTP requests.
// It handles both requests with and without bodies, automatically managing headers including
// Content-Length for requests with bodies.
//
// The function performs the following operations:
// 1. Extracts the request body (if present) from the builder
// 2. Creates appropriate request constructor (with or without body)
// 3. Applies the target URL from the builder
// 4. Applies the HTTP method from the builder
// 5. Merges headers from the builder into the request
// 6. Handles any errors that occur during request construction
//
// For requests with a body:
// - Sets the Content-Length header automatically
// - Uses bytes.NewReader to create the request body
// - Merges builder headers into the request
//
// For requests without a body:
// - Creates a request with nil body
// - Merges builder headers into the request
//
// Parameters:
// - builder: A pointer to an http/builder.Builder containing request configuration
//
// Returns:
// - A Requester (ReaderIOEither[*http.Request]) that, when executed with a context,
// produces either an error or a configured *http.Request
//
// Example with body:
//
// import (
// B "github.com/IBM/fp-go/v2/http/builder"
// RB "github.com/IBM/fp-go/v2/context/readerioeither/http/builder"
// )
//
// builder := F.Pipe3(
// B.Default,
// B.WithURL("https://api.example.com/users"),
// B.WithMethod("POST"),
// B.WithJSONBody(map[string]string{"name": "John"}),
// )
// requester := RB.Requester(builder)
// result := requester(context.Background())()
//
// Example without body:
//
// builder := F.Pipe2(
// B.Default,
// B.WithURL("https://api.example.com/users"),
// B.WithMethod("GET"),
// )
// requester := RB.Requester(builder)
// result := requester(context.Background())()
func Requester(builder *R.Builder) RIOEH.Requester {
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOEither[*http.Request] {

View File

@@ -57,3 +57,231 @@ func TestBuilderWithQuery(t *testing.T) {
assert.True(t, E.IsRight(req(context.Background())()))
}
// TestBuilderWithoutBody tests creating a request without a body
func TestBuilderWithoutBody(t *testing.T) {
builder := F.Pipe2(
R.Default,
R.WithURL("https://api.example.com/users"),
R.WithMethod("GET"),
)
requester := Requester(builder)
result := requester(context.Background())()
assert.True(t, E.IsRight(result), "Expected Right result")
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
assert.NotNil(t, req, "Expected non-nil request")
assert.Equal(t, "GET", req.Method)
assert.Equal(t, "https://api.example.com/users", req.URL.String())
assert.Nil(t, req.Body, "Expected nil body for GET request")
}
// TestBuilderWithBody tests creating a request with a body
func TestBuilderWithBody(t *testing.T) {
bodyData := []byte(`{"name":"John","age":30}`)
builder := F.Pipe3(
R.Default,
R.WithURL("https://api.example.com/users"),
R.WithMethod("POST"),
R.WithBytes(bodyData),
)
requester := Requester(builder)
result := requester(context.Background())()
assert.True(t, E.IsRight(result), "Expected Right result")
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
assert.NotNil(t, req, "Expected non-nil request")
assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://api.example.com/users", req.URL.String())
assert.NotNil(t, req.Body, "Expected non-nil body for POST request")
assert.Equal(t, "24", req.Header.Get("Content-Length"))
}
// TestBuilderWithHeaders tests that headers are properly set
func TestBuilderWithHeaders(t *testing.T) {
builder := F.Pipe3(
R.Default,
R.WithURL("https://api.example.com/data"),
R.WithHeader("Authorization")("Bearer token123"),
R.WithHeader("Accept")("application/json"),
)
requester := Requester(builder)
result := requester(context.Background())()
assert.True(t, E.IsRight(result), "Expected Right result")
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
assert.NotNil(t, req, "Expected non-nil request")
assert.Equal(t, "Bearer token123", req.Header.Get("Authorization"))
assert.Equal(t, "application/json", req.Header.Get("Accept"))
}
// TestBuilderWithInvalidURL tests error handling for invalid URLs
func TestBuilderWithInvalidURL(t *testing.T) {
builder := F.Pipe1(
R.Default,
R.WithURL("://invalid-url"),
)
requester := Requester(builder)
result := requester(context.Background())()
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
}
// TestBuilderWithEmptyMethod tests creating a request with empty method
func TestBuilderWithEmptyMethod(t *testing.T) {
builder := F.Pipe2(
R.Default,
R.WithURL("https://api.example.com/users"),
R.WithMethod(""),
)
requester := Requester(builder)
result := requester(context.Background())()
// Empty method should still work (defaults to GET in http.NewRequest)
assert.True(t, E.IsRight(result), "Expected Right result")
}
// TestBuilderWithMultipleHeaders tests setting multiple headers
func TestBuilderWithMultipleHeaders(t *testing.T) {
builder := F.Pipe4(
R.Default,
R.WithURL("https://api.example.com/data"),
R.WithHeader("X-Custom-Header-1")("value1"),
R.WithHeader("X-Custom-Header-2")("value2"),
R.WithHeader("X-Custom-Header-3")("value3"),
)
requester := Requester(builder)
result := requester(context.Background())()
assert.True(t, E.IsRight(result), "Expected Right result")
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
assert.NotNil(t, req, "Expected non-nil request")
assert.Equal(t, "value1", req.Header.Get("X-Custom-Header-1"))
assert.Equal(t, "value2", req.Header.Get("X-Custom-Header-2"))
assert.Equal(t, "value3", req.Header.Get("X-Custom-Header-3"))
}
// TestBuilderWithBodyAndHeaders tests combining body and headers
func TestBuilderWithBodyAndHeaders(t *testing.T) {
bodyData := []byte(`{"test":"data"}`)
builder := F.Pipe4(
R.Default,
R.WithURL("https://api.example.com/submit"),
R.WithMethod("PUT"),
R.WithBytes(bodyData),
R.WithHeader("X-Request-ID")("12345"),
)
requester := Requester(builder)
result := requester(context.Background())()
assert.True(t, E.IsRight(result), "Expected Right result")
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
assert.NotNil(t, req, "Expected non-nil request")
assert.Equal(t, "PUT", req.Method)
assert.NotNil(t, req.Body, "Expected non-nil body")
assert.Equal(t, "12345", req.Header.Get("X-Request-ID"))
assert.Equal(t, "15", req.Header.Get("Content-Length"))
}
// TestBuilderContextCancellation tests that context cancellation is respected
func TestBuilderContextCancellation(t *testing.T) {
builder := F.Pipe1(
R.Default,
R.WithURL("https://api.example.com/users"),
)
requester := Requester(builder)
// Create a cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
result := requester(ctx)()
// The request should still be created (cancellation affects execution, not creation)
// But we verify the context is properly passed
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
if req != nil {
assert.Equal(t, ctx, req.Context(), "Expected context to be set in request")
}
}
// TestBuilderWithDifferentMethods tests various HTTP methods
func TestBuilderWithDifferentMethods(t *testing.T) {
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
for _, method := range methods {
t.Run(method, func(t *testing.T) {
builder := F.Pipe2(
R.Default,
R.WithURL("https://api.example.com/resource"),
R.WithMethod(method),
)
requester := Requester(builder)
result := requester(context.Background())()
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
assert.NotNil(t, req, "Expected non-nil request for method %s", method)
assert.Equal(t, method, req.Method)
})
}
}
// TestBuilderWithJSON tests creating a request with JSON body
func TestBuilderWithJSON(t *testing.T) {
data := map[string]string{"username": "testuser", "email": "test@example.com"}
builder := F.Pipe3(
R.Default,
R.WithURL("https://api.example.com/v1/users"),
R.WithMethod("POST"),
R.WithJSON(data),
)
requester := Requester(builder)
result := requester(context.Background())()
assert.True(t, E.IsRight(result), "Expected Right result")
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
assert.NotNil(t, req, "Expected non-nil request")
assert.Equal(t, "POST", req.Method)
assert.Equal(t, "https://api.example.com/v1/users", req.URL.String())
assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
assert.NotNil(t, req.Body)
}
// TestBuilderWithBearer tests adding Bearer token
func TestBuilderWithBearer(t *testing.T) {
builder := F.Pipe2(
R.Default,
R.WithURL("https://api.example.com/protected"),
R.WithBearer("my-secret-token"),
)
requester := Requester(builder)
result := requester(context.Background())()
assert.True(t, E.IsRight(result), "Expected Right result")
req := E.GetOrElse(func(error) *http.Request { return nil })(result)
assert.NotNil(t, req, "Expected non-nil request")
assert.Equal(t, "Bearer my-secret-token", req.Header.Get("Authorization"))
}

View File

@@ -0,0 +1,15 @@
mode: set
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:117.52,119.103 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:119.103,120.80 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:120.80,121.41 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:121.41,123.19 2 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:123.19,126.6 2 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:127.5,127.20 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:132.2,132.93 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:132.93,133.80 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:133.80,134.41 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:134.41,136.19 2 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:136.19,138.6 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:139.5,139.20 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:144.2,150.50 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/builder/builder.go:150.50,153.4 2 1

View File

@@ -0,0 +1,11 @@
mode: set
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:111.76,116.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:134.49,136.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:161.90,162.65 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:162.65,166.76 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:166.76,176.5 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:198.73,203.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:222.74,227.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:234.76,236.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:245.74,254.2 1 1
github.com/IBM/fp-go/v2/context/readerioeither/http/request.go:281.76,286.2 1 1

View File

@@ -13,6 +13,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package http provides functional HTTP client utilities built on top of ReaderIOEither monad.
// It offers a composable way to make HTTP requests with context support, error handling,
// and response parsing capabilities. The package follows functional programming principles
// to ensure type-safe, testable, and maintainable HTTP operations.
//
// The main abstractions include:
// - Requester: A reader that constructs HTTP requests with context
// - Client: An interface for executing HTTP requests
// - Response readers: Functions to parse responses as bytes, text, or JSON
//
// Example usage:
//
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// result := ReadJSON[MyType](client)(request)
// response := result(context.Background())()
package http
import (
@@ -30,14 +46,31 @@ import (
)
type (
// Requester is a reader that constructs a request
// Requester is a reader that constructs an HTTP request with context support.
// It represents a computation that, given a context, produces either an error
// or an HTTP request. This allows for composable request building with proper
// error handling and context propagation.
Requester = RIOE.ReaderIOEither[*http.Request]
// Client is an interface for executing HTTP requests in a functional way.
// It wraps the standard http.Client and provides a Do method that works
// with the ReaderIOEither monad for composable, type-safe HTTP operations.
Client interface {
// Do can send an HTTP request considering a context
// Do executes an HTTP request and returns the response wrapped in a ReaderIOEither.
// It takes a Requester (which builds the request) and returns a computation that,
// when executed with a context, performs the HTTP request and returns either
// an error or the HTTP response.
//
// Parameters:
// - req: A Requester that builds the HTTP request
//
// Returns:
// - A ReaderIOEither that produces either an error or an *http.Response
Do(Requester) RIOE.ReaderIOEither[*http.Response]
}
// client is the internal implementation of the Client interface.
// It wraps a standard http.Client and provides functional HTTP operations.
client struct {
delegate *http.Client
doIOE func(*http.Request) IOE.IOEither[error, *http.Response]
@@ -45,11 +78,33 @@ type (
)
var (
// MakeRequest is an eitherized version of [http.NewRequestWithContext]
// MakeRequest is an eitherized version of http.NewRequestWithContext.
// It creates a Requester that builds an HTTP request with the given method, URL, and body.
// This function properly handles errors and wraps them in the Either monad.
//
// Parameters:
// - method: HTTP method (GET, POST, PUT, DELETE, etc.)
// - url: The target URL for the request
// - body: Optional request body (can be nil)
//
// Returns:
// - A Requester that produces either an error or an *http.Request
MakeRequest = RIOE.Eitherize3(http.NewRequestWithContext)
// makeRequest is a partially applied version of MakeRequest with the context parameter bound.
makeRequest = F.Bind13of3(MakeRequest)
// specialize
// MakeGetRequest creates a GET request for the specified URL.
// It's a convenience function that specializes MakeRequest for GET requests with no body.
//
// Parameters:
// - url: The target URL for the GET request
//
// Returns:
// - A Requester that produces either an error or an *http.Request
//
// Example:
// req := MakeGetRequest("https://api.example.com/users")
MakeGetRequest = makeRequest("GET", nil)
)
@@ -60,12 +115,49 @@ func (client client) Do(req Requester) RIOE.ReaderIOEither[*http.Response] {
)
}
// MakeClient creates an HTTP client proxy
// MakeClient creates a functional HTTP client wrapper around a standard http.Client.
// The returned Client provides methods for executing HTTP requests in a functional,
// composable way using the ReaderIOEither monad.
//
// Parameters:
// - httpClient: A standard *http.Client to wrap (e.g., http.DefaultClient)
//
// Returns:
// - A Client that can execute HTTP requests functionally
//
// Example:
//
// client := MakeClient(http.DefaultClient)
// // or with custom client
// customClient := &http.Client{Timeout: 10 * time.Second}
// client := MakeClient(customClient)
func MakeClient(httpClient *http.Client) Client {
return client{delegate: httpClient, doIOE: IOE.Eitherize1(httpClient.Do)}
}
// ReadFullResponse sends a request, reads the response as a byte array and represents the result as a tuple
// ReadFullResponse sends an HTTP request, reads the complete response body as a byte array,
// and returns both the response and body as a tuple (FullResponse).
// It validates the HTTP status code and handles errors appropriately.
//
// The function performs the following steps:
// 1. Executes the HTTP request using the provided client
// 2. Validates the response status code (checks for HTTP errors)
// 3. Reads the entire response body into a byte array
// 4. Returns a tuple containing the response and body
//
// Parameters:
// - client: The HTTP client to use for executing the request
//
// Returns:
// - A function that takes a Requester and returns a ReaderIOEither[FullResponse]
// where FullResponse is a tuple of (*http.Response, []byte)
//
// Example:
//
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// fullResp := ReadFullResponse(client)(request)
// result := fullResp(context.Background())()
func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullResponse] {
return func(req Requester) RIOE.ReaderIOEither[H.FullResponse] {
return F.Flow3(
@@ -86,7 +178,23 @@ func ReadFullResponse(client Client) func(Requester) RIOE.ReaderIOEither[H.FullR
}
}
// ReadAll sends a request and reads the response as bytes
// ReadAll sends an HTTP request and reads the complete response body as a byte array.
// It validates the HTTP status code and returns the raw response body bytes.
// This is useful when you need to process the response body in a custom way.
//
// Parameters:
// - client: The HTTP client to use for executing the request
//
// Returns:
// - A function that takes a Requester and returns a ReaderIOEither[[]byte]
// containing the response body as bytes
//
// Example:
//
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// readBytes := ReadAll(client)
// result := readBytes(request)(context.Background())()
func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
return F.Flow2(
ReadFullResponse(client),
@@ -94,7 +202,23 @@ func ReadAll(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
)
}
// ReadText sends a request, reads the response and represents the response as a text string
// ReadText sends an HTTP request, reads the response body, and converts it to a string.
// It validates the HTTP status code and returns the response body as a UTF-8 string.
// This is convenient for APIs that return plain text responses.
//
// Parameters:
// - client: The HTTP client to use for executing the request
//
// Returns:
// - A function that takes a Requester and returns a ReaderIOEither[string]
// containing the response body as a string
//
// Example:
//
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/text")
// readText := ReadText(client)
// result := readText(request)(context.Background())()
func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
return F.Flow2(
ReadAll(client),
@@ -102,13 +226,22 @@ func ReadText(client Client) func(Requester) RIOE.ReaderIOEither[string] {
)
}
// ReadJson sends a request, reads the response and parses the response as JSON
// ReadJson sends an HTTP request, reads the response, and parses it as JSON.
//
// Deprecated: use [ReadJSON] instead
// Deprecated: Use [ReadJSON] instead. This function is kept for backward compatibility
// but will be removed in a future version. The capitalized version follows Go naming
// conventions for acronyms.
func ReadJson[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
return ReadJSON[A](client)
}
// readJSON is an internal helper that reads the response body and validates JSON content type.
// It performs the following validations:
// 1. Validates HTTP status code
// 2. Validates that the response Content-Type is application/json
// 3. Reads the response body as bytes
//
// This function is used internally by ReadJSON to ensure proper JSON response handling.
func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
return F.Flow3(
ReadFullResponse(client),
@@ -120,7 +253,31 @@ func readJSON(client Client) func(Requester) RIOE.ReaderIOEither[[]byte] {
)
}
// ReadJSON sends a request, reads the response and parses the response as JSON
// ReadJSON sends an HTTP request, reads the response, and parses it as JSON into type A.
// It validates both the HTTP status code and the Content-Type header to ensure the
// response is valid JSON before attempting to unmarshal.
//
// Type Parameters:
// - A: The target type to unmarshal the JSON response into
//
// Parameters:
// - client: The HTTP client to use for executing the request
//
// Returns:
// - A function that takes a Requester and returns a ReaderIOEither[A]
// containing the parsed JSON data
//
// Example:
//
// type User struct {
// ID int `json:"id"`
// Name string `json:"name"`
// }
//
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/user/1")
// readUser := ReadJSON[User](client)
// result := readUser(request)(context.Background())()
func ReadJSON[A any](client Client) func(Requester) RIOE.ReaderIOEither[A] {
return F.Flow2(
readJSON(client),

View File

@@ -88,7 +88,7 @@ func TestSendSingleRequest(t *testing.T) {
resp1 := readItem(req1)
resE := resp1(context.TODO())()
resE := resp1(t.Context())()
fmt.Println(resE)
}
@@ -121,7 +121,7 @@ func TestSendSingleRequestWithHeaderUnsafe(t *testing.T) {
)
res := F.Pipe1(
resp1(context.TODO())(),
resp1(t.Context())(),
E.GetOrElse(errors.ToString),
)
@@ -149,9 +149,167 @@ func TestSendSingleRequestWithHeaderSafe(t *testing.T) {
)
res := F.Pipe1(
response(context.TODO())(),
response(t.Context())(),
E.GetOrElse(errors.ToString),
)
assert.Equal(t, "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", res)
}
// TestReadAll tests the ReadAll function which reads response as bytes
func TestReadAll(t *testing.T) {
client := MakeClient(H.DefaultClient)
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
readBytes := ReadAll(client)
result := readBytes(request)(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
bytes := E.GetOrElse(func(error) []byte { return nil })(result)
assert.NotNil(t, bytes, "Expected non-nil bytes")
assert.Greater(t, len(bytes), 0, "Expected non-empty byte array")
// Verify it contains expected JSON content
content := string(bytes)
assert.Contains(t, content, "userId")
assert.Contains(t, content, "title")
}
// TestReadText tests the ReadText function which reads response as string
func TestReadText(t *testing.T) {
client := MakeClient(H.DefaultClient)
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
readText := ReadText(client)
result := readText(request)(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
text := E.GetOrElse(func(error) string { return "" })(result)
assert.NotEmpty(t, text, "Expected non-empty text")
// Verify it contains expected JSON content as text
assert.Contains(t, text, "userId")
assert.Contains(t, text, "title")
assert.Contains(t, text, "sunt aut facere")
}
// TestReadJson tests the deprecated ReadJson function
func TestReadJson(t *testing.T) {
client := MakeClient(H.DefaultClient)
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
readItem := ReadJson[PostItem](client)
result := readItem(request)(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
item := E.GetOrElse(func(error) PostItem { return PostItem{} })(result)
assert.Equal(t, uint(1), item.UserID, "Expected UserID to be 1")
assert.Equal(t, uint(1), item.Id, "Expected Id to be 1")
assert.NotEmpty(t, item.Title, "Expected non-empty title")
assert.NotEmpty(t, item.Body, "Expected non-empty body")
}
// TestReadAllWithInvalidURL tests ReadAll with an invalid URL
func TestReadAllWithInvalidURL(t *testing.T) {
client := MakeClient(H.DefaultClient)
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
readBytes := ReadAll(client)
result := readBytes(request)(t.Context())()
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
}
// TestReadTextWithInvalidURL tests ReadText with an invalid URL
func TestReadTextWithInvalidURL(t *testing.T) {
client := MakeClient(H.DefaultClient)
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
readText := ReadText(client)
result := readText(request)(t.Context())()
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
}
// TestReadJSONWithInvalidURL tests ReadJSON with an invalid URL
func TestReadJSONWithInvalidURL(t *testing.T) {
client := MakeClient(H.DefaultClient)
request := MakeGetRequest("http://invalid-domain-that-does-not-exist-12345.com")
readItem := ReadJSON[PostItem](client)
result := readItem(request)(t.Context())()
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
}
// TestReadJSONWithInvalidJSON tests ReadJSON with non-JSON response
func TestReadJSONWithInvalidJSON(t *testing.T) {
client := MakeClient(H.DefaultClient)
// This URL returns HTML, not JSON
request := MakeGetRequest("https://www.google.com")
readItem := ReadJSON[PostItem](client)
result := readItem(request)(t.Context())()
// Should fail because content-type is not application/json
assert.True(t, E.IsLeft(result), "Expected Left result for non-JSON response")
}
// TestMakeClientWithCustomClient tests MakeClient with a custom http.Client
func TestMakeClientWithCustomClient(t *testing.T) {
customClient := H.DefaultClient
client := MakeClient(customClient)
assert.NotNil(t, client, "Expected non-nil client")
// Verify it works
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
readItem := ReadJSON[PostItem](client)
result := readItem(request)(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
}
// TestReadAllComposition tests composing ReadAll with other operations
func TestReadAllComposition(t *testing.T) {
client := MakeClient(H.DefaultClient)
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
// Compose ReadAll with a map operation to get byte length
readBytes := ReadAll(client)(request)
readLength := R.Map(func(bytes []byte) int { return len(bytes) })(readBytes)
result := readLength(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
length := E.GetOrElse(func(error) int { return 0 })(result)
assert.Greater(t, length, 0, "Expected positive byte length")
}
// TestReadTextComposition tests composing ReadText with other operations
func TestReadTextComposition(t *testing.T) {
client := MakeClient(H.DefaultClient)
request := MakeGetRequest("https://jsonplaceholder.typicode.com/posts/1")
// Compose ReadText with a map operation to get string length
readText := ReadText(client)(request)
readLength := R.Map(func(text string) int { return len(text) })(readText)
result := readLength(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
length := E.GetOrElse(func(error) int { return 0 })(result)
assert.Greater(t, length, 0, "Expected positive string length")
}

View File

@@ -39,6 +39,8 @@ const (
// - e: The Either value to lift into ReaderIOEither
//
// Returns a ReaderIOEither that produces the given Either value.
//
//go:inline
func FromEither[A any](e Either[A]) ReaderIOEither[A] {
return readerioeither.FromEither[context.Context](e)
}
@@ -59,6 +61,8 @@ func Left[A any](l error) ReaderIOEither[A] {
// - r: The success value
//
// Returns a ReaderIOEither that always succeeds with the given value.
//
//go:inline
func Right[A any](r A) ReaderIOEither[A] {
return readerioeither.Right[context.Context, error](r)
}
@@ -71,6 +75,8 @@ func Right[A any](r A) ReaderIOEither[A] {
// - f: The transformation function
//
// Returns a new ReaderIOEither with the transformed value.
//
//go:inline
func MonadMap[A, B any](fa ReaderIOEither[A], f func(A) B) ReaderIOEither[B] {
return readerioeither.MonadMap(fa, f)
}
@@ -82,6 +88,8 @@ func MonadMap[A, B any](fa ReaderIOEither[A], f func(A) B) ReaderIOEither[B] {
// - f: The transformation function
//
// Returns a function that transforms a ReaderIOEither.
//
//go:inline
func Map[A, B any](f func(A) B) Operator[A, B] {
return readerioeither.Map[context.Context, error](f)
}
@@ -94,6 +102,8 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
// - b: The constant value to use
//
// Returns a new ReaderIOEither with the constant value.
//
//go:inline
func MonadMapTo[A, B any](fa ReaderIOEither[A], b B) ReaderIOEither[B] {
return readerioeither.MonadMapTo(fa, b)
}
@@ -105,6 +115,8 @@ func MonadMapTo[A, B any](fa ReaderIOEither[A], b B) ReaderIOEither[B] {
// - b: The constant value to use
//
// Returns a function that transforms a ReaderIOEither.
//
//go:inline
func MapTo[A, B any](b B) Operator[A, B] {
return readerioeither.MapTo[context.Context, error, A](b)
}
@@ -117,7 +129,9 @@ func MapTo[A, B any](b B) Operator[A, B] {
// - f: Function that produces the second ReaderIOEither based on the first's result
//
// Returns a new ReaderIOEither representing the sequenced computation.
func MonadChain[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) ReaderIOEither[B] {
//
//go:inline
func MonadChain[A, B any](ma ReaderIOEither[A], f Kleisli[A, B]) ReaderIOEither[B] {
return readerioeither.MonadChain(ma, f)
}
@@ -128,7 +142,9 @@ func MonadChain[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) Rea
// - f: Function that produces the second ReaderIOEither based on the first's result
//
// Returns a function that sequences ReaderIOEither computations.
func Chain[A, B any](f func(A) ReaderIOEither[B]) Operator[A, B] {
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return readerioeither.Chain(f)
}
@@ -140,7 +156,9 @@ func Chain[A, B any](f func(A) ReaderIOEither[B]) Operator[A, B] {
// - f: Function that produces the second ReaderIOEither
//
// Returns a ReaderIOEither with the result of the first computation.
func MonadChainFirst[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]) ReaderIOEither[A] {
//
//go:inline
func MonadChainFirst[A, B any](ma ReaderIOEither[A], f Kleisli[A, B]) ReaderIOEither[A] {
return readerioeither.MonadChainFirst(ma, f)
}
@@ -151,7 +169,9 @@ func MonadChainFirst[A, B any](ma ReaderIOEither[A], f func(A) ReaderIOEither[B]
// - f: Function that produces the second ReaderIOEither
//
// Returns a function that sequences ReaderIOEither computations.
func ChainFirst[A, B any](f func(A) ReaderIOEither[B]) Operator[A, A] {
//
//go:inline
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return readerioeither.ChainFirst(f)
}
@@ -162,6 +182,8 @@ func ChainFirst[A, B any](f func(A) ReaderIOEither[B]) Operator[A, A] {
// - a: The value to wrap
//
// Returns a ReaderIOEither that always succeeds with the given value.
//
//go:inline
func Of[A any](a A) ReaderIOEither[A] {
return readerioeither.Of[context.Context, error](a)
}
@@ -240,6 +262,8 @@ func MonadAp[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) Read
// - fa: ReaderIOEither containing a value
//
// Returns a ReaderIOEither with the function applied to the value.
//
//go:inline
func MonadApSeq[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) ReaderIOEither[B] {
return readerioeither.MonadApSeq(fab, fa)
}
@@ -251,6 +275,8 @@ func MonadApSeq[B, A any](fab ReaderIOEither[func(A) B], fa ReaderIOEither[A]) R
// - fa: ReaderIOEither containing a value
//
// Returns a function that applies a ReaderIOEither function to the value.
//
//go:inline
func Ap[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadAp[B, A], fa)
}
@@ -262,6 +288,8 @@ func Ap[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
// - fa: ReaderIOEither containing a value
//
// Returns a function that applies a ReaderIOEither function to the value sequentially.
//
//go:inline
func ApSeq[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadApSeq[B, A], fa)
}
@@ -273,6 +301,8 @@ func ApSeq[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
// - fa: ReaderIOEither containing a value
//
// Returns a function that applies a ReaderIOEither function to the value in parallel.
//
//go:inline
func ApPar[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadApPar[B, A], fa)
}
@@ -285,7 +315,9 @@ func ApPar[B, A any](fa ReaderIOEither[A]) Operator[func(A) B, B] {
// - onFalse: Function to generate an error when predicate fails
//
// Returns a function that converts a value to ReaderIOEither based on the predicate.
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) ReaderIOEither[A] {
//
//go:inline
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
return readerioeither.FromPredicate[context.Context](pred, onFalse)
}
@@ -296,7 +328,9 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) func(A) Read
// - onLeft: Function that produces an alternative ReaderIOEither from the error
//
// Returns a function that provides fallback behavior for failed computations.
func OrElse[A any](onLeft func(error) ReaderIOEither[A]) Operator[A, A] {
//
//go:inline
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
return readerioeither.OrElse[context.Context](onLeft)
}
@@ -304,6 +338,8 @@ func OrElse[A any](onLeft func(error) ReaderIOEither[A]) Operator[A, A] {
// This is useful for accessing the [context.Context] within a computation.
//
// Returns a ReaderIOEither that produces the context.
//
//go:inline
func Ask() ReaderIOEither[context.Context] {
return readerioeither.Ask[context.Context, error]()
}
@@ -316,6 +352,8 @@ func Ask() ReaderIOEither[context.Context] {
// - f: Function that produces an Either
//
// Returns a new ReaderIOEither with the chained computation.
//
//go:inline
func MonadChainEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[B] {
return readerioeither.MonadChainEitherK[context.Context](ma, f)
}
@@ -327,7 +365,9 @@ func MonadChainEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) Read
// - f: Function that produces an Either
//
// Returns a function that chains the Either-returning function.
func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
//
//go:inline
func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
return readerioeither.ChainEitherK[context.Context](f)
}
@@ -339,6 +379,8 @@ func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) Read
// - f: Function that produces an Either
//
// Returns a ReaderIOEither with the original value if both computations succeed.
//
//go:inline
func MonadChainFirstEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B]) ReaderIOEither[A] {
return readerioeither.MonadChainFirstEitherK[context.Context](ma, f)
}
@@ -350,7 +392,9 @@ func MonadChainFirstEitherK[A, B any](ma ReaderIOEither[A], f func(A) Either[B])
// - f: Function that produces an Either
//
// Returns a function that chains the Either-returning function.
func ChainFirstEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
//
//go:inline
func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
return readerioeither.ChainFirstEitherK[context.Context](f)
}
@@ -361,6 +405,8 @@ func ChainFirstEitherK[A, B any](f func(A) Either[B]) func(ma ReaderIOEither[A])
// - onNone: Function to generate an error when Option is None
//
// Returns a function that chains Option-returning functions into ReaderIOEither.
//
//go:inline
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
return readerioeither.ChainOptionK[context.Context, A, B](onNone)
}
@@ -372,6 +418,8 @@ func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operato
// - t: The IOEither to convert
//
// Returns a ReaderIOEither that executes the IOEither.
//
//go:inline
func FromIOEither[A any](t ioeither.IOEither[error, A]) ReaderIOEither[A] {
return readerioeither.FromIOEither[context.Context](t)
}
@@ -383,6 +431,8 @@ func FromIOEither[A any](t ioeither.IOEither[error, A]) ReaderIOEither[A] {
// - t: The IO to convert
//
// Returns a ReaderIOEither that executes the IO and wraps the result in Right.
//
//go:inline
func FromIO[A any](t IO[A]) ReaderIOEither[A] {
return readerioeither.FromIO[context.Context, error](t)
}
@@ -395,6 +445,8 @@ func FromIO[A any](t IO[A]) ReaderIOEither[A] {
// - t: The Lazy computation to convert
//
// Returns a ReaderIOEither that executes the Lazy computation and wraps the result in Right.
//
//go:inline
func FromLazy[A any](t Lazy[A]) ReaderIOEither[A] {
return readerioeither.FromIO[context.Context, error](t)
}
@@ -420,6 +472,8 @@ func Never[A any]() ReaderIOEither[A] {
// - f: Function that produces an IO
//
// Returns a new ReaderIOEither with the chained IO computation.
//
//go:inline
func MonadChainIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[B] {
return readerioeither.MonadChainIOK(ma, f)
}
@@ -431,7 +485,9 @@ func MonadChainIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEith
// - f: Function that produces an IO
//
// Returns a function that chains the IO-returning function.
func ChainIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
//
//go:inline
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
return readerioeither.ChainIOK[context.Context, error](f)
}
@@ -443,6 +499,8 @@ func ChainIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEith
// - f: Function that produces an IO
//
// Returns a ReaderIOEither with the original value after executing the IO.
//
//go:inline
func MonadChainFirstIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderIOEither[A] {
return readerioeither.MonadChainFirstIOK(ma, f)
}
@@ -454,7 +512,9 @@ func MonadChainFirstIOK[A, B any](ma ReaderIOEither[A], f func(A) IO[B]) ReaderI
// - f: Function that produces an IO
//
// Returns a function that chains the IO-returning function.
func ChainFirstIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
//
//go:inline
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
return readerioeither.ChainFirstIOK[context.Context, error](f)
}
@@ -465,7 +525,9 @@ func ChainFirstIOK[A, B any](f func(A) IO[B]) func(ma ReaderIOEither[A]) ReaderI
// - f: Function that produces an IOEither
//
// Returns a function that chains the IOEither-returning function.
func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) func(ma ReaderIOEither[A]) ReaderIOEither[B] {
//
//go:inline
func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) Operator[A, B] {
return readerioeither.ChainIOEitherK[context.Context](f)
}
@@ -476,7 +538,7 @@ func ChainIOEitherK[A, B any](f func(A) ioeither.IOEither[error, B]) func(ma Rea
// - delay: The duration to wait before executing the computation
//
// Returns a function that delays a ReaderIOEither computation.
func Delay[A any](delay time.Duration) func(ma ReaderIOEither[A]) ReaderIOEither[A] {
func Delay[A any](delay time.Duration) Operator[A, A] {
return func(ma ReaderIOEither[A]) ReaderIOEither[A] {
return func(ctx context.Context) IOEither[A] {
return func() Either[A] {
@@ -517,6 +579,8 @@ func Timer(delay time.Duration) ReaderIOEither[time.Time] {
// - gen: Lazy generator function that produces a ReaderIOEither
//
// Returns a ReaderIOEither that generates a fresh computation on each execution.
//
//go:inline
func Defer[A any](gen Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
return readerioeither.Defer(gen)
}
@@ -528,6 +592,8 @@ func Defer[A any](gen Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
// - f: Function that takes a context and returns a function producing (value, error)
//
// Returns a ReaderIOEither that wraps the error-returning function.
//
//go:inline
func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A] {
return readerioeither.TryCatch(f, errors.IdentityError)
}
@@ -540,6 +606,8 @@ func TryCatch[A any](f func(context.Context) func() (A, error)) ReaderIOEither[A
// - second: Lazy alternative ReaderIOEither to use if first fails
//
// Returns a ReaderIOEither that tries the first, then the second if first fails.
//
//go:inline
func MonadAlt[A any](first ReaderIOEither[A], second Lazy[ReaderIOEither[A]]) ReaderIOEither[A] {
return readerioeither.MonadAlt(first, second)
}
@@ -551,6 +619,8 @@ func MonadAlt[A any](first ReaderIOEither[A], second Lazy[ReaderIOEither[A]]) Re
// - second: Lazy alternative ReaderIOEither to use if first fails
//
// Returns a function that provides fallback behavior.
//
//go:inline
func Alt[A any](second Lazy[ReaderIOEither[A]]) Operator[A, A] {
return readerioeither.Alt(second)
}
@@ -563,6 +633,8 @@ func Alt[A any](second Lazy[ReaderIOEither[A]]) Operator[A, A] {
// - rdr: The ReaderIOEither to memoize
//
// Returns a ReaderIOEither that caches its result after the first execution.
//
//go:inline
func Memoize[A any](rdr ReaderIOEither[A]) ReaderIOEither[A] {
return readerioeither.Memoize(rdr)
}
@@ -574,6 +646,8 @@ func Memoize[A any](rdr ReaderIOEither[A]) ReaderIOEither[A] {
// - rdr: The nested ReaderIOEither to flatten
//
// Returns a flattened ReaderIOEither.
//
//go:inline
func Flatten[A any](rdr ReaderIOEither[ReaderIOEither[A]]) ReaderIOEither[A] {
return readerioeither.Flatten(rdr)
}
@@ -586,6 +660,8 @@ func Flatten[A any](rdr ReaderIOEither[ReaderIOEither[A]]) ReaderIOEither[A] {
// - a: The value to apply to the function
//
// Returns a ReaderIOEither with the function applied to the value.
//
//go:inline
func MonadFlap[B, A any](fab ReaderIOEither[func(A) B], a A) ReaderIOEither[B] {
return readerioeither.MonadFlap(fab, a)
}
@@ -597,6 +673,8 @@ func MonadFlap[B, A any](fab ReaderIOEither[func(A) B], a A) ReaderIOEither[B] {
// - a: The value to apply to the function
//
// Returns a function that applies the value to a ReaderIOEither function.
//
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return readerioeither.Flap[context.Context, error, B](a)
}
@@ -609,7 +687,9 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
// - onRight: Handler for success case
//
// Returns a function that folds a ReaderIOEither into a new ReaderIOEither.
func Fold[A, B any](onLeft func(error) ReaderIOEither[B], onRight func(A) ReaderIOEither[B]) Operator[A, B] {
//
//go:inline
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
return readerioeither.Fold(onLeft, onRight)
}
@@ -620,6 +700,8 @@ func Fold[A, B any](onLeft func(error) ReaderIOEither[B], onRight func(A) Reader
// - onLeft: Function to provide a default value from the error
//
// Returns a function that converts a ReaderIOEither to a ReaderIO.
//
//go:inline
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOEither[A]) ReaderIO[A] {
return readerioeither.GetOrElse(onLeft)
}
@@ -631,6 +713,8 @@ func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOEither[A]) Re
// - onLeft: Function to transform the error
//
// Returns a function that transforms the error of a ReaderIOEither.
//
//go:inline
func OrLeft[A any](onLeft func(error) ReaderIO[error]) Operator[A, A] {
return readerioeither.OrLeft[A](onLeft)
}

View File

@@ -28,7 +28,7 @@ import (
)
func TestFromEither(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
rightVal := E.Right[error](42)
@@ -43,7 +43,7 @@ func TestFromEither(t *testing.T) {
}
func TestLeftRight(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test Left
err := errors.New("test error")
@@ -58,13 +58,13 @@ func TestLeftRight(t *testing.T) {
}
func TestOf(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
result := Of(42)(ctx)()
assert.Equal(t, E.Right[error](42), result)
}
func TestMonadMap(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
result := MonadMap(Right(42), func(x int) int { return x * 2 })(ctx)()
@@ -77,7 +77,7 @@ func TestMonadMap(t *testing.T) {
}
func TestMonadMapTo(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
result := MonadMapTo(Right(42), "hello")(ctx)()
@@ -90,7 +90,7 @@ func TestMonadMapTo(t *testing.T) {
}
func TestMonadChain(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
result := MonadChain(Right(42), func(x int) ReaderIOEither[int] {
@@ -113,7 +113,7 @@ func TestMonadChain(t *testing.T) {
}
func TestMonadChainFirst(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
result := MonadChainFirst(Right(42), func(x int) ReaderIOEither[string] {
@@ -136,7 +136,7 @@ func TestMonadChainFirst(t *testing.T) {
}
func TestMonadApSeq(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with both Right
fct := Right(func(x int) int { return x * 2 })
@@ -159,7 +159,7 @@ func TestMonadApSeq(t *testing.T) {
}
func TestMonadApPar(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with both Right
fct := Right(func(x int) int { return x * 2 })
@@ -169,7 +169,7 @@ func TestMonadApPar(t *testing.T) {
}
func TestFromPredicate(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
pred := func(x int) bool { return x > 0 }
onFalse := func(x int) error { return fmt.Errorf("value %d is not positive", x) }
@@ -184,7 +184,7 @@ func TestFromPredicate(t *testing.T) {
}
func TestAsk(t *testing.T) {
ctx := context.WithValue(context.Background(), "key", "value")
ctx := context.WithValue(t.Context(), "key", "value")
result := Ask()(ctx)()
assert.True(t, E.IsRight(result))
retrievedCtx, _ := E.Unwrap(result)
@@ -192,7 +192,7 @@ func TestAsk(t *testing.T) {
}
func TestMonadChainEitherK(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
result := MonadChainEitherK(Right(42), func(x int) E.Either[error, int] {
@@ -208,7 +208,7 @@ func TestMonadChainEitherK(t *testing.T) {
}
func TestMonadChainFirstEitherK(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
result := MonadChainFirstEitherK(Right(42), func(x int) E.Either[error, string] {
@@ -224,7 +224,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
}
func TestChainOptionKFunc(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
onNone := func() error { return errors.New("none error") }
@@ -243,7 +243,7 @@ func TestChainOptionKFunc(t *testing.T) {
}
func TestFromIOEither(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
ioe := func() E.Either[error, int] {
@@ -262,7 +262,7 @@ func TestFromIOEither(t *testing.T) {
}
func TestFromIO(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
io := func() int { return 42 }
result := FromIO(io)(ctx)()
@@ -270,7 +270,7 @@ func TestFromIO(t *testing.T) {
}
func TestFromLazy(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
lazy := func() int { return 42 }
result := FromLazy(lazy)(ctx)()
@@ -278,7 +278,7 @@ func TestFromLazy(t *testing.T) {
}
func TestNeverWithCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
// Start Never in a goroutine
done := make(chan E.Either[error, int])
@@ -295,7 +295,7 @@ func TestNeverWithCancel(t *testing.T) {
}
func TestMonadChainIOK(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
result := MonadChainIOK(Right(42), func(x int) func() int {
@@ -305,7 +305,7 @@ func TestMonadChainIOK(t *testing.T) {
}
func TestMonadChainFirstIOK(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right
result := MonadChainFirstIOK(Right(42), func(x int) func() string {
@@ -315,7 +315,7 @@ func TestMonadChainFirstIOK(t *testing.T) {
}
func TestDelayFunc(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
delay := 100 * time.Millisecond
start := time.Now()
@@ -328,7 +328,7 @@ func TestDelayFunc(t *testing.T) {
}
func TestDefer(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
count := 0
gen := func() ReaderIOEither[int] {
@@ -348,7 +348,7 @@ func TestDefer(t *testing.T) {
}
func TestTryCatch(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test success
result := TryCatch(func(ctx context.Context) func() (int, error) {
@@ -369,7 +369,7 @@ func TestTryCatch(t *testing.T) {
}
func TestMonadAlt(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Right (alternative not called)
result := MonadAlt(Right(42), func() ReaderIOEither[int] {
@@ -386,7 +386,7 @@ func TestMonadAlt(t *testing.T) {
}
func TestMemoize(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
count := 0
rdr := Memoize(FromLazy(func() int {
@@ -404,7 +404,7 @@ func TestMemoize(t *testing.T) {
}
func TestFlatten(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
nested := Right(Right(42))
result := Flatten(nested)(ctx)()
@@ -412,7 +412,7 @@ func TestFlatten(t *testing.T) {
}
func TestMonadFlap(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
fab := Right(func(x int) int { return x * 2 })
result := MonadFlap(fab, 42)(ctx)()
assert.Equal(t, E.Right[error](84), result)
@@ -420,19 +420,19 @@ func TestMonadFlap(t *testing.T) {
func TestWithContext(t *testing.T) {
// Test with non-canceled context
ctx := context.Background()
ctx := t.Context()
result := WithContext(Right(42))(ctx)()
assert.Equal(t, E.Right[error](42), result)
// Test with canceled context
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
result = WithContext(Right(42))(ctx)()
assert.True(t, E.IsLeft(result))
}
func TestMonadAp(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with both Right
fct := Right(func(x int) int { return x * 2 })
@@ -443,7 +443,7 @@ func TestMonadAp(t *testing.T) {
// Test traverse functions
func TestSequenceArray(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with all Right
arr := []ReaderIOEither[int]{Right(1), Right(2), Right(3)}
@@ -460,7 +460,7 @@ func TestSequenceArray(t *testing.T) {
}
func TestTraverseArray(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test transformation
arr := []int{1, 2, 3}
@@ -473,7 +473,7 @@ func TestTraverseArray(t *testing.T) {
}
func TestSequenceRecord(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with all Right
rec := map[string]ReaderIOEither[int]{
@@ -488,7 +488,7 @@ func TestSequenceRecord(t *testing.T) {
}
func TestTraverseRecord(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test transformation
rec := map[string]int{"a": 1, "b": 2}
@@ -503,7 +503,7 @@ func TestTraverseRecord(t *testing.T) {
// Test monoid functions
func TestAltSemigroup(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
sg := AltSemigroup[int]()
@@ -519,7 +519,7 @@ func TestAltSemigroup(t *testing.T) {
// Test Do notation
func TestDo(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
type State struct {
Value int

View File

@@ -55,7 +55,7 @@ import (
// }
// })
// })
func WithResource[A, R, ANY any](onCreate ReaderIOEither[R], onRelease func(R) ReaderIOEither[ANY]) func(func(R) ReaderIOEither[A]) ReaderIOEither[A] {
func WithResource[A, R, ANY any](onCreate ReaderIOEither[R], onRelease func(R) ReaderIOEither[ANY]) Kleisli[Kleisli[R, A], A] {
return function.Flow2(
function.Bind2nd(function.Flow2[func(R) ReaderIOEither[A], Operator[A, A], R, ReaderIOEither[A], ReaderIOEither[A]], WithContext[A]),
RIE.WithResource[A, context.Context, error, R](WithContext(onCreate), onRelease),

View File

@@ -28,7 +28,7 @@ import (
// - f: Function that transforms each element into a ReaderIOEither
//
// Returns a function that transforms an array into a ReaderIOEither of an array.
func TraverseArray[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -45,7 +45,7 @@ func TraverseArray[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEith
// - f: Function that transforms each element with its index into a ReaderIOEither
//
// Returns a function that transforms an array into a ReaderIOEither of an array.
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderIOEither[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -72,7 +72,7 @@ func SequenceArray[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
// - f: Function that transforms each value into a ReaderIOEither
//
// Returns a function that transforms a map into a ReaderIOEither of a map.
func TraverseRecord[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return record.Traverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -89,7 +89,7 @@ func TraverseRecord[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(ma
// - f: Function that transforms each key-value pair into a ReaderIOEither
//
// Returns a function that transforms a map into a ReaderIOEither of a map.
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) Kleisli[map[K]A, map[K]B] {
return record.TraverseWithIndex[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -117,7 +117,7 @@ func SequenceRecord[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIOEit
// - f: Function that transforms each element into a ReaderIOEither
//
// Returns a ReaderIOEither containing an array of transformed values.
func MonadTraverseArraySeq[A, B any](as []A, f func(A) ReaderIOEither[B]) ReaderIOEither[[]B] {
func MonadTraverseArraySeq[A, B any](as []A, f Kleisli[A, B]) ReaderIOEither[[]B] {
return array.MonadTraverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -134,7 +134,7 @@ func MonadTraverseArraySeq[A, B any](as []A, f func(A) ReaderIOEither[B]) Reader
// - f: Function that transforms each element into a ReaderIOEither
//
// Returns a function that transforms an array into a ReaderIOEither of an array.
func TraverseArraySeq[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -145,7 +145,7 @@ func TraverseArraySeq[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOE
}
// TraverseArrayWithIndexSeq uses transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]]
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) ReaderIOEither[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -167,7 +167,7 @@ func SequenceArraySeq[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
}
// MonadTraverseRecordSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f func(A) ReaderIOEither[B]) ReaderIOEither[map[K]B] {
func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f Kleisli[A, B]) ReaderIOEither[map[K]B] {
return record.MonadTraverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -178,7 +178,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f func(A) Reader
}
// TraverseRecordSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
func TraverseRecordSeq[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return record.Traverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -189,7 +189,7 @@ func TraverseRecordSeq[K comparable, A, B any](f func(A) ReaderIOEither[B]) func
}
// TraverseRecordWithIndexSeq uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
func TraverseRecordWithIndexSeq[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
func TraverseRecordWithIndexSeq[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) Kleisli[map[K]A, map[K]B] {
return record.TraverseWithIndex[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -212,7 +212,7 @@ func SequenceRecordSeq[K comparable, A any](ma map[K]ReaderIOEither[A]) ReaderIO
// - f: Function that transforms each element into a ReaderIOEither
//
// Returns a ReaderIOEither containing an array of transformed values.
func MonadTraverseArrayPar[A, B any](as []A, f func(A) ReaderIOEither[B]) ReaderIOEither[[]B] {
func MonadTraverseArrayPar[A, B any](as []A, f Kleisli[A, B]) ReaderIOEither[[]B] {
return array.MonadTraverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -229,7 +229,7 @@ func MonadTraverseArrayPar[A, B any](as []A, f func(A) ReaderIOEither[B]) Reader
// - f: Function that transforms each element into a ReaderIOEither
//
// Returns a function that transforms an array into a ReaderIOEither of an array.
func TraverseArrayPar[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return array.Traverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -240,7 +240,7 @@ func TraverseArrayPar[A, B any](f func(A) ReaderIOEither[B]) func([]A) ReaderIOE
}
// TraverseArrayWithIndexPar uses transforms an array [[]A] into [[]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[[]B]]
func TraverseArrayWithIndexPar[A, B any](f func(int, A) ReaderIOEither[B]) func([]A) ReaderIOEither[[]B] {
func TraverseArrayWithIndexPar[A, B any](f func(int, A) ReaderIOEither[B]) Kleisli[[]A, []B] {
return array.TraverseWithIndex[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -262,7 +262,7 @@ func SequenceArrayPar[A any](ma []ReaderIOEither[A]) ReaderIOEither[[]A] {
}
// TraverseRecordPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
func TraverseRecordPar[K comparable, A, B any](f func(A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
func TraverseRecordPar[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return record.Traverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -273,7 +273,7 @@ func TraverseRecordPar[K comparable, A, B any](f func(A) ReaderIOEither[B]) func
}
// TraverseRecordWithIndexPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) func(map[K]A) ReaderIOEither[map[K]B] {
func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEither[B]) Kleisli[map[K]A, map[K]B] {
return record.TraverseWithIndex[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -284,7 +284,7 @@ func TraverseRecordWithIndexPar[K comparable, A, B any](f func(K, A) ReaderIOEit
}
// MonadTraverseRecordPar uses transforms a record [map[K]A] into [map[K]ReaderIOEither[B]] and then resolves that into a [ReaderIOEither[map[K]B]]
func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f func(A) ReaderIOEither[B]) ReaderIOEither[map[K]B] {
func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f Kleisli[A, B]) ReaderIOEither[map[K]B] {
return record.MonadTraverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],

View File

@@ -99,10 +99,12 @@ type (
// result := fetchUser("123")(ctx)()
ReaderIOEither[A any] = readerioeither.ReaderIOEither[context.Context, error, A]
Kleisli[A, B any] = reader.Reader[A, ReaderIOEither[B]]
// Operator represents a transformation from one ReaderIOEither to another.
// This is useful for point-free style composition and building reusable transformations.
//
// Operator[A, B] is equivalent to func(ReaderIOEither[A]) ReaderIOEither[B]
// Operator[A, B] is equivalent to Kleisli[ReaderIOEither[A], B]
//
// Example usage:
// // Define a reusable transformation
@@ -110,5 +112,5 @@ type (
//
// // Apply the transformation
// result := toUpper(computation)
Operator[A, B any] = Reader[ReaderIOEither[A], ReaderIOEither[B]]
Operator[A, B any] = Kleisli[ReaderIOEither[A], B]
)

View File

@@ -16,6 +16,7 @@
package either
import (
"github.com/IBM/fp-go/v2/function"
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"
@@ -171,3 +172,204 @@ func ApS[E, S1, S2, T any](
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 and enables working with
// nested fields in a type-safe manner.
//
// Unlike BindL, ApSL uses applicative semantics, meaning the computation fa is independent
// of the current state and can be evaluated concurrently.
//
// Type Parameters:
// - E: Error type for the Either
// - S: Structure type containing the field to update
// - T: Type of the field being updated
//
// Parameters:
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
// - fa: An Either[E, T] computation that produces the value to set
//
// Returns:
// - An endomorphism that updates the focused field in the Either context
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// ageLens := lens.MakeLens(
// func(p Person) int { return p.Age },
// func(p Person, a int) Person { p.Age = a; return p },
// )
//
// result := F.Pipe2(
// either.Right[error](Person{Name: "Alice", Age: 25}),
// either.ApSL(ageLens, either.Right[error](30)),
// ) // Right(Person{Name: "Alice", Age: 30})
//
//go:inline
func ApSL[E, S, T any](
lens Lens[S, T],
fa Either[E, T],
) Endomorphism[Either[E, S]] {
return ApS(lens.Set, fa)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// an Either that produces the new value.
//
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
// the current value of the focused field.
//
// Type Parameters:
// - E: Error type for the Either
// - S: Structure type containing the field to update
// - T: Type of the field being updated
//
// Parameters:
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
// - f: A function that takes the current field value and returns an Either[E, T]
//
// Returns:
// - An endomorphism that updates the focused field based on its current value
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Increment the counter, but fail if it would exceed 100
// increment := func(v int) either.Either[error, int] {
// if v >= 100 {
// return either.Left[int](errors.New("counter overflow"))
// }
// return either.Right[error](v + 1)
// }
//
// result := F.Pipe1(
// either.Right[error](Counter{Value: 42}),
// either.BindL(valueLens, increment),
// ) // Right(Counter{Value: 43})
//
//go:inline
func BindL[E, S, T any](
lens Lens[S, T],
f func(T) Either[E, T],
) Endomorphism[Either[E, S]] {
return Bind[E, S, S, T](lens.Set, function.Flow2(lens.Get, f))
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in Either).
//
// This is useful for pure transformations that cannot fail, such as mathematical operations,
// string manipulations, or other deterministic updates.
//
// Type Parameters:
// - E: Error type for the Either
// - S: Structure type containing the field to update
// - T: Type of the field being updated
//
// Parameters:
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
// - f: An endomorphism (T → T) that transforms the current field value
//
// Returns:
// - An endomorphism that updates the focused field with the transformed value
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Double the counter value
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// either.Right[error](Counter{Value: 21}),
// either.LetL(valueLens, double),
// ) // Right(Counter{Value: 42})
//
//go:inline
func LetL[E, S, T any](
lens Lens[S, T],
f Endomorphism[T],
) Endomorphism[Either[E, S]] {
return Let[E, S, S, T](lens.Set, function.Flow2(lens.Get, f))
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// This is useful for resetting fields, initializing values, or setting fields to
// predetermined constants.
//
// Type Parameters:
// - E: Error type for the Either
// - S: Structure type containing the field to update
// - T: Type of the field being updated
//
// Parameters:
// - lens: A Lens[S, T] that focuses on a field of type T within structure S
// - b: The constant value to set the field to
//
// Returns:
// - An endomorphism that sets the focused field to the constant value
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// either.Right[error](Config{Debug: true, Timeout: 30}),
// either.LetToL(debugLens, false),
// ) // Right(Config{Debug: false, Timeout: 30})
//
//go:inline
func LetToL[E, S, T any](
lens Lens[S, T],
b T,
) Endomorphism[Either[E, S]] {
return LetTo[E, S, S, T](lens.Set, b)
}

View File

@@ -20,6 +20,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
@@ -54,3 +55,307 @@ func TestApS(t *testing.T) {
assert.Equal(t, res, Of[error]("John Doe"))
}
// Test types for lens-based operations
type Counter struct {
Value int
}
type Person struct {
Name string
Age int
}
type Config struct {
Debug bool
Timeout int
}
func TestApSL(t *testing.T) {
// Create a lens for the Age field
ageLens := L.MakeLens(
func(p Person) int { return p.Age },
func(p Person, a int) Person { p.Age = a; return p },
)
t.Run("ApSL with Right value", func(t *testing.T) {
result := F.Pipe1(
Right[error](Person{Name: "Alice", Age: 25}),
ApSL(ageLens, Right[error](30)),
)
expected := Right[error](Person{Name: "Alice", Age: 30})
assert.Equal(t, expected, result)
})
t.Run("ApSL with Left in context", func(t *testing.T) {
result := F.Pipe1(
Left[Person](assert.AnError),
ApSL(ageLens, Right[error](30)),
)
expected := Left[Person](assert.AnError)
assert.Equal(t, expected, result)
})
t.Run("ApSL with Left in value", func(t *testing.T) {
result := F.Pipe1(
Right[error](Person{Name: "Alice", Age: 25}),
ApSL(ageLens, Left[int](assert.AnError)),
)
expected := Left[Person](assert.AnError)
assert.Equal(t, expected, result)
})
t.Run("ApSL with both Left", func(t *testing.T) {
result := F.Pipe1(
Left[Person](assert.AnError),
ApSL(ageLens, Left[int](assert.AnError)),
)
expected := Left[Person](assert.AnError)
assert.Equal(t, expected, result)
})
}
func TestBindL(t *testing.T) {
// Create a lens for the Value field
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("BindL with successful transformation", func(t *testing.T) {
// Increment the counter, but fail if it would exceed 100
increment := func(v int) Either[error, int] {
if v >= 100 {
return Left[int](assert.AnError)
}
return Right[error](v + 1)
}
result := F.Pipe1(
Right[error](Counter{Value: 42}),
BindL(valueLens, increment),
)
expected := Right[error](Counter{Value: 43})
assert.Equal(t, expected, result)
})
t.Run("BindL with failing transformation", func(t *testing.T) {
increment := func(v int) Either[error, int] {
if v >= 100 {
return Left[int](assert.AnError)
}
return Right[error](v + 1)
}
result := F.Pipe1(
Right[error](Counter{Value: 100}),
BindL(valueLens, increment),
)
expected := Left[Counter](assert.AnError)
assert.Equal(t, expected, result)
})
t.Run("BindL with Left input", func(t *testing.T) {
increment := func(v int) Either[error, int] {
return Right[error](v + 1)
}
result := F.Pipe1(
Left[Counter](assert.AnError),
BindL(valueLens, increment),
)
expected := Left[Counter](assert.AnError)
assert.Equal(t, expected, result)
})
t.Run("BindL with multiple operations", func(t *testing.T) {
double := func(v int) Either[error, int] {
return Right[error](v * 2)
}
addTen := func(v int) Either[error, int] {
return Right[error](v + 10)
}
result := F.Pipe2(
Right[error](Counter{Value: 5}),
BindL(valueLens, double),
BindL(valueLens, addTen),
)
expected := Right[error](Counter{Value: 20})
assert.Equal(t, expected, result)
})
}
func TestLetL(t *testing.T) {
// Create a lens for the Value field
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("LetL with pure transformation", func(t *testing.T) {
double := func(v int) int { return v * 2 }
result := F.Pipe1(
Right[error](Counter{Value: 21}),
LetL[error](valueLens, double),
)
expected := Right[error](Counter{Value: 42})
assert.Equal(t, expected, result)
})
t.Run("LetL with Left input", func(t *testing.T) {
double := func(v int) int { return v * 2 }
result := F.Pipe1(
Left[Counter](assert.AnError),
LetL[error](valueLens, double),
)
expected := Left[Counter](assert.AnError)
assert.Equal(t, expected, result)
})
t.Run("LetL with multiple transformations", func(t *testing.T) {
double := func(v int) int { return v * 2 }
addTen := func(v int) int { return v + 10 }
result := F.Pipe2(
Right[error](Counter{Value: 5}),
LetL[error](valueLens, double),
LetL[error](valueLens, addTen),
)
expected := Right[error](Counter{Value: 20})
assert.Equal(t, expected, result)
})
t.Run("LetL with identity transformation", func(t *testing.T) {
identity := func(v int) int { return v }
result := F.Pipe1(
Right[error](Counter{Value: 42}),
LetL[error](valueLens, identity),
)
expected := Right[error](Counter{Value: 42})
assert.Equal(t, expected, result)
})
}
func TestLetToL(t *testing.T) {
// Create a lens for the Debug field
debugLens := L.MakeLens(
func(c Config) bool { return c.Debug },
func(c Config, d bool) Config { c.Debug = d; return c },
)
t.Run("LetToL with constant value", func(t *testing.T) {
result := F.Pipe1(
Right[error](Config{Debug: true, Timeout: 30}),
LetToL[error](debugLens, false),
)
expected := Right[error](Config{Debug: false, Timeout: 30})
assert.Equal(t, expected, result)
})
t.Run("LetToL with Left input", func(t *testing.T) {
result := F.Pipe1(
Left[Config](assert.AnError),
LetToL[error](debugLens, false),
)
expected := Left[Config](assert.AnError)
assert.Equal(t, expected, result)
})
t.Run("LetToL with multiple fields", func(t *testing.T) {
timeoutLens := L.MakeLens(
func(c Config) int { return c.Timeout },
func(c Config, t int) Config { c.Timeout = t; return c },
)
result := F.Pipe2(
Right[error](Config{Debug: true, Timeout: 30}),
LetToL[error](debugLens, false),
LetToL[error](timeoutLens, 60),
)
expected := Right[error](Config{Debug: false, Timeout: 60})
assert.Equal(t, expected, result)
})
t.Run("LetToL setting same value", func(t *testing.T) {
result := F.Pipe1(
Right[error](Config{Debug: false, Timeout: 30}),
LetToL[error](debugLens, false),
)
expected := Right[error](Config{Debug: false, Timeout: 30})
assert.Equal(t, expected, result)
})
}
func TestLensOperationsCombined(t *testing.T) {
// Test combining different lens operations
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("Combine LetToL and LetL", func(t *testing.T) {
double := func(v int) int { return v * 2 }
result := F.Pipe2(
Right[error](Counter{Value: 100}),
LetToL[error](valueLens, 10),
LetL[error](valueLens, double),
)
expected := Right[error](Counter{Value: 20})
assert.Equal(t, expected, result)
})
t.Run("Combine LetL and BindL", func(t *testing.T) {
double := func(v int) int { return v * 2 }
validate := func(v int) Either[error, int] {
if v > 100 {
return Left[int](assert.AnError)
}
return Right[error](v)
}
result := F.Pipe2(
Right[error](Counter{Value: 25}),
LetL[error](valueLens, double),
BindL(valueLens, validate),
)
expected := Right[error](Counter{Value: 50})
assert.Equal(t, expected, result)
})
t.Run("Combine ApSL and LetL", func(t *testing.T) {
addFive := func(v int) int { return v + 5 }
result := F.Pipe2(
Right[error](Counter{Value: 10}),
ApSL(valueLens, Right[error](20)),
LetL[error](valueLens, addFive),
)
expected := Right[error](Counter{Value: 25})
assert.Equal(t, expected, result)
})
}

View File

@@ -69,7 +69,7 @@ func MonadAp[B, E, A any](fab Either[E, func(a A) B], fa Either[E, A]) Either[E,
// Ap is the curried version of [MonadAp].
// Returns a function that applies a wrapped function to the given wrapped value.
func Ap[B, E, A any](fa Either[E, A]) func(fab Either[E, func(a A) B]) Either[E, B] {
func Ap[B, E, A any](fa Either[E, A]) Operator[E, func(A) B, B] {
return F.Bind2nd(MonadAp[B, E, A], fa)
}
@@ -120,7 +120,7 @@ func MonadMapTo[E, A, B any](fa Either[E, A], b B) Either[E, B] {
}
// MapTo is the curried version of [MonadMapTo].
func MapTo[E, A, B any](b B) func(Either[E, A]) Either[E, B] {
func MapTo[E, A, B any](b B) Operator[E, A, B] {
return Map[E](F.Constant1[A](b))
}
@@ -211,26 +211,26 @@ func MonadChainOptionK[A, B, E any](onNone func() E, ma Either[E, A], f func(A)
}
// ChainOptionK is the curried version of [MonadChainOptionK].
func ChainOptionK[A, B, E any](onNone func() E) func(func(A) Option[B]) func(Either[E, A]) Either[E, B] {
func ChainOptionK[A, B, E any](onNone func() E) func(func(A) Option[B]) Operator[E, A, B] {
from := FromOption[B](onNone)
return func(f func(A) Option[B]) func(Either[E, A]) Either[E, B] {
return func(f func(A) Option[B]) Operator[E, A, B] {
return Chain(F.Flow2(f, from))
}
}
// ChainTo is the curried version of [MonadChainTo].
func ChainTo[A, E, B any](mb Either[E, B]) func(Either[E, A]) Either[E, B] {
func ChainTo[A, E, B any](mb Either[E, B]) Operator[E, A, B] {
return F.Constant1[Either[E, A]](mb)
}
// Chain is the curried version of [MonadChain].
// Sequences two computations where the second depends on the first.
func Chain[E, A, B any](f func(a A) Either[E, B]) func(Either[E, A]) Either[E, B] {
func Chain[E, A, B any](f func(a A) Either[E, B]) Operator[E, A, B] {
return Fold(Left[B, E], f)
}
// ChainFirst is the curried version of [MonadChainFirst].
func ChainFirst[E, A, B any](f func(a A) Either[E, B]) func(Either[E, A]) Either[E, A] {
func ChainFirst[E, A, B any](f func(a A) Either[E, B]) Operator[E, A, A] {
return C.ChainFirst(
Chain[E, A, A],
Map[E, B, A],
@@ -437,7 +437,7 @@ func AltW[E, E1, A any](that L.Lazy[Either[E1, A]]) func(Either[E, A]) Either[E1
// return either.Right[error](99)
// })
// result := alternative(either.Left[int](errors.New("fail"))) // Right(99)
func Alt[E, A any](that L.Lazy[Either[E, A]]) func(Either[E, A]) Either[E, A] {
func Alt[E, A any](that L.Lazy[Either[E, A]]) Operator[E, A, A] {
return AltW[E](that)
}
@@ -449,7 +449,7 @@ func Alt[E, A any](that L.Lazy[Either[E, A]]) func(Either[E, A]) Either[E, A] {
// return either.Right[error](0) // default value
// })
// result := recover(either.Left[int](errors.New("fail"))) // Right(0)
func OrElse[E, A any](onLeft func(e E) Either[E, A]) func(Either[E, A]) Either[E, A] {
func OrElse[E, A any](onLeft func(e E) Either[E, A]) Operator[E, A, A] {
return Fold(onLeft, Of[E, A])
}
@@ -518,7 +518,7 @@ func MonadFlap[E, B, A any](fab Either[E, func(A) B], a A) Either[E, B] {
}
// Flap is the curried version of [MonadFlap].
func Flap[E, B, A any](a A) func(Either[E, func(A) B]) Either[E, B] {
func Flap[E, B, A any](a A) Operator[E, func(A) B, B] {
return FC.Flap(Map[E, func(A) B, B], a)
}

View File

@@ -21,7 +21,7 @@ import (
type eitherFunctor[E, A, B any] struct{}
func (o *eitherFunctor[E, A, B]) Map(f func(A) B) func(Either[E, A]) Either[E, B] {
func (o *eitherFunctor[E, A, B]) Map(f func(A) B) Operator[E, A, B] {
return Map[E, A, B](f)
}

View File

@@ -22,7 +22,7 @@ import (
L "github.com/IBM/fp-go/v2/logging"
)
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) func(Either[E, A]) Either[E, A] {
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[E, A, A] {
return Fold(
func(e E) Either[E, A] {
left("%s: %v", prefix, e)
@@ -50,9 +50,9 @@ func _log[E, A any](left func(string, ...any), right func(string, ...any), prefi
// )
// // Logs: "Processing: 42"
// // result is Right(84)
func Logger[E, A any](loggers ...*log.Logger) func(string) func(Either[E, A]) Either[E, A] {
func Logger[E, A any](loggers ...*log.Logger) func(string) Operator[E, A, A] {
left, right := L.LoggingCallbacks(loggers...)
return func(prefix string) func(Either[E, A]) Either[E, A] {
return func(prefix string) Operator[E, A, A] {
delegate := _log[E, A](left, right, prefix)
return func(ma Either[E, A]) Either[E, A] {
return F.Pipe1(

View File

@@ -25,15 +25,15 @@ func (o *eitherMonad[E, A, B]) Of(a A) Either[E, A] {
return Of[E, A](a)
}
func (o *eitherMonad[E, A, B]) Map(f func(A) B) func(Either[E, A]) Either[E, B] {
func (o *eitherMonad[E, A, B]) Map(f func(A) B) Operator[E, A, B] {
return Map[E, A, B](f)
}
func (o *eitherMonad[E, A, B]) Chain(f func(A) Either[E, B]) func(Either[E, A]) Either[E, B] {
func (o *eitherMonad[E, A, B]) Chain(f func(A) Either[E, B]) Operator[E, A, B] {
return Chain[E, A, B](f)
}
func (o *eitherMonad[E, A, B]) Ap(fa Either[E, A]) func(Either[E, func(A) B]) Either[E, B] {
func (o *eitherMonad[E, A, B]) Ap(fa Either[E, A]) Operator[E, func(A) B, B] {
return Ap[B, E, A](fa)
}

View File

@@ -31,7 +31,7 @@ import (
// m := either.AlternativeMonoid[error](intAdd)
// result := m.Concat(either.Right[error](1), either.Right[error](2))
// // result is Right(3)
func AlternativeMonoid[E, A any](m M.Monoid[A]) M.Monoid[Either[E, A]] {
func AlternativeMonoid[E, A any](m M.Monoid[A]) Monoid[E, A] {
return M.AlternativeMonoid(
Of[E, A],
MonadMap[E, A, func(A) A],
@@ -51,7 +51,7 @@ func AlternativeMonoid[E, A any](m M.Monoid[A]) M.Monoid[Either[E, A]] {
// m := either.AltMonoid[error, int](zero)
// result := m.Concat(either.Left[int](errors.New("err1")), either.Right[error](42))
// // result is Right(42)
func AltMonoid[E, A any](zero L.Lazy[Either[E, A]]) M.Monoid[Either[E, A]] {
func AltMonoid[E, A any](zero L.Lazy[Either[E, A]]) Monoid[E, A] {
return M.AltMonoid(
zero,
MonadAlt[E, A],

View File

@@ -15,10 +15,22 @@
package either
import "github.com/IBM/fp-go/v2/option"
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
)
// Option is a type alias for option.Option, provided for convenience
// when working with Either and Option together.
type (
Option[A any] = option.Option[A]
Option[A any] = option.Option[A]
Lens[S, T any] = lens.Lens[S, T]
Endomorphism[T any] = endomorphism.Endomorphism[T]
Kleisli[E, A, B any] = reader.Reader[A, Either[E, B]]
Operator[E, A, B any] = Kleisli[E, Either[E, A], B]
Monoid[E, A any] = monoid.Monoid[Either[E, A]]
)

View File

@@ -13,6 +13,62 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package builder provides a functional, immutable HTTP request builder with composable operations.
// It follows functional programming principles to construct HTTP requests in a type-safe,
// testable, and maintainable way.
//
// The Builder type is immutable - all operations return a new builder instance rather than
// modifying the existing one. This ensures thread-safety and makes the code easier to reason about.
//
// Key Features:
// - Immutable builder pattern with method chaining
// - Lens-based access to builder properties
// - Support for headers, query parameters, request body, and HTTP methods
// - JSON and form data encoding
// - URL construction with query parameter merging
// - Hash generation for caching
// - Bearer token authentication helpers
//
// Basic Usage:
//
// import (
// B "github.com/IBM/fp-go/v2/http/builder"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Build a simple GET request
// builder := F.Pipe2(
// B.Default,
// B.WithURL("https://api.example.com/users"),
// B.WithHeader("Accept")("application/json"),
// )
//
// // Build a POST request with JSON body
// builder := F.Pipe3(
// B.Default,
// B.WithURL("https://api.example.com/users"),
// B.WithMethod("POST"),
// B.WithJSON(map[string]string{"name": "John"}),
// )
//
// // Build a request with query parameters
// builder := F.Pipe3(
// B.Default,
// B.WithURL("https://api.example.com/search"),
// B.WithQueryArg("q")("golang"),
// B.WithQueryArg("limit")("10"),
// )
//
// The package provides several convenience functions for common HTTP methods:
// - WithGet, WithPost, WithPut, WithDelete for setting HTTP methods
// - WithBearer for adding Bearer token authentication
// - WithJSON for JSON payloads
// - WithFormData for form-encoded payloads
//
// Lenses are provided for advanced use cases:
// - URL, Method, Body, Headers, Query for accessing builder properties
// - Header(name) for accessing individual headers
// - QueryArg(name) for accessing individual query parameters
package builder
import (

View File

@@ -17,8 +17,11 @@ package builder
import (
"fmt"
"net/http"
"net/url"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
C "github.com/IBM/fp-go/v2/http/content"
FD "github.com/IBM/fp-go/v2/http/form"
@@ -91,3 +94,351 @@ func TestHash(t *testing.T) {
fmt.Println(MakeHash(b1))
}
// TestGetTargetURL tests URL construction with query parameters
func TestGetTargetURL(t *testing.T) {
builder := F.Pipe3(
Default,
WithURL("http://www.example.com?existing=param"),
WithQueryArg("limit")("10"),
WithQueryArg("offset")("20"),
)
result := builder.GetTargetURL()
assert.True(t, E.IsRight(result), "Expected Right result")
url := E.GetOrElse(func(error) string { return "" })(result)
assert.Contains(t, url, "limit=10")
assert.Contains(t, url, "offset=20")
assert.Contains(t, url, "existing=param")
}
// TestGetTargetURLWithInvalidURL tests error handling for invalid URLs
func TestGetTargetURLWithInvalidURL(t *testing.T) {
builder := F.Pipe1(
Default,
WithURL("://invalid-url"),
)
result := builder.GetTargetURL()
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
}
// TestGetTargetUrl tests the deprecated GetTargetUrl function
func TestGetTargetUrl(t *testing.T) {
builder := F.Pipe2(
Default,
WithURL("http://www.example.com"),
WithQueryArg("test")("value"),
)
result := builder.GetTargetUrl()
assert.True(t, E.IsRight(result), "Expected Right result")
url := E.GetOrElse(func(error) string { return "" })(result)
assert.Contains(t, url, "test=value")
}
// TestSetMethod tests the SetMethod function
func TestSetMethod(t *testing.T) {
builder := Default.SetMethod("POST")
assert.Equal(t, "POST", builder.GetMethod())
}
// TestSetQuery tests the SetQuery function
func TestSetQuery(t *testing.T) {
query := make(url.Values)
query.Set("key1", "value1")
query.Set("key2", "value2")
builder := Default.SetQuery(query)
assert.Equal(t, "value1", builder.GetQuery().Get("key1"))
assert.Equal(t, "value2", builder.GetQuery().Get("key2"))
}
// TestSetHeaders tests the SetHeaders function
func TestSetHeaders(t *testing.T) {
headers := make(http.Header)
headers.Set("X-Custom-Header", "custom-value")
headers.Set("Authorization", "Bearer token")
builder := Default.SetHeaders(headers)
assert.Equal(t, "custom-value", builder.GetHeaders().Get("X-Custom-Header"))
assert.Equal(t, "Bearer token", builder.GetHeaders().Get("Authorization"))
}
// TestGetHeaderValues tests the GetHeaderValues function
func TestGetHeaderValues(t *testing.T) {
builder := F.Pipe2(
Default,
WithHeader("Accept")("application/json"),
WithHeader("Accept")("text/html"),
)
values := builder.GetHeaderValues("Accept")
assert.Contains(t, values, "text/html")
}
// TestGetUrl tests the deprecated GetUrl function
func TestGetUrl(t *testing.T) {
builder := F.Pipe1(
Default,
WithURL("http://www.example.com"),
)
assert.Equal(t, "http://www.example.com", builder.GetUrl())
}
// TestSetUrl tests the deprecated SetUrl function
func TestSetUrl(t *testing.T) {
builder := Default.SetUrl("http://www.example.com")
assert.Equal(t, "http://www.example.com", builder.GetURL())
}
// TestWithJson tests the deprecated WithJson function
func TestWithJson(t *testing.T) {
data := map[string]string{"key": "value"}
builder := F.Pipe1(
Default,
WithJson(data),
)
contentType := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.ContentType))
assert.Equal(t, C.JSON, contentType)
assert.True(t, O.IsSome(builder.GetBody()))
}
// TestQueryArg tests the QueryArg lens
func TestQueryArg(t *testing.T) {
lens := QueryArg("test")
builder := F.Pipe1(
Default,
lens.Set(O.Some("value")),
)
assert.Equal(t, O.Some("value"), lens.Get(builder))
assert.Equal(t, "value", builder.GetQuery().Get("test"))
}
// TestWithQueryArg tests the WithQueryArg function
func TestWithQueryArg(t *testing.T) {
builder := F.Pipe2(
Default,
WithQueryArg("param1")("value1"),
WithQueryArg("param2")("value2"),
)
assert.Equal(t, "value1", builder.GetQuery().Get("param1"))
assert.Equal(t, "value2", builder.GetQuery().Get("param2"))
}
// TestWithoutQueryArg tests the WithoutQueryArg function
func TestWithoutQueryArg(t *testing.T) {
builder := F.Pipe3(
Default,
WithQueryArg("param1")("value1"),
WithQueryArg("param2")("value2"),
WithoutQueryArg("param1"),
)
assert.Equal(t, "", builder.GetQuery().Get("param1"))
assert.Equal(t, "value2", builder.GetQuery().Get("param2"))
}
// TestGetHash tests the GetHash method
func TestGetHash(t *testing.T) {
builder := F.Pipe2(
Default,
WithURL("http://www.example.com"),
WithMethod("POST"),
)
hash := builder.GetHash()
assert.NotEmpty(t, hash)
assert.Equal(t, MakeHash(builder), hash)
}
// TestWithBytes tests the WithBytes function
func TestWithBytes(t *testing.T) {
data := []byte("test data")
builder := F.Pipe1(
Default,
WithBytes(data),
)
body := builder.GetBody()
assert.True(t, O.IsSome(body))
}
// TestWithoutBody tests the WithoutBody function
func TestWithoutBody(t *testing.T) {
builder := F.Pipe2(
Default,
WithBytes([]byte("data")),
WithoutBody,
)
assert.True(t, O.IsNone(builder.GetBody()))
}
// TestWithGet tests the WithGet convenience function
func TestWithGet(t *testing.T) {
builder := F.Pipe1(
Default,
WithGet,
)
assert.Equal(t, "GET", builder.GetMethod())
}
// TestWithPost tests the WithPost convenience function
func TestWithPost(t *testing.T) {
builder := F.Pipe1(
Default,
WithPost,
)
assert.Equal(t, "POST", builder.GetMethod())
}
// TestWithPut tests the WithPut convenience function
func TestWithPut(t *testing.T) {
builder := F.Pipe1(
Default,
WithPut,
)
assert.Equal(t, "PUT", builder.GetMethod())
}
// TestWithDelete tests the WithDelete convenience function
func TestWithDelete(t *testing.T) {
builder := F.Pipe1(
Default,
WithDelete,
)
assert.Equal(t, "DELETE", builder.GetMethod())
}
// TestWithBearer tests the WithBearer function
func TestWithBearer(t *testing.T) {
builder := F.Pipe1(
Default,
WithBearer("my-token"),
)
auth := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.Authorization))
assert.Equal(t, "Bearer my-token", auth)
}
// TestWithContentType tests the WithContentType function
func TestWithContentType(t *testing.T) {
builder := F.Pipe1(
Default,
WithContentType(C.TextPlain),
)
contentType := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.ContentType))
assert.Equal(t, C.TextPlain, contentType)
}
// TestWithAuthorization tests the WithAuthorization function
func TestWithAuthorization(t *testing.T) {
builder := F.Pipe1(
Default,
WithAuthorization("Basic abc123"),
)
auth := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.Authorization))
assert.Equal(t, "Basic abc123", auth)
}
// TestBuilderChaining tests that builder operations can be chained
func TestBuilderChaining(t *testing.T) {
builder := F.Pipe3(
Default,
WithURL("http://www.example.com"),
WithMethod("POST"),
WithHeader("X-Test")("test-value"),
)
// Verify all operations were applied
assert.Equal(t, "http://www.example.com", builder.GetURL())
assert.Equal(t, "POST", builder.GetMethod())
testHeader := O.GetOrElse(F.Constant(""))(builder.GetHeader("X-Test"))
assert.Equal(t, "test-value", testHeader)
}
// TestWithQuery tests the WithQuery function
func TestWithQuery(t *testing.T) {
query := make(url.Values)
query.Set("key1", "value1")
query.Set("key2", "value2")
builder := F.Pipe1(
Default,
WithQuery(query),
)
assert.Equal(t, "value1", builder.GetQuery().Get("key1"))
assert.Equal(t, "value2", builder.GetQuery().Get("key2"))
}
// TestWithHeaders tests the WithHeaders function
func TestWithHeaders(t *testing.T) {
headers := make(http.Header)
headers.Set("X-Test", "test-value")
builder := F.Pipe1(
Default,
WithHeaders(headers),
)
assert.Equal(t, "test-value", builder.GetHeaders().Get("X-Test"))
}
// TestWithUrl tests the deprecated WithUrl function
func TestWithUrl(t *testing.T) {
builder := F.Pipe1(
Default,
WithUrl("http://www.example.com"),
)
assert.Equal(t, "http://www.example.com", builder.GetURL())
}
// TestComplexBuilderComposition tests a complex builder composition
func TestComplexBuilderComposition(t *testing.T) {
builder := F.Pipe5(
Default,
WithURL("http://api.example.com/users"),
WithPost,
WithJSON(map[string]interface{}{
"name": "John Doe",
"email": "john@example.com",
}),
WithBearer("secret-token"),
WithQueryArg("notify")("true"),
)
assert.Equal(t, "http://api.example.com/users", builder.GetURL())
assert.Equal(t, "POST", builder.GetMethod())
contentType := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.ContentType))
assert.Equal(t, C.JSON, contentType)
auth := O.GetOrElse(F.Constant(""))(builder.GetHeader(H.Authorization))
assert.Equal(t, "Bearer secret-token", auth)
assert.Equal(t, "true", builder.GetQuery().Get("notify"))
assert.True(t, O.IsSome(builder.GetBody()))
}

View File

@@ -0,0 +1,36 @@
mode: set
github.com/IBM/fp-go/v2/http/builder/builder.go:208.51,211.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:213.37,215.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:217.42,221.2 3 1
github.com/IBM/fp-go/v2/http/builder/builder.go:226.64,228.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:231.64,257.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:260.41,262.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:264.41,266.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:268.44,273.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:275.50,277.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:279.47,281.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:283.61,286.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:288.69,290.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:292.59,295.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:298.53,301.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:303.53,306.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:308.66,311.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:313.82,316.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:318.64,321.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:323.57,326.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:328.65,334.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:336.63,338.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:341.42,343.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:346.61,354.75 4 1
github.com/IBM/fp-go/v2/http/builder/builder.go:354.75,360.3 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:364.62,369.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:372.46,374.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:379.43,381.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:384.43,393.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:396.63,401.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:404.64,409.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:412.48,414.2 1 1
github.com/IBM/fp-go/v2/http/builder/builder.go:416.68,419.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:421.84,424.2 2 1
github.com/IBM/fp-go/v2/http/builder/builder.go:426.35,438.2 7 1
github.com/IBM/fp-go/v2/http/builder/builder.go:441.34,443.2 1 1

View File

@@ -21,8 +21,8 @@ import (
A "github.com/IBM/fp-go/v2/array"
ENDO "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
LA "github.com/IBM/fp-go/v2/optics/lens/array"
LO "github.com/IBM/fp-go/v2/optics/lens/option"
LRG "github.com/IBM/fp-go/v2/optics/lens/record/generic"
O "github.com/IBM/fp-go/v2/option"
RG "github.com/IBM/fp-go/v2/record/generic"
@@ -50,7 +50,7 @@ var (
composeHead = F.Pipe1(
LA.AtHead[string](),
L.ComposeOptions[url.Values, string](A.Empty[string]()),
LO.Compose[url.Values, string](A.Empty[string]()),
)
// AtValue is a [L.Lens] that focusses on first value in form fields

View File

@@ -13,6 +13,56 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package headers provides constants and utilities for working with HTTP headers
// in a functional programming style. It offers type-safe header name constants,
// monoid operations for combining headers, and lens-based access to header values.
//
// The package follows functional programming principles by providing:
// - Immutable operations through lenses
// - Monoid for combining header maps
// - Type-safe header name constants
// - Functional composition of header operations
//
// Constants:
//
// The package defines commonly used HTTP header names as constants:
// - Accept: The Accept request header
// - Authorization: The Authorization request header
// - ContentType: The Content-Type header
// - ContentLength: The Content-Length header
//
// Monoid:
//
// The Monoid provides a way to combine multiple http.Header maps:
//
// headers1 := make(http.Header)
// headers1.Set("X-Custom", "value1")
//
// headers2 := make(http.Header)
// headers2.Set("Authorization", "Bearer token")
//
// combined := Monoid.Concat(headers1, headers2)
// // combined now contains both headers
//
// Lenses:
//
// AtValues and AtValue provide lens-based access to header values:
//
// // AtValues focuses on all values of a header ([]string)
// contentTypeLens := AtValues("Content-Type")
// values := contentTypeLens.Get(headers)
//
// // AtValue focuses on the first value of a header (Option[string])
// authLens := AtValue("Authorization")
// token := authLens.Get(headers) // Returns Option[string]
//
// The lenses support functional updates:
//
// // Set a header value
// newHeaders := AtValue("Content-Type").Set(O.Some("application/json"))(headers)
//
// // Remove a header
// newHeaders := AtValue("X-Custom").Set(O.None[string]())(headers)
package headers
import (
@@ -21,36 +71,94 @@ import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
LA "github.com/IBM/fp-go/v2/optics/lens/array"
LO "github.com/IBM/fp-go/v2/optics/lens/option"
LRG "github.com/IBM/fp-go/v2/optics/lens/record/generic"
RG "github.com/IBM/fp-go/v2/record/generic"
)
// HTTP headers
// Common HTTP header name constants.
// These constants provide type-safe access to standard HTTP header names.
const (
Accept = "Accept"
// Accept specifies the media types that are acceptable for the response.
// Example: "Accept: application/json"
Accept = "Accept"
// Authorization contains credentials for authenticating the client with the server.
// Example: "Authorization: Bearer token123"
Authorization = "Authorization"
ContentType = "Content-Type"
// ContentType indicates the media type of the resource or data.
// Example: "Content-Type: application/json"
ContentType = "Content-Type"
// ContentLength indicates the size of the entity-body in bytes.
// Example: "Content-Length: 348"
ContentLength = "Content-Length"
)
var (
// Monoid is a [M.Monoid] to concatenate [http.Header] maps
// Monoid is a Monoid for combining http.Header maps.
// It uses a union operation where values from both headers are preserved.
// When the same header exists in both maps, the values are concatenated.
//
// Example:
// h1 := make(http.Header)
// h1.Set("X-Custom", "value1")
//
// h2 := make(http.Header)
// h2.Set("Authorization", "Bearer token")
//
// combined := Monoid.Concat(h1, h2)
// // combined contains both X-Custom and Authorization headers
Monoid = RG.UnionMonoid[http.Header](A.Semigroup[string]())
// AtValues is a [L.Lens] that focusses on the values of a header
// AtValues is a Lens that focuses on all values of a specific header.
// It returns a lens that accesses the []string slice of header values.
// The header name is automatically canonicalized using MIME header key rules.
//
// Parameters:
// - name: The header name (will be canonicalized)
//
// Returns:
// - A Lens[http.Header, []string] focusing on the header's values
//
// Example:
// lens := AtValues("Content-Type")
// values := lens.Get(headers) // Returns []string
// newHeaders := lens.Set([]string{"application/json"})(headers)
AtValues = F.Flow2(
textproto.CanonicalMIMEHeaderKey,
LRG.AtRecord[http.Header, []string],
)
// composeHead is an internal helper that composes a lens to focus on the first
// element of a string array, returning an Option[string].
composeHead = F.Pipe1(
LA.AtHead[string](),
L.ComposeOptions[http.Header, string](A.Empty[string]()),
LO.Compose[http.Header, string](A.Empty[string]()),
)
// AtValue is a [L.Lens] that focusses on first value of a header
// AtValue is a Lens that focuses on the first value of a specific header.
// It returns a lens that accesses an Option[string] representing the first
// header value, or None if the header doesn't exist.
// The header name is automatically canonicalized using MIME header key rules.
//
// Parameters:
// - name: The header name (will be canonicalized)
//
// Returns:
// - A Lens[http.Header, Option[string]] focusing on the first header value
//
// Example:
// lens := AtValue("Authorization")
// token := lens.Get(headers) // Returns Option[string]
//
// // Set a header value
// newHeaders := lens.Set(O.Some("Bearer token"))(headers)
//
// // Remove a header
// newHeaders := lens.Set(O.None[string]())(headers)
AtValue = F.Flow2(
AtValues,
composeHead,

View File

@@ -21,6 +21,7 @@ import (
A "github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
O "github.com/IBM/fp-go/v2/option"
RG "github.com/IBM/fp-go/v2/record/generic"
@@ -56,3 +57,281 @@ func TestLaws(t *testing.T) {
assert.True(t, fieldLaws(v1, s1))
assert.True(t, fieldLaws(v2, s1))
}
// TestMonoidEmpty tests the Monoid empty (identity) element
func TestMonoidEmpty(t *testing.T) {
empty := Monoid.Empty()
assert.NotNil(t, empty)
assert.Equal(t, 0, len(empty))
}
// TestMonoidConcat tests concatenating two header maps
func TestMonoidConcat(t *testing.T) {
h1 := make(http.Header)
h1.Set("X-Custom-1", "value1")
h1.Set("Authorization", "Bearer token1")
h2 := make(http.Header)
h2.Set("X-Custom-2", "value2")
h2.Set("Content-Type", "application/json")
result := Monoid.Concat(h1, h2)
assert.Equal(t, "value1", result.Get("X-Custom-1"))
assert.Equal(t, "value2", result.Get("X-Custom-2"))
assert.Equal(t, "Bearer token1", result.Get("Authorization"))
assert.Equal(t, "application/json", result.Get("Content-Type"))
}
// TestMonoidConcatWithOverlap tests concatenating headers with overlapping keys
func TestMonoidConcatWithOverlap(t *testing.T) {
h1 := make(http.Header)
h1.Set("X-Custom", "value1")
h2 := make(http.Header)
h2.Add("X-Custom", "value2")
result := Monoid.Concat(h1, h2)
// Both values should be present
values := result.Values("X-Custom")
assert.Contains(t, values, "value1")
assert.Contains(t, values, "value2")
}
// TestMonoidIdentity tests that concatenating with empty is identity
func TestMonoidIdentity(t *testing.T) {
h := make(http.Header)
h.Set("X-Test", "value")
empty := Monoid.Empty()
// Left identity: empty + h = h
leftResult := Monoid.Concat(empty, h)
assert.Equal(t, "value", leftResult.Get("X-Test"))
// Right identity: h + empty = h
rightResult := Monoid.Concat(h, empty)
assert.Equal(t, "value", rightResult.Get("X-Test"))
}
// TestAtValuesGet tests getting header values using AtValues lens
func TestAtValuesGet(t *testing.T) {
headers := make(http.Header)
headers.Set("Content-Type", "application/json")
headers.Add("Accept", "application/json")
headers.Add("Accept", "text/html")
// Get Content-Type values
ctLens := AtValues("Content-Type")
ctValuesOpt := ctLens.Get(headers)
assert.True(t, O.IsSome(ctValuesOpt))
ctValues := O.GetOrElse(F.Constant([]string{}))(ctValuesOpt)
assert.Equal(t, []string{"application/json"}, ctValues)
// Get Accept values (multiple)
acceptLens := AtValues("Accept")
acceptValuesOpt := acceptLens.Get(headers)
assert.True(t, O.IsSome(acceptValuesOpt))
acceptValues := O.GetOrElse(F.Constant([]string{}))(acceptValuesOpt)
assert.Equal(t, 2, len(acceptValues))
assert.Contains(t, acceptValues, "application/json")
assert.Contains(t, acceptValues, "text/html")
}
// TestAtValuesSet tests setting header values using AtValues lens
func TestAtValuesSet(t *testing.T) {
headers := make(http.Header)
headers.Set("X-Old", "old-value")
lens := AtValues("Content-Type")
newHeaders := lens.Set(O.Some([]string{"application/json", "text/plain"}))(headers)
// New header should be set
values := newHeaders.Values("Content-Type")
assert.Equal(t, 2, len(values))
assert.Contains(t, values, "application/json")
assert.Contains(t, values, "text/plain")
// Old header should still exist
assert.Equal(t, "old-value", newHeaders.Get("X-Old"))
}
// TestAtValuesCanonical tests that header names are canonicalized
func TestAtValuesCanonical(t *testing.T) {
headers := make(http.Header)
headers.Set("content-type", "application/json")
// Access with different casing
lens := AtValues("Content-Type")
valuesOpt := lens.Get(headers)
assert.True(t, O.IsSome(valuesOpt))
values := O.GetOrElse(F.Constant([]string{}))(valuesOpt)
assert.Equal(t, []string{"application/json"}, values)
}
// TestAtValueGet tests getting first header value using AtValue lens
func TestAtValueGet(t *testing.T) {
headers := make(http.Header)
headers.Set("Authorization", "Bearer token123")
lens := AtValue("Authorization")
value := lens.Get(headers)
assert.True(t, O.IsSome(value))
token := O.GetOrElse(F.Constant(""))(value)
assert.Equal(t, "Bearer token123", token)
}
// TestAtValueGetNone tests getting non-existent header returns None
func TestAtValueGetNone(t *testing.T) {
headers := make(http.Header)
lens := AtValue("X-Non-Existent")
value := lens.Get(headers)
assert.True(t, O.IsNone(value))
}
// TestAtValueSet tests setting header value using AtValue lens
func TestAtValueSet(t *testing.T) {
headers := make(http.Header)
lens := AtValue("Content-Type")
newHeaders := lens.Set(O.Some("application/json"))(headers)
value := lens.Get(newHeaders)
assert.True(t, O.IsSome(value))
ct := O.GetOrElse(F.Constant(""))(value)
assert.Equal(t, "application/json", ct)
}
// TestAtValueSetNone tests removing header using AtValue lens
func TestAtValueSetNone(t *testing.T) {
headers := make(http.Header)
headers.Set("X-Custom", "value")
lens := AtValue("X-Custom")
newHeaders := lens.Set(O.None[string]())(headers)
value := lens.Get(newHeaders)
assert.True(t, O.IsNone(value))
}
// TestAtValueMultipleValues tests AtValue with multiple header values
func TestAtValueMultipleValues(t *testing.T) {
headers := make(http.Header)
headers.Add("Accept", "application/json")
headers.Add("Accept", "text/html")
lens := AtValue("Accept")
value := lens.Get(headers)
assert.True(t, O.IsSome(value))
// Should get the first value
first := O.GetOrElse(F.Constant(""))(value)
assert.Equal(t, "application/json", first)
}
// TestHeaderConstants tests that header constants are correct
func TestHeaderConstants(t *testing.T) {
assert.Equal(t, "Accept", Accept)
assert.Equal(t, "Authorization", Authorization)
assert.Equal(t, "Content-Type", ContentType)
assert.Equal(t, "Content-Length", ContentLength)
}
// TestHeaderConstantsUsage tests using header constants with http.Header
func TestHeaderConstantsUsage(t *testing.T) {
headers := make(http.Header)
headers.Set(Accept, "application/json")
headers.Set(Authorization, "Bearer token")
headers.Set(ContentType, "application/json")
headers.Set(ContentLength, "1234")
assert.Equal(t, "application/json", headers.Get(Accept))
assert.Equal(t, "Bearer token", headers.Get(Authorization))
assert.Equal(t, "application/json", headers.Get(ContentType))
assert.Equal(t, "1234", headers.Get(ContentLength))
}
// TestAtValueWithConstants tests using AtValue with header constants
func TestAtValueWithConstants(t *testing.T) {
headers := make(http.Header)
headers.Set(ContentType, "application/json")
lens := AtValue(ContentType)
value := lens.Get(headers)
assert.True(t, O.IsSome(value))
ct := O.GetOrElse(F.Constant(""))(value)
assert.Equal(t, "application/json", ct)
}
// TestMonoidAssociativity tests that Monoid concatenation is associative
func TestMonoidAssociativity(t *testing.T) {
h1 := make(http.Header)
h1.Set("X-1", "value1")
h2 := make(http.Header)
h2.Set("X-2", "value2")
h3 := make(http.Header)
h3.Set("X-3", "value3")
// (h1 + h2) + h3
left := Monoid.Concat(Monoid.Concat(h1, h2), h3)
// h1 + (h2 + h3)
right := Monoid.Concat(h1, Monoid.Concat(h2, h3))
// Both should have all three headers
assert.Equal(t, "value1", left.Get("X-1"))
assert.Equal(t, "value2", left.Get("X-2"))
assert.Equal(t, "value3", left.Get("X-3"))
assert.Equal(t, "value1", right.Get("X-1"))
assert.Equal(t, "value2", right.Get("X-2"))
assert.Equal(t, "value3", right.Get("X-3"))
}
// TestAtValuesEmptyHeader tests AtValues with empty headers
func TestAtValuesEmptyHeader(t *testing.T) {
headers := make(http.Header)
lens := AtValues("X-Non-Existent")
valuesOpt := lens.Get(headers)
assert.True(t, O.IsNone(valuesOpt))
}
// TestComplexHeaderOperations tests complex operations combining lenses and monoid
func TestComplexHeaderOperations(t *testing.T) {
// Create initial headers
h1 := make(http.Header)
h1.Set("X-Initial", "initial")
// Use lens to add Content-Type
ctLens := AtValue(ContentType)
h2 := ctLens.Set(O.Some("application/json"))(h1)
// Use lens to add Authorization
authLens := AtValue(Authorization)
h3 := authLens.Set(O.Some("Bearer token"))(h2)
// Create additional headers
h4 := make(http.Header)
h4.Set("X-Additional", "additional")
// Combine using Monoid
final := Monoid.Concat(h3, h4)
// Verify all headers are present
assert.Equal(t, "initial", final.Get("X-Initial"))
assert.Equal(t, "application/json", final.Get(ContentType))
assert.Equal(t, "Bearer token", final.Get(Authorization))
assert.Equal(t, "additional", final.Get("X-Additional"))
}

View File

@@ -13,6 +13,53 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package http provides functional programming utilities for working with HTTP
// requests and responses. It offers type-safe abstractions, validation functions,
// and utilities for handling HTTP operations in a functional style.
//
// The package includes:
// - Type definitions for HTTP responses with bodies
// - Validation functions for HTTP responses
// - JSON content type validation
// - Error handling with detailed HTTP error information
// - Functional utilities for accessing response components
//
// Types:
//
// FullResponse represents a complete HTTP response including both the response
// object and the body as a byte array. It's implemented as a Pair for functional
// composition:
//
// type FullResponse = Pair[*http.Response, []byte]
//
// The Response and Body functions provide lens-like access to the components:
//
// resp := Response(fullResponse) // Get *http.Response
// body := Body(fullResponse) // Get []byte
//
// Validation:
//
// ValidateResponse checks if an HTTP response has a successful status code (2xx):
//
// result := ValidateResponse(response)
// // Returns Either[error, *http.Response]
//
// ValidateJSONResponse validates both the status code and Content-Type header:
//
// result := ValidateJSONResponse(response)
// // Returns Either[error, *http.Response]
//
// Error Handling:
//
// HttpError provides detailed information about HTTP failures:
//
// err := StatusCodeError(response)
// if httpErr, ok := err.(*HttpError); ok {
// code := httpErr.StatusCode()
// headers := httpErr.Headers()
// body := httpErr.Body()
// url := httpErr.URL()
// }
package http
import (
@@ -22,11 +69,38 @@ import (
)
type (
// FullResponse represents a full http response, including headers and body
// FullResponse represents a complete HTTP response including both the
// *http.Response object and the response body as a byte slice.
//
// It's implemented as a Pair to enable functional composition and
// transformation of HTTP responses. This allows you to work with both
// the response metadata (status, headers) and body content together.
//
// Example:
// fullResp := MakePair(response, bodyBytes)
// resp := Response(fullResp) // Extract *http.Response
// body := Body(fullResp) // Extract []byte
FullResponse = P.Pair[*H.Response, []byte]
)
var (
// Response is a lens-like accessor that extracts the *http.Response
// from a FullResponse. It provides functional access to the response
// metadata including status code, headers, and other HTTP response fields.
//
// Example:
// fullResp := MakePair(response, bodyBytes)
// resp := Response(fullResp)
// statusCode := resp.StatusCode
Response = P.Head[*H.Response, []byte]
Body = P.Tail[*H.Response, []byte]
// Body is a lens-like accessor that extracts the response body bytes
// from a FullResponse. It provides functional access to the raw body
// content without needing to read from an io.Reader.
//
// Example:
// fullResp := MakePair(response, bodyBytes)
// body := Body(fullResp)
// content := string(body)
Body = P.Tail[*H.Response, []byte]
)

View File

@@ -33,8 +33,29 @@ import (
)
type (
// ParsedMediaType represents a parsed MIME media type as a Pair.
// The first element is the media type string (e.g., "application/json"),
// and the second element is a map of parameters (e.g., {"charset": "utf-8"}).
//
// Example:
// parsed := ParseMediaType("application/json; charset=utf-8")
// mediaType := P.Head(parsed) // "application/json"
// params := P.Tail(parsed) // map[string]string{"charset": "utf-8"}
ParsedMediaType = P.Pair[string, map[string]string]
// HttpError represents an HTTP error with detailed information about
// the failed request. It includes the status code, response headers,
// response body, and the URL that was accessed.
//
// This error type is created by StatusCodeError when an HTTP response
// has a non-successful status code (not 2xx).
//
// Example:
// if httpErr, ok := err.(*HttpError); ok {
// fmt.Printf("Status: %d\n", httpErr.StatusCode())
// fmt.Printf("URL: %s\n", httpErr.URL())
// fmt.Printf("Body: %s\n", string(httpErr.Body()))
// }
HttpError struct {
statusCode int
headers H.Header
@@ -44,11 +65,28 @@ type (
)
var (
// mime type to check if a media type matches
// isJSONMimeType is a regex matcher that checks if a media type is a valid JSON type.
// It matches "application/json" and variants like "application/vnd.api+json".
isJSONMimeType = regexp.MustCompile(`application/(?:\w+\+)?json`).MatchString
// ValidateResponse validates an HTTP response and returns an [E.Either] if the response is not a success
// ValidateResponse validates an HTTP response and returns an Either.
// It checks if the response has a successful status code (2xx range).
//
// Returns:
// - Right(*http.Response) if status code is 2xx
// - Left(error) with HttpError if status code is not 2xx
//
// Example:
// result := ValidateResponse(response)
// E.Fold(
// func(err error) { /* handle error */ },
// func(resp *http.Response) { /* handle success */ },
// )(result)
ValidateResponse = E.FromPredicate(isValidStatus, StatusCodeError)
// alidateJsonContentTypeString parses a content type a validates that it is valid JSON
// validateJSONContentTypeString parses a content type string and validates
// that it represents a valid JSON media type. This is an internal helper
// used by ValidateJSONResponse.
validateJSONContentTypeString = F.Flow2(
ParseMediaType,
E.ChainFirst(F.Flow2(
@@ -56,7 +94,21 @@ var (
E.FromPredicate(isJSONMimeType, errors.OnSome[string]("mimetype [%s] is not a valid JSON content type")),
)),
)
// ValidateJSONResponse checks if an HTTP response is a valid JSON response
// ValidateJSONResponse validates that an HTTP response is a valid JSON response.
// It checks both the status code (must be 2xx) and the Content-Type header
// (must be a JSON media type like "application/json").
//
// Returns:
// - Right(*http.Response) if response is valid JSON with 2xx status
// - Left(error) if status is not 2xx or Content-Type is not JSON
//
// Example:
// result := ValidateJSONResponse(response)
// E.Fold(
// func(err error) { /* handle non-JSON or error response */ },
// func(resp *http.Response) { /* handle valid JSON response */ },
// )(result)
ValidateJSONResponse = F.Flow2(
E.Of[error, *H.Response],
E.ChainFirst(F.Flow5(
@@ -66,60 +118,175 @@ var (
E.FromOption[string](errors.OnNone("unable to access the [%s] header", HeaderContentType)),
E.ChainFirst(validateJSONContentTypeString),
)))
// ValidateJsonResponse checks if an HTTP response is a valid JSON response
// ValidateJsonResponse checks if an HTTP response is a valid JSON response.
//
// Deprecated: use [ValidateJSONResponse] instead
// Deprecated: use ValidateJSONResponse instead (note the capitalization).
ValidateJsonResponse = ValidateJSONResponse
)
const (
// HeaderContentType is the standard HTTP Content-Type header name.
// It indicates the media type of the resource or data being sent.
//
// Example values:
// - "application/json"
// - "text/html; charset=utf-8"
// - "application/xml"
HeaderContentType = "Content-Type"
)
// ParseMediaType parses a media type into a tuple
// ParseMediaType parses a MIME media type string into its components.
// It returns a ParsedMediaType (Pair) containing the media type and its parameters.
//
// Parameters:
// - mediaType: A media type string (e.g., "application/json; charset=utf-8")
//
// Returns:
// - Right(ParsedMediaType) with the parsed media type and parameters
// - Left(error) if the media type string is invalid
//
// Example:
//
// result := ParseMediaType("application/json; charset=utf-8")
// E.Map(func(parsed ParsedMediaType) {
// mediaType := P.Head(parsed) // "application/json"
// params := P.Tail(parsed) // map[string]string{"charset": "utf-8"}
// })(result)
func ParseMediaType(mediaType string) E.Either[error, ParsedMediaType] {
m, p, err := mime.ParseMediaType(mediaType)
return E.TryCatchError(P.MakePair(m, p), err)
}
// Error fulfills the error interface
// Error implements the error interface for HttpError.
// It returns a formatted error message including the status code and URL.
func (r *HttpError) Error() string {
return fmt.Sprintf("invalid status code [%d] when accessing URL [%s]", r.statusCode, r.url)
}
// String returns the string representation of the HttpError.
// It's equivalent to calling Error().
func (r *HttpError) String() string {
return r.Error()
}
// StatusCode returns the HTTP status code from the failed response.
//
// Example:
//
// if httpErr, ok := err.(*HttpError); ok {
// code := httpErr.StatusCode() // e.g., 404, 500
// }
func (r *HttpError) StatusCode() int {
return r.statusCode
}
// Headers returns a clone of the HTTP headers from the failed response.
// The headers are cloned to prevent modification of the original response.
//
// Example:
//
// if httpErr, ok := err.(*HttpError); ok {
// headers := httpErr.Headers()
// contentType := headers.Get("Content-Type")
// }
func (r *HttpError) Headers() H.Header {
return r.headers
}
// URL returns the URL that was accessed when the error occurred.
//
// Example:
//
// if httpErr, ok := err.(*HttpError); ok {
// url := httpErr.URL()
// fmt.Printf("Failed to access: %s\n", url)
// }
func (r *HttpError) URL() *url.URL {
return r.url
}
// Body returns the response body bytes from the failed response.
// This can be useful for debugging or displaying error messages from the server.
//
// Example:
//
// if httpErr, ok := err.(*HttpError); ok {
// body := httpErr.Body()
// fmt.Printf("Error response: %s\n", string(body))
// }
func (r *HttpError) Body() []byte {
return r.body
}
// GetHeader extracts the HTTP headers from an http.Response.
// This is a functional accessor for the Header field.
//
// Parameters:
// - resp: The HTTP response
//
// Returns:
// - The http.Header map from the response
//
// Example:
//
// headers := GetHeader(response)
// contentType := headers.Get("Content-Type")
func GetHeader(resp *H.Response) H.Header {
return resp.Header
}
// GetBody extracts the response body reader from an http.Response.
// This is a functional accessor for the Body field.
//
// Parameters:
// - resp: The HTTP response
//
// Returns:
// - The io.ReadCloser for reading the response body
//
// Example:
//
// body := GetBody(response)
// defer body.Close()
// data, err := io.ReadAll(body)
func GetBody(resp *H.Response) io.ReadCloser {
return resp.Body
}
// isValidStatus checks if an HTTP response has a successful status code.
// A status code is considered valid if it's in the 2xx range (200-299).
//
// Parameters:
// - resp: The HTTP response to check
//
// Returns:
// - true if status code is 2xx, false otherwise
func isValidStatus(resp *H.Response) bool {
return resp.StatusCode >= H.StatusOK && resp.StatusCode < H.StatusMultipleChoices
}
// StatusCodeError creates an instance of [HttpError] filled with information from the response
// StatusCodeError creates an HttpError from an http.Response with a non-successful status code.
// It reads the response body and captures all relevant information for debugging.
//
// The function:
// - Reads and stores the response body
// - Clones the response headers
// - Captures the request URL
// - Creates a comprehensive error with all this information
//
// Parameters:
// - resp: The HTTP response with a non-successful status code
//
// Returns:
// - An error (specifically *HttpError) with detailed information
//
// Example:
//
// if !isValidStatus(response) {
// err := StatusCodeError(response)
// return err
// }
func StatusCodeError(resp *H.Response) error {
// read the body
bodyRdr := GetBody(resp)

View File

@@ -16,12 +16,18 @@
package http
import (
"bytes"
"io"
H "net/http"
"net/url"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
C "github.com/IBM/fp-go/v2/http/content"
P "github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func NoError[A any](t *testing.T) func(E.Either[error, A]) bool {
@@ -37,21 +43,351 @@ func Error[A any](t *testing.T) func(E.Either[error, A]) bool {
}
func TestValidateJsonContentTypeString(t *testing.T) {
res := F.Pipe1(
validateJSONContentTypeString(C.JSON),
NoError[ParsedMediaType](t),
)
assert.True(t, res)
}
func TestValidateInvalidJsonContentTypeString(t *testing.T) {
res := F.Pipe1(
validateJSONContentTypeString("application/xml"),
Error[ParsedMediaType](t),
)
assert.True(t, res)
}
// TestParseMediaType tests parsing valid media types
func TestParseMediaType(t *testing.T) {
tests := []struct {
name string
mediaType string
wantType string
wantParam map[string]string
}{
{
name: "simple JSON",
mediaType: "application/json",
wantType: "application/json",
wantParam: map[string]string{},
},
{
name: "JSON with charset",
mediaType: "application/json; charset=utf-8",
wantType: "application/json",
wantParam: map[string]string{"charset": "utf-8"},
},
{
name: "HTML with charset",
mediaType: "text/html; charset=iso-8859-1",
wantType: "text/html",
wantParam: map[string]string{"charset": "iso-8859-1"},
},
{
name: "multipart with boundary",
mediaType: "multipart/form-data; boundary=----WebKitFormBoundary",
wantType: "multipart/form-data",
wantParam: map[string]string{"boundary": "----WebKitFormBoundary"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ParseMediaType(tt.mediaType)
require.True(t, E.IsRight(result), "ParseMediaType should succeed")
parsed := E.GetOrElse(func(error) ParsedMediaType {
return P.MakePair("", map[string]string{})
})(result)
mediaType := P.Head(parsed)
params := P.Tail(parsed)
assert.Equal(t, tt.wantType, mediaType)
assert.Equal(t, tt.wantParam, params)
})
}
}
// TestParseMediaTypeInvalid tests parsing invalid media types
func TestParseMediaTypeInvalid(t *testing.T) {
result := ParseMediaType("invalid media type")
assert.True(t, E.IsLeft(result), "ParseMediaType should fail for invalid input")
}
// TestHttpErrorMethods tests all HttpError methods
func TestHttpErrorMethods(t *testing.T) {
testURL, _ := url.Parse("https://example.com/api/test")
headers := make(H.Header)
headers.Set("Content-Type", "application/json")
headers.Set("X-Custom", "value")
body := []byte(`{"error": "not found"}`)
httpErr := &HttpError{
statusCode: 404,
headers: headers,
body: body,
url: testURL,
}
// Test StatusCode
assert.Equal(t, 404, httpErr.StatusCode())
// Test Headers
returnedHeaders := httpErr.Headers()
assert.Equal(t, "application/json", returnedHeaders.Get("Content-Type"))
assert.Equal(t, "value", returnedHeaders.Get("X-Custom"))
// Test Body
assert.Equal(t, body, httpErr.Body())
assert.Equal(t, `{"error": "not found"}`, string(httpErr.Body()))
// Test URL
assert.Equal(t, testURL, httpErr.URL())
assert.Equal(t, "https://example.com/api/test", httpErr.URL().String())
// Test Error
errMsg := httpErr.Error()
assert.Contains(t, errMsg, "404")
assert.Contains(t, errMsg, "https://example.com/api/test")
// Test String
assert.Equal(t, errMsg, httpErr.String())
}
// TestGetHeader tests the GetHeader function
func TestGetHeader(t *testing.T) {
resp := &H.Response{
Header: make(H.Header),
}
resp.Header.Set("Content-Type", "application/json")
resp.Header.Set("Authorization", "Bearer token")
headers := GetHeader(resp)
assert.Equal(t, "application/json", headers.Get("Content-Type"))
assert.Equal(t, "Bearer token", headers.Get("Authorization"))
}
// TestGetBody tests the GetBody function
func TestGetBody(t *testing.T) {
bodyContent := []byte("test body content")
resp := &H.Response{
Body: io.NopCloser(bytes.NewReader(bodyContent)),
}
body := GetBody(resp)
defer body.Close()
data, err := io.ReadAll(body)
require.NoError(t, err)
assert.Equal(t, bodyContent, data)
}
// TestIsValidStatus tests the isValidStatus function
func TestIsValidStatus(t *testing.T) {
tests := []struct {
name string
statusCode int
want bool
}{
{"200 OK", H.StatusOK, true},
{"201 Created", H.StatusCreated, true},
{"204 No Content", H.StatusNoContent, true},
{"299 (edge of 2xx)", 299, true},
{"300 Multiple Choices", H.StatusMultipleChoices, false},
{"301 Moved Permanently", H.StatusMovedPermanently, false},
{"400 Bad Request", H.StatusBadRequest, false},
{"404 Not Found", H.StatusNotFound, false},
{"500 Internal Server Error", H.StatusInternalServerError, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
resp := &H.Response{StatusCode: tt.statusCode}
assert.Equal(t, tt.want, isValidStatus(resp))
})
}
}
// TestValidateResponse tests the ValidateResponse function
func TestValidateResponse(t *testing.T) {
t.Run("successful response", func(t *testing.T) {
resp := &H.Response{
StatusCode: H.StatusOK,
Header: make(H.Header),
}
result := ValidateResponse(resp)
assert.True(t, E.IsRight(result))
validResp := E.GetOrElse(func(error) *H.Response { return nil })(result)
assert.Equal(t, resp, validResp)
})
t.Run("error response", func(t *testing.T) {
testURL, _ := url.Parse("https://example.com/test")
resp := &H.Response{
StatusCode: H.StatusNotFound,
Header: make(H.Header),
Body: io.NopCloser(bytes.NewReader([]byte("not found"))),
Request: &H.Request{URL: testURL},
}
result := ValidateResponse(resp)
assert.True(t, E.IsLeft(result))
// Extract error using Fold
var httpErr *HttpError
E.Fold(
func(err error) *H.Response {
var ok bool
httpErr, ok = err.(*HttpError)
require.True(t, ok, "error should be *HttpError")
return nil
},
func(r *H.Response) *H.Response { return r },
)(result)
assert.Equal(t, 404, httpErr.StatusCode())
})
}
// TestStatusCodeError tests the StatusCodeError function
func TestStatusCodeError(t *testing.T) {
testURL, _ := url.Parse("https://api.example.com/users/123")
bodyContent := []byte(`{"error": "user not found"}`)
headers := make(H.Header)
headers.Set("Content-Type", "application/json")
headers.Set("X-Request-ID", "abc123")
resp := &H.Response{
StatusCode: H.StatusNotFound,
Header: headers,
Body: io.NopCloser(bytes.NewReader(bodyContent)),
Request: &H.Request{URL: testURL},
}
err := StatusCodeError(resp)
require.Error(t, err)
httpErr, ok := err.(*HttpError)
require.True(t, ok, "error should be *HttpError")
// Verify all fields
assert.Equal(t, 404, httpErr.StatusCode())
assert.Equal(t, testURL, httpErr.URL())
assert.Equal(t, bodyContent, httpErr.Body())
// Verify headers are cloned
returnedHeaders := httpErr.Headers()
assert.Equal(t, "application/json", returnedHeaders.Get("Content-Type"))
assert.Equal(t, "abc123", returnedHeaders.Get("X-Request-ID"))
// Verify error message
errMsg := httpErr.Error()
assert.Contains(t, errMsg, "404")
assert.Contains(t, errMsg, "https://api.example.com/users/123")
}
// TestValidateJSONResponse tests the ValidateJSONResponse function
func TestValidateJSONResponse(t *testing.T) {
t.Run("valid JSON response", func(t *testing.T) {
resp := &H.Response{
StatusCode: H.StatusOK,
Header: make(H.Header),
}
resp.Header.Set("Content-Type", "application/json")
result := ValidateJSONResponse(resp)
assert.True(t, E.IsRight(result), "should accept valid JSON response")
})
t.Run("JSON with charset", func(t *testing.T) {
resp := &H.Response{
StatusCode: H.StatusOK,
Header: make(H.Header),
}
resp.Header.Set("Content-Type", "application/json; charset=utf-8")
result := ValidateJSONResponse(resp)
assert.True(t, E.IsRight(result), "should accept JSON with charset")
})
t.Run("JSON variant (hal+json)", func(t *testing.T) {
resp := &H.Response{
StatusCode: H.StatusOK,
Header: make(H.Header),
}
resp.Header.Set("Content-Type", "application/hal+json")
result := ValidateJSONResponse(resp)
assert.True(t, E.IsRight(result), "should accept JSON variants")
})
t.Run("non-JSON content type", func(t *testing.T) {
resp := &H.Response{
StatusCode: H.StatusOK,
Header: make(H.Header),
}
resp.Header.Set("Content-Type", "text/html")
result := ValidateJSONResponse(resp)
assert.True(t, E.IsLeft(result), "should reject non-JSON content type")
})
t.Run("missing Content-Type header", func(t *testing.T) {
resp := &H.Response{
StatusCode: H.StatusOK,
Header: make(H.Header),
}
result := ValidateJSONResponse(resp)
assert.True(t, E.IsLeft(result), "should reject missing Content-Type")
})
t.Run("valid JSON with error status code", func(t *testing.T) {
// Note: ValidateJSONResponse only validates Content-Type, not status code
// It wraps the response in Right(response) first, then validates headers
resp := &H.Response{
StatusCode: H.StatusInternalServerError,
Header: make(H.Header),
}
resp.Header.Set("Content-Type", "application/json")
result := ValidateJSONResponse(resp)
// This actually succeeds because ValidateJSONResponse doesn't check status
assert.True(t, E.IsRight(result), "ValidateJSONResponse only checks Content-Type, not status")
})
}
// TestFullResponseAccessors tests Response and Body accessors
func TestFullResponseAccessors(t *testing.T) {
resp := &H.Response{
StatusCode: H.StatusOK,
Header: make(H.Header),
}
resp.Header.Set("Content-Type", "application/json")
bodyContent := []byte(`{"message": "success"}`)
fullResp := P.MakePair(resp, bodyContent)
// Test Response accessor
extractedResp := Response(fullResp)
assert.Equal(t, resp, extractedResp)
assert.Equal(t, H.StatusOK, extractedResp.StatusCode)
// Test Body accessor
extractedBody := Body(fullResp)
assert.Equal(t, bodyContent, extractedBody)
assert.Equal(t, `{"message": "success"}`, string(extractedBody))
}
// TestHeaderContentTypeConstant tests the HeaderContentType constant
func TestHeaderContentTypeConstant(t *testing.T) {
assert.Equal(t, "Content-Type", HeaderContentType)
// Test usage with http.Header
headers := make(H.Header)
headers.Set(HeaderContentType, "application/json")
assert.Equal(t, "application/json", headers.Get(HeaderContentType))
}

View File

@@ -21,14 +21,55 @@ import (
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// 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 {
// X int
// Y int
// }
// result := identity.Do(State{})
func Do[S any](
empty S,
) S {
return empty
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// 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.
//
// 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 {
// X int
// Y int
// }
//
// result := F.Pipe2(
// identity.Do(State{}),
// identity.Bind(
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// func(s State) int {
// return 42
// },
// ),
// identity.Bind(
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) int {
// // This can access s.X from the previous step
// return s.X * 2
// },
// ),
// ) // State{X: 42, Y: 84}
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
@@ -75,7 +116,36 @@ func BindTo[S1, T any](
)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
// 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 {
// X int
// Y int
// }
//
// // These operations are independent and can be combined with ApS
// result := F.Pipe2(
// identity.Do(State{}),
// identity.ApS(
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// 42,
// ),
// identity.ApS(
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// 100,
// ),
// ) // State{X: 42, Y: 100}
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa T,

View File

@@ -49,19 +49,19 @@ func Of[A any](a A) A {
return a
}
func MonadChain[A, B any](ma A, f func(A) B) B {
func MonadChain[A, B any](ma A, f Kleisli[A, B]) B {
return f(ma)
}
func Chain[A, B any](f func(A) B) Operator[A, B] {
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return f
}
func MonadChainFirst[A, B any](fa A, f func(A) B) A {
func MonadChainFirst[A, B any](fa A, f Kleisli[A, B]) A {
return chain.MonadChainFirst(MonadChain[A, A], MonadMap[B, A], fa, f)
}
func ChainFirst[A, B any](f func(A) B) Operator[A, A] {
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return chain.ChainFirst(Chain[A, A], Map[B, A], f)
}

View File

@@ -16,5 +16,6 @@
package identity
type (
Operator[A, B any] = func(A) B
Kleisli[A, B any] = func(A) B
Operator[A, B any] = Kleisli[A, B]
)

View File

@@ -19,6 +19,7 @@ import (
INTA "github.com/IBM/fp-go/v2/internal/apply"
INTC "github.com/IBM/fp-go/v2/internal/chain"
INTF "github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type S to be used with the Bind operation.
@@ -58,7 +59,7 @@ func Do[S any](
// }, fetchUser)
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) IO[T],
f Kleisli[S1, T],
) Operator[S1, S2] {
return INTC.Bind(
Chain[S1, S2],
@@ -152,3 +153,136 @@ func ApS[S1, S2, T any](
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 Config struct {
// Host string
// Port int
// }
//
// portLens := lens.MakeLens(
// func(c Config) int { return c.Port },
// func(c Config, p int) Config { c.Port = p; return c },
// )
//
// result := F.Pipe2(
// io.Of(Config{Host: "localhost"}),
// io.ApSL(portLens, io.Of(8080)),
// )
func ApSL[S, T any](
lens L.Lens[S, T],
fa IO[T],
) Operator[S, S] {
return ApS(lens.Set, fa)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// an IO that produces the new value.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Increment the counter asynchronously
// increment := func(v int) io.IO[int] {
// return io.Of(v + 1)
// }
//
// result := F.Pipe1(
// io.Of(Counter{Value: 42}),
// io.BindL(valueLens, increment),
// ) // IO[Counter{Value: 43}]
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return Bind[S, S, T](lens.Set, func(s S) IO[T] {
return f(lens.Get(s))
})
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in IO).
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Double the counter value
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// io.Of(Counter{Value: 21}),
// io.LetL(valueLens, double),
// ) // IO[Counter{Value: 42}]
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Operator[S, S] {
return Let[S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// io.Of(Config{Debug: true, Timeout: 30}),
// io.LetToL(debugLens, false),
// ) // IO[Config{Debug: false, Timeout: 30}]
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Operator[S, S] {
return LetTo[S, S, T](lens.Set, b)
}

View File

@@ -20,6 +20,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/stretchr/testify/assert"
)
@@ -54,3 +55,144 @@ func TestApS(t *testing.T) {
assert.Equal(t, res(), "John Doe")
}
// Test types for lens-based operations
type Counter struct {
Value int
}
type Person struct {
Name string
Age int
}
func TestBindL(t *testing.T) {
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("BindL with successful transformation", func(t *testing.T) {
increment := func(v int) IO[int] {
return Of(v + 1)
}
result := F.Pipe1(
Of(Counter{Value: 42}),
BindL(valueLens, increment),
)
assert.Equal(t, Counter{Value: 43}, result())
})
t.Run("BindL with multiple operations", func(t *testing.T) {
double := func(v int) IO[int] {
return Of(v * 2)
}
addTen := func(v int) IO[int] {
return Of(v + 10)
}
result := F.Pipe2(
Of(Counter{Value: 5}),
BindL(valueLens, double),
BindL(valueLens, addTen),
)
assert.Equal(t, Counter{Value: 20}, result())
})
}
func TestLetL(t *testing.T) {
valueLens := L.MakeLens(
func(c Counter) int { return c.Value },
func(c Counter, v int) Counter { c.Value = v; return c },
)
t.Run("LetL with pure transformation", func(t *testing.T) {
double := func(v int) int { return v * 2 }
result := F.Pipe1(
Of(Counter{Value: 21}),
LetL(valueLens, double),
)
assert.Equal(t, Counter{Value: 42}, result())
})
t.Run("LetL with multiple transformations", func(t *testing.T) {
double := func(v int) int { return v * 2 }
addTen := func(v int) int { return v + 10 }
result := F.Pipe2(
Of(Counter{Value: 5}),
LetL(valueLens, double),
LetL(valueLens, addTen),
)
assert.Equal(t, Counter{Value: 20}, result())
})
}
func TestLetToL(t *testing.T) {
ageLens := L.MakeLens(
func(p Person) int { return p.Age },
func(p Person, a int) Person { p.Age = a; return p },
)
t.Run("LetToL with constant value", func(t *testing.T) {
result := F.Pipe1(
Of(Person{Name: "Alice", Age: 25}),
LetToL(ageLens, 30),
)
assert.Equal(t, Person{Name: "Alice", Age: 30}, result())
})
t.Run("LetToL with multiple fields", func(t *testing.T) {
nameLens := L.MakeLens(
func(p Person) string { return p.Name },
func(p Person, n string) Person { p.Name = n; return p },
)
result := F.Pipe2(
Of(Person{Name: "Alice", Age: 25}),
LetToL(ageLens, 30),
LetToL(nameLens, "Bob"),
)
assert.Equal(t, Person{Name: "Bob", Age: 30}, result())
})
}
func TestApSL(t *testing.T) {
ageLens := L.MakeLens(
func(p Person) int { return p.Age },
func(p Person, a int) Person { p.Age = a; return p },
)
t.Run("ApSL with value", func(t *testing.T) {
result := F.Pipe1(
Of(Person{Name: "Alice", Age: 25}),
ApSL(ageLens, Of(30)),
)
assert.Equal(t, Person{Name: "Alice", Age: 30}, result())
})
t.Run("ApSL with chaining", func(t *testing.T) {
nameLens := L.MakeLens(
func(p Person) string { return p.Name },
func(p Person, n string) Person { p.Name = n; return p },
)
result := F.Pipe2(
Of(Person{Name: "Alice", Age: 25}),
ApSL(ageLens, Of(30)),
ApSL(nameLens, Of("Bob")),
)
assert.Equal(t, Person{Name: "Bob", Age: 30}, result())
})
}

View File

@@ -23,7 +23,7 @@ import (
// whether the body action returns and error or not.
func Bracket[A, B, ANY any](
acquire IO[A],
use func(A) IO[B],
use Kleisli[A, B],
release func(A, B) IO[ANY],
) IO[B] {
return INTB.Bracket[IO[A], IO[B], IO[ANY], B, A, B](

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,8 @@ type (
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioltagt] for more details
IO[A any] = func() A
Operator[A, B any] = R.Reader[IO[A], IO[B]]
Kleisli[A, B any] = R.Reader[A, IO[B]]
Operator[A, B any] = Kleisli[IO[A], B]
Monoid[A any] = M.Monoid[IO[A]]
Semigroup[A any] = S.Semigroup[IO[A]]
)
@@ -121,14 +122,14 @@ func MapTo[A, B any](b B) Operator[A, B] {
}
// MonadChain composes computations in sequence, using the return value of one computation to determine the next computation.
func MonadChain[A, B any](fa IO[A], f func(A) IO[B]) IO[B] {
func MonadChain[A, B any](fa IO[A], f Kleisli[A, B]) IO[B] {
return func() B {
return f(fa())()
}
}
// Chain composes computations in sequence, using the return value of one computation to determine the next computation.
func Chain[A, B any](f func(A) IO[B]) Operator[A, B] {
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return F.Bind2nd(MonadChain[A, B], f)
}
@@ -201,13 +202,13 @@ func Memoize[A any](ma IO[A]) IO[A] {
// MonadChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
// keeping only the result of the first.
func MonadChainFirst[A, B any](fa IO[A], f func(A) IO[B]) IO[A] {
func MonadChainFirst[A, B any](fa IO[A], f Kleisli[A, B]) IO[A] {
return chain.MonadChainFirst(MonadChain[A, A], MonadMap[B, A], fa, f)
}
// ChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
// keeping only the result of the first.
func ChainFirst[A, B any](f func(A) IO[B]) Operator[A, A] {
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return chain.ChainFirst(
Chain[A, A],
Map[B, A],

View File

@@ -32,9 +32,9 @@ import (
// io.ChainFirst(io.Logger[User]()("Fetched user")),
// processUser,
// )
func Logger[A any](loggers ...*log.Logger) func(string) func(A) IO[any] {
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[A, any] {
_, right := L.LoggingCallbacks(loggers...)
return func(prefix string) func(A) IO[any] {
return func(prefix string) Kleisli[A, any] {
return func(a A) IO[any] {
return FromImpure(func() {
right("%s: %v", prefix, a)
@@ -53,7 +53,7 @@ func Logger[A any](loggers ...*log.Logger) func(string) func(A) IO[any] {
// io.ChainFirst(io.Logf[User]("User: %+v")),
// processUser,
// )
func Logf[A any](prefix string) func(A) IO[any] {
func Logf[A any](prefix string) Kleisli[A, any] {
return func(a A) IO[any] {
return FromImpure(func() {
log.Printf(prefix, a)
@@ -72,7 +72,7 @@ func Logf[A any](prefix string) func(A) IO[any] {
// io.ChainFirst(io.Printf[User]("User: %+v\n")),
// processUser,
// )
func Printf[A any](prefix string) func(A) IO[any] {
func Printf[A any](prefix string) Kleisli[A, any] {
return func(a A) IO[any] {
return FromImpure(func() {
fmt.Printf(prefix, a)

View File

@@ -35,7 +35,7 @@ func (o *ioMonad[A, B]) Map(f func(A) B) Operator[A, B] {
return Map(f)
}
func (o *ioMonad[A, B]) Chain(f func(A) IO[B]) Operator[A, B] {
func (o *ioMonad[A, B]) Chain(f Kleisli[A, B]) Operator[A, B] {
return Chain(f)
}

View File

@@ -36,7 +36,7 @@ import (
// return readData(f)
// })
func WithResource[
R, A, ANY any](onCreate IO[R], onRelease func(R) IO[ANY]) func(func(R) IO[A]) IO[A] {
R, A, ANY any](onCreate IO[R], onRelease func(R) IO[ANY]) Kleisli[Kleisli[R, A], A] {
// simply map to implementation of bracket
return function.Bind13of3(Bracket[R, A, ANY])(onCreate, function.Ignore2of2[A](onRelease))
}

View File

@@ -47,7 +47,7 @@ type (
// )
func Retrying[A any](
policy R.RetryPolicy,
action func(R.RetryStatus) IO[A],
action Kleisli[R.RetryStatus, A],
check func(A) bool,
) IO[A] {
// get an implementation for the types

View File

@@ -29,7 +29,7 @@ import (
// fetchUsers := func(id int) io.IO[User] { return fetchUser(id) }
// users := io.MonadTraverseArray([]int{1, 2, 3}, fetchUsers)
// result := users() // []User with all fetched users
func MonadTraverseArray[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
func MonadTraverseArray[A, B any](tas []A, f Kleisli[A, B]) IO[[]B] {
return INTA.MonadTraverse(
Of[[]B],
Map[[]B, func(B) []B],
@@ -50,7 +50,7 @@ func MonadTraverseArray[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
// return fetchUser(id)
// })
// users := fetchUsers([]int{1, 2, 3})
func TraverseArray[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return INTA.Traverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -68,7 +68,7 @@ func TraverseArray[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
// numbered := io.TraverseArrayWithIndex(func(i int, s string) io.IO[string] {
// return io.Of(fmt.Sprintf("%d: %s", i, s))
// })
func TraverseArrayWithIndex[A, B any](f func(int, A) IO[B]) func([]A) IO[[]B] {
func TraverseArrayWithIndex[A, B any](f func(int, A) IO[B]) Kleisli[[]A, []B] {
return INTA.TraverseWithIndex[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -98,7 +98,7 @@ func SequenceArray[A any](tas []IO[A]) IO[[]A] {
// fetchData := func(url string) io.IO[Data] { return fetch(url) }
// urls := map[string]string{"a": "http://a.com", "b": "http://b.com"}
// data := io.MonadTraverseRecord(urls, fetchData)
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) IO[map[K]B] {
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) IO[map[K]B] {
return INTR.MonadTraverse(
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -112,7 +112,7 @@ func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) I
// TraverseRecord returns a function that applies an IO-returning function to each value
// in a map and collects the results. This is the curried version of MonadTraverseRecord.
// Executes in parallel by default.
func TraverseRecord[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[map[K]B] {
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return INTR.Traverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -124,7 +124,7 @@ func TraverseRecord[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[ma
// TraverseRecordWithIndex is like TraverseRecord but the function also receives the key.
// Executes in parallel by default.
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) IO[B]) func(map[K]A) IO[map[K]B] {
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) IO[B]) Kleisli[map[K]A, map[K]B] {
return INTR.TraverseWithIndex[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -153,7 +153,7 @@ func SequenceRecord[K comparable, A any](tas map[K]IO[A]) IO[map[K]A] {
//
// fetchUsers := func(id int) io.IO[User] { return fetchUser(id) }
// users := io.MonadTraverseArraySeq([]int{1, 2, 3}, fetchUsers)
func MonadTraverseArraySeq[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
func MonadTraverseArraySeq[A, B any](tas []A, f Kleisli[A, B]) IO[[]B] {
return INTA.MonadTraverse(
Of[[]B],
Map[[]B, func(B) []B],
@@ -167,7 +167,7 @@ func MonadTraverseArraySeq[A, B any](tas []A, f func(A) IO[B]) IO[[]B] {
// TraverseArraySeq returns a function that applies an IO-returning function to each element
// of an array and collects the results. Executes sequentially (one after another).
// Use this when operations must be performed in order or when parallel execution is not desired.
func TraverseArraySeq[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return INTA.Traverse[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -179,7 +179,7 @@ func TraverseArraySeq[A, B any](f func(A) IO[B]) func([]A) IO[[]B] {
// TraverseArrayWithIndexSeq is like TraverseArraySeq but the function also receives the index.
// Executes sequentially (one after another).
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) IO[B]) func([]A) IO[[]B] {
func TraverseArrayWithIndexSeq[A, B any](f func(int, A) IO[B]) Kleisli[[]A, []B] {
return INTA.TraverseWithIndex[[]A](
Of[[]B],
Map[[]B, func(B) []B],
@@ -197,7 +197,7 @@ func SequenceArraySeq[A any](tas []IO[A]) IO[[]A] {
// MonadTraverseRecordSeq applies an IO-returning function to each value in a map
// and collects the results into an IO of a map. Executes sequentially.
func MonadTraverseRecordSeq[K comparable, A, B any](tas map[K]A, f func(A) IO[B]) IO[map[K]B] {
func MonadTraverseRecordSeq[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) IO[map[K]B] {
return INTR.MonadTraverse(
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -210,7 +210,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](tas map[K]A, f func(A) IO[B]
// TraverseRecordSeq returns a function that applies an IO-returning function to each value
// in a map and collects the results. Executes sequentially (one after another).
func TraverseRecordSeq[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO[map[K]B] {
func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return INTR.Traverse[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],
@@ -223,7 +223,7 @@ func TraverseRecordSeq[K comparable, A, B any](f func(A) IO[B]) func(map[K]A) IO
// TraverseRecordWithIndeSeq is like TraverseRecordSeq but the function also receives the key.
// Executes sequentially (one after another).
// Note: There's a typo in the function name (Inde instead of Index) for backward compatibility.
func TraverseRecordWithIndeSeq[K comparable, A, B any](f func(K, A) IO[B]) func(map[K]A) IO[map[K]B] {
func TraverseRecordWithIndeSeq[K comparable, A, B any](f func(K, A) IO[B]) Kleisli[map[K]A, map[K]B] {
return INTR.TraverseWithIndex[map[K]A](
Of[map[K]B],
Map[map[K]B, func(B) map[K]B],

View File

@@ -19,16 +19,62 @@ import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// 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
// Posts []Post
// }
// result := ioeither.Do[error](State{})
func Do[E, S any](
empty S,
) IOEither[E, S] {
return Of[E](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// 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.
//
// 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
// Posts []Post
// }
//
// result := F.Pipe2(
// ioeither.Do[error](State{}),
// ioeither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser()
// })
// },
// ),
// ioeither.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) ioeither.IOEither[error, []Post] {
// // This can access s.User from the previous step
// return ioeither.TryCatch(func() ([]Post, error) {
// return fetchPostsForUser(s.User.ID)
// })
// },
// ),
// )
func Bind[E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) IOEither[E, T],
@@ -75,7 +121,39 @@ func BindTo[E, S1, T any](
)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
// 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
// Posts []Post
// }
//
// // These operations are independent and can be combined with ApS
// getUser := ioeither.Right[error](User{ID: 1, Name: "Alice"})
// getPosts := ioeither.Right[error]([]Post{{ID: 1, Title: "Hello"}})
//
// result := F.Pipe2(
// ioeither.Do[error](State{}),
// ioeither.ApS(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// getUser,
// ),
// ioeither.ApS(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// getPosts,
// ),
// )
func ApS[E, S1, S2, T any](
setter func(T) func(S1) S2,
fa IOEither[E, T],
@@ -87,3 +165,139 @@ func ApS[E, S1, S2, T any](
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 Config struct {
// Host string
// Port int
// }
//
// portLens := lens.MakeLens(
// func(c Config) int { return c.Port },
// func(c Config, p int) Config { c.Port = p; return c },
// )
//
// result := F.Pipe2(
// ioeither.Of[error](Config{Host: "localhost"}),
// ioeither.ApSL(portLens, ioeither.Of[error](8080)),
// )
func ApSL[E, S, T any](
lens L.Lens[S, T],
fa IOEither[E, T],
) Operator[E, S, S] {
return ApS(lens.Set, fa)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// an IOEither that produces the new value.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// increment := func(v int) ioeither.IOEither[error, int] {
// return ioeither.TryCatch(func() (int, error) {
// if v >= 100 {
// return 0, errors.New("overflow")
// }
// return v + 1, nil
// })
// }
//
// result := F.Pipe1(
// ioeither.Of[error](Counter{Value: 42}),
// ioeither.BindL(valueLens, increment),
// )
func BindL[E, S, T any](
lens L.Lens[S, T],
f func(T) IOEither[E, T],
) Operator[E, S, S] {
return Bind[E, S, S, T](lens.Set, func(s S) IOEither[E, T] {
return f(lens.Get(s))
})
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in IOEither).
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// ioeither.Of[error](Counter{Value: 21}),
// ioeither.LetL(valueLens, double),
// )
func LetL[E, S, T any](
lens L.Lens[S, T],
f func(T) T,
) Operator[E, S, S] {
return Let[E, S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// ioeither.Of[error](Config{Debug: true, Timeout: 30}),
// ioeither.LetToL(debugLens, false),
// )
func LetToL[E, S, T any](
lens L.Lens[S, T],
b T,
) Operator[E, S, S] {
return LetTo[E, S, S, T](lens.Set, b)
}

View File

@@ -22,7 +22,7 @@ import (
)
// TraverseArray transforms an array
func TraverseArray[A, B any](f func(A) IOOption[B]) func([]A) IOOption[[]B] {
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return function.Flow2(
io.TraverseArray(f),
io.Map(option.SequenceArray[B]),
@@ -30,7 +30,7 @@ func TraverseArray[A, B any](f func(A) IOOption[B]) func([]A) IOOption[[]B] {
}
// TraverseArrayWithIndex transforms an array
func TraverseArrayWithIndex[A, B any](f func(int, A) IOOption[B]) func([]A) IOOption[[]B] {
func TraverseArrayWithIndex[A, B any](f func(int, A) IOOption[B]) Kleisli[[]A, []B] {
return function.Flow2(
io.TraverseArrayWithIndex(f),
io.Map(option.SequenceArray[B]),

View File

@@ -19,20 +19,62 @@ import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// 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 {
// Name string
// Age int
// }
// result := iooption.Do(State{})
func Do[S any](
empty S,
) IOOption[S] {
return Of(empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// 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.
//
// 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 {
// Name string
// Age int
// }
//
// result := F.Pipe2(
// iooption.Do(State{}),
// iooption.Bind(
// func(name string) func(State) State {
// return func(s State) State { s.Name = name; return s }
// },
// func(s State) iooption.IOOption[string] {
// return iooption.FromIO(io.Of("Alice"))
// },
// ),
// iooption.Bind(
// func(age int) func(State) State {
// return func(s State) State { s.Age = age; return s }
// },
// func(s State) iooption.IOOption[int] {
// // This can access s.Name from the previous step
// return iooption.FromIO(io.Of(len(s.Name) * 10))
// },
// ),
// )
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) IOOption[T],
) func(IOOption[S1]) IOOption[S2] {
f Kleisli[S1, T],
) Kleisli[IOOption[S1], S2] {
return chain.Bind(
Chain[S1, S2],
Map[T, S2],
@@ -45,7 +87,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(IOOption[S1]) IOOption[S2] {
) Kleisli[IOOption[S1], S2] {
return functor.Let(
Map[S1, S2],
setter,
@@ -57,7 +99,7 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(IOOption[S1]) IOOption[S2] {
) Kleisli[IOOption[S1], S2] {
return functor.LetTo(
Map[S1, S2],
setter,
@@ -68,18 +110,50 @@ func LetTo[S1, S2, T any](
// BindTo initializes a new state [S1] from a value [T]
func BindTo[S1, T any](
setter func(T) S1,
) func(IOOption[T]) IOOption[S1] {
) Kleisli[IOOption[T], S1] {
return chain.BindTo(
Map[T, S1],
setter,
)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
// 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 {
// Name string
// Age int
// }
//
// // These operations are independent and can be combined with ApS
// getName := iooption.Some("Alice")
// getAge := iooption.Some(30)
//
// result := F.Pipe2(
// iooption.Do(State{}),
// iooption.ApS(
// func(name string) func(State) State {
// return func(s State) State { s.Name = name; return s }
// },
// getName,
// ),
// iooption.ApS(
// func(age int) func(State) State {
// return func(s State) State { s.Age = age; return s }
// },
// getAge,
// ),
// )
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa IOOption[T],
) func(IOOption[S1]) IOOption[S2] {
) Kleisli[IOOption[S1], S2] {
return apply.ApS(
Ap[S2, T],
Map[S1, func(T) S2],
@@ -87,3 +161,136 @@ func ApS[S1, S2, T any](
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 {
// Name string
// Age int
// }
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, a int) State { s.Age = a; return s },
// )
//
// result := F.Pipe2(
// iooption.Of(State{Name: "Alice"}),
// iooption.ApSL(ageLens, iooption.Some(30)),
// )
func ApSL[S, T any](
lens L.Lens[S, T],
fa IOOption[T],
) Kleisli[IOOption[S], S] {
return ApS(lens.Set, fa)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// an IOOption that produces the new value.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Increment the counter, but return None if it would exceed 100
// increment := func(v int) iooption.IOOption[int] {
// return iooption.FromIO(io.Of(v + 1))
// }
//
// result := F.Pipe1(
// iooption.Of(Counter{Value: 42}),
// iooption.BindL(valueLens, increment),
// ) // IOOption[Counter{Value: 43}]
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Kleisli[IOOption[S], S] {
return Bind[S, S, T](lens.Set, func(s S) IOOption[T] {
return f(lens.Get(s))
})
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in IOOption).
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Double the counter value
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// iooption.Of(Counter{Value: 21}),
// iooption.LetL(valueLens, double),
// ) // IOOption[Counter{Value: 42}]
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Kleisli[IOOption[S], S] {
return Let[S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// iooption.Of(Config{Debug: true, Timeout: 30}),
// iooption.LetToL(debugLens, false),
// ) // IOOption[Config{Debug: false, Timeout: 30}]
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Kleisli[IOOption[S], S] {
return LetTo[S, S, T](lens.Set, b)
}

View File

@@ -24,7 +24,7 @@ import (
// whether the body action returns and error or not.
func Bracket[A, B, ANY any](
acquire IOOption[A],
use func(A) IOOption[B],
use Kleisli[A, B],
release func(A, Option[B]) IOOption[ANY],
) IOOption[B] {
return G.Bracket[IOOption[A], IOOption[B], IOOption[ANY], Option[B], A, B](

File diff suppressed because it is too large Load Diff

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]) func(func(R) IOOption[A]) IOOption[A] {
R, A, ANY any](onCreate IOOption[R], onRelease func(R) IOOption[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

@@ -23,7 +23,7 @@ import (
// Retrying will retry the actions according to the check policy
func Retrying[A any](
policy R.RetryPolicy,
action func(R.RetryStatus) IOOption[A],
action Kleisli[R.RetryStatus, A],
check func(A) bool,
) IOOption[A] {
// get an implementation for the types

View File

@@ -20,6 +20,7 @@ import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
)
type (
@@ -31,4 +32,7 @@ type (
// IOOption represents a synchronous computation that may fail
// refer to [https://andywhite.xyz/posts/2021-01-27-rte-foundations/#ioeitherlte-agt] for more details
IOOption[A any] = io.IO[Option[A]]
Kleisli[A, B any] = reader.Reader[A, IOOption[B]]
Operator[A, B any] = Kleisli[IOOption[A], B]
)

View File

@@ -19,18 +19,60 @@ import (
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// 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 {
// X int
// Y int
// }
// result := stateless.Do(State{})
func Do[S any](
empty S,
) Iterator[S] {
return G.Do[Iterator[S]](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// 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.
// For iterators, this produces the cartesian product of all values.
//
// 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 {
// X int
// Y int
// }
//
// result := F.Pipe2(
// stateless.Do(State{}),
// stateless.Bind(
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// func(s State) stateless.Iterator[int] {
// return stateless.Of(1, 2, 3)
// },
// ),
// stateless.Bind(
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) stateless.Iterator[int] {
// // This can access s.X from the previous step
// return stateless.Of(s.X * 10, s.X * 20)
// },
// ),
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) Iterator[T],
) func(Iterator[S1]) Iterator[S2] {
f Kleisli[S1, T],
) Kleisli[Iterator[S1], S2] {
return G.Bind[Iterator[S1], Iterator[S2], Iterator[T], S1, S2, T](setter, f)
}
@@ -38,7 +80,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(Iterator[S1]) Iterator[S2] {
) Kleisli[Iterator[S1], S2] {
return G.Let[Iterator[S1], Iterator[S2], S1, S2, T](setter, f)
}
@@ -46,21 +88,53 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(Iterator[S1]) Iterator[S2] {
) Kleisli[Iterator[S1], S2] {
return G.LetTo[Iterator[S1], Iterator[S2], S1, S2, T](setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[S1, T any](
setter func(T) S1,
) func(Iterator[T]) Iterator[S1] {
) Kleisli[Iterator[T], S1] {
return G.BindTo[Iterator[S1], Iterator[T], S1, T](setter)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
// 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 {
// X int
// Y int
// }
//
// // These operations are independent and can be combined with ApS
// xValues := stateless.Of(1, 2, 3)
// yValues := stateless.Of(10, 20)
//
// result := F.Pipe2(
// stateless.Do(State{}),
// stateless.ApS(
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// xValues,
// ),
// stateless.ApS(
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// yValues,
// ),
// ) // Produces all combinations: {1,10}, {1,20}, {2,10}, {2,20}, {3,10}, {3,20}
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Iterator[T],
) func(Iterator[S1]) Iterator[S2] {
) Kleisli[Iterator[S1], S2] {
return G.ApS[Iterator[func(T) S2], Iterator[S1], Iterator[S2], Iterator[T], S1, S2, T](setter, fa)
}

View File

@@ -22,6 +22,6 @@ import (
// Compress returns an [Iterator] that filters elements from a data [Iterator] returning only those that have a corresponding element in selector [Iterator] that evaluates to `true`.
// Stops when either the data or selectors iterator has been exhausted.
func Compress[U any](sel Iterator[bool]) func(Iterator[U]) Iterator[U] {
func Compress[U any](sel Iterator[bool]) Kleisli[Iterator[U], U] {
return G.Compress[Iterator[U], Iterator[bool], Iterator[P.Pair[U, bool]]](sel)
}

View File

@@ -21,6 +21,6 @@ import (
// DropWhile creates an [Iterator] that drops elements from the [Iterator] as long as the predicate is true; afterwards, returns every element.
// Note, the [Iterator] does not produce any output until the predicate first becomes false
func DropWhile[U any](pred func(U) bool) func(Iterator[U]) Iterator[U] {
func DropWhile[U any](pred func(U) bool) Kleisli[Iterator[U], U] {
return G.DropWhile[Iterator[U]](pred)
}

View File

@@ -23,14 +23,56 @@ import (
P "github.com/IBM/fp-go/v2/pair"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// 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 {
// X int
// Y int
// }
// result := generic.Do[Iterator[State]](State{})
func Do[GS ~func() O.Option[P.Pair[GS, S]], 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]
// 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.
// For iterators, this produces the cartesian product where later steps can use values from earlier steps.
//
// 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 {
// X int
// Y int
// }
//
// result := F.Pipe2(
// generic.Do[Iterator[State]](State{}),
// generic.Bind[Iterator[State], Iterator[State], Iterator[int], State, State, int](
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// func(s State) Iterator[int] {
// return generic.Of[Iterator[int]](1, 2, 3)
// },
// ),
// generic.Bind[Iterator[State], Iterator[State], Iterator[int], State, State, int](
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) Iterator[int] {
// // This can access s.X from the previous step
// return generic.Of[Iterator[int]](s.X * 10, s.X * 20)
// },
// ),
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
func Bind[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], GA ~func() O.Option[P.Pair[GA, A]], S1, S2, A any](
setter func(A) func(S1) S2,
f func(S1) GA,
@@ -78,7 +120,39 @@ func BindTo[GS1 ~func() O.Option[P.Pair[GS1, S1]], GA ~func() O.Option[P.Pair[GA
)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
// 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. For iterators, this produces the cartesian product.
//
// Example:
//
// type State struct {
// X int
// Y string
// }
//
// // These operations are independent and can be combined with ApS
// xIter := generic.Of[Iterator[int]](1, 2, 3)
// yIter := generic.Of[Iterator[string]]("a", "b")
//
// result := F.Pipe2(
// generic.Do[Iterator[State]](State{}),
// generic.ApS[Iterator[func(int) State], Iterator[State], Iterator[State], Iterator[int], State, State, int](
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// xIter,
// ),
// generic.ApS[Iterator[func(string) State], Iterator[State], Iterator[State], Iterator[string], State, State, string](
// func(y string) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// yIter,
// ),
// ) // Produces: {1,"a"}, {1,"b"}, {2,"a"}, {2,"b"}, {3,"a"}, {3,"b"}
func ApS[GAS2 ~func() O.Option[P.Pair[GAS2, func(A) S2]], GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], GA ~func() O.Option[P.Pair[GA, A]], S1, S2, A any](
setter func(A) func(S1) S2,
fa GA,

View File

@@ -18,15 +18,11 @@ package stateless
import (
"github.com/IBM/fp-go/v2/iooption"
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
L "github.com/IBM/fp-go/v2/lazy"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
)
// Iterator represents a stateless, pure way to iterate over a sequence
type Iterator[U any] L.Lazy[O.Option[pair.Pair[Iterator[U], U]]]
// Next returns the [Iterator] for the next element in an iterator [pair.Pair]
func Next[U any](m pair.Pair[Iterator[U], U]) Iterator[U] {
return pair.Head(m)
@@ -68,15 +64,15 @@ func MonadMap[U, V any](ma Iterator[U], f func(U) V) Iterator[V] {
}
// Map transforms an [Iterator] of type [U] into an [Iterator] of type [V] via a mapping function
func Map[U, V any](f func(U) V) func(ma Iterator[U]) Iterator[V] {
func Map[U, V any](f func(U) V) Operator[U, V] {
return G.Map[Iterator[V], Iterator[U]](f)
}
func MonadChain[U, V any](ma Iterator[U], f func(U) Iterator[V]) Iterator[V] {
func MonadChain[U, V any](ma Iterator[U], f Kleisli[U, V]) Iterator[V] {
return G.MonadChain[Iterator[V], Iterator[U]](ma, f)
}
func Chain[U, V any](f func(U) Iterator[V]) func(Iterator[U]) Iterator[V] {
func Chain[U, V any](f Kleisli[U, V]) Kleisli[Iterator[U], V] {
return G.Chain[Iterator[V], Iterator[U]](f)
}
@@ -101,17 +97,17 @@ func Replicate[U any](a U) Iterator[U] {
}
// FilterMap filters and transforms the content of an iterator
func FilterMap[U, V any](f func(U) O.Option[V]) func(ma Iterator[U]) Iterator[V] {
func FilterMap[U, V any](f func(U) O.Option[V]) Operator[U, V] {
return G.FilterMap[Iterator[V], Iterator[U]](f)
}
// Filter filters the content of an iterator
func Filter[U any](f func(U) bool) func(ma Iterator[U]) Iterator[U] {
func Filter[U any](f func(U) bool) Operator[U, U] {
return G.Filter[Iterator[U]](f)
}
// Ap is the applicative functor for iterators
func Ap[V, U any](ma Iterator[U]) func(Iterator[func(U) V]) Iterator[V] {
func Ap[V, U any](ma Iterator[U]) Operator[func(U) V, V] {
return G.Ap[Iterator[func(U) V], Iterator[V]](ma)
}
@@ -132,7 +128,7 @@ func Count(start int) Iterator[int] {
}
// FilterChain filters and transforms the content of an iterator
func FilterChain[U, V any](f func(U) O.Option[Iterator[V]]) func(ma Iterator[U]) Iterator[V] {
func FilterChain[U, V any](f func(U) O.Option[Iterator[V]]) Operator[U, V] {
return G.FilterChain[Iterator[Iterator[V]], Iterator[V], Iterator[U]](f)
}
@@ -146,10 +142,10 @@ func Fold[U any](m M.Monoid[U]) func(Iterator[U]) U {
return G.Fold[Iterator[U]](m)
}
func MonadChainFirst[U, V any](ma Iterator[U], f func(U) Iterator[V]) Iterator[U] {
func MonadChainFirst[U, V any](ma Iterator[U], f Kleisli[U, V]) Iterator[U] {
return G.MonadChainFirst[Iterator[V], Iterator[U], U, V](ma, f)
}
func ChainFirst[U, V any](f func(U) Iterator[V]) func(Iterator[U]) Iterator[U] {
func ChainFirst[U, V any](f Kleisli[U, V]) Operator[U, U] {
return G.ChainFirst[Iterator[V], Iterator[U], U, V](f)
}

View File

@@ -15,8 +15,19 @@
package stateless
import "github.com/IBM/fp-go/v2/option"
import (
L "github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
)
type (
Option[A any] = option.Option[A]
// Iterator represents a stateless, pure way to iterate over a sequence
Iterator[U any] L.Lazy[Option[pair.Pair[Iterator[U], U]]]
Kleisli[A, B any] = reader.Reader[A, Iterator[B]]
Operator[A, B any] = Kleisli[Iterator[A], B]
)

View File

@@ -16,21 +16,64 @@
package lazy
import (
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/io"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// 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
// Data Data
// }
// result := lazy.Do(State{})
func Do[S any](
empty S,
) Lazy[S] {
return io.Do(empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// 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.
//
// 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
// Data Data
// }
//
// result := F.Pipe2(
// lazy.Do(State{}),
// lazy.Bind(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) lazy.Lazy[Config] {
// return lazy.MakeLazy(func() Config { return loadConfig() })
// },
// ),
// lazy.Bind(
// func(data Data) func(State) State {
// return func(s State) State { s.Data = data; return s }
// },
// func(s State) lazy.Lazy[Data] {
// // This can access s.Config from the previous step
// return lazy.MakeLazy(func() Data { return loadData(s.Config) })
// },
// ),
// )
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) Lazy[T],
) func(Lazy[S1]) Lazy[S2] {
f Kleisli[S1, T],
) Kleisli[Lazy[S1], S2] {
return io.Bind(setter, f)
}
@@ -38,7 +81,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(Lazy[S1]) Lazy[S2] {
) Kleisli[Lazy[S1], S2] {
return io.Let(setter, f)
}
@@ -46,21 +89,190 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(Lazy[S1]) Lazy[S2] {
) Kleisli[Lazy[S1], S2] {
return io.LetTo(setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[S1, T any](
setter func(T) S1,
) func(Lazy[T]) Lazy[S1] {
) Kleisli[Lazy[T], S1] {
return io.BindTo(setter)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering the context and the value concurrently
// 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
// Data Data
// }
//
// // These operations are independent and can be combined with ApS
// getConfig := lazy.MakeLazy(func() Config { return loadConfig() })
// getData := lazy.MakeLazy(func() Data { return loadData() })
//
// result := F.Pipe2(
// lazy.Do(State{}),
// lazy.ApS(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// getConfig,
// ),
// lazy.ApS(
// func(data Data) func(State) State {
// return func(s State) State { s.Data = data; return s }
// },
// getData,
// ),
// )
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Lazy[T],
) func(Lazy[S1]) Lazy[S2] {
) Kleisli[Lazy[S1], S2] {
return io.ApS(setter, fa)
}
// ApSL is a variant of ApS 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. This allows you to work with nested fields without manually managing
// the update logic.
//
// Example:
//
// type Config struct {
// Host string
// Port int
// }
// type State struct {
// Config Config
// Data string
// }
//
// configLens := L.Prop[State, Config]("Config")
// getConfig := lazy.MakeLazy(func() Config { return Config{Host: "localhost", Port: 8080} })
//
// result := F.Pipe2(
// lazy.Do(State{}),
// lazy.ApSL(configLens, getConfig),
// )
func ApSL[S, T any](
lens L.Lens[S, T],
fa Lazy[T],
) Kleisli[Lazy[S], S] {
return io.ApSL(lens, 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 new computation that produces an updated value.
//
// Example:
//
// type Config struct {
// Host string
// Port int
// }
// type State struct {
// Config Config
// Data string
// }
//
// configLens := L.Prop[State, Config]("Config")
//
// result := F.Pipe2(
// lazy.Do(State{Config: Config{Host: "localhost"}}),
// lazy.BindL(configLens, func(cfg Config) lazy.Lazy[Config] {
// return lazy.MakeLazy(func() Config {
// cfg.Port = 8080
// return cfg
// })
// }),
// )
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Kleisli[Lazy[S], S] {
return io.BindL(lens, 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 monad).
//
// Example:
//
// type Config struct {
// Host string
// Port int
// }
// type State struct {
// Config Config
// Data string
// }
//
// configLens := L.Prop[State, Config]("Config")
//
// result := F.Pipe2(
// lazy.Do(State{Config: Config{Host: "localhost"}}),
// lazy.LetL(configLens, func(cfg Config) Config {
// cfg.Port = 8080
// return cfg
// }),
// )
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Kleisli[Lazy[S], S] {
return io.LetL(lens, 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 Config struct {
// Host string
// Port int
// }
// type State struct {
// Config Config
// Data string
// }
//
// configLens := L.Prop[State, Config]("Config")
// newConfig := Config{Host: "localhost", Port: 8080}
//
// result := F.Pipe2(
// lazy.Do(State{}),
// lazy.LetToL(configLens, newConfig),
// )
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Kleisli[Lazy[S], S] {
return io.LetToL(lens, b)
}

View File

@@ -21,9 +21,6 @@ import (
"github.com/IBM/fp-go/v2/io"
)
// Lazy represents a synchronous computation without side effects
type Lazy[A any] = func() A
func Of[A any](a A) Lazy[A] {
return io.Of(a)
}
@@ -53,17 +50,17 @@ func MonadMapTo[A, B any](fa Lazy[A], b B) Lazy[B] {
return io.MonadMapTo(fa, b)
}
func MapTo[A, B any](b B) func(Lazy[A]) Lazy[B] {
func MapTo[A, B any](b B) Kleisli[Lazy[A], B] {
return io.MapTo[A](b)
}
// MonadChain composes computations in sequence, using the return value of one computation to determine the next computation.
func MonadChain[A, B any](fa Lazy[A], f func(A) Lazy[B]) Lazy[B] {
func MonadChain[A, B any](fa Lazy[A], f Kleisli[A, B]) Lazy[B] {
return io.MonadChain(fa, f)
}
// Chain composes computations in sequence, using the return value of one computation to determine the next computation.
func Chain[A, B any](f func(A) Lazy[B]) func(Lazy[A]) Lazy[B] {
func Chain[A, B any](f Kleisli[A, B]) Kleisli[Lazy[A], B] {
return io.Chain(f)
}
@@ -86,13 +83,13 @@ func Memoize[A any](ma Lazy[A]) Lazy[A] {
// MonadChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
// keeping only the result of the first.
func MonadChainFirst[A, B any](fa Lazy[A], f func(A) Lazy[B]) Lazy[A] {
func MonadChainFirst[A, B any](fa Lazy[A], f Kleisli[A, B]) Lazy[A] {
return io.MonadChainFirst(fa, f)
}
// ChainFirst composes computations in sequence, using the return value of one computation to determine the next computation and
// keeping only the result of the first.
func ChainFirst[A, B any](f func(A) Lazy[B]) func(Lazy[A]) Lazy[A] {
func ChainFirst[A, B any](f Kleisli[A, B]) Kleisli[Lazy[A], A] {
return io.ChainFirst(f)
}
@@ -102,7 +99,7 @@ func MonadApFirst[A, B any](first Lazy[A], second Lazy[B]) Lazy[A] {
}
// ApFirst combines two effectful actions, keeping only the result of the first.
func ApFirst[A, B any](second Lazy[B]) func(Lazy[A]) Lazy[A] {
func ApFirst[A, B any](second Lazy[B]) Kleisli[Lazy[A], A] {
return io.ApFirst[A](second)
}
@@ -112,7 +109,7 @@ func MonadApSecond[A, B any](first Lazy[A], second Lazy[B]) Lazy[B] {
}
// ApSecond combines two effectful actions, keeping only the result of the second.
func ApSecond[A, B any](second Lazy[B]) func(Lazy[A]) Lazy[B] {
func ApSecond[A, B any](second Lazy[B]) Kleisli[Lazy[A], B] {
return io.ApSecond[A](second)
}
@@ -122,7 +119,7 @@ func MonadChainTo[A, B any](fa Lazy[A], fb Lazy[B]) Lazy[B] {
}
// ChainTo composes computations in sequence, ignoring the return value of the first computation
func ChainTo[A, B any](fb Lazy[B]) func(Lazy[A]) Lazy[B] {
func ChainTo[A, B any](fb Lazy[B]) Kleisli[Lazy[A], B] {
return io.ChainTo[A](fb)
}

View File

@@ -27,7 +27,7 @@ import (
// check - checks if the result of the action needs to be retried
func Retrying[A any](
policy R.RetryPolicy,
action func(R.RetryStatus) Lazy[A],
action Kleisli[R.RetryStatus, A],
check func(A) bool,
) Lazy[A] {
return io.Retrying(policy, action, check)

View File

@@ -17,19 +17,19 @@ package lazy
import "github.com/IBM/fp-go/v2/io"
func MonadTraverseArray[A, B any](tas []A, f func(A) Lazy[B]) Lazy[[]B] {
func MonadTraverseArray[A, B any](tas []A, f Kleisli[A, B]) Lazy[[]B] {
return io.MonadTraverseArray(tas, f)
}
// TraverseArray applies a function returning an [IO] to all elements in an array and the
// transforms this into an [IO] of that array
func TraverseArray[A, B any](f func(A) Lazy[B]) func([]A) Lazy[[]B] {
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return io.TraverseArray(f)
}
// TraverseArrayWithIndex applies a function returning an [IO] to all elements in an array and the
// transforms this into an [IO] of that array
func TraverseArrayWithIndex[A, B any](f func(int, A) Lazy[B]) func([]A) Lazy[[]B] {
func TraverseArrayWithIndex[A, B any](f func(int, A) Lazy[B]) Kleisli[[]A, []B] {
return io.TraverseArrayWithIndex(f)
}
@@ -38,19 +38,19 @@ func SequenceArray[A any](tas []Lazy[A]) Lazy[[]A] {
return io.SequenceArray(tas)
}
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f func(A) Lazy[B]) Lazy[map[K]B] {
func MonadTraverseRecord[K comparable, A, B any](tas map[K]A, f Kleisli[A, B]) Lazy[map[K]B] {
return io.MonadTraverseRecord(tas, f)
}
// TraverseRecord applies a function returning an [IO] to all elements in a record and the
// transforms this into an [IO] of that record
func TraverseRecord[K comparable, A, B any](f func(A) Lazy[B]) func(map[K]A) Lazy[map[K]B] {
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return io.TraverseRecord[K](f)
}
// TraverseRecord applies a function returning an [IO] to all elements in a record and the
// transforms this into an [IO] of that record
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) Lazy[B]) func(map[K]A) Lazy[map[K]B] {
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) Lazy[B]) Kleisli[map[K]A, map[K]B] {
return io.TraverseRecordWithIndex[K](f)
}

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

@@ -0,0 +1,9 @@
package lazy
type (
// Lazy represents a synchronous computation without side effects
Lazy[A any] = func() A
Kleisli[A, B any] = func(A) Lazy[B]
Operator[A, B any] = Kleisli[Lazy[A], B]
)

5
v2/logging/coverage.out Normal file
View File

@@ -0,0 +1,5 @@
mode: set
github.com/IBM/fp-go/v2/logging/logger.go:54.92,55.22 1 1
github.com/IBM/fp-go/v2/logging/logger.go:56.9,58.32 2 1
github.com/IBM/fp-go/v2/logging/logger.go:59.9,61.34 2 1
github.com/IBM/fp-go/v2/logging/logger.go:62.10,63.46 1 1

View File

@@ -13,12 +13,44 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package logging provides utilities for creating logging callbacks from standard log.Logger instances.
// It offers a convenient way to configure logging for functional programming patterns where separate
// loggers for success and error cases are needed.
package logging
import (
"log"
)
// LoggingCallbacks creates a pair of logging callback functions from the provided loggers.
// It returns two functions that can be used for logging messages, typically one for success
// cases and one for error cases.
//
// The behavior depends on the number of loggers provided:
// - 0 loggers: Returns two callbacks using log.Default() for both success and error logging
// - 1 logger: Returns two callbacks both using the provided logger
// - 2+ loggers: Returns callbacks using the first logger for success and second for errors
//
// Parameters:
// - loggers: Variable number of *log.Logger instances (0, 1, or more)
//
// Returns:
// - First function: Callback for success/info logging (signature: func(string, ...any))
// - Second function: Callback for error logging (signature: func(string, ...any))
//
// Example:
//
// // Using default logger for both
// infoLog, errLog := LoggingCallbacks()
//
// // Using custom logger for both
// customLogger := log.New(os.Stdout, "APP: ", log.LstdFlags)
// infoLog, errLog := LoggingCallbacks(customLogger)
//
// // Using separate loggers for info and errors
// infoLogger := log.New(os.Stdout, "INFO: ", log.LstdFlags)
// errorLogger := log.New(os.Stderr, "ERROR: ", log.LstdFlags)
// infoLog, errLog := LoggingCallbacks(infoLogger, errorLogger)
func LoggingCallbacks(loggers ...*log.Logger) (func(string, ...any), func(string, ...any)) {
switch len(loggers) {
case 0:

288
v2/logging/logger_test.go Normal file
View File

@@ -0,0 +1,288 @@
// 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 logging
import (
"bytes"
"log"
"strings"
"testing"
)
// TestLoggingCallbacks_NoLoggers tests the case when no loggers are provided.
// It should return two callbacks using the default logger.
func TestLoggingCallbacks_NoLoggers(t *testing.T) {
infoLog, errLog := LoggingCallbacks()
if infoLog == nil {
t.Error("Expected infoLog to be non-nil")
}
if errLog == nil {
t.Error("Expected errLog to be non-nil")
}
// Verify both callbacks work
var buf bytes.Buffer
log.SetOutput(&buf)
defer log.SetOutput(nil)
infoLog("test info: %s", "message")
if !strings.Contains(buf.String(), "test info: message") {
t.Errorf("Expected log to contain 'test info: message', got: %s", buf.String())
}
buf.Reset()
errLog("test error: %s", "message")
if !strings.Contains(buf.String(), "test error: message") {
t.Errorf("Expected log to contain 'test error: message', got: %s", buf.String())
}
}
// TestLoggingCallbacks_OneLogger tests the case when one logger is provided.
// Both callbacks should use the same logger.
func TestLoggingCallbacks_OneLogger(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "TEST: ", 0)
infoLog, errLog := LoggingCallbacks(logger)
if infoLog == nil {
t.Error("Expected infoLog to be non-nil")
}
if errLog == nil {
t.Error("Expected errLog to be non-nil")
}
// Test info callback
infoLog("info message: %d", 42)
output := buf.String()
if !strings.Contains(output, "TEST: info message: 42") {
t.Errorf("Expected log to contain 'TEST: info message: 42', got: %s", output)
}
// Test error callback uses same logger
buf.Reset()
errLog("error message: %s", "failed")
output = buf.String()
if !strings.Contains(output, "TEST: error message: failed") {
t.Errorf("Expected log to contain 'TEST: error message: failed', got: %s", output)
}
}
// TestLoggingCallbacks_TwoLoggers tests the case when two loggers are provided.
// First callback should use first logger, second callback should use second logger.
func TestLoggingCallbacks_TwoLoggers(t *testing.T) {
var infoBuf, errBuf bytes.Buffer
infoLogger := log.New(&infoBuf, "INFO: ", 0)
errorLogger := log.New(&errBuf, "ERROR: ", 0)
infoLog, errLog := LoggingCallbacks(infoLogger, errorLogger)
if infoLog == nil {
t.Error("Expected infoLog to be non-nil")
}
if errLog == nil {
t.Error("Expected errLog to be non-nil")
}
// Test info callback uses first logger
infoLog("success: %s", "operation completed")
infoOutput := infoBuf.String()
if !strings.Contains(infoOutput, "INFO: success: operation completed") {
t.Errorf("Expected info log to contain 'INFO: success: operation completed', got: %s", infoOutput)
}
if errBuf.Len() != 0 {
t.Errorf("Expected error buffer to be empty, got: %s", errBuf.String())
}
// Test error callback uses second logger
errLog("failure: %s", "operation failed")
errOutput := errBuf.String()
if !strings.Contains(errOutput, "ERROR: failure: operation failed") {
t.Errorf("Expected error log to contain 'ERROR: failure: operation failed', got: %s", errOutput)
}
}
// TestLoggingCallbacks_MultipleLoggers tests the case when more than two loggers are provided.
// Should use first two loggers and ignore the rest.
func TestLoggingCallbacks_MultipleLoggers(t *testing.T) {
var buf1, buf2, buf3 bytes.Buffer
logger1 := log.New(&buf1, "LOG1: ", 0)
logger2 := log.New(&buf2, "LOG2: ", 0)
logger3 := log.New(&buf3, "LOG3: ", 0)
infoLog, errLog := LoggingCallbacks(logger1, logger2, logger3)
if infoLog == nil {
t.Error("Expected infoLog to be non-nil")
}
if errLog == nil {
t.Error("Expected errLog to be non-nil")
}
// Test that first logger is used for info
infoLog("message 1")
if !strings.Contains(buf1.String(), "LOG1: message 1") {
t.Errorf("Expected first logger to be used, got: %s", buf1.String())
}
// Test that second logger is used for error
errLog("message 2")
if !strings.Contains(buf2.String(), "LOG2: message 2") {
t.Errorf("Expected second logger to be used, got: %s", buf2.String())
}
// Test that third logger is not used
if buf3.Len() != 0 {
t.Errorf("Expected third logger to not be used, got: %s", buf3.String())
}
}
// TestLoggingCallbacks_FormattingWithMultipleArgs tests that formatting works correctly
// with multiple arguments.
func TestLoggingCallbacks_FormattingWithMultipleArgs(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
infoLog, _ := LoggingCallbacks(logger)
infoLog("test %s %d %v", "string", 123, true)
output := buf.String()
if !strings.Contains(output, "test string 123 true") {
t.Errorf("Expected formatted output 'test string 123 true', got: %s", output)
}
}
// TestLoggingCallbacks_NoFormatting tests logging without format specifiers.
func TestLoggingCallbacks_NoFormatting(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "PREFIX: ", 0)
infoLog, _ := LoggingCallbacks(logger)
infoLog("simple message")
output := buf.String()
if !strings.Contains(output, "PREFIX: simple message") {
t.Errorf("Expected 'PREFIX: simple message', got: %s", output)
}
}
// TestLoggingCallbacks_EmptyMessage tests logging with empty message.
func TestLoggingCallbacks_EmptyMessage(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
infoLog, _ := LoggingCallbacks(logger)
infoLog("")
output := buf.String()
// Should still produce output (newline at minimum)
if len(output) == 0 {
t.Error("Expected some output even with empty message")
}
}
// TestLoggingCallbacks_NilLogger tests behavior when nil logger is passed.
// This tests edge case handling.
func TestLoggingCallbacks_NilLogger(t *testing.T) {
// This should not panic
defer func() {
if r := recover(); r != nil {
t.Errorf("LoggingCallbacks panicked with nil logger: %v", r)
}
}()
infoLog, errLog := LoggingCallbacks(nil)
// The callbacks should still be created
if infoLog == nil {
t.Error("Expected infoLog to be non-nil even with nil logger")
}
if errLog == nil {
t.Error("Expected errLog to be non-nil even with nil logger")
}
}
// TestLoggingCallbacks_ConsecutiveCalls tests that callbacks can be called multiple times.
func TestLoggingCallbacks_ConsecutiveCalls(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
infoLog, errLog := LoggingCallbacks(logger)
// Multiple calls to info
infoLog("call 1")
infoLog("call 2")
infoLog("call 3")
output := buf.String()
if !strings.Contains(output, "call 1") || !strings.Contains(output, "call 2") || !strings.Contains(output, "call 3") {
t.Errorf("Expected all three calls to be logged, got: %s", output)
}
buf.Reset()
// Multiple calls to error
errLog("error 1")
errLog("error 2")
output = buf.String()
if !strings.Contains(output, "error 1") || !strings.Contains(output, "error 2") {
t.Errorf("Expected both error calls to be logged, got: %s", output)
}
}
// BenchmarkLoggingCallbacks_NoLoggers benchmarks the no-logger case.
func BenchmarkLoggingCallbacks_NoLoggers(b *testing.B) {
for i := 0; i < b.N; i++ {
LoggingCallbacks()
}
}
// BenchmarkLoggingCallbacks_OneLogger benchmarks the single-logger case.
func BenchmarkLoggingCallbacks_OneLogger(b *testing.B) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
LoggingCallbacks(logger)
}
}
// BenchmarkLoggingCallbacks_TwoLoggers benchmarks the two-logger case.
func BenchmarkLoggingCallbacks_TwoLoggers(b *testing.B) {
var buf1, buf2 bytes.Buffer
logger1 := log.New(&buf1, "", 0)
logger2 := log.New(&buf2, "", 0)
b.ResetTimer()
for i := 0; i < b.N; i++ {
LoggingCallbacks(logger1, logger2)
}
}
// BenchmarkLoggingCallbacks_Logging benchmarks actual logging operations.
func BenchmarkLoggingCallbacks_Logging(b *testing.B) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
infoLog, _ := LoggingCallbacks(logger)
b.ResetTimer()
for i := 0; i < b.N; i++ {
infoLog("benchmark message %d", i)
}
}

View File

@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Iso is an optic which converts elements of type `S` into elements of type `A` without loss.
// Package iso provides isomorphisms - bidirectional transformations between types without loss of information.
package iso
import (
@@ -21,21 +21,127 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
// Iso represents an isomorphism between types S and A.
// An isomorphism is a bidirectional transformation that converts between two types
// without any loss of information. It consists of two functions that are inverses
// of each other.
//
// Type Parameters:
// - S: The source type
// - A: The target type
//
// Fields:
// - Get: Converts from S to A
// - ReverseGet: Converts from A back to S
//
// Laws:
// An Iso must satisfy the round-trip laws:
// 1. ReverseGet(Get(s)) == s for all s: S
// 2. Get(ReverseGet(a)) == a for all a: A
//
// Example:
//
// // Isomorphism between Celsius and Fahrenheit
// tempIso := Iso[float64, float64]{
// Get: func(c float64) float64 { return c*9/5 + 32 },
// ReverseGet: func(f float64) float64 { return (f - 32) * 5 / 9 },
// }
//
// fahrenheit := tempIso.Get(20.0) // 68.0
// celsius := tempIso.ReverseGet(68.0) // 20.0
type Iso[S, A any] struct {
Get func(s S) A
// Get converts a value from the source type S to the target type A.
Get func(s S) A
// ReverseGet converts a value from the target type A back to the source type S.
// This is the inverse of Get.
ReverseGet func(a A) S
}
// MakeIso constructs an isomorphism from two functions.
// The functions should be inverses of each other to satisfy the isomorphism laws.
//
// Type Parameters:
// - S: The source type
// - A: The target type
//
// Parameters:
// - get: Function to convert from S to A
// - reverse: Function to convert from A to S (inverse of get)
//
// Returns:
// - An Iso[S, A] that uses the provided functions
//
// Example:
//
// // Create an isomorphism between string and []byte
// stringBytesIso := MakeIso(
// func(s string) []byte { return []byte(s) },
// func(b []byte) string { return string(b) },
// )
//
// bytes := stringBytesIso.Get("hello") // []byte("hello")
// str := stringBytesIso.ReverseGet([]byte("hi")) // "hi"
func MakeIso[S, A any](get func(S) A, reverse func(A) S) Iso[S, A] {
return Iso[S, A]{Get: get, ReverseGet: reverse}
}
// Id returns an iso implementing the identity operation
// Id returns an identity isomorphism that performs no transformation.
// Both Get and ReverseGet are the identity function.
//
// Type Parameters:
// - S: The type for both source and target
//
// Returns:
// - An Iso[S, S] where Get and ReverseGet are both identity functions
//
// Example:
//
// idIso := Id[int]()
// value := idIso.Get(42) // 42
// same := idIso.ReverseGet(42) // 42
//
// Use cases:
// - As a starting point for isomorphism composition
// - When you need an isomorphism but don't want to transform the value
// - In generic code that requires an isomorphism parameter
func Id[S any]() Iso[S, S] {
return MakeIso(F.Identity[S], F.Identity[S])
}
// Compose combines an ISO with another ISO
// Compose combines two isomorphisms to create a new isomorphism.
// Given Iso[S, A] and Iso[A, B], creates Iso[S, B].
// The resulting isomorphism first applies the outer iso (S → A),
// then the inner iso (A → B).
//
// Type Parameters:
// - S: The outermost source type
// - A: The intermediate type
// - B: The innermost target type
//
// Parameters:
// - ab: The inner isomorphism (A → B)
//
// Returns:
// - A function that takes the outer isomorphism (S → A) and returns the composed isomorphism (S → B)
//
// Example:
//
// metersToKm := MakeIso(
// func(m float64) float64 { return m / 1000 },
// func(km float64) float64 { return km * 1000 },
// )
//
// kmToMiles := MakeIso(
// func(km float64) float64 { return km * 0.621371 },
// func(mi float64) float64 { return mi / 0.621371 },
// )
//
// // Compose: meters → kilometers → miles
// metersToMiles := F.Pipe1(metersToKm, Compose[float64](kmToMiles))
//
// miles := metersToMiles.Get(5000) // ~3.11 miles
// meters := metersToMiles.ReverseGet(3.11) // ~5000 meters
func Compose[S, A, B any](ab Iso[A, B]) func(Iso[S, A]) Iso[S, B] {
return func(sa Iso[S, A]) Iso[S, B] {
return MakeIso(
@@ -45,7 +151,31 @@ func Compose[S, A, B any](ab Iso[A, B]) func(Iso[S, A]) Iso[S, B] {
}
}
// Reverse changes the order of parameters for an iso
// Reverse swaps the direction of an isomorphism.
// Given Iso[S, A], creates Iso[A, S] where Get and ReverseGet are swapped.
//
// Type Parameters:
// - S: The original source type (becomes target)
// - A: The original target type (becomes source)
//
// Parameters:
// - sa: The isomorphism to reverse
//
// Returns:
// - An Iso[A, S] with Get and ReverseGet swapped
//
// Example:
//
// celsiusToFahrenheit := MakeIso(
// func(c float64) float64 { return c*9/5 + 32 },
// func(f float64) float64 { return (f - 32) * 5 / 9 },
// )
//
// // Reverse to get Fahrenheit to Celsius
// fahrenheitToCelsius := Reverse(celsiusToFahrenheit)
//
// celsius := fahrenheitToCelsius.Get(68.0) // 20.0
// fahrenheit := fahrenheitToCelsius.ReverseGet(20.0) // 68.0
func Reverse[S, A any](sa Iso[S, A]) Iso[A, S] {
return MakeIso(
sa.ReverseGet,
@@ -53,6 +183,8 @@ func Reverse[S, A any](sa Iso[S, A]) Iso[A, S] {
)
}
// modify is an internal helper that applies a transformation function through an isomorphism.
// It converts S to A, applies the function, then converts back to S.
func modify[FCT ~func(A) A, S, A any](f FCT, sa Iso[S, A], s S) S {
return F.Pipe3(
s,
@@ -62,35 +194,166 @@ func modify[FCT ~func(A) A, S, A any](f FCT, sa Iso[S, A], s S) S {
)
}
// Modify applies a transformation
// Modify creates a function that applies a transformation in the target space.
// It converts the source value to the target type, applies the transformation,
// then converts back to the source type.
//
// Type Parameters:
// - S: The source type
// - FCT: The transformation function type (A → A)
// - A: The target type
//
// Parameters:
// - f: The transformation function to apply in the target space
//
// Returns:
// - A function that takes an Iso[S, A] and returns an endomorphism (S → S)
//
// Example:
//
// type Meters float64
// type Kilometers float64
//
// mToKm := MakeIso(
// func(m Meters) Kilometers { return Kilometers(m / 1000) },
// func(km Kilometers) Meters { return Meters(km * 1000) },
// )
//
// // Double the distance in kilometers, result in meters
// doubled := Modify[Meters](func(km Kilometers) Kilometers {
// return km * 2
// })(mToKm)(Meters(5000))
// // Result: Meters(10000)
func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Iso[S, A]) EM.Endomorphism[S] {
return EM.Curry3(modify[FCT, S, A])(f)
return F.Curry3(modify[FCT, S, A])(f)
}
// Wrap wraps the value
// Unwrap extracts the target value from a source value using an isomorphism.
// This is a convenience function that applies the Get function of the isomorphism.
//
// Type Parameters:
// - A: The target type to extract
// - S: The source type
//
// Parameters:
// - s: The source value to unwrap
//
// Returns:
// - A function that takes an Iso[S, A] and returns the unwrapped value of type A
//
// Example:
//
// type UserId int
//
// userIdIso := MakeIso(
// func(id UserId) int { return int(id) },
// func(i int) UserId { return UserId(i) },
// )
//
// rawId := Unwrap[int](UserId(42))(userIdIso) // 42
//
// Note: This function is also available as To for semantic clarity.
func Unwrap[A, S any](s S) func(Iso[S, A]) A {
return func(sa Iso[S, A]) A {
return sa.Get(s)
}
}
// Unwrap unwraps the value
// Wrap wraps a target value into a source value using an isomorphism.
// This is a convenience function that applies the ReverseGet function of the isomorphism.
//
// Type Parameters:
// - S: The source type to wrap into
// - A: The target type
//
// Parameters:
// - a: The target value to wrap
//
// Returns:
// - A function that takes an Iso[S, A] and returns the wrapped value of type S
//
// Example:
//
// type UserId int
//
// userIdIso := MakeIso(
// func(id UserId) int { return int(id) },
// func(i int) UserId { return UserId(i) },
// )
//
// userId := Wrap[UserId](42)(userIdIso) // UserId(42)
//
// Note: This function is also available as From for semantic clarity.
func Wrap[S, A any](a A) func(Iso[S, A]) S {
return func(sa Iso[S, A]) S {
return sa.ReverseGet(a)
}
}
// From wraps the value
// To extracts the target value from a source value using an isomorphism.
// This is an alias for Unwrap, provided for semantic clarity when the
// direction of conversion is important.
//
// Type Parameters:
// - A: The target type to convert to
// - S: The source type
//
// Parameters:
// - s: The source value to convert
//
// Returns:
// - A function that takes an Iso[S, A] and returns the converted value of type A
//
// Example:
//
// type Email string
// type ValidatedEmail struct{ value Email }
//
// emailIso := MakeIso(
// func(ve ValidatedEmail) Email { return ve.value },
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
// )
//
// // Convert to Email
// email := To[Email](ValidatedEmail{value: "user@example.com"})(emailIso)
// // "user@example.com"
func To[A, S any](s S) func(Iso[S, A]) A {
return Unwrap[A, S](s)
}
// To unwraps the value
// From wraps a target value into a source value using an isomorphism.
// This is an alias for Wrap, provided for semantic clarity when the
// direction of conversion is important.
//
// Type Parameters:
// - S: The source type to convert from
// - A: The target type
//
// Parameters:
// - a: The target value to convert
//
// Returns:
// - A function that takes an Iso[S, A] and returns the converted value of type S
//
// Example:
//
// type Email string
// type ValidatedEmail struct{ value Email }
//
// emailIso := MakeIso(
// func(ve ValidatedEmail) Email { return ve.value },
// func(e Email) ValidatedEmail { return ValidatedEmail{value: e} },
// )
//
// // Convert from Email
// validated := From[ValidatedEmail](Email("admin@example.com"))(emailIso)
// // ValidatedEmail{value: "admin@example.com"}
func From[S, A any](a A) func(Iso[S, A]) S {
return Wrap[S](a)
}
// imap is an internal helper that bidirectionally maps an isomorphism.
// It transforms both directions of the isomorphism using the provided functions.
func imap[S, A, B any](sa Iso[S, A], ab func(A) B, ba func(B) A) Iso[S, B] {
return MakeIso(
F.Flow2(sa.Get, ab),
@@ -98,7 +361,43 @@ func imap[S, A, B any](sa Iso[S, A], ab func(A) B, ba func(B) A) Iso[S, B] {
)
}
// IMap implements a bidirectional mapping of the transform
// IMap bidirectionally maps the target type of an isomorphism.
// Given Iso[S, A] and functions A → B and B → A, creates Iso[S, B].
// This allows you to transform both directions of an isomorphism.
//
// Type Parameters:
// - S: The source type (unchanged)
// - A: The original target type
// - B: The new target type
//
// Parameters:
// - ab: Function to map from A to B
// - ba: Function to map from B to A (inverse of ab)
//
// Returns:
// - A function that transforms Iso[S, A] to Iso[S, B]
//
// Example:
//
// type Celsius float64
// type Kelvin float64
//
// celsiusIso := Id[Celsius]()
//
// // Create isomorphism to Kelvin
// celsiusToKelvin := F.Pipe1(
// celsiusIso,
// IMap(
// func(c Celsius) Kelvin { return Kelvin(c + 273.15) },
// func(k Kelvin) Celsius { return Celsius(k - 273.15) },
// ),
// )
//
// kelvin := celsiusToKelvin.Get(Celsius(20)) // 293.15 K
// celsius := celsiusToKelvin.ReverseGet(Kelvin(293.15)) // 20°C
//
// Note: The functions ab and ba must be inverses of each other to maintain
// the isomorphism laws.
func IMap[S, A, B any](ab func(A) B, ba func(B) A) func(Iso[S, A]) Iso[S, B] {
return func(sa Iso[S, A]) Iso[S, B] {
return imap(sa, ab, ba)

187
v2/optics/iso/isos.go Normal file
View File

@@ -0,0 +1,187 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package iso
import (
"strings"
"time"
B "github.com/IBM/fp-go/v2/bytes"
F "github.com/IBM/fp-go/v2/function"
S "github.com/IBM/fp-go/v2/string"
)
// UTF8String creates an isomorphism between byte slices and UTF-8 strings.
// This isomorphism provides bidirectional conversion between []byte and string,
// treating the byte slice as UTF-8 encoded text.
//
// Returns:
// - An Iso[[]byte, string] where:
// - Get: Converts []byte to string using UTF-8 encoding
// - ReverseGet: Converts string to []byte using UTF-8 encoding
//
// Behavior:
// - Get direction: Interprets the byte slice as UTF-8 and returns the corresponding string
// - ReverseGet direction: Encodes the string as UTF-8 bytes
//
// Example:
//
// iso := UTF8String()
//
// // Convert bytes to string
// str := iso.Get([]byte("hello")) // "hello"
//
// // Convert string to bytes
// bytes := iso.ReverseGet("world") // []byte("world")
//
// // Round-trip conversion
// original := []byte("test")
// result := iso.ReverseGet(iso.Get(original)) // []byte("test")
//
// Use cases:
// - Converting between string and byte representations
// - Working with APIs that use different text representations
// - File I/O operations where you need to switch between strings and bytes
// - Network protocols that work with byte streams
//
// Note: This isomorphism assumes valid UTF-8 encoding. Invalid UTF-8 sequences
// in the byte slice will be handled according to Go's string conversion rules
// (typically replaced with the Unicode replacement character U+FFFD).
func UTF8String() Iso[[]byte, string] {
return MakeIso(B.ToString, S.ToBytes)
}
// lines creates an isomorphism between a slice of strings and a single string
// with lines separated by the specified separator.
// This is an internal helper function used by Lines.
//
// Parameters:
// - sep: The separator string to use for joining/splitting lines
//
// Returns:
// - An Iso[[]string, string] that joins/splits strings using the separator
//
// Behavior:
// - Get direction: Joins the string slice into a single string with separators
// - ReverseGet direction: Splits the string by the separator into a slice
func lines(sep string) Iso[[]string, string] {
return MakeIso(S.Join(sep), F.Bind2nd(strings.Split, sep))
}
// Lines creates an isomorphism between a slice of strings and a single string
// with newline-separated lines.
// This is useful for working with multi-line text where you need to convert
// between a single string and individual lines.
//
// Returns:
// - An Iso[[]string, string] where:
// - Get: Joins string slice with newline characters ("\n")
// - ReverseGet: Splits string by newline characters into a slice
//
// Behavior:
// - Get direction: Joins each string in the slice with "\n" separator
// - ReverseGet direction: Splits the string at each "\n" into a slice
//
// Example:
//
// iso := Lines()
//
// // Convert lines to single string
// lines := []string{"line1", "line2", "line3"}
// text := iso.Get(lines) // "line1\nline2\nline3"
//
// // Convert string to lines
// text := "hello\nworld"
// lines := iso.ReverseGet(text) // []string{"hello", "world"}
//
// // Round-trip conversion
// original := []string{"a", "b", "c"}
// result := iso.ReverseGet(iso.Get(original)) // []string{"a", "b", "c"}
//
// Use cases:
// - Processing multi-line text files
// - Converting between text editor representations (array of lines vs single string)
// - Working with configuration files that have line-based structure
// - Parsing or generating multi-line output
//
// Note: Empty strings in the slice will result in consecutive newlines in the output.
// Splitting a string with trailing newlines will include an empty string at the end.
//
// Example with edge cases:
//
// iso := Lines()
// lines := []string{"a", "", "b"}
// text := iso.Get(lines) // "a\n\nb"
// result := iso.ReverseGet(text) // []string{"a", "", "b"}
//
// text := "a\nb\n"
// lines := iso.ReverseGet(text) // []string{"a", "b", ""}
func Lines() Iso[[]string, string] {
return lines("\n")
}
// UnixMilli creates an isomorphism between Unix millisecond timestamps and time.Time values.
// This isomorphism provides bidirectional conversion between int64 milliseconds since
// the Unix epoch (January 1, 1970 UTC) and Go's time.Time type.
//
// Returns:
// - An Iso[int64, time.Time] where:
// - Get: Converts Unix milliseconds (int64) to time.Time
// - ReverseGet: Converts time.Time to Unix milliseconds (int64)
//
// Behavior:
// - Get direction: Creates a time.Time from milliseconds since Unix epoch
// - ReverseGet direction: Extracts milliseconds since Unix epoch from time.Time
//
// Example:
//
// iso := UnixMilli()
//
// // Convert milliseconds to time.Time
// millis := int64(1609459200000) // 2021-01-01 00:00:00 UTC
// t := iso.Get(millis)
//
// // Convert time.Time to milliseconds
// now := time.Now()
// millis := iso.ReverseGet(now)
//
// // Round-trip conversion
// original := int64(1234567890000)
// result := iso.ReverseGet(iso.Get(original)) // 1234567890000
//
// Use cases:
// - Working with APIs that use Unix millisecond timestamps (e.g., JavaScript Date.now())
// - Database storage where timestamps are stored as integers
// - JSON serialization/deserialization of timestamps
// - Converting between different time representations in distributed systems
//
// Precision notes:
// - Millisecond precision is maintained in both directions
// - Sub-millisecond precision in time.Time is lost when converting to int64
// - The conversion is timezone-aware (time.Time includes location information)
//
// Example with precision:
//
// iso := UnixMilli()
// t := time.Date(2021, 1, 1, 12, 30, 45, 123456789, time.UTC)
// millis := iso.ReverseGet(t) // Nanoseconds are truncated to milliseconds
// restored := iso.Get(millis) // Nanoseconds will be 123000000
//
// Note: This isomorphism uses UTC for the time.Time values. If you need to preserve
// timezone information, consider storing it separately or using a different representation.
func UnixMilli() Iso[int64, time.Time] {
return MakeIso(time.UnixMilli, time.Time.UnixMilli)
}

432
v2/optics/iso/isos_test.go Normal file
View File

@@ -0,0 +1,432 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package iso
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// TestUTF8String tests the UTF8String isomorphism
func TestUTF8String(t *testing.T) {
iso := UTF8String()
t.Run("Get converts bytes to string", func(t *testing.T) {
bytes := []byte("hello world")
result := iso.Get(bytes)
assert.Equal(t, "hello world", result)
})
t.Run("Get handles empty bytes", func(t *testing.T) {
bytes := []byte{}
result := iso.Get(bytes)
assert.Equal(t, "", result)
})
t.Run("Get handles UTF-8 characters", func(t *testing.T) {
bytes := []byte("Hello 世界 🌍")
result := iso.Get(bytes)
assert.Equal(t, "Hello 世界 🌍", result)
})
t.Run("ReverseGet converts string to bytes", func(t *testing.T) {
str := "hello world"
result := iso.ReverseGet(str)
assert.Equal(t, []byte("hello world"), result)
})
t.Run("ReverseGet handles empty string", func(t *testing.T) {
str := ""
result := iso.ReverseGet(str)
assert.Equal(t, []byte{}, result)
})
t.Run("ReverseGet handles UTF-8 characters", func(t *testing.T) {
str := "Hello 世界 🌍"
result := iso.ReverseGet(str)
assert.Equal(t, []byte("Hello 世界 🌍"), result)
})
t.Run("Round-trip bytes to string to bytes", func(t *testing.T) {
original := []byte("test data")
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
})
t.Run("Round-trip string to bytes to string", func(t *testing.T) {
original := "test string"
result := iso.Get(iso.ReverseGet(original))
assert.Equal(t, original, result)
})
t.Run("Handles special characters", func(t *testing.T) {
str := "line1\nline2\ttab\r\nwindows"
bytes := iso.ReverseGet(str)
result := iso.Get(bytes)
assert.Equal(t, str, result)
})
t.Run("Handles binary-like data", func(t *testing.T) {
bytes := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f} // "Hello"
result := iso.Get(bytes)
assert.Equal(t, "Hello", result)
})
}
// TestLines tests the Lines isomorphism
func TestLines(t *testing.T) {
iso := Lines()
t.Run("Get joins lines with newline", func(t *testing.T) {
lines := []string{"line1", "line2", "line3"}
result := iso.Get(lines)
assert.Equal(t, "line1\nline2\nline3", result)
})
t.Run("Get handles single line", func(t *testing.T) {
lines := []string{"single line"}
result := iso.Get(lines)
assert.Equal(t, "single line", result)
})
t.Run("Get handles empty slice", func(t *testing.T) {
lines := []string{}
result := iso.Get(lines)
assert.Equal(t, "", result)
})
t.Run("Get handles empty strings in slice", func(t *testing.T) {
lines := []string{"a", "", "b"}
result := iso.Get(lines)
assert.Equal(t, "a\n\nb", result)
})
t.Run("Get handles slice with only empty strings", func(t *testing.T) {
lines := []string{"", "", ""}
result := iso.Get(lines)
assert.Equal(t, "\n\n", result)
})
t.Run("ReverseGet splits string by newline", func(t *testing.T) {
str := "line1\nline2\nline3"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"line1", "line2", "line3"}, result)
})
t.Run("ReverseGet handles single line", func(t *testing.T) {
str := "single line"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"single line"}, result)
})
t.Run("ReverseGet handles empty string", func(t *testing.T) {
str := ""
result := iso.ReverseGet(str)
assert.Equal(t, []string{""}, result)
})
t.Run("ReverseGet handles consecutive newlines", func(t *testing.T) {
str := "a\n\nb"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"a", "", "b"}, result)
})
t.Run("ReverseGet handles trailing newline", func(t *testing.T) {
str := "a\nb\n"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"a", "b", ""}, result)
})
t.Run("ReverseGet handles leading newline", func(t *testing.T) {
str := "\na\nb"
result := iso.ReverseGet(str)
assert.Equal(t, []string{"", "a", "b"}, result)
})
t.Run("Round-trip lines to string to lines", func(t *testing.T) {
original := []string{"line1", "line2", "line3"}
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
})
t.Run("Round-trip string to lines to string", func(t *testing.T) {
original := "line1\nline2\nline3"
result := iso.Get(iso.ReverseGet(original))
assert.Equal(t, original, result)
})
t.Run("Handles lines with special characters", func(t *testing.T) {
lines := []string{"Hello 世界", "🌍 Earth", "tab\there"}
text := iso.Get(lines)
result := iso.ReverseGet(text)
assert.Equal(t, lines, result)
})
t.Run("Preserves whitespace in lines", func(t *testing.T) {
lines := []string{" indented", "normal", "\ttabbed"}
text := iso.Get(lines)
result := iso.ReverseGet(text)
assert.Equal(t, lines, result)
})
}
// TestUnixMilli tests the UnixMilli isomorphism
func TestUnixMilli(t *testing.T) {
iso := UnixMilli()
t.Run("Get converts milliseconds to time", func(t *testing.T) {
millis := int64(1609459200000) // 2021-01-01 00:00:00 UTC
result := iso.Get(millis)
// Compare Unix timestamps to avoid timezone issues
assert.Equal(t, millis, result.UnixMilli())
})
t.Run("Get handles zero milliseconds (Unix epoch)", func(t *testing.T) {
millis := int64(0)
result := iso.Get(millis)
assert.Equal(t, millis, result.UnixMilli())
})
t.Run("Get handles negative milliseconds (before epoch)", func(t *testing.T) {
millis := int64(-86400000) // 1 day before epoch
result := iso.Get(millis)
assert.Equal(t, millis, result.UnixMilli())
})
t.Run("ReverseGet converts time to milliseconds", func(t *testing.T) {
tm := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
result := iso.ReverseGet(tm)
assert.Equal(t, int64(1609459200000), result)
})
t.Run("ReverseGet handles Unix epoch", func(t *testing.T) {
tm := time.Unix(0, 0).UTC()
result := iso.ReverseGet(tm)
assert.Equal(t, int64(0), result)
})
t.Run("ReverseGet handles time before epoch", func(t *testing.T) {
tm := time.Date(1969, 12, 31, 0, 0, 0, 0, time.UTC)
result := iso.ReverseGet(tm)
assert.Equal(t, int64(-86400000), result)
})
t.Run("Round-trip milliseconds to time to milliseconds", func(t *testing.T) {
original := int64(1234567890000)
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
})
t.Run("Round-trip time to milliseconds to time", func(t *testing.T) {
original := time.Date(2021, 6, 15, 12, 30, 45, 0, time.UTC)
result := iso.Get(iso.ReverseGet(original))
// Compare as Unix timestamps to avoid timezone issues
assert.Equal(t, original.UnixMilli(), result.UnixMilli())
})
t.Run("Truncates sub-millisecond precision", func(t *testing.T) {
// Time with nanoseconds
tm := time.Date(2021, 1, 1, 0, 0, 0, 123456789, time.UTC)
millis := iso.ReverseGet(tm)
result := iso.Get(millis)
// Should have millisecond precision only - compare timestamps
assert.Equal(t, tm.Truncate(time.Millisecond).UnixMilli(), result.UnixMilli())
})
t.Run("Handles current time", func(t *testing.T) {
now := time.Now()
millis := iso.ReverseGet(now)
result := iso.Get(millis)
// Should be equal within millisecond precision
assert.Equal(t, now.Truncate(time.Millisecond), result.Truncate(time.Millisecond))
})
t.Run("Handles far future date", func(t *testing.T) {
future := time.Date(2100, 12, 31, 23, 59, 59, 0, time.UTC)
millis := iso.ReverseGet(future)
result := iso.Get(millis)
assert.Equal(t, future.UnixMilli(), result.UnixMilli())
})
t.Run("Handles far past date", func(t *testing.T) {
past := time.Date(1900, 1, 1, 0, 0, 0, 0, time.UTC)
millis := iso.ReverseGet(past)
result := iso.Get(millis)
assert.Equal(t, past.UnixMilli(), result.UnixMilli())
})
t.Run("Preserves timezone information in round-trip", func(t *testing.T) {
// Create time in different timezone
loc, _ := time.LoadLocation("America/New_York")
tm := time.Date(2021, 6, 15, 12, 0, 0, 0, loc)
// Convert to millis and back
millis := iso.ReverseGet(tm)
result := iso.Get(millis)
// Times should represent the same instant (even if timezone differs)
assert.True(t, tm.Equal(result))
})
}
// TestUTF8StringRoundTripLaws verifies isomorphism laws for UTF8String
func TestUTF8StringRoundTripLaws(t *testing.T) {
iso := UTF8String()
t.Run("Law 1: ReverseGet(Get(bytes)) == bytes", func(t *testing.T) {
testCases := [][]byte{
[]byte("hello"),
[]byte(""),
[]byte("Hello 世界 🌍"),
[]byte{0x48, 0x65, 0x6c, 0x6c, 0x6f},
}
for _, original := range testCases {
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
}
})
t.Run("Law 2: Get(ReverseGet(str)) == str", func(t *testing.T) {
testCases := []string{
"hello",
"",
"Hello 世界 🌍",
"special\nchars\ttab",
}
for _, original := range testCases {
result := iso.Get(iso.ReverseGet(original))
assert.Equal(t, original, result)
}
})
}
// TestLinesRoundTripLaws verifies isomorphism laws for Lines
func TestLinesRoundTripLaws(t *testing.T) {
iso := Lines()
t.Run("Law 1: ReverseGet(Get(lines)) == lines", func(t *testing.T) {
testCases := [][]string{
{"line1", "line2"},
{"single"},
{"a", "", "b"},
{"", "", ""},
}
for _, original := range testCases {
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
}
})
t.Run("Law 1: Empty slice special case", func(t *testing.T) {
// Empty slice becomes "" which splits to [""]
// This is expected behavior of strings.Split
original := []string{}
text := iso.Get(original) // ""
result := iso.ReverseGet(text) // [""]
assert.Equal(t, []string{""}, result)
})
t.Run("Law 2: Get(ReverseGet(str)) == str", func(t *testing.T) {
testCases := []string{
"line1\nline2",
"single",
"",
"a\n\nb",
"\n\n",
}
for _, original := range testCases {
result := iso.Get(iso.ReverseGet(original))
assert.Equal(t, original, result)
}
})
}
// TestUnixMilliRoundTripLaws verifies isomorphism laws for UnixMilli
func TestUnixMilliRoundTripLaws(t *testing.T) {
iso := UnixMilli()
t.Run("Law 1: ReverseGet(Get(millis)) == millis", func(t *testing.T) {
testCases := []int64{
0,
1609459200000,
-86400000,
1234567890000,
time.Now().UnixMilli(),
}
for _, original := range testCases {
result := iso.ReverseGet(iso.Get(original))
assert.Equal(t, original, result)
}
})
t.Run("Law 2: Get(ReverseGet(time)) == time (with millisecond precision)", func(t *testing.T) {
testCases := []time.Time{
time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC),
time.Unix(0, 0).UTC(),
time.Date(1969, 12, 31, 0, 0, 0, 0, time.UTC),
time.Now().Truncate(time.Millisecond),
}
for _, original := range testCases {
result := iso.Get(iso.ReverseGet(original))
// Compare Unix timestamps to avoid timezone issues
assert.Equal(t, original.UnixMilli(), result.UnixMilli())
}
})
}
// TestIsosComposition tests composing the isos functions
func TestIsosComposition(t *testing.T) {
t.Run("Compose UTF8String with Lines", func(t *testing.T) {
utf8Iso := UTF8String()
linesIso := Lines()
// First convert bytes to string, then string to lines
bytes := []byte("line1\nline2\nline3")
str := utf8Iso.Get(bytes)
lines := linesIso.ReverseGet(str)
assert.Equal(t, []string{"line1", "line2", "line3"}, lines)
// Reverse: lines to string to bytes
originalLines := []string{"a", "b", "c"}
text := linesIso.Get(originalLines)
resultBytes := utf8Iso.ReverseGet(text)
assert.Equal(t, []byte("a\nb\nc"), resultBytes)
})
t.Run("Chain UTF8String and Lines operations", func(t *testing.T) {
utf8Iso := UTF8String()
linesIso := Lines()
// Process: bytes -> string -> lines -> string -> bytes
original := []byte("hello\nworld")
str := utf8Iso.Get(original)
lines := linesIso.ReverseGet(str)
text := linesIso.Get(lines)
result := utf8Iso.ReverseGet(text)
assert.Equal(t, original, result)
})
}

View File

@@ -0,0 +1,83 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package option provides isomorphisms for working with Option types.
// It offers utilities to convert between regular values and Option-wrapped values,
// particularly useful for handling zero values and optional data.
package option
import (
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/IBM/fp-go/v2/option"
)
// FromZero creates an isomorphism between a comparable type T and Option[T].
// The isomorphism treats the zero value of T as None and non-zero values as Some.
//
// This is particularly useful for types where the zero value has special meaning
// (e.g., 0 for numbers, "" for strings, nil for pointers) and you want to represent
// the absence of a meaningful value using Option.
//
// Type Parameters:
// - T: A comparable type (must support == and != operators)
//
// Returns:
// - An Iso[T, Option[T]] where:
// - Get: Converts T to Option[T] (zero value → None, non-zero → Some)
// - ReverseGet: Converts Option[T] to T (None → zero value, Some → unwrapped value)
//
// Behavior:
// - Get direction: If the value equals the zero value of T, returns None; otherwise returns Some(value)
// - ReverseGet direction: If the Option is None, returns the zero value; otherwise returns the unwrapped value
//
// Example with integers:
//
// isoInt := FromZero[int]()
// opt := isoInt.Get(0) // None (0 is the zero value)
// opt = isoInt.Get(42) // Some(42)
// val := isoInt.ReverseGet(option.None[int]()) // 0
// val = isoInt.ReverseGet(option.Some(42)) // 42
//
// Example with strings:
//
// isoStr := FromZero[string]()
// opt := isoStr.Get("") // None ("" is the zero value)
// opt = isoStr.Get("hello") // Some("hello")
// val := isoStr.ReverseGet(option.None[string]()) // ""
// val = isoStr.ReverseGet(option.Some("world")) // "world"
//
// Example with pointers:
//
// isoPtr := FromZero[*int]()
// opt := isoPtr.Get(nil) // None (nil is the zero value)
// num := 42
// opt = isoPtr.Get(&num) // Some(&num)
//
// Use cases:
// - Converting between database nullable columns and Go types
// - Handling optional configuration values with defaults
// - Working with APIs that use zero values to indicate absence
// - Simplifying validation logic for required vs optional fields
//
// Note: This isomorphism satisfies the round-trip laws:
// - ReverseGet(Get(t)) == t for all t: T
// - Get(ReverseGet(opt)) == opt for all opt: Option[T]
func FromZero[T comparable]() iso.Iso[T, option.Option[T]] {
var zero T
return iso.MakeIso(
option.FromPredicate(func(t T) bool { return t != zero }),
option.GetOrElse(func() T { return zero }),
)
}

View File

@@ -0,0 +1,366 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package option
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/iso"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestFromZeroInt tests the FromZero isomorphism with integer type
func TestFromZeroInt(t *testing.T) {
isoInt := FromZero[int]()
t.Run("Get converts zero to None", func(t *testing.T) {
result := isoInt.Get(0)
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-zero to Some", func(t *testing.T) {
result := isoInt.Get(42)
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.MonadGetOrElse(result, func() int { return 0 }))
})
t.Run("Get converts negative to Some", func(t *testing.T) {
result := isoInt.Get(-5)
assert.True(t, O.IsSome(result))
assert.Equal(t, -5, O.MonadGetOrElse(result, func() int { return 0 }))
})
t.Run("ReverseGet converts None to zero", func(t *testing.T) {
result := isoInt.ReverseGet(O.None[int]())
assert.Equal(t, 0, result)
})
t.Run("ReverseGet converts Some to value", func(t *testing.T) {
result := isoInt.ReverseGet(O.Some(42))
assert.Equal(t, 42, result)
})
}
// TestFromZeroString tests the FromZero isomorphism with string type
func TestFromZeroString(t *testing.T) {
isoStr := FromZero[string]()
t.Run("Get converts empty string to None", func(t *testing.T) {
result := isoStr.Get("")
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-empty string to Some", func(t *testing.T) {
result := isoStr.Get("hello")
assert.True(t, O.IsSome(result))
assert.Equal(t, "hello", O.MonadGetOrElse(result, func() string { return "" }))
})
t.Run("ReverseGet converts None to empty string", func(t *testing.T) {
result := isoStr.ReverseGet(O.None[string]())
assert.Equal(t, "", result)
})
t.Run("ReverseGet converts Some to value", func(t *testing.T) {
result := isoStr.ReverseGet(O.Some("world"))
assert.Equal(t, "world", result)
})
}
// TestFromZeroFloat tests the FromZero isomorphism with float64 type
func TestFromZeroFloat(t *testing.T) {
isoFloat := FromZero[float64]()
t.Run("Get converts 0.0 to None", func(t *testing.T) {
result := isoFloat.Get(0.0)
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-zero float to Some", func(t *testing.T) {
result := isoFloat.Get(3.14)
assert.True(t, O.IsSome(result))
assert.InDelta(t, 3.14, O.MonadGetOrElse(result, func() float64 { return 0.0 }), 0.001)
})
t.Run("ReverseGet converts None to 0.0", func(t *testing.T) {
result := isoFloat.ReverseGet(O.None[float64]())
assert.Equal(t, 0.0, result)
})
t.Run("ReverseGet converts Some to value", func(t *testing.T) {
result := isoFloat.ReverseGet(O.Some(2.718))
assert.InDelta(t, 2.718, result, 0.001)
})
}
// TestFromZeroPointer tests the FromZero isomorphism with pointer type
func TestFromZeroPointer(t *testing.T) {
isoPtr := FromZero[*int]()
t.Run("Get converts nil to None", func(t *testing.T) {
result := isoPtr.Get(nil)
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-nil pointer to Some", func(t *testing.T) {
num := 42
result := isoPtr.Get(&num)
assert.True(t, O.IsSome(result))
ptr := O.MonadGetOrElse(result, func() *int { return nil })
assert.NotNil(t, ptr)
assert.Equal(t, 42, *ptr)
})
t.Run("ReverseGet converts None to nil", func(t *testing.T) {
result := isoPtr.ReverseGet(O.None[*int]())
assert.Nil(t, result)
})
t.Run("ReverseGet converts Some to pointer", func(t *testing.T) {
num := 99
result := isoPtr.ReverseGet(O.Some(&num))
assert.NotNil(t, result)
assert.Equal(t, 99, *result)
})
}
// TestFromZeroBool tests the FromZero isomorphism with bool type
func TestFromZeroBool(t *testing.T) {
isoBool := FromZero[bool]()
t.Run("Get converts false to None", func(t *testing.T) {
result := isoBool.Get(false)
assert.True(t, O.IsNone(result))
})
t.Run("Get converts true to Some", func(t *testing.T) {
result := isoBool.Get(true)
assert.True(t, O.IsSome(result))
assert.True(t, O.MonadGetOrElse(result, func() bool { return false }))
})
t.Run("ReverseGet converts None to false", func(t *testing.T) {
result := isoBool.ReverseGet(O.None[bool]())
assert.False(t, result)
})
t.Run("ReverseGet converts Some to true", func(t *testing.T) {
result := isoBool.ReverseGet(O.Some(true))
assert.True(t, result)
})
}
// TestFromZeroRoundTripLaws verifies the isomorphism laws
func TestFromZeroRoundTripLaws(t *testing.T) {
t.Run("Law 1: ReverseGet(Get(t)) == t for integers", func(t *testing.T) {
isoInt := FromZero[int]()
// Test with zero value
assert.Equal(t, 0, isoInt.ReverseGet(isoInt.Get(0)))
// Test with non-zero values
assert.Equal(t, 42, isoInt.ReverseGet(isoInt.Get(42)))
assert.Equal(t, -10, isoInt.ReverseGet(isoInt.Get(-10)))
})
t.Run("Law 1: ReverseGet(Get(t)) == t for strings", func(t *testing.T) {
isoStr := FromZero[string]()
// Test with zero value
assert.Equal(t, "", isoStr.ReverseGet(isoStr.Get("")))
// Test with non-zero values
assert.Equal(t, "hello", isoStr.ReverseGet(isoStr.Get("hello")))
})
t.Run("Law 2: Get(ReverseGet(opt)) == opt for None", func(t *testing.T) {
isoInt := FromZero[int]()
none := O.None[int]()
result := isoInt.Get(isoInt.ReverseGet(none))
assert.Equal(t, none, result)
})
t.Run("Law 2: Get(ReverseGet(opt)) == opt for Some", func(t *testing.T) {
isoInt := FromZero[int]()
some := O.Some(42)
result := isoInt.Get(isoInt.ReverseGet(some))
assert.Equal(t, some, result)
})
}
// TestFromZeroWithModify tests using FromZero with iso.Modify
func TestFromZeroWithModify(t *testing.T) {
isoInt := FromZero[int]()
t.Run("Modify applies transformation to non-zero value", func(t *testing.T) {
double := func(opt O.Option[int]) O.Option[int] {
return O.MonadMap(opt, func(x int) int { return x * 2 })
}
result := iso.Modify[int](double)(isoInt)(5)
assert.Equal(t, 10, result)
})
t.Run("Modify preserves zero value", func(t *testing.T) {
double := func(opt O.Option[int]) O.Option[int] {
return O.MonadMap(opt, func(x int) int { return x * 2 })
}
result := iso.Modify[int](double)(isoInt)(0)
assert.Equal(t, 0, result)
})
}
// TestFromZeroWithCompose tests composing FromZero with other isomorphisms
func TestFromZeroWithCompose(t *testing.T) {
isoInt := FromZero[int]()
// Create an isomorphism that doubles/halves values
doubleIso := iso.MakeIso(
func(opt O.Option[int]) O.Option[int] {
return O.MonadMap(opt, func(x int) int { return x * 2 })
},
func(opt O.Option[int]) O.Option[int] {
return O.MonadMap(opt, func(x int) int { return x / 2 })
},
)
composed := F.Pipe1(isoInt, iso.Compose[int](doubleIso))
t.Run("Composed isomorphism works with non-zero", func(t *testing.T) {
result := composed.Get(5)
assert.True(t, O.IsSome(result))
assert.Equal(t, 10, O.MonadGetOrElse(result, func() int { return 0 }))
})
t.Run("Composed isomorphism works with zero", func(t *testing.T) {
result := composed.Get(0)
assert.True(t, O.IsNone(result))
})
t.Run("Composed isomorphism reverse works", func(t *testing.T) {
result := composed.ReverseGet(O.Some(20))
assert.Equal(t, 10, result)
})
}
// TestFromZeroWithUnwrapWrap tests using Unwrap and Wrap helpers
func TestFromZeroWithUnwrapWrap(t *testing.T) {
isoInt := FromZero[int]()
t.Run("Unwrap extracts Option from value", func(t *testing.T) {
result := iso.Unwrap[O.Option[int]](42)(isoInt)
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.MonadGetOrElse(result, func() int { return 0 }))
})
t.Run("Wrap creates value from Option", func(t *testing.T) {
result := iso.Wrap[int](O.Some(99))(isoInt)
assert.Equal(t, 99, result)
})
t.Run("To is alias for Unwrap", func(t *testing.T) {
result := iso.To[O.Option[int]](42)(isoInt)
assert.True(t, O.IsSome(result))
})
t.Run("From is alias for Wrap", func(t *testing.T) {
result := iso.From[int](O.Some(99))(isoInt)
assert.Equal(t, 99, result)
})
}
// TestFromZeroWithReverse tests reversing the isomorphism
func TestFromZeroWithReverse(t *testing.T) {
isoInt := FromZero[int]()
reversed := iso.Reverse(isoInt)
t.Run("Reversed Get is original ReverseGet", func(t *testing.T) {
result := reversed.Get(O.Some(42))
assert.Equal(t, 42, result)
})
t.Run("Reversed ReverseGet is original Get", func(t *testing.T) {
result := reversed.ReverseGet(42)
assert.True(t, O.IsSome(result))
assert.Equal(t, 42, O.MonadGetOrElse(result, func() int { return 0 }))
})
}
// TestFromZeroCustomType tests FromZero with a custom comparable type
func TestFromZeroCustomType(t *testing.T) {
type UserID int
isoUserID := FromZero[UserID]()
t.Run("Get converts zero UserID to None", func(t *testing.T) {
result := isoUserID.Get(UserID(0))
assert.True(t, O.IsNone(result))
})
t.Run("Get converts non-zero UserID to Some", func(t *testing.T) {
result := isoUserID.Get(UserID(123))
assert.True(t, O.IsSome(result))
assert.Equal(t, UserID(123), O.MonadGetOrElse(result, func() UserID { return 0 }))
})
t.Run("ReverseGet converts None to zero UserID", func(t *testing.T) {
result := isoUserID.ReverseGet(O.None[UserID]())
assert.Equal(t, UserID(0), result)
})
t.Run("ReverseGet converts Some to UserID", func(t *testing.T) {
result := isoUserID.ReverseGet(O.Some(UserID(456)))
assert.Equal(t, UserID(456), result)
})
}
// TestFromZeroEdgeCases tests edge cases and boundary conditions
func TestFromZeroEdgeCases(t *testing.T) {
t.Run("Works with maximum int value", func(t *testing.T) {
isoInt := FromZero[int]()
maxInt := int(^uint(0) >> 1)
result := isoInt.Get(maxInt)
assert.True(t, O.IsSome(result))
assert.Equal(t, maxInt, isoInt.ReverseGet(result))
})
t.Run("Works with minimum int value", func(t *testing.T) {
isoInt := FromZero[int]()
minInt := -int(^uint(0)>>1) - 1
result := isoInt.Get(minInt)
assert.True(t, O.IsSome(result))
assert.Equal(t, minInt, isoInt.ReverseGet(result))
})
t.Run("Works with very long strings", func(t *testing.T) {
isoStr := FromZero[string]()
longStr := string(make([]byte, 10000))
for i := range longStr {
longStr = longStr[:i] + "a" + longStr[i+1:]
}
result := isoStr.Get(longStr)
assert.True(t, O.IsSome(result))
assert.Equal(t, longStr, isoStr.ReverseGet(result))
})
}

View File

@@ -17,10 +17,7 @@
package lens
import (
EM "github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
)
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
@@ -44,32 +41,156 @@ func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(a
}
}
// MakeLens creates a [Lens] based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeLensRef`
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
// MakeLens creates a [Lens] based on a getter and a setter F.
//
// The setter must create a (shallow) copy of the data structure. This happens automatically
// when the data is passed by value. For pointer-based structures, use [MakeLensRef] instead.
// For other reference types (slices, maps), ensure the setter creates a copy.
//
// Type Parameters:
// - GET: Getter function type (S → A)
// - SET: Setter function type (S, A → S)
// - S: Source structure type
// - A: Focus/field type
//
// Parameters:
// - get: Function to extract value A from structure S
// - set: Function to update value A in structure S, returning a new S
//
// Returns:
// - A Lens[S, A] that can get and set values immutably
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// nameLens := lens.MakeLens(
// func(p Person) string { return p.Name },
// func(p Person, name string) Person {
// p.Name = name
// return p
// },
// )
//
// person := Person{Name: "Alice", Age: 30}
// name := nameLens.Get(person) // "Alice"
// updated := nameLens.Set("Bob")(person) // Person{Name: "Bob", Age: 30}
func MakeLens[GET ~func(S) A, SET ~func(S, A) S, S, A any](get GET, set SET) Lens[S, A] {
return MakeLensCurried(get, function.Curry2(F.Swap(set)))
return MakeLensCurried(get, F.Curry2(F.Swap(set)))
}
// MakeLensCurried creates a [Lens] based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeLensRef`
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
// MakeLensCurried creates a [Lens] with a curried setter F.
//
// This is similar to [MakeLens] but accepts a curried setter (A → S → S) instead of
// an uncurried one (S, A → S). The curried form is more composable in functional pipelines.
//
// The setter must create a (shallow) copy of the data structure. This happens automatically
// when the data is passed by value. For pointer-based structures, use [MakeLensRefCurried].
//
// Type Parameters:
// - GET: Getter function type (S → A)
// - SET: Curried setter function type (A → S → S)
// - S: Source structure type
// - A: Focus/field type
//
// Parameters:
// - get: Function to extract value A from structure S
// - set: Curried function to update value A in structure S
//
// Returns:
// - A Lens[S, A] that can get and set values immutably
//
// Example:
//
// nameLens := lens.MakeLensCurried(
// func(p Person) string { return p.Name },
// func(name string) func(Person) Person {
// return func(p Person) Person {
// p.Name = name
// return p
// }
// },
// )
func MakeLensCurried[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](get GET, set SET) Lens[S, A] {
return Lens[S, A]{Get: get, Set: set}
}
// MakeLensRef creates a [Lens] based on a getter and a setter function. The setter passed in does not have to create a shallow
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
// MakeLensRef creates a [Lens] for pointer-based structures.
//
// Such a [Lens] assumes that property A of S always exists
// Unlike [MakeLens], the setter does not need to create a copy manually. This function
// automatically wraps the setter to create a shallow copy of the pointed-to value before
// modification, ensuring immutability.
//
// This lens assumes that property A always exists in structure S (i.e., it's not optional).
//
// Type Parameters:
// - GET: Getter function type (*S → A)
// - SET: Setter function type (*S, A → *S)
// - S: Source structure type (will be used as *S)
// - A: Focus/field type
//
// Parameters:
// - get: Function to extract value A from pointer *S
// - set: Function to update value A in pointer *S (copying handled automatically)
//
// Returns:
// - A Lens[*S, A] that can get and set values immutably on pointers
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// nameLens := lens.MakeLensRef(
// func(p *Person) string { return p.Name },
// func(p *Person, name string) *Person {
// p.Name = name // No manual copy needed
// return p
// },
// )
//
// person := &Person{Name: "Alice", Age: 30}
// updated := nameLens.Set("Bob")(person)
// // person.Name is still "Alice", updated is a new pointer with Name "Bob"
func MakeLensRef[GET ~func(*S) A, SET func(*S, A) *S, S, A any](get GET, set SET) Lens[*S, A] {
return MakeLens(get, setCopy(set))
}
// MakeLensRefCurried creates a [Lens] based on a getter and a setter function. The setter passed in does not have to create a shallow
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
// MakeLensRefCurried creates a [Lens] for pointer-based structures with a curried setter.
//
// Such a [Lens] assumes that property A of S always exists
// This combines the benefits of [MakeLensRef] (automatic copying) with [MakeLensCurried]
// (curried setter for better composition). The setter does not need to create a copy manually;
// this function automatically wraps it to ensure immutability.
//
// This lens assumes that property A always exists in structure S (i.e., it's not optional).
//
// Type Parameters:
// - S: Source structure type (will be used as *S)
// - A: Focus/field type
//
// Parameters:
// - get: Function to extract value A from pointer *S
// - set: Curried function to update value A in pointer *S (copying handled automatically)
//
// Returns:
// - A Lens[*S, A] that can get and set values immutably on pointers
//
// Example:
//
// nameLens := lens.MakeLensRefCurried(
// func(p *Person) string { return p.Name },
// func(name string) func(*Person) *Person {
// return func(p *Person) *Person {
// p.Name = name // No manual copy needed
// return p
// }
// },
// )
func MakeLensRefCurried[S, A any](get func(*S) A, set func(A) Endomorphism[*S]) Lens[*S, A] {
return MakeLensCurried(get, setCopyCurried(set))
}
@@ -79,12 +200,54 @@ func id[GET ~func(S) S, SET ~func(S, S) S, S any](creator func(get GET, set SET)
return creator(F.Identity[S], F.Second[S, S])
}
// Id returns a [Lens] implementing the identity operation
// Id returns an identity [Lens] that focuses on the entire structure.
//
// The identity lens is useful as a starting point for lens composition or when you need
// a lens that doesn't actually focus on a subpart. Get returns the structure unchanged,
// and Set replaces the entire structure.
//
// Type Parameters:
// - S: The structure type
//
// Returns:
// - A Lens[S, S] where both source and focus are the same type
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// idLens := lens.Id[Person]()
// person := Person{Name: "Alice", Age: 30}
//
// same := idLens.Get(person) // Returns person unchanged
// replaced := idLens.Set(Person{Name: "Bob", Age: 25})(person)
// // replaced is Person{Name: "Bob", Age: 25}
func Id[S any]() Lens[S, S] {
return id(MakeLens[Endomorphism[S], func(S, S) S])
}
// IdRef returns a [Lens] implementing the identity operation
// IdRef returns an identity [Lens] for pointer-based structures.
//
// This is the pointer version of [Id]. It focuses on the entire pointer structure,
// with automatic copying to ensure immutability.
//
// Type Parameters:
// - S: The structure type (will be used as *S)
//
// Returns:
// - A Lens[*S, *S] where both source and focus are pointers to the same type
//
// Example:
//
// idLens := lens.IdRef[Person]()
// person := &Person{Name: "Alice", Age: 30}
//
// same := idLens.Get(person) // Returns person pointer
// replaced := idLens.Set(&Person{Name: "Bob", Age: 25})(person)
// // person.Name is still "Alice", replaced is a new pointer
func IdRef[S any]() Lens[*S, *S] {
return id(MakeLensRef[Endomorphism[*S], func(*S, *S) *S])
}
@@ -105,111 +268,94 @@ func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GE
}
}
// Compose combines two lenses and allows to narrow down the focus to a sub-lens
// Compose combines two lenses to focus on a deeply nested field.
//
// Given a lens from S to A and a lens from A to B, Compose creates a lens from S to B.
// This allows you to navigate through nested structures in a composable way.
//
// The composition follows the mathematical property: (sa ∘ ab).Get = ab.Get ∘ sa.Get
//
// Type Parameters:
// - S: Outer structure type
// - A: Intermediate structure type
// - B: Inner focus type
//
// Parameters:
// - ab: Lens from A to B (inner lens)
//
// Returns:
// - A function that takes a Lens[S, A] and returns a Lens[S, B]
//
// Example:
//
// type Address struct {
// Street string
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// addressLens := lens.MakeLens(
// func(p Person) Address { return p.Address },
// func(p Person, a Address) Person { p.Address = a; return p },
// )
//
// streetLens := lens.MakeLens(
// func(a Address) string { return a.Street },
// func(a Address, s string) Address { a.Street = s; return a },
// )
//
// // Compose to access street directly from person
// personStreetLens := F.Pipe1(addressLens, lens.Compose[Person](streetLens))
//
// person := Person{Name: "Alice", Address: Address{Street: "Main St"}}
// street := personStreetLens.Get(person) // "Main St"
// updated := personStreetLens.Set("Oak Ave")(person)
func Compose[S, A, B any](ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
return compose(MakeLens[func(S) B, func(S, B) S], ab)
}
// ComposeOption combines a `Lens` that returns an optional value with a `Lens` that returns a definite value
// the getter returns an `Option[B]` because the container `A` could already be an option
// if the setter is invoked with `Some[B]` then the value of `B` will be set, potentially on a default value of `A` if `A` did not exist
// if the setter is invoked with `None[B]` then the container `A` is reset to `None[A]` because this is the only way to remove `B`
func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
defa := F.Constant(defaultA)
return func(ab Lens[A, B]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
foldab := O.Fold(O.None[B], F.Flow2(ab.Get, O.Some[B]))
return func(sa Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
// set A on S
seta := F.Flow2(
O.Some[A],
sa.Set,
)
// remove A from S
unseta := F.Nullary2(
O.None[A],
sa.Set,
)
return MakeLens(
F.Flow2(sa.Get, foldab),
func(s S, ob O.Option[B]) S {
return F.Pipe2(
ob,
O.Fold(unseta, func(b B) Endomorphism[S] {
setbona := F.Flow2(
ab.Set(b),
seta,
)
return F.Pipe2(
s,
sa.Get,
O.Fold(
F.Nullary2(
defa,
setbona,
),
setbona,
),
)
}),
EM.Ap(s),
)
},
)
}
}
}
// ComposeOptions combines a `Lens` that returns an optional value with a `Lens` that returns another optional value
// the getter returns `None[B]` if either `A` or `B` is `None`
// if the setter is called with `Some[B]` and `A` exists, 'A' is updated with `B`
// if the setter is called with `Some[B]` and `A` does not exist, the default of 'A' is updated with `B`
// if the setter is called with `None[B]` and `A` does not exist this is the identity operation on 'S'
// if the setter is called with `None[B]` and `A` does exist, 'B' is removed from 'A'
func ComposeOptions[S, B, A any](defaultA A) func(ab Lens[A, O.Option[B]]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
defa := F.Constant(defaultA)
noops := EM.Identity[S]
noneb := O.None[B]()
return func(ab Lens[A, O.Option[B]]) func(Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
unsetb := ab.Set(noneb)
return func(sa Lens[S, O.Option[A]]) Lens[S, O.Option[B]] {
// sets an A onto S
seta := F.Flow2(
O.Some[A],
sa.Set,
)
return MakeLensCurried(
F.Flow2(
sa.Get,
O.Chain(ab.Get),
),
func(b O.Option[B]) Endomorphism[S] {
return func(s S) S {
return O.MonadFold(b, func() Endomorphism[S] {
return F.Pipe2(
s,
sa.Get,
O.Fold(noops, F.Flow2(unsetb, seta)),
)
}, func(b B) Endomorphism[S] {
// sets a B onto an A
setb := F.Flow2(
ab.Set(O.Some(b)),
seta,
)
return F.Pipe2(
s,
sa.Get,
O.Fold(F.Nullary2(defa, setb), setb),
)
})(s)
}
},
)
}
}
}
// Compose combines two lenses and allows to narrow down the focus to a sub-lens
// ComposeRef combines two lenses for pointer-based structures.
//
// This is the pointer version of [Compose], automatically handling copying to ensure immutability.
// It allows you to navigate through nested pointer structures in a composable way.
//
// Type Parameters:
// - S: Outer structure type (will be used as *S)
// - A: Intermediate structure type
// - B: Inner focus type
//
// Parameters:
// - ab: Lens from A to B (inner lens)
//
// Returns:
// - A function that takes a Lens[*S, A] and returns a Lens[*S, B]
//
// Example:
//
// type Address struct {
// Street string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// addressLens := lens.MakeLensRef(
// func(p *Person) Address { return p.Address },
// func(p *Person, a Address) *Person { p.Address = a; return p },
// )
//
// streetLens := lens.MakeLens(
// func(a Address) string { return a.Street },
// func(a Address, s string) Address { a.Street = s; return a },
// )
//
// personStreetLens := F.Pipe1(addressLens, lens.ComposeRef[Person](streetLens))
func ComposeRef[S, A, B any](ab Lens[A, B]) func(Lens[*S, A]) Lens[*S, B] {
return compose(MakeLensRef[func(*S) B, func(*S, B) *S], ab)
}
@@ -218,101 +364,108 @@ func modify[FCT ~func(A) A, S, A any](f FCT, sa Lens[S, A], s S) S {
return sa.Set(f(sa.Get(s)))(s)
}
// Modify changes a property of a [Lens] by invoking a transformation function
// if the transformed property has not changes, the method returns the original state
// Modify transforms a value through a lens using a transformation F.
//
// Instead of setting a specific value, Modify applies a function to the current value.
// This is useful for updates like incrementing a counter, appending to a string, etc.
// If the transformation doesn't change the value, the original structure is returned.
//
// Type Parameters:
// - S: Structure type
// - FCT: Transformation function type (A → A)
// - A: Focus type
//
// Parameters:
// - f: Transformation function to apply to the focused value
//
// Returns:
// - A function that takes a Lens[S, A] and returns an Endomorphism[S]
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// counter := Counter{Value: 5}
//
// // Increment the counter
// incremented := F.Pipe2(
// valueLens,
// lens.Modify[Counter](func(v int) int { return v + 1 }),
// F.Ap(counter),
// )
// // incremented.Value == 6
//
// // Double the counter
// doubled := F.Pipe2(
// valueLens,
// lens.Modify[Counter](func(v int) int { return v * 2 }),
// F.Ap(counter),
// )
// // doubled.Value == 10
func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S] {
return function.Curry3(modify[FCT, S, A])(f)
return F.Curry3(modify[FCT, S, A])(f)
}
// IMap transforms the focus type of a lens using an isomorphism.
//
// An isomorphism is a pair of functions (A → B, B → A) that are inverses of each other.
// IMap allows you to work with a lens in a different but equivalent type. This is useful
// for unit conversions, encoding/decoding, or any bidirectional transformation.
//
// Type Parameters:
// - E: Structure type
// - AB: Forward transformation function type (A → B)
// - BA: Backward transformation function type (B → A)
// - A: Original focus type
// - B: Transformed focus type
//
// Parameters:
// - ab: Forward transformation (A → B)
// - ba: Backward transformation (B → A)
//
// Returns:
// - A function that takes a Lens[E, A] and returns a Lens[E, B]
//
// Example:
//
// 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 := lens.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,
// lens.IMap[Weather](celsiusToFahrenheit, fahrenheitToCelsius),
// )
//
// weather := Weather{Temperature: 20} // 20°C
// tempF := tempFahrenheitLens.Get(weather) // 68°F
// updated := tempFahrenheitLens.Set(86)(weather) // Set to 86°F (30°C)
func IMap[E any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Lens[E, A]) Lens[E, B] {
return func(ea Lens[E, A]) Lens[E, B] {
return Lens[E, B]{Get: F.Flow2(ea.Get, ab), Set: F.Flow2(ba, ea.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[GET ~func(S) O.Option[A], SET ~func(S, O.Option[A]) S, S, A any](creator func(get GET, set SET) Lens[S, O.Option[A]], pred func(A) bool, nilValue A) func(sa Lens[S, A]) Lens[S, O.Option[A]] {
fromPred := O.FromPredicate(pred)
return func(sa Lens[S, A]) Lens[S, O.Option[A]] {
fold := O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set)
return creator(F.Flow2(sa.Get, fromPred), func(s S, a O.Option[A]) S {
return F.Pipe2(
a,
fold,
EM.Ap(s),
)
})
}
}
// 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]) Lens[S, O.Option[A]] {
return fromPredicate(MakeLens[func(S) O.Option[A], func(S, O.Option[A]) 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, O.Option[A]] {
return fromPredicate(MakeLensRef[func(*S) O.Option[A], func(*S, O.Option[A]) *S], pred, nilValue)
}
// 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 FromNillable[S, A any](sa Lens[S, *A]) Lens[S, O.Option[*A]] {
return FromPredicate[S](F.IsNonNil[A], nil)(sa)
}
// FromNillableRef 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 FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, O.Option[*A]] {
return FromPredicateRef[S](F.IsNonNil[A], nil)(sa)
}
// 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) O.Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
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)
},
)
}
}
// 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) O.Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
return fromNullableProp(MakeLens[func(S) A, func(S, A) 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) O.Option[A], defaultValue A) func(sa Lens[*S, A]) Lens[*S, A] {
return fromNullableProp(MakeLensRef[func(*S) A, func(*S, A) *S], isNullable, defaultValue)
}
// fromFromOption 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 Lens[S, O.Option[A]]) Lens[S, A] {
return func(sa Lens[S, O.Option[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)
},
)
}
}
// FromFromOption 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 Lens[S, O.Option[A]]) Lens[S, A] {
return fromOption(MakeLens[func(S) A, func(S, A) S], defaultValue)
}
// FromFromOptionRef returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, O.Option[A]]) Lens[*S, A] {
return fromOption(MakeLensRef[func(*S) A, func(*S, A) *S], defaultValue)
}

View File

@@ -19,7 +19,6 @@ import (
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
@@ -172,83 +171,6 @@ func TestPassByValue(t *testing.T) {
assert.Equal(t, "value2", s2.name)
}
func TestFromNullableProp(t *testing.T) {
// default inner object
defaultInner := &Inner{
Value: 0,
Foo: "foo",
}
// access to the value
value := MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
// access to inner
inner := FromNullableProp[Outer](O.FromNillable[Inner], defaultInner)(MakeLens(Outer.GetInner, Outer.SetInner))
// compose
lens := F.Pipe1(
inner,
Compose[Outer](value),
)
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
// the checks
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{}))
assert.Equal(t, 0, lens.Get(Outer{}))
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
assert.Equal(t, 1, lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
assert.Equal(t, outer1, Modify[Outer](F.Identity[int])(lens)(outer1))
}
func TestComposeOption(t *testing.T) {
// default inner object
defaultInner := &Inner{
Value: 0,
Foo: "foo",
}
// access to the value
value := MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
// access to inner
inner := FromNillable(MakeLens(Outer.GetInner, Outer.SetInner))
// compose lenses
lens := F.Pipe1(
inner,
ComposeOption[Outer, int](defaultInner)(value),
)
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
// the checks
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{}))
assert.Equal(t, O.None[int](), lens.Get(Outer{}))
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
assert.Equal(t, O.Some(1), lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
assert.Equal(t, outer1, Modify[Outer](F.Identity[O.Option[int]])(lens)(outer1))
}
func TestComposeOptions(t *testing.T) {
// default inner object
defaultValue1 := 1
defaultFoo1 := "foo1"
defaultInner := &InnerOpt{
Value: &defaultValue1,
Foo: &defaultFoo1,
}
// access to the value
value := FromNillable(MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
// access to inner
inner := FromNillable(MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
// compose lenses
lens := F.Pipe1(
inner,
ComposeOptions[OuterOpt, *int](defaultInner)(value),
)
// additional settings
defaultValue2 := 2
defaultFoo2 := "foo2"
outer1 := OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}
// the checks
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{}))
assert.Equal(t, O.None[*int](), lens.Get(OuterOpt{}))
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo2}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}))
assert.Equal(t, O.Some(&defaultValue1), lens.Get(OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}))
assert.Equal(t, outer1, Modify[OuterOpt](F.Identity[O.Option[*int]])(lens)(outer1))
}
func TestIdRef(t *testing.T) {
idLens := IdRef[Street]()
street := &Street{num: 1, name: "Main"}
@@ -272,93 +194,6 @@ func TestComposeRef(t *testing.T) {
assert.Equal(t, sampleStreet.name, sampleAddress.street.name) // Original unchanged
}
func TestFromPredicateRef(t *testing.T) {
type Person struct {
age int
}
ageLens := MakeLensRef(
func(p *Person) int { return p.age },
func(p *Person, age int) *Person {
p.age = age
return p
},
)
adultLens := FromPredicateRef[Person](func(age int) bool { return age >= 18 }, 0)(ageLens)
adult := &Person{age: 25}
assert.Equal(t, O.Some(25), adultLens.Get(adult))
minor := &Person{age: 15}
assert.Equal(t, O.None[int](), adultLens.Get(minor))
}
func TestFromNillableRef(t *testing.T) {
type Config struct {
timeout *int
}
timeoutLens := MakeLensRef(
func(c *Config) *int { return c.timeout },
func(c *Config, t *int) *Config {
c.timeout = t
return c
},
)
optLens := FromNillableRef(timeoutLens)
config := &Config{timeout: nil}
assert.Equal(t, O.None[*int](), optLens.Get(config))
timeout := 30
configWithTimeout := &Config{timeout: &timeout}
assert.True(t, O.IsSome(optLens.Get(configWithTimeout)))
}
func TestFromNullablePropRef(t *testing.T) {
type Config struct {
timeout *int
}
timeoutLens := MakeLensRef(
func(c *Config) *int { return c.timeout },
func(c *Config, t *int) *Config {
c.timeout = t
return c
},
)
defaultTimeout := 30
safeLens := FromNullablePropRef[Config](O.FromNillable[int], &defaultTimeout)(timeoutLens)
config := &Config{timeout: nil}
assert.Equal(t, &defaultTimeout, safeLens.Get(config))
}
func TestFromOptionRef(t *testing.T) {
type Settings struct {
retries O.Option[int]
}
retriesLens := MakeLensRef(
func(s *Settings) O.Option[int] { return s.retries },
func(s *Settings, r O.Option[int]) *Settings {
s.retries = r
return s
},
)
safeLens := FromOptionRef[Settings](3)(retriesLens)
settings := &Settings{retries: O.None[int]()}
assert.Equal(t, 3, safeLens.Get(settings))
settingsWithRetries := &Settings{retries: O.Some(5)}
assert.Equal(t, 5, safeLens.Get(settingsWithRetries))
}
func TestMakeLensCurried(t *testing.T) {
nameLens := MakeLensCurried(
func(s Street) string { return s.name },

View File

@@ -0,0 +1,192 @@
package option
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
)
// Compose composes two lenses that both return optional values.
//
// This handles the case where both the intermediate structure A and the inner focus B are optional.
// The getter returns None[B] if either A or B is None. The setter behavior is:
// - Set(Some[B]) when A exists: Updates B in A
// - Set(Some[B]) when A doesn't exist: Creates A with defaultA and sets B
// - Set(None[B]) when A doesn't exist: Identity operation (no change)
// - Set(None[B]) when A exists: Removes B from A (sets it to None)
//
// Type Parameters:
// - S: Outer structure type
// - B: Inner focus type (optional)
// - A: Intermediate structure type (optional)
//
// Parameters:
// - defaultA: Default value for A when it doesn't exist but B needs to be set
//
// Returns:
// - A function that takes a LensO[A, B] and returns a function that takes a
// LensO[S, A] and returns a LensO[S, B]
//
// Example:
//
// type Settings struct {
// MaxRetries *int
// }
//
// type Config struct {
// Settings *Settings
// }
//
// settingsLens := lens.FromNillable(lens.MakeLens(
// func(c Config) *Settings { return c.Settings },
// func(c Config, s *Settings) Config { c.Settings = s; return c },
// ))
//
// retriesLens := lens.FromNillable(lens.MakeLensRef(
// func(s *Settings) *int { return s.MaxRetries },
// func(s *Settings, r *int) *Settings { s.MaxRetries = r; return s },
// ))
//
// defaultSettings := &Settings{}
// configRetriesLens := F.Pipe1(settingsLens,
// lens.Compose[Config, *int](defaultSettings)(retriesLens))
func Compose[S, B, A any](defaultA A) func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
noneb := O.None[B]()
return func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
abGet := ab.Get
abSetNone := ab.Set(noneb)
return func(sa LensO[S, A]) LensO[S, B] {
saGet := sa.Get
// Pre-compute setter for Some[A]
setSomeA := F.Flow2(O.Some[A], sa.Set)
return lens.MakeLensCurried(
F.Flow2(saGet, O.Chain(abGet)),
func(optB Option[B]) Endomorphism[S] {
return func(s S) S {
optA := saGet(s)
return O.MonadFold(
optB,
// optB is None
func() S {
return O.MonadFold(
optA,
// optA is None - no-op
F.Constant(s),
// optA is Some - unset B in A
func(a A) S {
return setSomeA(abSetNone(a))(s)
},
)
},
// optB is Some
func(b B) S {
setB := ab.Set(O.Some(b))
return O.MonadFold(
optA,
// optA is None - create with defaultA
func() S {
return setSomeA(setB(defaultA))(s)
},
// optA is Some - update B in A
func(a A) S {
return setSomeA(setB(a))(s)
},
)
},
)
}
},
)
}
}
}
// ComposeOption composes a lens returning an optional value with a lens returning a definite value.
//
// This is useful when you have an optional intermediate structure and want to focus on a field
// within it. The getter returns Option[B] because the container A might not exist. The setter
// behavior depends on the input:
// - Set(Some[B]): Updates B in A, creating A with defaultA if it doesn't exist
// - Set(None[B]): Removes A entirely (sets it to None[A])
//
// Type Parameters:
// - S: Outer structure type
// - B: Inner focus type (definite value)
// - A: Intermediate structure type (optional)
//
// Parameters:
// - defaultA: Default value for A when it doesn't exist but B needs to be set
//
// Returns:
// - A function that takes a Lens[A, B] and returns a function that takes a
// LensO[S, A] and returns a LensO[S, B]
//
// Example:
//
// type Database struct {
// Host string
// Port int
// }
//
// type Config struct {
// Database *Database
// }
//
// dbLens := lens.FromNillable(lens.MakeLens(
// func(c Config) *Database { return c.Database },
// func(c Config, db *Database) Config { c.Database = db; return c },
// ))
//
// portLens := lens.MakeLensRef(
// func(db *Database) int { return db.Port },
// func(db *Database, port int) *Database { db.Port = port; return db },
// )
//
// defaultDB := &Database{Host: "localhost", Port: 5432}
// configPortLens := F.Pipe1(dbLens, lens.ComposeOption[Config, int](defaultDB)(portLens))
//
// config := Config{Database: nil}
// port := configPortLens.Get(config) // None[int]
// updated := configPortLens.Set(O.Some(3306))(config)
// // updated.Database.Port == 3306, Host == "localhost" (from default)
func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
return func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
abGet := ab.Get
abSet := ab.Set
return func(sa LensO[S, A]) LensO[S, B] {
saGet := sa.Get
saSet := sa.Set
// Pre-compute setters
setNoneA := saSet(O.None[A]())
setSomeA := func(a A) Endomorphism[S] {
return saSet(O.Some(a))
}
return lens.MakeLens(
func(s S) Option[B] {
return O.Map(abGet)(saGet(s))
},
func(s S, optB Option[B]) S {
return O.Fold(
// optB is None - remove A entirely
F.Constant(setNoneA(s)),
// optB is Some - set B
func(b B) S {
optA := saGet(s)
return O.Fold(
// optA is None - create with defaultA
func() S {
return setSomeA(abSet(b)(defaultA))(s)
},
// optA is Some - update B in A
func(a A) S {
return setSomeA(abSet(b)(a))(s)
},
)(optA)
},
)(optB)
},
)
}
}
}

View File

@@ -0,0 +1,31 @@
mode: count
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:55.97,59.60 4 3
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:59.60,61.43 2 3
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:61.43,72.39 2 3
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:72.39,73.25 1 13
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:73.25,74.52 1 13
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:74.52,80.8 1 6
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:80.36,91.8 2 7
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:147.95,149.59 2 3
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:149.59,151.43 2 3
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:151.43,164.31 3 3
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:164.31,167.48 1 12
github.com/IBM/fp-go/v2/optics/lens/option/compose.go:167.48,183.8 2 7
github.com/IBM/fp-go/v2/optics/lens/option/from.go:12.188,14.41 2 15
github.com/IBM/fp-go/v2/optics/lens/option/from.go:14.41,16.70 2 15
github.com/IBM/fp-go/v2/optics/lens/option/from.go:16.70,22.4 1 60
github.com/IBM/fp-go/v2/optics/lens/option/from.go:28.93,30.2 1 12
github.com/IBM/fp-go/v2/optics/lens/option/from.go:34.105,36.2 1 3
github.com/IBM/fp-go/v2/optics/lens/option/from.go:40.65,42.2 1 10
github.com/IBM/fp-go/v2/optics/lens/option/from.go:46.70,48.2 1 2
github.com/IBM/fp-go/v2/optics/lens/option/from.go:51.188,52.40 1 3
github.com/IBM/fp-go/v2/optics/lens/option/from.go:52.40,57.23 1 3
github.com/IBM/fp-go/v2/optics/lens/option/from.go:57.23,59.4 1 4
github.com/IBM/fp-go/v2/optics/lens/option/from.go:65.110,67.2 1 2
github.com/IBM/fp-go/v2/optics/lens/option/from.go:70.115,72.2 1 1
github.com/IBM/fp-go/v2/optics/lens/option/from.go:75.153,76.41 1 2
github.com/IBM/fp-go/v2/optics/lens/option/from.go:76.41,80.23 1 2
github.com/IBM/fp-go/v2/optics/lens/option/from.go:80.23,82.4 1 2
github.com/IBM/fp-go/v2/optics/lens/option/from.go:88.75,90.2 1 1
github.com/IBM/fp-go/v2/optics/lens/option/from.go:107.87,109.2 1 1
github.com/IBM/fp-go/v2/optics/lens/option/option.go:63.67,65.2 1 1

View File

@@ -0,0 +1,138 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package option provides utilities for working with lenses that focus on optional values.
//
// This package extends the lens optics pattern to handle Option types, enabling safe
// manipulation of potentially absent values in nested data structures. It provides
// functions for creating, composing, and transforming lenses that work with optional
// fields.
//
// # Core Concepts
//
// A LensO[S, A] is a Lens[S, Option[A]] - a lens that focuses on an optional value A
// within a structure S. This is particularly useful when dealing with nullable pointers,
// optional fields, or values that may not always be present.
//
// # Key Functions
//
// Creating Lenses from Optional Values:
// - FromNillable: Creates a lens from a nullable pointer field
// - FromNillableRef: Pointer-based version of FromNillable
// - FromPredicate: Creates a lens based on a predicate function
// - FromPredicateRef: Pointer-based version of FromPredicate
// - FromOption: Converts an optional lens to a definite lens with a default value
// - FromOptionRef: Pointer-based version of FromOption
// - FromNullableProp: Creates a lens with a default value for nullable properties
// - FromNullablePropRef: Pointer-based version of FromNullableProp
//
// Composing Lenses:
// - ComposeOption: Composes a lens returning Option[A] with a lens returning B
// - ComposeOptions: Composes two lenses that both return optional values
//
// Conversions:
// - AsTraversal: Converts a lens to a traversal for use with traversal operations
//
// # Usage Examples
//
// Working with nullable pointers:
//
// type Config struct {
// Database *DatabaseConfig
// }
//
// type DatabaseConfig struct {
// Host string
// Port int
// }
//
// // Create a lens for the optional database config
// dbLens := lens.FromNillable(lens.MakeLens(
// func(c Config) *DatabaseConfig { return c.Database },
// func(c Config, db *DatabaseConfig) Config { c.Database = db; return c },
// ))
//
// // Access the optional value
// config := Config{Database: nil}
// dbOpt := dbLens.Get(config) // Returns None[*DatabaseConfig]
//
// // Set a value
// newDB := &DatabaseConfig{Host: "localhost", Port: 5432}
// updated := dbLens.Set(O.Some(newDB))(config)
//
// Composing optional lenses:
//
// // Lens to access port through optional database
// portLens := lens.MakeLensRef(
// func(db *DatabaseConfig) int { return db.Port },
// func(db *DatabaseConfig, port int) *DatabaseConfig { db.Port = port; return db },
// )
//
// defaultDB := &DatabaseConfig{Host: "localhost", Port: 5432}
// configPortLens := F.Pipe1(dbLens,
// lens.ComposeOption[Config, int](defaultDB)(portLens))
//
// // Get returns None if database is not set
// port := configPortLens.Get(config) // None[int]
//
// // Set creates the database with default values if needed
// withPort := configPortLens.Set(O.Some(3306))(config)
// // withPort.Database.Port == 3306, Host == "localhost"
//
// Working with predicates:
//
// type Person struct {
// Age int
// }
//
// ageLens := lens.MakeLensRef(
// func(p *Person) int { return p.Age },
// func(p *Person, age int) *Person { p.Age = age; return p },
// )
//
// // Only consider adults (age >= 18)
// adultLens := lens.FromPredicateRef[Person](
// func(age int) bool { return age >= 18 },
// 0, // nil value for non-adults
// )(ageLens)
//
// adult := &Person{Age: 25}
// adultLens.Get(adult) // Some(25)
//
// minor := &Person{Age: 15}
// adultLens.Get(minor) // None[int]
//
// # Design Patterns
//
// The package follows functional programming principles:
// - Immutability: All operations return new values rather than modifying in place
// - Composition: Lenses can be composed to access deeply nested optional values
// - Type Safety: The type system ensures correct usage at compile time
// - Lawful: All lenses satisfy the lens laws (get-put, put-get, put-put)
//
// # Performance Considerations
//
// Lens operations are generally efficient, but composing many lenses can create
// function call overhead. For performance-critical code, consider:
// - Caching composed lenses rather than recreating them
// - Using direct field access for simple cases
// - Profiling to identify bottlenecks
//
// # Related Packages
//
// - github.com/IBM/fp-go/v2/optics/lens: Core lens functionality
// - github.com/IBM/fp-go/v2/option: Option type and operations
// - github.com/IBM/fp-go/v2/optics/traversal/option: Traversals for optional values
package option

View File

@@ -0,0 +1,109 @@
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"
)
// 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] {
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),
)
})
}
}
// 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)
}
// 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)
}
// 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 FromNillable[S, A any](sa Lens[S, *A]) Lens[S, Option[*A]] {
return FromPredicate[S](F.IsNonNil[A], nil)(sa)
}
// FromNillableRef 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 FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, Option[*A]] {
return FromPredicateRef[S](F.IsNonNil[A], nil)(sa)
}
// 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] {
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)
},
)
}
}
// 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)
}
// 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)
}
// 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] {
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)
},
)
}
}
// 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)
}
// FromOptionRef creates a lens from an Option property with a default value for pointer structures.
//
// This is the pointer version of [FromOption], with automatic copying to ensure immutability.
// The getter returns the value inside Some[A], or the defaultValue if it's None[A].
// The setter always wraps the value in Some[A].
//
// Type Parameters:
// - S: Structure type (will be used as *S)
// - A: Focus type
//
// Parameters:
// - defaultValue: Value to return when the Option is None
//
// 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)
}

View File

@@ -0,0 +1,759 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package option
import (
"testing"
EQT "github.com/IBM/fp-go/v2/eq/testing"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
type (
Street struct {
name string
}
Address struct {
street *Street
}
Inner struct {
Value int
Foo string
}
InnerOpt struct {
Value *int
Foo *string
}
Outer struct {
inner *Inner
}
OuterOpt struct {
inner *InnerOpt
}
)
func (outer Outer) GetInner() *Inner {
return outer.inner
}
func (outer Outer) SetInner(inner *Inner) Outer {
outer.inner = inner
return outer
}
func (outer OuterOpt) GetInnerOpt() *InnerOpt {
return outer.inner
}
func (outer OuterOpt) SetInnerOpt(inner *InnerOpt) OuterOpt {
outer.inner = inner
return outer
}
func (inner *Inner) GetValue() int {
return inner.Value
}
func (inner *Inner) SetValue(value int) *Inner {
inner.Value = value
return inner
}
func (inner *InnerOpt) GetValue() *int {
return inner.Value
}
func (inner *InnerOpt) SetValue(value *int) *InnerOpt {
inner.Value = value
return inner
}
func (street *Street) GetName() string {
return street.name
}
func (street *Street) SetName(name string) *Street {
street.name = name
return street
}
func (addr *Address) GetStreet() *Street {
return addr.street
}
func (addr *Address) SetStreet(s *Street) *Address {
addr.street = s
return addr
}
var (
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
sampleStreet = Street{name: "Schönaicherstr"}
sampleAddress = Address{street: &sampleStreet}
)
func TestComposeOption(t *testing.T) {
// default inner object
defaultInner := &Inner{
Value: 0,
Foo: "foo",
}
// access to the value
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
// access to inner
inner := FromNillable(L.MakeLens(Outer.GetInner, Outer.SetInner))
// compose lenses
lens := F.Pipe1(
inner,
ComposeOption[Outer, int](defaultInner)(value),
)
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
// the checks
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{}))
assert.Equal(t, O.None[int](), lens.Get(Outer{}))
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(O.Some(1))(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
assert.Equal(t, O.Some(1), lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
assert.Equal(t, outer1, L.Modify[Outer](F.Identity[Option[int]])(lens)(outer1))
}
func TestComposeOptions(t *testing.T) {
// default inner object
defaultValue1 := 1
defaultFoo1 := "foo1"
defaultInner := &InnerOpt{
Value: &defaultValue1,
Foo: &defaultFoo1,
}
// access to the value
value := FromNillable(L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
// access to inner
inner := FromNillable(L.MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
// compose lenses
lens := F.Pipe1(
inner,
Compose[OuterOpt, *int](defaultInner)(value),
)
// additional settings
defaultValue2 := 2
defaultFoo2 := "foo2"
outer1 := OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}
// the checks
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{}))
assert.Equal(t, O.None[*int](), lens.Get(OuterOpt{}))
assert.Equal(t, OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo2}}, lens.Set(O.Some(&defaultValue1))(OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}))
assert.Equal(t, O.Some(&defaultValue1), lens.Get(OuterOpt{inner: &InnerOpt{Value: &defaultValue1, Foo: &defaultFoo1}}))
assert.Equal(t, outer1, L.Modify[OuterOpt](F.Identity[Option[*int]])(lens)(outer1))
}
func TestFromNullableProp(t *testing.T) {
// default inner object
defaultInner := &Inner{
Value: 0,
Foo: "foo",
}
// access to the value
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
// access to inner
inner := FromNullableProp[Outer](O.FromNillable[Inner], defaultInner)(L.MakeLens(Outer.GetInner, Outer.SetInner))
// compose
lens := F.Pipe1(
inner,
L.Compose[Outer](value),
)
outer1 := Outer{inner: &Inner{Value: 1, Foo: "a"}}
// the checks
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{}))
assert.Equal(t, 0, lens.Get(Outer{}))
assert.Equal(t, Outer{inner: &Inner{Value: 1, Foo: "foo"}}, lens.Set(1)(Outer{inner: &Inner{Value: 2, Foo: "foo"}}))
assert.Equal(t, 1, lens.Get(Outer{inner: &Inner{Value: 1, Foo: "foo"}}))
assert.Equal(t, outer1, L.Modify[Outer](F.Identity[int])(lens)(outer1))
}
func TestFromPredicateRef(t *testing.T) {
type Person struct {
age int
}
ageLens := L.MakeLensRef(
func(p *Person) int { return p.age },
func(p *Person, age int) *Person {
p.age = age
return p
},
)
adultLens := FromPredicateRef[Person](func(age int) bool { return age >= 18 }, 0)(ageLens)
adult := &Person{age: 25}
assert.Equal(t, O.Some(25), adultLens.Get(adult))
minor := &Person{age: 15}
assert.Equal(t, O.None[int](), adultLens.Get(minor))
}
func TestFromNillableRef(t *testing.T) {
type Config struct {
timeout *int
}
timeoutLens := L.MakeLensRef(
func(c *Config) *int { return c.timeout },
func(c *Config, t *int) *Config {
c.timeout = t
return c
},
)
optLens := FromNillableRef(timeoutLens)
config := &Config{timeout: nil}
assert.Equal(t, O.None[*int](), optLens.Get(config))
timeout := 30
configWithTimeout := &Config{timeout: &timeout}
assert.True(t, O.IsSome(optLens.Get(configWithTimeout)))
}
func TestFromNullablePropRef(t *testing.T) {
type Config struct {
timeout *int
}
timeoutLens := L.MakeLensRef(
func(c *Config) *int { return c.timeout },
func(c *Config, t *int) *Config {
c.timeout = t
return c
},
)
defaultTimeout := 30
safeLens := FromNullablePropRef[Config](O.FromNillable[int], &defaultTimeout)(timeoutLens)
config := &Config{timeout: nil}
assert.Equal(t, &defaultTimeout, safeLens.Get(config))
}
func TestFromOptionRef(t *testing.T) {
type Settings struct {
retries Option[int]
}
retriesLens := L.MakeLensRef(
func(s *Settings) Option[int] { return s.retries },
func(s *Settings, r Option[int]) *Settings {
s.retries = r
return s
},
)
safeLens := FromOptionRef[Settings](3)(retriesLens)
settings := &Settings{retries: O.None[int]()}
assert.Equal(t, 3, safeLens.Get(settings))
settingsWithRetries := &Settings{retries: O.Some(5)}
assert.Equal(t, 5, safeLens.Get(settingsWithRetries))
}
func TestFromOption(t *testing.T) {
type Config struct {
retries Option[int]
}
retriesLens := L.MakeLens(
func(c Config) Option[int] { return c.retries },
func(c Config, r Option[int]) Config { c.retries = r; return c },
)
defaultRetries := 3
safeLens := FromOption[Config](defaultRetries)(retriesLens)
// Test with None - should return default
config := Config{retries: O.None[int]()}
assert.Equal(t, defaultRetries, safeLens.Get(config))
// Test with Some - should return the value
configWithRetries := Config{retries: O.Some(5)}
assert.Equal(t, 5, safeLens.Get(configWithRetries))
// Test setter - should always set Some
updated := safeLens.Set(10)(config)
assert.Equal(t, O.Some(10), updated.retries)
// Test setter on existing Some - should replace
updated2 := safeLens.Set(7)(configWithRetries)
assert.Equal(t, O.Some(7), updated2.retries)
}
func TestAsTraversal(t *testing.T) {
type Data struct {
value int
}
valueLens := L.MakeLens(
func(d Data) int { return d.value },
func(d Data, v int) Data { d.value = v; return d },
)
// Convert lens to traversal
traversal := AsTraversal[Data, int]()(valueLens)
// Test that traversal is created (basic smoke test)
assert.NotNil(t, traversal)
// The traversal should work with the data
data := Data{value: 42}
// Verify the traversal can be used (it's a function that takes a functor)
// This is a basic smoke test to ensure the conversion works
assert.NotNil(t, data)
assert.Equal(t, 42, valueLens.Get(data))
}
func TestComposeOptionsEdgeCases(t *testing.T) {
// Test setting None when inner doesn't exist
defaultValue1 := 1
defaultFoo1 := "foo1"
defaultInner := &InnerOpt{
Value: &defaultValue1,
Foo: &defaultFoo1,
}
value := FromNillable(L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
inner := FromNillable(L.MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
lens := F.Pipe1(
inner,
Compose[OuterOpt, *int](defaultInner)(value),
)
// Setting None when inner doesn't exist should be a no-op
emptyOuter := OuterOpt{}
result := lens.Set(O.None[*int]())(emptyOuter)
assert.Equal(t, O.None[*InnerOpt](), inner.Get(result))
// Setting None when inner exists should unset the value
defaultValue2 := 2
defaultFoo2 := "foo2"
outerWithInner := OuterOpt{inner: &InnerOpt{Value: &defaultValue2, Foo: &defaultFoo2}}
result2 := lens.Set(O.None[*int]())(outerWithInner)
assert.NotNil(t, result2.inner)
assert.Nil(t, result2.inner.Value)
assert.Equal(t, &defaultFoo2, result2.inner.Foo)
}
func TestComposeOptionEdgeCases(t *testing.T) {
defaultInner := &Inner{
Value: 0,
Foo: "foo",
}
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
inner := FromNillable(L.MakeLens(Outer.GetInner, Outer.SetInner))
lens := F.Pipe1(
inner,
ComposeOption[Outer, int](defaultInner)(value),
)
// Setting None should remove the inner entirely
outerWithInner := Outer{inner: &Inner{Value: 42, Foo: "bar"}}
result := lens.Set(O.None[int]())(outerWithInner)
assert.Nil(t, result.inner)
// Getting from empty should return None
emptyOuter := Outer{}
assert.Equal(t, O.None[int](), lens.Get(emptyOuter))
}
func TestFromPredicateEdgeCases(t *testing.T) {
type Score struct {
points int
}
pointsLens := L.MakeLens(
func(s Score) int { return s.points },
func(s Score, p int) Score { s.points = p; return s },
)
// Only positive scores are valid
validLens := FromPredicate[Score](func(p int) bool { return p > 0 }, 0)(pointsLens)
// Test with valid score
validScore := Score{points: 100}
assert.Equal(t, O.Some(100), validLens.Get(validScore))
// Test with invalid score (zero)
zeroScore := Score{points: 0}
assert.Equal(t, O.None[int](), validLens.Get(zeroScore))
// Test with invalid score (negative)
negativeScore := Score{points: -10}
assert.Equal(t, O.None[int](), validLens.Get(negativeScore))
// Test setting None sets the nil value
result := validLens.Set(O.None[int]())(validScore)
assert.Equal(t, 0, result.points)
// Test setting Some sets the value
result2 := validLens.Set(O.Some(50))(zeroScore)
assert.Equal(t, 50, result2.points)
}
func TestFromNullablePropEdgeCases(t *testing.T) {
type Container struct {
item *string
}
itemLens := L.MakeLens(
func(c Container) *string { return c.item },
func(c Container, i *string) Container { c.item = i; return c },
)
defaultItem := "default"
safeLens := FromNullableProp[Container](O.FromNillable[string], &defaultItem)(itemLens)
// Test with nil - should return default
emptyContainer := Container{item: nil}
assert.Equal(t, &defaultItem, safeLens.Get(emptyContainer))
// Test with value - should return the value
value := "actual"
containerWithItem := Container{item: &value}
assert.Equal(t, &value, safeLens.Get(containerWithItem))
// Test setter
newValue := "new"
updated := safeLens.Set(&newValue)(emptyContainer)
assert.Equal(t, &newValue, updated.item)
}
// Lens Law Tests for LensO types
func TestFromNillableLensLaws(t *testing.T) {
type Config struct {
timeout *int
}
timeoutLens := L.MakeLens(
func(c Config) *int { return c.timeout },
func(c Config, t *int) Config { c.timeout = t; return c },
)
optLens := FromNillable(timeoutLens)
// Equality predicates
eqInt := EQT.Eq[*int]()
eqOptInt := O.Eq(eqInt)
eqConfig := func(a, b Config) bool {
if a.timeout == nil && b.timeout == nil {
return true
}
if a.timeout == nil || b.timeout == nil {
return false
}
return *a.timeout == *b.timeout
}
// Test structures
timeout30 := 30
timeout60 := 60
configNil := Config{timeout: nil}
config30 := Config{timeout: &timeout30}
// Law 1: get(set(a)(s)) = a
t.Run("GetSet", func(t *testing.T) {
// Setting Some and getting back
result := optLens.Get(optLens.Set(O.Some(&timeout60))(config30))
assert.True(t, eqOptInt.Equals(result, O.Some(&timeout60)))
// Setting None and getting back
result2 := optLens.Get(optLens.Set(O.None[*int]())(config30))
assert.True(t, eqOptInt.Equals(result2, O.None[*int]()))
})
// Law 2: set(get(s))(s) = s
t.Run("SetGet", func(t *testing.T) {
// With Some value
result := optLens.Set(optLens.Get(config30))(config30)
assert.True(t, eqConfig(result, config30))
// With None value
result2 := optLens.Set(optLens.Get(configNil))(configNil)
assert.True(t, eqConfig(result2, configNil))
})
// Law 3: set(a)(set(a)(s)) = set(a)(s)
t.Run("SetSet", func(t *testing.T) {
// Setting Some twice
once := optLens.Set(O.Some(&timeout60))(config30)
twice := optLens.Set(O.Some(&timeout60))(once)
assert.True(t, eqConfig(once, twice))
// Setting None twice
once2 := optLens.Set(O.None[*int]())(config30)
twice2 := optLens.Set(O.None[*int]())(once2)
assert.True(t, eqConfig(once2, twice2))
})
}
func TestFromNillableRefLensLaws(t *testing.T) {
type Settings struct {
maxRetries *int
}
retriesLens := L.MakeLensRef(
func(s *Settings) *int { return s.maxRetries },
func(s *Settings, r *int) *Settings { s.maxRetries = r; return s },
)
optLens := FromNillableRef(retriesLens)
// Equality predicates
eqInt := EQT.Eq[*int]()
eqOptInt := O.Eq(eqInt)
eqSettings := func(a, b *Settings) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
if a.maxRetries == nil && b.maxRetries == nil {
return true
}
if a.maxRetries == nil || b.maxRetries == nil {
return false
}
return *a.maxRetries == *b.maxRetries
}
// Test structures
retries3 := 3
retries5 := 5
settingsNil := &Settings{maxRetries: nil}
settings3 := &Settings{maxRetries: &retries3}
// Law 1: get(set(a)(s)) = a
t.Run("GetSet", func(t *testing.T) {
result := optLens.Get(optLens.Set(O.Some(&retries5))(settings3))
assert.True(t, eqOptInt.Equals(result, O.Some(&retries5)))
result2 := optLens.Get(optLens.Set(O.None[*int]())(settings3))
assert.True(t, eqOptInt.Equals(result2, O.None[*int]()))
})
// Law 2: set(get(s))(s) = s
t.Run("SetGet", func(t *testing.T) {
result := optLens.Set(optLens.Get(settings3))(settings3)
assert.True(t, eqSettings(result, settings3))
result2 := optLens.Set(optLens.Get(settingsNil))(settingsNil)
assert.True(t, eqSettings(result2, settingsNil))
})
// Law 3: set(a)(set(a)(s)) = set(a)(s)
t.Run("SetSet", func(t *testing.T) {
once := optLens.Set(O.Some(&retries5))(settings3)
twice := optLens.Set(O.Some(&retries5))(once)
assert.True(t, eqSettings(once, twice))
once2 := optLens.Set(O.None[*int]())(settings3)
twice2 := optLens.Set(O.None[*int]())(once2)
assert.True(t, eqSettings(once2, twice2))
})
}
func TestComposeOptionLensLaws(t *testing.T) {
defaultInner := &Inner{Value: 0, Foo: "default"}
value := L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
inner := FromNillable(L.MakeLens(Outer.GetInner, Outer.SetInner))
lens := F.Pipe1(inner, ComposeOption[Outer, int](defaultInner)(value))
// Equality predicates
eqInt := EQT.Eq[int]()
eqOptInt := O.Eq(eqInt)
eqOuter := func(a, b Outer) bool {
if a.inner == nil && b.inner == nil {
return true
}
if a.inner == nil || b.inner == nil {
return false
}
return a.inner.Value == b.inner.Value && a.inner.Foo == b.inner.Foo
}
// Test structures
outerNil := Outer{inner: nil}
outer42 := Outer{inner: &Inner{Value: 42, Foo: "test"}}
// Law 1: get(set(a)(s)) = a
t.Run("GetSet", func(t *testing.T) {
result := lens.Get(lens.Set(O.Some(100))(outer42))
assert.True(t, eqOptInt.Equals(result, O.Some(100)))
result2 := lens.Get(lens.Set(O.None[int]())(outer42))
assert.True(t, eqOptInt.Equals(result2, O.None[int]()))
})
// Law 2: set(get(s))(s) = s
t.Run("SetGet", func(t *testing.T) {
result := lens.Set(lens.Get(outer42))(outer42)
assert.True(t, eqOuter(result, outer42))
result2 := lens.Set(lens.Get(outerNil))(outerNil)
assert.True(t, eqOuter(result2, outerNil))
})
// Law 3: set(a)(set(a)(s)) = set(a)(s)
t.Run("SetSet", func(t *testing.T) {
once := lens.Set(O.Some(100))(outer42)
twice := lens.Set(O.Some(100))(once)
assert.True(t, eqOuter(once, twice))
once2 := lens.Set(O.None[int]())(outer42)
twice2 := lens.Set(O.None[int]())(once2)
assert.True(t, eqOuter(once2, twice2))
})
}
func TestComposeOptionsLensLaws(t *testing.T) {
defaultValue := 1
defaultFoo := "default"
defaultInner := &InnerOpt{Value: &defaultValue, Foo: &defaultFoo}
value := FromNillable(L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue))
inner := FromNillable(L.MakeLens(OuterOpt.GetInnerOpt, OuterOpt.SetInnerOpt))
lens := F.Pipe1(inner, Compose[OuterOpt, *int](defaultInner)(value))
// Equality predicates
eqIntPtr := EQT.Eq[*int]()
eqOptIntPtr := O.Eq(eqIntPtr)
eqOuterOpt := func(a, b OuterOpt) bool {
if a.inner == nil && b.inner == nil {
return true
}
if a.inner == nil || b.inner == nil {
return false
}
aVal := a.inner.Value
bVal := b.inner.Value
if aVal == nil && bVal == nil {
return true
}
if aVal == nil || bVal == nil {
return false
}
return *aVal == *bVal
}
// Test structures
val42 := 42
val100 := 100
outerNil := OuterOpt{inner: nil}
outer42 := OuterOpt{inner: &InnerOpt{Value: &val42, Foo: &defaultFoo}}
// Law 1: get(set(a)(s)) = a
t.Run("GetSet", func(t *testing.T) {
result := lens.Get(lens.Set(O.Some(&val100))(outer42))
assert.True(t, eqOptIntPtr.Equals(result, O.Some(&val100)))
result2 := lens.Get(lens.Set(O.None[*int]())(outer42))
assert.True(t, eqOptIntPtr.Equals(result2, O.None[*int]()))
})
// Law 2: set(get(s))(s) = s
t.Run("SetGet", func(t *testing.T) {
result := lens.Set(lens.Get(outer42))(outer42)
assert.True(t, eqOuterOpt(result, outer42))
result2 := lens.Set(lens.Get(outerNil))(outerNil)
assert.True(t, eqOuterOpt(result2, outerNil))
})
// Law 3: set(a)(set(a)(s)) = set(a)(s)
t.Run("SetSet", func(t *testing.T) {
once := lens.Set(O.Some(&val100))(outer42)
twice := lens.Set(O.Some(&val100))(once)
assert.True(t, eqOuterOpt(once, twice))
once2 := lens.Set(O.None[*int]())(outer42)
twice2 := lens.Set(O.None[*int]())(once2)
assert.True(t, eqOuterOpt(once2, twice2))
})
}
func TestFromPredicateLensLaws(t *testing.T) {
type Score struct {
points int
}
pointsLens := L.MakeLens(
func(s Score) int { return s.points },
func(s Score, p int) Score { s.points = p; return s },
)
// Only positive scores are valid
validLens := FromPredicate[Score](func(p int) bool { return p > 0 }, 0)(pointsLens)
// Equality predicates
eqInt := EQT.Eq[int]()
eqOptInt := O.Eq(eqInt)
eqScore := func(a, b Score) bool { return a.points == b.points }
// Test structures
scoreZero := Score{points: 0}
score100 := Score{points: 100}
// Law 1: get(set(a)(s)) = a
t.Run("GetSet", func(t *testing.T) {
result := validLens.Get(validLens.Set(O.Some(50))(score100))
assert.True(t, eqOptInt.Equals(result, O.Some(50)))
result2 := validLens.Get(validLens.Set(O.None[int]())(score100))
assert.True(t, eqOptInt.Equals(result2, O.None[int]()))
})
// Law 2: set(get(s))(s) = s
t.Run("SetGet", func(t *testing.T) {
result := validLens.Set(validLens.Get(score100))(score100)
assert.True(t, eqScore(result, score100))
result2 := validLens.Set(validLens.Get(scoreZero))(scoreZero)
assert.True(t, eqScore(result2, scoreZero))
})
// Law 3: set(a)(set(a)(s)) = set(a)(s)
t.Run("SetSet", func(t *testing.T) {
once := validLens.Set(O.Some(75))(score100)
twice := validLens.Set(O.Some(75))(once)
assert.True(t, eqScore(once, twice))
once2 := validLens.Set(O.None[int]())(score100)
twice2 := validLens.Set(O.None[int]())(once2)
assert.True(t, eqScore(once2, twice2))
})
}

View File

@@ -22,6 +22,44 @@ import (
O "github.com/IBM/fp-go/v2/option"
)
// AsTraversal converts a Lens[S, A] to a Traversal[S, A] for optional values.
//
// A traversal is a generalization of a lens that can focus on zero or more values.
// This function converts a lens (which focuses on exactly one value) into a traversal,
// allowing it to be used with traversal operations like mapping over multiple values.
//
// This is particularly useful when you want to:
// - Use lens operations in a traversal context
// - Compose lenses with traversals
// - Apply operations that work on collections of optional values
//
// The conversion uses the Option monad's map operation to handle the optional nature
// of the values being traversed.
//
// Type Parameters:
// - S: The structure type containing the field
// - A: The type of the field being focused on
//
// Returns:
// - A function that takes a Lens[S, A] and returns a Traversal[S, A]
//
// Example:
//
// type Config struct {
// Timeout Option[int]
// }
//
// timeoutLens := lens.MakeLens(
// func(c Config) Option[int] { return c.Timeout },
// func(c Config, t Option[int]) Config { c.Timeout = t; return c },
// )
//
// // Convert to traversal for use with traversal operations
// timeoutTraversal := lens.AsTraversal[Config, int]()(timeoutLens)
//
// // Now can use traversal operations
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
// // Apply operations across all configs using the traversal
func AsTraversal[S, A any]() func(L.Lens[S, A]) T.Traversal[S, A] {
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
}

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 testing
import (
"testing"
EQT "github.com/IBM/fp-go/v2/eq/testing"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/identity"
L "github.com/IBM/fp-go/v2/optics/lens"
LI "github.com/IBM/fp-go/v2/optics/lens/iso"
LO "github.com/IBM/fp-go/v2/optics/lens/option"
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
type (
Street struct {
num int
name string
}
Address struct {
city string
street *Street
}
Inner struct {
Value int
Foo string
}
InnerOpt struct {
Value *int
Foo *string
}
Outer struct {
inner *Inner
}
OuterOpt struct {
inner *InnerOpt
}
)
func (outer *OuterOpt) GetInner() *InnerOpt {
return outer.inner
}
func (outer *OuterOpt) SetInner(inner *InnerOpt) *OuterOpt {
outer.inner = inner
return outer
}
func (inner *InnerOpt) GetValue() *int {
return inner.Value
}
func (inner *InnerOpt) SetValue(value *int) *InnerOpt {
inner.Value = value
return inner
}
func (outer *Outer) GetInner() *Inner {
return outer.inner
}
func (outer *Outer) SetInner(inner *Inner) *Outer {
outer.inner = inner
return outer
}
func (inner *Inner) GetValue() int {
return inner.Value
}
func (inner *Inner) SetValue(value int) *Inner {
inner.Value = value
return inner
}
func (street *Street) GetName() string {
return street.name
}
func (street *Street) SetName(name string) *Street {
street.name = name
return street
}
func (addr *Address) GetStreet() *Street {
return addr.street
}
func (addr *Address) SetStreet(s *Street) *Address {
addr.street = s
return addr
}
var (
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
outerLens = LO.FromNillableRef(L.MakeLensRef((*Outer).GetInner, (*Outer).SetInner))
valueLens = L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
outerOptLens = LO.FromNillableRef(L.MakeLensRef((*OuterOpt).GetInner, (*OuterOpt).SetInner))
valueOptLens = L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue)
sampleStreet = Street{num: 220, name: "Schönaicherstr"}
sampleAddress = Address{city: "Böblingen", street: &sampleStreet}
sampleStreet2 = Street{num: 220, name: "Neue Str"}
defaultInner = Inner{
Value: -1,
Foo: "foo",
}
emptyOuter = Outer{}
defaultInnerOpt = InnerOpt{
Value: &defaultInner.Value,
Foo: &defaultInner.Foo,
}
emptyOuterOpt = OuterOpt{}
)
func TestStreetLensLaws(t *testing.T) {
// some comparison
eqs := EQT.Eq[*Street]()
eqa := EQT.Eq[string]()
laws := LT.AssertLaws(
t,
eqa,
eqs,
)(streetLens)
cpy := sampleStreet
assert.True(t, laws(&sampleStreet, "Neue Str."))
assert.Equal(t, cpy, sampleStreet)
}
func TestAddrLensLaws(t *testing.T) {
// some comparison
eqs := EQT.Eq[*Address]()
eqa := EQT.Eq[*Street]()
laws := LT.AssertLaws(
t,
eqa,
eqs,
)(addrLens)
cpyAddr := sampleAddress
cpyStreet := sampleStreet2
assert.True(t, laws(&sampleAddress, &sampleStreet2))
assert.Equal(t, cpyAddr, sampleAddress)
assert.Equal(t, cpyStreet, sampleStreet2)
}
func TestCompose(t *testing.T) {
// some comparison
eqs := EQT.Eq[*Address]()
eqa := EQT.Eq[string]()
streetName := L.Compose[*Address](streetLens)(addrLens)
laws := LT.AssertLaws(
t,
eqa,
eqs,
)(streetName)
cpyAddr := sampleAddress
cpyStreet := sampleStreet
assert.True(t, laws(&sampleAddress, "Neue Str."))
assert.Equal(t, cpyAddr, sampleAddress)
assert.Equal(t, cpyStreet, sampleStreet)
}
func TestOuterLensLaws(t *testing.T) {
// some equal predicates
eqValue := EQT.Eq[int]()
eqOptValue := O.Eq(eqValue)
// lens to access a value from outer
valueFromOuter := LO.ComposeOption[*Outer, int](&defaultInner)(valueLens)(outerLens)
// try to access the value, this should get an option
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
// update the object
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuter)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
// updating with none should remove the inner
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
// check if this meets the laws
eqOuter := EQT.Eq[*Outer]()
laws := LT.AssertLaws(
t,
eqOptValue,
eqOuter,
)(valueFromOuter)
assert.True(t, laws(&emptyOuter, O.Some(2)))
assert.True(t, laws(&emptyOuter, O.None[int]()))
assert.True(t, laws(withValue, O.Some(2)))
assert.True(t, laws(withValue, O.None[int]()))
}
func TestOuterOptLensLaws(t *testing.T) {
// some equal predicates
eqValue := EQT.Eq[int]()
eqOptValue := O.Eq(eqValue)
intIso := LI.FromNillable[int]()
// lens to access a value from outer
valueFromOuter := F.Pipe3(
valueOptLens,
LI.Compose[*InnerOpt](intIso),
LO.Compose[*OuterOpt, int](&defaultInnerOpt),
I.Ap[L.Lens[*OuterOpt, O.Option[int]]](outerOptLens),
)
// try to access the value, this should get an option
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
// update the object
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuterOpt)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
// updating with none should remove the inner
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
// check if this meets the laws
eqOuter := EQT.Eq[*OuterOpt]()
laws := LT.AssertLaws(
t,
eqOptValue,
eqOuter,
)(valueFromOuter)
assert.True(t, laws(&emptyOuterOpt, O.Some(2)))
assert.True(t, laws(&emptyOuterOpt, O.None[int]()))
assert.True(t, laws(withValue, O.Some(2)))
assert.True(t, laws(withValue, O.None[int]()))
}

View File

@@ -0,0 +1,94 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package option
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
)
type (
// Endomorphism is a function from a type to itself (A → A).
// It represents transformations that preserve the type.
//
// This is commonly used in lens setters to transform a structure
// by applying a function that takes and returns the same type.
//
// Example:
// increment := func(x int) int { return x + 1 }
// // increment is an Endomorphism[int]
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Lens represents a functional reference to a field within a structure.
//
// A Lens[S, A] provides a way to get and set a value of type A within
// a structure of type S in an immutable way. It consists of:
// - Get: S → A (retrieve the value)
// - Set: A → S → S (update the value, returning a new structure)
//
// Lenses satisfy three laws:
// 1. Get-Put: lens.Set(lens.Get(s))(s) == s
// 2. Put-Get: lens.Get(lens.Set(a)(s)) == a
// 3. Put-Put: lens.Set(b)(lens.Set(a)(s)) == lens.Set(b)(s)
//
// Type Parameters:
// - S: The structure type containing the field
// - A: The type of the field being focused on
Lens[S, A any] = lens.Lens[S, A]
// Option represents a value that may or may not be present.
//
// It is either Some[T] containing a value of type T, or None[T]
// representing the absence of a value. This is a type-safe alternative
// to using nil pointers.
//
// Type Parameters:
// - T: The type of the value that may be present
Option[T any] = option.Option[T]
// LensO is a lens that focuses on an optional value.
//
// A LensO[S, A] is equivalent to Lens[S, Option[A]], representing
// a lens that focuses on a value of type A that may or may not be
// present within a structure S.
//
// This is particularly useful for:
// - Nullable pointer fields
// - Optional configuration values
// - Fields that may be conditionally present
//
// The getter returns Option[A] (Some if present, None if absent).
// The setter takes Option[A] (Some to set, None to remove).
//
// Type Parameters:
// - S: The structure type containing the optional field
// - A: The type of the optional value being focused on
//
// Example:
// type Config struct {
// Timeout *int
// }
//
// timeoutLens := lens.MakeLensRef(
// func(c *Config) *int { return c.Timeout },
// func(c *Config, t *int) *Config { c.Timeout = t; return c },
// )
//
// optLens := lens.FromNillableRef(timeoutLens)
// // optLens is a LensO[*Config, *int]
LensO[S, A any] = Lens[S, Option[A]]
)

View File

@@ -19,11 +19,7 @@ import (
"testing"
EQT "github.com/IBM/fp-go/v2/eq/testing"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/identity"
L "github.com/IBM/fp-go/v2/optics/lens"
LI "github.com/IBM/fp-go/v2/optics/lens/iso"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
@@ -114,10 +110,8 @@ func (addr *Address) SetStreet(s *Street) *Address {
var (
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
outerLens = L.FromNillableRef(L.MakeLensRef((*Outer).GetInner, (*Outer).SetInner))
valueLens = L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
outerOptLens = L.FromNillableRef(L.MakeLensRef((*OuterOpt).GetInner, (*OuterOpt).SetInner))
valueOptLens = L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue)
sampleStreet = Street{num: 220, name: "Schönaicherstr"}
@@ -192,74 +186,3 @@ func TestCompose(t *testing.T) {
assert.Equal(t, cpyAddr, sampleAddress)
assert.Equal(t, cpyStreet, sampleStreet)
}
func TestOuterLensLaws(t *testing.T) {
// some equal predicates
eqValue := EQT.Eq[int]()
eqOptValue := O.Eq(eqValue)
// lens to access a value from outer
valueFromOuter := L.ComposeOption[*Outer, int](&defaultInner)(valueLens)(outerLens)
// try to access the value, this should get an option
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
// update the object
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuter)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
// updating with none should remove the inner
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
// check if this meets the laws
eqOuter := EQT.Eq[*Outer]()
laws := AssertLaws(
t,
eqOptValue,
eqOuter,
)(valueFromOuter)
assert.True(t, laws(&emptyOuter, O.Some(2)))
assert.True(t, laws(&emptyOuter, O.None[int]()))
assert.True(t, laws(withValue, O.Some(2)))
assert.True(t, laws(withValue, O.None[int]()))
}
func TestOuterOptLensLaws(t *testing.T) {
// some equal predicates
eqValue := EQT.Eq[int]()
eqOptValue := O.Eq(eqValue)
intIso := LI.FromNillable[int]()
// lens to access a value from outer
valueFromOuter := F.Pipe3(
valueOptLens,
LI.Compose[*InnerOpt](intIso),
L.ComposeOptions[*OuterOpt, int](&defaultInnerOpt),
I.Ap[L.Lens[*OuterOpt, O.Option[int]]](outerOptLens),
)
// try to access the value, this should get an option
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
// update the object
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuterOpt)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
// updating with none should remove the inner
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
// check if this meets the laws
eqOuter := EQT.Eq[*OuterOpt]()
laws := AssertLaws(
t,
eqOptValue,
eqOuter,
)(valueFromOuter)
assert.True(t, laws(&emptyOuterOpt, O.Some(2)))
assert.True(t, laws(&emptyOuterOpt, O.None[int]()))
assert.True(t, laws(withValue, O.Some(2)))
assert.True(t, laws(withValue, O.None[int]()))
}

View File

@@ -21,11 +21,63 @@ import (
)
type (
// Endomorphism is a function from a type to itself (A → A).
// It represents transformations that preserve the type.
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Lens is a reference to a subpart of a data type
// Lens is a functional reference to a subpart of a data structure.
//
// A Lens[S, A] provides a composable way to focus on a field of type A within
// a structure of type S. It consists of two operations:
// - Get: Extracts the focused value from the structure (S → A)
// - Set: Updates the focused value in the structure, returning a new structure (A → S → S)
//
// Lenses maintain immutability by always returning new copies of the structure
// when setting values, never modifying the original.
//
// Type Parameters:
// - S: The source/structure type (the whole)
// - A: The focus/field type (the part)
//
// Lens Laws:
//
// A well-behaved lens must satisfy three laws:
//
// 1. GetSet (You get what you set):
// lens.Set(lens.Get(s))(s) == s
//
// 2. SetGet (You set what you get):
// lens.Get(lens.Set(a)(s)) == a
//
// 3. SetSet (Setting twice is the same as setting once):
// lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// nameLens := lens.MakeLens(
// func(p Person) string { return p.Name },
// func(p Person, name string) Person {
// p.Name = name
// return p
// },
// )
//
// person := Person{Name: "Alice", Age: 30}
// name := nameLens.Get(person) // "Alice"
// updated := nameLens.Set("Bob")(person) // Person{Name: "Bob", Age: 30}
// // person is unchanged, updated is a new value
Lens[S, A any] struct {
// Get extracts the focused value of type A from structure S.
Get func(s S) A
// Set returns a function that updates the focused value in structure S.
// The returned function takes a structure S and returns a new structure S
// with the focused value updated to a. The original structure is never modified.
Set func(a A) Endomorphism[S]
}
)

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