1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-26 13:06:09 +02:00

Compare commits

...

51 Commits

Author SHA1 Message Date
Dr. Carsten Leue
cc0c14c7cf fix: do not fail if coverage fails
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 11:35:16 +01:00
Dr. Carsten Leue
19159ad49e fix: WriteFile
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 10:42:46 +01:00
Dr. Carsten Leue
b9c8fb4ff1 fix: parameter order for Local and TapIOK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 17:55:32 +01:00
Dr. Carsten Leue
4529e720bc fix: add ChainThunkK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:59:51 +01:00
Dr. Carsten Leue
ef8b2ea65d fix: add ChainReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:31:51 +01:00
Dr. Carsten Leue
5bd7caafdd fix: better doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 13:42:29 +01:00
Dr. Carsten Leue
47ebcd79b1 fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:15:00 +01:00
Dr. Carsten Leue
dbad94806e fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:14:21 +01:00
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
364 changed files with 77251 additions and 1864 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
@@ -42,6 +42,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -64,13 +65,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
@@ -81,6 +82,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -106,6 +108,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -126,17 +129,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": [

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)
}

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)
}

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

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

273
v2/cli/README.md Normal file
View File

@@ -0,0 +1,273 @@
# CLI Package - Functional Wrappers for urfave/cli/v3
This package provides functional programming wrappers for the `github.com/urfave/cli/v3` library, enabling Effect-based command actions and type-safe flag handling through Prisms.
## Features
### 1. Effect-Based Command Actions
Transform CLI command actions into composable Effects that follow functional programming principles.
#### Key Functions
- **`ToAction(effect CommandEffect) func(context.Context, *C.Command) error`**
- Converts a CommandEffect into a standard urfave/cli Action function
- Enables Effect-based command handlers to work with cli/v3 framework
- **`FromAction(action func(context.Context, *C.Command) error) CommandEffect`**
- Lifts existing cli/v3 action handlers into the Effect type
- Allows gradual migration to functional style
- **`MakeCommand(name, usage string, flags []C.Flag, effect CommandEffect) *C.Command`**
- Creates a new Command with an Effect-based action
- Convenience function combining command creation with Effect conversion
- **`MakeCommandWithSubcommands(...) *C.Command`**
- Creates a Command with subcommands and an Effect-based action
#### Example Usage
```go
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Define an Effect-based command action
processEffect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process input...
return R.Of(F.Void{})
}
}
}
// Create command with Effect
command := cli.MakeCommand(
"process",
"Process input files",
[]C.Flag{
&C.StringFlag{Name: "input", Usage: "Input file path"},
},
processEffect,
)
// Or convert existing action to Effect
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Existing logic...
return nil
}
effect := cli.FromAction(existingAction)
```
### 2. Flag Type Prisms
Type-safe extraction and manipulation of CLI flags using Prisms from the optics package.
#### Available Prisms
- `StringFlagPrism()` - Extract `*C.StringFlag` from `C.Flag`
- `IntFlagPrism()` - Extract `*C.IntFlag` from `C.Flag`
- `BoolFlagPrism()` - Extract `*C.BoolFlag` from `C.Flag`
- `Float64FlagPrism()` - Extract `*C.Float64Flag` from `C.Flag`
- `DurationFlagPrism()` - Extract `*C.DurationFlag` from `C.Flag`
- `TimestampFlagPrism()` - Extract `*C.TimestampFlag` from `C.Flag`
- `StringSliceFlagPrism()` - Extract `*C.StringSliceFlag` from `C.Flag`
- `IntSliceFlagPrism()` - Extract `*C.IntSliceFlag` from `C.Flag`
- `Float64SliceFlagPrism()` - Extract `*C.Float64SliceFlag` from `C.Flag`
- `UintFlagPrism()` - Extract `*C.UintFlag` from `C.Flag`
- `Uint64FlagPrism()` - Extract `*C.Uint64Flag` from `C.Flag`
- `Int64FlagPrism()` - Extract `*C.Int64Flag` from `C.Flag`
#### Example Usage
```go
import (
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Extract a StringFlag from a Flag interface
var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
prism := cli.StringFlagPrism()
// Safe extraction returns Option
result := prism.GetOption(flag)
if O.IsSome(result) {
strFlag := O.MonadFold(result,
func() *C.StringFlag { return nil },
func(f *C.StringFlag) *C.StringFlag { return f },
)
// Use strFlag...
}
// Type mismatch returns None
var intFlag C.Flag = &C.IntFlag{Name: "count"}
result = prism.GetOption(intFlag) // Returns None
// Convert back to Flag
strFlag := &C.StringFlag{Name: "output"}
flag = prism.ReverseGet(strFlag)
```
## Type Definitions
### CommandEffect
```go
type CommandEffect = E.Effect[*C.Command, F.Void]
```
A CommandEffect represents a CLI command action as an Effect. It takes a `*C.Command` as context and produces a result wrapped in the Effect monad.
The Effect structure is:
```
func(*C.Command) -> func(context.Context) -> func() -> Result[Void]
```
This allows for:
- **Composability**: Effects can be composed using standard functional combinators
- **Testability**: Pure functions are easier to test
- **Error Handling**: Errors are explicitly represented in the Result type
- **Context Management**: Context flows naturally through the Effect
## Benefits
### 1. Functional Composition
Effects can be composed using standard functional programming patterns:
```go
import (
F "github.com/IBM/fp-go/v2/function"
RRIOE "github.com/IBM/fp-go/v2/context/readerreaderioresult"
)
// Compose multiple effects
validateInput := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
processData := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
saveResults := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
// Chain effects together
pipeline := F.Pipe3(
validateInput,
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return processData }),
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return saveResults }),
)
```
### 2. Type Safety
Prisms provide compile-time type safety when working with flags:
```go
// Type-safe flag extraction
flags := []C.Flag{
&C.StringFlag{Name: "input"},
&C.IntFlag{Name: "count"},
}
for _, flag := range flags {
// Safe extraction with pattern matching
O.MonadFold(
cli.StringFlagPrism().GetOption(flag),
func() { /* Not a string flag */ },
func(sf *C.StringFlag) { /* Handle string flag */ },
)
}
```
### 3. Error Handling
Errors are explicitly represented in the Result type:
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
if err := validateInput(cmd); err != nil {
return R.Left[F.Void](err) // Explicit error
}
return R.Of(F.Void{}) // Success
}
}
}
```
### 4. Testability
Pure functions are easier to test:
```go
func TestCommandEffect(t *testing.T) {
cmd := &C.Command{Name: "test"}
effect := myCommandEffect(cmd)
// Execute effect
result := effect(context.Background())()
// Assert on result
assert.True(t, R.IsRight(result))
}
```
## Migration Guide
### From Standard Actions to Effects
**Before:**
```go
command := &C.Command{
Name: "process",
Action: func(ctx context.Context, cmd *C.Command) error {
input := cmd.String("input")
// Process...
return nil
},
}
```
**After:**
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process...
return R.Of(F.Void{})
}
}
}
command := cli.MakeCommand("process", "Process files", flags, effect)
```
### Gradual Migration
You can mix both styles during migration:
```go
// Wrap existing action
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Legacy code...
return nil
}
// Use as Effect
effect := cli.FromAction(existingAction)
command := cli.MakeCommand("legacy", "Legacy command", flags, effect)
```
## See Also
- [Effect Package](../effect/) - Core Effect type definitions
- [Optics Package](../optics/) - Prism and other optics
- [urfave/cli/v3](https://github.com/urfave/cli) - Underlying CLI framework

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),
)
},
}

199
v2/cli/effect.go Normal file
View File

@@ -0,0 +1,199 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
R "github.com/IBM/fp-go/v2/result"
C "github.com/urfave/cli/v3"
)
// CommandEffect represents a CLI command action as an Effect.
// The Effect takes a *C.Command as context and produces a result.
type CommandEffect = E.Effect[*C.Command, F.Void]
// ToAction converts a CommandEffect into a standard urfave/cli Action function.
// This allows Effect-based command handlers to be used with the cli/v3 framework.
//
// The conversion process:
// 1. Takes the Effect which expects a *C.Command context
// 2. Executes it with the provided command
// 3. Runs the resulting IO operation
// 4. Converts the Result to either nil (success) or error (failure)
//
// # Parameters
//
// - effect: The CommandEffect to convert
//
// # Returns
//
// - A function compatible with C.Command.Action signature
//
// # Example Usage
//
// effect := func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// // Command logic here
// return R.Of(F.Void{})
// }
// }
// }
// action := ToAction(effect)
// command := &C.Command{
// Name: "example",
// Action: action,
// }
func ToAction(effect CommandEffect) func(context.Context, *C.Command) error {
return func(ctx context.Context, cmd *C.Command) error {
// Execute the effect: cmd -> ctx -> IO -> Result
return F.Pipe3(
ctx,
effect(cmd),
io.Run,
// Convert Result[Void] to error
ET.Fold(F.Identity[error], F.Constant1[F.Void, error](nil)),
)
}
}
// FromAction converts a standard urfave/cli Action function into a CommandEffect.
// This allows existing cli/v3 action handlers to be lifted into the Effect type.
//
// The conversion process:
// 1. Takes a standard action function (context.Context, *C.Command) -> error
// 2. Wraps it in the Effect structure
// 3. Converts the error result to a Result type
//
// # Parameters
//
// - action: The standard cli/v3 action function to convert
//
// # Returns
//
// - A CommandEffect that wraps the original action
//
// # Example Usage
//
// standardAction := func(ctx context.Context, cmd *C.Command) error {
// // Existing command logic
// return nil
// }
// effect := FromAction(standardAction)
// // Now can be composed with other Effects
func FromAction(action func(context.Context, *C.Command) error) CommandEffect {
return func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
err := action(ctx, cmd)
if err != nil {
return R.Left[F.Void](err)
}
return R.Of(F.Void{})
}
}
}
}
// MakeCommand creates a new Command with an Effect-based action.
// This is a convenience function that combines command creation with Effect conversion.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommand(
// "process",
// "Process data files",
// []C.Flag{
// &C.StringFlag{Name: "input", Usage: "Input file"},
// },
// func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// input := cmd.String("input")
// // Process input...
// return R.Of(F.Void{})
// }
// }
// },
// )
func MakeCommand(
name string,
usage string,
flags []C.Flag,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Action: ToAction(effect),
}
}
// MakeCommandWithSubcommands creates a new Command with subcommands and an Effect-based action.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - commands: The subcommands
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with subcommands and the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommandWithSubcommands(
// "app",
// "Application commands",
// []C.Flag{},
// []*C.Command{subCmd1, subCmd2},
// defaultEffect,
// )
func MakeCommandWithSubcommands(
name string,
usage string,
flags []C.Flag,
commands []*C.Command,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Commands: commands,
Action: ToAction(effect),
}
}

204
v2/cli/effect_test.go Normal file
View File

@@ -0,0 +1,204 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"context"
"errors"
"testing"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestToAction_Success(t *testing.T) {
t.Run("converts successful Effect to action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
})
}
func TestToAction_Failure(t *testing.T) {
t.Run("converts failed Effect to error", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Left[F.Void](expectedErr)
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestFromAction_Success(t *testing.T) {
t.Run("converts successful action to Effect", func(t *testing.T) {
// Arrange
action := func(ctx context.Context, cmd *C.Command) error {
return nil
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsRight(result))
})
}
func TestFromAction_Failure(t *testing.T) {
t.Run("converts failed action to Effect", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
action := func(ctx context.Context, cmd *C.Command) error {
return expectedErr
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsLeft(result))
err := R.MonadFold(result, F.Identity[error], func(F.Void) error { return nil })
assert.Equal(t, expectedErr, err)
})
}
func TestMakeCommand(t *testing.T) {
t.Run("creates command with Effect-based action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommand(
"test",
"Test command",
[]C.Flag{},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "test", cmd.Name)
assert.Equal(t, "Test command", cmd.Usage)
assert.NotNil(t, cmd.Action)
// Test the action
err := cmd.Action(context.Background(), cmd)
assert.NoError(t, err)
})
}
func TestMakeCommandWithSubcommands(t *testing.T) {
t.Run("creates command with subcommands and Effect-based action", func(t *testing.T) {
// Arrange
subCmd := &C.Command{Name: "sub"}
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommandWithSubcommands(
"parent",
"Parent command",
[]C.Flag{},
[]*C.Command{subCmd},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "parent", cmd.Name)
assert.Equal(t, "Parent command", cmd.Usage)
assert.Len(t, cmd.Commands, 1)
assert.Equal(t, "sub", cmd.Commands[0].Name)
assert.NotNil(t, cmd.Action)
})
}
func TestToAction_Integration(t *testing.T) {
t.Run("Effect can access command flags", func(t *testing.T) {
// Arrange
var capturedValue string
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
capturedValue = cmd.String("input")
return R.Of(F.Void{})
}
}
}
cmd := &C.Command{
Name: "test",
Flags: []C.Flag{
&C.StringFlag{
Name: "input",
Value: "default-value",
},
},
Action: ToAction(effect),
}
// Act
err := cmd.Action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
assert.Equal(t, "default-value", capturedValue)
})
}

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),
)
},
}

359
v2/cli/flags.go Normal file
View File

@@ -0,0 +1,359 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
C "github.com/urfave/cli/v3"
)
// StringFlagPrism creates a Prism for extracting a StringFlag from a Flag.
// This provides a type-safe way to work with string flags, handling type
// mismatches gracefully through the Option type.
//
// The prism's GetOption attempts to cast a Flag to *C.StringFlag.
// If the cast succeeds, it returns Some(*C.StringFlag); if it fails, it returns None.
//
// The prism's ReverseGet converts a *C.StringFlag back to a Flag.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringFlag] for safe StringFlag extraction
//
// # Example Usage
//
// prism := StringFlagPrism()
//
// // Extract StringFlag from Flag
// var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
// result := prism.GetOption(flag) // Some(*C.StringFlag{...})
//
// // Type mismatch returns None
// var intFlag C.Flag = &C.IntFlag{Name: "count"}
// result = prism.GetOption(intFlag) // None[*C.StringFlag]()
//
// // Convert back to Flag
// strFlag := &C.StringFlag{Name: "output"}
// flag = prism.ReverseGet(strFlag)
func StringFlagPrism() P.Prism[C.Flag, *C.StringFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringFlag] {
if sf, ok := flag.(*C.StringFlag); ok {
return O.Some(sf)
}
return O.None[*C.StringFlag]()
},
func(f *C.StringFlag) C.Flag { return f },
)
}
// IntFlagPrism creates a Prism for extracting an IntFlag from a Flag.
// This provides a type-safe way to work with integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntFlag] for safe IntFlag extraction
//
// # Example Usage
//
// prism := IntFlagPrism()
//
// // Extract IntFlag from Flag
// var flag C.Flag = &C.IntFlag{Name: "count", Value: 10}
// result := prism.GetOption(flag) // Some(*C.IntFlag{...})
func IntFlagPrism() P.Prism[C.Flag, *C.IntFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntFlag] {
if f, ok := flag.(*C.IntFlag); ok {
return O.Some(f)
}
return O.None[*C.IntFlag]()
},
func(f *C.IntFlag) C.Flag { return f },
)
}
// BoolFlagPrism creates a Prism for extracting a BoolFlag from a Flag.
// This provides a type-safe way to work with boolean flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.BoolFlag] for safe BoolFlag extraction
//
// # Example Usage
//
// prism := BoolFlagPrism()
//
// // Extract BoolFlag from Flag
// var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// result := prism.GetOption(flag) // Some(*C.BoolFlag{...})
func BoolFlagPrism() P.Prism[C.Flag, *C.BoolFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.BoolFlag] {
if f, ok := flag.(*C.BoolFlag); ok {
return O.Some(f)
}
return O.None[*C.BoolFlag]()
},
func(f *C.BoolFlag) C.Flag { return f },
)
}
// Float64FlagPrism creates a Prism for extracting a Float64Flag from a Flag.
// This provides a type-safe way to work with float64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64Flag] for safe Float64Flag extraction
//
// # Example Usage
//
// prism := Float64FlagPrism()
//
// // Extract Float64Flag from Flag
// var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// result := prism.GetOption(flag) // Some(*C.Float64Flag{...})
func Float64FlagPrism() P.Prism[C.Flag, *C.Float64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64Flag] {
if f, ok := flag.(*C.Float64Flag); ok {
return O.Some(f)
}
return O.None[*C.Float64Flag]()
},
func(f *C.Float64Flag) C.Flag { return f },
)
}
// DurationFlagPrism creates a Prism for extracting a DurationFlag from a Flag.
// This provides a type-safe way to work with duration flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.DurationFlag] for safe DurationFlag extraction
//
// # Example Usage
//
// prism := DurationFlagPrism()
//
// // Extract DurationFlag from Flag
// var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: 30 * time.Second}
// result := prism.GetOption(flag) // Some(*C.DurationFlag{...})
func DurationFlagPrism() P.Prism[C.Flag, *C.DurationFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.DurationFlag] {
if f, ok := flag.(*C.DurationFlag); ok {
return O.Some(f)
}
return O.None[*C.DurationFlag]()
},
func(f *C.DurationFlag) C.Flag { return f },
)
}
// TimestampFlagPrism creates a Prism for extracting a TimestampFlag from a Flag.
// This provides a type-safe way to work with timestamp flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.TimestampFlag] for safe TimestampFlag extraction
//
// # Example Usage
//
// prism := TimestampFlagPrism()
//
// // Extract TimestampFlag from Flag
// var flag C.Flag = &C.TimestampFlag{Name: "created"}
// result := prism.GetOption(flag) // Some(*C.TimestampFlag{...})
func TimestampFlagPrism() P.Prism[C.Flag, *C.TimestampFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.TimestampFlag] {
if f, ok := flag.(*C.TimestampFlag); ok {
return O.Some(f)
}
return O.None[*C.TimestampFlag]()
},
func(f *C.TimestampFlag) C.Flag { return f },
)
}
// StringSliceFlagPrism creates a Prism for extracting a StringSliceFlag from a Flag.
// This provides a type-safe way to work with string slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringSliceFlag] for safe StringSliceFlag extraction
//
// # Example Usage
//
// prism := StringSliceFlagPrism()
//
// // Extract StringSliceFlag from Flag
// var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// result := prism.GetOption(flag) // Some(*C.StringSliceFlag{...})
func StringSliceFlagPrism() P.Prism[C.Flag, *C.StringSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringSliceFlag] {
if f, ok := flag.(*C.StringSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.StringSliceFlag]()
},
func(f *C.StringSliceFlag) C.Flag { return f },
)
}
// IntSliceFlagPrism creates a Prism for extracting an IntSliceFlag from a Flag.
// This provides a type-safe way to work with int slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntSliceFlag] for safe IntSliceFlag extraction
//
// # Example Usage
//
// prism := IntSliceFlagPrism()
//
// // Extract IntSliceFlag from Flag
// var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// result := prism.GetOption(flag) // Some(*C.IntSliceFlag{...})
func IntSliceFlagPrism() P.Prism[C.Flag, *C.IntSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntSliceFlag] {
if f, ok := flag.(*C.IntSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.IntSliceFlag]()
},
func(f *C.IntSliceFlag) C.Flag { return f },
)
}
// Float64SliceFlagPrism creates a Prism for extracting a Float64SliceFlag from a Flag.
// This provides a type-safe way to work with float64 slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64SliceFlag] for safe Float64SliceFlag extraction
//
// # Example Usage
//
// prism := Float64SliceFlagPrism()
//
// // Extract Float64SliceFlag from Flag
// var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// result := prism.GetOption(flag) // Some(*C.Float64SliceFlag{...})
func Float64SliceFlagPrism() P.Prism[C.Flag, *C.Float64SliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64SliceFlag] {
if f, ok := flag.(*C.Float64SliceFlag); ok {
return O.Some(f)
}
return O.None[*C.Float64SliceFlag]()
},
func(f *C.Float64SliceFlag) C.Flag { return f },
)
}
// UintFlagPrism creates a Prism for extracting a UintFlag from a Flag.
// This provides a type-safe way to work with unsigned integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.UintFlag] for safe UintFlag extraction
//
// # Example Usage
//
// prism := UintFlagPrism()
//
// // Extract UintFlag from Flag
// var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// result := prism.GetOption(flag) // Some(*C.UintFlag{...})
func UintFlagPrism() P.Prism[C.Flag, *C.UintFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.UintFlag] {
if f, ok := flag.(*C.UintFlag); ok {
return O.Some(f)
}
return O.None[*C.UintFlag]()
},
func(f *C.UintFlag) C.Flag { return f },
)
}
// Uint64FlagPrism creates a Prism for extracting a Uint64Flag from a Flag.
// This provides a type-safe way to work with uint64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Uint64Flag] for safe Uint64Flag extraction
//
// # Example Usage
//
// prism := Uint64FlagPrism()
//
// // Extract Uint64Flag from Flag
// var flag C.Flag = &C.Uint64Flag{Name: "size"}
// result := prism.GetOption(flag) // Some(*C.Uint64Flag{...})
func Uint64FlagPrism() P.Prism[C.Flag, *C.Uint64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Uint64Flag] {
if f, ok := flag.(*C.Uint64Flag); ok {
return O.Some(f)
}
return O.None[*C.Uint64Flag]()
},
func(f *C.Uint64Flag) C.Flag { return f },
)
}
// Int64FlagPrism creates a Prism for extracting an Int64Flag from a Flag.
// This provides a type-safe way to work with int64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Int64Flag] for safe Int64Flag extraction
//
// # Example Usage
//
// prism := Int64FlagPrism()
//
// // Extract Int64Flag from Flag
// var flag C.Flag = &C.Int64Flag{Name: "offset"}
// result := prism.GetOption(flag) // Some(*C.Int64Flag{...})
func Int64FlagPrism() P.Prism[C.Flag, *C.Int64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Int64Flag] {
if f, ok := flag.(*C.Int64Flag); ok {
return O.Some(f)
}
return O.None[*C.Int64Flag]()
},
func(f *C.Int64Flag) C.Flag { return f },
)
}

287
v2/cli/flags_test.go Normal file
View File

@@ -0,0 +1,287 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"testing"
"time"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestStringFlagPrism_Success(t *testing.T) {
t.Run("extracts StringFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.StringFlag{Name: "input", Value: "test"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringFlag { return nil }, func(f *C.StringFlag) *C.StringFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "input", extracted.Name)
assert.Equal(t, "test", extracted.Value)
})
}
func TestStringFlagPrism_Failure(t *testing.T) {
t.Run("returns None for non-StringFlag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsNone(result))
})
}
func TestStringFlagPrism_ReverseGet(t *testing.T) {
t.Run("converts StringFlag back to Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
strFlag := &C.StringFlag{Name: "output", Value: "result"}
// Act
flag := prism.ReverseGet(strFlag)
// Assert
assert.NotNil(t, flag)
assert.IsType(t, &C.StringFlag{}, flag)
})
}
func TestIntFlagPrism_Success(t *testing.T) {
t.Run("extracts IntFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count", Value: 42}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntFlag { return nil }, func(f *C.IntFlag) *C.IntFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "count", extracted.Name)
assert.Equal(t, 42, extracted.Value)
})
}
func TestBoolFlagPrism_Success(t *testing.T) {
t.Run("extracts BoolFlag from Flag", func(t *testing.T) {
// Arrange
prism := BoolFlagPrism()
var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.BoolFlag { return nil }, func(f *C.BoolFlag) *C.BoolFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "verbose", extracted.Name)
assert.Equal(t, true, extracted.Value)
})
}
func TestFloat64FlagPrism_Success(t *testing.T) {
t.Run("extracts Float64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Float64FlagPrism()
var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64Flag { return nil }, func(f *C.Float64Flag) *C.Float64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratio", extracted.Name)
assert.Equal(t, 0.5, extracted.Value)
})
}
func TestDurationFlagPrism_Success(t *testing.T) {
t.Run("extracts DurationFlag from Flag", func(t *testing.T) {
// Arrange
prism := DurationFlagPrism()
duration := 30 * time.Second
var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: duration}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.DurationFlag { return nil }, func(f *C.DurationFlag) *C.DurationFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "timeout", extracted.Name)
assert.Equal(t, duration, extracted.Value)
})
}
func TestTimestampFlagPrism_Success(t *testing.T) {
t.Run("extracts TimestampFlag from Flag", func(t *testing.T) {
// Arrange
prism := TimestampFlagPrism()
var flag C.Flag = &C.TimestampFlag{Name: "created"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.TimestampFlag { return nil }, func(f *C.TimestampFlag) *C.TimestampFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "created", extracted.Name)
})
}
func TestStringSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts StringSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringSliceFlagPrism()
var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringSliceFlag { return nil }, func(f *C.StringSliceFlag) *C.StringSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "tags", extracted.Name)
})
}
func TestIntSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts IntSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntSliceFlagPrism()
var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntSliceFlag { return nil }, func(f *C.IntSliceFlag) *C.IntSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ports", extracted.Name)
})
}
func TestFloat64SliceFlagPrism_Success(t *testing.T) {
t.Run("extracts Float64SliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := Float64SliceFlagPrism()
var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64SliceFlag { return nil }, func(f *C.Float64SliceFlag) *C.Float64SliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratios", extracted.Name)
})
}
func TestUintFlagPrism_Success(t *testing.T) {
t.Run("extracts UintFlag from Flag", func(t *testing.T) {
// Arrange
prism := UintFlagPrism()
var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.UintFlag { return nil }, func(f *C.UintFlag) *C.UintFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "workers", extracted.Name)
assert.Equal(t, uint(4), extracted.Value)
})
}
func TestUint64FlagPrism_Success(t *testing.T) {
t.Run("extracts Uint64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Uint64FlagPrism()
var flag C.Flag = &C.Uint64Flag{Name: "size", Value: 1024}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Uint64Flag { return nil }, func(f *C.Uint64Flag) *C.Uint64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "size", extracted.Name)
assert.Equal(t, uint64(1024), extracted.Value)
})
}
func TestInt64FlagPrism_Success(t *testing.T) {
t.Run("extracts Int64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Int64FlagPrism()
var flag C.Flag = &C.Int64Flag{Name: "offset", Value: -100}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Int64Flag { return nil }, func(f *C.Int64Flag) *C.Int64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "offset", extracted.Name)
assert.Equal(t, int64(-100), extracted.Value)
})
}
func TestPrisms_EdgeCases(t *testing.T) {
t.Run("all prisms return None for wrong type", func(t *testing.T) {
// Arrange
var flag C.Flag = &C.StringFlag{Name: "test"}
// Act & Assert
assert.True(t, O.IsNone(IntFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(BoolFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(DurationFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(TimestampFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(StringSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(IntSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64SliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(UintFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Uint64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Int64FlagPrism().GetOption(flag)))
})
}

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

@@ -594,7 +594,7 @@ func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
// )
//
// // Create context with side effects (e.g., loading config)
// createContext := G.Of(context.WithValue(context.Background(), "key", "value"))
// createContext := G.Of(context.WithValue(t.Context(), "key", "value"))
//
// // A computation that uses the context
// getValue := readerio.FromReader(func(ctx context.Context) string {
@@ -664,7 +664,7 @@ func ReadIO[A any](r IO[context.Context]) func(ReaderIO[A]) IO[A] {
// getUser,
// addUser,
// )
// user := result(context.Background())() // Returns "Alice"
// user := result(t.Context())() // Returns "Alice"
//
// Timeout Example:
//
@@ -731,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:
//
@@ -740,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)
@@ -791,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,14 +496,14 @@ 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(context.Background(), "testKey", "testValue"))
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)
@@ -519,7 +519,7 @@ func TestReadIO(t *testing.T) {
func TestReadIOWithBackground(t *testing.T) {
// Test ReadIO with plain background context
contextIO := G.Of(context.Background())
contextIO := G.Of(t.Context())
rio := Of(42)
ioAction := ReadIO[int](contextIO)(rio)
@@ -530,7 +530,7 @@ func TestReadIOWithBackground(t *testing.T) {
func TestReadIOWithChain(t *testing.T) {
// Test ReadIO with chained operations
contextIO := G.Of(context.WithValue(context.Background(), "multiplier", 3))
contextIO := G.Of(context.WithValue(t.Context(), "multiplier", 3))
result := F.Pipe1(
FromReader(func(ctx context.Context) int {
@@ -552,7 +552,7 @@ func TestReadIOWithChain(t *testing.T) {
func TestReadIOWithMap(t *testing.T) {
// Test ReadIO with Map operations
contextIO := G.Of(context.Background())
contextIO := G.Of(t.Context())
result := F.Pipe2(
Of(5),
@@ -571,7 +571,7 @@ func TestReadIOWithSideEffects(t *testing.T) {
counter := 0
contextIO := func() context.Context {
counter++
return context.WithValue(context.Background(), "counter", counter)
return context.WithValue(t.Context(), "counter", counter)
}
rio := FromReader(func(ctx context.Context) int {
@@ -593,7 +593,7 @@ func TestReadIOMultipleExecutions(t *testing.T) {
counter := 0
contextIO := func() context.Context {
counter++
return context.Background()
return t.Context()
}
rio := Of(42)
@@ -609,7 +609,7 @@ func TestReadIOMultipleExecutions(t *testing.T) {
func TestReadIOComparisonWithRead(t *testing.T) {
// Compare ReadIO with Read to show the difference
ctx := context.WithValue(context.Background(), "key", "value")
ctx := context.WithValue(t.Context(), "key", "value")
rio := FromReader(func(ctx context.Context) string {
if val := ctx.Value("key"); val != nil {
@@ -642,7 +642,7 @@ func TestReadIOWithComplexContext(t *testing.T) {
contextIO := G.Of(
context.WithValue(
context.WithValue(context.Background(), userKey, "Alice"),
context.WithValue(t.Context(), userKey, "Alice"),
tokenKey,
"secret123",
),
@@ -668,7 +668,7 @@ func TestReadIOWithComplexContext(t *testing.T) {
func TestReadIOWithAsk(t *testing.T) {
// Test ReadIO combined with Ask
contextIO := G.Of(context.WithValue(context.Background(), "data", 100))
contextIO := G.Of(context.WithValue(t.Context(), "data", 100))
result := F.Pipe1(
Ask(),

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

@@ -21,6 +21,7 @@ import (
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/pair"
)
// WithContext wraps an existing [ReaderIOResult] and performs a context check for cancellation before delegating.
@@ -74,7 +75,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
//
@@ -85,3 +86,7 @@ func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
WithContext,
)
}
func pairFromContextCancel(newCtx context.Context, cancelFct context.CancelFunc) ContextCancel {
return pair.MakePair(cancelFct, newCtx)
}

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

@@ -13,6 +13,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package file provides context-aware file operations that integrate with the ReaderIOResult monad.
// It offers safe, composable file I/O operations that respect context cancellation and properly
// manage resources using the RAII pattern.
//
// All operations in this package:
// - Respect context.Context for cancellation and timeouts
// - Return ReaderIOResult for composable error handling
// - Automatically manage resource cleanup
// - Are safe to use in concurrent environments
//
// # Example Usage
//
// // Read a file with automatic resource management
// readOp := ReadFile("data.txt")
// result := readOp(ctx)()
//
// // Open and manually manage a file
// fileOp := Open("config.json")
// fileResult := fileOp(ctx)()
package file
import (
@@ -29,32 +48,181 @@ import (
)
var (
// Open opens a file for reading within the given context
// Open opens a file for reading within the given context.
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or ReadFile.
//
// Parameters:
// - path: The path to the file to open
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that opens the file
//
// Example:
//
// openFile := Open("data.txt")
// result := openFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// // Use file...
// },
// )
//
// See Also:
// - ReadFile: For reading entire file contents with automatic resource management
// - Close: For closing file handles
Open = F.Flow3(
IOEF.Open,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name
// Create creates or truncates a file for writing within the given context.
// If the file already exists, it is truncated. If it doesn't exist, it is created
// with mode 0666 (before umask).
//
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or WriteFile.
//
// Parameters:
// - path: The path to the file to create or truncate
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that creates the file
//
// Example:
//
// createFile := Create("output.txt")
// result := createFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// f.WriteString("Hello, World!")
// },
// )
//
// See Also:
// - WriteFile: For writing data to a file with automatic resource management
// - Open: For opening files for reading
// - Close: For closing file handles
Create = F.Flow3(
IOEF.Create,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name.
// The operation returns the filename on success, allowing for easy composition
// with other file operations.
//
// Parameters:
// - name: The path to the file to remove
//
// Returns:
// - ReaderIOResult[string]: A computation that removes the file and returns its name
//
// Example:
//
// removeOp := Remove("temp.txt")
// result := removeOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Failed to remove: %v", err) },
// func(name string) { log.Printf("Removed: %s", name) },
// )
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading file contents
Remove = F.Flow2(
IOEF.Remove,
RIOE.FromIOEither[string],
)
)
// Close closes an object
func Close[C io.Closer](c C) RIOE.ReaderIOResult[struct{}] {
// Close closes an io.Closer resource and returns a ReaderIOResult.
// This function is generic and works with any type that implements io.Closer,
// including os.File, network connections, and other closeable resources.
//
// The function captures any error that occurs during closing and returns it
// as part of the ReaderIOResult. On success, it returns Void (empty struct).
//
// Type Parameters:
// - C: Any type that implements io.Closer
//
// Parameters:
// - c: The resource to close
//
// Returns:
// - ReaderIOResult[Void]: A computation that closes the resource
//
// Example:
//
// file, _ := os.Open("data.txt")
// closeOp := Close(file)
// result := closeOp(ctx)()
//
// Note: This function is typically used with WithResource for automatic resource management
// rather than being called directly.
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading files with automatic closing
func Close[C io.Closer](c C) ReaderIOResult[Void] {
return F.Pipe2(
c,
IOEF.Close[C],
RIOE.FromIOEither[struct{}],
RIOE.FromIOEither[Void],
)
}
// ReadFile reads a file in the scope of a context
func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOResult[[]byte] {
// ReadFile reads the entire contents of a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// The operation:
// - Opens the file for reading
// - Reads all contents into a byte slice
// - Automatically closes the file when done
// - Respects context cancellation during the read operation
//
// Parameters:
// - path: The path to the file to read
//
// Returns:
// - ReaderIOResult[[]byte]: A computation that reads the file contents
//
// Example:
//
// readOp := ReadFile("config.json")
// result := readOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Read error: %v", err) },
// func(data []byte) { log.Printf("Read %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// ReadFile(path) = WithResource(Open(path), Close)(readAllBytes)
//
// See Also:
// - Open: For opening files without automatic reading
// - Close: For closing file handles
// - WithResource: For custom resource management patterns
func ReadFile(path string) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) ReaderIOResult[[]byte] {
return func(ctx context.Context) IOE.IOEither[error, []byte] {
return func() ET.Either[error, []byte] {
return file.ReadAll(ctx, r)
@@ -62,3 +230,48 @@ func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
}
})
}
// WriteFile writes data to a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// If the file doesn't exist, it is created with mode 0666 (before umask).
// If the file already exists, it is truncated before writing.
//
// The operation:
// - Creates or truncates the file for writing
// - Writes all data to the file
// - Automatically closes the file when done
// - Respects context cancellation during the write operation
//
// Parameters:
// - data: The byte slice to write to the file
//
// Returns:
// - Kleisli[string, []byte]: A function that takes a file path and returns a computation
// that writes the data and returns the written bytes on success
//
// Example:
//
// writeOp := WriteFile([]byte("Hello, World!"))
// result := writeOp("output.txt")(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Write error: %v", err) },
// func(data []byte) { log.Printf("Wrote %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// WriteFile(data) = Create >> WriteAll(data) >> Close
//
// See Also:
// - ReadFile: For reading file contents with automatic resource management
// - Create: For creating files without automatic writing
// - WriteAll: For writing to an already-open file handle
func WriteFile(data []byte) Kleisli[string, []byte] {
return F.Flow2(
Create,
WriteAll[*os.File](data),
)
}

View File

@@ -18,11 +18,16 @@ package file
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
R "github.com/IBM/fp-go/v2/context/readerioresult"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
J "github.com/IBM/fp-go/v2/json"
"github.com/stretchr/testify/assert"
)
type RecordType struct {
@@ -49,3 +54,267 @@ func ExampleReadFile() {
// Output:
// Right[string](Carsten)
}
func TestCreate(t *testing.T) {
ctx := context.Background()
t.Run("Success - creates new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_create.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was created
_, err := os.Stat(tempFile)
assert.NoError(t, err)
// Clean up file handle
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
})
t.Run("Success - truncates existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_truncate.txt")
// Create file with initial content
err := os.WriteFile(tempFile, []byte("initial content"), 0644)
assert.NoError(t, err)
// Create should truncate
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Close the file
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
// Verify file was truncated
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
// Try to create file in non-existent directory
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
createOp := Create(invalidPath)
result := createOp(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - file can be written to", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Write to the file
E.MonadFold(result,
func(err error) *os.File { t.Fatalf("Unexpected error: %v", err); return nil },
func(f *os.File) *os.File {
defer f.Close()
_, err := f.WriteString("test content")
assert.NoError(t, err)
return f
},
)
// Verify content was written
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, "test content", string(content))
})
t.Run("Context cancellation", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
createOp := Create(tempFile)
result := createOp(cancelCtx)()
// Note: File creation itself doesn't check context, but this tests the pattern
// In practice, context cancellation would affect subsequent operations
_ = result
})
}
func TestWriteFile(t *testing.T) {
ctx := context.Background()
t.Run("Success - writes data to new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
testData := []byte("Hello, World!")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify returned data
E.MonadFold(result,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, testData, content)
})
t.Run("Success - overwrites existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_overwrite.txt")
// Write initial content
err := os.WriteFile(tempFile, []byte("old content"), 0644)
assert.NoError(t, err)
// Overwrite with new content
newData := []byte("new content")
writeOp := WriteFile(newData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was overwritten
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, newData, content)
})
t.Run("Success - writes empty data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_empty.txt")
emptyData := []byte{}
writeOp := WriteFile(emptyData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file is empty
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Success - writes large data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_large.txt")
largeData := make([]byte, 1024*1024) // 1MB
for i := range largeData {
largeData[i] = byte(i % 256)
}
writeOp := WriteFile(largeData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, largeData, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(invalidPath)(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - writes binary data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_binary.bin")
binaryData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
writeOp := WriteFile(binaryData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify binary content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, binaryData, content)
})
t.Run("Integration - write then read", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_roundtrip.txt")
testData := []byte("Round trip test data")
// Write data
writeOp := WriteFile(testData)
writeResult := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(writeResult))
// Read data back
readOp := ReadFile(tempFile)
readResult := readOp(ctx)()
assert.True(t, E.IsRight(readResult))
// Verify data matches
E.MonadFold(readResult,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
})
t.Run("Composition with Map", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_compose.txt")
testData := []byte("test data")
// Write and transform result
pipeline := F.Pipe1(
WriteFile(testData)(tempFile),
R.Map(func(data []byte) int { return len(data) }),
)
result := pipeline(ctx)()
assert.True(t, E.IsRight(result))
E.MonadFold(result,
func(err error) int { t.Fatalf("Unexpected error: %v", err); return 0 },
func(length int) int {
assert.Equal(t, len(testData), length)
return length
},
)
})
t.Run("Context cancellation during write", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(cancelCtx)()
// Note: The actual write may complete before cancellation is checked
// This test verifies the pattern works with cancelled contexts
_ = result
})
}

View File

@@ -38,7 +38,7 @@ var (
)
// CreateTemp created a temp file with proper parametrization
func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
func CreateTemp(dir, pattern string) ReaderIOResult[*os.File] {
return F.Pipe2(
IOEF.CreateTemp(dir, pattern),
RIOE.FromIOEither[*os.File],
@@ -47,6 +47,6 @@ func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
}
// WithTempFile creates a temporary file, then invokes a callback to create a resource based on the file, then close and remove the temp file
func WithTempFile[A any](f func(*os.File) RIOE.ReaderIOResult[A]) RIOE.ReaderIOResult[A] {
func WithTempFile[A any](f Kleisli[*os.File, A]) ReaderIOResult[A] {
return RIOE.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
}

View File

@@ -16,7 +16,6 @@
package file
import (
"context"
"os"
"testing"
@@ -30,12 +29,12 @@ 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) {
res := WithTempFile(func(f *os.File) RIOE.ReaderIOResult[[]byte] {
res := WithTempFile(func(f *os.File) ReaderIOResult[[]byte] {
return F.Pipe2(
f,
onWriteAll[*os.File]([]byte("Carsten")),
@@ -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

@@ -0,0 +1,90 @@
// 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 file
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
)
type (
// ReaderIOResult represents a context-aware computation that performs side effects
// and can fail with an error. This is the main type used throughout the file package
// for all file operations.
//
// ReaderIOResult[A] is equivalent to:
// func(context.Context) func() Either[error, A]
//
// The computation:
// - Takes a context.Context for cancellation and timeouts
// - Performs side effects (IO operations)
// - Returns Either an error or a value of type A
//
// See Also:
// - readerioresult.ReaderIOResult: The underlying type definition
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// Void represents the absence of a meaningful value, similar to unit type in other languages.
// It is used when a function performs side effects but doesn't return a meaningful result.
//
// Void is typically used as the success type in operations like Close that perform
// an action but don't produce a useful value.
//
// Example:
// Close[*os.File](file) // Returns ReaderIOResult[Void]
//
// See Also:
// - function.Void: The underlying type definition
Void = function.Void
// Kleisli represents a Kleisli arrow for ReaderIOResult.
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
//
// Kleisli arrows are used for monadic composition, allowing you to chain operations
// that produce ReaderIOResults. They are particularly useful with Chain and Bind operations.
//
// Kleisli[A, B] is equivalent to:
// func(A) ReaderIOResult[B]
//
// Example:
// // A Kleisli arrow that reads a file given its path
// var readFileK Kleisli[string, []byte] = ReadFile
//
// See Also:
// - readerioresult.Kleisli: The underlying type definition
// - Operator: For transforming ReaderIOResults
Kleisli[A, B any] = readerioresult.Kleisli[A, B]
// Operator represents a transformation from one ReaderIOResult to another.
// This is useful for point-free style composition and building reusable transformations.
//
// Operator[A, B] is equivalent to:
// func(ReaderIOResult[A]) ReaderIOResult[B]
//
// Operators are used to transform computations without executing them, enabling
// powerful composition patterns.
//
// Example:
// // An operator that maps over file contents
// var toUpper Operator[[]byte, string] = Map(func(data []byte) string {
// return strings.ToUpper(string(data))
// })
//
// See Also:
// - readerioresult.Operator: The underlying type definition
// - Kleisli: For functions that produce ReaderIOResults
Operator[A, B any] = readerioresult.Operator[A, B]
)

View File

@@ -23,8 +23,8 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte] {
return func(w W) RIOE.ReaderIOResult[[]byte] {
func onWriteAll[W io.Writer](data []byte) Kleisli[W, []byte] {
return func(w W) ReaderIOResult[[]byte] {
return F.Pipe1(
RIOE.TryCatch(func(_ context.Context) func() ([]byte, error) {
return func() ([]byte, error) {
@@ -38,9 +38,9 @@ func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte]
}
// WriteAll uses a generator function to create a stream, writes data to it and closes it
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
func WriteAll[W io.WriteCloser](data []byte) Operator[W, []byte] {
onWrite := onWriteAll[W](data)
return func(onCreate RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
return func(onCreate ReaderIOResult[W]) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](
onCreate,
Close[W])(
@@ -50,7 +50,7 @@ func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]
}
// Write uses a generator function to create a stream, writes data to it and closes it
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOResult[W]) func(use func(W) RIOE.ReaderIOResult[R]) RIOE.ReaderIOResult[R] {
func Write[R any, W io.WriteCloser](acquire ReaderIOResult[W]) Kleisli[Kleisli[W, R], R] {
return RIOE.WithResource[R](
acquire,
Close[W])

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

@@ -19,6 +19,10 @@ import (
"context"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/result"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIOResult.
@@ -45,7 +49,7 @@ import (
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context], g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
@@ -70,6 +74,139 @@ func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFu
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
func Contramap[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
return Local[A](f)
}
func ContramapIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return LocalIOK[A](f)
}
// LocalIOK transforms the context using an IO-based function before passing it to a ReaderIOResult.
// This is similar to Local but the context transformation itself is wrapped in an IO effect.
//
// The function f takes a context and returns an IO effect that produces a ContextCancel
// (a pair of CancelFunc and the new Context). This allows the context transformation to
// perform side effects.
//
// # Use Cases
//
// This function is useful for sharing information via the Context that is computed through
// side effects that cannot fail, such as:
// - Generating unique request IDs or trace IDs
// - Recording timestamps or metrics
// - Logging context information
// - Computing derived values from existing context data
//
// The side effect is executed during the context transformation, and the resulting data is
// stored in the context for downstream computations to access.
//
// # Type Parameters
//
// - A: The success type (unchanged through the transformation)
//
// # Parameters
//
// - f: An IO-based Kleisli function that transforms the context
//
// # Returns
//
// - An Operator that applies the context transformation before executing the ReaderIOResult
//
// # Example Usage
//
// // Generate a request ID via side effect and add to context
// addRequestID := func(ctx context.Context) io.IO[ContextCancel] {
// return func() ContextCancel {
// // Side effect: generate unique ID
// requestID := uuid.New().String()
// // Share the ID via context
// newCtx := context.WithValue(ctx, "requestID", requestID)
// return pair.MakePair(func() {}, newCtx)
// }
// }
// adapted := LocalIOK[int](addRequestID)(computation)
//
// # See Also
//
// - Local: For pure context transformations
// - LocalIOResultK: For context transformations that can fail
//
//go:inline
func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return LocalIOResultK[A](function.Flow2(f, ioresult.FromIO))
}
// LocalIOResultK transforms the context using an IOResult-based function before passing it to a ReaderIOResult.
// This is similar to Local but the context transformation can fail with an error.
//
// The function f takes a context and returns an IOResult that produces either an error or a ContextCancel
// (a pair of CancelFunc and the new Context). If the transformation fails, the error is propagated
// and the original ReaderIOResult is not executed.
//
// # Use Cases
//
// This function is particularly useful for sharing information via the Context that is computed
// through side effects, such as:
// - Loading configuration from a file or database
// - Fetching authentication tokens from an external service
// - Computing derived values that require I/O operations
// - Validating and enriching context with data from external sources
//
// The side effect is executed during the context transformation, and the resulting data is
// stored in the context for downstream computations to access.
//
// # Type Parameters
//
// - A: The success type (unchanged through the transformation)
//
// # Parameters
//
// - f: An IOResult-based Kleisli function that transforms the context and may fail
//
// # Returns
//
// - An Operator that applies the context transformation before executing the ReaderIOResult
//
// # Example Usage
//
// // Load configuration via side effect and add to context
// loadConfig := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
// return func() result.Result[ContextCancel] {
// // Side effect: read from file system
// config, err := os.ReadFile("config.json")
// if err != nil {
// return result.Left[ContextCancel](err)
// }
// // Share the loaded config via context
// newCtx := context.WithValue(ctx, "config", config)
// return result.Of(pair.MakePair(func() {}, newCtx))
// }
// }
// adapted := LocalIOResultK[int](loadConfig)(computation)
//
// # See Also
//
// - Local: For pure context transformations
// - LocalIOK: For context transformations with side effects that cannot fail
//
//go:inline
func LocalIOResultK[A any](f ioresult.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
p, err := result.Unwrap(f(ctx)())
if err != nil {
return result.Left[A](err)
}
// unwrap
otherCancel, otherCtx := pair.Unpack(p)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}

View File

@@ -20,6 +20,10 @@ import (
"strconv"
"testing"
"github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -36,14 +40,14 @@ func TestPromapBasic(t *testing.T) {
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
toString := strconv.Itoa
adapted := Promap(addKey, toString)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, R.Of("42"), result)
})
@@ -61,13 +65,13 @@ func TestContramapBasic(t *testing.T) {
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
adapted := Contramap[int](addKey)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, R.Of(100), result)
})
@@ -85,14 +89,322 @@ func TestLocalBasic(t *testing.T) {
}
}
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
addUser := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "user", "Alice")
return newCtx, func() {}
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
adapted := Local[string](addUser)(getValue)
result := adapted(context.Background())()
result := adapted(t.Context())()
assert.Equal(t, R.Of("Alice"), result)
})
}
// TestLocalIOK_Success tests LocalIOK with successful context transformation
func TestLocalIOK_Success(t *testing.T) {
t.Run("transforms context with IO effect", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("user"); v != nil {
return R.Of(v.(string))
}
return R.Of("unknown")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Bob")
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := LocalIOK[string](addUser)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of("Bob"), result)
})
t.Run("preserves original value type", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("count"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addCount := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "count", 42)
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := LocalIOK[int](addCount)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(42), result)
})
}
// TestLocalIOK_CancelledContext tests LocalIOK with cancelled context
func TestLocalIOK_CancelledContext(t *testing.T) {
t.Run("returns error when context is cancelled", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Charlie")
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
adapted := LocalIOK[string](addUser)(getValue)
result := adapted(ctx)()
assert.True(t, R.IsLeft(result))
})
}
// TestLocalIOK_CancelFuncCalled tests that CancelFunc is properly called
func TestLocalIOK_CancelFuncCalled(t *testing.T) {
t.Run("calls cancel function after execution", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Dave")
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
return pair.MakePair(cancelFunc, newCtx)
}
}
adapted := LocalIOK[string](addUser)(getValue)
_ = adapted(t.Context())()
assert.True(t, cancelCalled, "cancel function should be called")
})
}
// TestLocalIOResultK_Success tests LocalIOResultK with successful context transformation
func TestLocalIOResultK_Success(t *testing.T) {
t.Run("transforms context with IOResult effect", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("role"); v != nil {
return R.Of(v.(string))
}
return R.Of("guest")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "admin")
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
adapted := LocalIOResultK[string](addRole)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of("admin"), result)
})
t.Run("preserves original value type", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("score"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addScore := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "score", 100)
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
adapted := LocalIOResultK[int](addScore)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(100), result)
})
}
// TestLocalIOResultK_Failure tests LocalIOResultK with failed context transformation
func TestLocalIOResultK_Failure(t *testing.T) {
t.Run("propagates transformation error", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
result := adapted(t.Context())()
assert.True(t, R.IsLeft(result))
_, err := R.UnwrapError(result)
assert.Equal(t, assert.AnError, err)
})
t.Run("does not execute original computation on transformation failure", func(t *testing.T) {
executed := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
executed = true
return R.Of("should not execute")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
_ = adapted(t.Context())()
assert.False(t, executed, "original computation should not execute")
})
}
// TestLocalIOResultK_CancelledContext tests LocalIOResultK with cancelled context
func TestLocalIOResultK_CancelledContext(t *testing.T) {
t.Run("returns error when context is cancelled", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "user")
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
adapted := LocalIOResultK[string](addRole)(getValue)
result := adapted(ctx)()
assert.True(t, R.IsLeft(result))
})
}
// TestLocalIOResultK_CancelFuncCalled tests that CancelFunc is properly called
func TestLocalIOResultK_CancelFuncCalled(t *testing.T) {
t.Run("calls cancel function after successful execution", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "user")
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
return R.Of(pair.MakePair(cancelFunc, newCtx))
}
}
adapted := LocalIOResultK[string](addRole)(getValue)
_ = adapted(t.Context())()
assert.True(t, cancelCalled, "cancel function should be called")
})
t.Run("does not call cancel function on transformation failure", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
_ = cancelFunc // avoid unused warning
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
_ = adapted(t.Context())()
assert.False(t, cancelCalled, "cancel function should not be called on failure")
})
}
// TestLocalIOResultK_Integration tests integration with other operations
func TestLocalIOResultK_Integration(t *testing.T) {
t.Run("composes with Map", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("value"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addValue := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "value", 10)
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
double := func(x int) int { return x * 2 }
adapted := F.Flow2(
LocalIOResultK[int](addValue),
Map(double),
)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(20), result)
})
}

View File

@@ -28,6 +28,7 @@ import (
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
@@ -222,7 +223,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 +453,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 +801,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 +896,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)
}
@@ -1041,7 +1042,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:
//
@@ -1054,14 +1055,14 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
func Local[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
otherCtx, otherCancel := f(ctx)
otherCancel, otherCtx := pair.Unpack(f(ctx))
defer otherCancel()
return rr(otherCtx)()
}
@@ -1112,7 +1113,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:
//
@@ -1121,11 +1122,12 @@ 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)
})
return Local[A](
func(ctx context.Context) ContextCancel {
return pairFromContextCancel(context.WithTimeout(ctx, timeout))
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIOResult computation.
@@ -1173,12 +1175,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)
@@ -1188,7 +1190,7 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// )
// value, err := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
return Local[A](func(ctx context.Context) ContextCancel {
return pairFromContextCancel(context.WithDeadline(ctx, deadline))
})
}

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"
@@ -54,6 +55,10 @@ type (
// Either[A] is equivalent to Either[error, A] from the either package.
Either[A any] = either.Either[error, A]
// Result represents a computation that can either succeed with a value of type A
// or fail with an error. This is an alias for result.Result[A].
//
// Result[A] is equivalent to Either[error, A]
Result[A any] = result.Result[A]
// Lazy represents a deferred computation that produces a value of type A when executed.
@@ -72,6 +77,10 @@ type (
// IOEither[A] is equivalent to func() Either[error, A]
IOEither[A any] = ioeither.IOEither[error, A]
// IOResult represents a side-effectful computation that can fail with an error.
// This combines IO (side effects) with Result (error handling).
//
// IOResult[A] is equivalent to func() Result[A]
IOResult[A any] = ioresult.IOResult[A]
// Reader represents a computation that depends on a context of type R.
@@ -113,10 +122,17 @@ 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]
// Kleisli represents a Kleisli arrow for ReaderIOResult.
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
//
// Kleisli arrows are used for monadic composition, allowing you to chain operations
// that produce ReaderIOResults. They are particularly useful with Chain operations.
//
// Kleisli[A, B] is equivalent to func(A) ReaderIOResult[B]
Kleisli[A, B any] = reader.Reader[A, ReaderIOResult[B]]
// Operator represents a transformation from one ReaderIOResult to another.
@@ -132,24 +148,76 @@ type (
// result := toUpper(computation)
Operator[A, B any] = Kleisli[ReaderIOResult[A], B]
ReaderResult[A any] = readerresult.ReaderResult[A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// ReaderResult represents a context-dependent computation that can fail.
// This is specialized to use context.Context as the context type.
//
// ReaderResult[A] is equivalent to func(context.Context) Result[A]
ReaderResult[A any] = readerresult.ReaderResult[A]
// ReaderEither represents a context-dependent computation that can fail.
// It takes a context of type R and produces an Either[E, A].
//
// ReaderEither[R, E, A] is equivalent to func(R) Either[E, A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
// ReaderOption represents a context-dependent computation that may not produce a value.
// It takes a context of type R and produces an Option[A].
//
// ReaderOption[R, A] is equivalent to func(R) Option[A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// Endomorphism represents a function from a type to itself.
// It is used for transformations that preserve the type.
//
// Endomorphism[A] is equivalent to func(A) A
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Consumer represents a function that consumes a value without producing a result.
// It is used for side effects like logging or updating state.
//
// Consumer[A] is equivalent to func(A)
Consumer[A any] = consumer.Consumer[A]
// Prism represents an optic for working with sum types (tagged unions).
// It provides a way to focus on a specific variant of a sum type.
Prism[S, T any] = prism.Prism[S, T]
Lens[S, T any] = lens.Lens[S, T]
// Lens represents an optic for working with product types (records/structs).
// It provides a way to focus on a specific field of a product type.
Lens[S, T any] = lens.Lens[S, T]
// Trampoline represents a computation that can be executed in a stack-safe manner.
// It is used for tail-recursive computations that would otherwise overflow the stack.
Trampoline[B, L any] = tailrec.Trampoline[B, L]
// Predicate represents a function that tests a value of type A.
// It returns true if the value satisfies the predicate, false otherwise.
//
// Predicate[A] is equivalent to func(A) bool
Predicate[A any] = predicate.Predicate[A]
// Pair represents a tuple of two values of types A and B.
// It is used to group two related values together.
Pair[A, B any] = pair.Pair[A, B]
// IORef represents a mutable reference that can be safely accessed in IO computations.
// It provides thread-safe read and write operations.
IORef[A any] = ioref.IORef[A]
// State represents a stateful computation that transforms a state of type S
// and produces a value of type A.
//
// State[S, A] is equivalent to func(S) Pair[A, S]
State[S, A any] = state.State[S, A]
// Void represents the absence of a value, similar to unit type in other languages.
// It is used when a function performs side effects but doesn't return a meaningful value.
Void = function.Void
// ContextCancel represents a pair of a cancel function and a context.
// It is used in operations that create new contexts with cancellation capabilities.
//
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
)

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)
}

View File

@@ -0,0 +1,396 @@
// 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 (
"context"
"errors"
"testing"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type Resource struct {
id string
acquired bool
released bool
}
func TestBracketSuccessPath(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
// Acquire resource
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.acquired = true
return result.Of(resource)
}
}
}
// Use resource successfully
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of("result from " + r.id)
}
}
}
}
// Release resource
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of("result from res1"), outcome)
assert.True(t, resource.acquired, "Resource should be acquired")
assert.True(t, resource.released, "Resource should be released")
}
func TestBracketUseFailure(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
useErr := errors.New("use failed")
// Acquire resource
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.acquired = true
return result.Of(resource)
}
}
}
// Use resource with failure
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Left[string](useErr)
}
}
}
}
// Release resource (should still be called)
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Left[string](useErr), outcome)
assert.True(t, resource.acquired, "Resource should be acquired")
assert.True(t, resource.released, "Resource should be released even on failure")
}
func TestBracketAcquireFailure(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
acquireErr := errors.New("acquire failed")
useCalled := false
releaseCalled := false
// Acquire resource fails
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
return result.Left[*Resource](acquireErr)
}
}
}
// Use should not be called
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
useCalled = true
return result.Of("should not reach here")
}
}
}
}
// Release should not be called
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
releaseCalled = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Left[string](acquireErr), outcome)
assert.False(t, resource.acquired, "Resource should not be acquired")
assert.False(t, useCalled, "Use should not be called when acquire fails")
assert.False(t, releaseCalled, "Release should not be called when acquire fails")
}
func TestBracketReleaseReceivesResult(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
var capturedResult Result[string]
// Acquire resource
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.acquired = true
return result.Of(resource)
}
}
}
// Use resource
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of("use result")
}
}
}
}
// Release captures the result
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
capturedResult = res
r.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of("use result"), outcome)
assert.Equal(t, result.Of("use result"), capturedResult)
assert.True(t, resource.released, "Resource should be released")
}
func TestBracketWithContextAccess(t *testing.T) {
cfg := AppConfig{DatabaseURL: "production-db", LogLevel: "debug"}
ctx := t.Context()
resource := &Resource{id: "res1"}
// Acquire uses outer context
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.id = c.DatabaseURL + "-resource"
resource.acquired = true
return result.Of(resource)
}
}
}
// Use uses both contexts
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
res := r.id + " with log level " + c.LogLevel
return result.Of(res)
}
}
}
}
// Release uses both contexts
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
assert.True(t, result.IsRight(outcome))
assert.True(t, resource.acquired)
assert.True(t, resource.released)
assert.Equal(t, "production-db-resource", resource.id)
}
func TestBracketMultipleResources(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource1 := &Resource{id: "res1"}
resource2 := &Resource{id: "res2"}
// Acquire first resource
acquire1 := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource1.acquired = true
return result.Of(resource1)
}
}
}
// Use first resource to acquire second
use1 := func(r1 *Resource) ReaderReaderIOResult[AppConfig, string] {
// Nested bracket for second resource
acquire2 := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource2.acquired = true
return result.Of(resource2)
}
}
}
use2 := func(r2 *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of(r1.id + " and " + r2.id)
}
}
}
}
release2 := func(r2 *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r2.released = true
return result.Of[any](nil)
}
}
}
}
return Bracket(acquire2, use2, release2)
}
release1 := func(r1 *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
r1.released = true
return result.Of[any](nil)
}
}
}
}
computation := Bracket(acquire1, use1, release1)
outcome := computation(cfg)(ctx)()
assert.Equal(t, result.Of("res1 and res2"), outcome)
assert.True(t, resource1.acquired && resource1.released, "Resource 1 should be acquired and released")
assert.True(t, resource2.acquired && resource2.released, "Resource 2 should be acquired and released")
}
func TestBracketReleaseErrorDoesNotAffectResult(t *testing.T) {
cfg := defaultConfig
ctx := t.Context()
resource := &Resource{id: "res1"}
releaseErr := errors.New("release failed")
// Acquire resource
acquire := func(c AppConfig) ReaderIOResult[context.Context, *Resource] {
return func(ctx context.Context) IOResult[*Resource] {
return func() Result[*Resource] {
resource.acquired = true
return result.Of(resource)
}
}
}
// Use resource successfully
use := func(r *Resource) ReaderReaderIOResult[AppConfig, string] {
return func(c AppConfig) ReaderIOResult[context.Context, string] {
return func(ctx context.Context) IOResult[string] {
return func() Result[string] {
return result.Of("use success")
}
}
}
}
// Release fails but shouldn't affect the result
release := func(r *Resource, res Result[string]) ReaderReaderIOResult[AppConfig, any] {
return func(c AppConfig) ReaderIOResult[context.Context, any] {
return func(ctx context.Context) IOResult[any] {
return func() Result[any] {
return result.Left[any](releaseErr)
}
}
}
}
computation := Bracket(acquire, use, release)
outcome := computation(cfg)(ctx)()
// The use result should be returned, not the release error
// (This behavior depends on the Bracket implementation)
assert.True(t, result.IsRight(outcome) || result.IsLeft(outcome))
assert.True(t, resource.acquired)
}

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