1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-24 12:57:26 +02:00

Compare commits

..

50 Commits

Author SHA1 Message Date
Dr. Carsten Leue
c4cac1cb3e fix: add FromReaderResult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-21 16:44:51 +01:00
Dr. Carsten Leue
a3fdb03df4 fix: better assertions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-13 09:27:49 +01:00
Dr. Carsten Leue
47727fd514 fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:51:34 +01:00
Dr. Carsten Leue
ece7d088ea fix: add support for go 1.26
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:50:30 +01:00
Dr. Carsten Leue
13d25eca32 fix: add composition logic to Iso
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 10:46:41 +01:00
Dr. Carsten Leue
a68e32308d fix: add filterable to Either and Result
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-12 09:28:42 +01:00
Dr. Carsten Leue
61b948425b fix: cleaner use of Kleisli
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-11 16:24:11 +01:00
Dr. Carsten Leue
a276f3acff fix: add llms.txt
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 09:48:19 +01:00
Dr. Carsten Leue
8c656a4297 fix: more Alt tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-10 08:52:39 +01:00
Dr. Carsten Leue
bd9a642e93 fix: implement Alt for Codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 18:31:00 +01:00
Dr. Carsten Leue
3b55cae265 fix: implement alternative monoid for codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-05 09:59:17 +01:00
Dr. Carsten Leue
1472fa5a50 fix: add some more validation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-04 17:58:08 +01:00
Dr. Carsten Leue
49deb57d24 fix: OrElse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 17:54:43 +01:00
Dr. Carsten Leue
abb55ddbd0 fix: validation logic and ChainLeft
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-03 14:00:44 +01:00
Dr. Carsten Leue
f6b01dffdc fix: add ModifiyReaderIOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-02 09:09:04 +01:00
Dr. Carsten Leue
43b666edbb fix: add bind to codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-31 11:47:50 +01:00
Dr. Carsten Leue
e42d765852 fix: readeriooption
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-30 16:59:32 +01:00
Dr. Carsten Leue
d2da8a32b4 fix: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-30 11:45:45 +01:00
Dr. Carsten Leue
7484af664b fix: add IOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 17:12:27 +01:00
Dr. Carsten Leue
ae38e3f8f4 fix: add IOK to IORef
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 17:07:12 +01:00
Dr. Carsten Leue
e0f854bda3 fix: executes_all_IO_operations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 10:25:39 +01:00
Dr. Carsten Leue
34786c3cd8 fix: more tests and lens generation fix for prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-29 10:11:46 +01:00
Dr. Carsten Leue
a7aa7e3560 fix: better DI example
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 22:45:17 +01:00
Dr. Carsten Leue
ff2a4299b2 fix: add some useful lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 17:39:34 +01:00
Dr. Carsten Leue
edd66d63e6 fix: more codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 14:51:35 +01:00
Dr. Carsten Leue
909aec8eba fix: better sequence iter
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-26 10:41:25 +01:00
Obed Tetteh
da0344f9bd feat(iterator): add Last function with Option return type (#155)
- Add Last function to retrieve the final element from an iterator,
  returning Some(element) for non-empty sequences and None for empty ones.
- Includes tests covering simple types and  complex types
- Add documentation including example code
2026-01-26 09:04:51 +01:00
Dr. Carsten Leue
cd79dd56b9 fix: simplify tests a bit
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 17:56:28 +01:00
Dr. Carsten Leue
df07599a9e fix: some docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:40:45 +01:00
Dr. Carsten Leue
30ad0e4dd8 doc: add validation docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:26:53 +01:00
Dr. Carsten Leue
2374d7f1e4 fix: support unexported fields for lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:18:44 +01:00
Dr. Carsten Leue
eafc008798 fix: doc for lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:00:11 +01:00
Dr. Carsten Leue
46bf065e34 fix: migrate CLI to github.com/urfave/v3
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 12:56:23 +01:00
renovate[bot]
b4e303423b chore(deps): update actions/checkout action to v6.0.2 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 12:39:53 +01:00
renovate[bot]
7afc098f58 fix(deps): update go dependencies (major) (#144)
* fix(deps): update go dependencies

* fix: fix renovate config

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

---------

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:28:56 +01:00
Dr. Carsten Leue
617e43de19 fix: improve codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 11:12:40 +01:00
Dr. Carsten Leue
0f7a6c0589 fix: prism doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-22 13:57:07 +01:00
Dr. Carsten Leue
e7f78e1a33 fix: more codecs and cleanup of type hints
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-21 09:23:48 +01:00
Dr. Carsten Leue
6505ab1791 fix: improve Retry implementation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-20 09:50:20 +01:00
Dr. Carsten Leue
cfa48985ec fix: WithLocal
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-19 18:36:02 +01:00
Dr. Carsten Leue
677523b70f fix: add nested reader transformer
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-19 16:34:39 +01:00
Dr. Carsten Leue
8243242cf1 doc: explain use of ReaderIOResult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-19 10:10:43 +01:00
Dr. Carsten Leue
9021a8e274 fix: simplify tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-18 18:33:15 +01:00
Dr. Carsten Leue
f3128e887b fix: more doc and tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-18 17:43:57 +01:00
Dr. Carsten Leue
4583694211 fix: more type magic
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-18 00:21:48 +01:00
Dr. Carsten Leue
b87c20d139 fix: better reader abstraction
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-17 22:36:26 +01:00
Dr. Carsten Leue
9fd5b90138 fix: add codec and implement some helpers along the way
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-16 23:21:10 +01:00
Dr. Carsten Leue
cdc2041d8e fix: implement ReadIO consistently
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-15 13:07:19 +01:00
Dr. Carsten Leue
777fff9a5a fix: implement ReadIO
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-15 12:24:46 +01:00
Carsten Leue
8acea9043f fix: refactor circuitbreaker (#152)
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-15 11:36:32 +01:00
416 changed files with 87891 additions and 2504 deletions

View File

@@ -24,15 +24,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x', '1.26.x']
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true # Enable Go module caching
@@ -64,13 +64,13 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.24.x', '1.25.x']
go-version: ['1.24.x', '1.25.x', '1.26.x']
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
cache: true # Enable Go module caching
@@ -126,17 +126,17 @@ jobs:
steps:
# full checkout for semantic-release
- name: Full checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ env.LATEST_GO_VERSION }}
cache: true # Enable Go module caching

16
go.sum
View File

@@ -1,7 +1,3 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -10,20 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g=
github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=

View File

@@ -22,7 +22,8 @@
"matchDepTypes": [
"golang"
],
"enabled": false
"enabled": false,
"description": "Disable updates to the go directive in go.mod files - the directive identifies the minimum compatible Go version and should stay as small as possible for maximum compatibility"
},
{
"matchUpdateTypes": [

1
v2/.bobignore Normal file
View File

@@ -0,0 +1 @@
reflect\reflect.go

226
v2/AGENTS.md Normal file
View File

@@ -0,0 +1,226 @@
# Agent Guidelines for fp-go/v2
This document provides guidelines for AI agents working on the fp-go/v2 project.
## Documentation Standards
### Go Doc Comments
1. **Use Standard Go Doc Format**
- Do NOT use markdown-style links like `[text](url)`
- Use simple type references: `ReaderResult`, `Validate[I, A]`, `validation.Success`
- Go's documentation system will automatically create links
2. **Structure**
```go
// FunctionName does something useful.
//
// Longer description explaining the purpose and behavior.
//
// # Type Parameters
//
// - T: Description of type parameter
//
// # Parameters
//
// - param: Description of parameter
//
// # Returns
//
// - ReturnType: Description of return value
//
// # Example Usage
//
// code example here
//
// # See Also
//
// - RelatedFunction: Brief description
func FunctionName[T any](param T) ReturnType {
```
3. **Code Examples**
- Use idiomatic Go patterns
- Prefer `result.Eitherize1(strconv.Atoi)` over manual error handling
- Show realistic, runnable examples
### File Headers
Always include the Apache 2.0 license header:
```go
// 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.
```
## Testing Standards
### Test Structure
1. **Organize Tests by Category**
```go
func TestFunctionName_Success(t *testing.T) {
t.Run("specific success case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_Failure(t *testing.T) {
t.Run("specific failure case", func(t *testing.T) {
// test code
})
}
func TestFunctionName_EdgeCases(t *testing.T) {
// edge case tests
}
func TestFunctionName_Integration(t *testing.T) {
// integration tests
}
```
2. **Use Direct Assertions**
- Prefer: `assert.Equal(t, validation.Success(expected), actual)`
- Avoid: Verbose `either.MonadFold` patterns unless necessary
- Exception: When you need to verify pointer is not nil or extract specific fields
3. **Use Idiomatic Patterns**
- Use `result.Eitherize1` for converting `(T, error)` functions
- Use `result.Of` for success values
- Use `result.Left` for error values
### Test Coverage
Include tests for:
- **Success cases**: Normal operation with various input types
- **Failure cases**: Error handling and error preservation
- **Edge cases**: Nil, empty, zero values, boundary conditions
- **Integration**: Composition with other functions
- **Type safety**: Verify type parameters work correctly
- **Benchmarks**: Performance-critical paths
### Example Test Pattern
```go
func TestFromReaderResult_Success(t *testing.T) {
t.Run("converts successful ReaderResult", func(t *testing.T) {
// Arrange
parseIntRR := result.Eitherize1(strconv.Atoi)
validator := FromReaderResult[string, int](parseIntRR)
// Act
result := validator("42")(nil)
// Assert
assert.Equal(t, validation.Success(42), result)
})
}
```
## Code Style
### Functional Patterns
1. **Prefer Composition**
```go
validator := F.Pipe1(
FromReaderResult[string, int](parseIntRR),
Chain(validatePositive),
)
```
2. **Use Type-Safe Helpers**
- `result.Eitherize1` for `func(T) (R, error)`
- `result.Of` for wrapping success values
- `result.Left` for wrapping errors
3. **Avoid Verbose Patterns**
- Don't manually handle `(value, error)` tuples when helpers exist
- Don't use `either.MonadFold` in tests unless necessary
### Error Handling
1. **In Production Code**
- Use `validation.Success` for successful validations
- Use `validation.FailureWithMessage` for simple failures
- Use `validation.FailureWithError` to preserve error causes
2. **In Tests**
- Verify error messages and causes
- Check error context is preserved
- Test error accumulation when applicable
## Common Patterns
### Converting Error-Based Functions
```go
// Good: Use Eitherize1
parseIntRR := result.Eitherize1(strconv.Atoi)
// Avoid: Manual error handling
parseIntRR := func(input string) result.Result[int] {
val, err := strconv.Atoi(input)
if err != nil {
return result.Left[int](err)
}
return result.Of(val)
}
```
### Testing Validation Results
```go
// Good: Direct comparison
assert.Equal(t, validation.Success(42), result)
// Avoid: Verbose extraction (unless you need to verify specific fields)
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
```
### Documentation Examples
```go
// Good: Concise and idiomatic
// parseIntRR := result.Eitherize1(strconv.Atoi)
// validator := FromReaderResult[string, int](parseIntRR)
// Avoid: Verbose manual patterns
// parseIntRR := func(input string) result.Result[int] {
// val, err := strconv.Atoi(input)
// if err != nil {
// return result.Left[int](err)
// }
// return result.Of(val)
// }
```
## Checklist for New Code
- [ ] Apache 2.0 license header included
- [ ] Go doc comments use standard format (no markdown links)
- [ ] Code examples are idiomatic and concise
- [ ] Tests cover success, failure, edge cases, and integration
- [ ] Tests use direct assertions where possible
- [ ] Benchmarks included for performance-critical code
- [ ] All tests pass
- [ ] Code uses functional composition patterns
- [ ] Error handling preserves context and causes

View File

@@ -584,5 +584,7 @@ func process(input string) types.Result[types.Option[int]] {
For more information, see:
- [README.md](./README.md) - Overview and quick start
- [FUNCTIONAL_IO.md](./FUNCTIONAL_IO.md) - Functional I/O patterns with Context and Reader
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison between standard and idiomatic packages
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2) - Complete API reference
- [Samples](./samples/) - Practical examples

829
v2/FUNCTIONAL_IO.md Normal file
View File

@@ -0,0 +1,829 @@
# Functional I/O in Go: Context, Errors, and the Reader Pattern
This document explores how functional programming principles apply to I/O operations in Go, comparing traditional imperative approaches with functional patterns using the `context/readerioresult` and `idiomatic/context/readerresult` packages.
## Table of Contents
- [Why Context in I/O Operations](#why-context-in-io-operations)
- [The Error-Value Tuple Pattern](#the-error-value-tuple-pattern)
- [Functional Approach: Reader Pattern](#functional-approach-reader-pattern)
- [Benefits of the Functional Approach](#benefits-of-the-functional-approach)
- [Side-by-Side Comparison](#side-by-side-comparison)
- [Advanced Patterns](#advanced-patterns)
- [When to Use Each Approach](#when-to-use-each-approach)
## Why Context in I/O Operations
In idiomatic Go, I/O operations conventionally take a `context.Context` as their first parameter:
```go
func QueryDatabase(ctx context.Context, query string) (Result, error)
func MakeHTTPRequest(ctx context.Context, url string) (*http.Response, error)
func ReadFile(ctx context.Context, path string) ([]byte, error)
```
### The Purpose of Context
The `context.Context` parameter serves several critical purposes:
1. **Cancellation Propagation**: Operations can be cancelled when the context is cancelled
2. **Deadline Management**: Operations respect timeouts and deadlines
3. **Request-Scoped Values**: Carry request metadata (trace IDs, user info, etc.)
4. **Resource Cleanup**: Signal to release resources when work is no longer needed
### Why Context Matters for I/O
I/O operations are inherently **effectful** - they interact with the outside world:
- Reading from disk, network, or database
- Writing to external systems
- Generating random numbers
- Reading the current time
These operations can:
- **Take time**: Network calls may be slow
- **Fail**: Connections drop, files don't exist
- **Block**: Waiting for external resources
- **Need cancellation**: User navigates away, request times out
Context provides a standard mechanism to control these operations across your entire application.
## The Error-Value Tuple Pattern
### Why Operations Must Return Errors
In Go, I/O operations return `(value, error)` tuples because:
1. **Context can be cancelled**: Even if the operation would succeed, cancellation must be represented
2. **External systems fail**: Networks fail, files are missing, permissions are denied
3. **Resources are exhausted**: Out of memory, disk full, connection pool exhausted
4. **Timeouts occur**: Operations exceed their deadline
**There cannot be I/O operations without error handling** because the context itself introduces a failure mode (cancellation) that must be represented in the return type.
### Traditional Go Pattern
```go
func ProcessUser(ctx context.Context, userID int) (User, error) {
// Check context before starting
if err := ctx.Err(); err != nil {
return User{}, err
}
// Fetch user from database
user, err := db.QueryUser(ctx, userID)
if err != nil {
return User{}, fmt.Errorf("query user: %w", err)
}
// Validate user
if user.Age < 18 {
return User{}, errors.New("user too young")
}
// Fetch user's posts
posts, err := db.QueryPosts(ctx, user.ID)
if err != nil {
return User{}, fmt.Errorf("query posts: %w", err)
}
user.Posts = posts
return user, nil
}
```
**Characteristics:**
- Explicit error checking at each step
- Manual error wrapping and propagation
- Context checked manually
- Imperative control flow
- Error handling mixed with business logic
## Functional Approach: Reader Pattern
### The Core Insight
In functional programming, we separate **what to compute** from **how to execute it**. Instead of functions that perform I/O directly, we create functions that **return descriptions of I/O operations**.
### Key Type: ReaderIOResult
```go
// A function that takes a context and returns a value or error
type ReaderIOResult[A any] = func(context.Context) (A, error)
```
This type represents:
- **Reader**: Depends on an environment (context.Context)
- **IO**: Performs side effects (I/O operations)
- **Result**: Can fail with an error
### Why This Is Better
The functional approach **carries the I/O aspect as the return value, not on the input**:
```go
// Traditional: I/O is implicit in the function execution
func fetchUser(ctx context.Context, id int) (User, error) {
// Performs I/O immediately
}
// Functional: I/O is explicit in the return type
func fetchUser(id int) ReaderIOResult[User] {
// Returns a description of I/O, doesn't execute yet
return func(ctx context.Context) (User, error) {
// I/O happens here when the function is called
}
}
```
**Key difference**: The functional version is a **curried function** where:
1. Business parameters come first: `fetchUser(id)`
2. Context comes last: `fetchUser(id)(ctx)`
3. The intermediate result is composable: `ReaderIOResult[User]`
## Benefits of the Functional Approach
### 1. Separation of Pure and Impure Code
```go
// Pure computation - no I/O, no context needed
func validateAge(user User) (User, error) {
if user.Age < 18 {
return User{}, errors.New("user too young")
}
return user, nil
}
// Impure I/O operation - needs context
func fetchUser(id int) ReaderIOResult[User] {
return func(ctx context.Context) (User, error) {
return db.QueryUser(ctx, id)
}
}
// Compose them - pure logic lifted into ReaderIOResult
pipeline := F.Pipe2(
fetchUser(42), // ReaderIOResult[User]
readerioresult.ChainEitherK(validateAge), // Lift pure function
)
// Execute when ready
user, err := pipeline(ctx)
```
**Benefits:**
- Pure functions are easier to test (no mocking needed)
- Pure functions are easier to reason about (no side effects)
- Clear boundary between logic and I/O
- Can test business logic independently
### 2. Composability
Functions compose naturally without manual error checking:
```go
// Traditional approach - manual error handling
func ProcessUserTraditional(ctx context.Context, userID int) (UserWithPosts, error) {
user, err := fetchUser(ctx, userID)
if err != nil {
return UserWithPosts{}, err
}
validated, err := validateUser(user)
if err != nil {
return UserWithPosts{}, err
}
posts, err := fetchPosts(ctx, validated.ID)
if err != nil {
return UserWithPosts{}, err
}
return enrichUser(validated, posts), nil
}
// Functional approach - automatic error propagation
func ProcessUserFunctional(userID int) ReaderIOResult[UserWithPosts] {
return F.Pipe3(
fetchUser(userID),
readerioresult.ChainEitherK(validateUser),
readerioresult.Chain(func(user User) ReaderIOResult[UserWithPosts] {
return F.Pipe2(
fetchPosts(user.ID),
readerioresult.Map(func(posts []Post) UserWithPosts {
return enrichUser(user, posts)
}),
)
}),
)
}
```
**Benefits:**
- No manual error checking
- Automatic short-circuiting on first error
- Clear data flow
- Easier to refactor and extend
### 3. Testability
```go
// Mock I/O operations by providing test implementations
func TestProcessUser(t *testing.T) {
// Create a mock that returns test data
mockFetchUser := func(id int) ReaderIOResult[User] {
return func(ctx context.Context) (User, error) {
return User{ID: id, Age: 25}, nil
}
}
// Test with mock - no database needed
result, err := mockFetchUser(42)(context.Background())
assert.NoError(t, err)
assert.Equal(t, 25, result.Age)
}
```
### 4. Lazy Evaluation
Operations are not executed until you provide the context:
```go
// Build the pipeline - no I/O happens yet
pipeline := F.Pipe3(
fetchUser(42),
readerioresult.Map(enrichUser),
readerioresult.Chain(saveUser),
)
// I/O only happens when we call it with a context
user, err := pipeline(ctx)
```
**Benefits:**
- Build complex operations as pure data structures
- Defer execution until needed
- Reuse pipelines with different contexts
- Test pipelines without executing I/O
### 5. Context Propagation
Context is automatically threaded through all operations:
```go
// Traditional - must pass context explicitly everywhere
func Process(ctx context.Context) error {
user, err := fetchUser(ctx, 42)
if err != nil {
return err
}
posts, err := fetchPosts(ctx, user.ID)
if err != nil {
return err
}
return savePosts(ctx, posts)
}
// Functional - context provided once at execution
func Process() ReaderIOResult[any] {
return F.Pipe2(
fetchUser(42),
readerioresult.Chain(func(user User) ReaderIOResult[any] {
return F.Pipe2(
fetchPosts(user.ID),
readerioresult.Chain(savePosts),
)
}),
)
}
// Context provided once
err := readerioresult.Fold(
func(err error) error { return err },
func(any) error { return nil },
)(Process())(ctx)
```
## Side-by-Side Comparison
### Example: User Service with Database Operations
#### Traditional Go Style
```go
package traditional
import (
"context"
"database/sql"
"fmt"
)
type User struct {
ID int
Name string
Email string
Age int
}
type UserService struct {
db *sql.DB
}
// Fetch user from database
func (s *UserService) GetUser(ctx context.Context, id int) (User, error) {
var user User
// Check context
if err := ctx.Err(); err != nil {
return User{}, err
}
// Query database
row := s.db.QueryRowContext(ctx,
"SELECT id, name, email, age FROM users WHERE id = ?", id)
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
if err != nil {
return User{}, fmt.Errorf("scan user: %w", err)
}
return user, nil
}
// Validate user
func (s *UserService) ValidateUser(ctx context.Context, user User) (User, error) {
if user.Age < 18 {
return User{}, fmt.Errorf("user %d is too young", user.ID)
}
if user.Email == "" {
return User{}, fmt.Errorf("user %d has no email", user.ID)
}
return user, nil
}
// Update user email
func (s *UserService) UpdateEmail(ctx context.Context, id int, email string) (User, error) {
// Check context
if err := ctx.Err(); err != nil {
return User{}, err
}
// Update database
_, err := s.db.ExecContext(ctx,
"UPDATE users SET email = ? WHERE id = ?", email, id)
if err != nil {
return User{}, fmt.Errorf("update email: %w", err)
}
// Fetch updated user
return s.GetUser(ctx, id)
}
// Process user: fetch, validate, update email
func (s *UserService) ProcessUser(ctx context.Context, id int, newEmail string) (User, error) {
// Fetch user
user, err := s.GetUser(ctx, id)
if err != nil {
return User{}, fmt.Errorf("get user: %w", err)
}
// Validate user
validated, err := s.ValidateUser(ctx, user)
if err != nil {
return User{}, fmt.Errorf("validate user: %w", err)
}
// Update email
updated, err := s.UpdateEmail(ctx, validated.ID, newEmail)
if err != nil {
return User{}, fmt.Errorf("update email: %w", err)
}
return updated, nil
}
```
**Characteristics:**
- ✗ Manual error checking at every step
- ✗ Context passed explicitly to every function
- ✗ Error wrapping is manual and verbose
- ✗ Business logic mixed with error handling
- ✗ Hard to test without database
- ✗ Difficult to compose operations
- ✓ Familiar to Go developers
- ✓ Explicit control flow
#### Functional Go Style (context/readerioresult)
```go
package functional
import (
"context"
"database/sql"
"fmt"
F "github.com/IBM/fp-go/v2/function"
RIO "github.com/IBM/fp-go/v2/context/readerioresult"
)
type User struct {
ID int
Name string
Email string
Age int
}
type UserService struct {
db *sql.DB
}
// Fetch user from database - returns a ReaderIOResult
func (s *UserService) GetUser(id int) RIO.ReaderIOResult[User] {
return func(ctx context.Context) (User, error) {
var user User
row := s.db.QueryRowContext(ctx,
"SELECT id, name, email, age FROM users WHERE id = ?", id)
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
if err != nil {
return User{}, fmt.Errorf("scan user: %w", err)
}
return user, nil
}
}
// Validate user - pure function (no I/O, no context)
func ValidateUser(user User) (User, error) {
if user.Age < 18 {
return User{}, fmt.Errorf("user %d is too young", user.ID)
}
if user.Email == "" {
return User{}, fmt.Errorf("user %d has no email", user.ID)
}
return user, nil
}
// Update user email - returns a ReaderIOResult
func (s *UserService) UpdateEmail(id int, email string) RIO.ReaderIOResult[User] {
return func(ctx context.Context) (User, error) {
_, err := s.db.ExecContext(ctx,
"UPDATE users SET email = ? WHERE id = ?", email, id)
if err != nil {
return User{}, fmt.Errorf("update email: %w", err)
}
// Chain to GetUser
return s.GetUser(id)(ctx)
}
}
// Process user: fetch, validate, update email - composable pipeline
func (s *UserService) ProcessUser(id int, newEmail string) RIO.ReaderIOResult[User] {
return F.Pipe3(
s.GetUser(id), // Fetch user
RIO.ChainEitherK(ValidateUser), // Validate (pure function)
RIO.Chain(func(user User) RIO.ReaderIOResult[User] {
return s.UpdateEmail(user.ID, newEmail) // Update email
}),
)
}
// Alternative: Using Do-notation for more complex flows
func (s *UserService) ProcessUserDo(id int, newEmail string) RIO.ReaderIOResult[User] {
return RIO.Chain(func(user User) RIO.ReaderIOResult[User] {
// Validate is pure, lift it into ReaderIOResult
validated, err := ValidateUser(user)
if err != nil {
return RIO.Left[User](err)
}
// Update with validated user
return s.UpdateEmail(validated.ID, newEmail)
})(s.GetUser(id))
}
```
**Characteristics:**
- ✓ Automatic error propagation (no manual checking)
- ✓ Context threaded automatically
- ✓ Pure functions separated from I/O
- ✓ Business logic clear and composable
- ✓ Easy to test (mock ReaderIOResult)
- ✓ Operations compose naturally
- ✓ Lazy evaluation (build pipeline, execute later)
- ✗ Requires understanding of functional patterns
- ✗ Less familiar to traditional Go developers
#### Idiomatic Functional Style (idiomatic/context/readerresult)
For even better performance with the same functional benefits:
```go
package idiomatic
import (
"context"
"database/sql"
"fmt"
F "github.com/IBM/fp-go/v2/function"
RR "github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
)
type User struct {
ID int
Name string
Email string
Age int
}
type UserService struct {
db *sql.DB
}
// ReaderResult is just: func(context.Context) (A, error)
// Same as ReaderIOResult but using native Go tuples
func (s *UserService) GetUser(id int) RR.ReaderResult[User] {
return func(ctx context.Context) (User, error) {
var user User
row := s.db.QueryRowContext(ctx,
"SELECT id, name, email, age FROM users WHERE id = ?", id)
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Age)
return user, err // Native tuple return
}
}
// Pure validation - returns native (User, error) tuple
func ValidateUser(user User) (User, error) {
if user.Age < 18 {
return User{}, fmt.Errorf("user %d is too young", user.ID)
}
if user.Email == "" {
return User{}, fmt.Errorf("user %d has no email", user.ID)
}
return user, nil
}
func (s *UserService) UpdateEmail(id int, email string) RR.ReaderResult[User] {
return func(ctx context.Context) (User, error) {
_, err := s.db.ExecContext(ctx,
"UPDATE users SET email = ? WHERE id = ?", email, id)
if err != nil {
return User{}, err
}
return s.GetUser(id)(ctx)
}
}
// Composable pipeline with native tuples
func (s *UserService) ProcessUser(id int, newEmail string) RR.ReaderResult[User] {
return F.Pipe3(
s.GetUser(id),
RR.ChainEitherK(ValidateUser), // Lift pure function
RR.Chain(func(user User) RR.ReaderResult[User] {
return s.UpdateEmail(user.ID, newEmail)
}),
)
}
```
**Characteristics:**
- ✓ All benefits of functional approach
-**2-10x better performance** (native tuples)
-**Zero allocations** for many operations
- ✓ More familiar to Go developers (uses (value, error))
- ✓ Seamless integration with existing Go code
- ✓ Same composability as ReaderIOResult
### Usage Comparison
```go
// Traditional
func HandleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
service := &UserService{db: db}
user, err := service.ProcessUser(ctx, 42, "new@email.com")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
// Functional (both styles)
func HandleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
service := &UserService{db: db}
// Build the pipeline (no execution yet)
pipeline := service.ProcessUser(42, "new@email.com")
// Execute with context
user, err := pipeline(ctx)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
// Or using Fold for cleaner error handling
func HandleRequestFold(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
service := &UserService{db: db}
RR.Fold(
func(err error) {
http.Error(w, err.Error(), http.StatusInternalServerError)
},
func(user User) {
json.NewEncoder(w).Encode(user)
},
)(service.ProcessUser(42, "new@email.com"))(ctx)
}
```
## Advanced Patterns
### Resource Management with Bracket
```go
// Traditional
func ProcessFile(ctx context.Context, path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return "", err
}
return string(data), nil
}
// Functional - guaranteed cleanup even on panic
func ProcessFile(path string) RIO.ReaderIOResult[string] {
return RIO.Bracket(
// Acquire resource
func(ctx context.Context) (*os.File, error) {
return os.Open(path)
},
// Release resource (always called)
func(file *os.File, err error) RIO.ReaderIOResult[any] {
return func(ctx context.Context) (any, error) {
return nil, file.Close()
}
},
// Use resource
func(file *os.File) RIO.ReaderIOResult[string] {
return func(ctx context.Context) (string, error) {
data, err := io.ReadAll(file)
return string(data), err
}
},
)
}
```
### Parallel Execution
```go
// Traditional - manual goroutines and sync
func FetchMultipleUsers(ctx context.Context, ids []int) ([]User, error) {
var wg sync.WaitGroup
users := make([]User, len(ids))
errs := make([]error, len(ids))
for i, id := range ids {
wg.Add(1)
go func(i, id int) {
defer wg.Done()
users[i], errs[i] = fetchUser(ctx, id)
}(i, id)
}
wg.Wait()
for _, err := range errs {
if err != nil {
return nil, err
}
}
return users, nil
}
// Functional - automatic parallelization
func FetchMultipleUsers(ids []int) RIO.ReaderIOResult[[]User] {
operations := A.Map(func(id int) RIO.ReaderIOResult[User] {
return fetchUser(id)
})(ids)
return RIO.TraverseArrayPar(F.Identity[RIO.ReaderIOResult[User]])(operations)
}
```
### Retry Logic
```go
// Traditional
func FetchWithRetry(ctx context.Context, url string, maxRetries int) ([]byte, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
if ctx.Err() != nil {
return nil, ctx.Err()
}
resp, err := http.Get(url)
if err == nil {
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
lastErr = err
time.Sleep(time.Second * time.Duration(i+1))
}
return nil, lastErr
}
// Functional
func FetchWithRetry(url string, maxRetries int) RIO.ReaderIOResult[[]byte] {
operation := func(ctx context.Context) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
return RIO.Retry(
maxRetries,
func(attempt int) time.Duration {
return time.Second * time.Duration(attempt)
},
)(operation)
}
```
## When to Use Each Approach
### Use Traditional Go Style When:
1. **Team familiarity**: Team is not familiar with functional programming
2. **Simple operations**: Single I/O operation with straightforward error handling
3. **Existing codebase**: Large codebase already using traditional patterns
4. **Learning curve**: Want to minimize onboarding time
5. **Explicit control**: Need very explicit control flow
### Use Functional Style (ReaderIOResult) When:
1. **Complex pipelines**: Multiple I/O operations that need composition
2. **Testability**: Need to test business logic separately from I/O
3. **Reusability**: Want to build reusable operation pipelines
4. **Error handling**: Want automatic error propagation
5. **Resource management**: Need guaranteed cleanup (Bracket)
6. **Parallel execution**: Need to parallelize operations easily
7. **Type safety**: Want the type system to track I/O effects
### Use Idiomatic Functional Style (idiomatic/context/readerresult) When:
1. **All functional benefits**: Want functional patterns with Go idioms
2. **Performance critical**: Need 2-10x better performance
3. **Zero allocations**: Memory efficiency is important
4. **Go integration**: Want seamless integration with existing Go code
5. **Production services**: Building high-throughput services
6. **Best of both worlds**: Want functional composition with Go's native patterns
## Summary
The functional approach to I/O in Go offers significant advantages:
1. **Separation of Concerns**: Pure logic separated from I/O effects
2. **Composability**: Operations compose naturally without manual error checking
3. **Testability**: Easy to test without mocking I/O
4. **Type Safety**: I/O effects visible in the type system
5. **Lazy Evaluation**: Build pipelines, execute when ready
6. **Context Propagation**: Automatic threading of context
7. **Performance**: Idiomatic version offers 2-10x speedup
The key insight is that **I/O operations return descriptions of effects** (ReaderIOResult) rather than performing effects immediately. This enables powerful composition patterns while maintaining Go's idiomatic error handling through the `(value, error)` tuple pattern.
For production Go services, the **idiomatic/context/readerresult** package provides the best balance: full functional programming capabilities with native Go performance and familiar error handling patterns.
## Further Reading
- [DESIGN.md](./DESIGN.md) - Design principles and patterns
- [IDIOMATIC_COMPARISON.md](./IDIOMATIC_COMPARISON.md) - Performance comparison
- [idiomatic/doc.go](./idiomatic/doc.go) - Idiomatic package overview
- [context/readerioresult](./context/readerioresult/) - ReaderIOResult package
- [idiomatic/context/readerresult](./idiomatic/context/readerresult/) - Idiomatic ReaderResult package

View File

@@ -447,6 +447,8 @@ func process() IOResult[string] {
## 📚 Documentation
- **[Design Decisions](./DESIGN.md)** - Key design principles and patterns explained
- **[Functional I/O in Go](./FUNCTIONAL_IO.md)** - Understanding Context, errors, and the Reader pattern for I/O operations
- **[Idiomatic vs Standard Packages](./IDIOMATIC_COMPARISON.md)** - Performance comparison and when to use each approach
- **[API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)** - Complete API reference
- **[Code Samples](./samples/)** - Practical examples and use cases
- **[Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)** - Information about generic type aliases
@@ -458,12 +460,15 @@ func process() IOResult[string] {
- **Either** - Type-safe error handling with left/right values
- **Result** - Simplified Either with error as left type (recommended for error handling)
- **IO** - Lazy evaluation and side effect management
- **IOOption** - Combine IO with Option for optional values with side effects
- **IOResult** - Combine IO with Result for error handling (recommended over IOEither)
- **Reader** - Dependency injection pattern
- **ReaderOption** - Combine Reader with Option for optional values with dependency injection
- **ReaderIOOption** - Combine Reader, IO, and Option for optional values with dependency injection and side effects
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
- **[Optics](./optics/README.md)** - Lens, Prism, Optional, and Traversal for immutable updates
#### Idiomatic Packages (Tuple-based, High Performance)
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples

View File

@@ -21,7 +21,7 @@ import (
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// From constructs an array from a set of variadic arguments
@@ -163,11 +163,11 @@ func FilterMapWithIndex[A, B any](f func(int, A) Option[B]) Operator[A, B] {
return G.FilterMapWithIndex[[]A, []B](f)
}
// FilterChain maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
// ChainOptionK maps an array with an iterating function that returns an [Option] of an array. It keeps only the Some values discarding the Nones and then flattens the result.
//
//go:inline
func FilterChain[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.FilterChain[[]A](f)
func ChainOptionK[A, B any](f option.Kleisli[A, []B]) Operator[A, B] {
return G.ChainOptionK[[]A](f)
}
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
@@ -190,6 +190,11 @@ func MonadReduce[A, B any](fa []A, f func(B, A) B, initial B) B {
return G.MonadReduce(fa, f, initial)
}
//go:inline
func MonadReduceWithIndex[A, B any](fa []A, f func(int, B, A) B, initial B) B {
return G.MonadReduceWithIndex(fa, f, initial)
}
// Reduce folds an array from left to right, applying a function to accumulate a result.
//
// Example:
@@ -234,6 +239,16 @@ func ReduceRef[A, B any](f func(B, *A) B, initial B) func([]A) B {
}
// Append adds an element to the end of an array, returning a new array.
// This is a non-curried version that takes both the array and element as parameters.
//
// Example:
//
// arr := []int{1, 2, 3}
// result := array.Append(arr, 4)
// // result: []int{1, 2, 3, 4}
// // arr: []int{1, 2, 3} (unchanged)
//
// For a curried version, see Push.
//
//go:inline
func Append[A any](as []A, a A) []A {
@@ -438,7 +453,7 @@ func Size[A any](as []A) int {
// the second contains elements for which it returns true.
//
//go:inline
func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
func MonadPartition[A any](as []A, pred func(A) bool) pair.Pair[[]A, []A] {
return G.MonadPartition(as, pred)
}
@@ -446,7 +461,7 @@ func MonadPartition[A any](as []A, pred func(A) bool) tuple.Tuple2[[]A, []A] {
// for which the predicate returns false, the right one those for which the predicate returns true
//
//go:inline
func Partition[A any](pred func(A) bool) func([]A) tuple.Tuple2[[]A, []A] {
func Partition[A any](pred func(A) bool) func([]A) pair.Pair[[]A, []A] {
return G.Partition[[]A](pred)
}
@@ -514,6 +529,83 @@ func Push[A any](a A) Operator[A, A] {
return G.Push[Operator[A, A]](a)
}
// Concat concatenates two arrays, appending the provided array to the end of the input array.
// This is a curried function that takes an array to append and returns a function that
// takes the base array and returns the concatenated result.
//
// The function creates a new array containing all elements from the base array followed
// by all elements from the appended array. Neither input array is modified.
//
// Type Parameters:
// - A: The type of elements in the arrays
//
// Parameters:
// - as: The array to append to the end of the base array
//
// Returns:
// - A function that takes a base array and returns a new array with `as` appended to its end
//
// Behavior:
// - Creates a new array with length equal to the sum of both input arrays
// - Copies all elements from the base array first
// - Appends all elements from the `as` array at the end
// - Returns the base array unchanged if `as` is empty
// - Returns `as` unchanged if the base array is empty
// - Does not modify either input array
//
// Example:
//
// base := []int{1, 2, 3}
// toAppend := []int{4, 5, 6}
// result := array.Concat(toAppend)(base)
// // result: []int{1, 2, 3, 4, 5, 6}
// // base: []int{1, 2, 3} (unchanged)
// // toAppend: []int{4, 5, 6} (unchanged)
//
// Example with empty arrays:
//
// base := []int{1, 2, 3}
// empty := []int{}
// result := array.Concat(empty)(base)
// // result: []int{1, 2, 3}
//
// Example with strings:
//
// words1 := []string{"hello", "world"}
// words2 := []string{"foo", "bar"}
// result := array.Concat(words2)(words1)
// // result: []string{"hello", "world", "foo", "bar"}
//
// Example with functional composition:
//
// numbers := []int{1, 2, 3}
// result := F.Pipe2(
// numbers,
// array.Map(N.Mul(2)),
// array.Concat([]int{10, 20}),
// )
// // result: []int{2, 4, 6, 10, 20}
//
// Use cases:
// - Combining multiple arrays into one
// - Building arrays incrementally
// - Implementing array-based data structures (queues, buffers)
// - Merging results from multiple operations
// - Creating array pipelines with functional composition
//
// Performance:
// - Time complexity: O(n + m) where n and m are the lengths of the arrays
// - Space complexity: O(n + m) for the new array
// - Optimized to avoid allocation when one array is empty
//
// Note: This function is immutable - it creates a new array rather than modifying
// the input arrays. For appending a single element, consider using Append or Push.
//
//go:inline
func Concat[A any](as []A) Operator[A, A] {
return F.Bind2nd(array.Concat[[]A, A], as)
}
// MonadFlap applies a value to an array of functions, producing an array of results.
// This is the monadic version that takes both parameters.
//

View File

@@ -16,308 +16,88 @@
package array
import (
"fmt"
"testing"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestReplicate(t *testing.T) {
result := Replicate(3, "a")
assert.Equal(t, []string{"a", "a", "a"}, result)
empty := Replicate(0, 42)
assert.Equal(t, []int{}, empty)
}
func TestMonadMap(t *testing.T) {
src := []int{1, 2, 3}
result := MonadMap(src, N.Mul(2))
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMonadMapRef(t *testing.T) {
src := []int{1, 2, 3}
result := MonadMapRef(src, func(x *int) int { return *x * 2 })
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestMapWithIndex(t *testing.T) {
src := []string{"a", "b", "c"}
mapper := MapWithIndex(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := mapper(src)
assert.Equal(t, []string{"0:a", "1:b", "2:c"}, result)
}
func TestMapRef(t *testing.T) {
src := []int{1, 2, 3}
mapper := MapRef(func(x *int) int { return *x * 2 })
result := mapper(src)
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestFilterWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
filter := FilterWithIndex(func(i, x int) bool {
return i%2 == 0 && x > 2
})
result := filter(src)
assert.Equal(t, []int{3, 5}, result)
}
func TestFilterRef(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
filter := FilterRef(func(x *int) bool { return *x > 2 })
result := filter(src)
assert.Equal(t, []int{3, 4, 5}, result)
}
func TestMonadFilterMap(t *testing.T) {
src := []int{1, 2, 3, 4}
result := MonadFilterMap(src, func(x int) O.Option[string] {
if x%2 == 0 {
return O.Some(fmt.Sprintf("even:%d", x))
}
return O.None[string]()
})
assert.Equal(t, []string{"even:2", "even:4"}, result)
}
func TestMonadFilterMapWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4}
result := MonadFilterMapWithIndex(src, func(i, x int) O.Option[string] {
if i%2 == 0 {
return O.Some(fmt.Sprintf("%d:%d", i, x))
}
return O.None[string]()
})
assert.Equal(t, []string{"0:1", "2:3"}, result)
}
func TestFilterMapWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4}
filter := FilterMapWithIndex(func(i, x int) O.Option[string] {
if i%2 == 0 {
return O.Some(fmt.Sprintf("%d:%d", i, x))
}
return O.None[string]()
})
result := filter(src)
assert.Equal(t, []string{"0:1", "2:3"}, result)
}
func TestFilterMapRef(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
filter := FilterMapRef(
func(x *int) bool { return *x > 2 },
func(x *int) string { return fmt.Sprintf("val:%d", *x) },
)
result := filter(src)
assert.Equal(t, []string{"val:3", "val:4", "val:5"}, result)
}
func TestReduceWithIndex(t *testing.T) {
src := []int{1, 2, 3}
reducer := ReduceWithIndex(func(i, acc, x int) int {
return acc + i + x
// TestMonadReduceWithIndex tests the MonadReduceWithIndex function
func TestMonadReduceWithIndex(t *testing.T) {
// Test with integers - sum with index multiplication
numbers := []int{1, 2, 3, 4, 5}
result := MonadReduceWithIndex(numbers, func(idx, acc, val int) int {
return acc + (val * idx)
}, 0)
result := reducer(src)
assert.Equal(t, 9, result) // 0 + (0+1) + (1+2) + (2+3) = 9
}
// Expected: 0*1 + 1*2 + 2*3 + 3*4 + 4*5 = 0 + 2 + 6 + 12 + 20 = 40
assert.Equal(t, 40, result)
func TestReduceRightWithIndex(t *testing.T) {
src := []string{"a", "b", "c"}
reducer := ReduceRightWithIndex(func(i int, x, acc string) string {
return fmt.Sprintf("%s%d:%s", acc, i, x)
// Test with empty array
empty := []int{}
result2 := MonadReduceWithIndex(empty, func(idx, acc, val int) int {
return acc + val
}, 10)
assert.Equal(t, 10, result2)
// Test with strings - concatenate with index
words := []string{"a", "b", "c"}
result3 := MonadReduceWithIndex(words, func(idx int, acc, val string) string {
return acc + val + string(rune('0'+idx))
}, "")
result := reducer(src)
assert.Equal(t, "2:c1:b0:a", result)
assert.Equal(t, "a0b1c2", result3)
}
func TestReduceRef(t *testing.T) {
src := []int{1, 2, 3}
reducer := ReduceRef(func(acc int, x *int) int {
return acc + *x
}, 0)
result := reducer(src)
assert.Equal(t, 6, result)
}
func TestZero(t *testing.T) {
result := Zero[int]()
assert.Equal(t, []int{}, result)
assert.True(t, IsEmpty(result))
}
func TestMonadChain(t *testing.T) {
src := []int{1, 2, 3}
result := MonadChain(src, func(x int) []int {
return []int{x, x * 10}
})
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
}
func TestChain(t *testing.T) {
src := []int{1, 2, 3}
chain := Chain(func(x int) []int {
return []int{x, x * 10}
})
result := chain(src)
assert.Equal(t, []int{1, 10, 2, 20, 3, 30}, result)
}
func TestMonadAp(t *testing.T) {
fns := []func(int) int{
N.Mul(2),
N.Add(10),
}
values := []int{1, 2}
result := MonadAp(fns, values)
assert.Equal(t, []int{2, 4, 11, 12}, result)
}
func TestMatchLeft(t *testing.T) {
matcher := MatchLeft(
func() string { return "empty" },
func(head int, tail []int) string {
return fmt.Sprintf("head:%d,tail:%v", head, tail)
},
)
assert.Equal(t, "empty", matcher([]int{}))
assert.Equal(t, "head:1,tail:[2 3]", matcher([]int{1, 2, 3}))
}
func TestTail(t *testing.T) {
assert.Equal(t, O.None[[]int](), Tail([]int{}))
assert.Equal(t, O.Some([]int{2, 3}), Tail([]int{1, 2, 3}))
assert.Equal(t, O.Some([]int{}), Tail([]int{1}))
}
func TestFirst(t *testing.T) {
assert.Equal(t, O.None[int](), First([]int{}))
assert.Equal(t, O.Some(1), First([]int{1, 2, 3}))
}
func TestLast(t *testing.T) {
assert.Equal(t, O.None[int](), Last([]int{}))
assert.Equal(t, O.Some(3), Last([]int{1, 2, 3}))
assert.Equal(t, O.Some(1), Last([]int{1}))
}
func TestUpsertAt(t *testing.T) {
src := []int{1, 2, 3}
upsert := UpsertAt(99)
result1 := upsert(src)
assert.Equal(t, []int{1, 2, 3, 99}, result1)
}
func TestSize(t *testing.T) {
assert.Equal(t, 0, Size([]int{}))
assert.Equal(t, 3, Size([]int{1, 2, 3}))
}
func TestMonadPartition(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
result := MonadPartition(src, func(x int) bool { return x > 2 })
assert.Equal(t, []int{1, 2}, result.F1)
assert.Equal(t, []int{3, 4, 5}, result.F2)
}
func TestIsNil(t *testing.T) {
var nilSlice []int
assert.True(t, IsNil(nilSlice))
assert.False(t, IsNil([]int{}))
assert.False(t, IsNil([]int{1}))
}
func TestIsNonNil(t *testing.T) {
var nilSlice []int
assert.False(t, IsNonNil(nilSlice))
assert.True(t, IsNonNil([]int{}))
assert.True(t, IsNonNil([]int{1}))
}
func TestConstNil(t *testing.T) {
result := ConstNil[int]()
assert.True(t, IsNil(result))
}
func TestSliceRight(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
slicer := SliceRight[int](2)
result := slicer(src)
assert.Equal(t, []int{3, 4, 5}, result)
}
func TestCopy(t *testing.T) {
src := []int{1, 2, 3}
copied := Copy(src)
assert.Equal(t, src, copied)
// Verify it's a different slice
copied[0] = 99
assert.Equal(t, 1, src[0])
assert.Equal(t, 99, copied[0])
}
func TestClone(t *testing.T) {
src := []int{1, 2, 3}
cloner := Clone(N.Mul(2))
result := cloner(src)
assert.Equal(t, []int{2, 4, 6}, result)
}
func TestFoldMapWithIndex(t *testing.T) {
src := []string{"a", "b", "c"}
folder := FoldMapWithIndex[string](S.Monoid)(func(i int, s string) string {
return fmt.Sprintf("%d:%s", i, s)
})
result := folder(src)
assert.Equal(t, "0:a1:b2:c", result)
}
func TestFold(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
folder := Fold(N.MonoidSum[int]())
result := folder(src)
assert.Equal(t, 15, result)
}
func TestPush(t *testing.T) {
src := []int{1, 2, 3}
pusher := Push(4)
result := pusher(src)
// TestAppend tests the Append function
func TestAppend(t *testing.T) {
// Test appending to non-empty array
arr := []int{1, 2, 3}
result := Append(arr, 4)
assert.Equal(t, []int{1, 2, 3, 4}, result)
// Verify original array is unchanged
assert.Equal(t, []int{1, 2, 3}, arr)
// Test appending to empty array
empty := []int{}
result2 := Append(empty, 1)
assert.Equal(t, []int{1}, result2)
// Test appending strings
words := []string{"hello", "world"}
result3 := Append(words, "!")
assert.Equal(t, []string{"hello", "world", "!"}, result3)
// Test appending to nil array
var nilArr []int
result4 := Append(nilArr, 42)
assert.Equal(t, []int{42}, result4)
}
func TestMonadFlap(t *testing.T) {
fns := []func(int) string{
func(x int) string { return fmt.Sprintf("a%d", x) },
func(x int) string { return fmt.Sprintf("b%d", x) },
}
result := MonadFlap(fns, 5)
assert.Equal(t, []string{"a5", "b5"}, result)
}
// TestStrictEquals tests the StrictEquals function
func TestStrictEquals(t *testing.T) {
eq := StrictEquals[int]()
func TestFlap(t *testing.T) {
fns := []func(int) string{
func(x int) string { return fmt.Sprintf("a%d", x) },
func(x int) string { return fmt.Sprintf("b%d", x) },
}
flapper := Flap[string](5)
result := flapper(fns)
assert.Equal(t, []string{"a5", "b5"}, result)
}
// Test equal arrays
arr1 := []int{1, 2, 3}
arr2 := []int{1, 2, 3}
assert.True(t, eq.Equals(arr1, arr2))
func TestPrepend(t *testing.T) {
src := []int{2, 3, 4}
prepender := Prepend(1)
result := prepender(src)
assert.Equal(t, []int{1, 2, 3, 4}, result)
// Test different arrays
arr3 := []int{1, 2, 4}
assert.False(t, eq.Equals(arr1, arr3))
// Test different lengths
arr4 := []int{1, 2}
assert.False(t, eq.Equals(arr1, arr4))
// Test empty arrays
empty1 := []int{}
empty2 := []int{}
assert.True(t, eq.Equals(empty1, empty2))
// Test with strings
strEq := StrictEquals[string]()
words1 := []string{"hello", "world"}
words2 := []string{"hello", "world"}
words3 := []string{"hello", "there"}
assert.True(t, strEq.Equals(words1, words2))
assert.False(t, strEq.Equals(words1, words3))
}

View File

@@ -24,8 +24,8 @@ import (
"github.com/IBM/fp-go/v2/internal/utils"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
@@ -163,11 +163,11 @@ func TestPartition(t *testing.T) {
return n > 2
}
assert.Equal(t, T.MakeTuple2(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, T.MakeTuple2(From(1), From(3)), Partition(pred)(From(1, 3)))
assert.Equal(t, pair.MakePair(Empty[int](), Empty[int]()), Partition(pred)(Empty[int]()))
assert.Equal(t, pair.MakePair(From(1), From(3)), Partition(pred)(From(1, 3)))
}
func TestFilterChain(t *testing.T) {
func TestChainOptionK(t *testing.T) {
src := From(1, 2, 3)
f := func(i int) O.Option[[]string] {
@@ -177,7 +177,7 @@ func TestFilterChain(t *testing.T) {
return O.None[[]string]()
}
res := FilterChain(f)(src)
res := ChainOptionK(f)(src)
assert.Equal(t, From("a1", "b1", "a3", "b3"), res)
}
@@ -764,3 +764,341 @@ func TestExtendUseCases(t *testing.T) {
assert.Equal(t, expected, result)
})
}
// TestConcat tests the Concat function
func TestConcat(t *testing.T) {
t.Run("Concat two non-empty arrays", func(t *testing.T) {
base := []int{1, 2, 3}
toAppend := []int{4, 5, 6}
result := Concat(toAppend)(base)
expected := []int{1, 2, 3, 4, 5, 6}
assert.Equal(t, expected, result)
})
t.Run("Concat with empty array to append", func(t *testing.T) {
base := []int{1, 2, 3}
empty := []int{}
result := Concat(empty)(base)
assert.Equal(t, base, result)
})
t.Run("Concat to empty base array", func(t *testing.T) {
empty := []int{}
toAppend := []int{1, 2, 3}
result := Concat(toAppend)(empty)
assert.Equal(t, toAppend, result)
})
t.Run("Concat two empty arrays", func(t *testing.T) {
empty1 := []int{}
empty2 := []int{}
result := Concat(empty2)(empty1)
assert.Equal(t, []int{}, result)
})
t.Run("Concat strings", func(t *testing.T) {
words1 := []string{"hello", "world"}
words2 := []string{"foo", "bar"}
result := Concat(words2)(words1)
expected := []string{"hello", "world", "foo", "bar"}
assert.Equal(t, expected, result)
})
t.Run("Concat single element arrays", func(t *testing.T) {
arr1 := []int{1}
arr2 := []int{2}
result := Concat(arr2)(arr1)
expected := []int{1, 2}
assert.Equal(t, expected, result)
})
t.Run("Does not modify original arrays", func(t *testing.T) {
base := []int{1, 2, 3}
toAppend := []int{4, 5, 6}
baseCopy := []int{1, 2, 3}
toAppendCopy := []int{4, 5, 6}
_ = Concat(toAppend)(base)
assert.Equal(t, baseCopy, base)
assert.Equal(t, toAppendCopy, toAppend)
})
t.Run("Concat with floats", func(t *testing.T) {
arr1 := []float64{1.1, 2.2}
arr2 := []float64{3.3, 4.4}
result := Concat(arr2)(arr1)
expected := []float64{1.1, 2.2, 3.3, 4.4}
assert.Equal(t, expected, result)
})
t.Run("Concat with structs", func(t *testing.T) {
type Person struct {
Name string
Age int
}
arr1 := []Person{{"Alice", 30}, {"Bob", 25}}
arr2 := []Person{{"Charlie", 35}}
result := Concat(arr2)(arr1)
expected := []Person{{"Alice", 30}, {"Bob", 25}, {"Charlie", 35}}
assert.Equal(t, expected, result)
})
t.Run("Concat large arrays", func(t *testing.T) {
arr1 := MakeBy(500, F.Identity[int])
arr2 := MakeBy(500, func(i int) int { return i + 500 })
result := Concat(arr2)(arr1)
assert.Equal(t, 1000, len(result))
assert.Equal(t, 0, result[0])
assert.Equal(t, 499, result[499])
assert.Equal(t, 500, result[500])
assert.Equal(t, 999, result[999])
})
t.Run("Concat multiple times", func(t *testing.T) {
arr1 := []int{1}
arr2 := []int{2}
arr3 := []int{3}
result := F.Pipe2(
arr1,
Concat(arr2),
Concat(arr3),
)
expected := []int{1, 2, 3}
assert.Equal(t, expected, result)
})
}
// TestConcatComposition tests Concat with other array operations
func TestConcatComposition(t *testing.T) {
t.Run("Concat after Map", func(t *testing.T) {
numbers := []int{1, 2, 3}
result := F.Pipe2(
numbers,
Map(N.Mul(2)),
Concat([]int{10, 20}),
)
expected := []int{2, 4, 6, 10, 20}
assert.Equal(t, expected, result)
})
t.Run("Map after Concat", func(t *testing.T) {
arr1 := []int{1, 2}
arr2 := []int{3, 4}
result := F.Pipe2(
arr1,
Concat(arr2),
Map(N.Mul(2)),
)
expected := []int{2, 4, 6, 8}
assert.Equal(t, expected, result)
})
t.Run("Concat with Filter", func(t *testing.T) {
arr1 := []int{1, 2, 3, 4}
arr2 := []int{5, 6, 7, 8}
result := F.Pipe2(
arr1,
Concat(arr2),
Filter(func(n int) bool { return n%2 == 0 }),
)
expected := []int{2, 4, 6, 8}
assert.Equal(t, expected, result)
})
t.Run("Concat with Reduce", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{4, 5, 6}
result := F.Pipe2(
arr1,
Concat(arr2),
Reduce(func(acc, x int) int { return acc + x }, 0),
)
expected := 21 // 1+2+3+4+5+6
assert.Equal(t, expected, result)
})
t.Run("Concat with Reverse", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{4, 5, 6}
result := F.Pipe2(
arr1,
Concat(arr2),
Reverse[int],
)
expected := []int{6, 5, 4, 3, 2, 1}
assert.Equal(t, expected, result)
})
t.Run("Concat with Flatten", func(t *testing.T) {
arr1 := [][]int{{1, 2}, {3, 4}}
arr2 := [][]int{{5, 6}}
result := F.Pipe2(
arr1,
Concat(arr2),
Flatten[int],
)
expected := []int{1, 2, 3, 4, 5, 6}
assert.Equal(t, expected, result)
})
t.Run("Multiple Concat operations", func(t *testing.T) {
arr1 := []int{1}
arr2 := []int{2}
arr3 := []int{3}
arr4 := []int{4}
result := Concat(arr4)(Concat(arr3)(Concat(arr2)(arr1)))
expected := []int{1, 2, 3, 4}
assert.Equal(t, expected, result)
})
}
// TestConcatUseCases demonstrates practical use cases for Concat
func TestConcatUseCases(t *testing.T) {
t.Run("Building array incrementally", func(t *testing.T) {
header := []string{"Name", "Age"}
data := []string{"Alice", "30"}
footer := []string{"Total: 1"}
result := F.Pipe2(
header,
Concat(data),
Concat(footer),
)
expected := []string{"Name", "Age", "Alice", "30", "Total: 1"}
assert.Equal(t, expected, result)
})
t.Run("Merging results from multiple operations", func(t *testing.T) {
evens := Filter(func(n int) bool { return n%2 == 0 })([]int{1, 2, 3, 4, 5, 6})
odds := Filter(func(n int) bool { return n%2 != 0 })([]int{1, 2, 3, 4, 5, 6})
result := Concat(odds)(evens)
expected := []int{2, 4, 6, 1, 3, 5}
assert.Equal(t, expected, result)
})
t.Run("Combining prefix and suffix", func(t *testing.T) {
prefix := []string{"Mr.", "Dr."}
names := []string{"Smith", "Jones"}
addPrefix := func(name string) []string {
return Map(func(p string) string { return p + " " + name })(prefix)
}
result := F.Pipe2(
names,
Chain(addPrefix),
F.Identity[[]string],
)
expected := []string{"Mr. Smith", "Dr. Smith", "Mr. Jones", "Dr. Jones"}
assert.Equal(t, expected, result)
})
t.Run("Queue-like behavior", func(t *testing.T) {
queue := []int{1, 2, 3}
newItems := []int{4, 5}
// Add items to end of queue
updatedQueue := Concat(newItems)(queue)
assert.Equal(t, []int{1, 2, 3, 4, 5}, updatedQueue)
assert.Equal(t, 1, updatedQueue[0]) // Front of queue
assert.Equal(t, 5, updatedQueue[len(updatedQueue)-1]) // Back of queue
})
t.Run("Combining configuration arrays", func(t *testing.T) {
defaultConfig := []string{"--verbose", "--color"}
userConfig := []string{"--output=file.txt", "--format=json"}
finalConfig := Concat(userConfig)(defaultConfig)
expected := []string{"--verbose", "--color", "--output=file.txt", "--format=json"}
assert.Equal(t, expected, finalConfig)
})
}
// TestConcatProperties tests mathematical properties of Concat
func TestConcatProperties(t *testing.T) {
t.Run("Associativity: (a + b) + c == a + (b + c)", func(t *testing.T) {
a := []int{1, 2}
b := []int{3, 4}
c := []int{5, 6}
// (a + b) + c
left := Concat(c)(Concat(b)(a))
// a + (b + c)
right := Concat(Concat(c)(b))(a)
assert.Equal(t, left, right)
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, left)
})
t.Run("Identity: a + [] == a and [] + a == a", func(t *testing.T) {
arr := []int{1, 2, 3}
empty := []int{}
// Right identity
rightResult := Concat(empty)(arr)
assert.Equal(t, arr, rightResult)
// Left identity
leftResult := Concat(arr)(empty)
assert.Equal(t, arr, leftResult)
})
t.Run("Length property: len(a + b) == len(a) + len(b)", func(t *testing.T) {
testCases := []struct {
arr1 []int
arr2 []int
}{
{[]int{1, 2, 3}, []int{4, 5}},
{[]int{1}, []int{2, 3, 4, 5}},
{[]int{}, []int{1, 2, 3}},
{[]int{1, 2, 3}, []int{}},
{MakeBy(100, F.Identity[int]), MakeBy(50, F.Identity[int])},
}
for _, tc := range testCases {
result := Concat(tc.arr2)(tc.arr1)
expectedLen := len(tc.arr1) + len(tc.arr2)
assert.Equal(t, expectedLen, len(result))
}
})
t.Run("Order preservation: elements maintain their relative order", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{4, 5, 6}
result := Concat(arr2)(arr1)
// Check arr1 elements are in order
assert.Equal(t, 1, result[0])
assert.Equal(t, 2, result[1])
assert.Equal(t, 3, result[2])
// Check arr2 elements are in order after arr1
assert.Equal(t, 4, result[3])
assert.Equal(t, 5, result[4])
assert.Equal(t, 6, result[5])
})
t.Run("Immutability: original arrays are not modified", func(t *testing.T) {
original1 := []int{1, 2, 3}
original2 := []int{4, 5, 6}
copy1 := []int{1, 2, 3}
copy2 := []int{4, 5, 6}
_ = Concat(original2)(original1)
assert.Equal(t, copy1, original1)
assert.Equal(t, copy2, original2)
})
}

View File

@@ -63,17 +63,26 @@ func Bind[S1, S2, T any](
// Let attaches the result of a pure computation to a context S1 to produce a context S2.
// Unlike Bind, the computation function returns a plain value T rather than []T.
// This is useful when you need to compute a derived value from the current context
// without introducing additional array elements.
//
// Example:
//
// result := array.Let(
// func(sum int) func(s struct{ X int }) struct{ X, Sum int } {
// return func(s struct{ X int }) struct{ X, Sum int } {
// return struct{ X, Sum int }{s.X, sum}
// }
// },
// func(s struct{ X int }) int { return s.X * 2 },
// type State1 struct{ X int }
// type State2 struct{ X, Double int }
//
// result := F.Pipe2(
// []State1{{X: 5}, {X: 10}},
// array.Let(
// func(double int) func(s State1) State2 {
// return func(s State1) State2 {
// return State2{X: s.X, Double: double}
// }
// },
// func(s State1) int { return s.X * 2 },
// ),
// )
// // result: []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
//
//go:inline
func Let[S1, S2, T any](
@@ -84,18 +93,25 @@ func Let[S1, S2, T any](
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
// This is useful for adding constant values to the context.
// This is useful for adding constant values to the context without computation.
//
// Example:
//
// result := array.LetTo(
// func(name string) func(s struct{ X int }) struct{ X int; Name string } {
// return func(s struct{ X int }) struct{ X int; Name string } {
// return struct{ X int; Name string }{s.X, name}
// }
// },
// "constant",
// type State1 struct{ X int }
// type State2 struct{ X int; Name string }
//
// result := F.Pipe2(
// []State1{{X: 1}, {X: 2}},
// array.LetTo(
// func(name string) func(s State1) State2 {
// return func(s State1) State2 {
// return State2{X: s.X, Name: name}
// }
// },
// "constant",
// ),
// )
// // result: []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
//
//go:inline
func LetTo[S1, S2, T any](
@@ -107,15 +123,19 @@ func LetTo[S1, S2, T any](
// BindTo initializes a new state S1 from a value T.
// This is typically the first operation after Do to start building the context.
// It transforms each element of type T into a state of type S1.
//
// Example:
//
// type State struct{ X int }
//
// result := F.Pipe2(
// []int{1, 2, 3},
// array.BindTo(func(x int) struct{ X int } {
// return struct{ X int }{x}
// array.BindTo(func(x int) State {
// return State{X: x}
// }),
// )
// // result: []State{{X: 1}, {X: 2}, {X: 3}}
//
//go:inline
func BindTo[S1, T any](

View File

@@ -22,57 +22,176 @@ import (
"github.com/stretchr/testify/assert"
)
type TestState1 struct {
X int
}
type TestState2 struct {
X int
Y int
}
// TestLet tests the Let function
func TestLet(t *testing.T) {
result := F.Pipe2(
Do(TestState1{}),
type State1 struct {
X int
}
type State2 struct {
X int
Double int
}
// Test Let with pure computation
result := F.Pipe1(
[]State1{{X: 5}, {X: 10}},
Let(
func(y int) func(s TestState1) TestState2 {
return func(s TestState1) TestState2 {
return TestState2{X: s.X, Y: y}
func(double int) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Double: double}
}
},
func(s TestState1) int { return s.X * 2 },
func(s State1) int { return s.X * 2 },
),
Map(func(s TestState2) int { return s.X + s.Y }),
)
assert.Equal(t, []int{0}, result)
expected := []State2{{X: 5, Double: 10}, {X: 10, Double: 20}}
assert.Equal(t, expected, result)
// Test Let with empty array
empty := []State1{}
result2 := F.Pipe1(
empty,
Let(
func(double int) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Double: double}
}
},
func(s State1) int { return s.X * 2 },
),
)
assert.Equal(t, []State2{}, result2)
}
// TestLetTo tests the LetTo function
func TestLetTo(t *testing.T) {
result := F.Pipe2(
Do(TestState1{X: 5}),
type State1 struct {
X int
}
type State2 struct {
X int
Name string
}
// Test LetTo with constant value
result := F.Pipe1(
[]State1{{X: 1}, {X: 2}},
LetTo(
func(y int) func(s TestState1) TestState2 {
return func(s TestState1) TestState2 {
return TestState2{X: s.X, Y: y}
func(name string) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Name: name}
}
},
42,
"constant",
),
Map(func(s TestState2) int { return s.X + s.Y }),
)
assert.Equal(t, []int{47}, result)
expected := []State2{{X: 1, Name: "constant"}, {X: 2, Name: "constant"}}
assert.Equal(t, expected, result)
// Test LetTo with different constant
result2 := F.Pipe1(
[]State1{{X: 10}},
LetTo(
func(name string) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Name: name}
}
},
"test",
),
)
expected2 := []State2{{X: 10, Name: "test"}}
assert.Equal(t, expected2, result2)
}
// TestBindTo tests the BindTo function
func TestBindTo(t *testing.T) {
type State struct {
X int
}
// Test BindTo with integers
result := F.Pipe1(
[]int{1, 2, 3},
BindTo(func(x int) TestState1 {
return TestState1{X: x}
BindTo(func(x int) State {
return State{X: x}
}),
)
expected := []TestState1{{X: 1}, {X: 2}, {X: 3}}
expected := []State{{X: 1}, {X: 2}, {X: 3}}
assert.Equal(t, expected, result)
// Test BindTo with strings
type StringState struct {
Value string
}
result2 := F.Pipe1(
[]string{"hello", "world"},
BindTo(func(s string) StringState {
return StringState{Value: s}
}),
)
expected2 := []StringState{{Value: "hello"}, {Value: "world"}}
assert.Equal(t, expected2, result2)
// Test BindTo with empty array
empty := []int{}
result3 := F.Pipe1(
empty,
BindTo(func(x int) State {
return State{X: x}
}),
)
assert.Equal(t, []State{}, result3)
}
// TestDoWithLetAndBindTo tests combining Do, Let, LetTo, and BindTo
func TestDoWithLetAndBindTo(t *testing.T) {
type State1 struct {
X int
}
type State2 struct {
X int
Double int
}
type State3 struct {
X int
Double int
Name string
}
// Test complex pipeline
result := F.Pipe3(
[]int{5, 10},
BindTo(func(x int) State1 {
return State1{X: x}
}),
Let(
func(double int) func(s State1) State2 {
return func(s State1) State2 {
return State2{X: s.X, Double: double}
}
},
func(s State1) int { return s.X * 2 },
),
LetTo(
func(name string) func(s State2) State3 {
return func(s State2) State3 {
return State3{X: s.X, Double: s.Double, Name: name}
}
},
"result",
),
)
expected := []State3{
{X: 5, Double: 10, Name: "result"},
{X: 10, Double: 20, Name: "result"},
}
assert.Equal(t, expected, result)
}

137
v2/array/coverage.out Normal file
View File

@@ -0,0 +1,137 @@
mode: set
github.com/IBM/fp-go/v2/array/any.go:34.65,36.2 1 1
github.com/IBM/fp-go/v2/array/any.go:48.51,50.2 1 1
github.com/IBM/fp-go/v2/array/array.go:30.33,32.2 1 1
github.com/IBM/fp-go/v2/array/array.go:37.52,39.2 1 1
github.com/IBM/fp-go/v2/array/array.go:44.39,46.2 1 0
github.com/IBM/fp-go/v2/array/array.go:52.50,54.2 1 0
github.com/IBM/fp-go/v2/array/array.go:58.54,61.23 3 0
github.com/IBM/fp-go/v2/array/array.go:61.23,63.3 1 0
github.com/IBM/fp-go/v2/array/array.go:64.2,64.11 1 0
github.com/IBM/fp-go/v2/array/array.go:70.62,72.2 1 0
github.com/IBM/fp-go/v2/array/array.go:83.48,85.2 1 1
github.com/IBM/fp-go/v2/array/array.go:89.52,91.2 1 0
github.com/IBM/fp-go/v2/array/array.go:93.55,96.23 3 0
github.com/IBM/fp-go/v2/array/array.go:96.23,98.14 2 0
github.com/IBM/fp-go/v2/array/array.go:98.14,100.4 1 0
github.com/IBM/fp-go/v2/array/array.go:102.2,102.15 1 0
github.com/IBM/fp-go/v2/array/array.go:105.75,108.23 3 0
github.com/IBM/fp-go/v2/array/array.go:108.23,110.14 2 0
github.com/IBM/fp-go/v2/array/array.go:110.14,112.4 1 0
github.com/IBM/fp-go/v2/array/array.go:114.2,114.15 1 0
github.com/IBM/fp-go/v2/array/array.go:120.54,122.2 1 1
github.com/IBM/fp-go/v2/array/array.go:127.68,129.2 1 0
github.com/IBM/fp-go/v2/array/array.go:132.58,134.2 1 0
github.com/IBM/fp-go/v2/array/array.go:140.67,142.2 1 0
github.com/IBM/fp-go/v2/array/array.go:148.78,150.2 1 0
github.com/IBM/fp-go/v2/array/array.go:155.65,157.2 1 1
github.com/IBM/fp-go/v2/array/array.go:162.76,164.2 1 0
github.com/IBM/fp-go/v2/array/array.go:169.69,171.2 1 1
github.com/IBM/fp-go/v2/array/array.go:174.80,175.26 1 0
github.com/IBM/fp-go/v2/array/array.go:175.26,177.3 1 0
github.com/IBM/fp-go/v2/array/array.go:180.64,182.25 2 0
github.com/IBM/fp-go/v2/array/array.go:182.25,184.3 1 0
github.com/IBM/fp-go/v2/array/array.go:185.2,185.16 1 0
github.com/IBM/fp-go/v2/array/array.go:189.65,191.2 1 1
github.com/IBM/fp-go/v2/array/array.go:194.79,196.2 1 1
github.com/IBM/fp-go/v2/array/array.go:206.62,208.2 1 1
github.com/IBM/fp-go/v2/array/array.go:214.76,216.2 1 0
github.com/IBM/fp-go/v2/array/array.go:221.67,223.2 1 1
github.com/IBM/fp-go/v2/array/array.go:229.81,231.2 1 0
github.com/IBM/fp-go/v2/array/array.go:235.66,236.24 1 0
github.com/IBM/fp-go/v2/array/array.go:236.24,238.3 1 0
github.com/IBM/fp-go/v2/array/array.go:254.37,256.2 1 1
github.com/IBM/fp-go/v2/array/array.go:261.34,263.2 1 1
github.com/IBM/fp-go/v2/array/array.go:266.37,268.2 1 1
github.com/IBM/fp-go/v2/array/array.go:273.25,275.2 1 1
github.com/IBM/fp-go/v2/array/array.go:280.24,282.2 1 0
github.com/IBM/fp-go/v2/array/array.go:287.25,289.2 1 1
github.com/IBM/fp-go/v2/array/array.go:295.56,297.2 1 0
github.com/IBM/fp-go/v2/array/array.go:308.54,310.2 1 1
github.com/IBM/fp-go/v2/array/array.go:316.53,318.2 1 0
github.com/IBM/fp-go/v2/array/array.go:324.50,326.2 1 1
github.com/IBM/fp-go/v2/array/array.go:331.76,333.2 1 1
github.com/IBM/fp-go/v2/array/array.go:338.83,340.2 1 0
github.com/IBM/fp-go/v2/array/array.go:346.38,348.2 1 0
github.com/IBM/fp-go/v2/array/array.go:354.36,356.2 1 1
github.com/IBM/fp-go/v2/array/array.go:362.37,364.2 1 0
github.com/IBM/fp-go/v2/array/array.go:370.36,372.2 1 0
github.com/IBM/fp-go/v2/array/array.go:375.49,376.26 1 1
github.com/IBM/fp-go/v2/array/array.go:376.26,380.35 4 1
github.com/IBM/fp-go/v2/array/array.go:380.35,385.4 4 1
github.com/IBM/fp-go/v2/array/array.go:386.3,386.16 1 1
github.com/IBM/fp-go/v2/array/array.go:395.50,397.26 2 1
github.com/IBM/fp-go/v2/array/array.go:397.26,398.18 1 1
github.com/IBM/fp-go/v2/array/array.go:398.18,400.4 1 1
github.com/IBM/fp-go/v2/array/array.go:401.3,401.25 1 1
github.com/IBM/fp-go/v2/array/array.go:406.60,407.36 1 1
github.com/IBM/fp-go/v2/array/array.go:407.36,409.3 1 1
github.com/IBM/fp-go/v2/array/array.go:419.36,421.2 1 1
github.com/IBM/fp-go/v2/array/array.go:424.49,426.2 1 1
github.com/IBM/fp-go/v2/array/array.go:432.49,434.2 1 1
github.com/IBM/fp-go/v2/array/array.go:440.42,442.2 1 0
github.com/IBM/fp-go/v2/array/array.go:447.30,449.2 1 1
github.com/IBM/fp-go/v2/array/array.go:456.78,458.2 1 0
github.com/IBM/fp-go/v2/array/array.go:464.75,466.2 1 1
github.com/IBM/fp-go/v2/array/array.go:469.32,471.2 1 0
github.com/IBM/fp-go/v2/array/array.go:474.35,476.2 1 0
github.com/IBM/fp-go/v2/array/array.go:479.28,481.2 1 0
github.com/IBM/fp-go/v2/array/array.go:486.50,488.2 1 0
github.com/IBM/fp-go/v2/array/array.go:493.29,495.2 1 0
github.com/IBM/fp-go/v2/array/array.go:500.47,502.2 1 0
github.com/IBM/fp-go/v2/array/array.go:507.67,509.2 1 1
github.com/IBM/fp-go/v2/array/array.go:514.81,516.2 1 0
github.com/IBM/fp-go/v2/array/array.go:521.45,523.2 1 0
github.com/IBM/fp-go/v2/array/array.go:528.38,530.2 1 0
github.com/IBM/fp-go/v2/array/array.go:605.43,607.2 1 1
github.com/IBM/fp-go/v2/array/array.go:613.52,615.2 1 0
github.com/IBM/fp-go/v2/array/array.go:621.49,623.2 1 0
github.com/IBM/fp-go/v2/array/array.go:628.44,630.2 1 0
github.com/IBM/fp-go/v2/array/array.go:714.33,716.2 1 1
github.com/IBM/fp-go/v2/array/array.go:780.53,781.26 1 1
github.com/IBM/fp-go/v2/array/array.go:781.26,782.47 1 1
github.com/IBM/fp-go/v2/array/array.go:782.47,782.67 1 1
github.com/IBM/fp-go/v2/array/array.go:839.31,841.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:36.7,38.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:60.20,62.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:91.20,93.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:120.20,122.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:143.19,145.2 1 1
github.com/IBM/fp-go/v2/array/bind.go:166.20,168.2 1 1
github.com/IBM/fp-go/v2/array/eq.go:35.37,37.49 2 1
github.com/IBM/fp-go/v2/array/eq.go:37.49,39.3 1 1
github.com/IBM/fp-go/v2/array/eq.go:43.45,45.2 1 1
github.com/IBM/fp-go/v2/array/find.go:33.65,35.2 1 1
github.com/IBM/fp-go/v2/array/find.go:48.79,50.2 1 1
github.com/IBM/fp-go/v2/array/find.go:68.78,70.2 1 1
github.com/IBM/fp-go/v2/array/find.go:76.89,78.2 1 1
github.com/IBM/fp-go/v2/array/find.go:89.64,91.2 1 1
github.com/IBM/fp-go/v2/array/find.go:97.78,99.2 1 1
github.com/IBM/fp-go/v2/array/find.go:105.77,107.2 1 1
github.com/IBM/fp-go/v2/array/find.go:113.88,115.2 1 1
github.com/IBM/fp-go/v2/array/magma.go:38.50,40.2 1 1
github.com/IBM/fp-go/v2/array/monad.go:39.65,41.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:35.36,37.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:48.42,50.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:52.45,54.2 1 1
github.com/IBM/fp-go/v2/array/monoid.go:68.45,73.48 3 1
github.com/IBM/fp-go/v2/array/monoid.go:73.48,75.3 1 1
github.com/IBM/fp-go/v2/array/monoid.go:77.2,77.12 1 1
github.com/IBM/fp-go/v2/array/sequence.go:27.19,29.2 1 1
github.com/IBM/fp-go/v2/array/sequence.go:69.22,71.2 1 1
github.com/IBM/fp-go/v2/array/sequence.go:92.53,98.2 1 1
github.com/IBM/fp-go/v2/array/sort.go:35.47,37.2 1 1
github.com/IBM/fp-go/v2/array/sort.go:65.68,67.2 1 1
github.com/IBM/fp-go/v2/array/sort.go:96.51,98.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:66.34,68.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:83.24,86.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:94.39,96.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:105.29,108.2 1 1
github.com/IBM/fp-go/v2/array/traverse.go:110.142,117.46 1 1
github.com/IBM/fp-go/v2/array/traverse.go:117.46,118.54 1 1
github.com/IBM/fp-go/v2/array/traverse.go:118.54,125.4 1 1
github.com/IBM/fp-go/v2/array/uniq.go:20.43,22.2 1 1
github.com/IBM/fp-go/v2/array/uniq.go:49.60,51.2 1 1
github.com/IBM/fp-go/v2/array/zip.go:38.73,40.2 1 1
github.com/IBM/fp-go/v2/array/zip.go:58.55,60.2 1 1
github.com/IBM/fp-go/v2/array/zip.go:81.62,83.2 1 1

View File

@@ -21,7 +21,7 @@ import (
FC "github.com/IBM/fp-go/v2/internal/functor"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// Of constructs a single element array
@@ -215,7 +215,7 @@ func Filter[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) AS {
return FilterWithIndex[AS](F.Ignore1of2[int](pred))
}
func FilterChain[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
func ChainOptionK[GA ~[]A, GB ~[]B, A, B any](f func(a A) O.Option[GB]) func(GA) GB {
return F.Flow2(
FilterMap[GA, []GB](f),
Flatten[[]GB],
@@ -234,7 +234,7 @@ func FilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) O.Option[B])
return F.Bind2nd(MonadFilterMapWithIndex[GA, GB, A, B], f)
}
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, GA] {
func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) pair.Pair[GA, GA] {
left := Empty[GA]()
right := Empty[GA]()
array.Reduce(as, func(c bool, a A) bool {
@@ -246,10 +246,10 @@ func MonadPartition[GA ~[]A, A any](as GA, pred func(A) bool) tuple.Tuple2[GA, G
return c
}, true)
// returns the partition
return tuple.MakeTuple2(left, right)
return pair.MakePair(left, right)
}
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] {
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) pair.Pair[GA, GA] {
return F.Bind2nd(MonadPartition[GA, A], pred)
}

View File

@@ -18,7 +18,7 @@ package generic
import (
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one
@@ -34,19 +34,19 @@ func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, f
// Zip takes two arrays and returns an array of corresponding pairs. If one input array is short, excess elements of the
// longer array are discarded
func Zip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) T.Tuple2[A, B]])(fb, T.MakeTuple2[A, B])
func Zip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](fb BS) func(AS) CS {
return F.Bind23of3(ZipWith[AS, BS, CS, func(A, B) pair.Pair[A, B]])(fb, pair.MakePair[A, B])
}
// Unzip is the function is reverse of [Zip]. Takes an array of pairs and return two corresponding arrays
func Unzip[AS ~[]A, BS ~[]B, CS ~[]T.Tuple2[A, B], A, B any](cs CS) T.Tuple2[AS, BS] {
func Unzip[AS ~[]A, BS ~[]B, CS ~[]pair.Pair[A, B], A, B any](cs CS) pair.Pair[AS, BS] {
l := len(cs)
as := make(AS, l)
bs := make(BS, l)
for i := range l {
t := cs[i]
as[i] = t.F1
bs[i] = t.F2
as[i] = pair.Head(t)
bs[i] = pair.Tail(t)
}
return T.MakeTuple2(as, bs)
return pair.MakePair(as, bs)
}

View File

@@ -764,14 +764,14 @@ func TestFoldMap(t *testing.T) {
t.Run("FoldMap with sum semigroup", func(t *testing.T) {
sumSemigroup := N.SemigroupSum[int]()
arr := From(1, 2, 3, 4)
result := FoldMap[int, int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
result := FoldMap[int](sumSemigroup)(func(x int) int { return x * 2 })(arr)
assert.Equal(t, 20, result) // (1*2) + (2*2) + (3*2) + (4*2) = 20
})
t.Run("FoldMap with string concatenation", func(t *testing.T) {
concatSemigroup := STR.Semigroup
arr := From(1, 2, 3)
result := FoldMap[int, string](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
result := FoldMap[int](concatSemigroup)(func(x int) string { return fmt.Sprintf("%d", x) })(arr)
assert.Equal(t, "123", result)
})
}

View File

@@ -0,0 +1,78 @@
// 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 array
import (
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestSequenceWithOption tests the generic Sequence function with Option monad
func TestSequenceWithOption(t *testing.T) {
// Test with Option monad - all Some values
opts := From(
O.Some(1),
O.Some(2),
O.Some(3),
)
// Use the Sequence function with Option's applicative monoid
monoid := O.ApplicativeMonoid(Monoid[int]())
seq := Sequence(O.Map(Of[int]), monoid)
result := seq(opts)
assert.Equal(t, O.Of(From(1, 2, 3)), result)
// Test with Option monad - contains None
optsWithNone := From(
O.Some(1),
O.None[int](),
O.Some(3),
)
result2 := seq(optsWithNone)
assert.True(t, O.IsNone(result2))
// Test with empty array
empty := Empty[Option[int]]()
result3 := seq(empty)
assert.Equal(t, O.Some(Empty[int]()), result3)
}
// TestMonadSequence tests the MonadSequence function
func TestMonadSequence(t *testing.T) {
// Test with Option monad
opts := From(
O.Some("hello"),
O.Some("world"),
)
monoid := O.ApplicativeMonoid(Monoid[string]())
result := MonadSequence(O.Map(Of[string]), monoid, opts)
assert.Equal(t, O.Of(From("hello", "world")), result)
// Test with None in the array
optsWithNone := From(
O.Some("hello"),
O.None[string](),
)
result2 := MonadSequence(O.Map(Of[string]), monoid, optsWithNone)
assert.Equal(t, O.None[[]string](), result2)
}

View File

@@ -16,7 +16,11 @@
package array
import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
"github.com/IBM/fp-go/v2/internal/traversable"
)
// Traverse maps each element of an array to an effect (HKT), then collects the results
@@ -55,9 +59,9 @@ import (
//
//go:inline
func Traverse[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
fof pointed.OfType[[]B, HKTRB],
fmap functor.MapType[[]B, func(B) []B, HKTRB, HKTAB],
fap apply.ApType[HKTB, HKTRB, HKTAB],
f func(A) HKTB) func([]A) HKTRB {
return array.Traverse[[]A](fof, fmap, fap, f)
@@ -71,7 +75,7 @@ func Traverse[A, B, HKTB, HKTAB, HKTRB any](
//
//go:inline
func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fof pointed.OfType[[]B, HKTRB],
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
@@ -83,7 +87,7 @@ func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
//go:inline
func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fof pointed.OfType[[]B, HKTRB],
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
@@ -93,7 +97,7 @@ func TraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
//go:inline
func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fof pointed.OfType[[]B, HKTRB],
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
@@ -102,3 +106,22 @@ func MonadTraverseWithIndex[A, B, HKTB, HKTAB, HKTRB any](
return array.MonadTraverseWithIndex(fof, fmap, fap, ta, f)
}
func MakeTraverseType[A, B, HKT_F_B, HKT_F_T_B, HKT_F_B_T_B any]() traversable.TraverseType[A, B, []A, []B, HKT_F_B, HKT_F_T_B, HKT_F_B_T_B] {
return func(
// ap
fof_b pointed.OfType[[]B, HKT_F_T_B],
fmap_b functor.MapType[[]B, func(B) []B, HKT_F_T_B, HKT_F_B_T_B],
fap_b apply.ApType[HKT_F_B, HKT_F_T_B, HKT_F_B_T_B],
) func(func(A) HKT_F_B) func([]A) HKT_F_T_B {
return func(f func(A) HKT_F_B) func([]A) HKT_F_T_B {
return Traverse(
fof_b,
fmap_b,
fap_b,
f,
)
}
}
}

View File

@@ -0,0 +1,164 @@
// 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 array
import (
"strconv"
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestMonadTraverse tests the MonadTraverse function
func TestMonadTraverse(t *testing.T) {
// Test converting integers to strings via Option
numbers := []int{1, 2, 3}
result := MonadTraverse(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(n int) O.Option[string] {
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsSome(result))
assert.Equal(t, []string{"1", "2", "3"}, O.GetOrElse(func() []string { return []string{} })(result))
// Test with a function that can return None
result2 := MonadTraverse(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(n int) O.Option[string] {
if n == 2 {
return O.None[string]()
}
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsNone(result2))
// Test with empty array
empty := []int{}
result3 := MonadTraverse(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
empty,
func(n int) O.Option[string] {
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsSome(result3))
assert.Equal(t, []string{}, O.GetOrElse(func() []string { return nil })(result3))
}
// TestTraverseWithIndex tests the TraverseWithIndex function
func TestTraverseWithIndex(t *testing.T) {
// Test with index-aware transformation
words := []string{"a", "b", "c"}
traverser := TraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
func(idx int, s string) O.Option[string] {
return O.Some(s + strconv.Itoa(idx))
},
)
result := traverser(words)
assert.True(t, O.IsSome(result))
assert.Equal(t, []string{"a0", "b1", "c2"}, O.GetOrElse(func() []string { return []string{} })(result))
// Test with conditional None based on index
traverser2 := TraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
func(idx int, s string) O.Option[string] {
if idx == 1 {
return O.None[string]()
}
return O.Some(s)
},
)
result2 := traverser2(words)
assert.True(t, O.IsNone(result2))
}
// TestMonadTraverseWithIndex tests the MonadTraverseWithIndex function
func TestMonadTraverseWithIndex(t *testing.T) {
// Test with index-aware transformation
numbers := []int{10, 20, 30}
result := MonadTraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(idx, n int) O.Option[string] {
return O.Some(strconv.Itoa(n * idx))
},
)
assert.True(t, O.IsSome(result))
// Expected: [10*0, 20*1, 30*2] = ["0", "20", "60"]
assert.Equal(t, []string{"0", "20", "60"}, O.GetOrElse(func() []string { return []string{} })(result))
// Test with None at specific index
result2 := MonadTraverseWithIndex(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
numbers,
func(idx, n int) O.Option[string] {
if idx == 2 {
return O.None[string]()
}
return O.Some(strconv.Itoa(n))
},
)
assert.True(t, O.IsNone(result2))
}
// TestMakeTraverseType tests the MakeTraverseType function
func TestMakeTraverseType(t *testing.T) {
// Create a traverse type for Option
traverseType := MakeTraverseType[int, string, O.Option[string], O.Option[[]string], O.Option[func(string) []string]]()
// Use it to traverse an array
numbers := []int{1, 2, 3}
result := traverseType(
O.Of[[]string],
O.Map[[]string, func(string) []string],
O.Ap[[]string, string],
)(func(n int) O.Option[string] {
return O.Some(strconv.Itoa(n * 2))
})(numbers)
assert.True(t, O.IsSome(result))
assert.Equal(t, []string{"2", "4", "6"}, O.GetOrElse(func() []string { return []string{} })(result))
}

View File

@@ -17,7 +17,7 @@ package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays,
@@ -55,8 +55,8 @@ func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
// // Result: [(a, 1), (b, 2)]
//
//go:inline
func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
return G.Zip[[]A, []B, []T.Tuple2[A, B]](fb)
func Zip[A, B any](fb []B) func([]A) []pair.Pair[A, B] {
return G.Zip[[]A, []B, []pair.Pair[A, B]](fb)
}
// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns
@@ -78,6 +78,6 @@ func Zip[A, B any](fb []B) func([]A) []T.Tuple2[A, B] {
// ages := result.Tail // [30, 25, 35]
//
//go:inline
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
func Unzip[A, B any](cs []pair.Pair[A, B]) pair.Pair[[]A, []B] {
return G.Unzip[[]A, []B](cs)
}

View File

@@ -19,7 +19,7 @@ import (
"fmt"
"testing"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/pair"
"github.com/stretchr/testify/assert"
)
@@ -40,7 +40,7 @@ func TestZip(t *testing.T) {
res := Zip[string](left)(right)
assert.Equal(t, From(T.MakeTuple2("a", 1), T.MakeTuple2("b", 2), T.MakeTuple2("c", 3)), res)
assert.Equal(t, From(pair.MakePair("a", 1), pair.MakePair("b", 2), pair.MakePair("c", 3)), res)
}
func TestUnzip(t *testing.T) {
@@ -51,6 +51,6 @@ func TestUnzip(t *testing.T) {
unzipped := Unzip(zipped)
assert.Equal(t, right, unzipped.F1)
assert.Equal(t, left, unzipped.F2)
assert.Equal(t, right, pair.Head(unzipped))
assert.Equal(t, left, pair.Tail(unzipped))
}

View File

@@ -194,6 +194,25 @@ func ArrayNotEmpty[T any](arr []T) Reader {
}
}
// ArrayEmpty checks if an array is empty.
//
// This is the complement of ArrayNotEmpty, asserting that a slice has no elements.
//
// Example:
//
// func TestArrayEmpty(t *testing.T) {
// empty := []int{}
// assert.ArrayEmpty(empty)(t) // Passes
//
// numbers := []int{1, 2, 3}
// assert.ArrayEmpty(numbers)(t) // Fails
// }
func ArrayEmpty[T any](arr []T) Reader {
return func(t *testing.T) bool {
return assert.Empty(t, arr)
}
}
// RecordNotEmpty checks if a map is not empty.
//
// Example:
@@ -211,6 +230,25 @@ func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
}
}
// RecordEmpty checks if a map is empty.
//
// This is the complement of RecordNotEmpty, asserting that a map has no key-value pairs.
//
// Example:
//
// func TestRecordEmpty(t *testing.T) {
// empty := map[string]int{}
// assert.RecordEmpty(empty)(t) // Passes
//
// config := map[string]int{"timeout": 30}
// assert.RecordEmpty(config)(t) // Fails
// }
func RecordEmpty[K comparable, T any](mp map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Empty(t, mp)
}
}
// StringNotEmpty checks if a string is not empty.
//
// Example:
@@ -504,15 +542,7 @@ func AllOf(readers []Reader) Reader {
//
//go:inline
func RunAll(testcases map[string]Reader) Reader {
return func(t *testing.T) bool {
current := true
for k, r := range testcases {
current = current && t.Run(k, func(t1 *testing.T) {
r(t1)
})
}
return current
}
return SequenceRecord(testcases)
}
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,

View File

@@ -85,6 +85,33 @@ func TestArrayNotEmpty(t *testing.T) {
})
}
func TestArrayEmpty(t *testing.T) {
t.Run("should pass for empty array", func(t *testing.T) {
arr := []int{}
result := ArrayEmpty(arr)(t)
if !result {
t.Error("Expected ArrayEmpty to pass for empty array")
}
})
t.Run("should fail for non-empty array", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{1, 2, 3}
result := ArrayEmpty(arr)(mockT)
if result {
t.Error("Expected ArrayEmpty to fail for non-empty array")
}
})
t.Run("should work with different types", func(t *testing.T) {
strArr := []string{}
result := ArrayEmpty(strArr)(t)
if !result {
t.Error("Expected ArrayEmpty to pass for empty string array")
}
})
}
func TestRecordNotEmpty(t *testing.T) {
t.Run("should pass for non-empty map", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
@@ -131,6 +158,33 @@ func TestArrayLength(t *testing.T) {
})
}
func TestRecordEmpty(t *testing.T) {
t.Run("should pass for empty map", func(t *testing.T) {
mp := map[string]int{}
result := RecordEmpty(mp)(t)
if !result {
t.Error("Expected RecordEmpty to pass for empty map")
}
})
t.Run("should fail for non-empty map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1, "b": 2}
result := RecordEmpty(mp)(mockT)
if result {
t.Error("Expected RecordEmpty to fail for non-empty map")
}
})
t.Run("should work with different key-value types", func(t *testing.T) {
mp := map[int]string{}
result := RecordEmpty(mp)(t)
if !result {
t.Error("Expected RecordEmpty to pass for empty map with int keys")
}
})
}
func TestRecordLength(t *testing.T) {
t.Run("should pass when map length matches", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
@@ -150,6 +204,33 @@ func TestRecordLength(t *testing.T) {
})
}
func TestStringNotEmpty(t *testing.T) {
t.Run("should pass for non-empty string", func(t *testing.T) {
str := "Hello, World!"
result := StringNotEmpty(str)(t)
if !result {
t.Error("Expected StringNotEmpty to pass for non-empty string")
}
})
t.Run("should fail for empty string", func(t *testing.T) {
mockT := &testing.T{}
str := ""
result := StringNotEmpty(str)(mockT)
if result {
t.Error("Expected StringNotEmpty to fail for empty string")
}
})
t.Run("should pass for string with whitespace", func(t *testing.T) {
str := " "
result := StringNotEmpty(str)(t)
if !result {
t.Error("Expected StringNotEmpty to pass for string with whitespace")
}
})
}
func TestStringLength(t *testing.T) {
t.Run("should pass when string length matches", func(t *testing.T) {
str := "hello"

122
v2/assert/from.go Normal file
View File

@@ -0,0 +1,122 @@
// 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 assert
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
// FromReaderIOResult converts a ReaderIOResult[Reader] into a Reader.
//
// This function bridges the gap between context-aware, IO-based computations that may fail
// (ReaderIOResult) and the simpler Reader type used for test assertions. It executes the
// ReaderIOResult computation using the test's context, handles any potential errors by
// converting them to test failures via NoError, and returns the resulting Reader.
//
// The conversion process:
// 1. Executes the ReaderIOResult with the test context (t.Context())
// 2. Runs the resulting IO operation ()
// 3. Extracts the Result, converting errors to test failures using NoError
// 4. Returns a Reader that can be applied to *testing.T
//
// This is particularly useful when you have test assertions that need to:
// - Access context for cancellation or deadlines
// - Perform IO operations (file access, network calls, etc.)
// - Handle potential errors gracefully in tests
//
// Parameters:
// - ri: A ReaderIOResult that produces a Reader when given a context and executed
//
// Returns:
// - A Reader that can be directly applied to *testing.T for assertion
//
// Example:
//
// func TestWithContext(t *testing.T) {
// // Create a ReaderIOResult that performs an IO operation
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
// return func() result.Result[assert.Reader] {
// // Simulate database check
// if err := db.PingContext(ctx); err != nil {
// return result.Error[assert.Reader](err)
// }
// return result.Of[assert.Reader](assert.NoError(nil))
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIOResult(checkDatabase)
// assertion(t)
// }
func FromReaderIOResult(ri ReaderIOResult[Reader]) Reader {
return func(t *testing.T) bool {
return F.Pipe1(
ri(t.Context())(),
result.GetOrElse(NoError),
)(t)
}
}
// FromReaderIO converts a ReaderIO[Reader] into a Reader.
//
// This function bridges the gap between context-aware, IO-based computations (ReaderIO)
// and the simpler Reader type used for test assertions. It executes the ReaderIO
// computation using the test's context and returns the resulting Reader.
//
// Unlike FromReaderIOResult, this function does not handle errors explicitly - it assumes
// the IO operation will succeed or that any errors are handled within the ReaderIO itself.
//
// The conversion process:
// 1. Executes the ReaderIO with the test context (t.Context())
// 2. Runs the resulting IO operation ()
// 3. Returns a Reader that can be applied to *testing.T
//
// This is particularly useful when you have test assertions that need to:
// - Access context for cancellation or deadlines
// - Perform IO operations that don't fail (or handle failures internally)
// - Integrate with context-aware testing utilities
//
// Parameters:
// - ri: A ReaderIO that produces a Reader when given a context and executed
//
// Returns:
// - A Reader that can be directly applied to *testing.T for assertion
//
// Example:
//
// func TestWithIO(t *testing.T) {
// // Create a ReaderIO that performs an IO operation
// logAndCheck := func(ctx context.Context) func() assert.Reader {
// return func() assert.Reader {
// // Log something using context
// logger.InfoContext(ctx, "Running test")
// // Return an assertion
// return assert.Equal(42)(computeValue())
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIO(logAndCheck)
// assertion(t)
// }
func FromReaderIO(ri ReaderIO[Reader]) Reader {
return func(t *testing.T) bool {
return ri(t.Context())()(t)
}
}

383
v2/assert/from_test.go Normal file
View File

@@ -0,0 +1,383 @@
// 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 assert
import (
"context"
"errors"
"testing"
"github.com/IBM/fp-go/v2/result"
)
func TestFromReaderIOResult(t *testing.T) {
t.Run("should pass when ReaderIOResult returns success with passing assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns a successful Reader
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Return a Reader that always passes
return result.Of[Reader](func(t *testing.T) bool {
return true
})
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass when ReaderIOResult returns success")
}
})
t.Run("should pass when ReaderIOResult returns success with Equal assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns a successful Equal assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](Equal(42)(42))
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with Equal assertion")
}
})
t.Run("should fail when ReaderIOResult returns error", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIOResult that returns an error
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Left[Reader](errors.New("test error"))
}
}
reader := FromReaderIOResult(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIOResult to fail when ReaderIOResult returns error")
}
})
t.Run("should fail when ReaderIOResult returns success but assertion fails", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIOResult that returns a failing assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](Equal(42)(43))
}
}
reader := FromReaderIOResult(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIOResult to fail when assertion fails")
}
})
t.Run("should use test context", func(t *testing.T) {
contextUsed := false
// Create a ReaderIOResult that checks if context is provided
ri := func(ctx context.Context) func() result.Result[Reader] {
if ctx != nil {
contextUsed = true
}
return func() result.Result[Reader] {
return result.Of[Reader](func(t *testing.T) bool {
return true
})
}
}
reader := FromReaderIOResult(ri)
reader(t)
if !contextUsed {
t.Error("Expected FromReaderIOResult to use test context")
}
})
t.Run("should work with NoError assertion", func(t *testing.T) {
// Create a ReaderIOResult that returns NoError assertion
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
return result.Of[Reader](NoError(nil))
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with NoError assertion")
}
})
t.Run("should work with complex assertions", func(t *testing.T) {
// Create a ReaderIOResult with multiple composed assertions
ri := func(ctx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
arr := []int{1, 2, 3}
assertions := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](3)(arr),
ArrayContains(2)(arr),
})
return result.Of[Reader](assertions)
}
}
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIOResult to pass with complex assertions")
}
})
}
func TestFromReaderIO(t *testing.T) {
t.Run("should pass when ReaderIO returns passing assertion", func(t *testing.T) {
// Create a ReaderIO that returns a Reader that always passes
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return func(t *testing.T) bool {
return true
}
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass when ReaderIO returns passing assertion")
}
})
t.Run("should pass when ReaderIO returns Equal assertion", func(t *testing.T) {
// Create a ReaderIO that returns an Equal assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Equal(42)(42)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Equal assertion")
}
})
t.Run("should fail when ReaderIO returns failing assertion", func(t *testing.T) {
mockT := &testing.T{}
// Create a ReaderIO that returns a failing assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Equal(42)(43)
}
}
reader := FromReaderIO(ri)
res := reader(mockT)
if res {
t.Error("Expected FromReaderIO to fail when assertion fails")
}
})
t.Run("should use test context", func(t *testing.T) {
contextUsed := false
// Create a ReaderIO that checks if context is provided
ri := func(ctx context.Context) func() Reader {
if ctx != nil {
contextUsed = true
}
return func() Reader {
return func(t *testing.T) bool {
return true
}
}
}
reader := FromReaderIO(ri)
reader(t)
if !contextUsed {
t.Error("Expected FromReaderIO to use test context")
}
})
t.Run("should work with NoError assertion", func(t *testing.T) {
// Create a ReaderIO that returns NoError assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return NoError(nil)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with NoError assertion")
}
})
t.Run("should work with Error assertion", func(t *testing.T) {
// Create a ReaderIO that returns Error assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
return Error(errors.New("expected error"))
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Error assertion")
}
})
t.Run("should work with complex assertions", func(t *testing.T) {
// Create a ReaderIO with multiple composed assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
mp := map[string]int{"a": 1, "b": 2}
return AllOf([]Reader{
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
ContainsKey[int]("a")(mp),
})
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with complex assertions")
}
})
t.Run("should work with string assertions", func(t *testing.T) {
// Create a ReaderIO with string assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
str := "hello world"
return AllOf([]Reader{
StringNotEmpty(str),
StringLength[any, any](11)(str),
})
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with string assertions")
}
})
t.Run("should work with Result assertions", func(t *testing.T) {
// Create a ReaderIO with Result assertions
ri := func(ctx context.Context) func() Reader {
return func() Reader {
successResult := result.Of[int](42)
return Success(successResult)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Success assertion")
}
})
t.Run("should work with Failure assertion", func(t *testing.T) {
// Create a ReaderIO with Failure assertion
ri := func(ctx context.Context) func() Reader {
return func() Reader {
failureResult := result.Left[int](errors.New("test error"))
return Failure(failureResult)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected FromReaderIO to pass with Failure assertion")
}
})
}
// TestFromReaderIOResultIntegration tests integration scenarios
func TestFromReaderIOResultIntegration(t *testing.T) {
t.Run("should work in a realistic scenario with context cancellation", func(t *testing.T) {
// Create a ReaderIOResult that uses the context
ri := func(testCtx context.Context) func() result.Result[Reader] {
return func() result.Result[Reader] {
// Check if context is valid
if testCtx == nil {
return result.Left[Reader](errors.New("context is nil"))
}
// Return a successful assertion
return result.Of[Reader](Equal("test")("test"))
}
}
// Use the actual testing.T from the subtest
reader := FromReaderIOResult(ri)
res := reader(t)
if !res {
t.Error("Expected integration test to pass")
}
})
}
// TestFromReaderIOIntegration tests integration scenarios
func TestFromReaderIOIntegration(t *testing.T) {
t.Run("should work in a realistic scenario with logging", func(t *testing.T) {
logCalled := false
// Create a ReaderIO that simulates logging
ri := func(ctx context.Context) func() Reader {
return func() Reader {
// Simulate logging with context
if ctx != nil {
logCalled = true
}
// Return an assertion
return Equal(100)(100)
}
}
reader := FromReaderIO(ri)
res := reader(t)
if !res {
t.Error("Expected integration test to pass")
}
if !logCalled {
t.Error("Expected logging to be called")
}
})
}

207
v2/assert/logger.go Normal file
View File

@@ -0,0 +1,207 @@
// 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 assert
import (
"testing"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/readerio"
)
// Logf creates a logging function that outputs formatted test messages using Go's testing.T.Logf.
//
// This function provides a functional programming approach to test logging, returning a
// [ReaderIO] that can be composed with other test operations. It's particularly useful
// for debugging tests, tracing execution flow, or documenting test behavior without
// affecting test outcomes.
//
// The function uses a curried design pattern:
// 1. First, you provide a format string (prefix) with format verbs (like %v, %d, %s)
// 2. This returns a function that takes a value of type T
// 3. That function returns a ReaderIO that performs the logging when executed
//
// # Parameters
//
// - prefix: A format string compatible with fmt.Printf (e.g., "Value: %v", "Count: %d")
// The format string should contain exactly one format verb that matches type T
//
// # Returns
//
// - A function that takes a value of type T and returns a [ReaderIO][*testing.T, Void]
// When executed, this ReaderIO logs the formatted message to the test output
//
// # Type Parameters
//
// - T: The type of value to be logged. Can be any type that can be formatted by fmt
//
// # Use Cases
//
// - Debugging test execution by logging intermediate values
// - Tracing the flow of complex test scenarios
// - Documenting test behavior in the test output
// - Logging values in functional pipelines without breaking the chain
// - Creating reusable logging operations for specific types
//
// # Example - Basic Logging
//
// func TestBasicLogging(t *testing.T) {
// // Create a logger for integers
// logInt := assert.Logf[int]("Processing value: %d")
//
// // Use it to log a value
// value := 42
// logInt(value)(t)() // Outputs: "Processing value: 42"
// }
//
// # Example - Logging in Test Pipeline
//
// func TestPipelineWithLogging(t *testing.T) {
// type User struct {
// Name string
// Age int
// }
//
// user := User{Name: "Alice", Age: 30}
//
// // Create a logger for User
// logUser := assert.Logf[User]("Testing user: %+v")
//
// // Log the user being tested
// logUser(user)(t)()
//
// // Continue with assertions
// assert.StringNotEmpty(user.Name)(t)
// assert.That(func(age int) bool { return age > 0 })(user.Age)(t)
// }
//
// # Example - Multiple Loggers for Different Types
//
// func TestMultipleLoggers(t *testing.T) {
// // Create type-specific loggers
// logString := assert.Logf[string]("String value: %s")
// logInt := assert.Logf[int]("Integer value: %d")
// logFloat := assert.Logf[float64]("Float value: %.2f")
//
// // Use them throughout the test
// logString("hello")(t)() // Outputs: "String value: hello"
// logInt(42)(t)() // Outputs: "Integer value: 42"
// logFloat(3.14159)(t)() // Outputs: "Float value: 3.14"
// }
//
// # Example - Logging Complex Structures
//
// func TestComplexStructureLogging(t *testing.T) {
// type Config struct {
// Host string
// Port int
// Timeout int
// }
//
// config := Config{Host: "localhost", Port: 8080, Timeout: 30}
//
// // Use %+v to include field names
// logConfig := assert.Logf[Config]("Configuration: %+v")
// logConfig(config)(t)()
// // Outputs: "Configuration: {Host:localhost Port:8080 Timeout:30}"
//
// // Or use %#v for Go-syntax representation
// logConfigGo := assert.Logf[Config]("Config (Go syntax): %#v")
// logConfigGo(config)(t)()
// // Outputs: "Config (Go syntax): assert.Config{Host:"localhost", Port:8080, Timeout:30}"
// }
//
// # Example - Debugging Test Failures
//
// func TestWithDebugLogging(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
// logSlice := assert.Logf[[]int]("Testing slice: %v")
//
// // Log the input data
// logSlice(numbers)(t)()
//
// // Perform assertions
// assert.ArrayNotEmpty(numbers)(t)
// assert.ArrayLength[int](5)(numbers)(t)
//
// // Log intermediate results
// sum := 0
// for _, n := range numbers {
// sum += n
// }
// logInt := assert.Logf[int]("Sum: %d")
// logInt(sum)(t)()
//
// assert.Equal(15)(sum)(t)
// }
//
// # Example - Conditional Logging
//
// func TestConditionalLogging(t *testing.T) {
// logDebug := assert.Logf[string]("DEBUG: %s")
//
// values := []int{1, 2, 3, 4, 5}
// for _, v := range values {
// if v%2 == 0 {
// logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
// }
// }
// // Outputs:
// // DEBUG: Found even number: 2
// // DEBUG: Found even number: 4
// }
//
// # Format Verbs
//
// Common format verbs you can use in the prefix string:
// - %v: Default format
// - %+v: Default format with field names for structs
// - %#v: Go-syntax representation
// - %T: Type of the value
// - %d: Integer in base 10
// - %s: String
// - %f: Floating point number
// - %t: Boolean (true/false)
// - %p: Pointer address
//
// See the fmt package documentation for a complete list of format verbs.
//
// # Notes
//
// - Logging does not affect test pass/fail status
// - Log output appears in test results when running with -v flag or when tests fail
// - The function returns Void, indicating it's used for side effects only
// - The ReaderIO pattern allows logging to be composed with other operations
//
// # Related Functions
//
// - [FromReaderIO]: Converts ReaderIO operations into test assertions
// - testing.T.Logf: The underlying Go testing log function
//
// # References
//
// - Go testing package: https://pkg.go.dev/testing
// - fmt package format verbs: https://pkg.go.dev/fmt
// - ReaderIO pattern: Combines Reader (context dependency) with IO (side effects)
func Logf[T any](prefix string) func(T) readerio.ReaderIO[*testing.T, Void] {
return func(a T) readerio.ReaderIO[*testing.T, Void] {
return func(t *testing.T) IO[Void] {
return io.FromImpure(func() {
t.Logf(prefix, a)
})
}
}
}

406
v2/assert/logger_test.go Normal file
View File

@@ -0,0 +1,406 @@
// 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 assert
import (
"fmt"
"testing"
)
// TestLogf_BasicInteger tests basic integer logging
func TestLogf_BasicInteger(t *testing.T) {
logInt := Logf[int]("Processing value: %d")
// This should not panic and should log the value
logInt(42)(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicString tests basic string logging
func TestLogf_BasicString(t *testing.T) {
logString := Logf[string]("String value: %s")
logString("hello world")(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicFloat tests basic float logging
func TestLogf_BasicFloat(t *testing.T) {
logFloat := Logf[float64]("Float value: %.2f")
logFloat(3.14159)(t)()
// Test passes if no panic occurs
}
// TestLogf_BasicBoolean tests basic boolean logging
func TestLogf_BasicBoolean(t *testing.T) {
logBool := Logf[bool]("Boolean value: %t")
logBool(true)(t)()
logBool(false)(t)()
// Test passes if no panic occurs
}
// TestLogf_ComplexStruct tests logging of complex structures
func TestLogf_ComplexStruct(t *testing.T) {
type User struct {
Name string
Age int
}
logUser := Logf[User]("User: %+v")
user := User{Name: "Alice", Age: 30}
logUser(user)(t)()
// Test passes if no panic occurs
}
// TestLogf_Slice tests logging of slices
func TestLogf_Slice(t *testing.T) {
logSlice := Logf[[]int]("Slice: %v")
numbers := []int{1, 2, 3, 4, 5}
logSlice(numbers)(t)()
// Test passes if no panic occurs
}
// TestLogf_Map tests logging of maps
func TestLogf_Map(t *testing.T) {
logMap := Logf[map[string]int]("Map: %v")
data := map[string]int{"a": 1, "b": 2, "c": 3}
logMap(data)(t)()
// Test passes if no panic occurs
}
// TestLogf_Pointer tests logging of pointers
func TestLogf_Pointer(t *testing.T) {
logPtr := Logf[*int]("Pointer: %p")
value := 42
logPtr(&value)(t)()
// Test passes if no panic occurs
}
// TestLogf_NilPointer tests logging of nil pointers
func TestLogf_NilPointer(t *testing.T) {
logPtr := Logf[*int]("Pointer: %v")
var nilPtr *int
logPtr(nilPtr)(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptyString tests logging of empty strings
func TestLogf_EmptyString(t *testing.T) {
logString := Logf[string]("String: '%s'")
logString("")(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptySlice tests logging of empty slices
func TestLogf_EmptySlice(t *testing.T) {
logSlice := Logf[[]int]("Slice: %v")
logSlice([]int{})(t)()
// Test passes if no panic occurs
}
// TestLogf_EmptyMap tests logging of empty maps
func TestLogf_EmptyMap(t *testing.T) {
logMap := Logf[map[string]int]("Map: %v")
logMap(map[string]int{})(t)()
// Test passes if no panic occurs
}
// TestLogf_MultipleTypes tests using multiple loggers for different types
func TestLogf_MultipleTypes(t *testing.T) {
logString := Logf[string]("String: %s")
logInt := Logf[int]("Integer: %d")
logFloat := Logf[float64]("Float: %.2f")
logString("test")(t)()
logInt(42)(t)()
logFloat(3.14)(t)()
// Test passes if no panic occurs
}
// TestLogf_WithinTestPipeline tests logging within a test pipeline
func TestLogf_WithinTestPipeline(t *testing.T) {
type Config struct {
Host string
Port int
}
config := Config{Host: "localhost", Port: 8080}
logConfig := Logf[Config]("Testing config: %+v")
logConfig(config)(t)()
// Continue with assertions
StringNotEmpty(config.Host)(t)
That(func(port int) bool { return port > 0 })(config.Port)(t)
// Test passes if no panic occurs and assertions pass
}
// TestLogf_NestedStructures tests logging of nested structures
func TestLogf_NestedStructures(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
logPerson := Logf[Person]("Person: %+v")
person := Person{
Name: "Bob",
Address: Address{
Street: "123 Main St",
City: "Springfield",
},
}
logPerson(person)(t)()
// Test passes if no panic occurs
}
// TestLogf_Interface tests logging of interface values
func TestLogf_Interface(t *testing.T) {
logAny := Logf[any]("Value: %v")
logAny(42)(t)()
logAny("string")(t)()
logAny([]int{1, 2, 3})(t)()
// Test passes if no panic occurs
}
// TestLogf_GoSyntaxFormat tests logging with Go-syntax format
func TestLogf_GoSyntaxFormat(t *testing.T) {
type Point struct {
X int
Y int
}
logPoint := Logf[Point]("Point: %#v")
point := Point{X: 10, Y: 20}
logPoint(point)(t)()
// Test passes if no panic occurs
}
// TestLogf_TypeFormat tests logging with type format
func TestLogf_TypeFormat(t *testing.T) {
logType := Logf[any]("Type: %T, Value: %v")
logType(42)(t)()
logType("string")(t)()
logType(3.14)(t)()
// Test passes if no panic occurs
}
// TestLogf_LargeNumbers tests logging of large numbers
func TestLogf_LargeNumbers(t *testing.T) {
logInt := Logf[int64]("Large number: %d")
logInt(9223372036854775807)(t)() // Max int64
// Test passes if no panic occurs
}
// TestLogf_NegativeNumbers tests logging of negative numbers
func TestLogf_NegativeNumbers(t *testing.T) {
logInt := Logf[int]("Number: %d")
logInt(-42)(t)()
logInt(-100)(t)()
// Test passes if no panic occurs
}
// TestLogf_SpecialFloats tests logging of special float values
func TestLogf_SpecialFloats(t *testing.T) {
logFloat := Logf[float64]("Float: %v")
logFloat(0.0)(t)()
logFloat(-0.0)(t)()
// Test passes if no panic occurs
}
// TestLogf_UnicodeStrings tests logging of unicode strings
func TestLogf_UnicodeStrings(t *testing.T) {
logString := Logf[string]("Unicode: %s")
logString("Hello, 世界")(t)()
logString("Emoji: 🎉🎊")(t)()
// Test passes if no panic occurs
}
// TestLogf_MultilineStrings tests logging of multiline strings
func TestLogf_MultilineStrings(t *testing.T) {
logString := Logf[string]("Multiline:\n%s")
multiline := `Line 1
Line 2
Line 3`
logString(multiline)(t)()
// Test passes if no panic occurs
}
// TestLogf_ReuseLogger tests reusing the same logger multiple times
func TestLogf_ReuseLogger(t *testing.T) {
logInt := Logf[int]("Value: %d")
for i := 0; i < 5; i++ {
logInt(i)(t)()
}
// Test passes if no panic occurs
}
// TestLogf_ConditionalLogging tests conditional logging based on values
func TestLogf_ConditionalLogging(t *testing.T) {
logDebug := Logf[string]("DEBUG: %s")
values := []int{1, 2, 3, 4, 5}
for _, v := range values {
if v%2 == 0 {
logDebug(fmt.Sprintf("Found even number: %d", v))(t)()
}
}
// Test passes if no panic occurs
}
// TestLogf_WithAssertions tests combining logging with assertions
func TestLogf_WithAssertions(t *testing.T) {
logInt := Logf[int]("Testing value: %d")
value := 42
logInt(value)(t)()
// Perform assertion after logging
Equal(42)(value)(t)
// Test passes if assertion passes
}
// TestLogf_DebuggingFailures demonstrates using logging to debug test failures
func TestLogf_DebuggingFailures(t *testing.T) {
logSlice := Logf[[]int]("Input slice: %v")
logInt := Logf[int]("Computed sum: %d")
numbers := []int{1, 2, 3, 4, 5}
logSlice(numbers)(t)()
sum := 0
for _, n := range numbers {
sum += n
}
logInt(sum)(t)()
Equal(15)(sum)(t)
// Test passes if assertion passes
}
// TestLogf_ComplexDataStructures tests logging of complex nested data
func TestLogf_ComplexDataStructures(t *testing.T) {
type Metadata struct {
Version string
Tags []string
}
type Document struct {
ID int
Title string
Metadata Metadata
}
logDoc := Logf[Document]("Document: %+v")
doc := Document{
ID: 1,
Title: "Test Document",
Metadata: Metadata{
Version: "1.0",
Tags: []string{"test", "example"},
},
}
logDoc(doc)(t)()
// Test passes if no panic occurs
}
// TestLogf_ArrayTypes tests logging of array types
func TestLogf_ArrayTypes(t *testing.T) {
logArray := Logf[[5]int]("Array: %v")
arr := [5]int{1, 2, 3, 4, 5}
logArray(arr)(t)()
// Test passes if no panic occurs
}
// TestLogf_ChannelTypes tests logging of channel types
func TestLogf_ChannelTypes(t *testing.T) {
logChan := Logf[chan int]("Channel: %v")
ch := make(chan int, 1)
logChan(ch)(t)()
close(ch)
// Test passes if no panic occurs
}
// TestLogf_FunctionTypes tests logging of function types
func TestLogf_FunctionTypes(t *testing.T) {
logFunc := Logf[func() int]("Function: %v")
fn := func() int { return 42 }
logFunc(fn)(t)()
// Test passes if no panic occurs
}

152
v2/assert/monoid.go Normal file
View File

@@ -0,0 +1,152 @@
// 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 assert
import (
"testing"
"github.com/IBM/fp-go/v2/boolean"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/reader"
)
// ApplicativeMonoid returns a [monoid.Monoid] for combining test assertion [Reader]s.
//
// This monoid combines multiple test assertions using logical AND (conjunction) semantics,
// meaning all assertions must pass for the combined assertion to pass. It leverages the
// applicative structure of Reader to execute multiple assertions with the same testing.T
// context and combines their boolean results using boolean.MonoidAll (logical AND).
//
// The monoid provides:
// - Concat: Combines two assertions such that both must pass (logical AND)
// - Empty: Returns an assertion that always passes (identity element)
//
// This is particularly useful for:
// - Composing multiple test assertions into a single assertion
// - Building complex test conditions from simpler ones
// - Creating reusable assertion combinators
// - Implementing test assertion DSLs
//
// # Monoid Laws
//
// The returned monoid satisfies the standard monoid laws:
//
// 1. Associativity:
// Concat(Concat(a1, a2), a3) ≡ Concat(a1, Concat(a2, a3))
//
// 2. Left Identity:
// Concat(Empty(), a) ≡ a
//
// 3. Right Identity:
// Concat(a, Empty()) ≡ a
//
// # Returns
//
// - A [monoid.Monoid][Reader] that combines assertions using logical AND
//
// # Example - Basic Usage
//
// func TestUserValidation(t *testing.T) {
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
// m := assert.ApplicativeMonoid()
//
// // Combine multiple assertions
// assertion := m.Concat(
// assert.Equal("Alice")(user.Name),
// m.Concat(
// assert.Equal(30)(user.Age),
// assert.StringNotEmpty(user.Email),
// ),
// )
//
// // Execute combined assertion
// assertion(t) // All three assertions must pass
// }
//
// # Example - Building Reusable Validators
//
// func TestWithReusableValidators(t *testing.T) {
// m := assert.ApplicativeMonoid()
//
// // Create a reusable validator
// validateUser := func(u User) assert.Reader {
// return m.Concat(
// assert.StringNotEmpty(u.Name),
// m.Concat(
// assert.True(u.Age > 0),
// assert.StringContains("@")(u.Email),
// ),
// )
// }
//
// user := User{Name: "Bob", Age: 25, Email: "bob@test.com"}
// validateUser(user)(t)
// }
//
// # Example - Using Empty for Identity
//
// func TestEmptyIdentity(t *testing.T) {
// m := assert.ApplicativeMonoid()
// assertion := assert.Equal(42)(42)
//
// // Empty is the identity - these are equivalent
// result1 := m.Concat(m.Empty(), assertion)(t)
// result2 := m.Concat(assertion, m.Empty())(t)
// result3 := assertion(t)
// // All three produce the same result
// }
//
// # Example - Combining with AllOf
//
// func TestCombiningWithAllOf(t *testing.T) {
// // ApplicativeMonoid provides the underlying mechanism for AllOf
// arr := []int{1, 2, 3, 4, 5}
//
// // These are conceptually equivalent:
// m := assert.ApplicativeMonoid()
// manual := m.Concat(
// assert.ArrayNotEmpty(arr),
// m.Concat(
// assert.ArrayLength[int](5)(arr),
// assert.ArrayContains(3)(arr),
// ),
// )
//
// // AllOf uses ApplicativeMonoid internally
// convenient := assert.AllOf([]assert.Reader{
// assert.ArrayNotEmpty(arr),
// assert.ArrayLength[int](5)(arr),
// assert.ArrayContains(3)(arr),
// })
//
// manual(t)
// convenient(t)
// }
//
// # Related Functions
//
// - [AllOf]: Convenient wrapper for combining multiple assertions using this monoid
// - [boolean.MonoidAll]: The underlying boolean monoid (logical AND with true as identity)
// - [reader.ApplicativeMonoid]: Generic applicative monoid for Reader types
//
// # References
//
// - Haskell Monoid: https://hackage.haskell.org/package/base/docs/Data-Monoid.html
// - Applicative Functors: https://hackage.haskell.org/package/base/docs/Control-Applicative.html
// - Boolean Monoid (All): https://hackage.haskell.org/package/base/docs/Data-Monoid.html#t:All
func ApplicativeMonoid() monoid.Monoid[Reader] {
return reader.ApplicativeMonoid[*testing.T](boolean.MonoidAll)
}

454
v2/assert/monoid_test.go Normal file
View File

@@ -0,0 +1,454 @@
// 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 assert
import (
"testing"
)
// TestApplicativeMonoid_Empty tests that Empty returns an assertion that always passes
func TestApplicativeMonoid_Empty(t *testing.T) {
m := ApplicativeMonoid()
empty := m.Empty()
result := empty(t)
if !result {
t.Error("Expected Empty() to return an assertion that always passes")
}
}
// TestApplicativeMonoid_Concat_BothPass tests that Concat returns true when both assertions pass
func TestApplicativeMonoid_Concat_BothPass(t *testing.T) {
m := ApplicativeMonoid()
assertion1 := Equal(42)(42)
assertion2 := Equal("hello")("hello")
combined := m.Concat(assertion1, assertion2)
result := combined(t)
if !result {
t.Error("Expected Concat to pass when both assertions pass")
}
}
// TestApplicativeMonoid_Concat_FirstFails tests that Concat returns false when first assertion fails
func TestApplicativeMonoid_Concat_FirstFails(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(43) // This will fail
assertion2 := Equal("hello")("hello")
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when first assertion fails")
}
}
// TestApplicativeMonoid_Concat_SecondFails tests that Concat returns false when second assertion fails
func TestApplicativeMonoid_Concat_SecondFails(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(42)
assertion2 := Equal("hello")("world") // This will fail
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when second assertion fails")
}
}
// TestApplicativeMonoid_Concat_BothFail tests that Concat returns false when both assertions fail
func TestApplicativeMonoid_Concat_BothFail(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
assertion1 := Equal(42)(43) // This will fail
assertion2 := Equal("hello")("world") // This will fail
combined := m.Concat(assertion1, assertion2)
result := combined(mockT)
if result {
t.Error("Expected Concat to fail when both assertions fail")
}
}
// TestApplicativeMonoid_LeftIdentity tests the left identity law: Concat(Empty(), a) = a
func TestApplicativeMonoid_LeftIdentity(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Concat(Empty(), assertion) should behave the same as assertion
combined := m.Concat(m.Empty(), assertion)
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Left identity law violated: Concat(Empty(), a) should equal a")
}
}
// TestApplicativeMonoid_RightIdentity tests the right identity law: Concat(a, Empty()) = a
func TestApplicativeMonoid_RightIdentity(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Concat(assertion, Empty()) should behave the same as assertion
combined := m.Concat(assertion, m.Empty())
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Right identity law violated: Concat(a, Empty()) should equal a")
}
}
// TestApplicativeMonoid_Associativity tests the associativity law: Concat(Concat(a, b), c) = Concat(a, Concat(b, c))
func TestApplicativeMonoid_Associativity(t *testing.T) {
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(2)
a3 := Equal(3)(3)
// Concat(Concat(a1, a2), a3)
left := m.Concat(m.Concat(a1, a2), a3)
// Concat(a1, Concat(a2, a3))
right := m.Concat(a1, m.Concat(a2, a3))
result1 := left(t)
result2 := right(t)
if result1 != result2 {
t.Error("Associativity law violated: Concat(Concat(a, b), c) should equal Concat(a, Concat(b, c))")
}
}
// TestApplicativeMonoid_AssociativityWithFailure tests associativity when assertions fail
func TestApplicativeMonoid_AssociativityWithFailure(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(3) // This will fail
a3 := Equal(3)(3)
// Concat(Concat(a1, a2), a3)
left := m.Concat(m.Concat(a1, a2), a3)
// Concat(a1, Concat(a2, a3))
right := m.Concat(a1, m.Concat(a2, a3))
result1 := left(mockT)
result2 := right(mockT)
if result1 != result2 {
t.Error("Associativity law violated even with failures")
}
if result1 || result2 {
t.Error("Expected both to fail when one assertion fails")
}
}
// TestApplicativeMonoid_ComplexAssertions tests combining complex assertions
func TestApplicativeMonoid_ComplexAssertions(t *testing.T) {
m := ApplicativeMonoid()
arr := []int{1, 2, 3, 4, 5}
mp := map[string]int{"a": 1, "b": 2}
arrayAssertions := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
),
)
mapAssertions := m.Concat(
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
)
combined := m.Concat(arrayAssertions, mapAssertions)
result := combined(t)
if !result {
t.Error("Expected complex combined assertions to pass")
}
}
// TestApplicativeMonoid_ComplexAssertionsWithFailure tests complex assertions when one fails
func TestApplicativeMonoid_ComplexAssertionsWithFailure(t *testing.T) {
mockT := &testing.T{}
m := ApplicativeMonoid()
arr := []int{1, 2, 3}
mp := map[string]int{"a": 1, "b": 2}
arrayAssertions := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr), // This will fail - array has 3 elements, not 5
ArrayContains(3)(arr),
),
)
mapAssertions := m.Concat(
RecordNotEmpty(mp),
RecordLength[string, int](2)(mp),
)
combined := m.Concat(arrayAssertions, mapAssertions)
result := combined(mockT)
if result {
t.Error("Expected complex combined assertions to fail when one assertion fails")
}
}
// TestApplicativeMonoid_MultipleConcat tests chaining multiple Concat operations
func TestApplicativeMonoid_MultipleConcat(t *testing.T) {
m := ApplicativeMonoid()
a1 := Equal(1)(1)
a2 := Equal(2)(2)
a3 := Equal(3)(3)
a4 := Equal(4)(4)
combined := m.Concat(
m.Concat(a1, a2),
m.Concat(a3, a4),
)
result := combined(t)
if !result {
t.Error("Expected multiple Concat operations to pass when all assertions pass")
}
}
// TestApplicativeMonoid_WithStringAssertions tests combining string assertions
func TestApplicativeMonoid_WithStringAssertions(t *testing.T) {
m := ApplicativeMonoid()
str := "hello world"
combined := m.Concat(
StringNotEmpty(str),
StringLength[any, any](11)(str),
)
result := combined(t)
if !result {
t.Error("Expected string assertions to pass")
}
}
// TestApplicativeMonoid_WithBooleanAssertions tests combining boolean assertions
func TestApplicativeMonoid_WithBooleanAssertions(t *testing.T) {
m := ApplicativeMonoid()
combined := m.Concat(
Equal(true)(true),
m.Concat(
Equal(false)(false),
Equal(true)(true),
),
)
result := combined(t)
if !result {
t.Error("Expected boolean assertions to pass")
}
}
// TestApplicativeMonoid_WithErrorAssertions tests combining error assertions
func TestApplicativeMonoid_WithErrorAssertions(t *testing.T) {
m := ApplicativeMonoid()
combined := m.Concat(
NoError(nil),
Equal("test")("test"),
)
result := combined(t)
if !result {
t.Error("Expected error assertions to pass")
}
}
// TestApplicativeMonoid_EmptyWithMultipleConcat tests Empty with multiple Concat operations
func TestApplicativeMonoid_EmptyWithMultipleConcat(t *testing.T) {
m := ApplicativeMonoid()
assertion := Equal(42)(42)
// Multiple Empty values should still act as identity
combined := m.Concat(
m.Empty(),
m.Concat(
assertion,
m.Empty(),
),
)
result1 := assertion(t)
result2 := combined(t)
if result1 != result2 {
t.Error("Multiple Empty values should still act as identity")
}
}
// TestApplicativeMonoid_OnlyEmpty tests using only Empty values
func TestApplicativeMonoid_OnlyEmpty(t *testing.T) {
m := ApplicativeMonoid()
// Concat of Empty values should still be Empty (identity)
combined := m.Concat(m.Empty(), m.Empty())
result := combined(t)
if !result {
t.Error("Expected Concat of Empty values to pass")
}
}
// TestApplicativeMonoid_RealWorldExample tests a realistic use case
func TestApplicativeMonoid_RealWorldExample(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
m := ApplicativeMonoid()
validateUser := func(u User) Reader {
return m.Concat(
StringNotEmpty(u.Name),
m.Concat(
That(func(age int) bool { return age > 0 })(u.Age),
m.Concat(
That(func(age int) bool { return age < 150 })(u.Age),
That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(u.Email),
),
),
)
}
validUser := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
result := validateUser(validUser)(t)
if !result {
t.Error("Expected valid user to pass all validations")
}
}
// TestApplicativeMonoid_RealWorldExampleWithFailure tests a realistic use case with failure
func TestApplicativeMonoid_RealWorldExampleWithFailure(t *testing.T) {
mockT := &testing.T{}
type User struct {
Name string
Age int
Email string
}
m := ApplicativeMonoid()
validateUser := func(u User) Reader {
return m.Concat(
StringNotEmpty(u.Name),
m.Concat(
That(func(age int) bool { return age > 0 })(u.Age),
m.Concat(
That(func(age int) bool { return age < 150 })(u.Age),
That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(u.Email),
),
),
)
}
invalidUser := User{Name: "Bob", Age: 200, Email: "bob@test.com"} // Age > 150
result := validateUser(invalidUser)(mockT)
if result {
t.Error("Expected invalid user to fail validation")
}
}
// TestApplicativeMonoid_IntegrationWithAllOf demonstrates relationship with AllOf
func TestApplicativeMonoid_IntegrationWithAllOf(t *testing.T) {
m := ApplicativeMonoid()
arr := []int{1, 2, 3, 4, 5}
// Using ApplicativeMonoid directly
manualCombination := m.Concat(
ArrayNotEmpty(arr),
m.Concat(
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
),
)
// Using AllOf (which uses ApplicativeMonoid internally)
allOfCombination := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
})
result1 := manualCombination(t)
result2 := allOfCombination(t)
if result1 != result2 {
t.Error("Expected manual combination and AllOf to produce same result")
}
if !result1 || !result2 {
t.Error("Expected both combinations to pass")
}
}

650
v2/assert/traverse.go Normal file
View File

@@ -0,0 +1,650 @@
// 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 assert
import (
"testing"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
)
// TraverseArray transforms an array of values into a test suite by applying a function
// that generates named test cases for each element.
//
// This function enables data-driven testing where you have a collection of test inputs
// and want to run a named subtest for each one. It follows the functional programming
// pattern of "traverse" - transforming a collection while preserving structure and
// accumulating effects (in this case, test execution).
//
// The function takes each element of the array, applies the provided function to generate
// a [Pair] of (test name, test assertion), and runs each as a separate subtest using
// Go's t.Run. All subtests must pass for the overall test to pass.
//
// # Parameters
//
// - f: A function that takes a value of type T and returns a [Pair] containing:
// - Head: The test name (string) for the subtest
// - Tail: The test assertion ([Reader]) to execute
//
// # Returns
//
// - A [Kleisli] function that takes an array of T and returns a [Reader] that:
// - Executes each element as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation and reporting via t.Run
//
// # Use Cases
//
// - Data-driven testing with multiple test cases
// - Parameterized tests where each parameter gets its own subtest
// - Testing collections where each element needs validation
// - Property-based testing with generated test data
//
// # Example - Basic Data-Driven Testing
//
// func TestMathOperations(t *testing.T) {
// type TestCase struct {
// Input int
// Expected int
// }
//
// testCases := []TestCase{
// {Input: 2, Expected: 4},
// {Input: 3, Expected: 9},
// {Input: 4, Expected: 16},
// }
//
// square := func(n int) int { return n * n }
//
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
// assertion := assert.Equal(tc.Expected)(square(tc.Input))
// return pair.MakePair(name, assertion)
// })
//
// traverse(testCases)(t)
// }
//
// # Example - String Validation
//
// func TestStringValidation(t *testing.T) {
// inputs := []string{"hello", "world", "test"}
//
// traverse := assert.TraverseArray(func(s string) assert.Pair[string, assert.Reader] {
// return pair.MakePair(
// fmt.Sprintf("validate_%s", s),
// assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(s),
// assert.That(func(str string) bool { return len(str) > 0 })(s),
// }),
// )
// })
//
// traverse(inputs)(t)
// }
//
// # Example - Complex Object Testing
//
// func TestUsers(t *testing.T) {
// type User struct {
// Name string
// Age int
// Email string
// }
//
// users := []User{
// {Name: "Alice", Age: 30, Email: "alice@example.com"},
// {Name: "Bob", Age: 25, Email: "bob@example.com"},
// }
//
// traverse := assert.TraverseArray(func(u User) assert.Pair[string, assert.Reader] {
// return pair.MakePair(
// fmt.Sprintf("user_%s", u.Name),
// assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(u.Name),
// assert.That(func(age int) bool { return age > 0 })(u.Age),
// assert.That(func(email string) bool {
// return len(email) > 0 && strings.Contains(email, "@")
// })(u.Email),
// }),
// )
// })
//
// traverse(users)(t)
// }
//
// # Comparison with RunAll
//
// TraverseArray and [RunAll] serve similar purposes but differ in their approach:
//
// - TraverseArray: Generates test cases from an array of data
//
// - Input: Array of values + function to generate test cases
//
// - Use when: You have test data and need to generate test cases from it
//
// - RunAll: Executes pre-defined named test cases
//
// - Input: Map of test names to assertions
//
// - Use when: You have already defined test cases with names
//
// # Related Functions
//
// - [SequenceSeq2]: Similar but works with Go iterators (Seq2) instead of arrays
// - [RunAll]: Executes a map of named test cases
// - [AllOf]: Combines multiple assertions without subtests
//
// # References
//
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
// - Go subtests: https://go.dev/blog/subtests
func TraverseArray[T any](f func(T) Pair[string, Reader]) Kleisli[[]T] {
return func(ts []T) Reader {
return func(t *testing.T) bool {
ok := true
for _, src := range ts {
test := f(src)
res := t.Run(pair.Head(test), func(t *testing.T) {
pair.Tail(test)(t)
})
ok = ok && res
}
return ok
}
}
}
// SequenceSeq2 executes a sequence of named test cases provided as a Go iterator.
//
// This function takes a [Seq2] iterator that yields (name, assertion) pairs and
// executes each as a separate subtest using Go's t.Run. It's similar to [TraverseArray]
// but works directly with Go's iterator protocol (introduced in Go 1.23) rather than
// requiring an array.
//
// The function iterates through all test cases, running each as a named subtest.
// All subtests must pass for the overall test to pass. This provides proper test
// isolation and clear reporting of which specific test cases fail.
//
// # Parameters
//
// - s: A [Seq2] iterator that yields pairs of:
// - Key: Test name (string) for the subtest
// - Value: Test assertion ([Reader]) to execute
//
// # Returns
//
// - A [Reader] that:
// - Executes each test case as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation via t.Run
//
// # Use Cases
//
// - Working with iterator-based test data
// - Lazy evaluation of test cases
// - Integration with Go 1.23+ iterator patterns
// - Memory-efficient testing of large test suites
//
// # Example - Basic Usage with Iterator
//
// func TestWithIterator(t *testing.T) {
// // Create an iterator of test cases
// testCases := func(yield func(string, assert.Reader) bool) {
// if !yield("test_addition", assert.Equal(4)(2+2)) {
// return
// }
// if !yield("test_subtraction", assert.Equal(1)(3-2)) {
// return
// }
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
// return
// }
// }
//
// assert.SequenceSeq2(testCases)(t)
// }
//
// # Example - Generated Test Cases
//
// func TestGeneratedCases(t *testing.T) {
// // Generate test cases on the fly
// generateTests := func(yield func(string, assert.Reader) bool) {
// for i := 1; i <= 5; i++ {
// name := fmt.Sprintf("test_%d", i)
// assertion := assert.Equal(i*i)(i * i)
// if !yield(name, assertion) {
// return
// }
// }
// }
//
// assert.SequenceSeq2(generateTests)(t)
// }
//
// # Example - Filtering Test Cases
//
// func TestFilteredCases(t *testing.T) {
// type TestCase struct {
// Name string
// Input int
// Expected int
// Skip bool
// }
//
// allCases := []TestCase{
// {Name: "test1", Input: 2, Expected: 4, Skip: false},
// {Name: "test2", Input: 3, Expected: 9, Skip: true},
// {Name: "test3", Input: 4, Expected: 16, Skip: false},
// }
//
// // Create iterator that filters out skipped tests
// activeTests := func(yield func(string, assert.Reader) bool) {
// for _, tc := range allCases {
// if !tc.Skip {
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
// if !yield(tc.Name, assertion) {
// return
// }
// }
// }
// }
//
// assert.SequenceSeq2(activeTests)(t)
// }
//
// # Comparison with TraverseArray
//
// SequenceSeq2 and [TraverseArray] serve similar purposes but differ in their input:
//
// - SequenceSeq2: Works with iterators (Seq2)
//
// - Input: Iterator yielding (name, assertion) pairs
//
// - Use when: Working with Go 1.23+ iterators or lazy evaluation
//
// - Memory: More efficient for large test suites (lazy evaluation)
//
// - TraverseArray: Works with arrays
//
// - Input: Array of values + transformation function
//
// - Use when: You have an array of test data
//
// - Memory: All test data must be in memory
//
// # Comparison with RunAll
//
// SequenceSeq2 and [RunAll] are very similar:
//
// - SequenceSeq2: Takes an iterator (Seq2)
// - RunAll: Takes a map[string]Reader
//
// Both execute named test cases as subtests. Choose based on your data structure:
// use SequenceSeq2 for iterators, RunAll for maps.
//
// # Related Functions
//
// - [TraverseArray]: Similar but works with arrays instead of iterators
// - [RunAll]: Executes a map of named test cases
// - [AllOf]: Combines multiple assertions without subtests
//
// # References
//
// - Go iterators: https://go.dev/blog/range-functions
// - Go subtests: https://go.dev/blog/subtests
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
func SequenceSeq2[T any](s Seq2[string, Reader]) Reader {
return func(t *testing.T) bool {
ok := true
for name, test := range s {
res := t.Run(name, func(t *testing.T) {
test(t)
})
ok = ok && res
}
return ok
}
}
// TraverseRecord transforms a map of values into a test suite by applying a function
// that generates test assertions for each map entry.
//
// This function enables data-driven testing where you have a map of test data and want
// to run a named subtest for each entry. The map keys become test names, and the function
// transforms each value into a test assertion. It follows the functional programming
// pattern of "traverse" - transforming a collection while preserving structure and
// accumulating effects (in this case, test execution).
//
// The function takes each key-value pair from the map, applies the provided function to
// generate a [Reader] assertion, and runs each as a separate subtest using Go's t.Run.
// All subtests must pass for the overall test to pass.
//
// # Parameters
//
// - f: A [Kleisli] function that takes a value of type T and returns a [Reader] assertion
//
// # Returns
//
// - A [Kleisli] function that takes a map[string]T and returns a [Reader] that:
// - Executes each map entry as a named subtest (using the key as the test name)
// - Returns true only if all subtests pass
// - Provides proper test isolation and reporting via t.Run
//
// # Use Cases
//
// - Data-driven testing with named test cases in a map
// - Testing configuration maps where keys are meaningful names
// - Validating collections where natural keys exist
// - Property-based testing with named scenarios
//
// # Example - Basic Configuration Testing
//
// func TestConfigurations(t *testing.T) {
// configs := map[string]int{
// "timeout": 30,
// "maxRetries": 3,
// "bufferSize": 1024,
// }
//
// validatePositive := assert.That(func(n int) bool { return n > 0 })
//
// traverse := assert.TraverseRecord(validatePositive)
// traverse(configs)(t)
// }
//
// # Example - User Validation
//
// func TestUserMap(t *testing.T) {
// type User struct {
// Name string
// Age int
// }
//
// users := map[string]User{
// "alice": {Name: "Alice", Age: 30},
// "bob": {Name: "Bob", Age: 25},
// "carol": {Name: "Carol", Age: 35},
// }
//
// validateUser := func(u User) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(u.Name),
// assert.That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
// })
// }
//
// traverse := assert.TraverseRecord(validateUser)
// traverse(users)(t)
// }
//
// # Example - API Endpoint Testing
//
// func TestEndpoints(t *testing.T) {
// type Endpoint struct {
// Path string
// Method string
// }
//
// endpoints := map[string]Endpoint{
// "get_users": {Path: "/api/users", Method: "GET"},
// "create_user": {Path: "/api/users", Method: "POST"},
// "delete_user": {Path: "/api/users/:id", Method: "DELETE"},
// }
//
// validateEndpoint := func(e Endpoint) assert.Reader {
// return assert.AllOf([]assert.Reader{
// assert.StringNotEmpty(e.Path),
// assert.That(func(path string) bool {
// return strings.HasPrefix(path, "/api/")
// })(e.Path),
// assert.That(func(method string) bool {
// return method == "GET" || method == "POST" ||
// method == "PUT" || method == "DELETE"
// })(e.Method),
// })
// }
//
// traverse := assert.TraverseRecord(validateEndpoint)
// traverse(endpoints)(t)
// }
//
// # Comparison with TraverseArray
//
// TraverseRecord and [TraverseArray] serve similar purposes but differ in their input:
//
// - TraverseRecord: Works with maps (records)
//
// - Input: Map with string keys + transformation function
//
// - Use when: You have named test data in a map
//
// - Test names: Derived from map keys
//
// - TraverseArray: Works with arrays
//
// - Input: Array of values + function that generates names and assertions
//
// - Use when: You have sequential test data
//
// - Test names: Generated by the transformation function
//
// # Comparison with SequenceRecord
//
// TraverseRecord and [SequenceRecord] are closely related:
//
// - TraverseRecord: Transforms values into assertions
//
// - Input: map[string]T + function T -> Reader
//
// - Use when: You need to transform data before asserting
//
// - SequenceRecord: Executes pre-defined assertions
//
// - Input: map[string]Reader
//
// - Use when: Assertions are already defined
//
// # Related Functions
//
// - [SequenceRecord]: Similar but takes pre-defined assertions
// - [TraverseArray]: Similar but works with arrays
// - [RunAll]: Alias for SequenceRecord
//
// # References
//
// - Haskell traverse: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:traverse
// - Go subtests: https://go.dev/blog/subtests
func TraverseRecord[T any](f Kleisli[T]) Kleisli[map[string]T] {
return func(m map[string]T) Reader {
return func(t *testing.T) bool {
ok := true
for name, src := range m {
res := t.Run(name, func(t *testing.T) {
f(src)(t)
})
ok = ok && res
}
return ok
}
}
}
// SequenceRecord executes a map of named test cases as subtests.
//
// This function takes a map where keys are test names and values are test assertions
// ([Reader]), and executes each as a separate subtest using Go's t.Run. It's the
// record (map) equivalent of [SequenceSeq2] and is actually aliased as [RunAll] for
// convenience.
//
// The function iterates through all map entries, running each as a named subtest.
// All subtests must pass for the overall test to pass. This provides proper test
// isolation and clear reporting of which specific test cases fail.
//
// # Parameters
//
// - m: A map[string]Reader where:
// - Keys: Test names (strings) for the subtests
// - Values: Test assertions ([Reader]) to execute
//
// # Returns
//
// - A [Reader] that:
// - Executes each map entry as a named subtest
// - Returns true only if all subtests pass
// - Provides proper test isolation via t.Run
//
// # Use Cases
//
// - Executing a collection of pre-defined named test cases
// - Organizing related tests in a map structure
// - Running multiple assertions with descriptive names
// - Building test suites programmatically
//
// # Example - Basic Named Tests
//
// func TestMathOperations(t *testing.T) {
// tests := map[string]assert.Reader{
// "addition": assert.Equal(4)(2 + 2),
// "subtraction": assert.Equal(1)(3 - 2),
// "multiplication": assert.Equal(6)(2 * 3),
// "division": assert.Equal(2)(6 / 3),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - String Validation Suite
//
// func TestStringValidations(t *testing.T) {
// testString := "hello world"
//
// tests := map[string]assert.Reader{
// "not_empty": assert.StringNotEmpty(testString),
// "correct_length": assert.StringLength[any, any](11)(testString),
// "has_space": assert.That(func(s string) bool {
// return strings.Contains(s, " ")
// })(testString),
// "lowercase": assert.That(func(s string) bool {
// return s == strings.ToLower(s)
// })(testString),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - Complex Object Validation
//
// func TestUserValidation(t *testing.T) {
// type User struct {
// Name string
// Age int
// Email string
// }
//
// user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
//
// tests := map[string]assert.Reader{
// "name_not_empty": assert.StringNotEmpty(user.Name),
// "age_positive": assert.That(func(age int) bool { return age > 0 })(user.Age),
// "age_reasonable": assert.That(func(age int) bool { return age < 150 })(user.Age),
// "email_valid": assert.That(func(email string) bool {
// return strings.Contains(email, "@") && strings.Contains(email, ".")
// })(user.Email),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Example - Array Validation Suite
//
// func TestArrayValidations(t *testing.T) {
// numbers := []int{1, 2, 3, 4, 5}
//
// tests := map[string]assert.Reader{
// "not_empty": assert.ArrayNotEmpty(numbers),
// "correct_length": assert.ArrayLength[int](5)(numbers),
// "contains_three": assert.ArrayContains(3)(numbers),
// "all_positive": assert.That(func(arr []int) bool {
// for _, n := range arr {
// if n <= 0 {
// return false
// }
// }
// return true
// })(numbers),
// }
//
// assert.SequenceRecord(tests)(t)
// }
//
// # Comparison with TraverseRecord
//
// SequenceRecord and [TraverseRecord] are closely related:
//
// - SequenceRecord: Executes pre-defined assertions
//
// - Input: map[string]Reader (assertions already created)
//
// - Use when: You have already defined test cases with assertions
//
// - TraverseRecord: Transforms values into assertions
//
// - Input: map[string]T + function T -> Reader
//
// - Use when: You need to transform data before asserting
//
// # Comparison with SequenceSeq2
//
// SequenceRecord and [SequenceSeq2] serve similar purposes but differ in their input:
//
// - SequenceRecord: Works with maps
//
// - Input: map[string]Reader
//
// - Use when: You have named test cases in a map
//
// - Iteration order: Non-deterministic (map iteration)
//
// - SequenceSeq2: Works with iterators
//
// - Input: Seq2[string, Reader]
//
// - Use when: You have test cases in an iterator
//
// - Iteration order: Deterministic (iterator order)
//
// # Note on Map Iteration Order
//
// Go maps have non-deterministic iteration order. If test execution order matters,
// consider using [SequenceSeq2] with an iterator that provides deterministic ordering,
// or use [TraverseArray] with a slice of test cases.
//
// # Related Functions
//
// - [RunAll]: Alias for SequenceRecord
// - [TraverseRecord]: Similar but transforms values into assertions
// - [SequenceSeq2]: Similar but works with iterators
// - [TraverseArray]: Similar but works with arrays
//
// # References
//
// - Go subtests: https://go.dev/blog/subtests
// - Haskell sequence: https://hackage.haskell.org/package/base/docs/Data-Traversable.html#v:sequence
func SequenceRecord(m map[string]Reader) Reader {
return TraverseRecord(reader.Ask[Reader]())(m)
}

960
v2/assert/traverse_test.go Normal file
View File

@@ -0,0 +1,960 @@
// 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 assert
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/pair"
)
// TestTraverseArray_EmptyArray tests that TraverseArray handles empty arrays correctly
func TestTraverseArray_EmptyArray(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(n)(n),
)
})
result := traverse([]int{})(t)
if !result {
t.Error("Expected TraverseArray to pass with empty array")
}
}
// TestTraverseArray_SingleElement tests TraverseArray with a single element
func TestTraverseArray_SingleElement(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(n*2)(n*2),
)
})
result := traverse([]int{5})(t)
if !result {
t.Error("Expected TraverseArray to pass with single element")
}
}
// TestTraverseArray_MultipleElements tests TraverseArray with multiple passing elements
func TestTraverseArray_MultipleElements(t *testing.T) {
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("square_%d", n),
Equal(n*n)(n*n),
)
})
result := traverse([]int{1, 2, 3, 4, 5})(t)
if !result {
t.Error("Expected TraverseArray to pass with all passing elements")
}
}
// TestTraverseArray_WithFailure tests that TraverseArray fails when one element fails
func TestTraverseArray_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("test_%d", n),
Equal(10)(n), // Will fail for all except 10
)
})
// Run in a subtest - we expect the subtests to fail, so t.Run returns false
result := traverse([]int{1, 2, 3})(t)
// The traverse should return false because assertions fail
if result {
t.Error("Expected traverse to return false when elements don't match")
}
}
// TestTraverseArray_MixedResults tests TraverseArray with some passing and some failing
func TestTraverseArray_MixedResults(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseArray(func(n int) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("is_even_%d", n),
Equal(0)(n%2), // Only passes for even numbers
)
})
result := traverse([]int{2, 3, 4})(t) // 3 is odd, should fail
// The traverse should return false because one assertion fails
if result {
t.Error("Expected traverse to return false when some elements fail")
}
}
// TestTraverseArray_StringData tests TraverseArray with string data
func TestTraverseArray_StringData(t *testing.T) {
words := []string{"hello", "world", "test"}
traverse := TraverseArray(func(s string) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("validate_%s", s),
AllOf([]Reader{
StringNotEmpty(s),
That(func(str string) bool { return len(str) > 0 })(s),
}),
)
})
result := traverse(words)(t)
if !result {
t.Error("Expected TraverseArray to pass with valid strings")
}
}
// TestTraverseArray_ComplexObjects tests TraverseArray with complex objects
func TestTraverseArray_ComplexObjects(t *testing.T) {
type User struct {
Name string
Age int
}
users := []User{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 25},
{Name: "Charlie", Age: 35},
}
traverse := TraverseArray(func(u User) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("user_%s", u.Name),
AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
}),
)
})
result := traverse(users)(t)
if !result {
t.Error("Expected TraverseArray to pass with valid users")
}
}
// TestTraverseArray_ComplexObjectsWithFailure tests TraverseArray with invalid complex objects
func TestTraverseArray_ComplexObjectsWithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
type User struct {
Name string
Age int
}
users := []User{
{Name: "Alice", Age: 30},
{Name: "", Age: 25}, // Invalid: empty name
{Name: "Charlie", Age: 35},
}
traverse := TraverseArray(func(u User) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("user_%s", u.Name),
AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 })(u.Age),
}),
)
})
result := traverse(users)(t)
// The traverse should return false because one user is invalid
if result {
t.Error("Expected traverse to return false with invalid user")
}
}
// TestTraverseArray_DataDrivenTesting demonstrates data-driven testing pattern
func TestTraverseArray_DataDrivenTesting(t *testing.T) {
type TestCase struct {
Input int
Expected int
}
testCases := []TestCase{
{Input: 2, Expected: 4},
{Input: 3, Expected: 9},
{Input: 4, Expected: 16},
{Input: 5, Expected: 25},
}
square := func(n int) int { return n * n }
traverse := TraverseArray(func(tc TestCase) Pair[string, Reader] {
return pair.MakePair(
fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected),
Equal(tc.Expected)(square(tc.Input)),
)
})
result := traverse(testCases)(t)
if !result {
t.Error("Expected all test cases to pass")
}
}
// TestSequenceSeq2_EmptySequence tests that SequenceSeq2 handles empty sequences correctly
func TestSequenceSeq2_EmptySequence(t *testing.T) {
emptySeq := func(yield func(string, Reader) bool) {
// Empty - yields nothing
}
result := SequenceSeq2[Reader](emptySeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with empty sequence")
}
}
// TestSequenceSeq2_SingleTest tests SequenceSeq2 with a single test
func TestSequenceSeq2_SingleTest(t *testing.T) {
singleSeq := func(yield func(string, Reader) bool) {
yield("test_one", Equal(42)(42))
}
result := SequenceSeq2[Reader](singleSeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with single test")
}
}
// TestSequenceSeq2_MultipleTests tests SequenceSeq2 with multiple passing tests
func TestSequenceSeq2_MultipleTests(t *testing.T) {
multiSeq := func(yield func(string, Reader) bool) {
if !yield("test_addition", Equal(4)(2+2)) {
return
}
if !yield("test_subtraction", Equal(1)(3-2)) {
return
}
if !yield("test_multiplication", Equal(6)(2*3)) {
return
}
}
result := SequenceSeq2[Reader](multiSeq)(t)
if !result {
t.Error("Expected SequenceSeq2 to pass with all passing tests")
}
}
// TestSequenceSeq2_WithFailure tests that SequenceSeq2 fails when one test fails
func TestSequenceSeq2_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
failSeq := func(yield func(string, Reader) bool) {
if !yield("test_pass", Equal(4)(2+2)) {
return
}
if !yield("test_fail", Equal(5)(2+2)) { // This will fail
return
}
if !yield("test_pass2", Equal(6)(2*3)) {
return
}
}
result := SequenceSeq2[Reader](failSeq)(t)
// The sequence should return false because one test fails
if result {
t.Error("Expected sequence to return false when one test fails")
}
}
// TestSequenceSeq2_GeneratedTests tests SequenceSeq2 with generated test cases
func TestSequenceSeq2_GeneratedTests(t *testing.T) {
generateTests := func(yield func(string, Reader) bool) {
for i := 1; i <= 5; i++ {
name := fmt.Sprintf("test_%d", i)
assertion := Equal(i * i)(i * i)
if !yield(name, assertion) {
return
}
}
}
result := SequenceSeq2[Reader](generateTests)(t)
if !result {
t.Error("Expected all generated tests to pass")
}
}
// TestSequenceSeq2_StringTests tests SequenceSeq2 with string assertions
func TestSequenceSeq2_StringTests(t *testing.T) {
stringSeq := func(yield func(string, Reader) bool) {
if !yield("test_hello", StringNotEmpty("hello")) {
return
}
if !yield("test_world", StringNotEmpty("world")) {
return
}
if !yield("test_length", StringLength[any, any](5)("hello")) {
return
}
}
result := SequenceSeq2[Reader](stringSeq)(t)
if !result {
t.Error("Expected all string tests to pass")
}
}
// TestSequenceSeq2_ArrayTests tests SequenceSeq2 with array assertions
func TestSequenceSeq2_ArrayTests(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
arraySeq := func(yield func(string, Reader) bool) {
if !yield("test_not_empty", ArrayNotEmpty(arr)) {
return
}
if !yield("test_length", ArrayLength[int](5)(arr)) {
return
}
if !yield("test_contains", ArrayContains(3)(arr)) {
return
}
}
result := SequenceSeq2[Reader](arraySeq)(t)
if !result {
t.Error("Expected all array tests to pass")
}
}
// TestSequenceSeq2_ComplexAssertions tests SequenceSeq2 with complex combined assertions
func TestSequenceSeq2_ComplexAssertions(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
userSeq := func(yield func(string, Reader) bool) {
if !yield("test_name", StringNotEmpty(user.Name)) {
return
}
if !yield("test_age", That(func(age int) bool { return age > 0 && age < 150 })(user.Age)) {
return
}
if !yield("test_email", That(func(email string) bool {
for _, ch := range email {
if ch == '@' {
return true
}
}
return false
})(user.Email)) {
return
}
}
result := SequenceSeq2[Reader](userSeq)(t)
if !result {
t.Error("Expected all user validation tests to pass")
}
}
// TestSequenceSeq2_EarlyTermination tests that SequenceSeq2 respects early termination
func TestSequenceSeq2_EarlyTermination(t *testing.T) {
executionCount := 0
earlyTermSeq := func(yield func(string, Reader) bool) {
executionCount++
if !yield("test_1", Equal(1)(1)) {
return
}
executionCount++
if !yield("test_2", Equal(2)(2)) {
return
}
executionCount++
// This should execute even though we don't check the return
yield("test_3", Equal(3)(3))
executionCount++
}
SequenceSeq2[Reader](earlyTermSeq)(t)
// All iterations should execute since we're not terminating early
if executionCount != 4 {
t.Errorf("Expected 4 executions, got %d", executionCount)
}
}
// TestSequenceSeq2_WithMapConversion demonstrates converting a map to Seq2
func TestSequenceSeq2_WithMapConversion(t *testing.T) {
testMap := map[string]Reader{
"test_addition": Equal(4)(2 + 2),
"test_multiplication": Equal(6)(2 * 3),
"test_subtraction": Equal(1)(3 - 2),
}
// Convert map to Seq2
mapSeq := func(yield func(string, Reader) bool) {
for name, assertion := range testMap {
if !yield(name, assertion) {
return
}
}
}
result := SequenceSeq2[Reader](mapSeq)(t)
if !result {
t.Error("Expected all map-based tests to pass")
}
}
// TestTraverseArray_vs_SequenceSeq2 demonstrates the relationship between the two functions
func TestTraverseArray_vs_SequenceSeq2(t *testing.T) {
type TestCase struct {
Name string
Input int
Expected int
}
testCases := []TestCase{
{Name: "test_1", Input: 2, Expected: 4},
{Name: "test_2", Input: 3, Expected: 9},
{Name: "test_3", Input: 4, Expected: 16},
}
// Using TraverseArray
traverseResult := TraverseArray(func(tc TestCase) Pair[string, Reader] {
return pair.MakePair(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input))
})(testCases)(t)
// Using SequenceSeq2
seqResult := SequenceSeq2[Reader](func(yield func(string, Reader) bool) {
for _, tc := range testCases {
if !yield(tc.Name, Equal(tc.Expected)(tc.Input*tc.Input)) {
return
}
}
})(t)
if traverseResult != seqResult {
t.Error("Expected TraverseArray and SequenceSeq2 to produce same result")
}
if !traverseResult || !seqResult {
t.Error("Expected both approaches to pass")
}
}
// TestTraverseRecord_EmptyMap tests that TraverseRecord handles empty maps correctly
func TestTraverseRecord_EmptyMap(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n)(n)
})
result := traverse(map[string]int{})(t)
if !result {
t.Error("Expected TraverseRecord to pass with empty map")
}
}
// TestTraverseRecord_SingleEntry tests TraverseRecord with a single map entry
func TestTraverseRecord_SingleEntry(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n * 2)(n * 2)
})
result := traverse(map[string]int{"test_5": 5})(t)
if !result {
t.Error("Expected TraverseRecord to pass with single entry")
}
}
// TestTraverseRecord_MultipleEntries tests TraverseRecord with multiple passing entries
func TestTraverseRecord_MultipleEntries(t *testing.T) {
traverse := TraverseRecord(func(n int) Reader {
return Equal(n * n)(n * n)
})
result := traverse(map[string]int{
"square_1": 1,
"square_2": 2,
"square_3": 3,
"square_4": 4,
"square_5": 5,
})(t)
if !result {
t.Error("Expected TraverseRecord to pass with all passing entries")
}
}
// TestTraverseRecord_WithFailure tests that TraverseRecord fails when one entry fails
func TestTraverseRecord_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseRecord(func(n int) Reader {
return Equal(10)(n) // Will fail for all except 10
})
result := traverse(map[string]int{
"test_1": 1,
"test_2": 2,
"test_3": 3,
})(t)
// The traverse should return false because entries don't match
if result {
t.Error("Expected traverse to return false when entries don't match")
}
}
// TestTraverseRecord_MixedResults tests TraverseRecord with some passing and some failing
func TestTraverseRecord_MixedResults(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
traverse := TraverseRecord(func(n int) Reader {
return Equal(0)(n % 2) // Only passes for even numbers
})
result := traverse(map[string]int{
"even_2": 2,
"odd_3": 3,
"even_4": 4,
})(t)
// The traverse should return false because some entries fail
if result {
t.Error("Expected traverse to return false when some entries fail")
}
}
// TestTraverseRecord_StringData tests TraverseRecord with string data
func TestTraverseRecord_StringData(t *testing.T) {
words := map[string]string{
"greeting": "hello",
"world": "world",
"test": "test",
}
traverse := TraverseRecord(func(s string) Reader {
return AllOf([]Reader{
StringNotEmpty(s),
That(func(str string) bool { return len(str) > 0 })(s),
})
})
result := traverse(words)(t)
if !result {
t.Error("Expected TraverseRecord to pass with valid strings")
}
}
// TestTraverseRecord_ComplexObjects tests TraverseRecord with complex objects
func TestTraverseRecord_ComplexObjects(t *testing.T) {
type User struct {
Name string
Age int
}
users := map[string]User{
"alice": {Name: "Alice", Age: 30},
"bob": {Name: "Bob", Age: 25},
"charlie": {Name: "Charlie", Age: 35},
}
traverse := TraverseRecord(func(u User) Reader {
return AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 && age < 150 })(u.Age),
})
})
result := traverse(users)(t)
if !result {
t.Error("Expected TraverseRecord to pass with valid users")
}
}
// TestTraverseRecord_ComplexObjectsWithFailure tests TraverseRecord with invalid complex objects
func TestTraverseRecord_ComplexObjectsWithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
type User struct {
Name string
Age int
}
users := map[string]User{
"alice": {Name: "Alice", Age: 30},
"invalid": {Name: "", Age: 25}, // Invalid: empty name
"charlie": {Name: "Charlie", Age: 35},
}
traverse := TraverseRecord(func(u User) Reader {
return AllOf([]Reader{
StringNotEmpty(u.Name),
That(func(age int) bool { return age > 0 })(u.Age),
})
})
result := traverse(users)(t)
// The traverse should return false because one user is invalid
if result {
t.Error("Expected traverse to return false with invalid user")
}
}
// TestTraverseRecord_ConfigurationTesting demonstrates configuration testing pattern
func TestTraverseRecord_ConfigurationTesting(t *testing.T) {
configs := map[string]int{
"timeout": 30,
"maxRetries": 3,
"bufferSize": 1024,
}
validatePositive := That(func(n int) bool { return n > 0 })
traverse := TraverseRecord(validatePositive)
result := traverse(configs)(t)
if !result {
t.Error("Expected all configuration values to be positive")
}
}
// TestTraverseRecord_APIEndpointTesting demonstrates API endpoint testing pattern
func TestTraverseRecord_APIEndpointTesting(t *testing.T) {
type Endpoint struct {
Path string
Method string
}
endpoints := map[string]Endpoint{
"get_users": {Path: "/api/users", Method: "GET"},
"create_user": {Path: "/api/users", Method: "POST"},
"delete_user": {Path: "/api/users/:id", Method: "DELETE"},
}
validateEndpoint := func(e Endpoint) Reader {
return AllOf([]Reader{
StringNotEmpty(e.Path),
That(func(path string) bool {
return len(path) > 0 && path[0] == '/'
})(e.Path),
That(func(method string) bool {
return method == "GET" || method == "POST" ||
method == "PUT" || method == "DELETE"
})(e.Method),
})
}
traverse := TraverseRecord(validateEndpoint)
result := traverse(endpoints)(t)
if !result {
t.Error("Expected all endpoints to be valid")
}
}
// TestSequenceRecord_EmptyMap tests that SequenceRecord handles empty maps correctly
func TestSequenceRecord_EmptyMap(t *testing.T) {
result := SequenceRecord(map[string]Reader{})(t)
if !result {
t.Error("Expected SequenceRecord to pass with empty map")
}
}
// TestSequenceRecord_SingleTest tests SequenceRecord with a single test
func TestSequenceRecord_SingleTest(t *testing.T) {
tests := map[string]Reader{
"test_one": Equal(42)(42),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected SequenceRecord to pass with single test")
}
}
// TestSequenceRecord_MultipleTests tests SequenceRecord with multiple passing tests
func TestSequenceRecord_MultipleTests(t *testing.T) {
tests := map[string]Reader{
"test_addition": Equal(4)(2 + 2),
"test_subtraction": Equal(1)(3 - 2),
"test_multiplication": Equal(6)(2 * 3),
"test_division": Equal(2)(6 / 3),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected SequenceRecord to pass with all passing tests")
}
}
// TestSequenceRecord_WithFailure tests that SequenceRecord fails when one test fails
func TestSequenceRecord_WithFailure(t *testing.T) {
t.Skip("Skipping test that intentionally creates failing subtests")
tests := map[string]Reader{
"test_pass": Equal(4)(2 + 2),
"test_fail": Equal(5)(2 + 2), // This will fail
"test_pass2": Equal(6)(2 * 3),
}
result := SequenceRecord(tests)(t)
// The sequence should return false because one test fails
if result {
t.Error("Expected sequence to return false when one test fails")
}
}
// TestSequenceRecord_StringTests tests SequenceRecord with string assertions
func TestSequenceRecord_StringTests(t *testing.T) {
testString := "hello world"
tests := map[string]Reader{
"not_empty": StringNotEmpty(testString),
"correct_length": StringLength[any, any](11)(testString),
"has_space": That(func(s string) bool {
for _, ch := range s {
if ch == ' ' {
return true
}
}
return false
})(testString),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all string tests to pass")
}
}
// TestSequenceRecord_ArrayTests tests SequenceRecord with array assertions
func TestSequenceRecord_ArrayTests(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
tests := map[string]Reader{
"not_empty": ArrayNotEmpty(arr),
"correct_length": ArrayLength[int](5)(arr),
"contains_three": ArrayContains(3)(arr),
"all_positive": That(func(arr []int) bool {
for _, n := range arr {
if n <= 0 {
return false
}
}
return true
})(arr),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all array tests to pass")
}
}
// TestSequenceRecord_ComplexAssertions tests SequenceRecord with complex combined assertions
func TestSequenceRecord_ComplexAssertions(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
tests := map[string]Reader{
"name_not_empty": StringNotEmpty(user.Name),
"age_positive": That(func(age int) bool { return age > 0 })(user.Age),
"age_reasonable": That(func(age int) bool { return age < 150 })(user.Age),
"email_valid": That(func(email string) bool {
hasAt := false
hasDot := false
for _, ch := range email {
if ch == '@' {
hasAt = true
}
if ch == '.' {
hasDot = true
}
}
return hasAt && hasDot
})(user.Email),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all user validation tests to pass")
}
}
// TestSequenceRecord_MathOperations demonstrates basic math operations testing
func TestSequenceRecord_MathOperations(t *testing.T) {
tests := map[string]Reader{
"addition": Equal(4)(2 + 2),
"subtraction": Equal(1)(3 - 2),
"multiplication": Equal(6)(2 * 3),
"division": Equal(2)(6 / 3),
"modulo": Equal(1)(7 % 3),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all math operations to pass")
}
}
// TestSequenceRecord_BooleanTests tests SequenceRecord with boolean assertions
func TestSequenceRecord_BooleanTests(t *testing.T) {
tests := map[string]Reader{
"true_is_true": Equal(true)(true),
"false_is_false": Equal(false)(false),
"not_true": Equal(false)(!true),
"not_false": Equal(true)(!false),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all boolean tests to pass")
}
}
// TestSequenceRecord_ErrorTests tests SequenceRecord with error assertions
func TestSequenceRecord_ErrorTests(t *testing.T) {
tests := map[string]Reader{
"no_error": NoError(nil),
"equal_value": Equal("test")("test"),
"not_empty": StringNotEmpty("hello"),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected all error tests to pass")
}
}
// TestTraverseRecord_vs_SequenceRecord demonstrates the relationship between the two functions
func TestTraverseRecord_vs_SequenceRecord(t *testing.T) {
type TestCase struct {
Input int
Expected int
}
testData := map[string]TestCase{
"test_1": {Input: 2, Expected: 4},
"test_2": {Input: 3, Expected: 9},
"test_3": {Input: 4, Expected: 16},
}
// Using TraverseRecord
traverseResult := TraverseRecord(func(tc TestCase) Reader {
return Equal(tc.Expected)(tc.Input * tc.Input)
})(testData)(t)
// Using SequenceRecord (manually creating the map)
tests := make(map[string]Reader)
for name, tc := range testData {
tests[name] = Equal(tc.Expected)(tc.Input * tc.Input)
}
seqResult := SequenceRecord(tests)(t)
if traverseResult != seqResult {
t.Error("Expected TraverseRecord and SequenceRecord to produce same result")
}
if !traverseResult || !seqResult {
t.Error("Expected both approaches to pass")
}
}
// TestSequenceRecord_WithAllOf demonstrates combining SequenceRecord with AllOf
func TestSequenceRecord_WithAllOf(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
tests := map[string]Reader{
"array_validations": AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
}),
"element_checks": AllOf([]Reader{
That(func(a []int) bool { return a[0] == 1 })(arr),
That(func(a []int) bool { return a[4] == 5 })(arr),
}),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected combined assertions to pass")
}
}
// TestTraverseRecord_ConfigValidation demonstrates real-world configuration validation
func TestTraverseRecord_ConfigValidation(t *testing.T) {
type Config struct {
Value int
Min int
Max int
}
configs := map[string]Config{
"timeout": {Value: 30, Min: 1, Max: 60},
"maxRetries": {Value: 3, Min: 1, Max: 10},
"bufferSize": {Value: 1024, Min: 512, Max: 4096},
}
validateConfig := func(c Config) Reader {
return AllOf([]Reader{
That(func(val int) bool { return val >= c.Min })(c.Value),
That(func(val int) bool { return val <= c.Max })(c.Value),
})
}
traverse := TraverseRecord(validateConfig)
result := traverse(configs)(t)
if !result {
t.Error("Expected all configurations to be within valid ranges")
}
}
// TestSequenceRecord_RealWorldExample demonstrates a realistic use case
func TestSequenceRecord_RealWorldExample(t *testing.T) {
type Response struct {
StatusCode int
Body string
}
response := Response{StatusCode: 200, Body: `{"status":"ok"}`}
tests := map[string]Reader{
"status_ok": Equal(200)(response.StatusCode),
"body_not_empty": StringNotEmpty(response.Body),
"body_is_json": That(func(s string) bool {
return len(s) > 0 && s[0] == '{' && s[len(s)-1] == '}'
})(response.Body),
}
result := SequenceRecord(tests)(t)
if !result {
t.Error("Expected response validation to pass")
}
}

View File

@@ -1,11 +1,32 @@
// 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 assert
import (
"iter"
"testing"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
@@ -13,23 +34,506 @@ import (
type (
// Result represents a computation that may fail with an error.
//
// This is an alias for [result.Result][T], which encapsulates either a successful
// value of type T or an error. It's commonly used in test assertions to represent
// operations that might fail, allowing for functional error handling without exceptions.
//
// A Result can be in one of two states:
// - Success: Contains a value of type T
// - Failure: Contains an error
//
// This type is particularly useful in testing scenarios where you need to:
// - Test functions that return results
// - Chain operations that might fail
// - Handle errors functionally
//
// Example:
//
// func TestResultHandling(t *testing.T) {
// successResult := result.Of[int](42)
// assert.Success(successResult)(t) // Passes
//
// failureResult := result.Error[int](errors.New("failed"))
// assert.Failure(failureResult)(t) // Passes
// }
//
// See also:
// - [Success]: Asserts a Result is successful
// - [Failure]: Asserts a Result contains an error
// - [result.Result]: The underlying Result type
Result[T any] = result.Result[T]
// Reader represents a test assertion that depends on a testing.T context and returns a boolean.
// Reader represents a test assertion that depends on a [testing.T] context and returns a boolean.
//
// This is the core type for all assertions in this package. It's an alias for
// [reader.Reader][*testing.T, bool], which is a function that takes a testing context
// and produces a boolean result indicating whether the assertion passed.
//
// The Reader pattern enables:
// - Composable assertions that can be combined using functional operators
// - Deferred execution - assertions are defined but not executed until applied to a test
// - Reusable assertion logic that can be applied to multiple tests
// - Functional composition of complex test conditions
//
// All assertion functions in this package return a Reader, which must be applied
// to a *testing.T to execute the assertion:
//
// assertion := assert.Equal(42)(result) // Creates a Reader
// assertion(t) // Executes the assertion
//
// Readers can be composed using functions like [AllOf], [ApplicativeMonoid], or
// functional operators from the reader package.
//
// Example:
//
// func TestReaderComposition(t *testing.T) {
// // Create individual assertions
// assertion1 := assert.Equal(42)(42)
// assertion2 := assert.StringNotEmpty("hello")
//
// // Combine them
// combined := assert.AllOf([]assert.Reader{assertion1, assertion2})
//
// // Execute the combined assertion
// combined(t)
// }
//
// See also:
// - [Kleisli]: Function that produces a Reader from a value
// - [AllOf]: Combines multiple Readers
// - [ApplicativeMonoid]: Monoid for combining Readers
Reader = reader.Reader[*testing.T, bool]
// Kleisli represents a function that produces a test assertion Reader from a value of type T.
// Kleisli represents a function that produces a test assertion [Reader] from a value of type T.
//
// This is an alias for [reader.Reader][T, Reader], which is a function that takes a value
// of type T and returns a Reader (test assertion). This pattern is fundamental to the
// "data last" principle used throughout this package.
//
// Kleisli functions enable:
// - Partial application of assertions - configure the expected value first, apply actual value later
// - Reusable assertion builders that can be applied to different values
// - Functional composition of assertion pipelines
// - Point-free style programming with assertions
//
// Most assertion functions in this package return a Kleisli, which must be applied
// to the actual value being tested, and then to a *testing.T:
//
// kleisli := assert.Equal(42) // Kleisli[int] - expects an int
// reader := kleisli(result) // Reader - assertion ready to execute
// reader(t) // Execute the assertion
//
// Or more concisely:
//
// assert.Equal(42)(result)(t)
//
// Example:
//
// func TestKleisliPattern(t *testing.T) {
// // Create a reusable assertion for positive numbers
// isPositive := assert.That(func(n int) bool { return n > 0 })
//
// // Apply it to different values
// isPositive(42)(t) // Passes
// isPositive(100)(t) // Passes
// // isPositive(-5)(t) would fail
//
// // Can be used with Local for property testing
// type User struct { Age int }
// checkAge := assert.Local(func(u User) int { return u.Age })(isPositive)
// checkAge(User{Age: 25})(t) // Passes
// }
//
// See also:
// - [Reader]: The assertion type produced by Kleisli
// - [Local]: Focuses a Kleisli on a property of a larger structure
Kleisli[T any] = reader.Reader[T, Reader]
// Predicate represents a function that tests a value of type T and returns a boolean.
//
// This is an alias for [predicate.Predicate][T], which is a simple function that
// takes a value and returns true or false based on some condition. Predicates are
// used with the [That] function to create custom assertions.
//
// Predicates enable:
// - Custom validation logic for any type
// - Reusable test conditions
// - Composition of complex validation rules
// - Integration with functional programming patterns
//
// Example:
//
// func TestPredicates(t *testing.T) {
// // Simple predicate
// isEven := func(n int) bool { return n%2 == 0 }
// assert.That(isEven)(42)(t) // Passes
//
// // String predicate
// hasPrefix := func(s string) bool { return strings.HasPrefix(s, "test") }
// assert.That(hasPrefix)("test_file.go")(t) // Passes
//
// // Complex predicate
// isValidEmail := func(s string) bool {
// return strings.Contains(s, "@") && strings.Contains(s, ".")
// }
// assert.That(isValidEmail)("user@example.com")(t) // Passes
// }
//
// See also:
// - [That]: Creates an assertion from a Predicate
// - [predicate.Predicate]: The underlying predicate type
Predicate[T any] = predicate.Predicate[T]
// Lens is a functional reference to a subpart of a data structure.
//
// This is an alias for [lens.Lens][S, T], which provides a composable way to focus
// on a specific field within a larger structure. Lenses enable getting and setting
// values in nested data structures in a functional, immutable way.
//
// In the context of testing, lenses are used with [LocalL] to focus assertions
// on specific properties of complex objects without manually extracting those properties.
//
// A Lens[S, T] focuses on a value of type T within a structure of type S.
//
// Example:
//
// func TestLensUsage(t *testing.T) {
// type Address struct { City string }
// type User struct { Name string; Address Address }
//
// // Define lenses (typically generated)
// addressLens := lens.Lens[User, Address]{...}
// cityLens := lens.Lens[Address, string]{...}
//
// // Compose lenses to focus on nested field
// userCityLens := lens.Compose(addressLens, cityLens)
//
// // Use with LocalL to assert on nested property
// user := User{Name: "Alice", Address: Address{City: "NYC"}}
// assert.LocalL(userCityLens)(assert.Equal("NYC"))(user)(t)
// }
//
// See also:
// - [LocalL]: Uses a Lens to focus assertions on a property
// - [lens.Lens]: The underlying lens type
// - [Optional]: Similar but for values that may not exist
Lens[S, T any] = lens.Lens[S, T]
// Optional is an optic that focuses on a value that may or may not be present.
//
// This is an alias for [optional.Optional][S, T], which is similar to a [Lens] but
// handles cases where the focused value might not exist. Optionals are useful for
// working with nullable fields, optional properties, or values that might be absent.
//
// In testing, Optionals are used with [FromOptional] to create assertions that
// verify whether an optional value is present and, if so, whether it satisfies
// certain conditions.
//
// An Optional[S, T] focuses on an optional value of type T within a structure of type S.
//
// Example:
//
// func TestOptionalUsage(t *testing.T) {
// type Config struct { Timeout *int }
//
// // Define optional (typically generated)
// timeoutOptional := optional.Optional[Config, int]{...}
//
// // Test when value is present
// config1 := Config{Timeout: ptr(30)}
// assert.FromOptional(timeoutOptional)(
// assert.Equal(30),
// )(config1)(t) // Passes
//
// // Test when value is absent
// config2 := Config{Timeout: nil}
// // FromOptional would fail because value is not present
// }
//
// See also:
// - [FromOptional]: Creates assertions for optional values
// - [optional.Optional]: The underlying optional type
// - [Lens]: Similar but for values that always exist
Optional[S, T any] = optional.Optional[S, T]
// Prism is an optic that focuses on a case of a sum type.
//
// This is an alias for [prism.Prism][S, T], which provides a way to focus on one
// variant of a sum type (like Result, Option, Either, etc.). Prisms enable pattern
// matching and extraction of values from sum types in a functional way.
//
// In testing, Prisms are used with [FromPrism] to create assertions that verify
// whether a value matches a specific case and, if so, whether the contained value
// satisfies certain conditions.
//
// A Prism[S, T] focuses on a value of type T that may be contained within a sum type S.
//
// Example:
//
// func TestPrismUsage(t *testing.T) {
// // Prism for extracting success value from Result
// successPrism := prism.Success[int]()
//
// // Test successful result
// successResult := result.Of[int](42)
// assert.FromPrism(successPrism)(
// assert.Equal(42),
// )(successResult)(t) // Passes
//
// // Prism for extracting error from Result
// failurePrism := prism.Failure[int]()
//
// // Test failed result
// failureResult := result.Error[int](errors.New("failed"))
// assert.FromPrism(failurePrism)(
// assert.Error,
// )(failureResult)(t) // Passes
// }
//
// See also:
// - [FromPrism]: Creates assertions for prism-focused values
// - [prism.Prism]: The underlying prism type
// - [Optional]: Similar but for optional values
Prism[S, T any] = prism.Prism[S, T]
// ReaderIOResult represents a context-aware, IO-based computation that may fail.
//
// This is an alias for [readerioresult.ReaderIOResult][A], which combines three
// computational effects:
// - Reader: Depends on a context (like context.Context)
// - IO: Performs side effects (like file I/O, network calls)
// - Result: May fail with an error
//
// In testing, ReaderIOResult is used with [FromReaderIOResult] to convert
// context-aware, effectful computations into test assertions. This is useful
// when your test assertions need to:
// - Access a context for cancellation or deadlines
// - Perform IO operations (database queries, API calls, file access)
// - Handle potential errors gracefully
//
// Example:
//
// func TestReaderIOResult(t *testing.T) {
// // Create a ReaderIOResult that performs IO and may fail
// checkDatabase := func(ctx context.Context) func() result.Result[assert.Reader] {
// return func() result.Result[assert.Reader] {
// // Perform database check with context
// if err := db.PingContext(ctx); err != nil {
// return result.Error[assert.Reader](err)
// }
// return result.Of[assert.Reader](assert.NoError(nil))
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIOResult(checkDatabase)
// assertion(t)
// }
//
// See also:
// - [FromReaderIOResult]: Converts ReaderIOResult to Reader
// - [ReaderIO]: Similar but without error handling
// - [readerioresult.ReaderIOResult]: The underlying type
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// ReaderIO represents a context-aware, IO-based computation.
//
// This is an alias for [readerio.ReaderIO][A], which combines two computational effects:
// - Reader: Depends on a context (like context.Context)
// - IO: Performs side effects (like logging, metrics)
//
// In testing, ReaderIO is used with [FromReaderIO] to convert context-aware,
// effectful computations into test assertions. This is useful when your test
// assertions need to:
// - Access a context for cancellation or deadlines
// - Perform IO operations that don't fail (or handle failures internally)
// - Integrate with context-aware utilities
//
// Example:
//
// func TestReaderIO(t *testing.T) {
// // Create a ReaderIO that performs IO
// logAndCheck := func(ctx context.Context) func() assert.Reader {
// return func() assert.Reader {
// // Log with context
// logger.InfoContext(ctx, "Running test")
// // Return assertion
// return assert.Equal(42)(computeValue())
// }
// }
//
// // Convert to Reader and execute
// assertion := assert.FromReaderIO(logAndCheck)
// assertion(t)
// }
//
// See also:
// - [FromReaderIO]: Converts ReaderIO to Reader
// - [ReaderIOResult]: Similar but with error handling
// - [readerio.ReaderIO]: The underlying type
ReaderIO[A any] = readerio.ReaderIO[A]
// Seq2 represents a Go iterator that yields key-value pairs.
//
// This is an alias for [iter.Seq2][K, A], which is Go's standard iterator type
// introduced in Go 1.23. It represents a sequence of key-value pairs that can be
// iterated over using a for-range loop.
//
// In testing, Seq2 is used with [SequenceSeq2] to execute a sequence of named
// test cases provided as an iterator. This enables:
// - Lazy evaluation of test cases
// - Memory-efficient testing of large test suites
// - Integration with Go's iterator patterns
// - Dynamic generation of test cases
//
// Example:
//
// func TestSeq2Usage(t *testing.T) {
// // Create an iterator of test cases
// testCases := func(yield func(string, assert.Reader) bool) {
// if !yield("test_addition", assert.Equal(4)(2+2)) {
// return
// }
// if !yield("test_multiplication", assert.Equal(6)(2*3)) {
// return
// }
// }
//
// // Execute all test cases
// assert.SequenceSeq2[assert.Reader](testCases)(t)
// }
//
// See also:
// - [SequenceSeq2]: Executes a Seq2 of test cases
// - [TraverseArray]: Similar but for arrays
// - [iter.Seq2]: The underlying iterator type
Seq2[K, A any] = iter.Seq2[K, A]
// Pair represents a tuple of two values with potentially different types.
//
// This is an alias for [pair.Pair][L, R], which holds two values: a "head" (or "left")
// of type L and a "tail" (or "right") of type R. Pairs are useful for grouping
// related values together without defining a custom struct.
//
// In testing, Pairs are used with [TraverseArray] to associate test names with
// their corresponding assertions. Each element in the array is transformed into
// a Pair[string, Reader] where the string is the test name and the Reader is
// the assertion to execute.
//
// Example:
//
// func TestPairUsage(t *testing.T) {
// type TestCase struct {
// Input int
// Expected int
// }
//
// testCases := []TestCase{
// {Input: 2, Expected: 4},
// {Input: 3, Expected: 9},
// }
//
// // Transform each test case into a named assertion
// traverse := assert.TraverseArray(func(tc TestCase) assert.Pair[string, assert.Reader] {
// name := fmt.Sprintf("square(%d)=%d", tc.Input, tc.Expected)
// assertion := assert.Equal(tc.Expected)(tc.Input * tc.Input)
// return pair.MakePair(name, assertion)
// })
//
// traverse(testCases)(t)
// }
//
// See also:
// - [TraverseArray]: Uses Pairs to create named test cases
// - [pair.Pair]: The underlying pair type
// - [pair.MakePair]: Creates a Pair
// - [pair.Head]: Extracts the first value
// - [pair.Tail]: Extracts the second value
Pair[L, R any] = pair.Pair[L, R]
// Void represents the absence of a meaningful value, similar to unit type in functional programming.
//
// This is an alias for [function.Void], which is used to represent operations that don't
// return a meaningful value but may perform side effects. In the context of testing, Void
// is used with IO operations that perform actions without producing a result.
//
// Void is conceptually similar to:
// - Unit type in functional languages (Haskell's (), Scala's Unit)
// - void in languages like C/Java (but as a value, not just a type)
// - Empty struct{} in Go (but with clearer semantic meaning)
//
// Example:
//
// func TestWithSideEffect(t *testing.T) {
// // An IO operation that logs but returns Void
// logOperation := func() function.Void {
// log.Println("Test executed")
// return function.Void{}
// }
//
// // Execute the operation
// logOperation()
// }
//
// See also:
// - [IO]: Wraps side-effecting operations
// - [function.Void]: The underlying void type
Void = function.Void
// IO represents a side-effecting computation that produces a value of type A.
//
// This is an alias for [io.IO][A], which encapsulates operations that perform side effects
// (like I/O operations, logging, or state mutations) and return a value. IO is a lazy
// computation - it describes an effect but doesn't execute it until explicitly run.
//
// In testing, IO is used to:
// - Defer execution of side effects until needed
// - Compose multiple side-effecting operations
// - Maintain referential transparency in test setup
// - Separate effect description from effect execution
//
// An IO[A] is essentially a function `func() A` that:
// - Encapsulates a side effect
// - Returns a value of type A when executed
// - Can be composed with other IO operations
//
// Example:
//
// func TestIOOperation(t *testing.T) {
// // Define an IO operation that reads a file
// readConfig := func() io.IO[string] {
// return func() string {
// data, _ := os.ReadFile("config.txt")
// return string(data)
// }
// }
//
// // The IO is not executed yet - it's just a description
// configIO := readConfig()
//
// // Execute the IO to get the result
// config := configIO()
// assert.StringNotEmpty(config)(t)
// }
//
// Example with composition:
//
// func TestIOComposition(t *testing.T) {
// // Chain multiple IO operations
// pipeline := io.Map(
// func(s string) int { return len(s) },
// )(readFileIO)
//
// // Execute the composed operation
// length := pipeline()
// assert.That(func(n int) bool { return n > 0 })(length)(t)
// }
//
// See also:
// - [ReaderIO]: Combines Reader and IO effects
// - [ReaderIOResult]: Adds error handling to ReaderIO
// - [io.IO]: The underlying IO type
// - [Void]: Represents operations without meaningful return values
IO[A any] = io.IO[A]
)

View File

@@ -1,7 +1,81 @@
// Copyright (c) 2024 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 builder provides a generic Builder pattern interface for constructing
// complex objects with validation.
//
// The Builder pattern is useful when:
// - Object construction requires multiple steps
// - Construction may fail with validation errors
// - You want to separate construction logic from the object itself
//
// Example usage:
//
// type PersonBuilder struct {
// name string
// age int
// }
//
// func (b PersonBuilder) Build() result.Result[Person] {
// if b.name == "" {
// return result.Error[Person](errors.New("name is required"))
// }
// if b.age < 0 {
// return result.Error[Person](errors.New("age must be non-negative"))
// }
// return result.Of(Person{Name: b.name, Age: b.age})
// }
package builder
type (
// Builder is a generic interface for the Builder pattern that constructs
// objects of type T with validation.
//
// The Build method returns a Result[T] which can be either:
// - Success: containing the constructed object of type T
// - Error: containing an error if validation or construction fails
//
// This allows builders to perform validation and return meaningful errors
// during the construction process, making it explicit that object creation
// may fail.
//
// Type Parameters:
// - T: The type of object being built
//
// Example:
//
// type ConfigBuilder struct {
// host string
// port int
// }
//
// func (b ConfigBuilder) Build() result.Result[Config] {
// if b.host == "" {
// return result.Error[Config](errors.New("host is required"))
// }
// if b.port <= 0 || b.port > 65535 {
// return result.Error[Config](errors.New("invalid port"))
// }
// return result.Of(Config{Host: b.host, Port: b.port})
// }
Builder[T any] interface {
// Build constructs and validates an object of type T.
//
// Returns:
// - Result[T]: A Result containing either the successfully built object
// or an error if validation or construction fails.
Build() Result[T]
}
)

374
v2/builder/builder_test.go Normal file
View File

@@ -0,0 +1,374 @@
// Copyright (c) 2024 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 builder
import (
"errors"
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// Test types for demonstration
type Person struct {
Name string
Age int
}
type PersonBuilder struct {
name string
age int
}
func (b PersonBuilder) WithName(name string) PersonBuilder {
b.name = name
return b
}
func (b PersonBuilder) WithAge(age int) PersonBuilder {
b.age = age
return b
}
func (b PersonBuilder) Build() Result[Person] {
if b.name == "" {
return result.Left[Person](errors.New("name is required"))
}
if b.age < 0 {
return result.Left[Person](errors.New("age must be non-negative"))
}
if b.age > 150 {
return result.Left[Person](errors.New("age must be realistic"))
}
return result.Of(Person{Name: b.name, Age: b.age})
}
func NewPersonBuilder(p Person) PersonBuilder {
return PersonBuilder{name: p.Name, age: p.Age}
}
// Config example for additional test coverage
type Config struct {
Host string
Port int
}
type ConfigBuilder struct {
host string
port int
}
func (b ConfigBuilder) WithHost(host string) ConfigBuilder {
b.host = host
return b
}
func (b ConfigBuilder) WithPort(port int) ConfigBuilder {
b.port = port
return b
}
func (b ConfigBuilder) Build() Result[Config] {
if b.host == "" {
return result.Left[Config](errors.New("host is required"))
}
if b.port <= 0 || b.port > 65535 {
return result.Left[Config](errors.New("port must be between 1 and 65535"))
}
return result.Of(Config{Host: b.host, Port: b.port})
}
func NewConfigBuilder(c Config) ConfigBuilder {
return ConfigBuilder{host: c.Host, port: c.Port}
}
// Tests for Builder interface
func TestBuilder_SuccessfulBuild(t *testing.T) {
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
res := builder.Build()
assert.True(t, result.IsRight(res), "Build should succeed")
person := result.ToOption(res)
assert.True(t, O.IsSome(person), "Result should contain a person")
p := O.GetOrElse(func() Person { return Person{} })(person)
assert.Equal(t, "Alice", p.Name)
assert.Equal(t, 30, p.Age)
}
func TestBuilder_ValidationFailure_MissingName(t *testing.T) {
builder := PersonBuilder{}.WithAge(30)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when name is missing")
err := result.Fold(
func(e error) error { return e },
func(Person) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "name is required", err.Error())
}
func TestBuilder_ValidationFailure_NegativeAge(t *testing.T) {
builder := PersonBuilder{}.
WithName("Bob").
WithAge(-5)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when age is negative")
err := result.Fold(
func(e error) error { return e },
func(Person) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "age must be non-negative", err.Error())
}
func TestBuilder_ValidationFailure_UnrealisticAge(t *testing.T) {
builder := PersonBuilder{}.
WithName("Charlie").
WithAge(200)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when age is unrealistic")
err := result.Fold(
func(e error) error { return e },
func(Person) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "age must be realistic", err.Error())
}
func TestBuilder_ConfigSuccessfulBuild(t *testing.T) {
builder := ConfigBuilder{}.
WithHost("localhost").
WithPort(8080)
res := builder.Build()
assert.True(t, result.IsRight(res), "Build should succeed")
config := result.ToOption(res)
assert.True(t, O.IsSome(config), "Result should contain a config")
c := O.GetOrElse(func() Config { return Config{} })(config)
assert.Equal(t, "localhost", c.Host)
assert.Equal(t, 8080, c.Port)
}
func TestBuilder_ConfigValidationFailure_MissingHost(t *testing.T) {
builder := ConfigBuilder{}.WithPort(8080)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail when host is missing")
err := result.Fold(
func(e error) error { return e },
func(Config) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "host is required", err.Error())
}
func TestBuilder_ConfigValidationFailure_InvalidPort(t *testing.T) {
tests := []struct {
name string
port int
}{
{"zero port", 0},
{"negative port", -1},
{"port too large", 70000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
builder := ConfigBuilder{}.
WithHost("localhost").
WithPort(tt.port)
res := builder.Build()
assert.True(t, result.IsLeft(res), "Build should fail for invalid port")
err := result.Fold(
func(e error) error { return e },
func(Config) error { return errors.New("unexpected success") },
)(res)
assert.Equal(t, "port must be between 1 and 65535", err.Error())
})
}
}
// Tests for BuilderPrism
func TestBuilderPrism_GetOption_ValidBuilder(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
personOpt := prism.GetOption(builder)
assert.True(t, O.IsSome(personOpt), "GetOption should return Some for valid builder")
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
assert.Equal(t, "Alice", person.Name)
assert.Equal(t, 30, person.Age)
}
func TestBuilderPrism_GetOption_InvalidBuilder(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
builder := PersonBuilder{}.WithAge(30) // Missing name
personOpt := prism.GetOption(builder)
assert.True(t, O.IsNone(personOpt), "GetOption should return None for invalid builder")
}
func TestBuilderPrism_ReverseGet(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
person := Person{Name: "Bob", Age: 25}
builder := prism.ReverseGet(person)
assert.Equal(t, "Bob", builder.name)
assert.Equal(t, 25, builder.age)
// Verify the builder can build the same person
res := builder.Build()
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
rebuilt := O.GetOrElse(func() Person { return Person{} })(result.ToOption(res))
assert.Equal(t, person, rebuilt)
}
func TestBuilderPrism_RoundTrip_ValidBuilder(t *testing.T) {
prism := BuilderPrism(NewPersonBuilder)
originalBuilder := PersonBuilder{}.
WithName("Charlie").
WithAge(35)
// Extract person from builder
personOpt := prism.GetOption(originalBuilder)
assert.True(t, O.IsSome(personOpt), "Should extract person from valid builder")
person := O.GetOrElse(func() Person { return Person{} })(personOpt)
// Reconstruct builder from person
rebuiltBuilder := prism.ReverseGet(person)
// Verify the rebuilt builder produces the same person
rebuiltRes := rebuiltBuilder.Build()
assert.True(t, result.IsRight(rebuiltRes), "Rebuilt builder should be valid")
rebuiltPerson := O.GetOrElse(func() Person { return Person{} })(result.ToOption(rebuiltRes))
assert.Equal(t, person, rebuiltPerson)
}
func TestBuilderPrism_ConfigPrism(t *testing.T) {
prism := BuilderPrism(NewConfigBuilder)
builder := ConfigBuilder{}.
WithHost("example.com").
WithPort(443)
configOpt := prism.GetOption(builder)
assert.True(t, O.IsSome(configOpt), "GetOption should return Some for valid config builder")
config := O.GetOrElse(func() Config { return Config{} })(configOpt)
assert.Equal(t, "example.com", config.Host)
assert.Equal(t, 443, config.Port)
}
func TestBuilderPrism_ConfigPrism_InvalidBuilder(t *testing.T) {
prism := BuilderPrism(NewConfigBuilder)
builder := ConfigBuilder{}.WithPort(8080) // Missing host
configOpt := prism.GetOption(builder)
assert.True(t, O.IsNone(configOpt), "GetOption should return None for invalid config builder")
}
func TestBuilderPrism_ConfigPrism_ReverseGet(t *testing.T) {
prism := BuilderPrism(NewConfigBuilder)
config := Config{Host: "api.example.com", Port: 9000}
builder := prism.ReverseGet(config)
assert.Equal(t, "api.example.com", builder.host)
assert.Equal(t, 9000, builder.port)
// Verify the builder can build the same config
res := builder.Build()
assert.True(t, result.IsRight(res), "Builder from ReverseGet should be valid")
rebuilt := O.GetOrElse(func() Config { return Config{} })(result.ToOption(res))
assert.Equal(t, config, rebuilt)
}
// Benchmark tests
func BenchmarkBuilder_SuccessfulBuild(b *testing.B) {
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.Build()
}
}
func BenchmarkBuilder_FailedBuild(b *testing.B) {
builder := PersonBuilder{}.WithAge(30) // Missing name
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = builder.Build()
}
}
func BenchmarkBuilderPrism_GetOption(b *testing.B) {
prism := BuilderPrism(NewPersonBuilder)
builder := PersonBuilder{}.
WithName("Alice").
WithAge(30)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = prism.GetOption(builder)
}
}
func BenchmarkBuilderPrism_ReverseGet(b *testing.B) {
prism := BuilderPrism(NewPersonBuilder)
person := Person{Name: "Bob", Age: 25}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = prism.ReverseGet(person)
}
}

View File

@@ -1,3 +1,18 @@
// Copyright (c) 2024 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 builder
import (
@@ -6,7 +21,61 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// BuilderPrism createa a [Prism] that converts between a builder and its type
// BuilderPrism creates a [Prism] that converts between a builder and its built type.
//
// A Prism is an optic that focuses on a case of a sum type, providing bidirectional
// conversion with the possibility of failure. This function creates a prism that:
// - Extracts: Attempts to build the object from the builder (may fail)
// - Constructs: Creates a builder from a valid object (always succeeds)
//
// The extraction direction (builder -> object) uses the Build method and converts
// the Result to an Option, where errors become None. The construction direction
// (object -> builder) uses the provided creator function.
//
// Type Parameters:
// - T: The type of the object being built
// - B: The builder type that implements Builder[T]
//
// Parameters:
// - creator: A function that creates a builder from a valid object of type T.
// This function should initialize the builder with all fields from the object.
//
// Returns:
// - Prism[B, T]: A prism that can convert between the builder and the built type.
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// type PersonBuilder struct {
// name string
// age int
// }
//
// func (b PersonBuilder) Build() result.Result[Person] {
// if b.name == "" {
// return result.Error[Person](errors.New("name required"))
// }
// return result.Of(Person{Name: b.name, Age: b.age})
// }
//
// func NewPersonBuilder(p Person) PersonBuilder {
// return PersonBuilder{name: p.Name, age: p.Age}
// }
//
// // Create a prism for PersonBuilder
// prism := BuilderPrism(NewPersonBuilder)
//
// // Use the prism to extract a Person from a valid builder
// builder := PersonBuilder{name: "Alice", age: 30}
// person := prism.GetOption(builder) // Some(Person{Name: "Alice", Age: 30})
//
// // Use the prism to create a builder from a Person
// p := Person{Name: "Bob", Age: 25}
// b := prism.ReverseGet(p) // PersonBuilder{name: "Bob", age: 25}
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
return prism.MakePrismWithName(F.Flow2(B.Build, result.ToOption[T]), creator, "BuilderPrism")
}

View File

@@ -4,7 +4,6 @@ import (
"time"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/identity"
"github.com/IBM/fp-go/v2/io"
@@ -14,6 +13,7 @@ import (
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/retry"
)
@@ -241,125 +241,155 @@ func isResetTimeExceeded(ct time.Time) option.Kleisli[openState, openState] {
})
}
// handleSuccessOnClosed handles a successful request when the circuit breaker is in closed state.
// It updates the closed state by recording the success and returns an IO operation that
// modifies the breaker state.
// handleSuccessOnClosed creates a Reader that handles successful requests when the circuit is closed.
// This function is used to update the circuit breaker state after a successful operation completes
// while the circuit is in the closed state.
//
// This function is part of the circuit breaker's state management for the closed state.
// When a request succeeds in closed state:
// 1. The current time is obtained
// 2. The addSuccess function is called with the current time to update the ClosedState
// 3. The updated ClosedState is wrapped in a Right (closed) BreakerState
// 4. The breaker state is modified with the new state
// The function takes a Reader that adds a success record to the ClosedState and lifts it to work
// with BreakerState by mapping over the Right (closed) side of the Either type. This ensures that
// success tracking only affects the closed state and leaves any open state unchanged.
//
// Parameters:
// - currentTime: An IO operation that provides the current time
// - addSuccess: A Reader that takes a time and returns an endomorphism for ClosedState,
// typically resetting failure counters or history
// - addSuccess: A Reader that takes the current time and returns an Endomorphism that updates
// the ClosedState by recording a successful operation. This typically increments a success
// counter or updates a success history.
//
// Returns:
// - An io.Kleisli that takes another io.Kleisli and chains them together.
// The outer Kleisli takes an Endomorphism[BreakerState] and returns BreakerState.
// This allows composing the success handling with other state modifications.
// - A Reader[time.Time, Endomorphism[BreakerState]] that, when given the current time, produces
// an endomorphism that updates the BreakerState by applying the success update to the closed
// state (if closed) or leaving the state unchanged (if open).
//
// Thread Safety: This function creates IO operations that will atomically modify the
// IORef[BreakerState] when executed. The state modifications are thread-safe.
//
// Type signature:
//
// io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState]
// Thread Safety: This is a pure function that creates new state instances. The returned
// endomorphism is safe for concurrent use as it does not mutate its input.
//
// Usage Context:
// - Called when a request succeeds while the circuit is closed
// - Resets failure tracking (counter or history) in the ClosedState
// - Keeps the circuit in closed state
// - Called after a successful request completes while the circuit is closed
// - Updates success metrics/counters in the ClosedState
// - Does not affect the circuit state if it's already open
// - Part of the normal operation flow when the circuit breaker is functioning properly
func handleSuccessOnClosed(
currentTime IO[time.Time],
addSuccess Reader[time.Time, Endomorphism[ClosedState]],
) io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState] {
) Reader[time.Time, Endomorphism[BreakerState]] {
return F.Flow2(
io.Chain,
identity.Flap[IO[BreakerState]](F.Pipe1(
currentTime,
io.Map(F.Flow2(
addSuccess,
either.Map[openState],
)))),
addSuccess,
either.Map[openState],
)
}
// handleFailureOnClosed handles a failed request when the circuit breaker is in closed state.
// It updates the closed state by recording the failure and checks if the circuit should open.
// handleFailureOnClosed creates a Reader that handles failed requests when the circuit is closed.
// This function manages the critical logic for determining whether a failure should cause the
// circuit breaker to open (transition from closed to open state).
//
// This function is part of the circuit breaker's state management for the closed state.
// When a request fails in closed state:
// 1. The current time is obtained
// 2. The addError function is called to record the failure in the ClosedState
// 3. The checkClosedState function is called to determine if the failure threshold is exceeded
// 4. If the threshold is exceeded (Check returns None):
// - The circuit transitions to open state using openCircuit
// - A new openState is created with resetAt time calculated from the retry policy
// 5. If the threshold is not exceeded (Check returns Some):
// - The circuit remains closed with the updated failure tracking
// The function orchestrates three key operations:
// 1. Records the failure in the ClosedState using addError
// 2. Checks if the failure threshold has been exceeded using checkClosedState
// 3. If threshold exceeded, opens the circuit; otherwise, keeps it closed with updated error count
//
// The decision flow is:
// - Add the error to the closed state's error tracking
// - Check if the updated closed state exceeds the failure threshold
// - If threshold exceeded (checkClosedState returns None):
// - Create a new openState with calculated reset time based on retry policy
// - Transition the circuit to open state (Left side of Either)
// - If threshold not exceeded (checkClosedState returns Some):
// - Keep the circuit closed with the updated error count
// - Continue allowing requests through
//
// Parameters:
// - currentTime: An IO operation that provides the current time
// - addError: A Reader that takes a time and returns an endomorphism for ClosedState,
// recording a failure (incrementing counter or adding to history)
// - checkClosedState: A Reader that takes a time and returns an option.Kleisli that checks
// if the ClosedState should remain closed. Returns Some if circuit stays closed, None if it should open.
// - openCircuit: A Reader that takes a time and returns an openState with calculated resetAt time
// - addError: A Reader that takes the current time and returns an Endomorphism that updates
// the ClosedState by recording a failed operation. This typically increments an error
// counter or adds to an error history.
// - checkClosedState: A Reader that takes the current time and returns an option.Kleisli that
// validates whether the ClosedState is still within acceptable failure thresholds.
// Returns Some(ClosedState) if threshold not exceeded, None if threshold exceeded.
// - openCircuit: A Reader that takes the current time and creates a new openState with
// appropriate reset time calculated from the retry policy. Used when transitioning to open.
//
// Returns:
// - An io.Kleisli that takes another io.Kleisli and chains them together.
// The outer Kleisli takes an Endomorphism[BreakerState] and returns BreakerState.
// This allows composing the failure handling with other state modifications.
// - A Reader[time.Time, Endomorphism[BreakerState]] that, when given the current time, produces
// an endomorphism that either:
// - Keeps the circuit closed with updated error tracking (if threshold not exceeded)
// - Opens the circuit with calculated reset time (if threshold exceeded)
//
// Thread Safety: This function creates IO operations that will atomically modify the
// IORef[BreakerState] when executed. The state modifications are thread-safe.
//
// Type signature:
//
// io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState]
//
// State Transitions:
// - Closed -> Closed: When failure threshold is not exceeded (Some from checkClosedState)
// - Closed -> Open: When failure threshold is exceeded (None from checkClosedState)
// Thread Safety: This is a pure function that creates new state instances. The returned
// endomorphism is safe for concurrent use as it does not mutate its input.
//
// Usage Context:
// - Called when a request fails while the circuit is closed
// - Records the failure in the ClosedState (counter or history)
// - May trigger transition to open state if threshold is exceeded
// - Called after a failed request completes while the circuit is closed
// - Implements the core circuit breaker logic for opening the circuit
// - Determines when to stop allowing requests through to protect the failing service
// - Critical for preventing cascading failures in distributed systems
//
// State Transition:
// - Closed (under threshold) -> Closed (with incremented error count)
// - Closed (at/over threshold) -> Open (with reset time for recovery attempt)
func handleFailureOnClosed(
currentTime IO[time.Time],
addError Reader[time.Time, Endomorphism[ClosedState]],
checkClosedState Reader[time.Time, option.Kleisli[ClosedState, ClosedState]],
openCircuit Reader[time.Time, openState],
) io.Kleisli[io.Kleisli[Endomorphism[BreakerState], BreakerState], BreakerState] {
return F.Flow2(
io.Chain,
identity.Flap[IO[BreakerState]](F.Pipe1(
currentTime,
io.Map(func(ct time.Time) either.Operator[openState, ClosedState, ClosedState] {
return either.Chain(F.Flow3(
addError(ct),
checkClosedState(ct),
option.Fold(
F.Pipe2(
ct,
lazy.Of,
lazy.Map(F.Flow2(
openCircuit,
createOpenCircuit,
)),
),
createClosedCircuit,
),
))
}))),
) Reader[time.Time, Endomorphism[BreakerState]] {
return F.Pipe2(
F.Pipe1(
addError,
reader.ApS(reader.Map[ClosedState], checkClosedState),
),
reader.Chain(F.Flow2(
reader.Map[ClosedState](option.Fold(
F.Pipe2(
openCircuit,
reader.Map[time.Time](createOpenCircuit),
lazy.Of,
),
F.Flow2(
createClosedCircuit,
reader.Of[time.Time],
),
)),
reader.Sequence,
)),
reader.Map[time.Time](either.Chain[openState, ClosedState, ClosedState]),
)
}
func handleErrorOnClosed2[E any](
checkError option.Kleisli[E, E],
onSuccess Reader[time.Time, Endomorphism[BreakerState]],
onFailure Reader[time.Time, Endomorphism[BreakerState]],
) reader.Kleisli[time.Time, E, Endomorphism[BreakerState]] {
return F.Flow3(
checkError,
option.MapTo[E](onFailure),
option.GetOrElse(lazy.Of(onSuccess)),
)
}
func stateModifier(
modify io.Kleisli[Endomorphism[BreakerState], BreakerState],
) reader.Operator[time.Time, Endomorphism[BreakerState], IO[BreakerState]] {
return reader.Map[time.Time](modify)
}
func reportOnClose2(
onClosed ReaderIO[time.Time, Void],
onOpened ReaderIO[time.Time, Void],
) readerio.Operator[time.Time, BreakerState, Void] {
return readerio.Chain(either.Fold(
reader.Of[openState](onOpened),
reader.Of[ClosedState](onClosed),
))
}
func applyAndReportClose2(
currentTime IO[time.Time],
metrics readerio.Operator[time.Time, BreakerState, Void],
) func(io.Kleisli[Endomorphism[BreakerState], BreakerState]) func(Reader[time.Time, Endomorphism[BreakerState]]) IO[Void] {
return func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) func(Reader[time.Time, Endomorphism[BreakerState]]) IO[Void] {
return F.Flow3(
reader.Map[time.Time](modify),
metrics,
readerio.ReadIO[Void](currentTime),
)
}
}
// MakeCircuitBreaker creates a circuit breaker implementation for a higher-kinded type.
@@ -402,6 +432,8 @@ func MakeCircuitBreaker[E, T, HKTT, HKTOP, HKTHKTT any](
chainFirstIOK func(io.Kleisli[T, BreakerState]) func(HKTT) HKTT,
chainFirstLeftIOK func(io.Kleisli[E, BreakerState]) func(HKTT) HKTT,
chainFirstIOK2 func(io.Kleisli[Either[E, T], Void]) func(HKTT) HKTT,
fromIO func(IO[func(HKTT) HKTT]) HKTOP,
flap func(HKTT) func(HKTOP) HKTHKTT,
flatten func(HKTHKTT) HKTT,
@@ -437,47 +469,22 @@ func MakeCircuitBreaker[E, T, HKTT, HKTOP, HKTHKTT any](
reader.Of[HKTT],
)
handleSuccess := handleSuccessOnClosed(currentTime, addSuccess)
handleFailure := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
handleSuccess2 := handleSuccessOnClosed(addSuccess)
handleFailure2 := handleFailureOnClosed(addError, checkClosedState, openCircuit)
handleError2 := handleErrorOnClosed2(checkError, handleSuccess2, handleFailure2)
metricsClose2 := reportOnClose2(metrics.Accept, metrics.Open)
apply2 := applyAndReportClose2(currentTime, metricsClose2)
onClosed := func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) Operator {
return F.Flow2(
// error case
chainFirstLeftIOK(F.Flow3(
checkError,
option.Fold(
// the error is not applicable, handle as success
F.Pipe2(
modify,
handleSuccess,
lazy.Of,
),
// the error is relevant, record it
F.Pipe2(
modify,
handleFailure,
reader.Of[E],
),
),
// metering
io.ChainFirst(either.Fold(
F.Flow2(
openedAtLens.Get,
metrics.Open,
),
func(c ClosedState) IO[Void] {
return io.Of(function.VOID)
},
)),
)),
// good case
chainFirstIOK(F.Pipe2(
modify,
handleSuccess,
reader.Of[T],
)),
)
return chainFirstIOK2(F.Flow2(
either.Fold(
handleError2,
reader.Of[T](handleSuccess2),
),
apply2(modify),
))
}
onCanary := func(modify io.Kleisli[Endomorphism[BreakerState], BreakerState]) Operator {

View File

@@ -5,12 +5,12 @@ import (
"testing"
"time"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioref"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
@@ -452,43 +452,128 @@ func TestIsResetTimeExceeded(t *testing.T) {
// TestHandleSuccessOnClosed tests the handleSuccessOnClosed function
func TestHandleSuccessOnClosed(t *testing.T) {
t.Run("resets failure count on success", func(t *testing.T) {
t.Run("updates closed state with success when circuit is closed", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addSuccess := reader.From1(ClosedState.AddSuccess)
currentTime := vt.Now()
// Create initial state with some failures
now := vt.Now()
// Create a simple addSuccess reader that increments a counter
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddSuccess(ct)
}
}
// Create initial closed state
initialClosed := MakeClosedStateCounter(3)
initialClosed = initialClosed.AddError(now)
initialClosed = initialClosed.AddError(now)
initialState := createClosedCircuit(initialClosed)
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
// Apply handleSuccessOnClosed
handler := handleSuccessOnClosed(addSuccess)
endomorphism := handler(currentTime)
result := endomorphism(initialState)
handler := handleSuccessOnClosed(currentTime, addSuccess)
// Verify the state is still closed
assert.True(t, IsClosed(result), "state should remain closed after success")
// Apply the handler
result := io.Run(handler(modify))
// Verify state is still closed and failures are reset
assert.True(t, IsClosed(result), "circuit should remain closed after success")
// Verify the closed state was updated
closedState := either.Fold(
func(openState) ClosedState { return initialClosed },
F.Identity[ClosedState],
)(result)
// The success should have been recorded (implementation-specific verification)
assert.NotNil(t, closedState, "closed state should be present")
})
t.Run("keeps circuit closed", func(t *testing.T) {
t.Run("does not affect open state", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addSuccess := reader.From1(ClosedState.AddSuccess)
currentTime := vt.Now()
initialState := createClosedCircuit(MakeClosedStateCounter(3))
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddSuccess(ct)
}
}
handler := handleSuccessOnClosed(currentTime, addSuccess)
result := io.Run(handler(modify))
// Create initial open state
initialOpen := openState{
openedAt: currentTime.Add(-1 * time.Minute),
resetAt: currentTime.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
initialState := createOpenCircuit(initialOpen)
assert.True(t, IsClosed(result), "circuit should remain closed")
// Apply handleSuccessOnClosed
handler := handleSuccessOnClosed(addSuccess)
endomorphism := handler(currentTime)
result := endomorphism(initialState)
// Verify the state remains open and unchanged
assert.True(t, IsOpen(result), "state should remain open")
// Extract and verify the open state is unchanged
openResult := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return initialOpen },
)(result)
assert.Equal(t, initialOpen.openedAt, openResult.openedAt, "openedAt should be unchanged")
assert.Equal(t, initialOpen.resetAt, openResult.resetAt, "resetAt should be unchanged")
assert.Equal(t, initialOpen.canaryRequest, openResult.canaryRequest, "canaryRequest should be unchanged")
})
t.Run("preserves time parameter through reader", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
time1 := vt.Now()
vt.Advance(1 * time.Hour)
time2 := vt.Now()
var capturedTime time.Time
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
capturedTime = ct
return F.Identity[ClosedState]
}
initialClosed := MakeClosedStateCounter(3)
initialState := createClosedCircuit(initialClosed)
handler := handleSuccessOnClosed(addSuccess)
// Apply with time1
endomorphism1 := handler(time1)
endomorphism1(initialState)
assert.Equal(t, time1, capturedTime, "should pass time1 to addSuccess")
// Apply with time2
endomorphism2 := handler(time2)
endomorphism2(initialState)
assert.Equal(t, time2, capturedTime, "should pass time2 to addSuccess")
})
t.Run("composes correctly with multiple successes", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
addSuccess := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddSuccess(ct)
}
}
initialClosed := MakeClosedStateCounter(3)
initialState := createClosedCircuit(initialClosed)
handler := handleSuccessOnClosed(addSuccess)
endomorphism := handler(currentTime)
// Apply multiple times
result1 := endomorphism(initialState)
result2 := endomorphism(result1)
result3 := endomorphism(result2)
// All should remain closed
assert.True(t, IsClosed(result1), "state should remain closed after first success")
assert.True(t, IsClosed(result2), "state should remain closed after second success")
assert.True(t, IsClosed(result3), "state should remain closed after third success")
})
}
@@ -496,9 +581,26 @@ func TestHandleSuccessOnClosed(t *testing.T) {
func TestHandleFailureOnClosed(t *testing.T) {
t.Run("keeps circuit closed when threshold not exceeded", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addError := reader.From1(ClosedState.AddError)
checkClosedState := reader.From1(ClosedState.Check)
currentTime := vt.Now()
// Create a closed state that allows 3 errors
initialClosed := MakeClosedStateCounter(3)
// addError increments error count
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
// checkClosedState returns Some if under threshold
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
// openCircuit creates an open state (shouldn't be called in this test)
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
@@ -508,26 +610,39 @@ func TestHandleFailureOnClosed(t *testing.T) {
}
}
// Create initial state with room for more failures
now := vt.Now()
initialClosed := MakeClosedStateCounter(5) // threshold is 5
initialClosed = initialClosed.AddError(now)
initialState := createClosedCircuit(initialClosed)
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
result := io.Run(handler(modify))
// First error - should stay closed
result1 := endomorphism(initialState)
assert.True(t, IsClosed(result1), "circuit should remain closed after first error")
assert.True(t, IsClosed(result), "circuit should remain closed when threshold not exceeded")
// Second error - should stay closed
result2 := endomorphism(result1)
assert.True(t, IsClosed(result2), "circuit should remain closed after second error")
})
t.Run("opens circuit when threshold exceeded", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addError := reader.From1(ClosedState.AddError)
checkClosedState := reader.From1(ClosedState.Check)
currentTime := vt.Now()
// Create a closed state that allows only 2 errors (opens at 2nd error)
initialClosed := MakeClosedStateCounter(2)
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
@@ -537,26 +652,85 @@ func TestHandleFailureOnClosed(t *testing.T) {
}
}
// Create initial state at threshold
now := vt.Now()
initialClosed := MakeClosedStateCounter(2) // threshold is 2
initialClosed = initialClosed.AddError(now)
initialState := createClosedCircuit(initialClosed)
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
result := io.Run(handler(modify))
// First error - should stay closed (count=1, threshold=2)
result1 := endomorphism(initialState)
assert.True(t, IsClosed(result1), "circuit should remain closed after first error")
assert.True(t, IsOpen(result), "circuit should open when threshold exceeded")
// Second error - should open (count=2, threshold=2)
result2 := endomorphism(result1)
assert.True(t, IsOpen(result2), "circuit should open when threshold reached")
})
t.Run("records failure in closed state", func(t *testing.T) {
t.Run("creates open state with correct reset time", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now
addError := reader.From1(ClosedState.AddError)
checkClosedState := reader.From1(ClosedState.Check)
currentTime := vt.Now()
expectedResetTime := currentTime.Add(5 * time.Minute)
initialClosed := MakeClosedStateCounter(1) // Opens at 1st error
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: expectedResetTime,
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// First error - should open immediately (threshold=1)
result1 := endomorphism(initialState)
assert.True(t, IsOpen(result1), "circuit should open after first error")
// Verify the open state has correct reset time
resultOpen := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return openState{} },
)(result1)
assert.Equal(t, expectedResetTime, resultOpen.resetAt, "reset time should match expected")
assert.Equal(t, currentTime, resultOpen.openedAt, "opened time should be current time")
})
t.Run("edge case: zero error threshold", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
// Create a closed state that allows 0 errors (opens immediately)
initialClosed := MakeClosedStateCounter(0)
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
@@ -566,14 +740,212 @@ func TestHandleFailureOnClosed(t *testing.T) {
}
}
initialState := createClosedCircuit(MakeClosedStateCounter(10))
ref := io.Run(ioref.MakeIORef(initialState))
modify := modifyV(ref)
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(currentTime, addError, checkClosedState, openCircuit)
result := io.Run(handler(modify))
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Should still be closed but with failure recorded
assert.True(t, IsClosed(result), "circuit should remain closed")
// First error should immediately open the circuit
result := endomorphism(initialState)
assert.True(t, IsOpen(result), "circuit should open immediately with zero threshold")
})
t.Run("edge case: very high error threshold", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
// Create a closed state that allows 1000 errors
initialClosed := MakeClosedStateCounter(1000)
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Apply many errors
result := initialState
for i := 0; i < 100; i++ {
result = endomorphism(result)
}
// Should still be closed after 100 errors
assert.True(t, IsClosed(result), "circuit should remain closed with high threshold")
})
t.Run("preserves time parameter through reader chain", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
time1 := vt.Now()
vt.Advance(2 * time.Hour)
time2 := vt.Now()
var capturedAddErrorTime, capturedCheckTime, capturedOpenTime time.Time
initialClosed := MakeClosedStateCounter(2) // Need 2 errors to open
addError := func(ct time.Time) Endomorphism[ClosedState] {
capturedAddErrorTime = ct
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
capturedCheckTime = ct
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
capturedOpenTime = ct
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
initialState := createClosedCircuit(initialClosed)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
// Apply with time1 - first error, stays closed
endomorphism1 := handler(time1)
result1 := endomorphism1(initialState)
assert.Equal(t, time1, capturedAddErrorTime, "addError should receive time1")
assert.Equal(t, time1, capturedCheckTime, "checkClosedState should receive time1")
// Apply with time2 - second error, should trigger open
endomorphism2 := handler(time2)
endomorphism2(result1)
assert.Equal(t, time2, capturedAddErrorTime, "addError should receive time2")
assert.Equal(t, time2, capturedCheckTime, "checkClosedState should receive time2")
assert.Equal(t, time2, capturedOpenTime, "openCircuit should receive time2")
})
t.Run("handles transition from closed to open correctly", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
initialClosed := MakeClosedStateCounter(2) // Opens at 2nd error
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Start with closed state
state := createClosedCircuit(initialClosed)
assert.True(t, IsClosed(state), "initial state should be closed")
// First error - should stay closed (count=1, threshold=2)
state = endomorphism(state)
assert.True(t, IsClosed(state), "should remain closed after first error")
// Second error - should open (count=2, threshold=2)
state = endomorphism(state)
assert.True(t, IsOpen(state), "should open after second error")
// Verify it's truly open with correct properties
resultOpen := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return openState{} },
)(state)
assert.False(t, resultOpen.canaryRequest, "canaryRequest should be false initially")
assert.Equal(t, currentTime, resultOpen.openedAt, "openedAt should be current time")
})
t.Run("does not affect already open state", func(t *testing.T) {
vt := NewVirtualTimer(time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC))
currentTime := vt.Now()
addError := func(ct time.Time) Endomorphism[ClosedState] {
return func(cs ClosedState) ClosedState {
return cs.AddError(ct)
}
}
checkClosedState := func(ct time.Time) option.Kleisli[ClosedState, ClosedState] {
return func(cs ClosedState) Option[ClosedState] {
return cs.Check(ct)
}
}
openCircuit := func(ct time.Time) openState {
return openState{
openedAt: ct,
resetAt: ct.Add(1 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: false,
}
}
// Start with an already open state
existingOpen := openState{
openedAt: currentTime.Add(-5 * time.Minute),
resetAt: currentTime.Add(5 * time.Minute),
retryStatus: retry.DefaultRetryStatus,
canaryRequest: true,
}
initialState := createOpenCircuit(existingOpen)
handler := handleFailureOnClosed(addError, checkClosedState, openCircuit)
endomorphism := handler(currentTime)
// Apply to open state - should not change it
result := endomorphism(initialState)
assert.True(t, IsOpen(result), "state should remain open")
// The open state should be unchanged since handleFailureOnClosed
// only operates on the Right (closed) side of the Either
openResult := either.Fold(
func(os openState) openState { return os },
func(ClosedState) openState { return openState{} },
)(result)
assert.Equal(t, existingOpen.openedAt, openResult.openedAt, "openedAt should be unchanged")
assert.Equal(t, existingOpen.resetAt, openResult.resetAt, "resetAt should be unchanged")
assert.Equal(t, existingOpen.canaryRequest, openResult.canaryRequest, "canaryRequest should be unchanged")
})
}

View File

@@ -28,7 +28,10 @@ import (
//
// Thread Safety: This type is immutable and safe for concurrent use.
type CircuitBreakerError struct {
Name string
// Name: The name identifying this circuit breaker instance
Name string
// ResetAt: The time at which the circuit breaker will transition from open to half-open state
ResetAt time.Time
}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
)
type (
@@ -110,6 +111,25 @@ type (
name string
logger *log.Logger
}
// voidMetrics is a no-op implementation of the Metrics interface that does nothing.
// All methods return the same pre-allocated IO[Void] operation that immediately returns
// without performing any action.
//
// This implementation is useful for:
// - Testing scenarios where metrics collection is not needed
// - Production environments where metrics overhead should be eliminated
// - Benchmarking circuit breaker logic without metrics interference
// - Default initialization when no metrics implementation is provided
//
// Thread Safety: This implementation is safe for concurrent use. The noop IO operation
// is immutable and can be safely shared across goroutines.
//
// Performance: This is the most efficient Metrics implementation as it performs no
// operations and has minimal memory overhead (single shared IO[Void] instance).
voidMetrics struct {
noop IO[Void]
}
)
// doLog is a helper method that creates an IO operation for logging a circuit breaker event.
@@ -206,3 +226,79 @@ func (m *loggingMetrics) Canary(ct time.Time) IO[Void] {
func MakeMetricsFromLogger(name string, logger *log.Logger) Metrics {
return &loggingMetrics{name: name, logger: logger}
}
// Open implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Open(_ time.Time) IO[Void] {
return m.noop
}
// Accept implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Accept(_ time.Time) IO[Void] {
return m.noop
}
// Canary implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Canary(_ time.Time) IO[Void] {
return m.noop
}
// Close implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Close(_ time.Time) IO[Void] {
return m.noop
}
// Reject implements the Metrics interface for voidMetrics.
// Returns a no-op IO operation that does nothing.
//
// Thread Safety: Safe for concurrent use.
func (m *voidMetrics) Reject(_ time.Time) IO[Void] {
return m.noop
}
// MakeVoidMetrics creates a no-op Metrics implementation that performs no operations.
// All methods return the same pre-allocated IO[Void] operation that does nothing when executed.
//
// This is useful for:
// - Testing scenarios where metrics collection is not needed
// - Production environments where metrics overhead should be eliminated
// - Benchmarking circuit breaker logic without metrics interference
// - Default initialization when no metrics implementation is provided
//
// Returns:
// - Metrics: A thread-safe no-op Metrics implementation
//
// Thread Safety: The returned Metrics implementation is safe for concurrent use.
// All methods return the same immutable IO[Void] operation.
//
// Performance: This is the most efficient Metrics implementation with minimal overhead.
// The IO[Void] operation is pre-allocated once and reused for all method calls.
//
// Example:
//
// metrics := MakeVoidMetrics()
//
// // All operations do nothing
// io.Run(metrics.Open(time.Now())) // No-op
// io.Run(metrics.Accept(time.Now())) // No-op
// io.Run(metrics.Reject(time.Now())) // No-op
//
// // Useful for testing
// breaker := MakeCircuitBreaker(
// // ... other parameters ...
// MakeVoidMetrics(), // No metrics overhead
// )
func MakeVoidMetrics() Metrics {
return &voidMetrics{io.Of(function.VOID)}
}

View File

@@ -504,3 +504,443 @@ func TestMetricsIOOperations(t *testing.T) {
assert.Len(t, lines, 3, "should execute multiple times")
})
}
// TestMakeVoidMetrics tests the MakeVoidMetrics constructor
func TestMakeVoidMetrics(t *testing.T) {
t.Run("creates valid Metrics implementation", func(t *testing.T) {
metrics := MakeVoidMetrics()
assert.NotNil(t, metrics, "MakeVoidMetrics should return non-nil Metrics")
})
t.Run("returns voidMetrics type", func(t *testing.T) {
metrics := MakeVoidMetrics()
_, ok := metrics.(*voidMetrics)
assert.True(t, ok, "should return *voidMetrics type")
})
t.Run("initializes noop IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics().(*voidMetrics)
assert.NotNil(t, metrics.noop, "noop IO operation should be initialized")
})
}
// TestVoidMetricsAccept tests the Accept method of voidMetrics
func TestVoidMetricsAccept(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Accept(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Accept(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics().(*voidMetrics)
timestamp := time.Now()
ioOp1 := metrics.Accept(timestamp)
ioOp2 := metrics.Accept(timestamp)
// Both should be non-nil (we can't compare functions directly in Go)
assert.NotNil(t, ioOp1, "should return non-nil IO operation")
assert.NotNil(t, ioOp2, "should return non-nil IO operation")
// Verify they execute without error
io.Run(ioOp1)
io.Run(ioOp2)
})
t.Run("ignores timestamp parameter", func(t *testing.T) {
metrics := MakeVoidMetrics()
time1 := time.Date(2026, 1, 9, 15, 30, 0, 0, time.UTC)
time2 := time.Date(2026, 1, 9, 16, 30, 0, 0, time.UTC)
ioOp1 := metrics.Accept(time1)
ioOp2 := metrics.Accept(time2)
// Should return same operation regardless of timestamp
io.Run(ioOp1)
io.Run(ioOp2)
// No assertions needed - just verify it doesn't panic
})
}
// TestVoidMetricsReject tests the Reject method of voidMetrics
func TestVoidMetricsReject(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Reject(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Reject(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Reject(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsOpen tests the Open method of voidMetrics
func TestVoidMetricsOpen(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Open(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Open(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Open(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsClose tests the Close method of voidMetrics
func TestVoidMetricsClose(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Close(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Close(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Close(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsCanary tests the Canary method of voidMetrics
func TestVoidMetricsCanary(t *testing.T) {
t.Run("returns non-nil IO operation", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Canary(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
})
t.Run("IO operation executes without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Canary(timestamp)
result := io.Run(ioOp)
assert.NotNil(t, result, "IO operation should execute successfully")
})
t.Run("returns same IO operation instance", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Canary(timestamp)
assert.NotNil(t, ioOp, "should return non-nil IO operation")
io.Run(ioOp) // Verify it executes without error
})
}
// TestVoidMetricsThreadSafety tests concurrent access to voidMetrics
func TestVoidMetricsThreadSafety(t *testing.T) {
t.Run("handles concurrent metric calls", func(t *testing.T) {
metrics := MakeVoidMetrics()
var wg sync.WaitGroup
numGoroutines := 100
wg.Add(numGoroutines * 5) // 5 methods
timestamp := time.Now()
// Launch multiple goroutines calling all methods concurrently
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
io.Run(metrics.Accept(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Reject(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Open(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Close(timestamp))
}()
go func() {
defer wg.Done()
io.Run(metrics.Canary(timestamp))
}()
}
wg.Wait()
// Test passes if no panic occurs
})
t.Run("all methods return valid IO operations concurrently", func(t *testing.T) {
metrics := MakeVoidMetrics()
var wg sync.WaitGroup
numGoroutines := 50
wg.Add(numGoroutines)
timestamp := time.Now()
results := make([]IO[Void], numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(idx int) {
defer wg.Done()
// Each goroutine calls a different method
switch idx % 5 {
case 0:
results[idx] = metrics.Accept(timestamp)
case 1:
results[idx] = metrics.Reject(timestamp)
case 2:
results[idx] = metrics.Open(timestamp)
case 3:
results[idx] = metrics.Close(timestamp)
case 4:
results[idx] = metrics.Canary(timestamp)
}
}(i)
}
wg.Wait()
// All results should be non-nil and executable
for i, result := range results {
assert.NotNil(t, result, "result %d should be non-nil", i)
io.Run(result) // Verify it executes without error
}
})
}
// TestVoidMetricsPerformance tests performance characteristics
func TestVoidMetricsPerformance(t *testing.T) {
t.Run("has minimal overhead", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
// Execute many operations quickly
iterations := 10000
for i := 0; i < iterations; i++ {
io.Run(metrics.Accept(timestamp))
io.Run(metrics.Reject(timestamp))
io.Run(metrics.Open(timestamp))
io.Run(metrics.Close(timestamp))
io.Run(metrics.Canary(timestamp))
}
// Test passes if it completes quickly without issues
})
t.Run("all methods return valid IO operations", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
// All methods should return non-nil IO operations
accept := metrics.Accept(timestamp)
reject := metrics.Reject(timestamp)
open := metrics.Open(timestamp)
close := metrics.Close(timestamp)
canary := metrics.Canary(timestamp)
assert.NotNil(t, accept, "Accept should return non-nil")
assert.NotNil(t, reject, "Reject should return non-nil")
assert.NotNil(t, open, "Open should return non-nil")
assert.NotNil(t, close, "Close should return non-nil")
assert.NotNil(t, canary, "Canary should return non-nil")
// All should execute without error
io.Run(accept)
io.Run(reject)
io.Run(open)
io.Run(close)
io.Run(canary)
})
}
// TestVoidMetricsIntegration tests integration scenarios
func TestVoidMetricsIntegration(t *testing.T) {
t.Run("can be used as drop-in replacement for loggingMetrics", func(t *testing.T) {
// Create both types of metrics
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
loggingMetrics := MakeMetricsFromLogger("TestCircuit", logger)
voidMetrics := MakeVoidMetrics()
timestamp := time.Now()
// Both should implement the same interface
var m1 Metrics = loggingMetrics
var m2 Metrics = voidMetrics
// Both should be callable
io.Run(m1.Accept(timestamp))
io.Run(m2.Accept(timestamp))
// Logging metrics should have output
assert.NotEmpty(t, buf.String(), "logging metrics should produce output")
// Void metrics should have no observable side effects
// (we can't directly test this, but the test passes if no panic occurs)
})
t.Run("simulates complete circuit breaker lifecycle without side effects", func(t *testing.T) {
metrics := MakeVoidMetrics()
baseTime := time.Date(2026, 1, 9, 15, 30, 0, 0, time.UTC)
// Simulate circuit breaker lifecycle - all should be no-ops
io.Run(metrics.Accept(baseTime))
io.Run(metrics.Accept(baseTime.Add(1 * time.Second)))
io.Run(metrics.Open(baseTime.Add(2 * time.Second)))
io.Run(metrics.Reject(baseTime.Add(3 * time.Second)))
io.Run(metrics.Canary(baseTime.Add(30 * time.Second)))
io.Run(metrics.Close(baseTime.Add(31 * time.Second)))
// Test passes if no panic occurs and completes quickly
})
}
// TestVoidMetricsEdgeCases tests edge cases
func TestVoidMetricsEdgeCases(t *testing.T) {
t.Run("handles zero time", func(t *testing.T) {
metrics := MakeVoidMetrics()
zeroTime := time.Time{}
io.Run(metrics.Accept(zeroTime))
io.Run(metrics.Reject(zeroTime))
io.Run(metrics.Open(zeroTime))
io.Run(metrics.Close(zeroTime))
io.Run(metrics.Canary(zeroTime))
// Test passes if no panic occurs
})
t.Run("handles far future time", func(t *testing.T) {
metrics := MakeVoidMetrics()
futureTime := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
io.Run(metrics.Accept(futureTime))
io.Run(metrics.Reject(futureTime))
io.Run(metrics.Open(futureTime))
io.Run(metrics.Close(futureTime))
io.Run(metrics.Canary(futureTime))
// Test passes if no panic occurs
})
t.Run("IO operations are idempotent", func(t *testing.T) {
metrics := MakeVoidMetrics()
timestamp := time.Now()
ioOp := metrics.Accept(timestamp)
// Execute same operation multiple times
io.Run(ioOp)
io.Run(ioOp)
io.Run(ioOp)
// Test passes if no panic occurs
})
}
// TestMetricsComparison compares loggingMetrics and voidMetrics
func TestMetricsComparison(t *testing.T) {
t.Run("both implement Metrics interface", func(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
var m1 Metrics = MakeMetricsFromLogger("Test", logger)
var m2 Metrics = MakeVoidMetrics()
assert.NotNil(t, m1)
assert.NotNil(t, m2)
})
t.Run("voidMetrics has no observable side effects unlike loggingMetrics", func(t *testing.T) {
var buf bytes.Buffer
logger := log.New(&buf, "", 0)
loggingMetrics := MakeMetricsFromLogger("Test", logger)
voidMetrics := MakeVoidMetrics()
timestamp := time.Now()
// Logging metrics produces output
io.Run(loggingMetrics.Accept(timestamp))
assert.NotEmpty(t, buf.String(), "logging metrics should produce output")
// Void metrics has no observable output
// (we can only verify it doesn't panic)
io.Run(voidMetrics.Accept(timestamp))
})
}

View File

@@ -34,6 +34,7 @@ import (
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/retry"
"github.com/IBM/fp-go/v2/state"
)
@@ -79,10 +80,13 @@ type (
// and produces a value of type A. Used for dependency injection and configuration.
Reader[R, A any] = reader.Reader[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// openState represents the internal state when the circuit breaker is open.
// In the open state, requests are blocked to give the failing service time to recover.
// The circuit breaker will transition to a half-open state (canary request) after resetAt.
openState struct {
// openedAt is the time when the circuit breaker opened the circuit
openedAt time.Time
// resetAt is the time when the circuit breaker should attempt a canary request

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateTraverseTuple(f *os.File, i int) {
@@ -422,10 +423,10 @@ func ApplyCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateApplyHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func createCombinations(n int, all, prev []int) [][]int {
@@ -284,10 +285,10 @@ func BindCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateBindHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func Commands() []*C.Command {

View File

@@ -16,7 +16,7 @@
package cli
import (
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"strings"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
// Deprecated:
@@ -261,10 +262,10 @@ func ContextReaderIOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateContextReaderIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateMakeProvider(f *os.File, i int) {
@@ -221,10 +222,10 @@ func DICommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateDIHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func eitherHKT(typeE string) func(typeA string) string {
@@ -190,10 +191,10 @@ func EitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func identityHKT(typeA string) string {
@@ -93,10 +94,10 @@ func IdentityCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIdentityHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func nonGenericIO(param string) string {
@@ -102,10 +103,10 @@ func IOCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
// [GA ~func() ET.Either[E, A], GB ~func() ET.Either[E, B], GTAB ~func() ET.Either[E, T.Tuple2[A, B]], E, A, B any](a GA, b GB) GTAB {
@@ -273,10 +274,10 @@ func IOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func nonGenericIOOption(param string) string {
@@ -107,10 +108,10 @@ func IOOptionCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateIOOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -17,6 +17,7 @@ package cli
import (
"bytes"
"context"
"go/ast"
"go/parser"
"go/token"
@@ -28,7 +29,7 @@ import (
"text/template"
S "github.com/IBM/fp-go/v2/string"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (
@@ -86,7 +87,9 @@ type templateData struct {
}
const lensStructTemplate = `
// {{.Name}}Lenses provides lenses for accessing fields of {{.Name}}
// {{.Name}}Lenses provides [lenses] for accessing fields of [{{.Name}}]
//
// [lenses]: __lens.Lens
type {{.Name}}Lenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
@@ -100,7 +103,10 @@ type {{.Name}}Lenses{{.TypeParams}} struct {
{{- end}}
}
// {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}}
// {{.Name}}RefLenses provides [lenses] for accessing fields of [{{.Name}}] via a reference to [{{.Name}}]
//
//
// [lenses]: __lens.Lens
type {{.Name}}RefLenses{{.TypeParams}} struct {
// mandatory fields
{{- range .Fields}}
@@ -111,23 +117,32 @@ type {{.Name}}RefLenses{{.TypeParams}} struct {
{{- if .IsComparable}}
{{.Name}}O __lens_option.LensO[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
{{- end}}
// prisms
{{- range .Fields}}
{{.Name}}P __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
// {{.Name}}Prisms provides prisms for accessing fields of {{.Name}}
// {{.Name}}Prisms provides [prisms] for accessing fields of [{{.Name}}]
//
// [prisms]: __prism.Prism
type {{.Name}}Prisms{{.TypeParams}} struct {
{{- range .Fields}}
{{.Name}} __prism.Prism[{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
// {{.Name}}RefPrisms provides [prisms] for accessing fields of [{{.Name}}] via a reference to [{{.Name}}]
//
// [prisms]: __prism.Prism
type {{.Name}}RefPrisms{{.TypeParams}} struct {
{{- range .Fields}}
{{.Name}} __prism.Prism[*{{$.Name}}{{$.TypeParamNames}}, {{.TypeName}}]
{{- end}}
}
`
const lensConstructorTemplate = `
// Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields
// Make{{.Name}}Lenses creates a new [{{.Name}}Lenses] with [lenses] for all fields
//
// [lenses]:__lens.Lens
func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
@@ -157,7 +172,9 @@ func Make{{.Name}}Lenses{{.TypeParams}}() {{.Name}}Lenses{{.TypeParamNames}} {
}
}
// Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields
// Make{{.Name}}RefLenses creates a new [{{.Name}}RefLenses] with [lenses] for all fields
//
// [lenses]:__lens.Lens
func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames}} {
// mandatory lenses
{{- range .Fields}}
@@ -195,7 +212,9 @@ func Make{{.Name}}RefLenses{{.TypeParams}}() {{.Name}}RefLenses{{.TypeParamNames
}
}
// Make{{.Name}}Prisms creates a new {{.Name}}Prisms with prisms for all fields
// Make{{.Name}}Prisms creates a new [{{.Name}}Prisms] with [prisms] for all fields
//
// [prisms]:__prism.Prism
func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
{{- range .Fields}}
{{- if .IsComparable}}
@@ -235,6 +254,49 @@ func Make{{.Name}}Prisms{{.TypeParams}}() {{.Name}}Prisms{{.TypeParamNames}} {
{{- end}}
}
}
// Make{{.Name}}RefPrisms creates a new [{{.Name}}RefPrisms] with [prisms] for all fields
//
// [prisms]:__prism.Prism
func Make{{.Name}}RefPrisms{{.TypeParams}}() {{.Name}}RefPrisms{{.TypeParamNames}} {
{{- range .Fields}}
{{- if .IsComparable}}
_fromNonZero{{.Name}} := __option.FromNonZero[{{.TypeName}}]()
_prism{{.Name}} := __prism.MakePrismWithName(
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return _fromNonZero{{.Name}}(s.{{.Name}}) },
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
{{- if .IsEmbedded}}
var result {{$.Name}}{{$.TypeParamNames}}
result.{{.Name}} = v
return &result
{{- else}}
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
{{- end}}
},
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
)
{{- else}}
_prism{{.Name}} := __prism.MakePrismWithName(
func(s *{{$.Name}}{{$.TypeParamNames}}) __option.Option[{{.TypeName}}] { return __option.Some(s.{{.Name}}) },
func(v {{.TypeName}}) *{{$.Name}}{{$.TypeParamNames}} {
{{- if .IsEmbedded}}
var result {{$.Name}}{{$.TypeParamNames}}
result.{{.Name}} = v
return &result
{{- else}}
return &{{$.Name}}{{$.TypeParamNames}}{ {{.Name}}: v }
{{- end}}
},
"{{$.Name}}{{$.TypeParamNames}}.{{.Name}}",
)
{{- end}}
{{- end}}
return {{.Name}}RefPrisms{{.TypeParamNames}} {
{{- range .Fields}}
{{.Name}}: _prism{{.Name}},
{{- end}}
}
}
`
var (
@@ -535,9 +597,9 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
fieldTypeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
fieldTypeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := fieldTypeName
@@ -697,9 +759,9 @@ func parseFile(filename string) ([]structInfo, string, error) {
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
typeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
typeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := typeName
isComparable := false
@@ -934,12 +996,12 @@ func LensCommand() *C.Command {
flagVerbose,
flagIncludeTestFiles,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateLensHelpers(
ctx.String(keyLensDir),
ctx.String(keyFilename),
ctx.Bool(keyVerbose),
ctx.Bool(keyIncludeTestFile),
cmd.String(keyLensDir),
cmd.String(keyFilename),
cmd.Bool(keyVerbose),
cmd.Bool(keyIncludeTestFile),
)
},
}

View File

@@ -1086,3 +1086,255 @@ type ComparableBox[T comparable] struct {
// Verify that MakeLensRef is NOT used (since both fields are comparable)
assert.NotContains(t, contentStr, "__lens.MakeLensRefWithName(", "Should not use MakeLensRefWithName when all fields are comparable")
}
func TestParseFileWithUnexportedFields(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 {
PublicName string
privateName string
PublicValue int
privateValue *int
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
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, 4, "Should include both exported and unexported fields")
// Check exported field
assert.Equal(t, "PublicName", config.Fields[0].Name)
assert.Equal(t, "string", config.Fields[0].TypeName)
assert.False(t, config.Fields[0].IsOptional)
// Check unexported field
assert.Equal(t, "privateName", config.Fields[1].Name)
assert.Equal(t, "string", config.Fields[1].TypeName)
assert.False(t, config.Fields[1].IsOptional)
// Check exported int field
assert.Equal(t, "PublicValue", config.Fields[2].Name)
assert.Equal(t, "int", config.Fields[2].TypeName)
assert.False(t, config.Fields[2].IsOptional)
// Check unexported pointer field
assert.Equal(t, "privateValue", config.Fields[3].Name)
assert.Equal(t, "*int", config.Fields[3].TypeName)
assert.True(t, config.Fields[3].IsOptional)
}
func TestGenerateLensHelpersWithUnexportedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type MixedStruct struct {
PublicField string
privateField int
OptionalPrivate *string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, 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, "MixedStructLenses")
assert.Contains(t, contentStr, "MakeMixedStructLenses")
// Check that lenses are generated for all fields (exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[MixedStruct, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[MixedStruct, int]")
assert.Contains(t, contentStr, "OptionalPrivate __lens.Lens[MixedStruct, *string]")
// Check lens constructors
assert.Contains(t, contentStr, "func(s MixedStruct) string { return s.PublicField }")
assert.Contains(t, contentStr, "func(s MixedStruct) int { return s.privateField }")
assert.Contains(t, contentStr, "func(s MixedStruct) *string { return s.OptionalPrivate }")
// Check setters
assert.Contains(t, contentStr, "func(s MixedStruct, v string) MixedStruct { s.PublicField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v int) MixedStruct { s.privateField = v; return s }")
assert.Contains(t, contentStr, "func(s MixedStruct, v *string) MixedStruct { s.OptionalPrivate = v; return s }")
}
func TestParseFileWithOnlyUnexportedFields(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type PrivateConfig struct {
name string
value int
enabled bool
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
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 PrivateConfig struct
config := structs[0]
assert.Equal(t, "PrivateConfig", config.Name)
assert.Len(t, config.Fields, 3, "Should include all unexported fields")
// Check all fields are unexported
assert.Equal(t, "name", config.Fields[0].Name)
assert.Equal(t, "value", config.Fields[1].Name)
assert.Equal(t, "enabled", config.Fields[2].Name)
}
func TestGenerateLensHelpersWithUnexportedEmbeddedFields(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
type BaseConfig struct {
publicBase string
privateBase int
}
// fp-go:Lens
type ExtendedConfig struct {
BaseConfig
PublicField string
privateField bool
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen_lens.go"
err = generateLensHelpers(tmpDir, outputFile, false, 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, "ExtendedConfigLenses")
// Check that lenses are generated for embedded unexported fields
assert.Contains(t, contentStr, "publicBase __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateBase __lens.Lens[ExtendedConfig, int]")
// Check that lenses are generated for direct fields (both exported and unexported)
assert.Contains(t, contentStr, "PublicField __lens.Lens[ExtendedConfig, string]")
assert.Contains(t, contentStr, "privateField __lens.Lens[ExtendedConfig, bool]")
}
func TestParseFileWithMixedFieldVisibility(t *testing.T) {
// Create a temporary test file with various field visibility patterns
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type ComplexStruct struct {
// Exported fields
Name string
Age int
Email *string
// Unexported fields
password string
secretKey []byte
internalID *int
// Mixed with tags
PublicWithTag string ` + "`json:\"public,omitempty\"`" + `
privateWithTag int ` + "`json:\"private,omitempty\"`" + `
}
`
err := os.WriteFile(testFile, []byte(testCode), 0o644)
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 ComplexStruct
complex := structs[0]
assert.Equal(t, "ComplexStruct", complex.Name)
assert.Len(t, complex.Fields, 8, "Should include all fields regardless of visibility")
// Verify field names and types
fieldNames := []string{"Name", "Age", "Email", "password", "secretKey", "internalID", "PublicWithTag", "privateWithTag"}
for i, expectedName := range fieldNames {
assert.Equal(t, expectedName, complex.Fields[i].Name, "Field %d should be %s", i, expectedName)
}
// Check optional fields
assert.False(t, complex.Fields[0].IsOptional, "Name should not be optional")
assert.True(t, complex.Fields[2].IsOptional, "Email (pointer) should be optional")
assert.True(t, complex.Fields[5].IsOptional, "internalID (pointer) should be optional")
assert.True(t, complex.Fields[6].IsOptional, "PublicWithTag (with omitempty) should be optional")
assert.True(t, complex.Fields[7].IsOptional, "privateWithTag (with omitempty) should be optional")
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func optionHKT(typeA string) string {
@@ -200,10 +201,10 @@ func OptionCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateUnsliced(f *os.File, i int) {
@@ -423,10 +424,10 @@ func PipeCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generatePipeHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateReaderFrom(f, fg *os.File, i int) {
@@ -154,10 +155,10 @@ func ReaderCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateReaderHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,13 +16,14 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func generateReaderIOEitherFrom(f, fg *os.File, i int) {
@@ -284,10 +285,10 @@ func ReaderIOEitherCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateReaderIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -16,6 +16,7 @@
package cli
import (
"context"
"fmt"
"log"
"os"
@@ -23,7 +24,7 @@ import (
"strings"
"time"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func writeTupleType(f *os.File, symbol string, i int) {
@@ -615,10 +616,10 @@ func TupleCommand() *C.Command {
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateTupleHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
cmd.String(keyFilename),
cmd.Int(keyCount),
)
},
}

View File

@@ -177,3 +177,255 @@ func Local[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
}
}
}
// Compose is an alias for Local that emphasizes the composition aspect of consumer transformation.
// It composes a preprocessing function with a consumer, creating a new consumer that applies
// the function before consuming the value.
//
// This function is semantically identical to Local but uses terminology that may be more familiar
// to developers coming from functional programming backgrounds where "compose" is a common operation.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#profunctor
//
// The name "Compose" highlights that we're composing two operations:
// 1. The transformation function f: R2 -> R1
// 2. The consumer c: R1 -> ()
//
// Result: A composed consumer: R2 -> ()
//
// Type Parameters:
// - R1: The input type of the original Consumer (what it expects)
// - R2: The input type of the new Consumer (what you have)
//
// Parameters:
// - f: A function that converts R2 to R1 (preprocessing function)
//
// Returns:
// - An Operator that transforms Consumer[R1] into Consumer[R2]
//
// Example - Basic composition:
//
// // Consumer that logs integers
// logInt := func(x int) {
// fmt.Printf("Value: %d\n", x)
// }
//
// // Compose with a string-to-int parser
// parseToInt := func(s string) int {
// n, _ := strconv.Atoi(s)
// return n
// }
//
// logString := consumer.Compose(parseToInt)(logInt)
// logString("42") // Logs: "Value: 42"
//
// Example - Composing multiple transformations:
//
// type Data struct {
// Value string
// }
//
// type Wrapper struct {
// Data Data
// }
//
// // Consumer that logs strings
// logString := func(s string) {
// fmt.Println(s)
// }
//
// // Compose transformations step by step
// extractData := func(w Wrapper) Data { return w.Data }
// extractValue := func(d Data) string { return d.Value }
//
// logData := consumer.Compose(extractValue)(logString)
// logWrapper := consumer.Compose(extractData)(logData)
//
// logWrapper(Wrapper{Data: Data{Value: "Hello"}}) // Logs: "Hello"
//
// Example - Function composition style:
//
// // Compose is particularly useful when thinking in terms of function composition
// type Request struct {
// Body []byte
// }
//
// // Consumer that processes strings
// processString := func(s string) {
// fmt.Printf("Processing: %s\n", s)
// }
//
// // Compose byte-to-string conversion with processing
// bytesToString := func(b []byte) string {
// return string(b)
// }
// extractBody := func(r Request) []byte {
// return r.Body
// }
//
// // Chain compositions
// processBytes := consumer.Compose(bytesToString)(processString)
// processRequest := consumer.Compose(extractBody)(processBytes)
//
// processRequest(Request{Body: []byte("test")}) // Logs: "Processing: test"
//
// Relationship to Local:
// - Compose and Local are identical in implementation
// - Compose emphasizes the functional composition aspect
// - Local emphasizes the environment/context transformation aspect
// - Use Compose when thinking about function composition
// - Use Local when thinking about adapting to different contexts
//
// Use Cases:
// - Building processing pipelines with clear composition semantics
// - Adapting consumers in a functional programming style
// - Creating reusable consumer transformations
// - Chaining multiple preprocessing steps
func Compose[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
return Local(f)
}
// Contramap is the categorical name for the contravariant functor operation on Consumers.
// It transforms a Consumer by preprocessing its input, making it the dual of the covariant
// functor's map operation.
//
// See: https://github.com/fantasyland/fantasy-land?tab=readme-ov-file#contravariant
//
// In category theory, a contravariant functor reverses the direction of morphisms.
// While a covariant functor maps f: A -> B to map(f): F[A] -> F[B],
// a contravariant functor maps f: A -> B to contramap(f): F[B] -> F[A].
//
// For Consumers:
// - Consumer[A] is contravariant in A
// - Given f: R2 -> R1, contramap(f) transforms Consumer[R1] to Consumer[R2]
// - The direction is reversed: we go from Consumer[R1] to Consumer[R2]
//
// This is semantically identical to Local and Compose, but uses the standard
// categorical terminology that emphasizes the contravariant nature of the transformation.
//
// Type Parameters:
// - R1: The input type of the original Consumer (what it expects)
// - R2: The input type of the new Consumer (what you have)
//
// Parameters:
// - f: A function that converts R2 to R1 (preprocessing function)
//
// Returns:
// - An Operator that transforms Consumer[R1] into Consumer[R2]
//
// Example - Basic contravariant mapping:
//
// // Consumer that logs integers
// logInt := func(x int) {
// fmt.Printf("Value: %d\n", x)
// }
//
// // Contramap with a string-to-int parser
// parseToInt := func(s string) int {
// n, _ := strconv.Atoi(s)
// return n
// }
//
// logString := consumer.Contramap(parseToInt)(logInt)
// logString("42") // Logs: "Value: 42"
//
// Example - Demonstrating contravariance:
//
// // In covariant functors (like Option, Array), map goes "forward":
// // map: (A -> B) -> F[A] -> F[B]
// //
// // In contravariant functors (like Consumer), contramap goes "backward":
// // contramap: (B -> A) -> F[A] -> F[B]
//
// type Animal struct{ Name string }
// type Dog struct{ Animal Animal; Breed string }
//
// // Consumer for animals
// consumeAnimal := func(a Animal) {
// fmt.Printf("Animal: %s\n", a.Name)
// }
//
// // Function from Dog to Animal (B -> A)
// dogToAnimal := func(d Dog) Animal {
// return d.Animal
// }
//
// // Contramap creates Consumer[Dog] from Consumer[Animal]
// // Direction is reversed: Consumer[Animal] -> Consumer[Dog]
// consumeDog := consumer.Contramap(dogToAnimal)(consumeAnimal)
//
// consumeDog(Dog{
// Animal: Animal{Name: "Buddy"},
// Breed: "Golden Retriever",
// }) // Logs: "Animal: Buddy"
//
// Example - Contravariant functor laws:
//
// // Law 1: Identity
// // contramap(identity) = identity
// identity := func(x int) int { return x }
// consumer1 := consumer.Contramap(identity)(consumeInt)
// // consumer1 behaves identically to consumeInt
//
// // Law 2: Composition
// // contramap(f . g) = contramap(g) . contramap(f)
// // Note: composition order is reversed compared to covariant map
// f := func(s string) int { n, _ := strconv.Atoi(s); return n }
// g := func(b bool) string { if b { return "1" } else { return "0" } }
//
// // These two are equivalent:
// consumer2 := consumer.Contramap(func(b bool) int { return f(g(b)) })(consumeInt)
// consumer3 := consumer.Contramap(g)(consumer.Contramap(f)(consumeInt))
//
// Example - Practical use with type hierarchies:
//
// type Logger interface {
// Log(string)
// }
//
// type Message struct {
// Text string
// Timestamp time.Time
// }
//
// // Consumer that logs strings
// logString := func(s string) {
// fmt.Println(s)
// }
//
// // Contramap to handle Message types
// extractText := func(m Message) string {
// return fmt.Sprintf("[%s] %s", m.Timestamp.Format(time.RFC3339), m.Text)
// }
//
// logMessage := consumer.Contramap(extractText)(logString)
// logMessage(Message{
// Text: "Hello",
// Timestamp: time.Now(),
// }) // Logs: "[2024-01-20T10:00:00Z] Hello"
//
// Relationship to Local and Compose:
// - Contramap, Local, and Compose are identical in implementation
// - Contramap emphasizes the categorical/theoretical aspect
// - Local emphasizes the context transformation aspect
// - Compose emphasizes the function composition aspect
// - Use Contramap when working with category theory concepts
// - Use Local when adapting to different contexts
// - Use Compose when building functional pipelines
//
// Category Theory Background:
// - Consumer[A] forms a contravariant functor
// - The contravariant functor laws must hold:
// 1. contramap(id) = id
// 2. contramap(f ∘ g) = contramap(g) ∘ contramap(f)
// - This is dual to the covariant functor (map) operation
// - Consumers are contravariant because they consume rather than produce values
//
// Use Cases:
// - Working with contravariant functors in a categorical style
// - Adapting consumers to work with more specific types
// - Building type-safe consumer transformations
// - Implementing profunctor patterns (Consumer is a profunctor)
func Contramap[R1, R2 any](f func(R2) R1) Operator[R1, R2] {
return Local(f)
}

View File

@@ -381,3 +381,513 @@ func TestLocal(t *testing.T) {
assert.Equal(t, 42, captured)
})
}
func TestContramap(t *testing.T) {
t.Run("basic contravariant mapping", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
parseToInt := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
consumeString := Contramap(parseToInt)(consumeInt)
consumeString("42")
assert.Equal(t, 42, captured)
})
t.Run("contravariant identity law", func(t *testing.T) {
// contramap(identity) = identity
var captured int
consumeInt := func(x int) {
captured = x
}
identity := function.Identity[int]
consumeIdentity := Contramap(identity)(consumeInt)
consumeIdentity(42)
assert.Equal(t, 42, captured)
// Should behave identically to original consumer
consumeInt(100)
capturedDirect := captured
consumeIdentity(100)
capturedMapped := captured
assert.Equal(t, capturedDirect, capturedMapped)
})
t.Run("contravariant composition law", func(t *testing.T) {
// contramap(f . g) = contramap(g) . contramap(f)
var captured int
consumeInt := func(x int) {
captured = x
}
f := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
g := func(b bool) string {
if b {
return "1"
}
return "0"
}
// Compose f and g manually
fg := func(b bool) int {
return f(g(b))
}
// Method 1: contramap(f . g)
consumer1 := Contramap(fg)(consumeInt)
consumer1(true)
result1 := captured
// Method 2: contramap(g) . contramap(f)
consumer2 := Contramap(g)(Contramap(f)(consumeInt))
consumer2(true)
result2 := captured
assert.Equal(t, result1, result2)
assert.Equal(t, 1, result1)
})
t.Run("type hierarchy adaptation", func(t *testing.T) {
type Animal struct {
Name string
}
type Dog struct {
Animal Animal
Breed string
}
var capturedName string
consumeAnimal := func(a Animal) {
capturedName = a.Name
}
dogToAnimal := func(d Dog) Animal {
return d.Animal
}
consumeDog := Contramap(dogToAnimal)(consumeAnimal)
consumeDog(Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
})
assert.Equal(t, "Buddy", capturedName)
})
t.Run("field extraction with contramap", func(t *testing.T) {
type Message struct {
Text string
Timestamp time.Time
}
var capturedText string
consumeString := func(s string) {
capturedText = s
}
extractText := func(m Message) string {
return m.Text
}
consumeMessage := Contramap(extractText)(consumeString)
consumeMessage(Message{
Text: "Hello",
Timestamp: time.Now(),
})
assert.Equal(t, "Hello", capturedText)
})
t.Run("multiple contramap applications", func(t *testing.T) {
type Level3 struct{ Value int }
type Level2 struct{ L3 Level3 }
type Level1 struct{ L2 Level2 }
var captured int
consumeInt := func(x int) {
captured = x
}
extract3 := func(l3 Level3) int { return l3.Value }
extract2 := func(l2 Level2) Level3 { return l2.L3 }
extract1 := func(l1 Level1) Level2 { return l1.L2 }
// Chain contramap operations
consumeLevel3 := Contramap(extract3)(consumeInt)
consumeLevel2 := Contramap(extract2)(consumeLevel3)
consumeLevel1 := Contramap(extract1)(consumeLevel2)
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
assert.Equal(t, 42, captured)
})
t.Run("contramap with calculation", func(t *testing.T) {
type Rectangle struct {
Width int
Height int
}
var capturedArea int
consumeArea := func(area int) {
capturedArea = area
}
calculateArea := func(r Rectangle) int {
return r.Width * r.Height
}
consumeRectangle := Contramap(calculateArea)(consumeArea)
consumeRectangle(Rectangle{Width: 5, Height: 10})
assert.Equal(t, 50, capturedArea)
})
t.Run("contramap preserves side effects", func(t *testing.T) {
callCount := 0
consumer := func(x int) {
callCount++
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
contramappedConsumer := Contramap(transform)(consumer)
contramappedConsumer("1")
contramappedConsumer("2")
contramappedConsumer("3")
assert.Equal(t, 3, callCount)
})
t.Run("contramap with pointer types", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
dereference := func(p *int) int {
if p == nil {
return 0
}
return *p
}
consumePointer := Contramap(dereference)(consumeInt)
value := 42
consumePointer(&value)
assert.Equal(t, 42, captured)
consumePointer(nil)
assert.Equal(t, 0, captured)
})
t.Run("contramap equivalence with Local", func(t *testing.T) {
var capturedLocal, capturedContramap int
consumeIntLocal := func(x int) {
capturedLocal = x
}
consumeIntContramap := func(x int) {
capturedContramap = x
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
// Both should produce identical results
consumerLocal := Local(transform)(consumeIntLocal)
consumerContramap := Contramap(transform)(consumeIntContramap)
consumerLocal("42")
consumerContramap("42")
assert.Equal(t, capturedLocal, capturedContramap)
assert.Equal(t, 42, capturedLocal)
})
}
func TestCompose(t *testing.T) {
t.Run("basic composition", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
parseToInt := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
consumeString := Compose(parseToInt)(consumeInt)
consumeString("42")
assert.Equal(t, 42, captured)
})
t.Run("composing multiple transformations", func(t *testing.T) {
type Data struct {
Value string
}
type Wrapper struct {
Data Data
}
var captured string
consumeString := func(s string) {
captured = s
}
extractData := func(w Wrapper) Data { return w.Data }
extractValue := func(d Data) string { return d.Value }
// Compose step by step
consumeData := Compose(extractValue)(consumeString)
consumeWrapper := Compose(extractData)(consumeData)
consumeWrapper(Wrapper{Data: Data{Value: "Hello"}})
assert.Equal(t, "Hello", captured)
})
t.Run("function composition style", func(t *testing.T) {
type Request struct {
Body []byte
}
var captured string
processString := func(s string) {
captured = s
}
bytesToString := func(b []byte) string {
return string(b)
}
extractBody := func(r Request) []byte {
return r.Body
}
// Chain compositions
processBytes := Compose(bytesToString)(processString)
processRequest := Compose(extractBody)(processBytes)
processRequest(Request{Body: []byte("test")})
assert.Equal(t, "test", captured)
})
t.Run("compose with identity", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
identity := function.Identity[int]
composedConsumer := Compose(identity)(consumeInt)
composedConsumer(42)
assert.Equal(t, 42, captured)
})
t.Run("compose with field extraction", func(t *testing.T) {
type User struct {
Name string
Email string
Age int
}
var capturedName string
consumeName := func(name string) {
capturedName = name
}
extractName := func(u User) string {
return u.Name
}
consumeUser := Compose(extractName)(consumeName)
consumeUser(User{Name: "Alice", Email: "alice@example.com", Age: 30})
assert.Equal(t, "Alice", capturedName)
})
t.Run("compose with calculation", func(t *testing.T) {
type Circle struct {
Radius float64
}
var capturedArea float64
consumeArea := func(area float64) {
capturedArea = area
}
calculateArea := func(c Circle) float64 {
return 3.14159 * c.Radius * c.Radius
}
consumeCircle := Compose(calculateArea)(consumeArea)
consumeCircle(Circle{Radius: 5.0})
assert.InDelta(t, 78.53975, capturedArea, 0.00001)
})
t.Run("compose with slice operations", func(t *testing.T) {
var captured int
consumeLength := func(n int) {
captured = n
}
getLength := func(s []string) int {
return len(s)
}
consumeSlice := Compose(getLength)(consumeLength)
consumeSlice([]string{"a", "b", "c", "d"})
assert.Equal(t, 4, captured)
})
t.Run("compose with map operations", func(t *testing.T) {
var captured bool
consumeHasKey := func(has bool) {
captured = has
}
hasKey := func(m map[string]int) bool {
_, exists := m["key"]
return exists
}
consumeMap := Compose(hasKey)(consumeHasKey)
consumeMap(map[string]int{"key": 42})
assert.True(t, captured)
consumeMap(map[string]int{"other": 42})
assert.False(t, captured)
})
t.Run("compose preserves consumer behavior", func(t *testing.T) {
callCount := 0
consumer := func(x int) {
callCount++
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
composedConsumer := Compose(transform)(consumer)
composedConsumer("1")
composedConsumer("2")
composedConsumer("3")
assert.Equal(t, 3, callCount)
})
t.Run("compose with error handling", func(t *testing.T) {
type Result struct {
Value int
Error error
}
var captured int
consumeInt := func(x int) {
captured = x
}
extractValue := func(r Result) int {
if r.Error != nil {
return -1
}
return r.Value
}
consumeResult := Compose(extractValue)(consumeInt)
consumeResult(Result{Value: 42, Error: nil})
assert.Equal(t, 42, captured)
consumeResult(Result{Value: 100, Error: assert.AnError})
assert.Equal(t, -1, captured)
})
t.Run("compose equivalence with Local", func(t *testing.T) {
var capturedLocal, capturedCompose int
consumeIntLocal := func(x int) {
capturedLocal = x
}
consumeIntCompose := func(x int) {
capturedCompose = x
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
// Both should produce identical results
consumerLocal := Local(transform)(consumeIntLocal)
consumerCompose := Compose(transform)(consumeIntCompose)
consumerLocal("42")
consumerCompose("42")
assert.Equal(t, capturedLocal, capturedCompose)
assert.Equal(t, 42, capturedLocal)
})
t.Run("compose equivalence with Contramap", func(t *testing.T) {
var capturedCompose, capturedContramap int
consumeIntCompose := func(x int) {
capturedCompose = x
}
consumeIntContramap := func(x int) {
capturedContramap = x
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
// All three should produce identical results
consumerCompose := Compose(transform)(consumeIntCompose)
consumerContramap := Contramap(transform)(consumeIntContramap)
consumerCompose("42")
consumerContramap("42")
assert.Equal(t, capturedCompose, capturedContramap)
assert.Equal(t, 42, capturedCompose)
})
}

View File

@@ -44,7 +44,7 @@ import (
// return result.Of("done")
// }
//
// ctx, cancel := context.WithCancel(context.Background())
// ctx, cancel := context.WithCancel(t.Context())
// cancel() // Cancel immediately
//
// wrapped := WithContext(ctx, computation)

View File

@@ -61,7 +61,7 @@ import (
//
// // Safely read file with automatic cleanup
// safeRead := Bracket(acquireFile, readFile, closeFile)
// result := safeRead(context.Background())()
// result := safeRead(t.Context())()
//
//go:inline
func Bracket[

View File

@@ -50,7 +50,7 @@ import (
// // Sequence it to apply Config first
// sequenced := SequenceReader[Config, int](getMultiplier)
// cfg := Config{Timeout: 30}
// result := sequenced(cfg)(context.Background())() // Returns 60
// result := sequenced(cfg)(t.Context())() // Returns 60
//
//go:inline
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
@@ -107,7 +107,7 @@ func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]]
//
// // Provide Config to get final result
// cfg := Config{Multiplier: 5}
// finalResult := result(cfg)(context.Background())() // Returns 50
// finalResult := result(cfg)(t.Context())() // Returns 50
//
//go:inline
func TraverseReader[R, A, B any](

View File

@@ -81,7 +81,7 @@ func SLogWithCallback[A any](
// Chain(SLog[string]("Extracted name")),
// )
//
// result := pipeline(context.Background())()
// result := pipeline(t.Context())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Logs: "Extracted name" value="Alice"
//

View File

@@ -45,7 +45,7 @@ func TestPromapBasic(t *testing.T) {
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, "42", result)
})
@@ -69,7 +69,7 @@ func TestContramapBasic(t *testing.T) {
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, 100, result)
})
@@ -90,7 +90,7 @@ func TestLocalBasic(t *testing.T) {
}
adapted := Local[bool](addTimeout)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.True(t, result)
})

View File

@@ -560,6 +560,63 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
return RIO.Read[A](r)
}
// ReadIO executes a ReaderIO computation by providing a context wrapped in an IO effect.
// This is useful when the context itself needs to be computed or retrieved through side effects.
//
// The function takes an IO[context.Context] (an effectful computation that produces a context) and returns
// a function that can execute a ReaderIO[A] to produce an IO[A].
//
// This is particularly useful in scenarios where:
// - The context needs to be created with side effects (e.g., loading configuration)
// - The context requires initialization or setup
// - You want to compose context creation with the computation that uses it
//
// The execution flow is:
// 1. Execute the IO[context.Context] to get the context
// 2. Pass the context to the ReaderIO[A] to get an IO[A]
// 3. Execute the resulting IO[A] to get the final result A
//
// Type Parameters:
// - A: The result type of the ReaderIO computation
//
// Parameters:
// - r: An IO effect that produces a context.Context
//
// Returns:
// - A function that takes a ReaderIO[A] and returns an IO[A]
//
// Example:
//
// import (
// "context"
// G "github.com/IBM/fp-go/v2/io"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Create context with side effects (e.g., loading config)
// createContext := G.Of(context.WithValue(t.Context(), "key", "value"))
//
// // A computation that uses the context
// getValue := readerio.FromReader(func(ctx context.Context) string {
// if val := ctx.Value("key"); val != nil {
// return val.(string)
// }
// return "default"
// })
//
// // Compose them together
// result := readerio.ReadIO[string](createContext)(getValue)
// value := result() // Executes both effects and returns "value"
//
// Comparison with Read:
// - [Read]: Takes a pure context.Context value and executes the ReaderIO immediately
// - [ReadIO]: Takes an IO[context.Context] and chains the effects together
//
//go:inline
func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
return RIO.ReadIO[A](r)
}
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
//
// This is the Reader's local operation, which allows you to modify the environment
@@ -607,7 +664,7 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
// getUser,
// addUser,
// )
// user := result(context.Background())() // Returns "Alice"
// user := result(t.Context())() // Returns "Alice"
//
// Timeout Example:
//
@@ -674,7 +731,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// fetchData,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{} after 5s timeout
// data := result(t.Context())() // Returns Data{} after 5s timeout
//
// Successful Example:
//
@@ -683,7 +740,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// quickFetch,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{Value: "quick"}
// data := result(t.Context())() // Returns Data{Value: "quick"}
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
@@ -734,12 +791,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// fetchData,
// readerio.WithDeadline[Data](deadline),
// )
// data := result(context.Background())() // Returns Data{} if past deadline
// data := result(t.Context())() // Returns Data{} if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)

View File

@@ -31,7 +31,7 @@ func TestMonadMap(t *testing.T) {
rio := Of(5)
doubled := MonadMap(rio, N.Mul(2))
result := doubled(context.Background())()
result := doubled(t.Context())()
assert.Equal(t, 10, result)
}
@@ -41,14 +41,14 @@ func TestMap(t *testing.T) {
Map(utils.Double),
)
assert.Equal(t, 2, g(context.Background())())
assert.Equal(t, 2, g(t.Context())())
}
func TestMonadMapTo(t *testing.T) {
rio := Of(42)
replaced := MonadMapTo(rio, "constant")
result := replaced(context.Background())()
result := replaced(t.Context())()
assert.Equal(t, "constant", result)
}
@@ -58,7 +58,7 @@ func TestMapTo(t *testing.T) {
MapTo[int]("constant"),
)
assert.Equal(t, "constant", result(context.Background())())
assert.Equal(t, "constant", result(t.Context())())
}
func TestMonadChain(t *testing.T) {
@@ -67,7 +67,7 @@ func TestMonadChain(t *testing.T) {
return Of(n * 3)
})
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestChain(t *testing.T) {
@@ -78,7 +78,7 @@ func TestChain(t *testing.T) {
}),
)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestMonadChainFirst(t *testing.T) {
@@ -89,7 +89,7 @@ func TestMonadChainFirst(t *testing.T) {
return Of("side effect")
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -104,7 +104,7 @@ func TestChainFirst(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -117,7 +117,7 @@ func TestMonadTap(t *testing.T) {
return Of(func() {})
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -132,14 +132,14 @@ func TestTap(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestOf(t *testing.T) {
rio := Of(100)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 100, result)
}
@@ -149,7 +149,7 @@ func TestMonadAp(t *testing.T) {
faIO := Of(5)
result := MonadAp(fabIO, faIO)
assert.Equal(t, 10, result(context.Background())())
assert.Equal(t, 10, result(t.Context())())
}
func TestAp(t *testing.T) {
@@ -158,7 +158,7 @@ func TestAp(t *testing.T) {
Ap[int](Of(1)),
)
assert.Equal(t, 2, g(context.Background())())
assert.Equal(t, 2, g(t.Context())())
}
func TestMonadApSeq(t *testing.T) {
@@ -166,7 +166,7 @@ func TestMonadApSeq(t *testing.T) {
faIO := Of(5)
result := MonadApSeq(fabIO, faIO)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestApSeq(t *testing.T) {
@@ -175,7 +175,7 @@ func TestApSeq(t *testing.T) {
ApSeq[int](Of(5)),
)
assert.Equal(t, 15, g(context.Background())())
assert.Equal(t, 15, g(t.Context())())
}
func TestMonadApPar(t *testing.T) {
@@ -183,7 +183,7 @@ func TestMonadApPar(t *testing.T) {
faIO := Of(5)
result := MonadApPar(fabIO, faIO)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestApPar(t *testing.T) {
@@ -192,12 +192,12 @@ func TestApPar(t *testing.T) {
ApPar[int](Of(5)),
)
assert.Equal(t, 15, g(context.Background())())
assert.Equal(t, 15, g(t.Context())())
}
func TestAsk(t *testing.T) {
rio := Ask()
ctx := context.WithValue(context.Background(), "key", "value")
ctx := context.WithValue(t.Context(), "key", "value")
result := rio(ctx)()
assert.Equal(t, ctx, result)
@@ -207,7 +207,7 @@ func TestFromIO(t *testing.T) {
ioAction := G.Of(42)
rio := FromIO(ioAction)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 42, result)
}
@@ -217,7 +217,7 @@ func TestFromReader(t *testing.T) {
}
rio := FromReader(rdr)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 42, result)
}
@@ -226,7 +226,7 @@ func TestFromLazy(t *testing.T) {
lazy := func() int { return 42 }
rio := FromLazy(lazy)
result := rio(context.Background())()
result := rio(t.Context())()
assert.Equal(t, 42, result)
}
@@ -236,7 +236,7 @@ func TestMonadChainIOK(t *testing.T) {
return G.Of(n * 4)
})
assert.Equal(t, 20, result(context.Background())())
assert.Equal(t, 20, result(t.Context())())
}
func TestChainIOK(t *testing.T) {
@@ -247,7 +247,7 @@ func TestChainIOK(t *testing.T) {
}),
)
assert.Equal(t, 20, result(context.Background())())
assert.Equal(t, 20, result(t.Context())())
}
func TestMonadChainFirstIOK(t *testing.T) {
@@ -258,7 +258,7 @@ func TestMonadChainFirstIOK(t *testing.T) {
return G.Of("side effect")
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -273,7 +273,7 @@ func TestChainFirstIOK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -286,7 +286,7 @@ func TestMonadTapIOK(t *testing.T) {
return G.Of(func() {})
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -301,7 +301,7 @@ func TestTapIOK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -313,8 +313,8 @@ func TestDefer(t *testing.T) {
return Of(counter)
})
result1 := rio(context.Background())()
result2 := rio(context.Background())()
result1 := rio(t.Context())()
result2 := rio(t.Context())()
assert.Equal(t, 1, result1)
assert.Equal(t, 2, result2)
@@ -328,8 +328,8 @@ func TestMemoize(t *testing.T) {
return counter
}))
result1 := memoized(context.Background())()
result2 := memoized(context.Background())()
result1 := memoized(t.Context())()
result2 := memoized(t.Context())()
assert.Equal(t, 1, result1)
assert.Equal(t, 1, result2) // Same value, memoized
@@ -339,7 +339,7 @@ func TestFlatten(t *testing.T) {
nested := Of(Of(42))
flattened := Flatten(nested)
result := flattened(context.Background())()
result := flattened(t.Context())()
assert.Equal(t, 42, result)
}
@@ -347,7 +347,7 @@ func TestMonadFlap(t *testing.T) {
fabIO := Of(N.Mul(3))
result := MonadFlap(fabIO, 7)
assert.Equal(t, 21, result(context.Background())())
assert.Equal(t, 21, result(t.Context())())
}
func TestFlap(t *testing.T) {
@@ -356,7 +356,7 @@ func TestFlap(t *testing.T) {
Flap[int](7),
)
assert.Equal(t, 21, result(context.Background())())
assert.Equal(t, 21, result(t.Context())())
}
func TestMonadChainReaderK(t *testing.T) {
@@ -365,7 +365,7 @@ func TestMonadChainReaderK(t *testing.T) {
return func(ctx context.Context) int { return n * 2 }
})
assert.Equal(t, 10, result(context.Background())())
assert.Equal(t, 10, result(t.Context())())
}
func TestChainReaderK(t *testing.T) {
@@ -376,7 +376,7 @@ func TestChainReaderK(t *testing.T) {
}),
)
assert.Equal(t, 10, result(context.Background())())
assert.Equal(t, 10, result(t.Context())())
}
func TestMonadChainFirstReaderK(t *testing.T) {
@@ -389,7 +389,7 @@ func TestMonadChainFirstReaderK(t *testing.T) {
}
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -406,7 +406,7 @@ func TestChainFirstReaderK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -421,7 +421,7 @@ func TestMonadTapReaderK(t *testing.T) {
}
})
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
@@ -438,14 +438,14 @@ func TestTapReaderK(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 42, value)
assert.Equal(t, 42, sideEffect)
}
func TestRead(t *testing.T) {
rio := Of(42)
ctx := context.Background()
ctx := t.Context()
ioAction := Read[int](ctx)(rio)
result := ioAction()
@@ -463,7 +463,7 @@ func TestComplexPipeline(t *testing.T) {
Map(N.Add(10)),
)
assert.Equal(t, 20, result(context.Background())()) // (5 * 2) + 10 = 20
assert.Equal(t, 20, result(t.Context())()) // (5 * 2) + 10 = 20
}
func TestFromIOWithChain(t *testing.T) {
@@ -476,7 +476,7 @@ func TestFromIOWithChain(t *testing.T) {
}),
)
assert.Equal(t, 15, result(context.Background())())
assert.Equal(t, 15, result(t.Context())())
}
func TestTapWithLogging(t *testing.T) {
@@ -496,7 +496,192 @@ func TestTapWithLogging(t *testing.T) {
}),
)
value := result(context.Background())()
value := result(t.Context())()
assert.Equal(t, 84, value)
assert.Equal(t, []int{42, 84}, logged)
}
func TestReadIO(t *testing.T) {
// Test basic ReadIO functionality
contextIO := G.Of(context.WithValue(t.Context(), "testKey", "testValue"))
rio := FromReader(func(ctx context.Context) string {
if val := ctx.Value("testKey"); val != nil {
return val.(string)
}
return "default"
})
ioAction := ReadIO[string](contextIO)(rio)
result := ioAction()
assert.Equal(t, "testValue", result)
}
func TestReadIOWithBackground(t *testing.T) {
// Test ReadIO with plain background context
contextIO := G.Of(t.Context())
rio := Of(42)
ioAction := ReadIO[int](contextIO)(rio)
result := ioAction()
assert.Equal(t, 42, result)
}
func TestReadIOWithChain(t *testing.T) {
// Test ReadIO with chained operations
contextIO := G.Of(context.WithValue(t.Context(), "multiplier", 3))
result := F.Pipe1(
FromReader(func(ctx context.Context) int {
if val := ctx.Value("multiplier"); val != nil {
return val.(int)
}
return 1
}),
Chain(func(n int) ReaderIO[int] {
return Of(n * 10)
}),
)
ioAction := ReadIO[int](contextIO)(result)
value := ioAction()
assert.Equal(t, 30, value) // 3 * 10
}
func TestReadIOWithMap(t *testing.T) {
// Test ReadIO with Map operations
contextIO := G.Of(t.Context())
result := F.Pipe2(
Of(5),
Map(N.Mul(2)),
Map(N.Add(10)),
)
ioAction := ReadIO[int](contextIO)(result)
value := ioAction()
assert.Equal(t, 20, value) // (5 * 2) + 10
}
func TestReadIOWithSideEffects(t *testing.T) {
// Test ReadIO with side effects in context creation
counter := 0
contextIO := func() context.Context {
counter++
return context.WithValue(t.Context(), "counter", counter)
}
rio := FromReader(func(ctx context.Context) int {
if val := ctx.Value("counter"); val != nil {
return val.(int)
}
return 0
})
ioAction := ReadIO[int](contextIO)(rio)
result := ioAction()
assert.Equal(t, 1, result)
assert.Equal(t, 1, counter)
}
func TestReadIOMultipleExecutions(t *testing.T) {
// Test that ReadIO creates fresh effects on each execution
counter := 0
contextIO := func() context.Context {
counter++
return t.Context()
}
rio := Of(42)
ioAction := ReadIO[int](contextIO)(rio)
result1 := ioAction()
result2 := ioAction()
assert.Equal(t, 42, result1)
assert.Equal(t, 42, result2)
assert.Equal(t, 2, counter) // Context IO executed twice
}
func TestReadIOComparisonWithRead(t *testing.T) {
// Compare ReadIO with Read to show the difference
ctx := context.WithValue(t.Context(), "key", "value")
rio := FromReader(func(ctx context.Context) string {
if val := ctx.Value("key"); val != nil {
return val.(string)
}
return "default"
})
// Using Read (direct context)
ioAction1 := Read[string](ctx)(rio)
result1 := ioAction1()
// Using ReadIO (context wrapped in IO)
contextIO := G.Of(ctx)
ioAction2 := ReadIO[string](contextIO)(rio)
result2 := ioAction2()
assert.Equal(t, result1, result2)
assert.Equal(t, "value", result1)
assert.Equal(t, "value", result2)
}
func TestReadIOWithComplexContext(t *testing.T) {
// Test ReadIO with complex context manipulation
type contextKey string
const (
userKey contextKey = "user"
tokenKey contextKey = "token"
)
contextIO := G.Of(
context.WithValue(
context.WithValue(t.Context(), userKey, "Alice"),
tokenKey,
"secret123",
),
)
rio := FromReader(func(ctx context.Context) map[string]string {
result := make(map[string]string)
if user := ctx.Value(userKey); user != nil {
result["user"] = user.(string)
}
if token := ctx.Value(tokenKey); token != nil {
result["token"] = token.(string)
}
return result
})
ioAction := ReadIO[map[string]string](contextIO)(rio)
result := ioAction()
assert.Equal(t, "Alice", result["user"])
assert.Equal(t, "secret123", result["token"])
}
func TestReadIOWithAsk(t *testing.T) {
// Test ReadIO combined with Ask
contextIO := G.Of(context.WithValue(t.Context(), "data", 100))
result := F.Pipe1(
Ask(),
Map(func(ctx context.Context) int {
if val := ctx.Value("data"); val != nil {
return val.(int)
}
return 0
}),
)
ioAction := ReadIO[int](contextIO)(result)
value := ioAction()
assert.Equal(t, 100, value)
}

View File

@@ -53,7 +53,7 @@ import (
// }
//
// countdown := TailRec(countdownStep)
// result := countdown(10)(context.Background())() // Returns "Done!"
// result := countdown(10)(t.Context())() // Returns "Done!"
//
// Example - Sum with context:
//
@@ -77,7 +77,7 @@ import (
// }
//
// sum := TailRec(sumStep)
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(context.Background())()
// result := sum(SumState{numbers: []int{1, 2, 3, 4, 5}})(t.Context())()
// // Returns 15, safe even for very large slices
//
//go:inline

View File

@@ -80,7 +80,7 @@ import (
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute
// ctx := context.Background()
// ctx := t.Context()
// result := retryingFetch(ctx)() // Returns "success" after 3 attempts
//
//go:inline

View File

@@ -56,7 +56,7 @@ This creates several problems:
```go
computation := getComputation()
ctx := context.Background()
ctx := t.Context()
cfg := Config{Value: 42}
// Must apply in this specific order
@@ -176,7 +176,7 @@ db := Database{ConnectionString: "localhost:5432"}
query := queryWithDB(db) // ✅ Database injected
// Use query with any context
result := query(context.Background())()
result := query(t.Context())()
```
### 3. Point-Free Composition
@@ -289,7 +289,7 @@ withConfig := traversed(getValue)
// Now we can provide Config to get the final result
cfg := Config{Multiplier: 5, Prefix: "Result"}
ctx := context.Background()
ctx := t.Context()
result := withConfig(cfg)(ctx)() // Returns Right("Result: 50")
```

View File

@@ -74,7 +74,7 @@ func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
// safeFetch := WithContextK(fetchUser)
//
// // If context is cancelled, returns immediately without executing fetchUser
// ctx, cancel := context.WithCancel(context.Background())
// ctx, cancel := context.WithCancel(t.Context())
// cancel() // Cancel immediately
// result := safeFetch(123)(ctx)() // Returns context.Canceled error
//

View File

@@ -4,6 +4,7 @@ import (
"time"
"github.com/IBM/fp-go/v2/circuitbreaker"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/retry"
)
@@ -27,6 +28,9 @@ func MakeCircuitBreaker[T any](
Left,
ChainFirstIOK,
ChainFirstLeftIOK,
readerio.ChainFirstIOK,
FromIO,
Flap,
Flatten,

View File

@@ -187,7 +187,7 @@ func main() {
result := cb(env)
// Execute the protected operation
ctx := context.Background()
ctx := t.Context()
protectedOp := pair.Tail(result)
outcome := protectedOp(ctx)()
}

View File

@@ -113,7 +113,7 @@
// }
//
// // Execute the computation
// ctx := context.Background()
// ctx := t.Context()
// result := fetchUser("123")(ctx)()
// // result is Either[error, User]
//
@@ -161,7 +161,7 @@
// All operations respect context cancellation. When a context is cancelled, operations
// will return an error containing the cancellation cause:
//
// ctx, cancel := context.WithCancelCause(context.Background())
// ctx, cancel := context.WithCancelCause(t.Context())
// cancel(errors.New("operation cancelled"))
// result := computation(ctx)() // Returns Left with cancellation error
//

View File

@@ -37,7 +37,7 @@ import (
// return either.Eq(eq.FromEquals(func(x, y int) bool { return x == y }))(a, b)
// })
// eqRIE := Eq(eqInt)
// ctx := context.Background()
// ctx := t.Context()
// equal := eqRIE(ctx).Equals(Right[int](42), Right[int](42)) // true
//
//go:inline

View File

@@ -16,7 +16,6 @@
package file
import (
"context"
"os"
"testing"
@@ -30,7 +29,7 @@ func TestWithTempFile(t *testing.T) {
res := WithTempFile(onWriteAll[*os.File]([]byte("Carsten")))
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
}
func TestWithTempFileOnClosedFile(t *testing.T) {
@@ -43,5 +42,5 @@ func TestWithTempFileOnClosedFile(t *testing.T) {
)
})
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(context.Background())())
assert.Equal(t, E.Of[error]([]byte("Carsten")), res(t.Context())())
}

View File

@@ -43,7 +43,7 @@ import (
// onNegative := func(n int) error { return fmt.Errorf("%d is not positive", n) }
//
// filter := readerioresult.FilterOrElse(isPositive, onNegative)
// result := filter(readerioresult.Right(42))(context.Background())()
// result := filter(readerioresult.Right(42))(t.Context())()
//
//go:inline
func FilterOrElse[A any](pred Predicate[A], onFalse func(A) error) Operator[A, A] {

View File

@@ -71,7 +71,7 @@ import (
//
// // Now we can partially apply the Config
// cfg := Config{Timeout: 30}
// ctx := context.Background()
// ctx := t.Context()
// result := sequenced(ctx)(cfg)() // Returns Right(60)
//
// This is especially useful in point-free style when building computation pipelines:
@@ -133,7 +133,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
//
// // Partially apply the Database
// db := Database{ConnectionString: "localhost:5432"}
// ctx := context.Background()
// ctx := t.Context()
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
//
// In point-free style, this enables clean composition:
@@ -195,7 +195,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R
//
// // Partially apply the Config
// cfg := Config{MaxRetries: 3}
// ctx := context.Background()
// ctx := t.Context()
// result := sequenced(ctx)(cfg)() // Returns Right(3)
//
// // With invalid config
@@ -276,7 +276,7 @@ func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kl
//
// // Now we can provide the Config to get the final result
// cfg := Config{Multiplier: 5}
// ctx := context.Background()
// ctx := t.Context()
// finalResult := result(cfg)(ctx)() // Returns Right(50)
//
// In point-free style, this enables clean composition:

View File

@@ -22,6 +22,7 @@ import (
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
)
// Example_sequenceReader_basicUsage demonstrates the basic usage of SequenceReader
@@ -233,7 +234,7 @@ func Example_sequenceReaderResult_errorHandling() {
ctx := context.Background()
pipeline := F.Pipe2(
sequenced(ctx),
RIOE.Map(func(x int) int { return x * 2 }),
RIOE.Map(N.Mul(2)),
RIOE.Chain(func(x int) RIOE.ReaderIOResult[string] {
return RIOE.Of(fmt.Sprintf("Result: %d", x))
}),

View File

@@ -41,7 +41,7 @@ func TestSequenceReader(t *testing.T) {
// The Reader environment (string) is now the first parameter
sequenced := SequenceReader(original)
ctx := context.Background()
ctx := t.Context()
// Test original
result1 := original(ctx)()
@@ -75,7 +75,7 @@ func TestSequenceReader(t *testing.T) {
}
db := Database{ConnectionString: "localhost:5432"}
ctx := context.Background()
ctx := t.Context()
expected := "Query on localhost:5432"
@@ -106,7 +106,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with error
result1 := original(ctx)()
@@ -132,7 +132,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Sequence
sequenced := SequenceReader(original)
@@ -158,7 +158,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReader(original)
// Test with zero values
@@ -184,7 +184,7 @@ func TestSequenceReader(t *testing.T) {
}
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
sequenced := SequenceReader(original)
@@ -217,14 +217,14 @@ func TestSequenceReader(t *testing.T) {
withConfig := sequenced(cfg)
// Now we have a ReaderIOResult[int] that can be used in different contexts
ctx1 := context.Background()
ctx1 := t.Context()
result1 := withConfig(ctx1)()
assert.True(t, either.IsRight(result1))
value1, _ := either.Unwrap(result1)
assert.Equal(t, 50, value1)
// Can reuse with different context
ctx2 := context.Background()
ctx2 := t.Context()
result2 := withConfig(ctx2)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
@@ -246,7 +246,7 @@ func TestSequenceReaderIO(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReaderIO(original)
// Test original
@@ -273,7 +273,7 @@ func TestSequenceReaderIO(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with error
result1 := original(ctx)()
@@ -303,7 +303,7 @@ func TestSequenceReaderIO(t *testing.T) {
}
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
sequenced := SequenceReaderIO(original)
@@ -327,7 +327,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReaderResult(original)
// Test original
@@ -356,7 +356,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with error
result1 := original(ctx)()
@@ -384,7 +384,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test original with inner error
result1 := original(ctx)()
@@ -421,7 +421,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
// Test outer error
sequenced1 := SequenceReaderResult(makeOriginal(-20))
@@ -460,7 +460,7 @@ func TestSequenceReaderResult(t *testing.T) {
}
}
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
sequenced := SequenceReaderResult(original)
@@ -484,7 +484,7 @@ func TestSequenceEdgeCases(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
empty := Empty{}
sequenced := SequenceReader(original)
@@ -514,7 +514,7 @@ func TestSequenceEdgeCases(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
data := &Data{Value: 100}
sequenced := SequenceReader(original)
@@ -544,7 +544,7 @@ func TestSequenceEdgeCases(t *testing.T) {
}
}
ctx := context.Background()
ctx := t.Context()
sequenced := SequenceReader(original)
// Call multiple times with same inputs
@@ -583,7 +583,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -614,7 +614,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsLeft(finalResult))
@@ -643,7 +643,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Database and execute
db := Database{Prefix: "ID"}
ctx := context.Background()
ctx := t.Context()
finalResult := result(db)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -673,7 +673,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Settings and execute
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := context.Background()
ctx := t.Context()
finalResult := result(settings)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -705,14 +705,14 @@ func TestTraverseReader(t *testing.T) {
withConfig := result(cfg)
// Can now use with different contexts
ctx1 := context.Background()
ctx1 := t.Context()
finalResult1 := withConfig(ctx1)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)
assert.Equal(t, 30, value1)
// Reuse with different context
ctx2 := context.Background()
ctx2 := t.Context()
finalResult2 := withConfig(ctx2)()
assert.True(t, either.IsRight(finalResult2))
value2, _ := either.Unwrap(finalResult2)
@@ -746,7 +746,7 @@ func TestTraverseReader(t *testing.T) {
result := traversed(original)
// Use canceled context
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
cfg := Config{Value: 5}
@@ -778,7 +778,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config with zero offset
cfg := Config{Offset: 0}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -807,7 +807,7 @@ func TestTraverseReader(t *testing.T) {
// Provide Config and execute
cfg := Config{Multiplier: 4}
ctx := context.Background()
ctx := t.Context()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
@@ -843,7 +843,7 @@ func TestTraverseReader(t *testing.T) {
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
ctx := context.Background()
ctx := t.Context()
finalResult1 := result(rules1)(ctx)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)

View File

@@ -42,7 +42,7 @@
// )
//
// requester := RB.Requester(builder)
// result := requester(context.Background())()
// result := requester(t.Context())()
package builder
import (
@@ -103,7 +103,7 @@ import (
// B.WithJSONBody(map[string]string{"name": "John"}),
// )
// requester := RB.Requester(builder)
// result := requester(context.Background())()
// result := requester(t.Context())()
//
// Example without body:
//
@@ -113,7 +113,7 @@ import (
// B.WithMethod("GET"),
// )
// requester := RB.Requester(builder)
// result := requester(context.Background())()
// result := requester(t.Context())()
func Requester(builder *R.Builder) RIOEH.Requester {
withBody := F.Curry3(func(data []byte, url string, method string) RIOE.ReaderIOResult[*http.Request] {

View File

@@ -46,7 +46,7 @@ func TestBuilderWithQuery(t *testing.T) {
RIOE.Map(func(r *http.Request) *url.URL {
return r.URL
}),
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[any] {
RIOE.ChainFirstIOK(func(u *url.URL) IO.IO[Void] {
return IO.FromImpure(func() {
q := u.Query()
assert.Equal(t, "10", q.Get("limit"))
@@ -55,7 +55,7 @@ func TestBuilderWithQuery(t *testing.T) {
}),
)
assert.True(t, E.IsRight(req(context.Background())()))
assert.True(t, E.IsRight(req(t.Context())()))
}
// TestBuilderWithoutBody tests creating a request without a body
@@ -67,7 +67,7 @@ func TestBuilderWithoutBody(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -90,7 +90,7 @@ func TestBuilderWithBody(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -112,7 +112,7 @@ func TestBuilderWithHeaders(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -130,7 +130,7 @@ func TestBuilderWithInvalidURL(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsLeft(result), "Expected Left result for invalid URL")
}
@@ -144,7 +144,7 @@ func TestBuilderWithEmptyMethod(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
// Empty method should still work (defaults to GET in http.NewRequest)
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -161,7 +161,7 @@ func TestBuilderWithMultipleHeaders(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -185,7 +185,7 @@ func TestBuilderWithBodyAndHeaders(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -207,7 +207,7 @@ func TestBuilderContextCancellation(t *testing.T) {
requester := Requester(builder)
// Create a cancelled context
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel() // Cancel immediately
result := requester(ctx)()
@@ -233,7 +233,7 @@ func TestBuilderWithDifferentMethods(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result for method %s", method)
@@ -256,7 +256,7 @@ func TestBuilderWithJSON(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")
@@ -277,7 +277,7 @@ func TestBuilderWithBearer(t *testing.T) {
)
requester := Requester(builder)
result := requester(context.Background())()
result := requester(t.Context())()
assert.True(t, E.IsRight(result), "Expected Right result")

View File

@@ -0,0 +1,7 @@
package builder
import "github.com/IBM/fp-go/v2/function"
type (
Void = function.Void
)

View File

@@ -28,7 +28,7 @@
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// result := ReadJSON[MyType](client)(request)
// response := result(context.Background())()
// response := result(t.Context())()
package http
import (
@@ -157,8 +157,8 @@ func MakeClient(httpClient *http.Client) Client {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// fullResp := ReadFullResponse(client)(request)
// result := fullResp(context.Background())()
func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
// result := fullResp(t.Context())()
func ReadFullResponse(client Client) RIOE.Operator[*http.Request, H.FullResponse] {
return func(req Requester) RIOE.ReaderIOResult[H.FullResponse] {
return F.Flow3(
client.Do(req),
@@ -194,8 +194,8 @@ func ReadFullResponse(client Client) RIOE.Kleisli[Requester, H.FullResponse] {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/data")
// readBytes := ReadAll(client)
// result := readBytes(request)(context.Background())()
func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
// result := readBytes(request)(t.Context())()
func ReadAll(client Client) RIOE.Operator[*http.Request, []byte] {
return F.Flow2(
ReadFullResponse(client),
RIOE.Map(H.Body),
@@ -218,8 +218,8 @@ func ReadAll(client Client) RIOE.Kleisli[Requester, []byte] {
// client := MakeClient(http.DefaultClient)
// request := MakeGetRequest("https://api.example.com/text")
// readText := ReadText(client)
// result := readText(request)(context.Background())()
func ReadText(client Client) RIOE.Kleisli[Requester, string] {
// result := readText(request)(t.Context())()
func ReadText(client Client) RIOE.Operator[*http.Request, string] {
return F.Flow2(
ReadAll(client),
RIOE.Map(B.ToString),
@@ -231,7 +231,7 @@ func ReadText(client Client) RIOE.Kleisli[Requester, string] {
// 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) RIOE.Kleisli[Requester, A] {
func ReadJson[A any](client Client) RIOE.Operator[*http.Request, A] {
return ReadJSON[A](client)
}
@@ -242,7 +242,7 @@ func ReadJson[A any](client Client) RIOE.Kleisli[Requester, A] {
// 3. Reads the response body as bytes
//
// This function is used internally by ReadJSON to ensure proper JSON response handling.
func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
func readJSON(client Client) RIOE.Operator[*http.Request, []byte] {
return F.Flow3(
ReadFullResponse(client),
RIOE.ChainFirstEitherK(F.Flow2(
@@ -277,8 +277,8 @@ func readJSON(client Client) RIOE.Kleisli[Requester, []byte] {
// 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) RIOE.Kleisli[Requester, A] {
// result := readUser(request)(t.Context())()
func ReadJSON[A any](client Client) RIOE.Operator[*http.Request, A] {
return F.Flow2(
readJSON(client),
RIOE.ChainEitherK(J.Unmarshal[A]),

View File

@@ -65,7 +65,7 @@ var (
// This function assumes the context contains logging information; it will panic if not present.
getLoggingContext = F.Flow3(
loggingContextValue,
option.ToType[loggingContext],
option.InstanceOf[loggingContext],
option.GetOrElse(getDefaultLoggingContext),
)
)
@@ -429,7 +429,7 @@ func LogEntryExitWithCallback[A any](
// loggedFetch := LogEntryExit[User]("fetchUser")(fetchUser(123))
//
// // Execute
// result := loggedFetch(context.Background())()
// result := loggedFetch(t.Context())()
// // Logs:
// // [entering 1] fetchUser
// // [exiting 1] fetchUser [0.1s]
@@ -441,7 +441,7 @@ func LogEntryExitWithCallback[A any](
// }
//
// logged := LogEntryExit[string]("failingOp")(failingOp())
// result := logged(context.Background())()
// result := logged(t.Context())()
// // Logs:
// // [entering 2] failingOp
// // [throwing 2] failingOp [0.0s]: connection timeout
@@ -461,7 +461,7 @@ func LogEntryExitWithCallback[A any](
// LogEntryExit[[]Order]("fetchOrders"),
// )
//
// result := pipeline(context.Background())()
// result := pipeline(t.Context())()
// // Logs:
// // [entering 3] fetchUser
// // [exiting 3] fetchUser [0.1s]
@@ -474,8 +474,8 @@ func LogEntryExitWithCallback[A any](
// op1 := LogEntryExit[Data]("operation1")(fetchData(1))
// op2 := LogEntryExit[Data]("operation2")(fetchData(2))
//
// go op1(context.Background())()
// go op2(context.Background())()
// go op1(t.Context())()
// go op2(t.Context())()
// // Logs (order may vary):
// // [entering 5] operation1
// // [entering 6] operation2
@@ -615,7 +615,7 @@ func SLogWithCallback[A any](
// Map(func(u User) string { return u.Name }),
// )
//
// result := pipeline(context.Background())()
// result := pipeline(t.Context())()
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
// // If error, logs: "Fetched user" error="user not found"
//
@@ -679,7 +679,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
// Map(func(u User) string { return u.Name }),
// )
//
// result := pipeline(context.Background())()
// result := pipeline(t.Context())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Returns: result.Of("Alice")
//
@@ -694,7 +694,7 @@ func SLog[A any](message string) Kleisli[Result[A], A] {
// TapSLog[Payment]("Payment processed"),
// )
//
// result := processOrder(context.Background())()
// result := processOrder(t.Context())()
// // Logs each successful step with the intermediate values
// // If any step fails, subsequent TapSLog calls don't log
//

View File

@@ -26,7 +26,7 @@ func TestLoggingContext(t *testing.T) {
LogEntryExit[string]("TestLoggingContext2"),
)
assert.Equal(t, result.Of("Sample"), data(context.Background())())
assert.Equal(t, result.Of("Sample"), data(t.Context())())
}
// TestLogEntryExitSuccess tests successful operation logging
@@ -43,7 +43,7 @@ func TestLogEntryExitSuccess(t *testing.T) {
LogEntryExit[string]("TestOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of("success value"), res)
@@ -70,7 +70,7 @@ func TestLogEntryExitError(t *testing.T) {
LogEntryExit[string]("FailingOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsLeft(res))
@@ -105,7 +105,7 @@ func TestLogEntryExitNested(t *testing.T) {
}),
)
res := outerOp(context.Background())()
res := outerOp(t.Context())()
assert.True(t, result.IsRight(res))
@@ -137,7 +137,7 @@ func TestLogEntryExitWithCallback(t *testing.T) {
LogEntryExitWithCallback[int](slog.LevelDebug, customCallback, "DebugOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of(42), res)
@@ -163,7 +163,7 @@ func TestLogEntryExitDisabled(t *testing.T) {
LogEntryExit[string]("DisabledOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsRight(res))
@@ -197,7 +197,7 @@ func TestLogEntryExitF(t *testing.T) {
LogEntryExitF(onEntry, onExit),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsRight(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
@@ -234,7 +234,7 @@ func TestLogEntryExitFWithError(t *testing.T) {
LogEntryExitF(onEntry, onExit),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsLeft(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
@@ -257,7 +257,7 @@ func TestLoggingIDUniqueness(t *testing.T) {
Of(i),
LogEntryExit[int]("Operation"),
)
op(context.Background())()
op(t.Context())()
}
logOutput := buf.String()
@@ -287,7 +287,7 @@ func TestLogEntryExitWithContextLogger(t *testing.T) {
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
ctx := logging.WithLogger(contextLogger)(t.Context())
operation := F.Pipe1(
Of("context value"),
@@ -326,7 +326,7 @@ func TestLogEntryExitTiming(t *testing.T) {
LogEntryExit[string]("SlowOperation"),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.True(t, result.IsRight(res))
@@ -379,7 +379,7 @@ func TestLogEntryExitChainedOperations(t *testing.T) {
)),
)
res := pipeline(context.Background())()
res := pipeline(t.Context())()
assert.Equal(t, result.Of("2"), res)
@@ -408,7 +408,7 @@ func TestTapSLog(t *testing.T) {
Map(N.Mul(2)),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of(84), res)
@@ -443,7 +443,7 @@ func TestTapSLogInPipeline(t *testing.T) {
TapSLog[int]("Step 3: Final length"),
)
res := pipeline(context.Background())()
res := pipeline(t.Context())()
assert.Equal(t, result.Of(11), res)
@@ -472,7 +472,7 @@ func TestTapSLogWithError(t *testing.T) {
Map(N.Mul(2)),
)
res := pipeline(context.Background())()
res := pipeline(t.Context())()
assert.True(t, result.IsLeft(res))
@@ -504,7 +504,7 @@ func TestTapSLogWithStruct(t *testing.T) {
Map(func(u User) string { return u.Name }),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of("Alice"), res)
@@ -530,7 +530,7 @@ func TestTapSLogDisabled(t *testing.T) {
Map(N.Mul(2)),
)
res := operation(context.Background())()
res := operation(t.Context())()
assert.Equal(t, result.Of(84), res)
@@ -546,7 +546,7 @@ func TestTapSLogWithContextLogger(t *testing.T) {
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
ctx := logging.WithLogger(contextLogger)(t.Context())
operation := F.Pipe2(
Of("test value"),
@@ -572,7 +572,7 @@ func TestSLogLogsSuccessValue(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
// Create a Result and log it
res1 := result.Of(42)
@@ -594,7 +594,7 @@ func TestSLogLogsErrorValue(t *testing.T) {
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
ctx := t.Context()
testErr := errors.New("test error")
// Create an error Result and log it
@@ -620,7 +620,7 @@ func TestSLogWithCallbackCustomLevel(t *testing.T) {
return logger
}
ctx := context.Background()
ctx := t.Context()
// Create a Result and log it with custom callback
res1 := result.Of(42)
@@ -645,7 +645,7 @@ func TestSLogWithCallbackLogsError(t *testing.T) {
return logger
}
ctx := context.Background()
ctx := t.Context()
testErr := errors.New("warning error")
// Create an error Result and log it with custom callback

View File

@@ -43,7 +43,7 @@ func TestPromapBasic(t *testing.T) {
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, R.Of("42"), result)
})
@@ -67,7 +67,7 @@ func TestContramapBasic(t *testing.T) {
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, R.Of(100), result)
})
@@ -91,7 +91,7 @@ func TestLocalBasic(t *testing.T) {
}
adapted := Local[string](addUser)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, R.Of("Alice"), result)
})

View File

@@ -222,7 +222,7 @@ func withCancelCauseFunc[A any](cancel context.CancelCauseFunc, ma IOResult[A])
return function.Pipe3(
ma,
ioresult.Swap[A],
ioeither.ChainFirstIOK[A](func(err error) func() any {
ioeither.ChainFirstIOK[A](func(err error) func() Void {
return io.FromImpure(func() { cancel(err) })
}),
ioeither.Swap[A],
@@ -452,7 +452,7 @@ func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
// Returns a function that chains Option-returning functions into ReaderIOResult.
//
//go:inline
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainOptionK[context.Context, A, B](onNone)
}
@@ -800,7 +800,7 @@ func FromReaderResult[A any](ma ReaderResult[A]) ReaderIOResult[A] {
}
//go:inline
func FromReaderOption[A any](onNone func() error) Kleisli[ReaderOption[context.Context, A], A] {
func FromReaderOption[A any](onNone Lazy[error]) Kleisli[ReaderOption[context.Context, A], A] {
return RIOR.FromReaderOption[context.Context, A](onNone)
}
@@ -895,17 +895,17 @@ func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
}
//go:inline
func ChainReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
func ChainReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, B] {
return RIOR.ChainReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func ChainFirstReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
func ChainFirstReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderOptionK[context.Context, A, B](onNone)
}
//go:inline
func TapReaderOptionK[A, B any](onNone func() error) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
func TapReaderOptionK[A, B any](onNone Lazy[error]) func(readeroption.Kleisli[context.Context, A, B]) Operator[A, A] {
return RIOR.TapReaderOptionK[context.Context, A, B](onNone)
}
@@ -914,6 +914,21 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.Read[A](r)
}
//go:inline
func ReadIO[A any](r IO[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.ReadIO[A](r)
}
//go:inline
func ReadIOEither[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.ReadIOEither[A](r)
}
//go:inline
func ReadIOResult[A any](r IOResult[context.Context]) func(ReaderIOResult[A]) IOResult[A] {
return RIOR.ReadIOResult[A](r)
}
// MonadChainLeft chains a computation on the left (error) side of a [ReaderIOResult].
// If the input is a Left value, it applies the function f to transform the error and potentially
// change the error type. If the input is a Right value, it passes through unchanged.
@@ -1026,7 +1041,7 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
// getUser,
// addUser,
// )
// value, err := result(context.Background())() // Returns ("Alice", nil)
// value, err := result(t.Context())() // Returns ("Alice", nil)
//
// Timeout Example:
//
@@ -1097,7 +1112,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// fetchData,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) after 5s
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) after 5s
//
// Successful Example:
//
@@ -1106,7 +1121,7 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// quickFetch,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // Returns (Data{Value: "quick"}, nil)
// value, err := result(t.Context())() // Returns (Data{Value: "quick"}, nil)
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
@@ -1158,12 +1173,12 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// fetchData,
// readerioresult.WithDeadline[Data](deadline),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
// value, err := result(t.Context())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// parentCtx, cancel := context.WithDeadline(t.Context(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)

View File

@@ -36,56 +36,56 @@ func TestFromEither(t *testing.T) {
t.Run("Right value", func(t *testing.T) {
either := E.Right[error]("success")
result := FromEither(either)
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
})
t.Run("Left value", func(t *testing.T) {
err := errors.New("test error")
either := E.Left[string](err)
result := FromEither(either)
assert.Equal(t, E.Left[string](err), result(context.Background())())
assert.Equal(t, E.Left[string](err), result(t.Context())())
})
}
func TestFromResult(t *testing.T) {
t.Run("Success", func(t *testing.T) {
result := FromResult(E.Right[error](42))
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("Error", func(t *testing.T) {
err := errors.New("test error")
result := FromResult(E.Left[int](err))
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
func TestLeft(t *testing.T) {
err := errors.New("test error")
result := Left[string](err)
assert.Equal(t, E.Left[string](err), result(context.Background())())
assert.Equal(t, E.Left[string](err), result(t.Context())())
}
func TestRight(t *testing.T) {
result := Right("success")
assert.Equal(t, E.Right[error]("success"), result(context.Background())())
assert.Equal(t, E.Right[error]("success"), result(t.Context())())
}
func TestOf(t *testing.T) {
result := Of(42)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestMonadMap(t *testing.T) {
t.Run("Map over Right", func(t *testing.T) {
result := MonadMap(Of(5), N.Mul(2))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("Map over Left", func(t *testing.T) {
err := errors.New("test error")
result := MonadMap(Left[int](err), N.Mul(2))
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -93,34 +93,34 @@ func TestMap(t *testing.T) {
t.Run("Map with success", func(t *testing.T) {
mapper := Map(N.Mul(2))
result := mapper(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("Map with error", func(t *testing.T) {
err := errors.New("test error")
mapper := Map(N.Mul(2))
result := mapper(Left[int](err))
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
func TestMonadMapTo(t *testing.T) {
t.Run("MapTo with success", func(t *testing.T) {
result := MonadMapTo(Of("original"), 42)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("MapTo with error", func(t *testing.T) {
err := errors.New("test error")
result := MonadMapTo(Left[string](err), 42)
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
func TestMapTo(t *testing.T) {
mapper := MapTo[string](42)
result := mapper(Of("original"))
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestMonadChain(t *testing.T) {
@@ -128,7 +128,7 @@ func TestMonadChain(t *testing.T) {
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
return Of(x * 2)
})
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("Chain with error in first", func(t *testing.T) {
@@ -136,7 +136,7 @@ func TestMonadChain(t *testing.T) {
result := MonadChain(Left[int](err), func(x int) ReaderIOResult[int] {
return Of(x * 2)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
t.Run("Chain with error in second", func(t *testing.T) {
@@ -144,7 +144,7 @@ func TestMonadChain(t *testing.T) {
result := MonadChain(Of(5), func(x int) ReaderIOResult[int] {
return Left[int](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -153,7 +153,7 @@ func TestChain(t *testing.T) {
return Of(x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestMonadChainFirst(t *testing.T) {
@@ -161,7 +161,7 @@ func TestMonadChainFirst(t *testing.T) {
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
return Of("ignored")
})
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
})
t.Run("ChainFirst propagates error from second", func(t *testing.T) {
@@ -169,7 +169,7 @@ func TestMonadChainFirst(t *testing.T) {
result := MonadChainFirst(Of(5), func(x int) ReaderIOResult[string] {
return Left[string](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -178,7 +178,7 @@ func TestChainFirst(t *testing.T) {
return Of("ignored")
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
}
func TestMonadApSeq(t *testing.T) {
@@ -186,7 +186,7 @@ func TestMonadApSeq(t *testing.T) {
fab := Of(N.Mul(2))
fa := Of(5)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ApSeq with error in function", func(t *testing.T) {
@@ -194,7 +194,7 @@ func TestMonadApSeq(t *testing.T) {
fab := Left[func(int) int](err)
fa := Of(5)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
t.Run("ApSeq with error in value", func(t *testing.T) {
@@ -202,7 +202,7 @@ func TestMonadApSeq(t *testing.T) {
fab := Of(N.Mul(2))
fa := Left[int](err)
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -210,7 +210,7 @@ func TestApSeq(t *testing.T) {
fa := Of(5)
fab := Of(N.Mul(2))
result := MonadApSeq(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestApPar(t *testing.T) {
@@ -218,11 +218,11 @@ func TestApPar(t *testing.T) {
fa := Of(5)
fab := Of(N.Mul(2))
result := MonadApPar(fab, fa)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ApPar with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
fa := Of(5)
fab := Of(N.Mul(2))
@@ -239,7 +239,7 @@ func TestFromPredicate(t *testing.T) {
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
)
result := pred(5)
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
})
t.Run("Predicate false", func(t *testing.T) {
@@ -248,7 +248,7 @@ func TestFromPredicate(t *testing.T) {
func(x int) error { return fmt.Errorf("value %d is not positive", x) },
)
result := pred(-5)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsLeft(res))
})
}
@@ -259,7 +259,7 @@ func TestOrElse(t *testing.T) {
return Of(42)
})
result := fallback(Of(10))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("OrElse with error", func(t *testing.T) {
@@ -268,13 +268,13 @@ func TestOrElse(t *testing.T) {
return Of(42)
})
result := fallback(Left[int](err))
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
}
func TestAsk(t *testing.T) {
result := Ask()
ctx := context.Background()
ctx := t.Context()
res := result(ctx)()
assert.True(t, E.IsRight(res))
ctxResult := E.ToOption(res)
@@ -286,7 +286,7 @@ func TestMonadChainEitherK(t *testing.T) {
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
return E.Right[error](x * 2)
})
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ChainEitherK with error", func(t *testing.T) {
@@ -294,7 +294,7 @@ func TestMonadChainEitherK(t *testing.T) {
result := MonadChainEitherK(Of(5), func(x int) Either[int] {
return E.Left[int](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -303,7 +303,7 @@ func TestChainEitherK(t *testing.T) {
return E.Right[error](x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestMonadChainFirstEitherK(t *testing.T) {
@@ -311,7 +311,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
return E.Right[error]("ignored")
})
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
})
t.Run("ChainFirstEitherK propagates error", func(t *testing.T) {
@@ -319,7 +319,7 @@ func TestMonadChainFirstEitherK(t *testing.T) {
result := MonadChainFirstEitherK(Of(5), func(x int) Either[string] {
return E.Left[string](err)
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -328,7 +328,7 @@ func TestChainFirstEitherK(t *testing.T) {
return E.Right[error]("ignored")
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
}
func TestChainOptionK(t *testing.T) {
@@ -339,7 +339,7 @@ func TestChainOptionK(t *testing.T) {
return O.Some(x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ChainOptionK with None", func(t *testing.T) {
@@ -349,7 +349,7 @@ func TestChainOptionK(t *testing.T) {
return O.None[int]()
})
result := chainer(Of(5))
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsLeft(res))
})
}
@@ -358,44 +358,44 @@ func TestFromIOEither(t *testing.T) {
t.Run("FromIOEither with success", func(t *testing.T) {
ioe := IOE.Of[error](42)
result := FromIOEither(ioe)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("FromIOEither with error", func(t *testing.T) {
err := errors.New("test error")
ioe := IOE.Left[int](err)
result := FromIOEither(ioe)
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
func TestFromIOResult(t *testing.T) {
ioe := IOE.Of[error](42)
result := FromIOResult(ioe)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestFromIO(t *testing.T) {
io := IOG.Of(42)
result := FromIO(io)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestFromReader(t *testing.T) {
reader := R.Of[context.Context](42)
result := FromReader(reader)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestFromLazy(t *testing.T) {
lazy := func() int { return 42 }
result := FromLazy(lazy)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestNever(t *testing.T) {
t.Run("Never with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
result := Never[int]()
// Cancel immediately
@@ -406,7 +406,7 @@ func TestNever(t *testing.T) {
})
t.Run("Never with timeout", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
result := Never[int]()
@@ -419,7 +419,7 @@ func TestMonadChainIOK(t *testing.T) {
result := MonadChainIOK(Of(5), func(x int) IOG.IO[int] {
return IOG.Of(x * 2)
})
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestChainIOK(t *testing.T) {
@@ -427,14 +427,14 @@ func TestChainIOK(t *testing.T) {
return IOG.Of(x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestMonadChainFirstIOK(t *testing.T) {
result := MonadChainFirstIOK(Of(5), func(x int) IOG.IO[string] {
return IOG.Of("ignored")
})
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
}
func TestChainFirstIOK(t *testing.T) {
@@ -442,7 +442,7 @@ func TestChainFirstIOK(t *testing.T) {
return IOG.Of("ignored")
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](5), result(context.Background())())
assert.Equal(t, E.Right[error](5), result(t.Context())())
}
func TestChainIOEitherK(t *testing.T) {
@@ -451,7 +451,7 @@ func TestChainIOEitherK(t *testing.T) {
return IOE.Of[error](x * 2)
})
result := chainer(Of(5))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
})
t.Run("ChainIOEitherK with error", func(t *testing.T) {
@@ -460,7 +460,7 @@ func TestChainIOEitherK(t *testing.T) {
return IOE.Left[int](err)
})
result := chainer(Of(5))
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -469,7 +469,7 @@ func TestDelay(t *testing.T) {
start := time.Now()
delayed := Delay[int](100 * time.Millisecond)
result := delayed(Of(42))
res := result(context.Background())()
res := result(t.Context())()
elapsed := time.Since(start)
assert.True(t, E.IsRight(res))
@@ -477,7 +477,7 @@ func TestDelay(t *testing.T) {
})
t.Run("Delay with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
delayed := Delay[int](100 * time.Millisecond)
result := delayed(Of(42))
@@ -500,11 +500,11 @@ func TestDefer(t *testing.T) {
})
// First execution
res1 := deferred(context.Background())()
res1 := deferred(t.Context())()
assert.True(t, E.IsRight(res1))
// Second execution should generate a new computation
res2 := deferred(context.Background())()
res2 := deferred(t.Context())()
assert.True(t, E.IsRight(res2))
// Counter should be incremented for each execution
@@ -518,7 +518,7 @@ func TestTryCatch(t *testing.T) {
return 42, nil
}
})
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("TryCatch with error", func(t *testing.T) {
@@ -528,7 +528,7 @@ func TestTryCatch(t *testing.T) {
return 0, err
}
})
assert.Equal(t, E.Left[int](err), result(context.Background())())
assert.Equal(t, E.Left[int](err), result(t.Context())())
})
}
@@ -537,7 +537,7 @@ func TestMonadAlt(t *testing.T) {
first := Of(42)
second := func() ReaderIOResult[int] { return Of(100) }
result := MonadAlt(first, second)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
})
t.Run("Alt with first error", func(t *testing.T) {
@@ -545,7 +545,7 @@ func TestMonadAlt(t *testing.T) {
first := Left[int](err)
second := func() ReaderIOResult[int] { return Of(100) }
result := MonadAlt(first, second)
assert.Equal(t, E.Right[error](100), result(context.Background())())
assert.Equal(t, E.Right[error](100), result(t.Context())())
})
}
@@ -553,7 +553,7 @@ func TestAlt(t *testing.T) {
err := errors.New("test error")
alternative := Alt(func() ReaderIOResult[int] { return Of(100) })
result := alternative(Left[int](err))
assert.Equal(t, E.Right[error](100), result(context.Background())())
assert.Equal(t, E.Right[error](100), result(t.Context())())
}
func TestMemoize(t *testing.T) {
@@ -564,13 +564,13 @@ func TestMemoize(t *testing.T) {
}))
// First execution
res1 := computation(context.Background())()
res1 := computation(t.Context())()
assert.True(t, E.IsRight(res1))
val1 := E.ToOption(res1)
assert.Equal(t, O.Of(1), val1)
// Second execution should return cached value
res2 := computation(context.Background())()
res2 := computation(t.Context())()
assert.True(t, E.IsRight(res2))
val2 := E.ToOption(res2)
assert.Equal(t, O.Of(1), val2)
@@ -582,19 +582,19 @@ func TestMemoize(t *testing.T) {
func TestFlatten(t *testing.T) {
nested := Of(Of(42))
result := Flatten(nested)
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestMonadFlap(t *testing.T) {
fab := Of(N.Mul(2))
result := MonadFlap(fab, 5)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestFlap(t *testing.T) {
flapper := Flap[int](5)
result := flapper(Of(N.Mul(2)))
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestFold(t *testing.T) {
@@ -608,7 +608,7 @@ func TestFold(t *testing.T) {
},
)
result := folder(Of(42))
assert.Equal(t, E.Right[error]("success: 42"), result(context.Background())())
assert.Equal(t, E.Right[error]("success: 42"), result(t.Context())())
})
t.Run("Fold with error", func(t *testing.T) {
@@ -622,7 +622,7 @@ func TestFold(t *testing.T) {
},
)
result := folder(Left[int](err))
assert.Equal(t, E.Right[error]("error: test error"), result(context.Background())())
assert.Equal(t, E.Right[error]("error: test error"), result(t.Context())())
})
}
@@ -634,7 +634,7 @@ func TestGetOrElse(t *testing.T) {
}
})
result := getter(Of(42))
assert.Equal(t, 42, result(context.Background())())
assert.Equal(t, 42, result(t.Context())())
})
t.Run("GetOrElse with error", func(t *testing.T) {
@@ -645,19 +645,19 @@ func TestGetOrElse(t *testing.T) {
}
})
result := getter(Left[int](err))
assert.Equal(t, 0, result(context.Background())())
assert.Equal(t, 0, result(t.Context())())
})
}
func TestWithContext(t *testing.T) {
t.Run("WithContext with valid context", func(t *testing.T) {
computation := WithContext(Of(42))
result := computation(context.Background())()
result := computation(t.Context())()
assert.Equal(t, E.Right[error](42), result)
})
t.Run("WithContext with cancelled context", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
computation := WithContext(Of(42))
@@ -672,7 +672,7 @@ func TestEitherize0(t *testing.T) {
}
eitherized := Eitherize0(f)
result := eitherized()
assert.Equal(t, E.Right[error](42), result(context.Background())())
assert.Equal(t, E.Right[error](42), result(t.Context())())
}
func TestUneitherize0(t *testing.T) {
@@ -680,7 +680,7 @@ func TestUneitherize0(t *testing.T) {
return Of(42)
}
uneitherized := Uneitherize0(f)
result, err := uneitherized(context.Background())
result, err := uneitherized(t.Context())
assert.NoError(t, err)
assert.Equal(t, 42, result)
}
@@ -691,7 +691,7 @@ func TestEitherize1(t *testing.T) {
}
eitherized := Eitherize1(f)
result := eitherized(5)
assert.Equal(t, E.Right[error](10), result(context.Background())())
assert.Equal(t, E.Right[error](10), result(t.Context())())
}
func TestUneitherize1(t *testing.T) {
@@ -699,14 +699,14 @@ func TestUneitherize1(t *testing.T) {
return Of(x * 2)
}
uneitherized := Uneitherize1(f)
result, err := uneitherized(context.Background(), 5)
result, err := uneitherized(t.Context(), 5)
assert.NoError(t, err)
assert.Equal(t, 10, result)
}
func TestSequenceT2(t *testing.T) {
result := SequenceT2(Of(1), Of(2))
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
tuple := E.ToOption(res)
assert.True(t, O.IsSome(tuple))
@@ -717,13 +717,13 @@ func TestSequenceT2(t *testing.T) {
func TestSequenceSeqT2(t *testing.T) {
result := SequenceSeqT2(Of(1), Of(2))
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
}
func TestSequenceParT2(t *testing.T) {
result := SequenceParT2(Of(1), Of(2))
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
}
@@ -734,7 +734,7 @@ func TestTraverseArray(t *testing.T) {
return Of(x * 2)
})
result := traverser(arr)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res)
assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
@@ -750,7 +750,7 @@ func TestTraverseArray(t *testing.T) {
return Of(x * 2)
})
result := traverser(arr)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsLeft(res))
})
}
@@ -758,7 +758,7 @@ func TestTraverseArray(t *testing.T) {
func TestSequenceArray(t *testing.T) {
arr := []ReaderIOResult[int]{Of(1), Of(2), Of(3)}
result := SequenceArray(arr)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res)
assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
@@ -769,7 +769,7 @@ func TestTraverseRecord(t *testing.T) {
result := TraverseRecord[string](func(x int) ReaderIOResult[int] {
return Of(x * 2)
})(rec)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
recOpt := E.ToOption(res)
assert.True(t, O.IsSome(recOpt))
@@ -784,7 +784,7 @@ func TestSequenceRecord(t *testing.T) {
"b": Of(2),
}
result := SequenceRecord(rec)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, E.IsRight(res))
recOpt := E.ToOption(res)
assert.True(t, O.IsSome(recOpt))
@@ -798,7 +798,7 @@ func TestAltSemigroup(t *testing.T) {
err := errors.New("test error")
result := sg.Concat(Left[int](err), Of(42))
res := result(context.Background())()
res := result(t.Context())()
assert.Equal(t, E.Right[error](42), res)
}
@@ -810,7 +810,7 @@ func TestApplicativeMonoid(t *testing.T) {
))
result := intAddMonoid.Concat(Of(5), Of(10))
res := result(context.Background())()
res := result(t.Context())()
assert.Equal(t, E.Right[error](15), res)
}
@@ -835,7 +835,7 @@ func TestBracket(t *testing.T) {
}
result := Bracket(acquire, use, release)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, acquired)
assert.True(t, released)
@@ -863,7 +863,7 @@ func TestBracket(t *testing.T) {
}
result := Bracket(acquire, use, release)
res := result(context.Background())()
res := result(t.Context())()
assert.True(t, acquired)
assert.True(t, released)

View File

@@ -29,7 +29,7 @@ import (
func TestInnerContextCancelSemantics(t *testing.T) {
// start with a simple context
outer := context.Background()
outer := t.Context()
parent, parentCancel := context.WithCancel(outer)
defer parentCancel()
@@ -49,7 +49,7 @@ func TestInnerContextCancelSemantics(t *testing.T) {
func TestOuterContextCancelSemantics(t *testing.T) {
// start with a simple context
outer := context.Background()
outer := t.Context()
parent, outerCancel := context.WithCancel(outer)
defer outerCancel()
@@ -69,7 +69,7 @@ func TestOuterContextCancelSemantics(t *testing.T) {
func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
// start with a simple context
outer := context.Background()
outer := t.Context()
parent, outerCancel := context.WithCancel(outer)
defer outerCancel()
@@ -95,7 +95,7 @@ func TestOuterAndInnerContextCancelSemantics(t *testing.T) {
func TestCancelCauseSemantics(t *testing.T) {
// start with a simple context
outer := context.Background()
outer := t.Context()
parent, outerCancel := context.WithCancelCause(outer)
defer outerCancel(nil)
@@ -119,7 +119,7 @@ func TestCancelCauseSemantics(t *testing.T) {
func TestTimer(t *testing.T) {
delta := 3 * time.Second
timer := Timer(delta)
ctx := context.Background()
ctx := t.Context()
t0 := time.Now()
res := timer(ctx)()
@@ -146,7 +146,7 @@ func TestCanceledApply(t *testing.T) {
Ap[string](errValue),
)
res := applied(context.Background())()
res := applied(t.Context())()
assert.Equal(t, E.Left[string](err), res)
}
@@ -159,7 +159,7 @@ func TestRegularApply(t *testing.T) {
Ap[string](value),
)
res := applied(context.Background())()
res := applied(t.Context())()
assert.Equal(t, E.Of[error]("CARSTEN"), res)
}
@@ -187,7 +187,7 @@ func TestWithResourceNoErrors(t *testing.T) {
resRIOE := WithResource[int](acquire, release)(body)
res := resRIOE(context.Background())()
res := resRIOE(t.Context())()
assert.Equal(t, 1, countAcquire)
assert.Equal(t, 1, countBody)
@@ -217,7 +217,7 @@ func TestWithResourceErrorInBody(t *testing.T) {
resRIOE := WithResource[int](acquire, release)(body)
res := resRIOE(context.Background())()
res := resRIOE(t.Context())()
assert.Equal(t, 1, countAcquire)
assert.Equal(t, 0, countBody)
@@ -247,7 +247,7 @@ func TestWithResourceErrorInAcquire(t *testing.T) {
resRIOE := WithResource[int](acquire, release)(body)
res := resRIOE(context.Background())()
res := resRIOE(t.Context())()
assert.Equal(t, 0, countAcquire)
assert.Equal(t, 0, countBody)
@@ -277,7 +277,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
resRIOE := WithResource[int](acquire, release)(body)
res := resRIOE(context.Background())()
res := resRIOE(t.Context())()
assert.Equal(t, 1, countAcquire)
assert.Equal(t, 1, countBody)
@@ -286,7 +286,7 @@ func TestWithResourceErrorInRelease(t *testing.T) {
}
func TestMonadChainFirstLeft(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves original error", func(t *testing.T) {
@@ -353,7 +353,7 @@ func TestMonadChainFirstLeft(t *testing.T) {
}
func TestChainFirstLeft(t *testing.T) {
ctx := context.Background()
ctx := t.Context()
// Test with Left value - function returns Left, always preserves original error
t.Run("Left value with function returning Left preserves error", func(t *testing.T) {

View File

@@ -108,7 +108,7 @@ import (
// countdown := readerioresult.TailRec(countdownStep)
//
// // With cancellation
// ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
// ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
// defer cancel()
// result := countdown(10)(ctx)() // Will be cancelled after ~500ms
//
@@ -141,7 +141,7 @@ import (
// }
//
// processFiles := readerioresult.TailRec(processStep)
// ctx, cancel := context.WithCancel(context.Background())
// ctx, cancel := context.WithCancel(t.Context())
//
// // Can be cancelled at any point during processing
// go func() {
@@ -159,7 +159,7 @@ import (
//
// // Safe for very large inputs with cancellation support
// largeCountdown := readerioresult.TailRec(countdownStep)
// ctx := context.Background()
// ctx := t.Context()
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
//
// # Performance Considerations

View File

@@ -44,7 +44,7 @@ func TestTailRec_BasicRecursion(t *testing.T) {
}
countdown := TailRec(countdownStep)
result := countdown(5)(context.Background())()
result := countdown(5)(t.Context())()
assert.Equal(t, E.Of[error]("Done!"), result)
}
@@ -71,7 +71,7 @@ func TestTailRec_FactorialRecursion(t *testing.T) {
}
factorial := TailRec(factorialStep)
result := factorial(FactorialState{n: 5, acc: 1})(context.Background())()
result := factorial(FactorialState{n: 5, acc: 1})(t.Context())()
assert.Equal(t, E.Of[error](120), result) // 5! = 120
}
@@ -95,7 +95,7 @@ func TestTailRec_ErrorHandling(t *testing.T) {
}
errorRecursion := TailRec(errorStep)
result := errorRecursion(5)(context.Background())()
result := errorRecursion(5)(t.Context())()
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
@@ -125,7 +125,7 @@ func TestTailRec_ContextCancellation(t *testing.T) {
slowRecursion := TailRec(slowStep)
// Create a context that will be cancelled after 100ms
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
start := time.Now()
@@ -159,7 +159,7 @@ func TestTailRec_ImmediateCancellation(t *testing.T) {
countdown := TailRec(countdownStep)
// Create an already cancelled context
ctx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(t.Context())
cancel()
result := countdown(5)(ctx)()
@@ -186,7 +186,7 @@ func TestTailRec_StackSafety(t *testing.T) {
}
countdown := TailRec(countdownStep)
result := countdown(largeN)(context.Background())()
result := countdown(largeN)(t.Context())()
assert.Equal(t, E.Of[error](0), result)
}
@@ -217,7 +217,7 @@ func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
countdown := TailRec(countdownStep)
// Cancel after 50ms to allow some iterations but not all
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
defer cancel()
result := countdown(largeN)(ctx)()
@@ -274,7 +274,7 @@ func TestTailRec_ComplexState(t *testing.T) {
errors: []error{},
}
result := processItems(initialState)(context.Background())()
result := processItems(initialState)(t.Context())()
assert.Equal(t, E.Of[error]([]string{"item1", "item2", "item3"}), result)
})
@@ -286,7 +286,7 @@ func TestTailRec_ComplexState(t *testing.T) {
errors: []error{},
}
result := processItems(initialState)(context.Background())()
result := processItems(initialState)(t.Context())()
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
@@ -336,7 +336,7 @@ func TestTailRec_CancellationDuringProcessing(t *testing.T) {
}
// Cancel after 100ms (should allow ~5 files to be processed)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
start := time.Now()
@@ -366,7 +366,7 @@ func TestTailRec_ZeroIterations(t *testing.T) {
}
immediate := TailRec(immediateStep)
result := immediate(100)(context.Background())()
result := immediate(100)(t.Context())()
assert.Equal(t, E.Of[error]("immediate"), result)
}
@@ -392,7 +392,7 @@ func TestTailRec_ContextWithDeadline(t *testing.T) {
slowRecursion := TailRec(slowStep)
// Set deadline 80ms from now
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(80*time.Millisecond))
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(80*time.Millisecond))
defer cancel()
result := slowRecursion(10)(ctx)()
@@ -427,7 +427,7 @@ func TestTailRec_ContextWithValue(t *testing.T) {
}
valueRecursion := TailRec(valueStep)
ctx := context.WithValue(context.Background(), testKey, "test-value")
ctx := context.WithValue(t.Context(), testKey, "test-value")
result := valueRecursion(3)(ctx)()
assert.Equal(t, E.Of[error]("Done!"), result)

View File

@@ -107,7 +107,7 @@ import (
// retryingFetch := Retrying(policy, fetchData, shouldRetry)
//
// // Execute with a cancellable context
// ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
// defer cancel()
// ioResult := retryingFetch(ctx)
// finalResult := ioResult()

View File

@@ -48,7 +48,7 @@ func WithLock[A any](lock ReaderIOResult[context.CancelFunc]) Operator[A, A] {
function.Constant1[context.CancelFunc, ReaderIOResult[A]],
WithResource[A](lock, function.Flow2(
io.FromImpure[context.CancelFunc],
FromIO[any],
FromIO[Void],
)),
)
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioref"
@@ -113,7 +114,7 @@ type (
// }
//
// The computation is executed by providing a context and then invoking the result:
// ctx := context.Background()
// ctx := t.Context()
// result := fetchUser("123")(ctx)()
ReaderIOResult[A any] = RIOR.ReaderIOResult[context.Context, A]
@@ -152,4 +153,6 @@ type (
IORef[A any] = ioref.IORef[A]
State[S, A any] = state.State[S, A]
Void = function.Void
)

View File

@@ -0,0 +1,453 @@
// 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 readerreaderioresult
import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
// 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 with two reader contexts.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
// type OuterEnv struct {
// Database string
// }
// type InnerEnv struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
// result := readerreaderioeither.Do[OuterEnv, InnerEnv, error](State{})
//
//go:inline
func Do[R, S any](
empty S,
) ReaderReaderIOResult[R, S] {
return Of[R](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access both the outer (R) and inner (C) reader environments.
//
// 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
// }
// type OuterEnv struct {
// Database string
// }
// type InnerEnv struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
//
// result := F.Pipe2(
// readerreaderioeither.Do[OuterEnv, InnerEnv, error](State{}),
// readerreaderioeither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerreaderioeither.ReaderReaderIOResult[OuterEnv, InnerEnv, error, User] {
// return func(outer OuterEnv) readerioeither.ReaderIOEither[InnerEnv, error, User] {
// return readerioeither.Asks(func(inner InnerEnv) ioeither.IOEither[error, User] {
// return inner.UserRepo.FindUser(outer.Database)
// })
// }
// },
// ),
// readerreaderioeither.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readerreaderioeither.ReaderReaderIOResult[OuterEnv, InnerEnv, error, []Post] {
// return func(outer OuterEnv) readerioeither.ReaderIOEither[InnerEnv, error, []Post] {
// return readerioeither.Asks(func(inner InnerEnv) ioeither.IOEither[error, []Post] {
// return inner.PostRepo.FindPostsByUser(outer.Database, s.User.ID)
// })
// }
// },
// ),
// )
//
//go:inline
func Bind[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderReaderIOResult[R, T],
) Operator[R, S1, S2] {
return chain.Bind(
Chain[R, S1, S2],
Map[R, T, S2],
setter,
f,
)
}
// Let attaches a pure computation result to a context [S1] to produce a context [S2].
// Unlike [Bind], the computation function f is pure (doesn't perform effects).
//
// Example:
//
// readerreaderioeither.Let(
// func(fullName string) func(State) State {
// return func(s State) State { s.FullName = fullName; return s }
// },
// func(s State) string {
// return s.FirstName + " " + s.LastName
// },
// )
//
//go:inline
func Let[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[R, S1, S2] {
return functor.Let(
Map[R, S1, S2],
setter,
f,
)
}
// LetTo attaches a constant value to a context [S1] to produce a context [S2].
//
// Example:
//
// readerreaderioeither.LetTo(
// func(status string) func(State) State {
// return func(s State) State { s.Status = status; return s }
// },
// "active",
// )
//
//go:inline
func LetTo[R, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[R, S1, S2] {
return functor.LetTo(
Map[R, S1, S2],
setter,
b,
)
}
// BindTo wraps a value of type T into a context S1 using the provided setter function.
// This is typically used as the first operation after [Do] to initialize the context.
//
// Example:
//
// F.Pipe1(
// readerreaderioeither.Of[OuterEnv, InnerEnv, error](42),
// readerreaderioeither.BindTo(func(n int) State { return State{Count: n} }),
// )
//
//go:inline
func BindTo[R, S1, T any](
setter func(T) S1,
) Operator[R, T, S1] {
return chain.BindTo(
Map[R, T, S1],
setter,
)
}
// ApS applies a computation in parallel (applicative style) and attaches its result to the context.
// Unlike [Bind], this doesn't allow the computation to depend on the current context state.
//
// Example:
//
// readerreaderioeither.ApS(
// func(count int) func(State) State {
// return func(s State) State { s.Count = count; return s }
// },
// getCount, // ReaderReaderIOResult[OuterEnv, InnerEnv, error, int]
// )
//
//go:inline
func ApS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderReaderIOResult[R, T],
) Operator[R, S1, S2] {
return apply.ApS(
Ap[S2, R, T],
Map[R, S1, func(T) S2],
setter,
fa,
)
}
// ApSL is a lens-based version of [ApS] that uses a lens to focus on a specific field in the context.
//
//go:inline
func ApSL[R, S, T any](
lens Lens[S, T],
fa ReaderReaderIOResult[R, T],
) Operator[R, S, S] {
return ApS(lens.Set, fa)
}
// BindL is a lens-based version of [Bind] that uses a lens to focus on a specific field in the context.
//
//go:inline
func BindL[R, S, T any](
lens Lens[S, T],
f func(T) ReaderReaderIOResult[R, T],
) Operator[R, S, S] {
return Bind(lens.Set, F.Flow2(lens.Get, f))
}
// LetL is a lens-based version of [Let] that uses a lens to focus on a specific field in the context.
//
//go:inline
func LetL[R, S, T any](
lens Lens[S, T],
f func(T) T,
) Operator[R, S, S] {
return Let[R](lens.Set, F.Flow2(lens.Get, f))
}
// LetToL is a lens-based version of [LetTo] that uses a lens to focus on a specific field in the context.
//
//go:inline
func LetToL[R, S, T any](
lens Lens[S, T],
b T,
) Operator[R, S, S] {
return LetTo[R](lens.Set, b)
}
// BindIOEitherK binds a computation that returns an IOEither to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindIOEitherK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f ioeither.Kleisli[error, S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromIOEither[R, T]))
}
//go:inline
func BindIOResultK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f ioresult.Kleisli[S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromIOResult[R, T]))
}
// BindIOK binds a computation that returns an IO to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindIOK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f io.Kleisli[S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromIO[R, T]))
}
// BindReaderK binds a computation that returns a Reader to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindReaderK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[R, S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromReader[R, T]))
}
// BindReaderIOK binds a computation that returns a ReaderIO to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindReaderIOK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f readerio.Kleisli[R, S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromReaderIO[R, T]))
}
// BindEitherK binds a computation that returns an Either to the context.
// The Kleisli function is automatically lifted into ReaderReaderIOResult.
//
//go:inline
func BindEitherK[R, S1, S2, T any](
setter func(T) func(S1) S2,
f either.Kleisli[error, S1, T],
) Operator[R, S1, S2] {
return Bind(setter, F.Flow2(f, FromEither[R, T]))
}
// BindIOEitherKL is a lens-based version of [BindIOEitherK].
//
//go:inline
func BindIOEitherKL[R, S, T any](
lens Lens[S, T],
f ioeither.Kleisli[error, T, T],
) Operator[R, S, S] {
return BindL(lens, F.Flow2(f, FromIOEither[R, T]))
}
// BindIOKL is a lens-based version of [BindIOK].
//
//go:inline
func BindIOKL[R, S, T any](
lens Lens[S, T],
f io.Kleisli[T, T],
) Operator[R, S, S] {
return BindL(lens, F.Flow2(f, FromIO[R, T]))
}
// BindReaderKL is a lens-based version of [BindReaderK].
//
//go:inline
func BindReaderKL[R, S, T any](
lens Lens[S, T],
f reader.Kleisli[R, T, T],
) Operator[R, S, S] {
return BindL(lens, F.Flow2(f, FromReader[R, T]))
}
// BindReaderIOKL is a lens-based version of [BindReaderIOK].
//
//go:inline
func BindReaderIOKL[R, S, T any](
lens Lens[S, T],
f readerio.Kleisli[R, T, T],
) Operator[R, S, S] {
return BindL(lens, F.Flow2(f, FromReaderIO[R, T]))
}
// ApIOEitherS applies an IOEither computation and attaches its result to the context.
//
//go:inline
func ApIOEitherS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa IOEither[error, T],
) Operator[R, S1, S2] {
return ApS(setter, FromIOEither[R](fa))
}
// ApIOS applies an IO computation and attaches its result to the context.
//
//go:inline
func ApIOS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa IO[T],
) Operator[R, S1, S2] {
return ApS(setter, FromIO[R](fa))
}
// ApReaderS applies a Reader computation and attaches its result to the context.
//
//go:inline
func ApReaderS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[R, T],
) Operator[R, S1, S2] {
return ApS(setter, FromReader(fa))
}
// ApReaderIOS applies a ReaderIO computation and attaches its result to the context.
//
//go:inline
func ApReaderIOS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIO[R, T],
) Operator[R, S1, S2] {
return ApS(setter, FromReaderIO(fa))
}
// ApEitherS applies an Either computation and attaches its result to the context.
//
//go:inline
func ApEitherS[R, S1, S2, T any](
setter func(T) func(S1) S2,
fa Either[error, T],
) Operator[R, S1, S2] {
return ApS(setter, FromEither[R](fa))
}
// ApIOEitherSL is a lens-based version of [ApIOEitherS].
//
//go:inline
func ApIOEitherSL[R, S, T any](
lens Lens[S, T],
fa IOEither[error, T],
) Operator[R, S, S] {
return ApIOEitherS[R](lens.Set, fa)
}
// ApIOSL is a lens-based version of [ApIOS].
//
//go:inline
func ApIOSL[R, S, T any](
lens Lens[S, T],
fa IO[T],
) Operator[R, S, S] {
return ApSL(lens, FromIO[R](fa))
}
// ApReaderSL is a lens-based version of [ApReaderS].
//
//go:inline
func ApReaderSL[R, S, T any](
lens Lens[S, T],
fa Reader[R, T],
) Operator[R, S, S] {
return ApReaderS(lens.Set, fa)
}
// ApReaderIOSL is a lens-based version of [ApReaderIOS].
//
//go:inline
func ApReaderIOSL[R, S, T any](
lens Lens[S, T],
fa ReaderIO[R, T],
) Operator[R, S, S] {
return ApReaderIOS(lens.Set, fa)
}
// ApEitherSL is a lens-based version of [ApEitherS].
//
//go:inline
func ApEitherSL[R, S, T any](
lens Lens[S, T],
fa Either[error, T],
) Operator[R, S, S] {
return ApEitherS[R](lens.Set, fa)
}

View File

@@ -0,0 +1,720 @@
// 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 readerreaderioresult
import (
"errors"
"testing"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type AppConfig struct {
DatabaseURL string
LogLevel string
}
var defaultConfig = AppConfig{
DatabaseURL: "postgres://localhost",
LogLevel: "info",
}
func getLastName(s utils.Initial) ReaderReaderIOResult[AppConfig, string] {
return Of[AppConfig]("Doe")
}
func getGivenName(s utils.WithLastName) ReaderReaderIOResult[AppConfig, string] {
return Of[AppConfig]("John")
}
func TestDo(t *testing.T) {
res := Do[AppConfig](utils.Empty)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsRight(outcome))
assert.Equal(t, result.Of(utils.Empty), outcome)
}
func TestBind(t *testing.T) {
res := F.Pipe3(
Do[AppConfig](utils.Empty),
Bind(utils.SetLastName, getLastName),
Bind(utils.SetGivenName, getGivenName),
Map[AppConfig](utils.GetFullName),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("John Doe"), outcome)
}
func TestBindWithError(t *testing.T) {
testErr := errors.New("bind error")
getLastNameErr := func(s utils.Initial) ReaderReaderIOResult[AppConfig, string] {
return Left[AppConfig, string](testErr)
}
res := F.Pipe3(
Do[AppConfig](utils.Empty),
Bind(utils.SetLastName, getLastNameErr),
Bind(utils.SetGivenName, getGivenName),
Map[AppConfig](utils.GetFullName),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
}
func TestLet(t *testing.T) {
type State struct {
FirstName string
LastName string
FullName string
}
res := F.Pipe2(
Do[AppConfig](State{FirstName: "John", LastName: "Doe"}),
Let[AppConfig](
func(fullName string) func(State) State {
return func(s State) State {
s.FullName = fullName
return s
}
},
func(s State) string {
return s.FirstName + " " + s.LastName
},
),
Map[AppConfig](func(s State) string { return s.FullName }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("John Doe"), outcome)
}
func TestLetTo(t *testing.T) {
type State struct {
Status string
}
res := F.Pipe2(
Do[AppConfig](State{}),
LetTo[AppConfig](
func(status string) func(State) State {
return func(s State) State {
s.Status = status
return s
}
},
"active",
),
Map[AppConfig](func(s State) string { return s.Status }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("active"), outcome)
}
func TestBindTo(t *testing.T) {
type State struct {
Count int
}
res := F.Pipe2(
Of[AppConfig](42),
BindTo[AppConfig](func(n int) State { return State{Count: n} }),
Map[AppConfig](func(s State) int { return s.Count }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestApS(t *testing.T) {
res := F.Pipe3(
Do[AppConfig](utils.Empty),
ApS(utils.SetLastName, Of[AppConfig]("Doe")),
ApS(utils.SetGivenName, Of[AppConfig]("John")),
Map[AppConfig](utils.GetFullName),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("John Doe"), outcome)
}
func TestApSWithError(t *testing.T) {
testErr := errors.New("aps error")
res := F.Pipe3(
Do[AppConfig](utils.Empty),
ApS(utils.SetLastName, Left[AppConfig, string](testErr)),
ApS(utils.SetGivenName, Of[AppConfig]("John")),
Map[AppConfig](utils.GetFullName),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
}
func TestBindReaderK(t *testing.T) {
type State struct {
Config string
}
getConfigPath := func(s State) func(AppConfig) string {
return func(cfg AppConfig) string {
return cfg.DatabaseURL
}
}
res := F.Pipe2(
Do[AppConfig](State{}),
BindReaderK(
func(path string) func(State) State {
return func(s State) State {
s.Config = path
return s
}
},
getConfigPath,
),
Map[AppConfig](func(s State) string { return s.Config }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of("postgres://localhost"), outcome)
}
func TestBindIOResultK(t *testing.T) {
type State struct {
Value int
ParsedValue int
}
parseValue := func(s State) ioresult.IOResult[int] {
return func() result.Result[int] {
if s.Value < 0 {
return result.Left[int](errors.New("negative value"))
}
return result.Of(s.Value * 2)
}
}
t.Run("success case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindIOResultK[AppConfig](
func(parsed int) func(State) State {
return func(s State) State {
s.ParsedValue = parsed
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.ParsedValue }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(10), outcome)
})
t.Run("error case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: -5}),
BindIOResultK[AppConfig](
func(parsed int) func(State) State {
return func(s State) State {
s.ParsedValue = parsed
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.ParsedValue }),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestBindIOK(t *testing.T) {
type State struct {
Value int
}
getValue := func(s State) io.IO[int] {
return func() int {
return s.Value * 2
}
}
res := F.Pipe2(
Do[AppConfig](State{Value: 21}),
BindIOK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
getValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
}
func TestBindReaderIOK(t *testing.T) {
type State struct {
Value int
}
getValue := func(s State) readerio.ReaderIO[AppConfig, int] {
return func(cfg AppConfig) io.IO[int] {
return func() int {
return s.Value + len(cfg.DatabaseURL)
}
}
}
res := F.Pipe2(
Do[AppConfig](State{Value: 10}),
BindReaderIOK(
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
getValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
// 10 + len("postgres://localhost") = 10 + 20 = 30
assert.Equal(t, result.Of(30), outcome)
}
func TestBindEitherK(t *testing.T) {
type State struct {
Value int
}
parseValue := func(s State) either.Either[error, int] {
if s.Value < 0 {
return either.Left[int](errors.New("negative"))
}
return either.Right[error](s.Value * 2)
}
t.Run("success case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindEitherK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(10), outcome)
})
t.Run("error case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: -5}),
BindEitherK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestBindIOEitherK(t *testing.T) {
type State struct {
Value int
}
parseValue := func(s State) ioeither.IOEither[error, int] {
return func() either.Either[error, int] {
if s.Value < 0 {
return either.Left[int](errors.New("negative"))
}
return either.Right[error](s.Value * 2)
}
}
t.Run("success case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindIOEitherK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(10), outcome)
})
t.Run("error case", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: -5}),
BindIOEitherK[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value = v
return s
}
},
parseValue,
),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.True(t, result.IsLeft(outcome))
})
}
func TestLensOperations(t *testing.T) {
type State struct {
Value int
}
valueLens := lens.MakeLens(
func(s State) int { return s.Value },
func(s State, v int) State {
s.Value = v
return s
},
)
t.Run("ApSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApSL(valueLens, Of[AppConfig](42)),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("BindL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 10}),
BindL(valueLens, func(v int) ReaderReaderIOResult[AppConfig, int] {
return Of[AppConfig](v * 2)
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
})
t.Run("LetL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 10}),
LetL[AppConfig](valueLens, func(v int) int { return v * 3 }),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(30), outcome)
})
t.Run("LetToL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
LetToL[AppConfig](valueLens, 99),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
})
t.Run("BindIOEitherKL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindIOEitherKL[AppConfig](valueLens, func(v int) ioeither.IOEither[error, int] {
return ioeither.Of[error](v * 2)
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(10), outcome)
})
t.Run("BindIOKL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 7}),
BindIOKL[AppConfig](valueLens, func(v int) io.IO[int] {
return func() int { return v * 3 }
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(21), outcome)
})
t.Run("BindReaderKL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 5}),
BindReaderKL(valueLens, func(v int) reader.Reader[AppConfig, int] {
return func(cfg AppConfig) int {
return v + len(cfg.LogLevel)
}
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
// 5 + len("info") = 5 + 4 = 9
assert.Equal(t, result.Of(9), outcome)
})
t.Run("BindReaderIOKL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{Value: 10}),
BindReaderIOKL(valueLens, func(v int) readerio.ReaderIO[AppConfig, int] {
return func(cfg AppConfig) io.IO[int] {
return func() int {
return v + len(cfg.DatabaseURL)
}
}
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
// 10 + len("postgres://localhost") = 10 + 20 = 30
assert.Equal(t, result.Of(30), outcome)
})
t.Run("ApIOEitherSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApIOEitherSL[AppConfig](valueLens, ioeither.Of[error](42)),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("ApIOSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApIOSL[AppConfig](valueLens, func() int { return 99 }),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
})
t.Run("ApReaderSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApReaderSL(valueLens, func(cfg AppConfig) int {
return len(cfg.LogLevel)
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(4), outcome)
})
t.Run("ApReaderIOSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApReaderIOSL(valueLens, func(cfg AppConfig) io.IO[int] {
return func() int { return len(cfg.DatabaseURL) }
}),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
})
t.Run("ApEitherSL", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApEitherSL[AppConfig](valueLens, either.Right[error](77)),
Map[AppConfig](func(s State) int { return s.Value }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(77), outcome)
})
}
func TestApOperations(t *testing.T) {
type State struct {
Value1 int
Value2 int
}
t.Run("ApIOEitherS", func(t *testing.T) {
res := F.Pipe3(
Do[AppConfig](State{}),
ApIOEitherS[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
ioeither.Of[error](10),
),
ApIOEitherS[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value2 = v
return s
}
},
ioeither.Of[error](20),
),
Map[AppConfig](func(s State) int { return s.Value1 + s.Value2 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(30), outcome)
})
t.Run("ApIOS", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApIOS[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
func() int { return 42 },
),
Map[AppConfig](func(s State) int { return s.Value1 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("ApReaderS", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApReaderS(
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
func(cfg AppConfig) int { return len(cfg.LogLevel) },
),
Map[AppConfig](func(s State) int { return s.Value1 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(4), outcome)
})
t.Run("ApReaderIOS", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApReaderIOS(
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
func(cfg AppConfig) io.IO[int] {
return func() int { return len(cfg.DatabaseURL) }
},
),
Map[AppConfig](func(s State) int { return s.Value1 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(20), outcome)
})
t.Run("ApEitherS", func(t *testing.T) {
res := F.Pipe2(
Do[AppConfig](State{}),
ApEitherS[AppConfig](
func(v int) func(State) State {
return func(s State) State {
s.Value1 = v
return s
}
},
either.Right[error](99),
),
Map[AppConfig](func(s State) int { return s.Value1 }),
)
outcome := res(defaultConfig)(t.Context())()
assert.Equal(t, result.Of(99), outcome)
})
}

View File

@@ -0,0 +1,95 @@
// 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 readerreaderioresult
import (
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
)
// Bracket ensures that a resource is properly cleaned up regardless of whether the operation
// succeeds or fails. It follows the acquire-use-release pattern with access to both outer (R)
// and inner (C) reader contexts.
//
// The release action is always called after the use action completes, whether it succeeds or fails.
// This makes it ideal for managing resources like file handles, database connections, or locks.
//
// Parameters:
// - acquire: Acquires the resource, returning a ReaderReaderIOEither[R, C, E, A]
// - use: Uses the acquired resource to perform an operation, returning ReaderReaderIOEither[R, C, E, B]
// - release: Releases the resource, receiving both the resource and the result of use
//
// Returns:
// - A ReaderReaderIOEither[R, C, E, B] that safely manages the resource lifecycle
//
// The release function receives:
// - The acquired resource (A)
// - The result of the use function (Either[E, B])
//
// Example:
//
// type OuterConfig struct {
// ConnectionPool string
// }
// type InnerConfig struct {
// Timeout time.Duration
// }
//
// // Acquire a database connection
// acquire := func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, *sql.DB] {
// return func(inner InnerConfig) ioeither.IOEither[error, *sql.DB] {
// return ioeither.TryCatch(
// func() (*sql.DB, error) {
// return sql.Open("postgres", outer.ConnectionPool)
// },
// func(err error) error { return err },
// )
// }
// }
//
// // Use the connection
// use := func(db *sql.DB) readerreaderioeither.ReaderReaderIOEither[OuterConfig, InnerConfig, error, []User] {
// return func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, []User] {
// return func(inner InnerConfig) ioeither.IOEither[error, []User] {
// return queryUsers(db, inner.Timeout)
// }
// }
// }
//
// // Release the connection
// release := func(db *sql.DB, result either.Either[error, []User]) readerreaderioeither.ReaderReaderIOEither[OuterConfig, InnerConfig, error, any] {
// return func(outer OuterConfig) readerioeither.ReaderIOEither[InnerConfig, error, any] {
// return func(inner InnerConfig) ioeither.IOEither[error, any] {
// return ioeither.TryCatch(
// func() (any, error) {
// return nil, db.Close()
// },
// func(err error) error { return err },
// )
// }
// }
// }
//
// result := readerreaderioeither.Bracket(acquire, use, release)
//
//go:inline
func Bracket[
R, A, B, ANY any](
acquire ReaderReaderIOResult[R, A],
use Kleisli[R, A, B],
release func(A, Result[B]) ReaderReaderIOResult[R, ANY],
) ReaderReaderIOResult[R, B] {
return RRIOE.Bracket(acquire, use, release)
}

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