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

Compare commits

...

56 Commits

Author SHA1 Message Date
Dr. Carsten Leue
a4e790ac3d fix: improve bind
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 13:05:55 +01:00
Dr. Carsten Leue
1af6501cd8 fix: add bind variations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 12:42:14 +01:00
Dr. Carsten Leue
600521b220 fix: refactor
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-11 11:01:49 +01:00
Dr. Carsten Leue
62fcd186a3 fix: introcuce readeioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-10 18:44:14 +01:00
Dr. Carsten Leue
2db7e83651 fix: introduce IOResult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-10 16:26:52 +01:00
Dr. Carsten Leue
8d92df83ad fix: introduce Result type for convenience
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-10 12:08:50 +01:00
Dr. Carsten Leue
0c742b81e6 fix: better performance for either
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-08 10:01:14 +01:00
Dr. Carsten Leue
a1d6c94b15 fix: run benchmarks
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-08 09:31:48 +01:00
Dr. Carsten Leue
e4e28a6556 fix: more Kleisli simplified types
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 17:53:47 +01:00
Dr. Carsten Leue
7e7cc06f11 fix: add more Kleisli definitions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 17:42:54 +01:00
Dr. Carsten Leue
54d5dbd04a fix: more tests for iso and prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 17:31:27 +01:00
Dr. Carsten Leue
51adce0c95 fix: better package import
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 16:15:16 +01:00
Dr. Carsten Leue
aa5e908810 fix: introduce Kleisli type
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 14:35:46 +01:00
Dr. Carsten Leue
b3bd5e9ad3 fix: bind docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 16:18:15 +01:00
Dr. Carsten Leue
178df09ff7 fix: document ApS
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 16:08:35 +01:00
Dr. Carsten Leue
92eb9715bd fix: implement some useful prisms
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 13:53:02 +01:00
Dr. Carsten Leue
41ebb04ae0 fix: upload coverage to coverall
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 11:45:01 +01:00
Dr. Carsten Leue
b2705e3adf fix: disable to version updates
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 11:39:17 +01:00
renovate[bot]
b232183e47 chore(config): migrate config renovate.json (#143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 11:26:23 +01:00
Dr. Carsten Leue
0f9f89f16d doc: improve documentation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 11:20:50 +01:00
Dr. Carsten Leue
0d3a8634b1 fix: add inline annotations
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 10:54:24 +01:00
Dr. Carsten Leue
56c8f1b034 fix: fix build
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 09:57:58 +01:00
Dr. Carsten Leue
bad86cd769 fix: try to fix build
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 09:50:03 +01:00
Dr. Carsten Leue
d0c5f32111 fix: add go 1.25 to build matrix
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 09:43:27 +01:00
renovate[bot]
77745c1348 fix(deps): update go dependencies (#142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-06 09:39:27 +01:00
李鑫
2c96cef500 feat: add array.MonadReduce() function (#132)
Signed-off-by: lixin <lixin@dustess.com>
Co-authored-by: lixin <lixin@dustess.com>
2025-11-06 09:28:20 +01:00
Carsten Leue
3385c705dc Implement v2 using type aliases (#141)
* fix: initial checkin of v2

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

* fix: slowly migrate IO

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

* fix: migrate MonadTraverseArray and TraverseArray

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

* fix: migrate traversal

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

* fix: complete migration of IO

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

* fix: migrate ioeither

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

* fix: refactorY

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

* fix: next step in migration

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

* fix: adjust IO generation code

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

* fix: get rid of more IO methods

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

* fix: get rid of more IO

* fix: convert iooption

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

* fix: convert reader

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

* fix: convert a bit of reader

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

* fix: new build script

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

* fix: cleanup

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

* fix: reformat

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

* fix: simplify

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

* fix: some cleanup

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

* fix: adjust Pair to Haskell semantic

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

* fix: documentation and testcases

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

* fix: some performance optimizations

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

* fix: remove coverage

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

* fix: better doc

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

---------

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 09:27:00 +01:00
ibm-mend-app[bot]
7874859c4b Add .whitesource configuration file (#140)
Co-authored-by: ibm-mend-app[bot] <142626574+ibm-mend-app[bot]@users.noreply.github.com>
2025-11-06 09:22:43 +01:00
renovate[bot]
25e3d1d85c fix(deps): update module github.com/stretchr/testify to v1.11.1 (#139)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 22:40:53 +00:00
renovate[bot]
d7ff994fb7 fix(deps): update module github.com/stretchr/testify to v1.11.0 (#138)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 01:44:33 +00:00
renovate[bot]
1cdca552b2 chore(deps): update actions/checkout action to v4.3.0 (#137)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-11 18:38:27 +00:00
renovate[bot]
73480ca030 fix(deps): update module github.com/urfave/cli/v2 to v2.27.7 (#136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-14 10:26:20 +00:00
renovate[bot]
734e2b0055 chore(deps): update actions/setup-node action to v4.4.0 (#134)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 10:54:36 +00:00
renovate[bot]
4c28859e89 chore(deps): update actions/setup-node action to v4.3.0 (#131)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-17 06:44:51 +00:00
renovate[bot]
a516849c07 fix(deps): update module github.com/urfave/cli/v2 to v2.27.6 (#130)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 22:50:57 +00:00
sarabande
29200d34dc docs(iterator): update Cycle function documentation (#128) 2025-03-02 00:21:41 +01:00
renovate[bot]
7a3989989b chore(deps): update actions/setup-node action to v4.2.0 (#127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 22:04:25 +00:00
renovate[bot]
6a6d53f025 fix(deps): update module github.com/stretchr/testify to v1.10.0 (#126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-23 15:33:28 +00:00
renovate[bot]
078da752cd chore(deps): update actions/setup-node action to v4.1.0 2024-10-24 20:12:43 +00:00
renovate[bot]
1a489fde27 chore(deps): update actions/checkout action to v4.2.2 2024-10-23 19:31:36 +00:00
renovate[bot]
a135b2acae fix(deps): update module github.com/urfave/cli/v2 to v2.27.5 2024-10-13 20:09:16 +00:00
renovate[bot]
9e9dfa1f5f chore(deps): update actions/checkout action to v4.2.1 2024-10-07 22:39:01 +00:00
renovate[bot]
dd87ea12b3 chore(deps): update actions/checkout action to v4.2.0 2024-09-25 21:29:08 +00:00
renovate[bot]
5fc0d18c97 chore(deps): update actions/setup-node action to v4.0.4 2024-09-19 20:14:45 +00:00
CRaLFa
76c1297576 fix typos in README and comment (#119)
Signed-off-by: CRaLFa <minami.smd@gmail.com>
2024-09-12 09:40:19 +02:00
Dr. Carsten Leue
68aeb4c725 fix: script
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-08-27 16:57:59 +02:00
Dr. Carsten Leue
53f3fa1828 fix: semantic release
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-08-27 16:43:21 +02:00
Dr. Carsten Leue
97e1e4d92d fix: add test for go 1.23
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-08-27 16:39:52 +02:00
Dr. Carsten Leue
ec57d5cd4a fix: go mod tidy
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-08-27 16:37:22 +02:00
renovate[bot]
0ae5b43724 fix(deps): update module github.com/urfave/cli/v2 to v2.27.4 2024-08-11 19:12:08 +00:00
renovate[bot]
e73e14c0ae fix(deps): update module github.com/urfave/cli/v2 to v2.27.3 2024-07-25 03:34:59 +00:00
renovate[bot]
325bc376f9 chore(deps): update actions/setup-node action to v4.0.3 2024-07-09 20:06:20 +00:00
Dr. Carsten Leue
f646ace9fe chore: git mod tidy
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-06-24 09:24:15 +02:00
Laglangyue
9f8161fbc1 chore: ignore fp-go file for unix machine (#115)
Signed-off-by: tangjiafu <tangjiafu@apache.org>
Co-authored-by: tangjiafu <tangjiafu@apache.org>
2024-06-24 09:22:40 +02:00
renovate[bot]
1c4f2c0403 chore(deps): update actions/checkout action to v4.1.7 2024-06-13 02:39:23 +00:00
Dr. Carsten Leue
3b3b80aed0 fix: some more tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2024-05-27 11:54:04 +02:00
907 changed files with 128269 additions and 187 deletions

View File

@@ -4,9 +4,7 @@ on:
push:
branches:
- main
pull_request:
workflow_dispatch:
inputs:
dryRun:
@@ -15,35 +13,109 @@ on:
required: false
env:
# Currently no way to detect automatically
DEFAULT_BRANCH: main
GO_VERSION: 1.21.6 # renovate: datasource=golang-version depName=golang
NODE_VERSION: 20
LATEST_GO_VERSION: 1.25.2 # renovate: datasource=golang-version depName=golang
NODE_VERSION: 24
DRY_RUN: true
jobs:
build:
build-v1:
name: Build v1 (Go ${{ matrix.go-version }})
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.20.x', '1.21.x', '1.22.x']
go-version: ['1.20.x', '1.21.x', '1.22.x', '1.23.x', '1.24.x', '1.25.x']
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
- name: Set up go ${{ matrix.go-version }}
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
-
name: Tests
cache: true # Enable Go module caching
- name: Run tests
run: |
go mod tidy
go test -v ./...
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: ./coverage.txt
flag-name: v1-go-${{ matrix.go-version }}
parallel: true
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v5
# with:
# file: ./coverage.txt
# flags: v1,go-${{ matrix.go-version }}
# name: v1-go-${{ matrix.go-version }}
# fail_ci_if_error: false
# env:
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
build-v2:
name: Build v2 (Go ${{ matrix.go-version }})
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.24.x', '1.25.x']
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache: true # Enable Go module caching
- name: Run tests
run: |
cd v2
go mod tidy
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
file: ./v2/coverage.txt
flag-name: v2-go-${{ matrix.go-version }}
parallel: true
# - name: Upload coverage to Codecov
# uses: codecov/codecov-action@v5
# with:
# file: ./v2/coverage.txt
# flags: v2,go-${{ matrix.go-version }}
# name: v2-go-${{ matrix.go-version }}
# fail_ci_if_error: false
# env:
# CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
coveralls-finish:
name: Finish Coveralls
needs:
- build-v1
- build-v2
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
release:
needs: [build]
name: Release
needs:
- build-v1
- build-v2
if: github.repository == 'IBM/fp-go' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
@@ -51,38 +123,37 @@ jobs:
contents: write
issues: write
pull-requests: write
steps:
# full checkout for semantic-release
- name: Full checkout
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
fetch-depth: 0
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Set up go ${{env.GO_VERSION}}
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{env.GO_VERSION}}
go-version: ${{ env.LATEST_GO_VERSION }}
cache: true # Enable Go module caching
# The dry-run evaluation is only made for non PR events. Manual trigger w/dryRun true, main branch and any tagged branches will set DRY run to false
- name: Check dry run
- name: Determine release mode
id: release-mode
run: |
if [[ "${{github.event_name}}" == "workflow_dispatch" && "${{ github.event.inputs.dryRun }}" != "true" ]]; then
echo "DRY_RUN=false" >> $GITHUB_ENV
elif [[ "${{github.ref}}" == "refs/heads/${{env.DEFAULT_BRANCH}}" ]]; then
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.dryRun }}" != "true" ]]; then
echo "DRY_RUN=false" >> $GITHUB_ENV
elif [[ "${{github.ref}}" =~ ^refs/heads/v[0-9]+(\.[0-9]+)?$ ]]; then
elif [[ "${{ github.ref }}" == "refs/heads/${{ env.DEFAULT_BRANCH }}" ]]; then
echo "DRY_RUN=false" >> $GITHUB_ENV
elif [[ "${{ github.ref }}" =~ ^refs/heads/v[0-9]+(\.[0-9]+)?$ ]]; then
echo "DRY_RUN=false" >> $GITHUB_ENV
fi
- name: Semantic Release
- name: Run semantic release
run: |
npx -p "conventional-changelog-conventionalcommits@<8" -p semantic-release semantic-release --dry-run ${{env.DRY_RUN}}
npx -p conventional-changelog-conventionalcommits -p semantic-release semantic-release --dry-run ${{ env.DRY_RUN }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored
View File

@@ -1,4 +1,6 @@
fp-go.exe
fp-go
main.exe
build/
.idea
.idea
*.exe

3
.whitesource Normal file
View File

@@ -0,0 +1,3 @@
{
"settingsInheritedFrom": "ibm-mend-config/mend-config@main"
}

347
README.md
View File

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

View File

@@ -141,6 +141,10 @@ func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B {
return current
}
func MonadReduce[A, B any](fa []A, f func(B, A) B, initial B) B {
return G.MonadReduce(fa, f, initial)
}
func Reduce[A, B any](f func(B, A) B, initial B) func([]A) B {
return G.Reduce[[]A](f, initial)
}

View File

@@ -21,8 +21,8 @@ import (
type (
either struct {
isLeft bool
value any
isLeft bool
}
// Either defines a data structure that logically holds either an E or an A. The flag discriminates the cases
@@ -31,7 +31,7 @@ type (
// String prints some debug info for the object
//
// go:noinline
//go:noinline
func eitherString(s *either) string {
if s.isLeft {
return fmt.Sprintf("Left[%T](%v)", s.value, s.value)
@@ -41,7 +41,7 @@ func eitherString(s *either) string {
// Format prints some debug info for the object
//
// go:noinline
//go:noinline
func eitherFormat(e *either, f fmt.State, c rune) {
switch c {
case 's':
@@ -73,12 +73,12 @@ func IsRight[E, A any](val Either[E, A]) bool {
// Left creates a new instance of an [Either] representing the left value.
func Left[A, E any](value E) Either[E, A] {
return Either[E, A]{true, value}
return Either[E, A]{value, true}
}
// Right creates a new instance of an [Either] representing the right value.
func Right[E, A any](value A) Either[E, A] {
return Either[E, A]{false, value}
return Either[E, A]{value, false}
}
// MonadFold extracts the values from an [Either] by invoking the [onLeft] callback or the [onRight] callback depending on the case
@@ -94,8 +94,7 @@ func Unwrap[E, A any](ma Either[E, A]) (A, E) {
if ma.isLeft {
var a A
return a, ma.value.(E)
} else {
var e E
return ma.value.(A), e
}
var e E
return ma.value.(A), e
}

8
go.mod
View File

@@ -3,15 +3,15 @@ module github.com/IBM/fp-go
go 1.20
require (
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.2
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v2 v2.27.7
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

26
go.sum
View File

@@ -1,5 +1,9 @@
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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -8,12 +12,22 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
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/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
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=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -17,6 +17,37 @@ package array
func Slice[GA ~[]A, A any](low, high int) func(as GA) GA {
return func(as GA) GA {
length := len(as)
// Handle negative indices - count backward from the end
if low < 0 {
low = length + low
if low < 0 {
low = 0
}
}
if high < 0 {
high = length + high
if high < 0 {
high = 0
}
}
// Start index > array length: return empty array
if low > length {
return Empty[GA, A]()
}
// End index > array length: slice to the end
if high > length {
high = length
}
// Start >= end: return empty array
if low >= high {
return Empty[GA, A]()
}
return as[low:high]
}
}

View File

@@ -13,7 +13,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package file
package bracket
import (
F "github.com/IBM/fp-go/function"

View File

@@ -19,8 +19,7 @@ import (
G "github.com/IBM/fp-go/iterator/stateless/generic"
)
// DropWhile creates an [Iterator] that drops elements from the [Iterator] as long as the predicate is true; afterwards, returns every element.
// Note, the [Iterator] does not produce any output until the predicate first becomes false
// Cycle creates an [Iterator] containing an [Iterator] repeated an infinite number of times.
func Cycle[U any](ma Iterator[U]) Iterator[U] {
return G.Cycle[Iterator[U]](ma)
}

View File

@@ -35,7 +35,7 @@ type Option[A any] struct {
// optString prints some debug info for the object
//
// go:noinline
//go:noinline
func optString(isSome bool, value any) string {
if isSome {
return fmt.Sprintf("Some[%T](%v)", value, value)
@@ -45,7 +45,7 @@ func optString(isSome bool, value any) string {
// optFormat prints some debug info for the object
//
// go:noinline
//go:noinline
func optFormat(isSome bool, value any, f fmt.State, c rune) {
switch c {
case 's':
@@ -78,7 +78,7 @@ func (s Option[A]) MarshalJSON() ([]byte, error) {
// optUnmarshalJSON unmarshals the [Option] from a JSON string
//
// go:noinline
//go:noinline
func optUnmarshalJSON(isSome *bool, value any, data []byte) error {
// decode the value
if bytes.Equal(data, jsonNull) {

View File

@@ -64,7 +64,7 @@ func Reverse[T any](o Ord[T]) Ord[T] {
}, o.Equals)
}
// Contramap creates an odering under a transformation function
// Contramap creates an ordering under a transformation function
func Contramap[A, B any](f func(B) A) func(Ord[A]) Ord[B] {
return func(o Ord[A]) Ord[B] {
return MakeOrd(func(x, y B) int {

View File

@@ -34,14 +34,14 @@ type (
// String prints some debug info for the object
//
// go:noinline
//go:noinline
func pairString(s *pair) string {
return fmt.Sprintf("Pair[%T, %T](%v, %v)", s.h, s.t, s.h, s.t)
}
// Format prints some debug info for the object
//
// go:noinline
//go:noinline
func pairFormat(e *pair, f fmt.State, c rune) {
switch c {
case 's':

View File

@@ -23,3 +23,8 @@ import (
func Eq[K comparable, V any](e E.Eq[V]) E.Eq[map[K]V] {
return G.Eq[map[K]V, K, V](e)
}
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
func FromStrictEquals[K, V comparable]() E.Eq[map[K]V] {
return G.FromStrictEquals[map[K]V]()
}

48
record/eq_test.go Normal file
View File

@@ -0,0 +1,48 @@
// Copyright (c) 2024 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package record
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFromStrictEquals(t *testing.T) {
m1 := map[string]string{
"a": "A",
"b": "B",
}
m2 := map[string]string{
"a": "A",
"b": "C",
}
m3 := map[string]string{
"a": "A",
"b": "B",
}
m4 := map[string]string{
"a": "A",
"b": "B",
"c": "C",
}
e := FromStrictEquals[string, string]()
assert.True(t, e.Equals(m1, m1))
assert.True(t, e.Equals(m1, m3))
assert.False(t, e.Equals(m1, m2))
assert.False(t, e.Equals(m1, m4))
}

View File

@@ -37,3 +37,8 @@ func Eq[M ~map[K]V, K comparable, V any](e E.Eq[V]) E.Eq[M] {
return equals(left, right, eq)
})
}
// FromStrictEquals constructs an [EQ.Eq] from the canonical comparison function
func FromStrictEquals[M ~map[K]V, K, V comparable]() E.Eq[M] {
return Eq[M](E.FromStrictEquals[V]())
}

View File

@@ -54,3 +54,69 @@ func TestUnionMonoid(t *testing.T) {
assert.Equal(t, res, m.Concat(x, y))
}
func TestUnionFirstMonoid(t *testing.T) {
m := UnionFirstMonoid[string, string]()
e := Empty[string, string]()
x := map[string]string{
"a": "a1",
"b": "b1",
"c": "c1",
}
y := map[string]string{
"b": "b2",
"c": "c2",
"d": "d2",
}
res := map[string]string{
"a": "a1",
"b": "b1",
"c": "c1",
"d": "d2",
}
assert.Equal(t, x, m.Concat(x, m.Empty()))
assert.Equal(t, x, m.Concat(m.Empty(), x))
assert.Equal(t, x, m.Concat(x, e))
assert.Equal(t, x, m.Concat(e, x))
assert.Equal(t, res, m.Concat(x, y))
}
func TestUnionLastMonoid(t *testing.T) {
m := UnionLastMonoid[string, string]()
e := Empty[string, string]()
x := map[string]string{
"a": "a1",
"b": "b1",
"c": "c1",
}
y := map[string]string{
"b": "b2",
"c": "c2",
"d": "d2",
}
res := map[string]string{
"a": "a1",
"b": "b2",
"c": "c2",
"d": "d2",
}
assert.Equal(t, x, m.Concat(x, m.Empty()))
assert.Equal(t, x, m.Concat(m.Empty(), x))
assert.Equal(t, x, m.Concat(x, e))
assert.Equal(t, x, m.Concat(e, x))
assert.Equal(t, res, m.Concat(x, y))
}

View File

@@ -176,3 +176,25 @@ func TestFromArrayMap(t *testing.T) {
"C": "C",
}, res2)
}
func TestEmpty(t *testing.T) {
nonEmpty := map[string]string{
"a": "A",
"b": "B",
}
empty := Empty[string, string]()
assert.True(t, IsEmpty(empty))
assert.False(t, IsEmpty(nonEmpty))
assert.False(t, IsNonEmpty(empty))
assert.True(t, IsNonEmpty(nonEmpty))
}
func TestHas(t *testing.T) {
nonEmpty := map[string]string{
"a": "A",
"b": "B",
}
assert.True(t, Has("a", nonEmpty))
assert.False(t, Has("c", nonEmpty))
}

View File

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

319
v2/README.md Normal file
View File

@@ -0,0 +1,319 @@
# fp-go V2: Enhanced Functional Programming for Go 1.24+
[![Go Reference](https://pkg.go.dev/badge/github.com/IBM/fp-go/v2.svg)](https://pkg.go.dev/github.com/IBM/fp-go/v2)
[![Coverage Status](https://coveralls.io/repos/github/IBM/fp-go/badge.svg?branch=main&flag=v2)](https://coveralls.io/github/IBM/fp-go?branch=main)
Version 2 of fp-go leverages [generic type aliases](https://github.com/golang/go/issues/46477) introduced in Go 1.24, providing a more ergonomic and streamlined API.
## 📚 Table of Contents
- [Requirements](#-requirements)
- [Breaking Changes](#-breaking-changes)
- [Key Improvements](#-key-improvements)
- [Migration Guide](#-migration-guide)
- [Installation](#-installation)
- [What's New](#-whats-new)
## 🔧 Requirements
- **Go 1.24 or later** (for generic type alias support)
## ⚠️ Breaking Changes
### 1. Generic Type Aliases
V2 uses [generic type aliases](https://github.com/golang/go/issues/46477) which require Go 1.24+. This is the most significant change and enables cleaner type definitions.
**V1:**
```go
type ReaderIOEither[R, E, A any] RD.Reader[R, IOE.IOEither[E, A]]
```
**V2:**
```go
type ReaderIOEither[R, E, A any] = RD.Reader[R, IOE.IOEither[E, A]]
```
### 2. Generic Type Parameter Ordering
Type parameters that **cannot** be inferred from function arguments now come first, improving type inference.
**V1:**
```go
// Ap in V1 - less intuitive ordering
func Ap[R, E, A, B any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
```
**V2:**
```go
// Ap in V2 - B comes first as it cannot be inferred
func Ap[B, R, E, A any](fa ReaderIOEither[R, E, A]) func(ReaderIOEither[R, E, func(A) B]) ReaderIOEither[R, E, B]
```
This change allows the Go compiler to infer more types automatically, reducing the need for explicit type parameters.
### 3. Pair Monad Semantics
Monadic operations for `Pair` now operate on the **second argument** to align with the [Haskell definition](https://hackage.haskell.org/package/TypeCompose-0.9.14/docs/Data-Pair.html).
**V1:**
```go
// Operations on first element
pair := MakePair(1, "hello")
result := Map(func(x int) int { return x * 2 })(pair) // Pair(2, "hello")
```
**V2:**
```go
// Operations on second element (Haskell-compatible)
pair := MakePair(1, "hello")
result := Map(func(s string) string { return s + "!" })(pair) // Pair(1, "hello!")
```
## ✨ Key Improvements
### 1. Simplified Type Declarations
Generic type aliases eliminate the need for namespace imports in type declarations.
**V1 Approach:**
```go
import (
ET "github.com/IBM/fp-go/either"
OPT "github.com/IBM/fp-go/option"
)
func processData(input string) ET.Either[error, OPT.Option[int]] {
// implementation
}
```
**V2 Approach:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
// Define type aliases once
type Either[A any] = either.Either[error, A]
type Option[A any] = option.Option[A]
// Use them throughout your codebase
func processData(input string) Either[Option[int]] {
// implementation
}
```
### 2. No More `generic` Subpackages
The library implementation no longer requires separate `generic` subpackages, making the codebase simpler and easier to understand.
**V1 Structure:**
```
either/
either.go
generic/
either.go // Generic implementation
```
**V2 Structure:**
```
either/
either.go // Single, clean implementation
```
### 3. Better Type Inference
The reordered type parameters allow the Go compiler to infer more types automatically:
**V1:**
```go
// Often need explicit type parameters
result := Map[Context, error, int, string](transform)(value)
```
**V2:**
```go
// Compiler can infer more types
result := Map(transform)(value) // Cleaner!
```
## 🚀 Migration Guide
### Step 1: Update Go Version
Ensure you're using Go 1.24 or later:
```bash
go version # Should show go1.24 or higher
```
### Step 2: Update Import Paths
Change all import paths from `github.com/IBM/fp-go` to `github.com/IBM/fp-go/v2`:
**Before:**
```go
import (
"github.com/IBM/fp-go/either"
"github.com/IBM/fp-go/option"
)
```
**After:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
```
### Step 3: Remove `generic` Subpackage Imports
If you were using generic subpackages, remove them:
**Before:**
```go
import (
E "github.com/IBM/fp-go/either/generic"
)
```
**After:**
```go
import (
"github.com/IBM/fp-go/v2/either"
)
```
### Step 4: Update Type Parameter Order
Review functions like `Ap` where type parameter order has changed. The compiler will help identify these:
**Before:**
```go
result := Ap[Context, error, int, string](value)(funcInContext)
```
**After:**
```go
result := Ap[string, Context, error, int](value)(funcInContext)
// Or better yet, let the compiler infer:
result := Ap(value)(funcInContext)
```
### Step 5: Update Pair Operations
If you're using `Pair`, update operations to work on the second element:
**Before (V1):**
```go
pair := MakePair(42, "data")
// Map operates on first element
result := Map(func(x int) int { return x * 2 })(pair)
```
**After (V2):**
```go
pair := MakePair(42, "data")
// Map operates on second element
result := Map(func(s string) string { return s + "!" })(pair)
```
### Step 6: Simplify Type Aliases
Create project-wide type aliases for common patterns:
```go
// types.go - Define once, use everywhere
package myapp
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ioeither"
)
type Either[A any] = either.Either[error, A]
type Option[A any] = option.Option[A]
type IOEither[A any] = ioeither.IOEither[error, A]
```
## 📦 Installation
```bash
go get github.com/IBM/fp-go/v2
```
## 🆕 What's New
### Cleaner API Surface
The elimination of `generic` subpackages means:
- Fewer imports to manage
- Simpler package structure
- Easier to navigate documentation
- More intuitive API
### Example: Before and After
**V1 Complex Example:**
```go
import (
ET "github.com/IBM/fp-go/either"
EG "github.com/IBM/fp-go/either/generic"
IOET "github.com/IBM/fp-go/ioeither"
IOEG "github.com/IBM/fp-go/ioeither/generic"
)
func process() IOET.IOEither[error, string] {
return IOEG.Map[error, int, string](
strconv.Itoa,
)(fetchData())
}
```
**V2 Simplified Example:**
```go
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/ioeither"
)
type IOEither[A any] = ioeither.IOEither[error, A]
func process() IOEither[string] {
return ioeither.Map(
strconv.Itoa,
)(fetchData())
}
```
## 📚 Additional Resources
- [Main README](../README.md) - Core concepts and design philosophy
- [API Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2)
- [Code Samples](../samples/)
- [Go 1.24 Release Notes](https://tip.golang.org/doc/go1.24)
## 🤔 Should I Migrate?
**Migrate to V2 if:**
- ✅ You can use Go 1.24+
- ✅ You want cleaner, more maintainable code
- ✅ You want better type inference
- ✅ You're starting a new project
**Stay on V1 if:**
- ⚠️ You're locked to Go < 1.24
- ⚠️ Migration effort outweighs benefits for your project
- ⚠️ You need stability in production (V2 is newer)
## 🐛 Issues and Feedback
Found a bug or have a suggestion? Please [open an issue](https://github.com/IBM/fp-go/issues) on GitHub.
## 📄 License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.

50
v2/array/any.go Normal file
View File

@@ -0,0 +1,50 @@
// 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 (
G "github.com/IBM/fp-go/v2/array/generic"
)
// AnyWithIndex tests if any of the elements in the array matches the predicate.
// The predicate receives both the index and the element.
// Returns true if at least one element satisfies the predicate, false otherwise.
//
// Example:
//
// hasEvenAtEvenIndex := array.AnyWithIndex(func(i, x int) bool {
// return i%2 == 0 && x%2 == 0
// })
// result := hasEvenAtEvenIndex([]int{1, 3, 4, 5}) // true (4 is at index 2)
//
//go:inline
func AnyWithIndex[A any](pred func(int, A) bool) func([]A) bool {
return G.AnyWithIndex[[]A](pred)
}
// Any tests if any of the elements in the array matches the predicate.
// Returns true if at least one element satisfies the predicate, false otherwise.
// Returns false for an empty array.
//
// Example:
//
// hasEven := array.Any(func(x int) bool { return x%2 == 0 })
// result := hasEven([]int{1, 3, 4, 5}) // true
//
//go:inline
func Any[A any](pred func(A) bool) func([]A) bool {
return G.Any[[]A](pred)
}

30
v2/array/any_test.go Normal file
View File

@@ -0,0 +1,30 @@
// 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"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestAny(t *testing.T) {
anyBool := Any(F.Identity[bool])
assert.True(t, anyBool(From(false, true, false)))
assert.False(t, anyBool(From(false, false, false)))
}

538
v2/array/array.go Normal file
View File

@@ -0,0 +1,538 @@
// 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 (
G "github.com/IBM/fp-go/v2/array/generic"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/tuple"
)
// From constructs an array from a set of variadic arguments
//
//go:inline
func From[A any](data ...A) []A {
return G.From[[]A](data...)
}
// MakeBy returns a `Array` of length `n` with element `i` initialized with `f(i)`.
//
//go:inline
func MakeBy[F ~func(int) A, A any](n int, f F) []A {
return G.MakeBy[[]A](n, f)
}
// Replicate creates a `Array` containing a value repeated the specified number of times.
//
//go:inline
func Replicate[A any](n int, a A) []A {
return G.Replicate[[]A](n, a)
}
// MonadMap applies a function to each element of an array, returning a new array with the results.
// This is the monadic version of Map that takes the array as the first parameter.
//
//go:inline
func MonadMap[A, B any](as []A, f func(a A) B) []B {
return G.MonadMap[[]A, []B](as, f)
}
// MonadMapRef applies a function to a pointer to each element of an array, returning a new array with the results.
// This is useful when you need to access elements by reference without copying.
func MonadMapRef[A, B any](as []A, f func(a *A) B) []B {
count := len(as)
bs := make([]B, count)
for i := count - 1; i >= 0; i-- {
bs[i] = f(&as[i])
}
return bs
}
// MapWithIndex applies a function to each element and its index in an array, returning a new array with the results.
//
//go:inline
func MapWithIndex[A, B any](f func(int, A) B) func([]A) []B {
return G.MapWithIndex[[]A, []B](f)
}
// Map applies a function to each element of an array, returning a new array with the results.
// This is the curried version that returns a function.
//
// Example:
//
// double := array.Map(func(x int) int { return x * 2 })
// result := double([]int{1, 2, 3}) // [2, 4, 6]
//
//go:inline
func Map[A, B any](f func(a A) B) func([]A) []B {
return G.Map[[]A, []B](f)
}
// MapRef applies a function to a pointer to each element of an array, returning a new array with the results.
// This is the curried version that returns a function.
func MapRef[A, B any](f func(a *A) B) func([]A) []B {
return F.Bind2nd(MonadMapRef[A, B], f)
}
func filterRef[A any](fa []A, pred func(a *A) bool) []A {
var result []A
count := len(fa)
for i := 0; i < count; i++ {
a := fa[i]
if pred(&a) {
result = append(result, a)
}
}
return result
}
func filterMapRef[A, B any](fa []A, pred func(a *A) bool, f func(a *A) B) []B {
var result []B
count := len(fa)
for i := 0; i < count; i++ {
a := fa[i]
if pred(&a) {
result = append(result, f(&a))
}
}
return result
}
// Filter returns a new array with all elements from the original array that match a predicate
//
//go:inline
func Filter[A any](pred func(A) bool) EM.Endomorphism[[]A] {
return G.Filter[[]A](pred)
}
// FilterWithIndex returns a new array with all elements from the original array that match a predicate
//
//go:inline
func FilterWithIndex[A any](pred func(int, A) bool) EM.Endomorphism[[]A] {
return G.FilterWithIndex[[]A](pred)
}
// FilterRef returns a new array with all elements from the original array that match a predicate operating on pointers.
func FilterRef[A any](pred func(*A) bool) EM.Endomorphism[[]A] {
return F.Bind2nd(filterRef[A], pred)
}
// MonadFilterMap maps an array with a function that returns an Option and keeps only the Some values.
// This is the monadic version that takes the array as the first parameter.
//
//go:inline
func MonadFilterMap[A, B any](fa []A, f func(A) O.Option[B]) []B {
return G.MonadFilterMap[[]A, []B](fa, f)
}
// MonadFilterMapWithIndex maps an array with a function that takes an index and returns an Option,
// keeping only the Some values. This is the monadic version that takes the array as the first parameter.
//
//go:inline
func MonadFilterMapWithIndex[A, B any](fa []A, f func(int, A) O.Option[B]) []B {
return G.MonadFilterMapWithIndex[[]A, []B](fa, f)
}
// FilterMap maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
//
//go:inline
func FilterMap[A, B any](f func(A) O.Option[B]) func([]A) []B {
return G.FilterMap[[]A, []B](f)
}
// FilterMapWithIndex maps an array with an iterating function that returns an [O.Option] and it keeps only the Some values discarding the Nones.
//
//go:inline
func FilterMapWithIndex[A, B any](f func(int, A) O.Option[B]) func([]A) []B {
return G.FilterMapWithIndex[[]A, []B](f)
}
// FilterChain maps an array with an iterating function that returns an [O.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 func(A) O.Option[[]B]) func([]A) []B {
return G.FilterChain[[]A](f)
}
// FilterMapRef filters an array using a predicate on pointers and maps the matching elements using a function on pointers.
func FilterMapRef[A, B any](pred func(a *A) bool, f func(a *A) B) func([]A) []B {
return func(fa []A) []B {
return filterMapRef(fa, pred, f)
}
}
func reduceRef[A, B any](fa []A, f func(B, *A) B, initial B) B {
current := initial
count := len(fa)
for i := 0; i < count; i++ {
current = f(current, &fa[i])
}
return current
}
//go:inline
func MonadReduce[A, B any](fa []A, f func(B, A) B, initial B) B {
return G.MonadReduce(fa, f, initial)
}
// Reduce folds an array from left to right, applying a function to accumulate a result.
//
// Example:
//
// sum := array.Reduce(func(acc, x int) int { return acc + x }, 0)
// result := sum([]int{1, 2, 3, 4, 5}) // 15
//
//go:inline
func Reduce[A, B any](f func(B, A) B, initial B) func([]A) B {
return G.Reduce[[]A](f, initial)
}
// ReduceWithIndex folds an array from left to right with access to the index,
// applying a function to accumulate a result.
//
//go:inline
func ReduceWithIndex[A, B any](f func(int, B, A) B, initial B) func([]A) B {
return G.ReduceWithIndex[[]A](f, initial)
}
// ReduceRight folds an array from right to left, applying a function to accumulate a result.
//
//go:inline
func ReduceRight[A, B any](f func(A, B) B, initial B) func([]A) B {
return G.ReduceRight[[]A](f, initial)
}
// ReduceRightWithIndex folds an array from right to left with access to the index,
// applying a function to accumulate a result.
//
//go:inline
func ReduceRightWithIndex[A, B any](f func(int, A, B) B, initial B) func([]A) B {
return G.ReduceRightWithIndex[[]A](f, initial)
}
// ReduceRef folds an array from left to right using pointers to elements,
// applying a function to accumulate a result.
func ReduceRef[A, B any](f func(B, *A) B, initial B) func([]A) B {
return func(as []A) B {
return reduceRef(as, f, initial)
}
}
// Append adds an element to the end of an array, returning a new array.
//
//go:inline
func Append[A any](as []A, a A) []A {
return G.Append(as, a)
}
// IsEmpty checks if an array has no elements.
//
//go:inline
func IsEmpty[A any](as []A) bool {
return G.IsEmpty(as)
}
// IsNonEmpty checks if an array has at least one element.
func IsNonEmpty[A any](as []A) bool {
return len(as) > 0
}
// Empty returns an empty array of type A.
//
//go:inline
func Empty[A any]() []A {
return G.Empty[[]A]()
}
// Zero returns an empty array of type A (alias for Empty).
func Zero[A any]() []A {
return Empty[A]()
}
// Of constructs a single element array
//
//go:inline
func Of[A any](a A) []A {
return G.Of[[]A](a)
}
// MonadChain applies a function that returns an array to each element and flattens the results.
// This is the monadic version that takes the array as the first parameter (also known as FlatMap).
//
//go:inline
func MonadChain[A, B any](fa []A, f func(a A) []B) []B {
return G.MonadChain(fa, f)
}
// Chain applies a function that returns an array to each element and flattens the results.
// This is the curried version (also known as FlatMap).
//
// Example:
//
// duplicate := array.Chain(func(x int) []int { return []int{x, x} })
// result := duplicate([]int{1, 2, 3}) // [1, 1, 2, 2, 3, 3]
//
//go:inline
func Chain[A, B any](f func(A) []B) func([]A) []B {
return G.Chain[[]A](f)
}
// MonadAp applies an array of functions to an array of values, producing all combinations.
// This is the monadic version that takes both arrays as parameters.
//
//go:inline
func MonadAp[B, A any](fab []func(A) B, fa []A) []B {
return G.MonadAp[[]B](fab, fa)
}
// Ap applies an array of functions to an array of values, producing all combinations.
// This is the curried version.
//
//go:inline
func Ap[B, A any](fa []A) func([]func(A) B) []B {
return G.Ap[[]B, []func(A) B](fa)
}
// Match performs pattern matching on an array, calling onEmpty if empty or onNonEmpty if not.
//
//go:inline
func Match[A, B any](onEmpty func() B, onNonEmpty func([]A) B) func([]A) B {
return G.Match(onEmpty, onNonEmpty)
}
// MatchLeft performs pattern matching on an array, calling onEmpty if empty or onNonEmpty with head and tail if not.
//
//go:inline
func MatchLeft[A, B any](onEmpty func() B, onNonEmpty func(A, []A) B) func([]A) B {
return G.MatchLeft(onEmpty, onNonEmpty)
}
// Tail returns all elements except the first, wrapped in an Option.
// Returns None if the array is empty.
//
//go:inline
func Tail[A any](as []A) O.Option[[]A] {
return G.Tail(as)
}
// Head returns the first element of an array, wrapped in an Option.
// Returns None if the array is empty.
//
//go:inline
func Head[A any](as []A) O.Option[A] {
return G.Head(as)
}
// First returns the first element of an array, wrapped in an Option (alias for Head).
// Returns None if the array is empty.
//
//go:inline
func First[A any](as []A) O.Option[A] {
return G.First(as)
}
// Last returns the last element of an array, wrapped in an Option.
// Returns None if the array is empty.
//
//go:inline
func Last[A any](as []A) O.Option[A] {
return G.Last(as)
}
// PrependAll inserts a separator before each element of an array.
func PrependAll[A any](middle A) EM.Endomorphism[[]A] {
return func(as []A) []A {
count := len(as)
dst := count * 2
result := make([]A, dst)
for i := count - 1; i >= 0; i-- {
dst--
result[dst] = as[i]
dst--
result[dst] = middle
}
return result
}
}
// Intersperse inserts a separator between each element of an array.
//
// Example:
//
// result := array.Intersperse(0)([]int{1, 2, 3}) // [1, 0, 2, 0, 3]
func Intersperse[A any](middle A) EM.Endomorphism[[]A] {
prepend := PrependAll(middle)
return func(as []A) []A {
if IsEmpty(as) {
return as
}
return prepend(as)[1:]
}
}
// Intercalate inserts a separator between elements and concatenates them using a Monoid.
func Intercalate[A any](m M.Monoid[A]) func(A) func([]A) A {
return func(middle A) func([]A) A {
return Match(m.Empty, F.Flow2(Intersperse(middle), ConcatAll(m)))
}
}
// Flatten converts a nested array into a flat array by concatenating all inner arrays.
//
// Example:
//
// result := array.Flatten([][]int{{1, 2}, {3, 4}, {5}}) // [1, 2, 3, 4, 5]
//
//go:inline
func Flatten[A any](mma [][]A) []A {
return G.Flatten(mma)
}
// Slice extracts a subarray from index low (inclusive) to high (exclusive).
func Slice[A any](low, high int) func(as []A) []A {
return array.Slice[[]A](low, high)
}
// Lookup returns the element at the specified index, wrapped in an Option.
// Returns None if the index is out of bounds.
//
//go:inline
func Lookup[A any](idx int) func([]A) O.Option[A] {
return G.Lookup[[]A](idx)
}
// UpsertAt returns a function that inserts or updates an element at a specific index.
// If the index is out of bounds, the element is appended.
//
//go:inline
func UpsertAt[A any](a A) EM.Endomorphism[[]A] {
return G.UpsertAt[[]A](a)
}
// Size returns the number of elements in an array.
//
//go:inline
func Size[A any](as []A) int {
return G.Size(as)
}
// MonadPartition splits an array into two arrays based on a predicate.
// The first array contains elements for which the predicate returns false,
// 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] {
return G.MonadPartition(as, pred)
}
// Partition creates two new arrays out of one, the left result contains the elements
// 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] {
return G.Partition[[]A](pred)
}
// IsNil checks if the array is set to nil
func IsNil[A any](as []A) bool {
return array.IsNil(as)
}
// IsNonNil checks if the array is set to nil
func IsNonNil[A any](as []A) bool {
return array.IsNonNil(as)
}
// ConstNil returns a nil array
func ConstNil[A any]() []A {
return array.ConstNil[[]A]()
}
// SliceRight extracts a subarray from the specified start index to the end.
//
//go:inline
func SliceRight[A any](start int) EM.Endomorphism[[]A] {
return G.SliceRight[[]A](start)
}
// Copy creates a shallow copy of the array
//
//go:inline
func Copy[A any](b []A) []A {
return G.Copy(b)
}
// Clone creates a deep copy of the array using the provided endomorphism to clone the values
//
//go:inline
func Clone[A any](f func(A) A) func(as []A) []A {
return G.Clone[[]A](f)
}
// FoldMap maps and folds an array. Map the Array passing each value to the iterating function. Then fold the results using the provided Monoid.
//
//go:inline
func FoldMap[A, B any](m M.Monoid[B]) func(func(A) B) func([]A) B {
return G.FoldMap[[]A](m)
}
// FoldMapWithIndex maps and folds an array. Map the Array passing each value to the iterating function. Then fold the results using the provided Monoid.
//
//go:inline
func FoldMapWithIndex[A, B any](m M.Monoid[B]) func(func(int, A) B) func([]A) B {
return G.FoldMapWithIndex[[]A](m)
}
// Fold folds the array using the provided Monoid.
//
//go:inline
func Fold[A any](m M.Monoid[A]) func([]A) A {
return G.Fold[[]A](m)
}
// Push adds an element to the end of an array (alias for Append).
//
//go:inline
func Push[A any](a A) EM.Endomorphism[[]A] {
return G.Push[EM.Endomorphism[[]A]](a)
}
// MonadFlap applies a value to an array of functions, producing an array of results.
// This is the monadic version that takes both parameters.
//
//go:inline
func MonadFlap[B, A any](fab []func(A) B, a A) []B {
return G.MonadFlap[func(A) B, []func(A) B, []B](fab, a)
}
// Flap applies a value to an array of functions, producing an array of results.
// This is the curried version.
//
//go:inline
func Flap[B, A any](a A) func([]func(A) B) []B {
return G.Flap[func(A) B, []func(A) B, []B](a)
}
// Prepend adds an element to the beginning of an array, returning a new array.
//
//go:inline
func Prepend[A any](head A) EM.Endomorphism[[]A] {
return G.Prepend[EM.Endomorphism[[]A]](head)
}

View File

@@ -0,0 +1,323 @@
// 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 (
"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, func(x int) int { return x * 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
}, 0)
result := reducer(src)
assert.Equal(t, 9, result) // 0 + (0+1) + (1+2) + (2+3) = 9
}
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)
}, "")
result := reducer(src)
assert.Equal(t, "2:c1:b0:a", result)
}
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{
func(x int) int { return x * 2 },
func(x int) int { return x + 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(func(x int) int { return x * 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)
assert.Equal(t, []int{1, 2, 3, 4}, result)
}
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)
}
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)
}
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)
}

216
v2/array/array_test.go Normal file
View File

@@ -0,0 +1,216 @@
// 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 (
"fmt"
"strings"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
func TestMap1(t *testing.T) {
src := []string{"a", "b", "c"}
up := Map(strings.ToUpper)(src)
var up1 = []string{}
for _, s := range src {
up1 = append(up1, strings.ToUpper(s))
}
var up2 = []string{}
for i := range src {
up2 = append(up2, strings.ToUpper(src[i]))
}
assert.Equal(t, up, up1)
assert.Equal(t, up, up2)
}
func TestMap(t *testing.T) {
mapper := Map(utils.Upper)
src := []string{"a", "b", "c"}
dst := mapper(src)
assert.Equal(t, dst, []string{"A", "B", "C"})
}
func TestReduceRight(t *testing.T) {
values := From("a", "b", "c")
f := func(a, acc string) string {
return fmt.Sprintf("%s%s", acc, a)
}
b := ""
assert.Equal(t, "cba", ReduceRight(f, b)(values))
assert.Equal(t, "", ReduceRight(f, b)(Empty[string]()))
}
func TestReduce(t *testing.T) {
values := MakeBy(101, F.Identity[int])
sum := func(val int, current int) int {
return val + current
}
reducer := Reduce(sum, 0)
result := reducer(values)
assert.Equal(t, result, 5050)
}
func TestEmpty(t *testing.T) {
assert.True(t, IsNonEmpty(MakeBy(101, F.Identity[int])))
assert.True(t, IsEmpty([]int{}))
}
func TestAp(t *testing.T) {
assert.Equal(t,
[]int{2, 4, 6, 3, 6, 9},
F.Pipe1(
[]func(int) int{
utils.Double,
utils.Triple,
},
Ap[int]([]int{1, 2, 3}),
),
)
}
func TestIntercalate(t *testing.T) {
is := Intercalate(S.Monoid)("-")
assert.Equal(t, "", is(Empty[string]()))
assert.Equal(t, "a", is([]string{"a"}))
assert.Equal(t, "a-b-c", is([]string{"a", "b", "c"}))
assert.Equal(t, "a--c", is([]string{"a", "", "c"}))
assert.Equal(t, "a-b", is([]string{"a", "b"}))
assert.Equal(t, "a-b-c-d", is([]string{"a", "b", "c", "d"}))
}
func TestIntersperse(t *testing.T) {
// Test with empty array
assert.Equal(t, []int{}, Intersperse(0)([]int{}))
// Test with single element
assert.Equal(t, []int{1}, Intersperse(0)([]int{1}))
// Test with multiple elements
assert.Equal(t, []int{1, 0, 2, 0, 3}, Intersperse(0)([]int{1, 2, 3}))
}
func TestPrependAll(t *testing.T) {
empty := Empty[int]()
prep := PrependAll(0)
assert.Equal(t, empty, prep(empty))
assert.Equal(t, []int{0, 1, 0, 2, 0, 3}, prep([]int{1, 2, 3}))
assert.Equal(t, []int{0, 1}, prep([]int{1}))
assert.Equal(t, []int{0, 1, 0, 2, 0, 3, 0, 4}, prep([]int{1, 2, 3, 4}))
}
func TestFlatten(t *testing.T) {
assert.Equal(t, []int{1, 2, 3}, Flatten([][]int{{1}, {2}, {3}}))
}
func TestLookup(t *testing.T) {
data := []int{0, 1, 2}
none := O.None[int]()
assert.Equal(t, none, Lookup[int](-1)(data))
assert.Equal(t, none, Lookup[int](10)(data))
assert.Equal(t, O.Some(1), Lookup[int](1)(data))
}
func TestSlice(t *testing.T) {
data := []int{0, 1, 2, 3}
assert.Equal(t, []int{1, 2}, Slice[int](1, 3)(data))
}
func TestFrom(t *testing.T) {
assert.Equal(t, []int{1, 2, 3}, From(1, 2, 3))
}
func TestPartition(t *testing.T) {
pred := func(n int) bool {
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)))
}
func TestFilterChain(t *testing.T) {
src := From(1, 2, 3)
f := func(i int) O.Option[[]string] {
if i%2 != 0 {
return O.Of(From(fmt.Sprintf("a%d", i), fmt.Sprintf("b%d", i)))
}
return O.None[[]string]()
}
res := FilterChain(f)(src)
assert.Equal(t, From("a1", "b1", "a3", "b3"), res)
}
func TestFilterMap(t *testing.T) {
src := From(1, 2, 3)
f := func(i int) O.Option[string] {
if i%2 != 0 {
return O.Of(fmt.Sprintf("a%d", i))
}
return O.None[string]()
}
res := FilterMap(f)(src)
assert.Equal(t, From("a1", "a3"), res)
}
func TestFoldMap(t *testing.T) {
src := From("a", "b", "c")
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
assert.Equal(t, "ABC", fold(src))
}
func ExampleFoldMap() {
src := From("a", "b", "c")
fold := FoldMap[string](S.Monoid)(strings.ToUpper)
fmt.Println(fold(src))
// Output: ABC
}

148
v2/array/bind.go Normal file
View File

@@ -0,0 +1,148 @@
// 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 (
G "github.com/IBM/fp-go/v2/array/generic"
)
// Do creates an empty context of type S to be used with the Bind operation.
// This is the starting point for monadic do-notation style computations.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
// result := array.Do(State{})
//
//go:inline
func Do[S any](
empty S,
) []S {
return G.Do[[]S](empty)
}
// Bind attaches the result of a computation to a context S1 to produce a context S2.
// The setter function defines how to update the context with the computation result.
// This enables monadic composition where each step can produce multiple results.
//
// Example:
//
// result := F.Pipe2(
// array.Do(struct{ X, Y int }{}),
// array.Bind(
// func(x int) func(s struct{}) struct{ X int } {
// return func(s struct{}) struct{ X int } { return struct{ X int }{x} }
// },
// func(s struct{}) []int { return []int{1, 2} },
// ),
// )
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) []T,
) func([]S1) []S2 {
return G.Bind[[]S1, []S2](setter, f)
}
// 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.
//
// 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 },
// )
//
//go:inline
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func([]S1) []S2 {
return G.Let[[]S1, []S2](setter, f)
}
// LetTo attaches a constant value to a context S1 to produce a context S2.
// This is useful for adding constant values to the context.
//
// 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",
// )
//
//go:inline
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func([]S1) []S2 {
return G.LetTo[[]S1, []S2](setter, b)
}
// BindTo initializes a new state S1 from a value T.
// This is typically the first operation after Do to start building the context.
//
// Example:
//
// result := F.Pipe2(
// []int{1, 2, 3},
// array.BindTo(func(x int) struct{ X int } {
// return struct{ X int }{x}
// }),
// )
//
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) func([]T) []S1 {
return G.BindTo[[]S1, []T](setter)
}
// ApS attaches a value to a context S1 to produce a context S2 by considering
// the context and the value concurrently (using applicative semantics).
// This produces all combinations of context values and array values.
//
// Example:
//
// result := array.ApS(
// func(y int) func(s struct{ X int }) struct{ X, Y int } {
// return func(s struct{ X int }) struct{ X, Y int } {
// return struct{ X, Y int }{s.X, y}
// }
// },
// []int{10, 20},
// )
//
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa []T,
) func([]S1) []S2 {
return G.ApS[[]S1, []S2](setter, fa)
}

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"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
type TestState1 struct {
X int
}
type TestState2 struct {
X int
Y int
}
func TestLet(t *testing.T) {
result := F.Pipe2(
Do(TestState1{}),
Let(
func(y int) func(s TestState1) TestState2 {
return func(s TestState1) TestState2 {
return TestState2{X: s.X, Y: y}
}
},
func(s TestState1) int { return s.X * 2 },
),
Map(func(s TestState2) int { return s.X + s.Y }),
)
assert.Equal(t, []int{0}, result)
}
func TestLetTo(t *testing.T) {
result := F.Pipe2(
Do(TestState1{X: 5}),
LetTo(
func(y int) func(s TestState1) TestState2 {
return func(s TestState1) TestState2 {
return TestState2{X: s.X, Y: y}
}
},
42,
),
Map(func(s TestState2) int { return s.X + s.Y }),
)
assert.Equal(t, []int{47}, result)
}
func TestBindTo(t *testing.T) {
result := F.Pipe1(
[]int{1, 2, 3},
BindTo(func(x int) TestState1 {
return TestState1{X: x}
}),
)
expected := []TestState1{{X: 1}, {X: 2}, {X: 3}}
assert.Equal(t, expected, result)
}

56
v2/array/bind_test.go Normal file
View File

@@ -0,0 +1,56 @@
// 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"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
"github.com/stretchr/testify/assert"
)
func getLastName(s utils.Initial) []string {
return Of("Doe")
}
func getGivenName(s utils.WithLastName) []string {
return Of("John")
}
func TestBind(t *testing.T) {
res := F.Pipe3(
Do(utils.Empty),
Bind(utils.SetLastName, getLastName),
Bind(utils.SetGivenName, getGivenName),
Map(utils.GetFullName),
)
assert.Equal(t, res, Of("John Doe"))
}
func TestApS(t *testing.T) {
res := F.Pipe3(
Do(utils.Empty),
ApS(utils.SetLastName, Of("Doe")),
ApS(utils.SetGivenName, Of("John")),
Map(utils.GetFullName),
)
assert.Equal(t, res, Of("John Doe"))
}

251
v2/array/doc.go Normal file
View File

@@ -0,0 +1,251 @@
// 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 provides functional programming utilities for working with Go slices.
//
// This package treats Go slices as immutable arrays and provides a rich set of operations
// for transforming, filtering, folding, and combining arrays in a functional style.
// All operations return new arrays rather than modifying existing ones.
//
// # Core Concepts
//
// The array package implements several functional programming abstractions:
// - Functor: Transform array elements with Map
// - Applicative: Apply functions in arrays to values in arrays
// - Monad: Chain operations that produce arrays with Chain/FlatMap
// - Foldable: Reduce arrays to single values with Reduce/Fold
// - Traversable: Transform arrays while preserving structure
//
// # Basic Operations
//
// // Creating arrays
// arr := array.From(1, 2, 3, 4, 5)
// repeated := array.Replicate(3, "hello")
// generated := array.MakeBy(5, func(i int) int { return i * 2 })
//
// // Transforming arrays
// doubled := array.Map(func(x int) int { return x * 2 })(arr)
// filtered := array.Filter(func(x int) bool { return x > 2 })(arr)
//
// // Combining arrays
// combined := array.Flatten([][]int{{1, 2}, {3, 4}})
// zipped := array.Zip([]string{"a", "b"})([]int{1, 2})
//
// # Mapping and Filtering
//
// Transform array elements with Map, or filter elements with Filter:
//
// numbers := []int{1, 2, 3, 4, 5}
//
// // Map transforms each element
// doubled := array.Map(func(x int) int { return x * 2 })(numbers)
// // Result: [2, 4, 6, 8, 10]
//
// // Filter keeps elements matching a predicate
// evens := array.Filter(func(x int) bool { return x%2 == 0 })(numbers)
// // Result: [2, 4]
//
// // FilterMap combines both operations
// import "github.com/IBM/fp-go/v2/option"
// result := array.FilterMap(func(x int) option.Option[int] {
// if x%2 == 0 {
// return option.Some(x * 2)
// }
// return option.None[int]()
// })(numbers)
// // Result: [4, 8]
//
// # Folding and Reducing
//
// Reduce arrays to single values:
//
// numbers := []int{1, 2, 3, 4, 5}
//
// // Sum all elements
// sum := array.Reduce(func(acc, x int) int { return acc + x }, 0)(numbers)
// // Result: 15
//
// // Using a Monoid
// import "github.com/IBM/fp-go/v2/monoid"
// sum := array.Fold(monoid.MonoidSum[int]())(numbers)
// // Result: 15
//
// # Chaining Operations
//
// Chain operations that produce arrays (also known as FlatMap):
//
// numbers := []int{1, 2, 3}
// result := array.Chain(func(x int) []int {
// return []int{x, x * 10}
// })(numbers)
// // Result: [1, 10, 2, 20, 3, 30]
//
// # Finding Elements
//
// Search for elements matching predicates:
//
// numbers := []int{1, 2, 3, 4, 5}
//
// // Find first element > 3
// first := array.FindFirst(func(x int) bool { return x > 3 })(numbers)
// // Result: Some(4)
//
// // Find last element > 3
// last := array.FindLast(func(x int) bool { return x > 3 })(numbers)
// // Result: Some(5)
//
// // Get head and tail
// head := array.Head(numbers) // Some(1)
// tail := array.Tail(numbers) // Some([2, 3, 4, 5])
//
// # Sorting
//
// Sort arrays using Ord instances:
//
// import "github.com/IBM/fp-go/v2/ord"
//
// numbers := []int{3, 1, 4, 1, 5}
// sorted := array.Sort(ord.FromStrictCompare[int]())(numbers)
// // Result: [1, 1, 3, 4, 5]
//
// // Sort by extracted key
// type Person struct { Name string; Age int }
// people := []Person{{"Alice", 30}, {"Bob", 25}}
// byAge := array.SortByKey(ord.FromStrictCompare[int](), func(p Person) int {
// return p.Age
// })(people)
//
// # Uniqueness
//
// Remove duplicate elements:
//
// numbers := []int{1, 2, 2, 3, 3, 3}
// unique := array.StrictUniq(numbers)
// // Result: [1, 2, 3]
//
// // Unique by key
// type Person struct { Name string; Age int }
// people := []Person{{"Alice", 30}, {"Bob", 25}, {"Alice", 35}}
// uniqueByName := array.Uniq(func(p Person) string { return p.Name })(people)
// // Result: [{"Alice", 30}, {"Bob", 25}]
//
// # Zipping
//
// Combine multiple arrays:
//
// names := []string{"Alice", "Bob", "Charlie"}
// ages := []int{30, 25, 35}
//
// // Zip into tuples
// pairs := array.Zip(ages)(names)
// // Result: [(Alice, 30), (Bob, 25), (Charlie, 35)]
//
// // Zip with custom function
// result := array.ZipWith(names, ages, func(name string, age int) string {
// return fmt.Sprintf("%s is %d", name, age)
// })
//
// # Monadic Do Notation
//
// Build complex array computations using do-notation style:
//
// result := array.Do(
// struct{ X, Y int }{},
// )(
// array.Bind(
// func(x int) func(s struct{}) struct{ X int } {
// return func(s struct{}) struct{ X int } { return struct{ X int }{x} }
// },
// func(s struct{}) []int { return []int{1, 2, 3} },
// ),
// array.Bind(
// func(y int) func(s struct{ X int }) struct{ X, Y int } {
// return func(s struct{ X int }) struct{ X, Y int } {
// return struct{ X, Y int }{s.X, y}
// }
// },
// func(s struct{ X int }) []int { return []int{4, 5} },
// ),
// )
// // Produces all combinations: [{1,4}, {1,5}, {2,4}, {2,5}, {3,4}, {3,5}]
//
// # Sequence and Traverse
//
// Transform arrays of effects into effects of arrays:
//
// import "github.com/IBM/fp-go/v2/option"
//
// // Sequence: []Option[A] -> Option[[]A]
// opts := []option.Option[int]{
// option.Some(1),
// option.Some(2),
// option.Some(3),
// }
// result := array.ArrayOption[int]()(opts)
// // Result: Some([1, 2, 3])
//
// // If any is None, result is None
// opts2 := []option.Option[int]{
// option.Some(1),
// option.None[int](),
// option.Some(3),
// }
// result2 := array.ArrayOption[int]()(opts2)
// // Result: None
//
// # Equality and Comparison
//
// Compare arrays for equality:
//
// import "github.com/IBM/fp-go/v2/eq"
//
// eq := array.Eq(eq.FromStrictEquals[int]())
// equal := eq.Equals([]int{1, 2, 3}, []int{1, 2, 3})
// // Result: true
//
// # Monoid Operations
//
// Combine arrays using monoid operations:
//
// import "github.com/IBM/fp-go/v2/monoid"
//
// // Concatenate arrays
// m := array.Monoid[int]()
// result := m.Concat([]int{1, 2}, []int{3, 4})
// // Result: [1, 2, 3, 4]
//
// // Concatenate multiple arrays efficiently
// result := array.ArrayConcatAll(
// []int{1, 2},
// []int{3, 4},
// []int{5, 6},
// )
// // Result: [1, 2, 3, 4, 5, 6]
//
// # Performance Considerations
//
// Most operations create new arrays rather than modifying existing ones. For performance-critical
// code, consider:
// - Using Copy for shallow copies when needed
// - Using Clone with a custom cloning function for deep copies
// - Batching operations to minimize intermediate allocations
// - Using ArrayConcatAll for efficient concatenation of multiple arrays
//
// # Subpackages
//
// - array/generic: Generic implementations for custom array-like types
// - array/nonempty: Operations for non-empty arrays with compile-time guarantees
// - array/testing: Testing utilities for array laws and properties
package array

51
v2/array/eq.go Normal file
View File

@@ -0,0 +1,51 @@
// 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 (
E "github.com/IBM/fp-go/v2/eq"
)
func equals[T any](left []T, right []T, eq func(T, T) bool) bool {
if len(left) != len(right) {
return false
}
for i, v1 := range left {
v2 := right[i]
if !eq(v1, v2) {
return false
}
}
return true
}
// Eq creates an equality checker for arrays given an equality checker for elements.
// Two arrays are considered equal if they have the same length and all corresponding
// elements are equal according to the provided Eq instance.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/eq"
//
// intArrayEq := array.Eq(eq.FromStrictEquals[int]())
// result := intArrayEq.Equals([]int{1, 2, 3}, []int{1, 2, 3}) // true
// result2 := intArrayEq.Equals([]int{1, 2, 3}, []int{1, 2, 4}) // false
func Eq[T any](e E.Eq[T]) E.Eq[[]T] {
eq := e.Equals
return E.FromEquals(func(left, right []T) bool {
return equals(left, right, eq)
})
}

44
v2/array/eq_test.go Normal file
View File

@@ -0,0 +1,44 @@
// 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"
E "github.com/IBM/fp-go/v2/eq"
"github.com/stretchr/testify/assert"
)
func TestEq(t *testing.T) {
intEq := Eq(E.FromStrictEquals[int]())
// Test equal arrays
assert.True(t, intEq.Equals([]int{1, 2, 3}, []int{1, 2, 3}))
// Test different lengths
assert.False(t, intEq.Equals([]int{1, 2, 3}, []int{1, 2}))
// Test different values
assert.False(t, intEq.Equals([]int{1, 2, 3}, []int{1, 2, 4}))
// Test empty arrays
assert.True(t, intEq.Equals([]int{}, []int{}))
// Test string arrays
stringEq := Eq(E.FromStrictEquals[string]())
assert.True(t, stringEq.Equals([]string{"a", "b"}, []string{"a", "b"}))
assert.False(t, stringEq.Equals([]string{"a", "b"}, []string{"a", "c"}))
}

View File

@@ -0,0 +1,77 @@
// 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 (
"fmt"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
)
func Example_any() {
pred := func(val int) bool {
return val&2 == 0
}
data1 := From(1, 2, 3)
fmt.Println(Any(pred)(data1))
// Output:
// true
}
func Example_any_filter() {
pred := func(val int) bool {
return val&2 == 0
}
data1 := From(1, 2, 3)
// Any tests if any of the entries in the array matches the condition
Any := F.Flow2(
Filter(pred),
IsNonEmpty[int],
)
fmt.Println(Any(data1))
// Output:
// true
}
func Example_any_find() {
pred := func(val int) bool {
return val&2 == 0
}
data1 := From(1, 2, 3)
// Any tests if any of the entries in the array matches the condition
Any := F.Flow2(
FindFirst(pred),
O.IsSome[int],
)
fmt.Println(Any(data1))
// Output:
// true
}

View File

@@ -0,0 +1,55 @@
// 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 (
"fmt"
F "github.com/IBM/fp-go/v2/function"
)
func Example_find() {
pred := func(val int) bool {
return val&2 == 0
}
data1 := From(1, 2, 3)
fmt.Println(FindFirst(pred)(data1))
// Output:
// Some[int](1)
}
func Example_find_filter() {
pred := func(val int) bool {
return val&2 == 0
}
data1 := From(1, 2, 3)
Find := F.Flow2(
Filter(pred),
Head[int],
)
fmt.Println(Find(data1))
// Output:
// Some[int](1)
}

View File

@@ -0,0 +1,59 @@
// 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 (
"fmt"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
)
// Example_basic adapts examples from [https://github.com/inato/fp-ts-cheatsheet#basic-manipulation]
func Example_basic() {
someArray := From(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) // []int
isEven := func(num int) bool {
return num%2 == 0
}
square := func(num int) int {
return num * num
}
// filter and map
result := F.Pipe2(
someArray,
Filter(isEven),
Map(square),
) // [0 4 16 36 64]
// or in one go with filterMap
resultFilterMap := F.Pipe1(
someArray,
FilterMap(
F.Flow2(O.FromPredicate(isEven), O.Map(square)),
),
)
fmt.Println(result)
fmt.Println(resultFilterMap)
// Output:
// [0 4 16 36 64]
// [0 4 16 36 64]
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
import (
"fmt"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/number/integer"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/ord"
S "github.com/IBM/fp-go/v2/string"
)
type user struct {
name string
age O.Option[int]
}
func (user user) GetName() string {
return user.name
}
func (user user) GetAge() O.Option[int] {
return user.age
}
// Example_sort adapts examples from [https://github.com/inato/fp-ts-cheatsheet#sort-elements-with-ord]
func Example_sort() {
strings := From("zyx", "abc", "klm")
sortedStrings := F.Pipe1(
strings,
Sort(S.Ord),
) // => ['abc', 'klm', 'zyx']
// reverse sort
reverseSortedStrings := F.Pipe1(
strings,
Sort(ord.Reverse(S.Ord)),
) // => ['zyx', 'klm', 'abc']
// sort Option
optionalNumbers := From(O.Some(1337), O.None[int](), O.Some(42))
sortedNums := F.Pipe1(
optionalNumbers,
Sort(O.Ord(I.Ord)),
)
// complex object with different rules
byName := F.Pipe1(
S.Ord,
ord.Contramap(user.GetName),
) // ord.Ord[user]
byAge := F.Pipe1(
O.Ord(I.Ord),
ord.Contramap(user.GetAge),
) // ord.Ord[user]
sortedUsers := F.Pipe1(
From(user{name: "a", age: O.Of(30)}, user{name: "d", age: O.Of(10)}, user{name: "c"}, user{name: "b", age: O.Of(10)}),
SortBy(From(byAge, byName)),
)
fmt.Println(sortedStrings)
fmt.Println(reverseSortedStrings)
fmt.Println(sortedNums)
fmt.Println(sortedUsers)
// Output:
// [abc klm zyx]
// [zyx klm abc]
// [None[int] Some[int](42) Some[int](1337)]
// [{c {false 0}} {b {true 10}} {d {true 10}} {a {true 30}}]
}

115
v2/array/find.go Normal file
View File

@@ -0,0 +1,115 @@
// 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 (
G "github.com/IBM/fp-go/v2/array/generic"
O "github.com/IBM/fp-go/v2/option"
)
// FindFirst finds the first element which satisfies a predicate function.
// Returns Some(element) if found, None if no element matches.
//
// Example:
//
// findGreaterThan3 := array.FindFirst(func(x int) bool { return x > 3 })
// result := findGreaterThan3([]int{1, 2, 4, 5}) // Some(4)
// result2 := findGreaterThan3([]int{1, 2, 3}) // None
//
//go:inline
func FindFirst[A any](pred func(A) bool) func([]A) O.Option[A] {
return G.FindFirst[[]A](pred)
}
// FindFirstWithIndex finds the first element which satisfies a predicate function that also receives the index.
// Returns Some(element) if found, None if no element matches.
//
// Example:
//
// findEvenAtEvenIndex := array.FindFirstWithIndex(func(i, x int) bool {
// return i%2 == 0 && x%2 == 0
// })
// result := findEvenAtEvenIndex([]int{1, 3, 4, 5}) // Some(4)
//
//go:inline
func FindFirstWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
return G.FindFirstWithIndex[[]A](pred)
}
// FindFirstMap finds the first element for which the selector function returns Some.
// This combines finding and mapping in a single operation.
//
// Example:
//
// import "strconv"
//
// parseFirst := array.FindFirstMap(func(s string) option.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return option.Some(n)
// }
// return option.None[int]()
// })
// result := parseFirst([]string{"a", "42", "b"}) // Some(42)
//
//go:inline
func FindFirstMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
return G.FindFirstMap[[]A](sel)
}
// FindFirstMapWithIndex finds the first element for which the selector function returns Some.
// The selector receives both the index and the element.
//
//go:inline
func FindFirstMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
return G.FindFirstMapWithIndex[[]A](sel)
}
// FindLast finds the last element which satisfies a predicate function.
// Returns Some(element) if found, None if no element matches.
//
// Example:
//
// findGreaterThan3 := array.FindLast(func(x int) bool { return x > 3 })
// result := findGreaterThan3([]int{1, 4, 2, 5}) // Some(5)
//
//go:inline
func FindLast[A any](pred func(A) bool) func([]A) O.Option[A] {
return G.FindLast[[]A](pred)
}
// FindLastWithIndex finds the last element which satisfies a predicate function that also receives the index.
// Returns Some(element) if found, None if no element matches.
//
//go:inline
func FindLastWithIndex[A any](pred func(int, A) bool) func([]A) O.Option[A] {
return G.FindLastWithIndex[[]A](pred)
}
// FindLastMap finds the last element for which the selector function returns Some.
// This combines finding and mapping in a single operation, searching from the end.
//
//go:inline
func FindLastMap[A, B any](sel func(A) O.Option[B]) func([]A) O.Option[B] {
return G.FindLastMap[[]A](sel)
}
// FindLastMapWithIndex finds the last element for which the selector function returns Some.
// The selector receives both the index and the element, searching from the end.
//
//go:inline
func FindLastMapWithIndex[A, B any](sel func(int, A) O.Option[B]) func([]A) O.Option[B] {
return G.FindLastMapWithIndex[[]A](sel)
}

105
v2/array/find_test.go Normal file
View File

@@ -0,0 +1,105 @@
// 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 (
"fmt"
"testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
func TestFindFirstWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
finder := FindFirstWithIndex(func(i, x int) bool {
return i > 2 && x%2 == 0
})
result := finder(src)
assert.Equal(t, O.Some(4), result)
notFound := FindFirstWithIndex(func(i, x int) bool {
return i > 10
})
assert.Equal(t, O.None[int](), notFound(src))
}
func TestFindFirstMap(t *testing.T) {
src := []string{"a", "42", "b", "100"}
finder := FindFirstMap(func(s string) O.Option[int] {
if len(s) > 1 {
return O.Some(len(s))
}
return O.None[int]()
})
result := finder(src)
assert.Equal(t, O.Some(2), result)
}
func TestFindFirstMapWithIndex(t *testing.T) {
src := []string{"a", "b", "c", "d"}
finder := FindFirstMapWithIndex(func(i int, s string) O.Option[string] {
if i > 1 {
return O.Some(fmt.Sprintf("%d:%s", i, s))
}
return O.None[string]()
})
result := finder(src)
assert.Equal(t, O.Some("2:c"), result)
}
func TestFindLast(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
finder := FindLast(func(x int) bool { return x%2 == 0 })
result := finder(src)
assert.Equal(t, O.Some(4), result)
notFound := FindLast(func(x int) bool { return x > 10 })
assert.Equal(t, O.None[int](), notFound(src))
}
func TestFindLastWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
finder := FindLastWithIndex(func(i, x int) bool {
return i < 3 && x%2 == 0
})
result := finder(src)
assert.Equal(t, O.Some(2), result)
}
func TestFindLastMap(t *testing.T) {
src := []string{"a", "42", "b", "100"}
finder := FindLastMap(func(s string) O.Option[int] {
if len(s) > 1 {
return O.Some(len(s))
}
return O.None[int]()
})
result := finder(src)
assert.Equal(t, O.Some(3), result)
}
func TestFindLastMapWithIndex(t *testing.T) {
src := []string{"a", "b", "c", "d"}
finder := FindLastMapWithIndex(func(i int, s string) O.Option[string] {
if i < 3 {
return O.Some(fmt.Sprintf("%d:%s", i, s))
}
return O.None[string]()
})
result := finder(src)
assert.Equal(t, O.Some("2:c"), result)
}

34
v2/array/generic/any.go Normal file
View File

@@ -0,0 +1,34 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
)
// AnyWithIndex tests if any of the elements in the array matches the predicate
func AnyWithIndex[AS ~[]A, PRED ~func(int, A) bool, A any](pred PRED) func(AS) bool {
return F.Flow2(
FindFirstWithIndex[AS](pred),
O.IsSome[A],
)
}
// Any tests if any of the elements in the array matches the predicate
func Any[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) bool {
return AnyWithIndex[AS](F.Ignore1of2[int](pred))
}

366
v2/array/generic/array.go Normal file
View File

@@ -0,0 +1,366 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
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"
)
// Of constructs a single element array
func Of[GA ~[]A, A any](value A) GA {
return GA{value}
}
func Reduce[GA ~[]A, A, B any](f func(B, A) B, initial B) func(GA) B {
return func(as GA) B {
return MonadReduce(as, f, initial)
}
}
func ReduceWithIndex[GA ~[]A, A, B any](f func(int, B, A) B, initial B) func(GA) B {
return func(as GA) B {
return MonadReduceWithIndex(as, f, initial)
}
}
func ReduceRight[GA ~[]A, A, B any](f func(A, B) B, initial B) func(GA) B {
return func(as GA) B {
return MonadReduceRight(as, f, initial)
}
}
func ReduceRightWithIndex[GA ~[]A, A, B any](f func(int, A, B) B, initial B) func(GA) B {
return func(as GA) B {
return MonadReduceRightWithIndex(as, f, initial)
}
}
func MonadReduce[GA ~[]A, A, B any](fa GA, f func(B, A) B, initial B) B {
return array.Reduce(fa, f, initial)
}
func MonadReduceWithIndex[GA ~[]A, A, B any](fa GA, f func(int, B, A) B, initial B) B {
return array.ReduceWithIndex(fa, f, initial)
}
func MonadReduceRight[GA ~[]A, A, B any](fa GA, f func(A, B) B, initial B) B {
return array.ReduceRight(fa, f, initial)
}
func MonadReduceRightWithIndex[GA ~[]A, A, B any](fa GA, f func(int, A, B) B, initial B) B {
return array.ReduceRightWithIndex(fa, f, initial)
}
// From constructs an array from a set of variadic arguments
func From[GA ~[]A, A any](data ...A) GA {
return data
}
// MakeBy returns a `Array` of length `n` with element `i` initialized with `f(i)`.
func MakeBy[AS ~[]A, F ~func(int) A, A any](n int, f F) AS {
// sanity check
if n <= 0 {
return Empty[AS]()
}
// run the generator function across the input
as := make(AS, n)
for i := n - 1; i >= 0; i-- {
as[i] = f(i)
}
return as
}
func Replicate[AS ~[]A, A any](n int, a A) AS {
return MakeBy[AS](n, F.Constant1[int](a))
}
func Lookup[GA ~[]A, A any](idx int) func(GA) O.Option[A] {
none := O.None[A]()
if idx < 0 {
return F.Constant1[GA](none)
}
return func(as GA) O.Option[A] {
if idx < len(as) {
return O.Some(as[idx])
}
return none
}
}
func Tail[GA ~[]A, A any](as GA) O.Option[GA] {
if array.IsEmpty(as) {
return O.None[GA]()
}
return O.Some(as[1:])
}
func Head[GA ~[]A, A any](as GA) O.Option[A] {
if array.IsEmpty(as) {
return O.None[A]()
}
return O.Some(as[0])
}
func First[GA ~[]A, A any](as GA) O.Option[A] {
return Head(as)
}
func Last[GA ~[]A, A any](as GA) O.Option[A] {
if array.IsEmpty(as) {
return O.None[A]()
}
return O.Some(as[len(as)-1])
}
func Append[GA ~[]A, A any](as GA, a A) GA {
return array.Append(as, a)
}
func Empty[GA ~[]A, A any]() GA {
return array.Empty[GA]()
}
func UpsertAt[GA ~[]A, A any](a A) func(GA) GA {
return array.UpsertAt[GA](a)
}
func MonadMap[GA ~[]A, GB ~[]B, A, B any](as GA, f func(a A) B) GB {
return array.MonadMap[GA, GB](as, f)
}
func Map[GA ~[]A, GB ~[]B, A, B any](f func(a A) B) func(GA) GB {
return array.Map[GA, GB](f)
}
func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(int, A) B) GB {
return array.MonadMapWithIndex[GA, GB](as, f)
}
func MapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
return F.Bind2nd(MonadMapWithIndex[GA, GB, A, B], f)
}
func Size[GA ~[]A, A any](as GA) int {
return len(as)
}
func filterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
result := make(GB, 0, len(fa))
for _, a := range fa {
O.Map(func(b B) B {
result = append(result, b)
return b
})(f(a))
}
return result
}
func filterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(int, A) O.Option[B]) GB {
result := make(GB, 0, len(fa))
for i, a := range fa {
O.Map(func(b B) B {
result = append(result, b)
return b
})(f(i, a))
}
return result
}
func MonadFilterMap[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(A) O.Option[B]) GB {
return filterMap[GA, GB](fa, f)
}
func MonadFilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](fa GA, f func(int, A) O.Option[B]) GB {
return filterMapWithIndex[GA, GB](fa, f)
}
func filterWithIndex[AS ~[]A, PRED ~func(int, A) bool, A any](fa AS, pred PRED) AS {
result := make(AS, 0, len(fa))
for i, a := range fa {
if pred(i, a) {
result = append(result, a)
}
}
return result
}
func FilterWithIndex[AS ~[]A, PRED ~func(int, A) bool, A any](pred PRED) func(AS) AS {
return F.Bind2nd(filterWithIndex[AS, PRED, A], pred)
}
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 {
return F.Flow2(
FilterMap[GA, []GB](f),
Flatten[[]GB],
)
}
func Flatten[GAA ~[]GA, GA ~[]A, A any](mma GAA) GA {
return MonadChain(mma, F.Identity[GA])
}
func FilterMap[GA ~[]A, GB ~[]B, A, B any](f func(A) O.Option[B]) func(GA) GB {
return F.Bind2nd(MonadFilterMap[GA, GB, A, B], f)
}
func FilterMapWithIndex[GA ~[]A, GB ~[]B, A, B any](f func(int, A) O.Option[B]) func(GA) GB {
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] {
left := Empty[GA]()
right := Empty[GA]()
array.Reduce(as, func(c bool, a A) bool {
if pred(a) {
right = append(right, a)
} else {
left = append(left, a)
}
return c
}, true)
// returns the partition
return tuple.MakeTuple2(left, right)
}
func Partition[GA ~[]A, A any](pred func(A) bool) func(GA) tuple.Tuple2[GA, GA] {
return F.Bind2nd(MonadPartition[GA, A], pred)
}
func MonadChain[AS ~[]A, BS ~[]B, A, B any](fa AS, f func(a A) BS) BS {
return array.Reduce(fa, func(bs BS, a A) BS {
return append(bs, f(a)...)
}, Empty[BS]())
}
func Chain[AS ~[]A, BS ~[]B, A, B any](f func(A) BS) func(AS) BS {
return F.Bind2nd(MonadChain[AS, BS, A, B], f)
}
func MonadAp[BS ~[]B, ABS ~[]func(A) B, AS ~[]A, B, A any](fab ABS, fa AS) BS {
return MonadChain(fab, F.Bind1st(MonadMap[AS, BS, A, B], fa))
}
func Ap[BS ~[]B, ABS ~[]func(A) B, AS ~[]A, B, A any](fa AS) func(ABS) BS {
return F.Bind2nd(MonadAp[BS, ABS, AS], fa)
}
func IsEmpty[AS ~[]A, A any](as AS) bool {
return array.IsEmpty(as)
}
func IsNil[GA ~[]A, A any](as GA) bool {
return array.IsNil(as)
}
func IsNonNil[GA ~[]A, A any](as GA) bool {
return array.IsNonNil(as)
}
func Match[AS ~[]A, A, B any](onEmpty func() B, onNonEmpty func(AS) B) func(AS) B {
return func(as AS) B {
if IsEmpty(as) {
return onEmpty()
}
return onNonEmpty(as)
}
}
func MatchLeft[AS ~[]A, A, B any](onEmpty func() B, onNonEmpty func(A, AS) B) func(AS) B {
return func(as AS) B {
if IsEmpty(as) {
return onEmpty()
}
return onNonEmpty(as[0], as[1:])
}
}
//go:inline
func Slice[AS ~[]A, A any](start int, end int) func(AS) AS {
return array.Slice[AS](start, end)
}
//go:inline
func SliceRight[AS ~[]A, A any](start int) func(AS) AS {
return array.SliceRight[AS](start)
}
func Copy[AS ~[]A, A any](b AS) AS {
buf := make(AS, len(b))
copy(buf, b)
return buf
}
func Clone[AS ~[]A, A any](f func(A) A) func(as AS) AS {
// implementation assumes that map does not optimize for the empty array
return Map[AS, AS](f)
}
func FoldMap[AS ~[]A, A, B any](m M.Monoid[B]) func(func(A) B) func(AS) B {
empty := m.Empty()
concat := m.Concat
return func(f func(A) B) func(AS) B {
return func(as AS) B {
return array.Reduce(as, func(cur B, a A) B {
return concat(cur, f(a))
}, empty)
}
}
}
func FoldMapWithIndex[AS ~[]A, A, B any](m M.Monoid[B]) func(func(int, A) B) func(AS) B {
empty := m.Empty()
concat := m.Concat
return func(f func(int, A) B) func(AS) B {
return func(as AS) B {
return array.ReduceWithIndex(as, func(idx int, cur B, a A) B {
return concat(cur, f(idx, a))
}, empty)
}
}
}
func Fold[AS ~[]A, A any](m M.Monoid[A]) func(AS) A {
empty := m.Empty()
concat := m.Concat
return func(as AS) A {
return array.Reduce(as, concat, empty)
}
}
func Push[ENDO ~func(GA) GA, GA ~[]A, A any](a A) ENDO {
return F.Bind2nd(array.Push[GA, A], a)
}
func MonadFlap[FAB ~func(A) B, GFAB ~[]FAB, GB ~[]B, A, B any](fab GFAB, a A) GB {
return FC.MonadFlap(MonadMap[GFAB, GB], fab, a)
}
func Flap[FAB ~func(A) B, GFAB ~[]FAB, GB ~[]B, A, B any](a A) func(GFAB) GB {
return FC.Flap(Map[GFAB, GB], a)
}
func Prepend[ENDO ~func(AS) AS, AS []A, A any](head A) ENDO {
return array.Prepend[ENDO](head)
}

163
v2/array/generic/bind.go Normal file
View File

@@ -0,0 +1,163 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
// result := generic.Do[[]State, State](State{})
func Do[GS ~[]S, S any](
empty S,
) GS {
return Of[GS](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps.
// For arrays, this produces the cartesian product where later steps can use values from earlier steps.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
//
// result := F.Pipe2(
// generic.Do[[]State, State](State{}),
// generic.Bind[[]State, []State, []int, State, State, int](
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// func(s State) []int {
// return []int{1, 2, 3}
// },
// ),
// generic.Bind[[]State, []State, []int, State, State, int](
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) []int {
// // This can access s.X from the previous step
// return []int{s.X * 10, s.X * 20}
// },
// ),
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
func Bind[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) GT,
) func(GS1) GS2 {
return C.Bind(
Chain[GS1, GS2, S1, S2],
Map[GT, GS2, T, S2],
setter,
f,
)
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
func Let[GS1 ~[]S1, GS2 ~[]S2, S1, S2, T any](
key func(T) func(S1) S2,
f func(S1) T,
) func(GS1) GS2 {
return F.Let(
Map[GS1, GS2, S1, S2],
key,
f,
)
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
func LetTo[GS1 ~[]S1, GS2 ~[]S2, S1, S2, B any](
key func(B) func(S1) S2,
b B,
) func(GS1) GS2 {
return F.LetTo(
Map[GS1, GS2, S1, S2],
key,
b,
)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[GS1 ~[]S1, GT ~[]T, S1, T any](
setter func(T) S1,
) func(GT) GS1 {
return C.BindTo(
Map[GT, GS1, T, S1],
setter,
)
}
// ApS attaches a value to a context [S1] to produce a context [S2] by considering
// the context and the value concurrently (using Applicative rather than Monad).
// This allows independent computations to be combined without one depending on the result of the other.
//
// Unlike Bind, which sequences operations, ApS can be used when operations are independent
// and can conceptually run in parallel. For arrays, this produces the cartesian product.
//
// Example:
//
// type State struct {
// X int
// Y string
// }
//
// // These operations are independent and can be combined with ApS
// xValues := []int{1, 2}
// yValues := []string{"a", "b"}
//
// result := F.Pipe2(
// generic.Do[[]State, State](State{}),
// generic.ApS[[]State, []State, []int, State, State, int](
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// xValues,
// ),
// generic.ApS[[]State, []State, []string, State, State, string](
// func(y string) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// yValues,
// ),
// ) // [{1,"a"}, {1,"b"}, {2,"a"}, {2,"b"}]
func ApS[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
setter func(T) func(S1) S2,
fa GT,
) func(GS1) GS2 {
return A.ApS(
Ap[GS2, []func(T) S2, GT, S2, T],
Map[GS1, []func(T) S2, S1, func(T) S2],
setter,
fa,
)
}

97
v2/array/generic/find.go Normal file
View File

@@ -0,0 +1,97 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
)
// FindFirstWithIndex finds the first element which satisfies a predicate (or a refinement) function
func FindFirstWithIndex[AS ~[]A, PRED ~func(int, A) bool, A any](pred PRED) func(AS) O.Option[A] {
none := O.None[A]()
return func(as AS) O.Option[A] {
for i, a := range as {
if pred(i, a) {
return O.Some(a)
}
}
return none
}
}
// FindFirst finds the first element which satisfies a predicate (or a refinement) function
func FindFirst[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) O.Option[A] {
return FindFirstWithIndex[AS](F.Ignore1of2[int](pred))
}
// FindFirstMapWithIndex finds the first element returned by an [O.Option] based selector function
func FindFirstMapWithIndex[AS ~[]A, PRED ~func(int, A) O.Option[B], A, B any](pred PRED) func(AS) O.Option[B] {
none := O.None[B]()
return func(as AS) O.Option[B] {
count := len(as)
for i := 0; i < count; i++ {
out := pred(i, as[i])
if O.IsSome(out) {
return out
}
}
return none
}
}
// FindFirstMap finds the first element returned by an [O.Option] based selector function
func FindFirstMap[AS ~[]A, PRED ~func(A) O.Option[B], A, B any](pred PRED) func(AS) O.Option[B] {
return FindFirstMapWithIndex[AS](F.Ignore1of2[int](pred))
}
// FindLastWithIndex finds the first element which satisfies a predicate (or a refinement) function
func FindLastWithIndex[AS ~[]A, PRED ~func(int, A) bool, A any](pred PRED) func(AS) O.Option[A] {
none := O.None[A]()
return func(as AS) O.Option[A] {
for i := len(as) - 1; i >= 0; i-- {
a := as[i]
if pred(i, a) {
return O.Some(a)
}
}
return none
}
}
// FindLast finds the first element which satisfies a predicate (or a refinement) function
func FindLast[AS ~[]A, PRED ~func(A) bool, A any](pred PRED) func(AS) O.Option[A] {
return FindLastWithIndex[AS](F.Ignore1of2[int](pred))
}
// FindLastMapWithIndex finds the first element returned by an [O.Option] based selector function
func FindLastMapWithIndex[AS ~[]A, PRED ~func(int, A) O.Option[B], A, B any](pred PRED) func(AS) O.Option[B] {
none := O.None[B]()
return func(as AS) O.Option[B] {
for i := len(as) - 1; i >= 0; i-- {
out := pred(i, as[i])
if O.IsSome(out) {
return out
}
}
return none
}
}
// FindLastMap finds the first element returned by an [O.Option] based selector function
func FindLastMap[AS ~[]A, PRED ~func(A) O.Option[B], A, B any](pred PRED) func(AS) O.Option[B] {
return FindLastMapWithIndex[AS](F.Ignore1of2[int](pred))
}

43
v2/array/generic/monad.go Normal file
View File

@@ -0,0 +1,43 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
"github.com/IBM/fp-go/v2/internal/monad"
)
type arrayMonad[A, B any, GA ~[]A, GB ~[]B, GAB ~[]func(A) B] struct{}
func (o *arrayMonad[A, B, GA, GB, GAB]) Of(a A) GA {
return Of[GA](a)
}
func (o *arrayMonad[A, B, GA, GB, GAB]) Map(f func(A) B) func(GA) GB {
return Map[GA, GB](f)
}
func (o *arrayMonad[A, B, GA, GB, GAB]) Chain(f func(A) GB) func(GA) GB {
return Chain[GA](f)
}
func (o *arrayMonad[A, B, GA, GB, GAB]) Ap(fa GA) func(GAB) GB {
return Ap[GB, GAB](fa)
}
// Monad implements the monadic operations for an array
func Monad[A, B any, GA ~[]A, GB ~[]B, GAB ~[]func(A) B]() monad.Monad[A, B, GA, GB, GAB] {
return &arrayMonad[A, B, GA, GB, GAB]{}
}

56
v2/array/generic/sort.go Normal file
View File

@@ -0,0 +1,56 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
"sort"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/ord"
)
// Sort implements a stable sort on the array given the provided ordering
func Sort[GA ~[]T, T any](ord O.Ord[T]) func(ma GA) GA {
return SortByKey[GA](ord, F.Identity[T])
}
// SortByKey implements a stable sort on the array given the provided ordering on an extracted key
func SortByKey[GA ~[]T, K, T any](ord O.Ord[K], f func(T) K) func(ma GA) GA {
return func(ma GA) GA {
// nothing to sort
l := len(ma)
if l < 2 {
return ma
}
// copy
cpy := make(GA, l)
copy(cpy, ma)
sort.Slice(cpy, func(i, j int) bool {
return ord.Compare(f(cpy[i]), f(cpy[j])) < 0
})
return cpy
}
}
// SortBy implements a stable sort on the array given the provided ordering
func SortBy[GA ~[]T, GO ~[]O.Ord[T], T any](ord GO) func(ma GA) GA {
return F.Pipe2(
ord,
Fold[GO](O.Monoid[T]()),
Sort[GA, T],
)
}

32
v2/array/generic/uniq.go Normal file
View File

@@ -0,0 +1,32 @@
package generic
import F "github.com/IBM/fp-go/v2/function"
// StrictUniq converts an array of arbitrary items into an array or unique items
// where uniqueness is determined by the built-in uniqueness constraint
func StrictUniq[AS ~[]A, A comparable](as AS) AS {
return Uniq[AS](F.Identity[A])(as)
}
// uniquePredUnsafe returns a predicate on a map for uniqueness
func uniquePredUnsafe[PRED ~func(A) K, A any, K comparable](f PRED) func(int, A) bool {
lookup := make(map[K]bool)
return func(_ int, a A) bool {
k := f(a)
_, has := lookup[k]
if has {
return false
}
lookup[k] = true
return true
}
}
// Uniq converts an array of arbitrary items into an array or unique items
// where uniqueness is determined based on a key extractor function
func Uniq[AS ~[]A, PRED ~func(A) K, A any, K comparable](f PRED) func(as AS) AS {
return func(as AS) AS {
// we need to create a new predicate for each iteration
return filterWithIndex(as, uniquePredUnsafe(f))
}
}

52
v2/array/generic/zip.go Normal file
View File

@@ -0,0 +1,52 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package generic
import (
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
T "github.com/IBM/fp-go/v2/tuple"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays, collecting the results in a new array. If one
// input array is short, excess elements of the longer array are discarded.
func ZipWith[AS ~[]A, BS ~[]B, CS ~[]C, FCT ~func(A, B) C, A, B, C any](fa AS, fb BS, f FCT) CS {
l := N.Min(len(fa), len(fb))
res := make(CS, l)
for i := l - 1; i >= 0; i-- {
res[i] = f(fa[i], fb[i])
}
return res
}
// 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])
}
// 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] {
l := len(cs)
as := make(AS, l)
bs := make(BS, l)
for i := l - 1; i >= 0; i-- {
t := cs[i]
as[i] = t.F1
bs[i] = t.F2
}
return T.MakeTuple2(as, bs)
}

40
v2/array/magma.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
import (
M "github.com/IBM/fp-go/v2/monoid"
)
// ConcatAll concatenates all elements of an array using the provided Monoid.
// This reduces the array to a single value by repeatedly applying the Monoid's concat operation.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
//
// // Sum all numbers
// sumAll := array.ConcatAll(monoid.MonoidSum[int]())
// result := sumAll([]int{1, 2, 3, 4, 5}) // 15
//
// // Concatenate all strings
// concatStrings := array.ConcatAll(monoid.MonoidString())
// result2 := concatStrings([]string{"Hello", " ", "World"}) // "Hello World"
//
//go:inline
func ConcatAll[A any](m M.Monoid[A]) func([]A) A {
return Reduce(m.Concat, m.Empty())
}

36
v2/array/magma_test.go Normal file
View File

@@ -0,0 +1,36 @@
// 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"
"github.com/stretchr/testify/assert"
M "github.com/IBM/fp-go/v2/monoid"
)
var subInt = M.MakeMonoid(func(first int, second int) int {
return first - second
}, 0)
func TestConcatAll(t *testing.T) {
var subAll = ConcatAll(subInt)
assert.Equal(t, subAll([]int{1, 2, 3}), -6)
}

161
v2/array/misc_test.go Normal file
View File

@@ -0,0 +1,161 @@
// 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"
OR "github.com/IBM/fp-go/v2/ord"
"github.com/stretchr/testify/assert"
)
func TestAnyWithIndex(t *testing.T) {
src := []int{1, 2, 3, 4, 5}
checker := AnyWithIndex(func(i, x int) bool {
return i == 2 && x == 3
})
assert.True(t, checker(src))
checker2 := AnyWithIndex(func(i, x int) bool {
return i == 10
})
assert.False(t, checker2(src))
}
func TestSemigroup(t *testing.T) {
sg := Semigroup[int]()
result := sg.Concat([]int{1, 2}, []int{3, 4})
assert.Equal(t, []int{1, 2, 3, 4}, result)
}
func TestArrayConcatAll(t *testing.T) {
result := ArrayConcatAll(
[]int{1, 2},
[]int{3, 4},
[]int{5, 6},
)
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, result)
// Test with empty arrays
result2 := ArrayConcatAll(
[]int{},
[]int{1},
[]int{},
)
assert.Equal(t, []int{1}, result2)
}
func TestMonad(t *testing.T) {
m := Monad[int, string]()
// Test Map
mapFn := m.Map(func(x int) string {
return string(rune('a' + x - 1))
})
mapped := mapFn([]int{1, 2, 3})
assert.Equal(t, []string{"a", "b", "c"}, mapped)
// Test Chain
chainFn := m.Chain(func(x int) []string {
return []string{string(rune('a' + x - 1))}
})
chained := chainFn([]int{1, 2})
assert.Equal(t, []string{"a", "b"}, chained)
// Test Of
ofResult := m.Of(42)
assert.Equal(t, []int{42}, ofResult)
}
func TestSortByKey(t *testing.T) {
type Person struct {
Name string
Age int
}
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Charlie", 35},
}
sorter := SortByKey(OR.FromStrictCompare[int](), func(p Person) int {
return p.Age
})
result := sorter(people)
assert.Equal(t, "Bob", result[0].Name)
assert.Equal(t, "Alice", result[1].Name)
assert.Equal(t, "Charlie", result[2].Name)
}
func TestMonadTraverse(t *testing.T) {
result := MonadTraverse(
O.Of[[]int],
O.Map[[]int, func(int) []int],
O.Ap[[]int, int],
[]int{1, 3, 5},
func(n int) O.Option[int] {
if n%2 == 1 {
return O.Some(n * 2)
}
return O.None[int]()
},
)
assert.Equal(t, O.Some([]int{2, 6, 10}), result)
// Test with None case
result2 := MonadTraverse(
O.Of[[]int],
O.Map[[]int, func(int) []int],
O.Ap[[]int, int],
[]int{1, 2, 3},
func(n int) O.Option[int] {
if n%2 == 1 {
return O.Some(n * 2)
}
return O.None[int]()
},
)
assert.Equal(t, O.None[[]int](), result2)
}
func TestUniqByKey(t *testing.T) {
type Person struct {
Name string
Age int
}
people := []Person{
{"Alice", 30},
{"Bob", 25},
{"Alice", 35},
{"Charlie", 30},
}
uniquer := Uniq(func(p Person) string {
return p.Name
})
result := uniquer(people)
assert.Equal(t, 3, len(result))
assert.Equal(t, "Alice", result[0].Name)
assert.Equal(t, "Bob", result[1].Name)
assert.Equal(t, "Charlie", result[2].Name)
}

41
v2/array/monad.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright (c) 2024 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
"github.com/IBM/fp-go/v2/internal/monad"
)
// Monad returns the monadic operations for an array.
// This provides a structured way to access all monad operations (Map, Chain, Ap, Of)
// for arrays in a single interface.
//
// The Monad interface is useful when you need to pass monadic operations as parameters
// or when working with generic code that operates on any monad.
//
// Example:
//
// m := array.Monad[int, string]()
// result := m.Chain([]int{1, 2, 3}, func(x int) []string {
// return []string{fmt.Sprintf("%d", x), fmt.Sprintf("%d!", x)}
// })
// // Result: ["1", "1!", "2", "2!", "3", "3!"]
//
//go:inline
func Monad[A, B any]() monad.Monad[A, B, []A, []B, []func(A) B] {
return G.Monad[A, B, []A, []B, []func(A) B]()
}

89
v2/array/monoid.go Normal file
View File

@@ -0,0 +1,89 @@
// 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 (
"github.com/IBM/fp-go/v2/internal/array"
M "github.com/IBM/fp-go/v2/monoid"
S "github.com/IBM/fp-go/v2/semigroup"
)
func concat[T any](left, right []T) []T {
// some performance checks
ll := len(left)
if ll == 0 {
return right
}
lr := len(right)
if lr == 0 {
return left
}
// need to copy
buf := make([]T, ll+lr)
copy(buf[copy(buf, left):], right)
return buf
}
// Monoid returns a Monoid instance for arrays.
// The Monoid combines arrays through concatenation, with an empty array as the identity element.
//
// Example:
//
// m := array.Monoid[int]()
// result := m.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
// empty := m.Empty() // []
func Monoid[T any]() M.Monoid[[]T] {
return M.MakeMonoid(concat[T], Empty[T]())
}
// Semigroup returns a Semigroup instance for arrays.
// The Semigroup combines arrays through concatenation.
//
// Example:
//
// s := array.Semigroup[int]()
// result := s.Concat([]int{1, 2}, []int{3, 4}) // [1, 2, 3, 4]
func Semigroup[T any]() S.Semigroup[[]T] {
return S.MakeSemigroup(concat[T])
}
func addLen[A any](count int, data []A) int {
return count + len(data)
}
// ArrayConcatAll efficiently concatenates multiple arrays into a single array.
// This function pre-allocates the exact amount of memory needed and performs
// a single copy operation for each input array, making it more efficient than
// repeated concatenations.
//
// Example:
//
// result := array.ArrayConcatAll(
// []int{1, 2},
// []int{3, 4},
// []int{5, 6},
// ) // [1, 2, 3, 4, 5, 6]
func ArrayConcatAll[A any](data ...[]A) []A {
// get the full size
count := array.Reduce(data, addLen[A], 0)
buf := make([]A, count)
// copy
array.Reduce(data, func(idx int, seg []A) int {
return idx + copy(buf[idx:], seg)
}, 0)
// returns the final array
return buf
}

26
v2/array/monoid_test.go Normal file
View File

@@ -0,0 +1,26 @@
// 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"
M "github.com/IBM/fp-go/v2/monoid/testing"
)
func TestMonoid(t *testing.T) {
M.AssertLaws(t, Monoid[int]())([][]int{{}, {1}, {1, 2}})
}

136
v2/array/nonempty/array.go Normal file
View File

@@ -0,0 +1,136 @@
// 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 nonempty
import (
G "github.com/IBM/fp-go/v2/array/generic"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/array"
S "github.com/IBM/fp-go/v2/semigroup"
)
// NonEmptyArray represents an array with at least one element
type NonEmptyArray[A any] []A
// Of constructs a single element array
func Of[A any](first A) NonEmptyArray[A] {
return G.Of[NonEmptyArray[A]](first)
}
// From constructs a [NonEmptyArray] from a set of variadic arguments
func From[A any](first A, data ...A) NonEmptyArray[A] {
count := len(data)
if count == 0 {
return Of(first)
}
// allocate the requested buffer
buffer := make(NonEmptyArray[A], count+1)
buffer[0] = first
copy(buffer[1:], data)
return buffer
}
func IsEmpty[A any](_ NonEmptyArray[A]) bool {
return false
}
func IsNonEmpty[A any](_ NonEmptyArray[A]) bool {
return true
}
func MonadMap[A, B any](as NonEmptyArray[A], f func(a A) B) NonEmptyArray[B] {
return G.MonadMap[NonEmptyArray[A], NonEmptyArray[B]](as, f)
}
func Map[A, B any](f func(a A) B) func(NonEmptyArray[A]) NonEmptyArray[B] {
return F.Bind2nd(MonadMap[A, B], f)
}
func Reduce[A, B any](f func(B, A) B, initial B) func(NonEmptyArray[A]) B {
return func(as NonEmptyArray[A]) B {
return array.Reduce(as, f, initial)
}
}
func ReduceRight[A, B any](f func(A, B) B, initial B) func(NonEmptyArray[A]) B {
return func(as NonEmptyArray[A]) B {
return array.ReduceRight(as, f, initial)
}
}
func Tail[A any](as NonEmptyArray[A]) []A {
return as[1:]
}
func Head[A any](as NonEmptyArray[A]) A {
return as[0]
}
func First[A any](as NonEmptyArray[A]) A {
return as[0]
}
func Last[A any](as NonEmptyArray[A]) A {
return as[len(as)-1]
}
func Size[A any](as NonEmptyArray[A]) int {
return G.Size(as)
}
func Flatten[A any](mma NonEmptyArray[NonEmptyArray[A]]) NonEmptyArray[A] {
return G.Flatten(mma)
}
func MonadChain[A, B any](fa NonEmptyArray[A], f func(a A) NonEmptyArray[B]) NonEmptyArray[B] {
return G.MonadChain(fa, f)
}
func Chain[A, B any](f func(A) NonEmptyArray[B]) func(NonEmptyArray[A]) NonEmptyArray[B] {
return G.Chain[NonEmptyArray[A]](f)
}
func MonadAp[B, A any](fab NonEmptyArray[func(A) B], fa NonEmptyArray[A]) NonEmptyArray[B] {
return G.MonadAp[NonEmptyArray[B]](fab, fa)
}
func Ap[B, A any](fa NonEmptyArray[A]) func(NonEmptyArray[func(A) B]) NonEmptyArray[B] {
return G.Ap[NonEmptyArray[B], NonEmptyArray[func(A) B]](fa)
}
// FoldMap maps and folds a [NonEmptyArray]. Map the [NonEmptyArray] passing each value to the iterating function. Then fold the results using the provided [Semigroup].
func FoldMap[A, B any](s S.Semigroup[B]) func(func(A) B) func(NonEmptyArray[A]) B {
return func(f func(A) B) func(NonEmptyArray[A]) B {
return func(as NonEmptyArray[A]) B {
return array.Reduce(Tail(as), func(cur B, a A) B {
return s.Concat(cur, f(a))
}, f(Head(as)))
}
}
}
// Fold folds the [NonEmptyArray] using the provided [Semigroup].
func Fold[A any](s S.Semigroup[A]) func(NonEmptyArray[A]) A {
return func(as NonEmptyArray[A]) A {
return array.Reduce(Tail(as), s.Concat, Head(as))
}
}
// Prepend prepends a single value to an array
func Prepend[A any](head A) EM.Endomorphism[NonEmptyArray[A]] {
return array.Prepend[EM.Endomorphism[NonEmptyArray[A]]](head)
}

95
v2/array/sequence.go Normal file
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 array
import (
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
)
// Sequence takes an array where elements are HKT<A> (higher kinded type) and,
// using an applicative of that HKT, returns an HKT of []A.
//
// For example, it can turn:
// - []Either[error, string] into Either[error, []string]
// - []Option[int] into Option[[]int]
//
// Sequence requires an Applicative of the HKT you are targeting. To turn an
// []Either[E, A] into an Either[E, []A], it needs an Applicative for Either.
// To turn an []Option[A] into an Option[[]A], it needs an Applicative for Option.
//
// Note: We need to pass the members of the applicative explicitly because Go does not
// support higher kinded types or template methods on structs or interfaces.
//
// Type parameters:
// - HKTA = HKT<A> (e.g., Option[A], Either[E, A])
// - HKTRA = HKT<[]A> (e.g., Option[[]A], Either[E, []A])
// - HKTFRA = HKT<func(A)[]A> (e.g., Option[func(A)[]A])
//
// Example:
//
// import "github.com/IBM/fp-go/v2/option"
//
// opts := []option.Option[int]{
// option.Some(1),
// option.Some(2),
// option.Some(3),
// }
//
// seq := array.Sequence(
// option.Of[[]int],
// option.MonadMap[[]int, func(int) []int],
// option.MonadAp[[]int, int],
// )
// result := seq(opts) // Some([1, 2, 3])
func Sequence[A, HKTA, HKTRA, HKTFRA any](
_of func([]A) HKTRA,
_map func(HKTRA, func([]A) func(A) []A) HKTFRA,
_ap func(HKTFRA, HKTA) HKTRA,
) func([]HKTA) HKTRA {
ca := F.Curry2(Append[A])
empty := _of(Empty[A]())
return Reduce(func(fas HKTRA, fa HKTA) HKTRA {
return _ap(_map(fas, ca), fa)
}, empty)
}
// ArrayOption returns a function to convert a sequence of options into an option of a sequence.
// If all options are Some, returns Some containing an array of all values.
// If any option is None, returns None.
//
// Example:
//
// opts := []option.Option[int]{
// option.Some(1),
// option.Some(2),
// option.Some(3),
// }
// result := array.ArrayOption[int]()(opts) // Some([1, 2, 3])
//
// opts2 := []option.Option[int]{
// option.Some(1),
// option.None[int](),
// option.Some(3),
// }
// result2 := array.ArrayOption[int]()(opts2) // None
func ArrayOption[A any]() func([]O.Option[A]) O.Option[[]A] {
return Sequence(
O.Of[[]A],
O.MonadMap[[]A, func(A) []A],
O.MonadAp[[]A, A],
)
}

31
v2/array/sequence_test.go Normal file
View File

@@ -0,0 +1,31 @@
// 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"
"github.com/stretchr/testify/assert"
O "github.com/IBM/fp-go/v2/option"
)
func TestSequenceOption(t *testing.T) {
seq := ArrayOption[int]()
assert.Equal(t, O.Of([]int{1, 3}), seq([]O.Option[int]{O.Of(1), O.Of(3)}))
assert.Equal(t, O.None[[]int](), seq([]O.Option[int]{O.Of(1), O.None[int]()}))
}

405
v2/array/slice_test.go Normal file
View File

@@ -0,0 +1,405 @@
// 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"
"github.com/stretchr/testify/assert"
)
// TestSliceBasicCases tests normal slicing operations
func TestSliceBasicCases(t *testing.T) {
data := []int{0, 1, 2, 3, 4, 5}
t.Run("normal slice from middle", func(t *testing.T) {
assert.Equal(t, []int{1, 2, 3}, Slice[int](1, 4)(data))
})
t.Run("slice from start", func(t *testing.T) {
assert.Equal(t, []int{0, 1, 2}, Slice[int](0, 3)(data))
})
t.Run("slice to end", func(t *testing.T) {
assert.Equal(t, []int{3, 4, 5}, Slice[int](3, 6)(data))
})
t.Run("slice single element", func(t *testing.T) {
assert.Equal(t, []int{2}, Slice[int](2, 3)(data))
})
t.Run("slice entire array", func(t *testing.T) {
assert.Equal(t, []int{0, 1, 2, 3, 4, 5}, Slice[int](0, 6)(data))
})
}
// TestSliceNegativeIndices tests negative index handling (counting from end)
func TestSliceNegativeIndices(t *testing.T) {
data := []int{0, 1, 2, 3, 4, 5}
t.Run("negative start index", func(t *testing.T) {
// -2 means length + (-2) = 6 - 2 = 4
assert.Equal(t, []int{4, 5}, Slice[int](-2, 6)(data))
})
t.Run("negative end index", func(t *testing.T) {
// -2 means length + (-2) = 6 - 2 = 4
assert.Equal(t, []int{0, 1, 2, 3}, Slice[int](0, -2)(data))
})
t.Run("both negative indices", func(t *testing.T) {
// -4 = 2, -2 = 4
assert.Equal(t, []int{2, 3}, Slice[int](-4, -2)(data))
})
t.Run("negative index beyond array start", func(t *testing.T) {
// -10 would be -4, clamped to 0
assert.Equal(t, []int{0, 1, 2}, Slice[int](-10, 3)(data))
})
t.Run("negative end index beyond array start", func(t *testing.T) {
// -10 would be -4, clamped to 0
assert.Equal(t, []int{}, Slice[int](0, -10)(data))
})
}
// TestSliceEmptyArray tests slicing on empty arrays (totality proof)
func TestSliceEmptyArray(t *testing.T) {
empty := []int{}
t.Run("slice empty array with zero indices", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](0, 0)(empty))
})
t.Run("slice empty array with positive indices", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](0, 5)(empty))
})
t.Run("slice empty array with negative indices", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](-1, -1)(empty))
})
t.Run("slice empty array with mixed indices", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](-5, 5)(empty))
})
}
// TestSliceOutOfBounds tests out-of-bounds scenarios (totality proof)
func TestSliceOutOfBounds(t *testing.T) {
data := []int{0, 1, 2, 3, 4}
t.Run("start index beyond array length", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](10, 15)(data))
})
t.Run("end index beyond array length", func(t *testing.T) {
assert.Equal(t, []int{2, 3, 4}, Slice[int](2, 100)(data))
})
t.Run("both indices beyond array length", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](10, 20)(data))
})
t.Run("start equals array length", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](5, 10)(data))
})
t.Run("end equals array length", func(t *testing.T) {
assert.Equal(t, []int{3, 4}, Slice[int](3, 5)(data))
})
}
// TestSliceInvalidRanges tests invalid range scenarios (totality proof)
func TestSliceInvalidRanges(t *testing.T) {
data := []int{0, 1, 2, 3, 4}
t.Run("start equals end", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](2, 2)(data))
})
t.Run("start greater than end", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](4, 2)(data))
})
t.Run("start greater than end with negative indices", func(t *testing.T) {
// -1 = 4, -3 = 2
assert.Equal(t, []int{}, Slice[int](-1, -3)(data))
})
t.Run("zero range at start", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](0, 0)(data))
})
t.Run("zero range at end", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](5, 5)(data))
})
}
// TestSliceEdgeCases tests additional edge cases (totality proof)
func TestSliceEdgeCases(t *testing.T) {
t.Run("single element array - slice all", func(t *testing.T) {
data := []int{42}
assert.Equal(t, []int{42}, Slice[int](0, 1)(data))
})
t.Run("single element array - slice none", func(t *testing.T) {
data := []int{42}
assert.Equal(t, []int{}, Slice[int](1, 1)(data))
})
t.Run("single element array - negative indices", func(t *testing.T) {
data := []int{42}
assert.Equal(t, []int{42}, Slice[int](-1, 1)(data))
})
t.Run("large array slice", func(t *testing.T) {
data := MakeBy(1000, func(i int) int { return i })
result := Slice[int](100, 200)(data)
assert.Equal(t, 100, len(result))
assert.Equal(t, 100, result[0])
assert.Equal(t, 199, result[99])
})
}
// TestSliceWithDifferentTypes tests that Slice works with different types (totality proof)
func TestSliceWithDifferentTypes(t *testing.T) {
t.Run("string slice", func(t *testing.T) {
data := []string{"a", "b", "c", "d", "e"}
assert.Equal(t, []string{"b", "c", "d"}, Slice[string](1, 4)(data))
})
t.Run("float slice", func(t *testing.T) {
data := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
assert.Equal(t, []float64{2.2, 3.3}, Slice[float64](1, 3)(data))
})
t.Run("bool slice", func(t *testing.T) {
data := []bool{true, false, true, false}
assert.Equal(t, []bool{false, true}, Slice[bool](1, 3)(data))
})
t.Run("struct slice", func(t *testing.T) {
type Point struct{ X, Y int }
data := []Point{{1, 2}, {3, 4}, {5, 6}}
assert.Equal(t, []Point{{3, 4}}, Slice[Point](1, 2)(data))
})
t.Run("pointer slice", func(t *testing.T) {
a, b, c := 1, 2, 3
data := []*int{&a, &b, &c}
result := Slice[*int](1, 3)(data)
assert.Equal(t, 2, len(result))
assert.Equal(t, 2, *result[0])
assert.Equal(t, 3, *result[1])
})
}
// TestSliceNilArray tests behavior with nil arrays (totality proof)
func TestSliceNilArray(t *testing.T) {
var nilArray []int
t.Run("slice nil array with zero indices", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](0, 0)(nilArray))
})
t.Run("slice nil array with positive indices", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](0, 5)(nilArray))
})
t.Run("slice nil array with negative indices", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](-1, 1)(nilArray))
})
t.Run("slice nil array with out of bounds indices", func(t *testing.T) {
assert.Equal(t, []int{}, Slice[int](10, 20)(nilArray))
})
}
// TestSliceComposition tests that Slice can be composed with other functions
func TestSliceComposition(t *testing.T) {
data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
t.Run("compose multiple slices", func(t *testing.T) {
// First slice [2:8], then slice [1:4] of result
slice1 := Slice[int](2, 8)
slice2 := Slice[int](1, 4)
result := slice2(slice1(data))
// [2,3,4,5,6,7] -> [3,4,5]
assert.Equal(t, []int{3, 4, 5}, result)
})
t.Run("slice then map", func(t *testing.T) {
sliced := Slice[int](2, 5)(data)
mapped := Map(func(x int) int { return x * 2 })(sliced)
assert.Equal(t, []int{4, 6, 8}, mapped)
})
t.Run("slice then filter", func(t *testing.T) {
sliced := Slice[int](0, 6)(data)
filtered := Filter(func(x int) bool { return x%2 == 0 })(sliced)
assert.Equal(t, []int{0, 2, 4}, filtered)
})
}
// TestSliceImmutability tests that Slice doesn't modify the original array
func TestSliceImmutability(t *testing.T) {
original := []int{0, 1, 2, 3, 4}
originalCopy := []int{0, 1, 2, 3, 4}
t.Run("slicing doesn't modify original", func(t *testing.T) {
result := Slice[int](1, 4)(original)
assert.Equal(t, []int{1, 2, 3}, result)
assert.Equal(t, originalCopy, original)
})
t.Run("slice shares underlying array with original", func(t *testing.T) {
// Note: Go's slice operation creates a view of the underlying array,
// not a deep copy. This is expected behavior and matches Go's built-in slice semantics.
result := Slice[int](1, 4)(original)
result[0] = 999
// The original array is affected because slices share the underlying array
assert.Equal(t, 999, original[1], "Slices share underlying array (expected Go behavior)")
})
}
// TestSliceTotality is a comprehensive test proving Slice is a total function
// A total function is defined for all possible inputs and never panics
func TestSliceTotality(t *testing.T) {
testCases := []struct {
name string
data []int
low int
high int
panic bool // Should always be false for a total function
}{
// Normal cases
{"normal range", []int{1, 2, 3, 4, 5}, 1, 3, false},
{"full range", []int{1, 2, 3}, 0, 3, false},
{"empty result", []int{1, 2, 3}, 1, 1, false},
// Edge cases with empty/nil arrays
{"empty array", []int{}, 0, 0, false},
{"empty array with indices", []int{}, 1, 5, false},
{"nil array", nil, 0, 5, false},
// Negative indices
{"negative low", []int{1, 2, 3, 4, 5}, -2, 5, false},
{"negative high", []int{1, 2, 3, 4, 5}, 0, -1, false},
{"both negative", []int{1, 2, 3, 4, 5}, -3, -1, false},
{"negative beyond bounds", []int{1, 2, 3}, -100, -50, false},
// Out of bounds
{"low beyond length", []int{1, 2, 3}, 10, 20, false},
{"high beyond length", []int{1, 2, 3}, 1, 100, false},
{"both beyond length", []int{1, 2, 3}, 10, 20, false},
// Invalid ranges
{"low equals high", []int{1, 2, 3}, 2, 2, false},
{"low greater than high", []int{1, 2, 3}, 3, 1, false},
{"negative invalid range", []int{1, 2, 3, 4, 5}, -1, -3, false},
// Extreme values
{"very large indices", []int{1, 2, 3}, 1000000, 2000000, false},
{"very negative indices", []int{1, 2, 3}, -1000000, -500000, false},
{"mixed extreme", []int{1, 2, 3}, -1000000, 1000000, false},
// Zero values
{"zero indices", []int{1, 2, 3}, 0, 0, false},
{"zero low", []int{1, 2, 3}, 0, 3, false},
{"zero high", []int{1, 2, 3}, 0, 0, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// This test proves totality by ensuring no panic occurs
defer func() {
if r := recover(); r != nil {
if !tc.panic {
t.Errorf("Slice panicked unexpectedly: %v", r)
}
} else {
if tc.panic {
t.Errorf("Slice should have panicked but didn't")
}
}
}()
// Execute the function - if it's total, it will never panic
result := Slice[int](tc.low, tc.high)(tc.data)
// Additional verification: result should always be a valid slice
assert.NotNil(t, result, "Result should never be nil")
assert.True(t, len(result) >= 0, "Result length should be non-negative")
})
}
}
// TestSlicePropertyBased tests mathematical properties of Slice
func TestSlicePropertyBased(t *testing.T) {
data := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
t.Run("identity: Slice(0, len) returns copy of array", func(t *testing.T) {
result := Slice[int](0, len(data))(data)
assert.Equal(t, data, result)
})
t.Run("empty: Slice(i, i) always returns empty", func(t *testing.T) {
for i := 0; i <= len(data); i++ {
result := Slice[int](i, i)(data)
assert.Equal(t, []int{}, result)
}
})
t.Run("length property: len(Slice(i, j)) = max(0, min(j, len) - max(i, 0))", func(t *testing.T) {
testCases := []struct{ low, high, expected int }{
{0, 5, 5},
{2, 7, 5},
{5, 5, 0},
{3, 2, 0}, // invalid range
{-2, 10, 2}, // -2 becomes 8, so slice [8:10] has length 2
{0, 100, 10},
}
for _, tc := range testCases {
result := Slice[int](tc.low, tc.high)(data)
assert.Equal(t, tc.expected, len(result),
"Slice(%d, %d) should have length %d", tc.low, tc.high, tc.expected)
}
})
t.Run("concatenation: Slice(0,i) + Slice(i,len) = original", func(t *testing.T) {
for i := 0; i <= len(data); i++ {
left := Slice[int](0, i)(data)
right := Slice[int](i, len(data))(data)
concatenated := append(left, right...)
assert.Equal(t, data, concatenated)
}
})
t.Run("subset property: all elements in slice are in original", func(t *testing.T) {
result := Slice[int](2, 7)(data)
for _, elem := range result {
found := false
for _, orig := range data {
if elem == orig {
found = true
break
}
}
assert.True(t, found, "Element %d should be in original array", elem)
}
})
}

98
v2/array/sort.go Normal file
View File

@@ -0,0 +1,98 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
O "github.com/IBM/fp-go/v2/ord"
)
// Sort implements a stable sort on the array given the provided ordering.
// The sort is stable, meaning that elements that compare equal retain their original order.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/ord"
//
// numbers := []int{3, 1, 4, 1, 5, 9, 2, 6}
// sorted := array.Sort(ord.FromStrictCompare[int]())(numbers)
// // Result: [1, 1, 2, 3, 4, 5, 6, 9]
//
//go:inline
func Sort[T any](ord O.Ord[T]) func(ma []T) []T {
return G.Sort[[]T](ord)
}
// SortByKey implements a stable sort on the array given the provided ordering on an extracted key.
// This is useful when you want to sort complex types by a specific field.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/ord"
//
// type Person struct {
// Name string
// Age int
// }
//
// people := []Person{
// {"Alice", 30},
// {"Bob", 25},
// {"Charlie", 35},
// }
//
// sortByAge := array.SortByKey(
// ord.FromStrictCompare[int](),
// func(p Person) int { return p.Age },
// )
// sorted := sortByAge(people)
// // Result: [{"Bob", 25}, {"Alice", 30}, {"Charlie", 35}]
//
//go:inline
func SortByKey[K, T any](ord O.Ord[K], f func(T) K) func(ma []T) []T {
return G.SortByKey[[]T](ord, f)
}
// SortBy implements a stable sort on the array using multiple ordering criteria.
// The orderings are applied in sequence: if two elements are equal according to the first
// ordering, the second ordering is used, and so on.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/ord"
//
// type Person struct {
// LastName string
// FirstName string
// }
//
// people := []Person{
// {"Smith", "John"},
// {"Smith", "Alice"},
// {"Jones", "Bob"},
// }
//
// sortByName := array.SortBy([]ord.Ord[Person]{
// ord.Contramap(func(p Person) string { return p.LastName })(ord.FromStrictCompare[string]()),
// ord.Contramap(func(p Person) string { return p.FirstName })(ord.FromStrictCompare[string]()),
// })
// sorted := sortByName(people)
// // Result: [{"Jones", "Bob"}, {"Smith", "Alice"}, {"Smith", "John"}]
//
//go:inline
func SortBy[T any](ord []O.Ord[T]) func(ma []T) []T {
return G.SortBy[[]T](ord)
}

36
v2/array/sort_test.go Normal file
View File

@@ -0,0 +1,36 @@
// 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/ord"
"github.com/stretchr/testify/assert"
)
func TestSort(t *testing.T) {
ordInt := O.FromStrictCompare[int]()
input := []int{2, 1, 3}
res := Sort(ordInt)(input)
assert.Equal(t, []int{1, 2, 3}, res)
assert.Equal(t, []int{2, 1, 3}, input)
}

74
v2/array/testing/laws.go Normal file
View File

@@ -0,0 +1,74 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testing
import (
"testing"
RA "github.com/IBM/fp-go/v2/array"
EQ "github.com/IBM/fp-go/v2/eq"
L "github.com/IBM/fp-go/v2/internal/monad/testing"
)
// AssertLaws asserts the apply monad laws for the array monad
func AssertLaws[A, B, C any](t *testing.T,
eqa EQ.Eq[A],
eqb EQ.Eq[B],
eqc EQ.Eq[C],
ab func(A) B,
bc func(B) C,
) func(a A) bool {
return L.AssertLaws(t,
RA.Eq(eqa),
RA.Eq(eqb),
RA.Eq(eqc),
RA.Of[A],
RA.Of[B],
RA.Of[C],
RA.Of[func(A) A],
RA.Of[func(A) B],
RA.Of[func(B) C],
RA.Of[func(func(A) B) B],
RA.MonadMap[A, A],
RA.MonadMap[A, B],
RA.MonadMap[A, C],
RA.MonadMap[B, C],
RA.MonadMap[func(B) C, func(func(A) B) func(A) C],
RA.MonadChain[A, A],
RA.MonadChain[A, B],
RA.MonadChain[A, C],
RA.MonadChain[B, C],
RA.MonadAp[A, A],
RA.MonadAp[B, A],
RA.MonadAp[C, B],
RA.MonadAp[C, A],
RA.MonadAp[B, func(A) B],
RA.MonadAp[func(A) C, func(A) B],
ab,
bc,
)
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testing
import (
"fmt"
"testing"
EQ "github.com/IBM/fp-go/v2/eq"
"github.com/stretchr/testify/assert"
)
func TestMonadLaws(t *testing.T) {
// some comparison
eqa := EQ.FromStrictEquals[bool]()
eqb := EQ.FromStrictEquals[int]()
eqc := EQ.FromStrictEquals[string]()
ab := func(a bool) int {
if a {
return 1
}
return 0
}
bc := func(b int) string {
return fmt.Sprintf("value %d", b)
}
laws := AssertLaws(t, eqa, eqb, eqc, ab, bc)
assert.True(t, laws(true))
assert.True(t, laws(false))
}

82
v2/array/traverse.go Normal file
View File

@@ -0,0 +1,82 @@
// 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 (
"github.com/IBM/fp-go/v2/internal/array"
)
// Traverse maps each element of an array to an effect (HKT), then collects the results
// into an effect of an array. This is like a combination of Map and Sequence.
//
// Unlike Sequence which works with []HKT<A> -> HKT<[]A>, Traverse works with
// []A -> (A -> HKT<B>) -> HKT<[]B>, allowing you to transform elements while sequencing effects.
//
// Type parameters:
// - HKTB = HKT<B> (e.g., Option[B], Either[E, B])
// - HKTAB = HKT<func(B)[]B> (intermediate type for applicative)
// - HKTRB = HKT<[]B> (e.g., Option[[]B], Either[E, []B])
//
// Example:
//
// import (
// "github.com/IBM/fp-go/v2/option"
// "strconv"
// )
//
// // Parse strings to ints, returning None if any parse fails
// parseAll := array.Traverse(
// option.Of[[]int],
// option.Map[[]int, func(int) []int],
// option.Ap[[]int, int],
// func(s string) option.Option[int] {
// if n, err := strconv.Atoi(s); err == nil {
// return option.Some(n)
// }
// return option.None[int]()
// },
// )
//
// result := parseAll([]string{"1", "2", "3"}) // Some([1, 2, 3])
// result2 := parseAll([]string{"1", "x", "3"}) // None
//
//go:inline
func Traverse[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
f func(A) HKTB) func([]A) HKTRB {
return array.Traverse[[]A](fof, fmap, fap, f)
}
// MonadTraverse is the monadic version of Traverse that takes the array as a parameter.
// It maps each element of an array to an effect (HKT), then collects the results
// into an effect of an array.
//
// This is useful when you want to apply the traverse operation directly without currying.
//
//go:inline
func MonadTraverse[A, B, HKTB, HKTAB, HKTRB any](
fof func([]B) HKTRB,
fmap func(func([]B) func(B) []B) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
ta []A,
f func(A) HKTB) HKTRB {
return array.MonadTraverse(fof, fmap, fap, ta, f)
}

43
v2/array/traverse_test.go Normal file
View File

@@ -0,0 +1,43 @@
// 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"
)
type ArrayType = []int
func TestTraverse(t *testing.T) {
traverse := Traverse(
O.Of[ArrayType],
O.Map[ArrayType, func(int) ArrayType],
O.Ap[ArrayType, int],
func(n int) O.Option[int] {
if n%2 == 0 {
return O.None[int]()
}
return O.Of(n)
})
assert.Equal(t, O.None[[]int](), traverse(ArrayType{1, 2}))
assert.Equal(t, O.Of(ArrayType{1, 3}), traverse(ArrayType{1, 3}))
}

51
v2/array/uniq.go Normal file
View File

@@ -0,0 +1,51 @@
package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
)
// StrictUniq converts an array of arbitrary items into an array of unique items
// where uniqueness is determined by the built-in equality constraint (comparable).
// The first occurrence of each unique value is kept, subsequent duplicates are removed.
//
// Example:
//
// numbers := []int{1, 2, 2, 3, 3, 3, 4}
// unique := array.StrictUniq(numbers) // [1, 2, 3, 4]
//
// strings := []string{"a", "b", "a", "c", "b"}
// unique2 := array.StrictUniq(strings) // ["a", "b", "c"]
//
//go:inline
func StrictUniq[A comparable](as []A) []A {
return G.StrictUniq(as)
}
// Uniq converts an array of arbitrary items into an array of unique items
// where uniqueness is determined based on a key extractor function.
// The first occurrence of each unique key is kept, subsequent duplicates are removed.
//
// This is useful for removing duplicates from arrays of complex types based on a specific field.
//
// Example:
//
// type Person struct {
// Name string
// Age int
// }
//
// people := []Person{
// {"Alice", 30},
// {"Bob", 25},
// {"Alice", 35}, // duplicate name
// {"Charlie", 30},
// }
//
// uniqueByName := array.Uniq(func(p Person) string { return p.Name })
// result := uniqueByName(people)
// // Result: [{"Alice", 30}, {"Bob", 25}, {"Charlie", 30}]
//
//go:inline
func Uniq[A any, K comparable](f func(A) K) func(as []A) []A {
return G.Uniq[[]A](f)
}

14
v2/array/uniq_test.go Normal file
View File

@@ -0,0 +1,14 @@
package array
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUniq(t *testing.T) {
data := From(1, 2, 3, 2, 4, 1)
uniq := StrictUniq(data)
assert.Equal(t, From(1, 2, 3, 4), uniq)
}

83
v2/array/zip.go Normal file
View File

@@ -0,0 +1,83 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array
import (
G "github.com/IBM/fp-go/v2/array/generic"
T "github.com/IBM/fp-go/v2/tuple"
)
// ZipWith applies a function to pairs of elements at the same index in two arrays,
// collecting the results in a new array. If one input array is shorter, excess elements
// of the longer array are discarded.
//
// Example:
//
// names := []string{"Alice", "Bob", "Charlie"}
// ages := []int{30, 25, 35}
//
// result := array.ZipWith(names, ages, func(name string, age int) string {
// return fmt.Sprintf("%s is %d years old", name, age)
// })
// // Result: ["Alice is 30 years old", "Bob is 25 years old", "Charlie is 35 years old"]
//
//go:inline
func ZipWith[FCT ~func(A, B) C, A, B, C any](fa []A, fb []B, f FCT) []C {
return G.ZipWith[[]A, []B, []C](fa, fb, f)
}
// Zip takes two arrays and returns an array of corresponding pairs (tuples).
// If one input array is shorter, excess elements of the longer array are discarded.
//
// Example:
//
// names := []string{"Alice", "Bob", "Charlie"}
// ages := []int{30, 25, 35}
//
// pairs := array.Zip(ages)(names)
// // Result: [(Alice, 30), (Bob, 25), (Charlie, 35)]
//
// // With different lengths
// pairs2 := array.Zip([]int{1, 2})([]string{"a", "b", "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)
}
// Unzip is the reverse of Zip. It takes an array of pairs (tuples) and returns
// two corresponding arrays, one containing all first elements and one containing all second elements.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/tuple"
//
// pairs := []tuple.Tuple2[string, int]{
// tuple.MakeTuple2("Alice", 30),
// tuple.MakeTuple2("Bob", 25),
// tuple.MakeTuple2("Charlie", 35),
// }
//
// result := array.Unzip(pairs)
// // Result: (["Alice", "Bob", "Charlie"], [30, 25, 35])
// names := result.Head // ["Alice", "Bob", "Charlie"]
// ages := result.Tail // [30, 25, 35]
//
//go:inline
func Unzip[A, B any](cs []T.Tuple2[A, B]) T.Tuple2[[]A, []B] {
return G.Unzip[[]A, []B](cs)
}

56
v2/array/zip_test.go Normal file
View File

@@ -0,0 +1,56 @@
// 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 (
"fmt"
"testing"
T "github.com/IBM/fp-go/v2/tuple"
"github.com/stretchr/testify/assert"
)
func TestZipWith(t *testing.T) {
left := From(1, 2, 3)
right := From("a", "b", "c", "d")
res := ZipWith(left, right, func(l int, r string) string {
return fmt.Sprintf("%s%d", r, l)
})
assert.Equal(t, From("a1", "b2", "c3"), res)
}
func TestZip(t *testing.T) {
left := From(1, 2, 3)
right := From("a", "b", "c", "d")
res := Zip[string](left)(right)
assert.Equal(t, From(T.MakeTuple2("a", 1), T.MakeTuple2("b", 2), T.MakeTuple2("c", 3)), res)
}
func TestUnzip(t *testing.T) {
left := From(1, 2, 3)
right := From("a", "b", "c")
zipped := Zip[string](left)(right)
unzipped := Unzip(zipped)
assert.Equal(t, right, unzipped.F1)
assert.Equal(t, left, unzipped.F2)
}

109
v2/assert/assert_test.go Normal file
View File

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

59
v2/boolean/boolean.go Normal file
View File

@@ -0,0 +1,59 @@
// 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 boolean
import (
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/ord"
)
var (
// MonoidAny is the boolean [monoid.Monoid] under disjunction
MonoidAny = monoid.MakeMonoid(
func(l, r bool) bool {
return l || r
},
false,
)
// MonoidAll is the boolean [monoid.Monoid] under conjuction
MonoidAll = monoid.MakeMonoid(
func(l, r bool) bool {
return l && r
},
true,
)
// Eq is the equals predicate for boolean
Eq = eq.FromStrictEquals[bool]()
// Ord is the strict ordering for boolean
Ord = ord.MakeOrd(func(l, r bool) int {
if l {
if r {
return 0
}
return +1
}
if r {
return -1
}
return 0
}, func(l, r bool) bool {
return l == r
})
)

246
v2/boolean/boolean_test.go Normal file
View File

@@ -0,0 +1,246 @@
// 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 boolean
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestMonoidAny(t *testing.T) {
t.Run("identity element is false", func(t *testing.T) {
assert.Equal(t, false, MonoidAny.Empty())
})
t.Run("false OR false = false", func(t *testing.T) {
result := MonoidAny.Concat(false, false)
assert.Equal(t, false, result)
})
t.Run("false OR true = true", func(t *testing.T) {
result := MonoidAny.Concat(false, true)
assert.Equal(t, true, result)
})
t.Run("true OR false = true", func(t *testing.T) {
result := MonoidAny.Concat(true, false)
assert.Equal(t, true, result)
})
t.Run("true OR true = true", func(t *testing.T) {
result := MonoidAny.Concat(true, true)
assert.Equal(t, true, result)
})
t.Run("left identity: empty OR x = x", func(t *testing.T) {
assert.Equal(t, true, MonoidAny.Concat(MonoidAny.Empty(), true))
assert.Equal(t, false, MonoidAny.Concat(MonoidAny.Empty(), false))
})
t.Run("right identity: x OR empty = x", func(t *testing.T) {
assert.Equal(t, true, MonoidAny.Concat(true, MonoidAny.Empty()))
assert.Equal(t, false, MonoidAny.Concat(false, MonoidAny.Empty()))
})
t.Run("associativity: (a OR b) OR c = a OR (b OR c)", func(t *testing.T) {
a, b, c := true, false, true
left := MonoidAny.Concat(MonoidAny.Concat(a, b), c)
right := MonoidAny.Concat(a, MonoidAny.Concat(b, c))
assert.Equal(t, left, right)
})
}
func TestMonoidAll(t *testing.T) {
t.Run("identity element is true", func(t *testing.T) {
assert.Equal(t, true, MonoidAll.Empty())
})
t.Run("false AND false = false", func(t *testing.T) {
result := MonoidAll.Concat(false, false)
assert.Equal(t, false, result)
})
t.Run("false AND true = false", func(t *testing.T) {
result := MonoidAll.Concat(false, true)
assert.Equal(t, false, result)
})
t.Run("true AND false = false", func(t *testing.T) {
result := MonoidAll.Concat(true, false)
assert.Equal(t, false, result)
})
t.Run("true AND true = true", func(t *testing.T) {
result := MonoidAll.Concat(true, true)
assert.Equal(t, true, result)
})
t.Run("left identity: empty AND x = x", func(t *testing.T) {
assert.Equal(t, true, MonoidAll.Concat(MonoidAll.Empty(), true))
assert.Equal(t, false, MonoidAll.Concat(MonoidAll.Empty(), false))
})
t.Run("right identity: x AND empty = x", func(t *testing.T) {
assert.Equal(t, true, MonoidAll.Concat(true, MonoidAll.Empty()))
assert.Equal(t, false, MonoidAll.Concat(false, MonoidAll.Empty()))
})
t.Run("associativity: (a AND b) AND c = a AND (b AND c)", func(t *testing.T) {
a, b, c := true, false, true
left := MonoidAll.Concat(MonoidAll.Concat(a, b), c)
right := MonoidAll.Concat(a, MonoidAll.Concat(b, c))
assert.Equal(t, left, right)
})
}
func TestEq(t *testing.T) {
t.Run("true equals true", func(t *testing.T) {
assert.True(t, Eq.Equals(true, true))
})
t.Run("false equals false", func(t *testing.T) {
assert.True(t, Eq.Equals(false, false))
})
t.Run("true not equals false", func(t *testing.T) {
assert.False(t, Eq.Equals(true, false))
})
t.Run("false not equals true", func(t *testing.T) {
assert.False(t, Eq.Equals(false, true))
})
t.Run("reflexivity: x equals x", func(t *testing.T) {
assert.True(t, Eq.Equals(true, true))
assert.True(t, Eq.Equals(false, false))
})
t.Run("symmetry: if x equals y then y equals x", func(t *testing.T) {
assert.Equal(t, Eq.Equals(true, false), Eq.Equals(false, true))
assert.Equal(t, Eq.Equals(true, true), Eq.Equals(true, true))
})
t.Run("transitivity: if x equals y and y equals z then x equals z", func(t *testing.T) {
x, y, z := true, true, true
if Eq.Equals(x, y) && Eq.Equals(y, z) {
assert.True(t, Eq.Equals(x, z))
}
})
}
func TestOrd(t *testing.T) {
t.Run("false < true", func(t *testing.T) {
result := Ord.Compare(false, true)
assert.Equal(t, -1, result)
})
t.Run("true > false", func(t *testing.T) {
result := Ord.Compare(true, false)
assert.Equal(t, 1, result)
})
t.Run("true == true", func(t *testing.T) {
result := Ord.Compare(true, true)
assert.Equal(t, 0, result)
})
t.Run("false == false", func(t *testing.T) {
result := Ord.Compare(false, false)
assert.Equal(t, 0, result)
})
t.Run("Equals method works", func(t *testing.T) {
assert.True(t, Ord.Equals(true, true))
assert.True(t, Ord.Equals(false, false))
assert.False(t, Ord.Equals(true, false))
assert.False(t, Ord.Equals(false, true))
})
t.Run("reflexivity: x <= x", func(t *testing.T) {
assert.True(t, Ord.Compare(true, true) == 0)
assert.True(t, Ord.Compare(false, false) == 0)
})
t.Run("antisymmetry: if x <= y and y <= x then x == y", func(t *testing.T) {
x, y := true, true
if Ord.Compare(x, y) <= 0 && Ord.Compare(y, x) <= 0 {
assert.Equal(t, 0, Ord.Compare(x, y))
}
})
t.Run("transitivity: if x <= y and y <= z then x <= z", func(t *testing.T) {
x, y, z := false, true, true
if Ord.Compare(x, y) <= 0 && Ord.Compare(y, z) <= 0 {
assert.True(t, Ord.Compare(x, z) <= 0)
}
})
t.Run("totality: x <= y or y <= x", func(t *testing.T) {
assert.True(t, Ord.Compare(true, false) >= 0 || Ord.Compare(false, true) >= 0)
assert.True(t, Ord.Compare(false, true) <= 0 || Ord.Compare(true, false) <= 0)
})
}
// Example tests that also serve as documentation
func ExampleMonoidAny() {
// Combine booleans with OR
result := MonoidAny.Concat(false, true)
println(result) // true
// Identity element
identity := MonoidAny.Empty()
println(identity) // false
// Output:
}
func ExampleMonoidAll() {
// Combine booleans with AND
result := MonoidAll.Concat(true, true)
println(result) // true
// Identity element
identity := MonoidAll.Empty()
println(identity) // true
// Output:
}
func ExampleEq() {
// Check equality
equal := Eq.Equals(true, true)
println(equal) // true
notEqual := Eq.Equals(true, false)
println(notEqual) // false
// Output:
}
func ExampleOrd() {
// Compare booleans (false < true)
cmp := Ord.Compare(false, true)
println(cmp) // -1
cmp2 := Ord.Compare(true, false)
println(cmp2) // 1
cmp3 := Ord.Compare(true, true)
println(cmp3) // 0
// Output:
}

138
v2/boolean/doc.go Normal file
View File

@@ -0,0 +1,138 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package boolean provides functional programming utilities for working with boolean values.
//
// This package offers algebraic structures (Monoid, Eq, Ord) for boolean values,
// enabling functional composition and reasoning about boolean operations.
//
// # Monoids
//
// The package provides two monoid instances for booleans:
//
// - MonoidAny: Combines booleans using logical OR (disjunction), with false as identity
// - MonoidAll: Combines booleans using logical AND (conjunction), with true as identity
//
// # MonoidAny - Logical OR
//
// MonoidAny implements the boolean monoid under disjunction (OR operation).
// The identity element is false, meaning false OR x = x for any boolean x.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/boolean"
//
// // Combine multiple booleans with OR
// result := boolean.MonoidAny.Concat(false, true) // true
// result2 := boolean.MonoidAny.Concat(false, false) // false
//
// // Identity element
// identity := boolean.MonoidAny.Empty() // false
//
// // Check if any value in a collection is true
// import "github.com/IBM/fp-go/v2/array"
// values := []bool{false, false, true, false}
// anyTrue := array.Fold(boolean.MonoidAny)(values) // true
//
// # MonoidAll - Logical AND
//
// MonoidAll implements the boolean monoid under conjunction (AND operation).
// The identity element is true, meaning true AND x = x for any boolean x.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/boolean"
//
// // Combine multiple booleans with AND
// result := boolean.MonoidAll.Concat(true, true) // true
// result2 := boolean.MonoidAll.Concat(true, false) // false
//
// // Identity element
// identity := boolean.MonoidAll.Empty() // true
//
// // Check if all values in a collection are true
// import "github.com/IBM/fp-go/v2/array"
// values := []bool{true, true, true}
// allTrue := array.Fold(boolean.MonoidAll)(values) // true
//
// # Equality
//
// The Eq instance provides structural equality for booleans:
//
// import "github.com/IBM/fp-go/v2/boolean"
//
// equal := boolean.Eq.Equals(true, true) // true
// equal2 := boolean.Eq.Equals(true, false) // false
//
// # Ordering
//
// The Ord instance provides a total ordering for booleans where false < true:
//
// import "github.com/IBM/fp-go/v2/boolean"
//
// cmp := boolean.Ord.Compare(false, true) // -1 (false < true)
// cmp2 := boolean.Ord.Compare(true, false) // +1 (true > false)
// cmp3 := boolean.Ord.Compare(true, true) // 0 (equal)
//
// # Use Cases
//
// The boolean package is particularly useful for:
//
// - Combining multiple boolean conditions functionally
// - Implementing validation logic that accumulates results
// - Working with predicates in a composable way
// - Folding collections of boolean values
//
// Example - Validation:
//
// import (
// "github.com/IBM/fp-go/v2/array"
// "github.com/IBM/fp-go/v2/boolean"
// )
//
// type User struct {
// Name string
// Email string
// Age int
// }
//
// // Define validation predicates
// validations := []func(User) bool{
// func(u User) bool { return len(u.Name) > 0 },
// func(u User) bool { return len(u.Email) > 0 },
// func(u User) bool { return u.Age >= 18 },
// }
//
// // Check if user passes all validations
// user := User{"Alice", "alice@example.com", 25}
// results := array.Map(func(v func(User) bool) bool {
// return v(user)
// })(validations)
// allValid := array.Fold(boolean.MonoidAll)(results) // true
//
// Example - Any Match:
//
// import (
// "github.com/IBM/fp-go/v2/array"
// "github.com/IBM/fp-go/v2/boolean"
// )
//
// // Check if any number is even
// numbers := []int{1, 3, 5, 7, 8, 9}
// checks := array.Map(func(n int) bool {
// return n%2 == 0
// })(numbers)
// hasEven := array.Fold(boolean.MonoidAny)(checks) // true
package boolean

22
v2/boolean/types.go Normal file
View File

@@ -0,0 +1,22 @@
// 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 boolean
import "github.com/IBM/fp-go/v2/monoid"
type (
Monoid = monoid.Monoid[bool]
)

62
v2/bounded/bounded.go Normal file
View File

@@ -0,0 +1,62 @@
// 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 bounded
import "github.com/IBM/fp-go/v2/ord"
type Bounded[T any] interface {
ord.Ord[T]
Top() T
Bottom() T
}
type bounded[T any] struct {
c func(x, y T) int
e func(x, y T) bool
t T
b T
}
func (b bounded[T]) Equals(x, y T) bool {
return b.e(x, y)
}
func (b bounded[T]) Compare(x, y T) int {
return b.c(x, y)
}
func (b bounded[T]) Top() T {
return b.t
}
func (b bounded[T]) Bottom() T {
return b.b
}
// MakeBounded creates an instance of a bounded type
func MakeBounded[T any](o ord.Ord[T], t, b T) Bounded[T] {
return bounded[T]{c: o.Compare, e: o.Equals, t: t, b: b}
}
// Clamp returns a function that clamps against the bounds defined in the bounded type
func Clamp[T any](b Bounded[T]) func(T) T {
return ord.Clamp(b)(b.Bottom(), b.Top())
}
// Reverse reverses the ordering and swaps the bounds
func Reverse[T any](b Bounded[T]) Bounded[T] {
return MakeBounded(ord.Reverse(b), b.Bottom(), b.Top())
}

212
v2/bounded/bounded_test.go Normal file
View File

@@ -0,0 +1,212 @@
// 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 bounded
import (
"testing"
"github.com/IBM/fp-go/v2/ord"
"github.com/stretchr/testify/assert"
)
func TestMakeBounded(t *testing.T) {
t.Run("creates bounded instance with correct top and bottom", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
assert.Equal(t, 100, b.Top())
assert.Equal(t, 0, b.Bottom())
})
t.Run("preserves ordering from Ord", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
assert.Equal(t, -1, b.Compare(5, 10))
assert.Equal(t, 0, b.Compare(5, 5))
assert.Equal(t, 1, b.Compare(10, 5))
})
t.Run("preserves equality from Ord", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
assert.True(t, b.Equals(5, 5))
assert.False(t, b.Equals(5, 10))
})
}
func TestClamp(t *testing.T) {
t.Run("returns value within bounds unchanged", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
clamp := Clamp(b)
assert.Equal(t, 50, clamp(50))
assert.Equal(t, 0, clamp(0))
assert.Equal(t, 100, clamp(100))
})
t.Run("clamps value above top to top", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
clamp := Clamp(b)
assert.Equal(t, 100, clamp(150))
assert.Equal(t, 100, clamp(200))
})
t.Run("clamps value below bottom to bottom", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
clamp := Clamp(b)
assert.Equal(t, 0, clamp(-10))
assert.Equal(t, 0, clamp(-100))
})
t.Run("works with float64", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[float64](), 1.0, 0.0)
clamp := Clamp(b)
assert.Equal(t, 0.5, clamp(0.5))
assert.Equal(t, 1.0, clamp(1.5))
assert.Equal(t, 0.0, clamp(-0.5))
})
t.Run("works with strings", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[string](), "z", "a")
clamp := Clamp(b)
assert.Equal(t, "m", clamp("m"))
assert.Equal(t, "z", clamp("zzz"))
assert.Equal(t, "a", clamp("A"))
})
}
func TestReverse(t *testing.T) {
t.Run("reverses the ordering", func(t *testing.T) {
original := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
reversed := Reverse(original)
// In original: 5 < 10, so Compare(5, 10) = -1
assert.Equal(t, -1, original.Compare(5, 10))
// In reversed: 5 > 10, so Compare(5, 10) = 1
assert.Equal(t, 1, reversed.Compare(5, 10))
})
t.Run("swaps top and bottom values", func(t *testing.T) {
original := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
reversed := Reverse(original)
// Reverse swaps the bounds
assert.Equal(t, original.Bottom(), reversed.Top())
assert.Equal(t, original.Top(), reversed.Bottom())
})
t.Run("double reverse returns to original ordering", func(t *testing.T) {
original := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
reversed := Reverse(original)
doubleReversed := Reverse(reversed)
assert.Equal(t, original.Compare(5, 10), doubleReversed.Compare(5, 10))
assert.Equal(t, original.Compare(10, 5), doubleReversed.Compare(10, 5))
})
t.Run("preserves equality", func(t *testing.T) {
original := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
reversed := Reverse(original)
assert.Equal(t, original.Equals(5, 5), reversed.Equals(5, 5))
assert.Equal(t, original.Equals(5, 10), reversed.Equals(5, 10))
})
}
func TestBoundedLaws(t *testing.T) {
t.Run("bottom is less than or equal to all values", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
testValues := []int{0, 25, 50, 75, 100}
for _, v := range testValues {
assert.True(t, b.Compare(b.Bottom(), v) <= 0,
"Bottom (%d) should be <= %d", b.Bottom(), v)
}
})
t.Run("top is greater than or equal to all values", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
testValues := []int{0, 25, 50, 75, 100}
for _, v := range testValues {
assert.True(t, b.Compare(b.Top(), v) >= 0,
"Top (%d) should be >= %d", b.Top(), v)
}
})
t.Run("bottom is less than or equal to top", func(t *testing.T) {
b := MakeBounded(ord.FromStrictCompare[int](), 100, 0)
assert.True(t, b.Compare(b.Bottom(), b.Top()) <= 0,
"Bottom should be <= Top")
})
}
// Example tests
func ExampleMakeBounded() {
// Create a bounded type for percentages (0-100)
percentage := MakeBounded(
ord.FromStrictCompare[int](),
100, // top
0, // bottom
)
println(percentage.Top()) // 100
println(percentage.Bottom()) // 0
// Output:
}
func ExampleClamp() {
// Create bounded type for percentages
percentage := MakeBounded(
ord.FromStrictCompare[int](),
100, // top
0, // bottom
)
clamp := Clamp(percentage)
println(clamp(50)) // 50 (within bounds)
println(clamp(150)) // 100 (clamped to top)
println(clamp(-10)) // 0 (clamped to bottom)
// Output:
}
func ExampleReverse() {
original := MakeBounded(
ord.FromStrictCompare[int](),
100, // top
0, // bottom
)
reversed := Reverse(original)
// Ordering is reversed
println(original.Compare(5, 10)) // -1 (5 < 10)
println(reversed.Compare(5, 10)) // 1 (5 > 10 in reversed)
// Bounds are swapped
println(reversed.Top()) // 0
println(reversed.Bottom()) // 100
// Output:
}

149
v2/bounded/doc.go Normal file
View File

@@ -0,0 +1,149 @@
// 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 bounded provides types and functions for working with bounded ordered types.
//
// A Bounded type extends Ord with minimum (Bottom) and maximum (Top) values,
// representing types that have well-defined lower and upper bounds.
//
// # Bounded Interface
//
// The Bounded interface combines ordering (Ord) with boundary values:
//
// type Bounded[T any] interface {
// ord.Ord[T]
// Top() T // Maximum value
// Bottom() T // Minimum value
// }
//
// # Creating Bounded Instances
//
// Use MakeBounded to create a Bounded instance from an Ord and boundary values:
//
// import (
// "github.com/IBM/fp-go/v2/bounded"
// "github.com/IBM/fp-go/v2/ord"
// )
//
// // Bounded integers from 0 to 100
// boundedInt := bounded.MakeBounded(
// ord.FromStrictCompare[int](),
// 100, // top
// 0, // bottom
// )
//
// top := boundedInt.Top() // 100
// bottom := boundedInt.Bottom() // 0
//
// # Clamping Values
//
// The Clamp function restricts values to stay within the bounds:
//
// import (
// "github.com/IBM/fp-go/v2/bounded"
// "github.com/IBM/fp-go/v2/ord"
// )
//
// // Create bounded type for percentages (0-100)
// percentage := bounded.MakeBounded(
// ord.FromStrictCompare[int](),
// 100, // top
// 0, // bottom
// )
//
// clamp := bounded.Clamp(percentage)
//
// result1 := clamp(50) // 50 (within bounds)
// result2 := clamp(150) // 100 (clamped to top)
// result3 := clamp(-10) // 0 (clamped to bottom)
//
// # Reversing Bounds
//
// The Reverse function swaps the ordering and bounds:
//
// import (
// "github.com/IBM/fp-go/v2/bounded"
// "github.com/IBM/fp-go/v2/ord"
// )
//
// original := bounded.MakeBounded(
// ord.FromStrictCompare[int](),
// 100, // top
// 0, // bottom
// )
//
// reversed := bounded.Reverse(original)
//
// // In reversed, ordering is flipped and bounds are swapped
// // Compare(10, 20) returns 1 instead of -1
// // Top() returns 0 and Bottom() returns 100
//
// # Use Cases
//
// Bounded types are useful for:
//
// - Representing ranges with well-defined limits (e.g., percentages, grades)
// - Implementing safe arithmetic that stays within bounds
// - Validating input values against constraints
// - Creating domain-specific types with natural boundaries
//
// # Example - Temperature Range
//
// import (
// "github.com/IBM/fp-go/v2/bounded"
// "github.com/IBM/fp-go/v2/ord"
// )
//
// // Celsius temperature range for a thermostat
// thermostat := bounded.MakeBounded(
// ord.FromStrictCompare[float64](),
// 30.0, // max temperature
// 15.0, // min temperature
// )
//
// clampTemp := bounded.Clamp(thermostat)
//
// // User tries to set temperature
// desired := 35.0
// actual := clampTemp(desired) // 30.0 (clamped to maximum)
//
// # Example - Bounded Characters
//
// import (
// "github.com/IBM/fp-go/v2/bounded"
// "github.com/IBM/fp-go/v2/ord"
// )
//
// // Lowercase letters only
// lowercase := bounded.MakeBounded(
// ord.FromStrictCompare[rune](),
// 'z', // top
// 'a', // bottom
// )
//
// clampChar := bounded.Clamp(lowercase)
//
// result1 := clampChar('m') // 'm' (within bounds)
// result2 := clampChar('A') // 'a' (clamped to bottom)
// result3 := clampChar('~') // 'z' (clamped to top)
//
// # Laws
//
// Bounded instances must satisfy the Ord laws plus:
//
// - Bottom is less than or equal to all values: Compare(Bottom(), x) <= 0
// - Top is greater than or equal to all values: Compare(Top(), x) >= 0
// - Bottom <= Top: Compare(Bottom(), Top()) <= 0
package bounded

7
v2/builder/builder.go Normal file
View File

@@ -0,0 +1,7 @@
package builder
type (
Builder[T any] interface {
Build() Result[T]
}
)

12
v2/builder/prism.go Normal file
View File

@@ -0,0 +1,12 @@
package builder
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/result"
)
// BuilderPrism createa a [Prism] that converts between a builder and its type
func BuilderPrism[T any, B Builder[T]](creator func(T) B) Prism[B, T] {
return prism.MakePrism(F.Flow2(B.Build, result.ToOption[T]), creator)
}

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

@@ -0,0 +1,15 @@
package builder
import (
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
)
type (
Result[T any] = result.Result[T]
Prism[S, A any] = prism.Prism[S, A]
Option[T any] = option.Option[T]
)

28
v2/bytes/bytes.go Normal file
View File

@@ -0,0 +1,28 @@
// 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 bytes
func Empty() []byte {
return Monoid.Empty()
}
func ToString(a []byte) string {
return string(a)
}
func Size(as []byte) int {
return len(as)
}

221
v2/bytes/bytes_test.go Normal file
View File

@@ -0,0 +1,221 @@
// 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 bytes
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEmpty(t *testing.T) {
t.Run("returns empty byte slice", func(t *testing.T) {
result := Empty()
assert.NotNil(t, result)
assert.Equal(t, 0, len(result))
})
t.Run("is identity for Monoid", func(t *testing.T) {
data := []byte("test")
// Left identity: empty + data = data
left := Monoid.Concat(Empty(), data)
assert.Equal(t, data, left)
// Right identity: data + empty = data
right := Monoid.Concat(data, Empty())
assert.Equal(t, data, right)
})
}
func TestToString(t *testing.T) {
t.Run("converts byte slice to string", func(t *testing.T) {
result := ToString([]byte("hello"))
assert.Equal(t, "hello", result)
})
t.Run("handles empty byte slice", func(t *testing.T) {
result := ToString([]byte{})
assert.Equal(t, "", result)
})
t.Run("handles binary data", func(t *testing.T) {
data := []byte{0x48, 0x65, 0x6c, 0x6c, 0x6f} // "Hello"
result := ToString(data)
assert.Equal(t, "Hello", result)
})
}
func TestSize(t *testing.T) {
t.Run("returns size of byte slice", func(t *testing.T) {
assert.Equal(t, 0, Size([]byte{}))
assert.Equal(t, 5, Size([]byte("hello")))
assert.Equal(t, 10, Size([]byte("0123456789")))
})
t.Run("handles binary data", func(t *testing.T) {
data := []byte{0x00, 0x01, 0x02, 0x03}
assert.Equal(t, 4, Size(data))
})
}
func TestMonoidConcat(t *testing.T) {
t.Run("concatenates two byte slices", func(t *testing.T) {
result := Monoid.Concat([]byte("Hello"), []byte(" World"))
assert.Equal(t, []byte("Hello World"), result)
})
t.Run("handles empty slices", func(t *testing.T) {
result1 := Monoid.Concat([]byte{}, []byte("test"))
assert.Equal(t, []byte("test"), result1)
result2 := Monoid.Concat([]byte("test"), []byte{})
assert.Equal(t, []byte("test"), result2)
result3 := Monoid.Concat([]byte{}, []byte{})
assert.Equal(t, []byte{}, result3)
})
t.Run("associativity: (a + b) + c = a + (b + c)", func(t *testing.T) {
a := []byte("a")
b := []byte("b")
c := []byte("c")
left := Monoid.Concat(Monoid.Concat(a, b), c)
right := Monoid.Concat(a, Monoid.Concat(b, c))
assert.Equal(t, left, right)
})
}
func TestConcatAll(t *testing.T) {
t.Run("concatenates multiple byte slices", func(t *testing.T) {
result := ConcatAll(
[]byte("Hello"),
[]byte(" "),
[]byte("World"),
[]byte("!"),
)
assert.Equal(t, []byte("Hello World!"), result)
})
t.Run("handles empty input", func(t *testing.T) {
result := ConcatAll()
assert.Equal(t, []byte{}, result)
})
t.Run("handles single slice", func(t *testing.T) {
result := ConcatAll([]byte("test"))
assert.Equal(t, []byte("test"), result)
})
t.Run("handles slices with empty elements", func(t *testing.T) {
result := ConcatAll(
[]byte("a"),
[]byte{},
[]byte("b"),
[]byte{},
[]byte("c"),
)
assert.Equal(t, []byte("abc"), result)
})
t.Run("handles binary data", func(t *testing.T) {
result := ConcatAll(
[]byte{0x01, 0x02},
[]byte{0x03, 0x04},
[]byte{0x05},
)
assert.Equal(t, []byte{0x01, 0x02, 0x03, 0x04, 0x05}, result)
})
}
func TestOrd(t *testing.T) {
t.Run("compares byte slices lexicographically", func(t *testing.T) {
// "abc" < "abd"
assert.Equal(t, -1, Ord.Compare([]byte("abc"), []byte("abd")))
// "abd" > "abc"
assert.Equal(t, 1, Ord.Compare([]byte("abd"), []byte("abc")))
// "abc" == "abc"
assert.Equal(t, 0, Ord.Compare([]byte("abc"), []byte("abc")))
})
t.Run("handles different lengths", func(t *testing.T) {
// "ab" < "abc"
assert.Equal(t, -1, Ord.Compare([]byte("ab"), []byte("abc")))
// "abc" > "ab"
assert.Equal(t, 1, Ord.Compare([]byte("abc"), []byte("ab")))
})
t.Run("handles empty slices", func(t *testing.T) {
// "" < "a"
assert.Equal(t, -1, Ord.Compare([]byte{}, []byte("a")))
// "a" > ""
assert.Equal(t, 1, Ord.Compare([]byte("a"), []byte{}))
// "" == ""
assert.Equal(t, 0, Ord.Compare([]byte{}, []byte{}))
})
t.Run("Equals method works", func(t *testing.T) {
assert.True(t, Ord.Equals([]byte("test"), []byte("test")))
assert.False(t, Ord.Equals([]byte("test"), []byte("Test")))
assert.True(t, Ord.Equals([]byte{}, []byte{}))
})
t.Run("handles binary data", func(t *testing.T) {
assert.Equal(t, -1, Ord.Compare([]byte{0x01}, []byte{0x02}))
assert.Equal(t, 1, Ord.Compare([]byte{0x02}, []byte{0x01}))
assert.Equal(t, 0, Ord.Compare([]byte{0x01, 0x02}, []byte{0x01, 0x02}))
})
}
// Example tests
func ExampleEmpty() {
empty := Empty()
println(len(empty)) // 0
// Output:
}
func ExampleToString() {
str := ToString([]byte("hello"))
println(str) // hello
// Output:
}
func ExampleSize() {
size := Size([]byte("hello"))
println(size) // 5
// Output:
}
func ExampleConcatAll() {
result := ConcatAll(
[]byte("Hello"),
[]byte(" "),
[]byte("World"),
)
println(string(result)) // Hello World
// Output:
}

114
v2/bytes/doc.go Normal file
View File

@@ -0,0 +1,114 @@
// 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 bytes provides functional programming utilities for working with byte slices.
//
// This package offers algebraic structures (Monoid, Ord) and utility functions
// for byte slice operations in a functional style.
//
// # Monoid
//
// The Monoid instance for byte slices combines them through concatenation,
// with an empty byte slice as the identity element.
//
// Example:
//
// import "github.com/IBM/fp-go/v2/bytes"
//
// // Concatenate byte slices
// result := bytes.Monoid.Concat([]byte("Hello"), []byte(" World"))
// // result: []byte("Hello World")
//
// // Identity element
// empty := bytes.Empty() // []byte{}
//
// # ConcatAll
//
// Efficiently concatenates multiple byte slices into a single slice:
//
// import "github.com/IBM/fp-go/v2/bytes"
//
// result := bytes.ConcatAll(
// []byte("Hello"),
// []byte(" "),
// []byte("World"),
// )
// // result: []byte("Hello World")
//
// # Ordering
//
// The Ord instance provides lexicographic ordering for byte slices:
//
// import "github.com/IBM/fp-go/v2/bytes"
//
// cmp := bytes.Ord.Compare([]byte("abc"), []byte("abd"))
// // cmp: -1 (abc < abd)
//
// equal := bytes.Ord.Equals([]byte("test"), []byte("test"))
// // equal: true
//
// # Utility Functions
//
// The package provides several utility functions:
//
// // Get empty byte slice
// empty := bytes.Empty() // []byte{}
//
// // Convert to string
// str := bytes.ToString([]byte("hello")) // "hello"
//
// // Get size
// size := bytes.Size([]byte("hello")) // 5
//
// # Use Cases
//
// The bytes package is particularly useful for:
//
// - Building byte buffers functionally
// - Combining multiple byte slices efficiently
// - Comparing byte slices lexicographically
// - Working with binary data in a functional style
//
// # Example - Building a Protocol Message
//
// import (
// "github.com/IBM/fp-go/v2/bytes"
// "encoding/binary"
// )
//
// // Build a simple protocol message
// header := []byte{0x01, 0x02}
// length := make([]byte, 4)
// binary.BigEndian.PutUint32(length, 100)
// payload := []byte("data")
//
// message := bytes.ConcatAll(header, length, payload)
//
// # Example - Sorting Byte Slices
//
// import (
// "github.com/IBM/fp-go/v2/array"
// "github.com/IBM/fp-go/v2/bytes"
// )
//
// data := [][]byte{
// []byte("zebra"),
// []byte("apple"),
// []byte("mango"),
// }
//
// sorted := array.Sort(bytes.Ord)(data)
// // sorted: [[]byte("apple"), []byte("mango"), []byte("zebra")]
package bytes

34
v2/bytes/monoid.go Normal file
View File

@@ -0,0 +1,34 @@
// 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 bytes
import (
"bytes"
A "github.com/IBM/fp-go/v2/array"
O "github.com/IBM/fp-go/v2/ord"
)
var (
// monoid for byte arrays
Monoid = A.Monoid[byte]()
// ConcatAll concatenates all bytes
ConcatAll = A.ArrayConcatAll[byte]
// Ord implements the default ordering on bytes
Ord = O.MakeOrd(bytes.Compare, bytes.Equal)
)

26
v2/bytes/monoid_test.go Normal file
View File

@@ -0,0 +1,26 @@
// 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 bytes
import (
"testing"
M "github.com/IBM/fp-go/v2/monoid/testing"
)
func TestMonoid(t *testing.T) {
M.AssertLaws(t, Monoid)([][]byte{[]byte(""), []byte("a"), []byte("some value")})
}

432
v2/cli/apply.go Normal file
View File

@@ -0,0 +1,432 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
)
func generateTraverseTuple(f *os.File, i int) {
fmt.Fprintf(f, "\n// TraverseTuple%d is a utility function used to implement the sequence operation for higher kinded types based only on map and ap.\n", i)
fmt.Fprintf(f, "// The function takes a [Tuple%d] of base types and %d functions that transform these based types into higher higher kinded types. It returns a higher kinded type of a [Tuple%d] with the resolved values.\n", i, i, i)
fmt.Fprintf(f, "func TraverseTuple%d[\n", i)
// map as the starting point
fmt.Fprintf(f, " MAP ~func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "func(T%d)", j+1)
}
fmt.Fprintf(f, " ")
fmt.Fprintf(f, "T.")
writeTupleType(f, "T", i)
fmt.Fprintf(f, ") func(HKT_T1)")
if i > 1 {
fmt.Fprintf(f, " HKT_F")
for k := 1; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
} else {
fmt.Fprintf(f, " HKT_TUPLE%d", i)
}
fmt.Fprintf(f, ",\n")
// the applicatives
for j := 1; j < i; j++ {
fmt.Fprintf(f, " AP%d ~func(", j)
fmt.Fprintf(f, "HKT_T%d) func(", j+1)
fmt.Fprintf(f, "HKT_F")
for k := j; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
fmt.Fprintf(f, ")")
if j+1 < i {
fmt.Fprintf(f, " HKT_F")
for k := j + 1; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
} else {
fmt.Fprintf(f, " HKT_TUPLE%d", i)
}
fmt.Fprintf(f, ",\n")
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " F%d ~func(A%d) HKT_T%d,\n", j+1, j+1, j+1)
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " A%d, T%d,\n", j+1, j+1)
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " HKT_T%d, // HKT[T%d]\n", j+1, j+1)
}
for j := 1; j < i; j++ {
fmt.Fprintf(f, " HKT_F")
for k := j; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
fmt.Fprintf(f, ", // HKT[")
for k := j; k < i; k++ {
fmt.Fprintf(f, "func(T%d) ", k+1)
}
fmt.Fprintf(f, "T.")
writeTupleType(f, "T", i)
fmt.Fprintf(f, "]\n")
}
fmt.Fprintf(f, " HKT_TUPLE%d any, // HKT[", i)
writeTupleType(f, "T", i)
fmt.Fprintf(f, "]\n")
fmt.Fprintf(f, "](\n")
// the callbacks
fmt.Fprintf(f, " fmap MAP,\n")
for j := 1; j < i; j++ {
fmt.Fprintf(f, " fap%d AP%d,\n", j, j)
}
// the transformer functions
for j := 1; j <= i; j++ {
fmt.Fprintf(f, " f%d F%d,\n", j, j)
}
// the parameters
fmt.Fprintf(f, " t T.Tuple%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "A%d", j+1)
}
fmt.Fprintf(f, "],\n")
fmt.Fprintf(f, ") HKT_TUPLE%d {\n", i)
fmt.Fprintf(f, " return F.Pipe%d(\n", i)
fmt.Fprintf(f, " f1(t.F1),\n")
fmt.Fprintf(f, " fmap(tupleConstructor%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, "]()),\n")
for j := 1; j < i; j++ {
fmt.Fprintf(f, " fap%d(f%d(t.F%d)),\n", j, j+1, j+1)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintf(f, "}\n")
}
func generateSequenceTuple(f *os.File, i int) {
fmt.Fprintf(f, "\n// SequenceTuple%d is a utility function used to implement the sequence operation for higher kinded types based only on map and ap.\n", i)
fmt.Fprintf(f, "// The function takes a [Tuple%d] of higher higher kinded types and returns a higher kinded type of a [Tuple%d] with the resolved values.\n", i, i)
fmt.Fprintf(f, "func SequenceTuple%d[\n", i)
// map as the starting point
fmt.Fprintf(f, " MAP ~func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "func(T%d)", j+1)
}
fmt.Fprintf(f, " ")
fmt.Fprintf(f, "T.")
writeTupleType(f, "T", i)
fmt.Fprintf(f, ") func(HKT_T1)")
if i > 1 {
fmt.Fprintf(f, " HKT_F")
for k := 1; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
} else {
fmt.Fprintf(f, " HKT_TUPLE%d", i)
}
fmt.Fprintf(f, ",\n")
// the applicatives
for j := 1; j < i; j++ {
fmt.Fprintf(f, " AP%d ~func(", j)
fmt.Fprintf(f, "HKT_T%d) func(", j+1)
fmt.Fprintf(f, "HKT_F")
for k := j; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
fmt.Fprintf(f, ")")
if j+1 < i {
fmt.Fprintf(f, " HKT_F")
for k := j + 1; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
} else {
fmt.Fprintf(f, " HKT_TUPLE%d", i)
}
fmt.Fprintf(f, ",\n")
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " T%d,\n", j+1)
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " HKT_T%d, // HKT[T%d]\n", j+1, j+1)
}
for j := 1; j < i; j++ {
fmt.Fprintf(f, " HKT_F")
for k := j; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
fmt.Fprintf(f, ", // HKT[")
for k := j; k < i; k++ {
fmt.Fprintf(f, "func(T%d) ", k+1)
}
fmt.Fprintf(f, "T.")
writeTupleType(f, "T", i)
fmt.Fprintf(f, "]\n")
}
fmt.Fprintf(f, " HKT_TUPLE%d any, // HKT[", i)
writeTupleType(f, "T", i)
fmt.Fprintf(f, "]\n")
fmt.Fprintf(f, "](\n")
// the callbacks
fmt.Fprintf(f, " fmap MAP,\n")
for j := 1; j < i; j++ {
fmt.Fprintf(f, " fap%d AP%d,\n", j, j)
}
// the parameters
fmt.Fprintf(f, " t T.Tuple%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "HKT_T%d", j+1)
}
fmt.Fprintf(f, "],\n")
fmt.Fprintf(f, ") HKT_TUPLE%d {\n", i)
fmt.Fprintf(f, " return F.Pipe%d(\n", i)
fmt.Fprintf(f, " t.F1,\n")
fmt.Fprintf(f, " fmap(tupleConstructor%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, "]()),\n")
for j := 1; j < i; j++ {
fmt.Fprintf(f, " fap%d(t.F%d),\n", j, j+1)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintf(f, "}\n")
}
func generateSequenceT(f *os.File, i int) {
fmt.Fprintf(f, "\n// SequenceT%d is a utility function used to implement the sequence operation for higher kinded types based only on map and ap.\n", i)
fmt.Fprintf(f, "// The function takes %d higher higher kinded types and returns a higher kinded type of a [Tuple%d] with the resolved values.\n", i, i)
fmt.Fprintf(f, "func SequenceT%d[\n", i)
// map as the starting point
fmt.Fprintf(f, " MAP ~func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "func(T%d)", j+1)
}
fmt.Fprintf(f, " ")
fmt.Fprintf(f, "T.")
writeTupleType(f, "T", i)
fmt.Fprintf(f, ") func(HKT_T1)")
if i > 1 {
fmt.Fprintf(f, " HKT_F")
for k := 1; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
} else {
fmt.Fprintf(f, " HKT_TUPLE%d", i)
}
fmt.Fprintf(f, ",\n")
// the applicatives
for j := 1; j < i; j++ {
fmt.Fprintf(f, " AP%d ~func(", j)
fmt.Fprintf(f, "HKT_T%d) func(", j+1)
fmt.Fprintf(f, "HKT_F")
for k := j; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
fmt.Fprintf(f, ")")
if j+1 < i {
fmt.Fprintf(f, " HKT_F")
for k := j + 1; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
} else {
fmt.Fprintf(f, " HKT_TUPLE%d", i)
}
fmt.Fprintf(f, ",\n")
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " T%d,\n", j+1)
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " HKT_T%d, // HKT[T%d]\n", j+1, j+1)
}
for j := 1; j < i; j++ {
fmt.Fprintf(f, " HKT_F")
for k := j; k < i; k++ {
fmt.Fprintf(f, "_T%d", k+1)
}
fmt.Fprintf(f, ", // HKT[")
for k := j; k < i; k++ {
fmt.Fprintf(f, "func(T%d) ", k+1)
}
fmt.Fprintf(f, "T.")
writeTupleType(f, "T", i)
fmt.Fprintf(f, "]\n")
}
fmt.Fprintf(f, " HKT_TUPLE%d any, // HKT[", i)
writeTupleType(f, "T", i)
fmt.Fprintf(f, "]\n")
fmt.Fprintf(f, "](\n")
// the callbacks
fmt.Fprintf(f, " fmap MAP,\n")
for j := 1; j < i; j++ {
fmt.Fprintf(f, " fap%d AP%d,\n", j, j)
}
// the parameters
for j := 0; j < i; j++ {
fmt.Fprintf(f, " t%d HKT_T%d,\n", j+1, j+1)
}
fmt.Fprintf(f, ") HKT_TUPLE%d {\n", i)
fmt.Fprintf(f, " return F.Pipe%d(\n", i)
fmt.Fprintf(f, " t1,\n")
fmt.Fprintf(f, " fmap(tupleConstructor%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, "]()),\n")
for j := 1; j < i; j++ {
fmt.Fprintf(f, " fap%d(t%d),\n", j, j+1)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintf(f, "}\n")
}
func generateTupleConstructor(f *os.File, i int) {
// Create the optionize version
fmt.Fprintf(f, "\n// tupleConstructor%d returns a curried version of [T.MakeTuple%d]\n", i, i)
fmt.Fprintf(f, "func tupleConstructor%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any]()")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " func(T%d)", j+1)
}
fmt.Fprintf(f, " T.Tuple%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, "] {\n")
fmt.Fprintf(f, " return F.Curry%d(T.MakeTuple%d[", i, i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, "])\n")
fmt.Fprintf(f, "}\n")
}
func generateApplyHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n\n", pkg)
// print out some helpers
fmt.Fprintf(f, `
import (
F "github.com/IBM/fp-go/v2/function"
T "github.com/IBM/fp-go/v2/tuple"
)
`)
for i := 1; i <= count; i++ {
// tuple constructor
generateTupleConstructor(f, i)
// sequenceT
generateSequenceT(f, i)
// sequenceTuple
generateSequenceTuple(f, i)
// traverseTuple
generateTraverseTuple(f, i)
}
return nil
}
func ApplyCommand() *C.Command {
return &C.Command{
Name: "apply",
Usage: "generate code for the sequence operations of apply",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateApplyHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

294
v2/cli/bind.go Normal file
View File

@@ -0,0 +1,294 @@
// 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 (
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
)
func createCombinations(n int, all, prev []int) [][]int {
l := len(prev)
if l == n {
return [][]int{prev}
}
var res [][]int
for idx, val := range all {
cpy := make([]int, l+1)
copy(cpy, prev)
cpy[l] = val
res = append(res, createCombinations(n, all[idx+1:], cpy)...)
}
return res
}
func remaining(comb []int, total int) []int {
var res []int
mp := make(map[int]int)
for _, idx := range comb {
mp[idx] = idx
}
for i := 1; i <= total; i++ {
_, ok := mp[i]
if !ok {
res = append(res, i)
}
}
return res
}
func generateCombSingleBind(f *os.File, comb [][]int, total int) {
for _, c := range comb {
// remaining indexes
rem := remaining(c, total)
// bind function
fmt.Fprintf(f, "\n// Bind")
for _, idx := range c {
fmt.Fprintf(f, "%d", idx)
}
fmt.Fprintf(f, "of%d takes a function with %d parameters and returns a new function with %d parameters that will bind these parameters to the positions [", total, total, len(c))
for i, idx := range c {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "%d", idx)
}
fmt.Fprintf(f, "] of the original function.\n// The return value of is a function with the remaining %d parameters at positions [", len(rem))
for i, idx := range rem {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "%d", idx)
}
fmt.Fprintf(f, "] of the original function.\n")
fmt.Fprintf(f, "func Bind")
for _, idx := range c {
fmt.Fprintf(f, "%d", idx)
}
fmt.Fprintf(f, "of%d[F ~func(", total)
for i := 0; i < total; i++ {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", i+1)
}
fmt.Fprintf(f, ") R")
for i := 0; i < total; i++ {
fmt.Fprintf(f, ", T%d", i+1)
}
fmt.Fprintf(f, ", R any](f F) func(")
for i, idx := range c {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", idx)
}
fmt.Fprintf(f, ") func(")
for i, idx := range rem {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", idx)
}
fmt.Fprintf(f, ") R {\n")
fmt.Fprintf(f, " return func(")
for i, idx := range c {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", idx, idx)
}
fmt.Fprintf(f, ") func(")
for i, idx := range rem {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", idx)
}
fmt.Fprintf(f, ") R {\n")
fmt.Fprintf(f, " return func(")
for i, idx := range rem {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", idx, idx)
}
fmt.Fprintf(f, ") R {\n")
fmt.Fprintf(f, " return f(")
for i := 1; i <= total; i++ {
if i > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", i)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, "}\n")
// ignore function
fmt.Fprintf(f, "\n// Ignore")
for _, idx := range c {
fmt.Fprintf(f, "%d", idx)
}
fmt.Fprintf(f, "of%d takes a function with %d parameters and returns a new function with %d parameters that will ignore the values at positions [", total, len(rem), total)
for i, idx := range c {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "%d", idx)
}
fmt.Fprintf(f, "] and pass the remaining %d parameters to the original function\n", len(rem))
fmt.Fprintf(f, "func Ignore")
for _, idx := range c {
fmt.Fprintf(f, "%d", idx)
}
fmt.Fprintf(f, "of%d[", total)
// start with the undefined parameters
for i, idx := range c {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", idx)
}
if len(c) > 0 {
fmt.Fprintf(f, " any, ")
}
fmt.Fprintf(f, "F ~func(")
for i, idx := range rem {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", idx)
}
fmt.Fprintf(f, ") R")
for _, idx := range rem {
fmt.Fprintf(f, ", T%d", idx)
}
fmt.Fprintf(f, ", R any](f F) func(")
for i := 0; i < total; i++ {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", i+1)
}
fmt.Fprintf(f, ") R {\n")
fmt.Fprintf(f, " return func(")
for i := 0; i < total; i++ {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", i+1, i+1)
}
fmt.Fprintf(f, ") R {\n")
fmt.Fprintf(f, " return f(")
for i, idx := range rem {
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", idx)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, "}\n")
}
}
func generateSingleBind(f *os.File, total int) {
fmt.Fprintf(f, "// Combinations for a total of %d arguments\n", total)
// construct the indexes
all := make([]int, total)
for i := 0; i < total; i++ {
all[i] = i + 1
}
// for all permutations of a certain length
for j := 0; j < total; j++ {
// get combinations of that size
comb := createCombinations(j+1, all, []int{})
generateCombSingleBind(f, comb, total)
}
}
func generateBind(f *os.File, i int) {
for j := 1; j < i; j++ {
generateSingleBind(f, j)
}
}
func generateBindHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n", pkg)
generateBind(f, count)
return nil
}
func BindCommand() *C.Command {
return &C.Command{
Name: "bind",
Usage: "generate code for binder functions etc",
Description: "Code generation for bind, etc",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateBindHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

40
v2/cli/commands.go Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
C "github.com/urfave/cli/v2"
)
func Commands() []*C.Command {
return []*C.Command{
PipeCommand(),
IdentityCommand(),
OptionCommand(),
EitherCommand(),
TupleCommand(),
BindCommand(),
ApplyCommand(),
ContextReaderIOEitherCommand(),
ReaderIOEitherCommand(),
ReaderCommand(),
IOEitherCommand(),
IOCommand(),
IOOptionCommand(),
DICommand(),
LensCommand(),
}
}

39
v2/cli/common.go Normal file
View File

@@ -0,0 +1,39 @@
// 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 (
C "github.com/urfave/cli/v2"
)
const (
keyFilename = "filename"
keyCount = "count"
)
var (
flagFilename = &C.StringFlag{
Name: keyFilename,
Value: "gen.go",
Usage: "Name of the generated file",
}
flagCount = &C.IntFlag{
Name: keyCount,
Value: 20,
Usage: "Number of variations to create",
}
)

View File

@@ -0,0 +1,271 @@
// 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 (
"fmt"
"log"
"os"
"path/filepath"
"strings"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
)
// Deprecated:
func generateNestedCallbacks(i, total int) string {
var buf strings.Builder
for j := i; j < total; j++ {
if j > i {
buf.WriteString(" ")
}
buf.WriteString(fmt.Sprintf("func(T%d)", j+1))
}
if i > 0 {
buf.WriteString(" ")
}
buf.WriteString(tupleType("T")(total))
return buf.String()
}
func generateNestedCallbacksPlain(i, total int) string {
fs := A.MakeBy(total-i, func(j int) string {
return fmt.Sprintf("func(T%d)", j+i+1)
})
ts := A.Of(tupleTypePlain("T")(total))
return joinAll(" ")(fs, ts)
}
func generateContextReaderIOEitherEitherize(f, fg *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// Eitherize%d converts a function with %d parameters returning a tuple into a function with %d parameters returning a [ReaderIOEither[R]]\n// The inverse function is [Uneitherize%d]\n", i, i, i, i)
fmt.Fprintf(f, "func Eitherize%d[F ~func(context.Context", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ") (R, error)")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ", R any](f F) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") ReaderIOEither[R] {\n")
fmt.Fprintf(f, " return G.Eitherize%d[ReaderIOEither[R]](f)\n", i)
fmt.Fprintln(f, "}")
// generic version
fmt.Fprintf(fg, "\n// Eitherize%d converts a function with %d parameters returning a tuple into a function with %d parameters returning a [GRA]\n// The inverse function is [Uneitherize%d]\n", i, i, i, i)
fmt.Fprintf(fg, "func Eitherize%d[GRA ~func(context.Context) GIOA, F ~func(context.Context", i)
for j := 0; j < i; j++ {
fmt.Fprintf(fg, ", T%d", j)
}
fmt.Fprintf(fg, ") (R, error), GIOA ~func() E.Either[error, R]")
for j := 0; j < i; j++ {
fmt.Fprintf(fg, ", T%d", j)
}
fmt.Fprintf(fg, ", R any](f F) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "T%d", j)
}
fmt.Fprintf(fg, ") GRA {\n")
fmt.Fprintf(fg, " return RE.Eitherize%d[GRA](f)\n", i)
fmt.Fprintln(fg, "}")
}
func generateContextReaderIOEitherUneitherize(f, fg *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// Uneitherize%d converts a function with %d parameters returning a [ReaderIOEither[R]] into a function with %d parameters returning a tuple.\n// The first parameter is considered to be the [context.Context].\n", i, i+1, i)
fmt.Fprintf(f, "func Uneitherize%d[F ~func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") ReaderIOEither[R]")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ", R any](f F) func(context.Context")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ") (R, error) {\n")
fmt.Fprintf(f, " return G.Uneitherize%d[ReaderIOEither[R]", i)
fmt.Fprintf(f, ", func(context.Context")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ")(R, error)](f)\n")
fmt.Fprintln(f, "}")
// generic version
fmt.Fprintf(fg, "\n// Uneitherize%d converts a function with %d parameters returning a [GRA] into a function with %d parameters returning a tuple.\n// The first parameter is considered to be the [context.Context].\n", i, i, i)
fmt.Fprintf(fg, "func Uneitherize%d[GRA ~func(context.Context) GIOA, F ~func(context.Context", i)
for j := 0; j < i; j++ {
fmt.Fprintf(fg, ", T%d", j)
}
fmt.Fprintf(fg, ") (R, error), GIOA ~func() E.Either[error, R]")
for j := 0; j < i; j++ {
fmt.Fprintf(fg, ", T%d", j)
}
fmt.Fprintf(fg, ", R any](f func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "T%d", j)
}
fmt.Fprintf(fg, ") GRA) F {\n")
fmt.Fprintf(fg, " return func(c context.Context")
for j := 0; j < i; j++ {
fmt.Fprintf(fg, ", t%d T%d", j, j)
}
fmt.Fprintf(fg, ") (R, error) {\n")
fmt.Fprintf(fg, " return E.UnwrapError(f(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "t%d", j)
}
fmt.Fprintf(fg, ")(c)())\n")
fmt.Fprintf(fg, " }\n")
fmt.Fprintf(fg, "}\n")
}
func nonGenericContextReaderIOEither(param string) string {
return fmt.Sprintf("ReaderIOEither[%s]", param)
}
var extrasContextReaderIOEither = A.Empty[string]()
func generateContextReaderIOEitherSequenceT(f *os.File, i int) {
generateGenericSequenceT("", nonGenericContextReaderIOEither, extrasContextReaderIOEither)(f, i)
generateGenericSequenceT("Seq", nonGenericContextReaderIOEither, extrasContextReaderIOEither)(f, i)
generateGenericSequenceT("Par", nonGenericContextReaderIOEither, extrasContextReaderIOEither)(f, i)
}
func generateContextReaderIOEitherSequenceTuple(f *os.File, i int) {
generateGenericSequenceTuple("", nonGenericContextReaderIOEither, extrasContextReaderIOEither)(f, i)
generateGenericSequenceTuple("Seq", nonGenericContextReaderIOEither, extrasContextReaderIOEither)(f, i)
generateGenericSequenceTuple("Par", nonGenericContextReaderIOEither, extrasContextReaderIOEither)(f, i)
}
func generateContextReaderIOEitherTraverseTuple(f *os.File, i int) {
generateGenericTraverseTuple("", nonGenericContextReaderIOEither, extrasContextReaderIOEither)(f, i)
generateGenericTraverseTuple("Seq", nonGenericContextReaderIOEither, extrasContextReaderIOEither)(f, i)
generateGenericTraverseTuple("Par", nonGenericContextReaderIOEither, extrasContextReaderIOEither)(f, i)
}
func generateContextReaderIOEitherHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// construct subdirectory
genFilename := filepath.Join("generic", filename)
err = os.MkdirAll("generic", os.ModePerm)
if err != nil {
return err
}
fg, err := os.Create(filepath.Clean(genFilename))
if err != nil {
return err
}
defer fg.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
writePackage(f, pkg)
fmt.Fprintf(f, `
import (
"context"
G "github.com/IBM/fp-go/v2/context/%s/generic"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/tuple"
)
`, pkg)
writePackage(fg, "generic")
fmt.Fprintf(fg, `
import (
"context"
E "github.com/IBM/fp-go/v2/either"
RE "github.com/IBM/fp-go/v2/readerioeither/generic"
)
`)
generateContextReaderIOEitherEitherize(f, fg, 0)
generateContextReaderIOEitherUneitherize(f, fg, 0)
for i := 1; i <= count; i++ {
// eitherize
generateContextReaderIOEitherEitherize(f, fg, i)
generateContextReaderIOEitherUneitherize(f, fg, i)
// sequenceT
generateContextReaderIOEitherSequenceT(f, i)
// sequenceTuple
generateContextReaderIOEitherSequenceTuple(f, i)
// traverseTuple
generateContextReaderIOEitherTraverseTuple(f, i)
}
return nil
}
func ContextReaderIOEitherCommand() *C.Command {
return &C.Command{
Name: "contextreaderioeither",
Usage: "generate code for ContextReaderIOEither",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateContextReaderIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

231
v2/cli/di.go Normal file
View File

@@ -0,0 +1,231 @@
// 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 (
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
)
func generateMakeProvider(f *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// MakeProvider%d creates a [DIE.Provider] for an [InjectionToken] from a function with %d dependencies\n", i, i)
fmt.Fprintf(f, "func MakeProvider%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any, R any](\n")
fmt.Fprintf(f, " token InjectionToken[R],\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d Dependency[T%d],\n", j+1, j+1)
}
fmt.Fprintf(f, " f func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOE.IOEither[error, R],\n")
fmt.Fprintf(f, ") DIE.Provider {\n")
fmt.Fprint(f, " return DIE.MakeProvider(\n")
fmt.Fprint(f, " token,\n")
fmt.Fprintf(f, " MakeProviderFactory%d(\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d,\n", j+1)
}
fmt.Fprint(f, " f,\n")
fmt.Fprint(f, " ))\n")
fmt.Fprintf(f, "}\n")
}
func generateMakeTokenWithDefault(f *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// MakeTokenWithDefault%d creates an [InjectionToken] with a default implementation with %d dependencies\n", i, i)
fmt.Fprintf(f, "func MakeTokenWithDefault%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any, R any](\n")
fmt.Fprintf(f, " name string,\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d Dependency[T%d],\n", j+1, j+1)
}
fmt.Fprintf(f, " f func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOE.IOEither[error, R],\n")
fmt.Fprintf(f, ") InjectionToken[R] {\n")
fmt.Fprintf(f, " return MakeTokenWithDefault[R](name, MakeProviderFactory%d(\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d,\n", j+1)
}
fmt.Fprint(f, " f,\n")
fmt.Fprint(f, " ))\n")
fmt.Fprintf(f, "}\n")
}
func generateMakeProviderFactory(f *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// MakeProviderFactory%d creates a [DIE.ProviderFactory] from a function with %d arguments and %d dependencies\n", i, i, i)
fmt.Fprintf(f, "func MakeProviderFactory%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any, R any](\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d Dependency[T%d],\n", j+1, j+1)
}
fmt.Fprintf(f, " f func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOE.IOEither[error, R],\n")
fmt.Fprintf(f, ") DIE.ProviderFactory {\n")
fmt.Fprint(f, " return DIE.MakeProviderFactory(\n")
fmt.Fprint(f, " A.From[DIE.Dependency](\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d,\n", j+1)
}
fmt.Fprint(f, " ),\n")
fmt.Fprintf(f, " eraseProviderFactory%d(\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d,\n", j+1)
}
fmt.Fprint(f, " f,\n")
fmt.Fprint(f, " ),\n")
fmt.Fprint(f, " )\n")
fmt.Fprintf(f, "}\n")
}
func generateEraseProviderFactory(f *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// eraseProviderFactory%d creates a function that takes a variadic number of untyped arguments and from a function of %d strongly typed arguments and %d dependencies\n", i, i, i)
fmt.Fprintf(f, "func eraseProviderFactory%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any, R any](\n")
for j := 0; j < i; j++ {
fmt.Fprintf(f, " d%d Dependency[T%d],\n", j+1, j+1)
}
fmt.Fprintf(f, " f func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOE.IOEither[error, R]) func(params ...any) IOE.IOEither[error, any] {\n")
fmt.Fprintf(f, " ft := eraseTuple(T.Tupled%d(f))\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " t%d := lookupAt[T%d](%d, d%d)\n", j+1, j+1, j, j+1)
}
fmt.Fprint(f, " return func(params ...any) IOE.IOEither[error, any] {\n")
fmt.Fprintf(f, " return ft(E.SequenceT%d(\n", i)
for j := 0; j < i; j++ {
fmt.Fprintf(f, " t%d(params),\n", j+1)
}
fmt.Fprint(f, " ))\n")
fmt.Fprint(f, " }\n")
fmt.Fprintf(f, "}\n")
}
func generateDIHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n\n", pkg)
fmt.Fprint(f, `
import (
E "github.com/IBM/fp-go/v2/either"
IOE "github.com/IBM/fp-go/v2/ioeither"
T "github.com/IBM/fp-go/v2/tuple"
A "github.com/IBM/fp-go/v2/array"
DIE "github.com/IBM/fp-go/v2/di/erasure"
)
`)
for i := 1; i <= count; i++ {
generateEraseProviderFactory(f, i)
generateMakeProviderFactory(f, i)
generateMakeTokenWithDefault(f, i)
generateMakeProvider(f, i)
}
return nil
}
func DICommand() *C.Command {
return &C.Command{
Name: "di",
Usage: "generate code for the dependency injection package",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateDIHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

200
v2/cli/either.go Normal file
View File

@@ -0,0 +1,200 @@
// 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 (
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
)
func eitherHKT(typeE string) func(typeA string) string {
return func(typeA string) string {
return fmt.Sprintf("Either[%s, %s]", typeE, typeA)
}
}
func generateEitherTraverseTuple(f *os.File, i int) {
generateTraverseTuple1(eitherHKT("E"), "E")(f, i)
}
func generateEitherSequenceTuple(f *os.File, i int) {
generateSequenceTuple1(eitherHKT("E"), "E")(f, i)
}
func generateEitherSequenceT(f *os.File, i int) {
generateSequenceT1(eitherHKT("E"), "E")(f, i)
}
func generateUneitherize(f *os.File, i int) {
// Create the optionize version
fmt.Fprintf(f, "\n// Uneitherize%d converts a function with %d parameters returning an Either into a function with %d parameters returning a tuple\n// The inverse function is [Eitherize%d]\n", i, i, i, i)
fmt.Fprintf(f, "func Uneitherize%d[F ~func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") Either[error, R]")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ", R any](f F) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") (R, error) {\n")
fmt.Fprintf(f, " return func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", j, j)
}
fmt.Fprintf(f, ") (R, error) {\n")
fmt.Fprintf(f, " return UnwrapError(f(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", j)
}
fmt.Fprintln(f, "))")
fmt.Fprintln(f, " }")
fmt.Fprintln(f, "}")
}
func generateEitherize(f *os.File, i int) {
// Create the optionize version
fmt.Fprintf(f, "\n// Eitherize%d converts a function with %d parameters returning a tuple into a function with %d parameters returning an Either\n// The inverse function is [Uneitherize%d]\n", i, i, i, i)
fmt.Fprintf(f, "func Eitherize%d[F ~func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") (R, error)")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ", R any](f F) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") Either[error, R] {\n")
fmt.Fprintf(f, " return func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", j, j)
}
fmt.Fprintf(f, ") Either[error, R] {\n")
fmt.Fprintf(f, " return TryCatchError(f(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", j)
}
fmt.Fprintln(f, "))")
fmt.Fprintln(f, " }")
fmt.Fprintln(f, "}")
}
func generateEitherHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n\n", pkg)
fmt.Fprintf(f, `
import (
A "github.com/IBM/fp-go/v2/internal/apply"
T "github.com/IBM/fp-go/v2/tuple"
)
`)
// zero level functions
// optionize
generateEitherize(f, 0)
// unoptionize
generateUneitherize(f, 0)
for i := 1; i <= count; i++ {
// optionize
generateEitherize(f, i)
// unoptionize
generateUneitherize(f, i)
// sequenceT
generateEitherSequenceT(f, i)
// sequenceTuple
generateEitherSequenceTuple(f, i)
// traverseTuple
generateEitherTraverseTuple(f, i)
}
return nil
}
func EitherCommand() *C.Command {
return &C.Command{
Name: "either",
Usage: "generate code for Either",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

31
v2/cli/header.go Normal file
View File

@@ -0,0 +1,31 @@
// 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 (
"fmt"
"os"
"time"
)
func writePackage(f *os.File, pkg string) {
// print package
fmt.Fprintf(f, "package %s\n\n", pkg)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
}

103
v2/cli/identity.go Normal file
View File

@@ -0,0 +1,103 @@
// 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 (
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
)
func identityHKT(typeA string) string {
return typeA
}
func generateIdentityTraverseTuple(f *os.File, i int) {
generateTraverseTuple1(identityHKT, "")(f, i)
}
func generateIdentitySequenceTuple(f *os.File, i int) {
generateSequenceTuple1(identityHKT, "")(f, i)
}
func generateIdentitySequenceT(f *os.File, i int) {
generateSequenceT1(identityHKT, "")(f, i)
}
func generateIdentityHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n\n", pkg)
fmt.Fprintf(f, `
import (
A "github.com/IBM/fp-go/v2/internal/apply"
T "github.com/IBM/fp-go/v2/tuple"
)
`)
for i := 1; i <= count; i++ {
// sequenceT
generateIdentitySequenceT(f, i)
// sequenceTuple
generateIdentitySequenceTuple(f, i)
// traverseTuple
generateIdentityTraverseTuple(f, i)
}
return nil
}
func IdentityCommand() *C.Command {
return &C.Command{
Name: "identity",
Usage: "generate code for Identity",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateIdentityHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

112
v2/cli/io.go Normal file
View File

@@ -0,0 +1,112 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"fmt"
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
)
func nonGenericIO(param string) string {
return fmt.Sprintf("IO[%s]", param)
}
var extrasIO = A.Empty[string]()
func generateIOSequenceT(f *os.File, i int) {
generateGenericSequenceT("", nonGenericIO, extrasIO)(f, i)
generateGenericSequenceT("Seq", nonGenericIO, extrasIO)(f, i)
generateGenericSequenceT("Par", nonGenericIO, extrasIO)(f, i)
}
func generateIOSequenceTuple(f *os.File, i int) {
generateGenericSequenceTuple("", nonGenericIO, extrasIO)(f, i)
generateGenericSequenceTuple("Seq", nonGenericIO, extrasIO)(f, i)
generateGenericSequenceTuple("Par", nonGenericIO, extrasIO)(f, i)
}
func generateIOTraverseTuple(f *os.File, i int) {
generateGenericTraverseTuple("", nonGenericIO, extrasIO)(f, i)
generateGenericTraverseTuple("Seq", nonGenericIO, extrasIO)(f, i)
generateGenericTraverseTuple("Par", nonGenericIO, extrasIO)(f, i)
}
func generateIOHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n\n", pkg)
fmt.Fprintf(f, `
import (
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/internal/apply"
)
`)
for i := 1; i <= count; i++ {
// sequenceT
generateIOSequenceT(f, i)
// sequenceTuple
generateIOSequenceTuple(f, i)
// traverseTuple
generateIOTraverseTuple(f, i)
}
return nil
}
func IOCommand() *C.Command {
return &C.Command{
Name: "io",
Usage: "generate code for IO",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateIOHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

283
v2/cli/ioeither.go Normal file
View File

@@ -0,0 +1,283 @@
// 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 (
"fmt"
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
)
// [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 {
func nonGenericIOEither(param string) string {
return fmt.Sprintf("IOEither[E, %s]", param)
}
var extrasIOEither = A.From("E")
func generateIOEitherSequenceT(f, fg *os.File, i int) {
generateGenericSequenceT("", nonGenericIOEither, extrasIOEither)(f, i)
generateGenericSequenceT("Seq", nonGenericIOEither, extrasIOEither)(f, i)
generateGenericSequenceT("Par", nonGenericIOEither, extrasIOEither)(f, i)
}
func generateIOEitherSequenceTuple(f, fg *os.File, i int) {
generateGenericSequenceTuple("", nonGenericIOEither, extrasIOEither)(f, i)
generateGenericSequenceTuple("Seq", nonGenericIOEither, extrasIOEither)(f, i)
generateGenericSequenceTuple("Par", nonGenericIOEither, extrasIOEither)(f, i)
}
func generateIOEitherTraverseTuple(f, fg *os.File, i int) {
generateGenericTraverseTuple("", nonGenericIOEither, extrasIOEither)(f, i)
generateGenericTraverseTuple("Seq", nonGenericIOEither, extrasIOEither)(f, i)
generateGenericTraverseTuple("Par", nonGenericIOEither, extrasIOEither)(f, i)
}
func generateIOEitherUneitherize(f, fg *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// Uneitherize%d converts a function with %d parameters returning a tuple into a function with %d parameters returning a [IOEither[error, R]]\n", i, i+1, i)
fmt.Fprintf(f, "func Uneitherize%d[F ~func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOEither[error, R]")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j+1)
}
fmt.Fprintf(f, ", R any](f F) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") (R, error) {\n")
fmt.Fprintf(f, " return G.Uneitherize%d[IOEither[error, R]](f)\n", i)
fmt.Fprintln(f, "}")
// generic version
fmt.Fprintf(fg, "\n// Uneitherize%d converts a function with %d parameters returning a tuple into a function with %d parameters returning a [GIOA]\n", i, i, i)
fmt.Fprintf(fg, "func Uneitherize%d[GIOA ~func() ET.Either[error, R], GTA ~func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "T%d", j+1)
}
fmt.Fprintf(fg, ") GIOA")
for j := 0; j < i; j++ {
fmt.Fprintf(fg, ", T%d", j+1)
}
fmt.Fprintf(fg, ", R any](f GTA) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "T%d", j+1)
}
fmt.Fprintf(fg, ") (R, error) {\n")
fmt.Fprintf(fg, " return func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "t%d T%d", j+1, j+1)
}
fmt.Fprintf(fg, ") (R, error) {\n")
fmt.Fprintf(fg, " return ET.Unwrap(f(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "t%d", j+1)
}
fmt.Fprintf(fg, ")())\n")
fmt.Fprintf(fg, " }\n")
fmt.Fprintf(fg, "}\n")
}
func generateIOEitherEitherize(f, fg *os.File, i int) {
// non generic version
fmt.Fprintf(f, "\n// Eitherize%d converts a function with %d parameters returning a tuple into a function with %d parameters returning a [IOEither[error, R]]\n", i, i+1, i)
fmt.Fprintf(f, "func Eitherize%d[F ~func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") (R, error)")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j+1)
}
fmt.Fprintf(f, ", R any](f F) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, ") IOEither[error, R] {\n")
fmt.Fprintf(f, " return G.Eitherize%d[IOEither[error, R]](f)\n", i)
fmt.Fprintln(f, "}")
// generic version
fmt.Fprintf(fg, "\n// Eitherize%d converts a function with %d parameters returning a tuple into a function with %d parameters returning a [GIOA]\n", i, i, i)
fmt.Fprintf(fg, "func Eitherize%d[GIOA ~func() ET.Either[error, R], F ~func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "T%d", j+1)
}
fmt.Fprintf(fg, ") (R, error)")
for j := 0; j < i; j++ {
fmt.Fprintf(fg, ", T%d", j+1)
}
fmt.Fprintf(fg, ", R any](f F) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "T%d", j+1)
}
fmt.Fprintf(fg, ") GIOA {\n")
fmt.Fprintf(fg, " e := ET.Eitherize%d(f)\n", i)
fmt.Fprintf(fg, " return func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "t%d T%d", j+1, j+1)
}
fmt.Fprintf(fg, ") GIOA {\n")
fmt.Fprintf(fg, " return func() ET.Either[error, R] {\n")
fmt.Fprintf(fg, " return e(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "t%d", j+1)
}
fmt.Fprintf(fg, ")\n")
fmt.Fprintf(fg, " }}\n")
fmt.Fprintf(fg, "}\n")
}
func generateIOEitherHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// construct subdirectory
genFilename := filepath.Join("generic", filename)
err = os.MkdirAll("generic", os.ModePerm)
if err != nil {
return err
}
fg, err := os.Create(filepath.Clean(genFilename))
if err != nil {
return err
}
defer fg.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n\n", pkg)
fmt.Fprintf(f, `
import (
G "github.com/IBM/fp-go/v2/%s/generic"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/tuple"
)
`, pkg)
// some header
fmt.Fprintln(fg, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(fg, "// This file was generated by robots at")
fmt.Fprintf(fg, "// %s\n", time.Now())
fmt.Fprintf(fg, "package generic\n\n")
fmt.Fprintf(fg, `
import (
ET "github.com/IBM/fp-go/v2/either"
)
`)
// eitherize
generateIOEitherEitherize(f, fg, 0)
// uneitherize
generateIOEitherUneitherize(f, fg, 0)
for i := 1; i <= count; i++ {
// eitherize
generateIOEitherEitherize(f, fg, i)
// uneitherize
generateIOEitherUneitherize(f, fg, i)
// sequenceT
generateIOEitherSequenceT(f, fg, i)
// sequenceTuple
generateIOEitherSequenceTuple(f, fg, i)
// traverseTuple
generateIOEitherTraverseTuple(f, fg, i)
}
return nil
}
func IOEitherCommand() *C.Command {
return &C.Command{
Name: "ioeither",
Usage: "generate code for IOEither",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateIOEitherHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

117
v2/cli/iooption.go Normal file
View File

@@ -0,0 +1,117 @@
// 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 (
"fmt"
"log"
"os"
"path/filepath"
"time"
A "github.com/IBM/fp-go/v2/array"
C "github.com/urfave/cli/v2"
)
func nonGenericIOOption(param string) string {
return fmt.Sprintf("IOOption[%s]", param)
}
func genericIOOption(param string) string {
return fmt.Sprintf("func() O.Option[%s]", param)
}
var extrasIOOption = A.Empty[string]()
func generateIOOptionSequenceT(f *os.File, i int) {
generateGenericSequenceT("", nonGenericIOOption, extrasIOOption)(f, i)
generateGenericSequenceT("Seq", nonGenericIOOption, extrasIOOption)(f, i)
generateGenericSequenceT("Par", nonGenericIOOption, extrasIOOption)(f, i)
}
func generateIOOptionSequenceTuple(f *os.File, i int) {
generateGenericSequenceTuple("", nonGenericIOOption, extrasIOOption)(f, i)
generateGenericSequenceTuple("Seq", nonGenericIOOption, extrasIOOption)(f, i)
generateGenericSequenceTuple("Par", nonGenericIOOption, extrasIOOption)(f, i)
}
func generateIOOptionTraverseTuple(f *os.File, i int) {
generateGenericTraverseTuple("", nonGenericIOOption, extrasIOOption)(f, i)
generateGenericTraverseTuple("Seq", nonGenericIOOption, extrasIOOption)(f, i)
generateGenericTraverseTuple("Par", nonGenericIOOption, extrasIOOption)(f, i)
}
func generateIOOptionHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n\n", pkg)
fmt.Fprintf(f, `
import (
"github.com/IBM/fp-go/v2/tuple"
"github.com/IBM/fp-go/v2/internal/apply"
)
`)
for i := 1; i <= count; i++ {
// sequenceT
generateIOOptionSequenceT(f, i)
// sequenceTuple
generateIOOptionSequenceTuple(f, i)
// traverseTuple
generateIOOptionTraverseTuple(f, i)
}
return nil
}
func IOOptionCommand() *C.Command {
return &C.Command{
Name: "iooption",
Usage: "generate code for IOOption",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateIOOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

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

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

503
v2/cli/lens_test.go Normal file
View File

@@ -0,0 +1,503 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cli
import (
"bytes"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHasLensAnnotation(t *testing.T) {
tests := []struct {
name string
comment string
expected bool
}{
{
name: "has annotation",
comment: "// fp-go:Lens",
expected: true,
},
{
name: "has annotation with other text",
comment: "// This is a struct with fp-go:Lens annotation",
expected: true,
},
{
name: "no annotation",
comment: "// This is just a regular comment",
expected: false,
},
{
name: "nil comment",
comment: "",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var doc *ast.CommentGroup
if tt.comment != "" {
doc = &ast.CommentGroup{
List: []*ast.Comment{
{Text: tt.comment},
},
}
}
result := hasLensAnnotation(doc)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetTypeName(t *testing.T) {
tests := []struct {
name string
code string
expected string
}{
{
name: "simple type",
code: "type T struct { F string }",
expected: "string",
},
{
name: "pointer type",
code: "type T struct { F *string }",
expected: "*string",
},
{
name: "slice type",
code: "type T struct { F []int }",
expected: "[]int",
},
{
name: "map type",
code: "type T struct { F map[string]int }",
expected: "map[string]int",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
require.NoError(t, err)
var fieldType ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
fieldType = field.Type
return false
}
return true
})
require.NotNil(t, fieldType)
result := getTypeName(fieldType)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsPointerType(t *testing.T) {
tests := []struct {
name string
code string
expected bool
}{
{
name: "pointer type",
code: "type T struct { F *string }",
expected: true,
},
{
name: "non-pointer type",
code: "type T struct { F string }",
expected: false,
},
{
name: "slice type",
code: "type T struct { F []string }",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "", "package test\n"+tt.code, 0)
require.NoError(t, err)
var fieldType ast.Expr
ast.Inspect(file, func(n ast.Node) bool {
if field, ok := n.(*ast.Field); ok && len(field.Names) > 0 {
fieldType = field.Type
return false
}
return true
})
require.NotNil(t, fieldType)
result := isPointerType(fieldType)
assert.Equal(t, tt.expected, result)
})
}
}
func TestHasOmitEmpty(t *testing.T) {
tests := []struct {
name string
tag string
expected bool
}{
{
name: "has omitempty",
tag: "`json:\"field,omitempty\"`",
expected: true,
},
{
name: "has omitempty with other options",
tag: "`json:\"field,omitempty,string\"`",
expected: true,
},
{
name: "no omitempty",
tag: "`json:\"field\"`",
expected: false,
},
{
name: "no tag",
tag: "",
expected: false,
},
{
name: "different tag",
tag: "`xml:\"field\"`",
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var tag *ast.BasicLit
if tt.tag != "" {
tag = &ast.BasicLit{
Value: tt.tag,
}
}
result := hasOmitEmpty(tag)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseFile(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Person struct {
Name string
Age int
Phone *string
}
// fp-go:Lens
type Address struct {
Street string
City string
}
// Not annotated
type Other struct {
Field string
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 2)
// Check Person struct
person := structs[0]
assert.Equal(t, "Person", person.Name)
assert.Len(t, person.Fields, 3)
assert.Equal(t, "Name", person.Fields[0].Name)
assert.Equal(t, "string", person.Fields[0].TypeName)
assert.False(t, person.Fields[0].IsOptional)
assert.Equal(t, "Age", person.Fields[1].Name)
assert.Equal(t, "int", person.Fields[1].TypeName)
assert.False(t, person.Fields[1].IsOptional)
assert.Equal(t, "Phone", person.Fields[2].Name)
assert.Equal(t, "*string", person.Fields[2].TypeName)
assert.True(t, person.Fields[2].IsOptional)
// Check Address struct
address := structs[1]
assert.Equal(t, "Address", address.Name)
assert.Len(t, address.Fields, 2)
assert.Equal(t, "Street", address.Fields[0].Name)
assert.Equal(t, "City", address.Fields[1].Name)
}
func TestParseFileWithOmitEmpty(t *testing.T) {
// Create a temporary test file
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.go")
testCode := `package testpkg
// fp-go:Lens
type Config struct {
Name string
Value string ` + "`json:\"value,omitempty\"`" + `
Count int ` + "`json:\",omitempty\"`" + `
Optional *string ` + "`json:\"optional,omitempty\"`" + `
Required int ` + "`json:\"required\"`" + `
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Parse the file
structs, pkg, err := parseFile(testFile)
require.NoError(t, err)
// Verify results
assert.Equal(t, "testpkg", pkg)
assert.Len(t, structs, 1)
// Check Config struct
config := structs[0]
assert.Equal(t, "Config", config.Name)
assert.Len(t, config.Fields, 5)
// Name - no tag, not optional
assert.Equal(t, "Name", config.Fields[0].Name)
assert.Equal(t, "string", config.Fields[0].TypeName)
assert.False(t, config.Fields[0].IsOptional)
// Value - has omitempty, should be optional
assert.Equal(t, "Value", config.Fields[1].Name)
assert.Equal(t, "string", config.Fields[1].TypeName)
assert.True(t, config.Fields[1].IsOptional, "Value field with omitempty should be optional")
// Count - has omitempty (no field name in tag), should be optional
assert.Equal(t, "Count", config.Fields[2].Name)
assert.Equal(t, "int", config.Fields[2].TypeName)
assert.True(t, config.Fields[2].IsOptional, "Count field with omitempty should be optional")
// Optional - pointer with omitempty, should be optional
assert.Equal(t, "Optional", config.Fields[3].Name)
assert.Equal(t, "*string", config.Fields[3].TypeName)
assert.True(t, config.Fields[3].IsOptional)
// Required - has json tag but no omitempty, not optional
assert.Equal(t, "Required", config.Fields[4].Name)
assert.Equal(t, "int", config.Fields[4].TypeName)
assert.False(t, config.Fields[4].IsOptional, "Required field without omitempty should not be optional")
}
func TestGenerateLensHelpers(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// fp-go:Lens
type TestStruct struct {
Name string
Value *int
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file exists
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
require.NoError(t, err)
// Read and verify the generated content
content, err := os.ReadFile(genPath)
require.NoError(t, err)
contentStr := string(content)
// Check for expected content
assert.Contains(t, contentStr, "package testpkg")
assert.Contains(t, contentStr, "Code generated by go generate")
assert.Contains(t, contentStr, "TestStructLens")
assert.Contains(t, contentStr, "MakeTestStructLens")
assert.Contains(t, contentStr, "L.Lens[TestStruct, string]")
assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]")
assert.Contains(t, contentStr, "I.FromZero")
}
func TestGenerateLensHelpersNoAnnotations(t *testing.T) {
// Create a temporary directory with test files
tmpDir := t.TempDir()
testCode := `package testpkg
// No annotation
type TestStruct struct {
Name string
}
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
require.NoError(t, err)
// Generate lens code (should not create file)
outputFile := "gen.go"
err = generateLensHelpers(tmpDir, outputFile, false)
require.NoError(t, err)
// Verify the generated file does not exist
genPath := filepath.Join(tmpDir, outputFile)
_, err = os.Stat(genPath)
assert.True(t, os.IsNotExist(err))
}
func TestLensTemplates(t *testing.T) {
s := structInfo{
Name: "TestStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "*int", IsOptional: true},
},
}
// Test struct template
var structBuf bytes.Buffer
err := structTmpl.Execute(&structBuf, s)
require.NoError(t, err)
structStr := structBuf.String()
assert.Contains(t, structStr, "type TestStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[TestStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[TestStruct, *int]")
// Test constructor template
var constructorBuf bytes.Buffer
err = constructorTmpl.Execute(&constructorBuf, s)
require.NoError(t, err)
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeTestStructLenses() TestStructLenses")
assert.Contains(t, constructorStr, "return TestStructLenses{")
assert.Contains(t, constructorStr, "Name: L.MakeLens(")
assert.Contains(t, constructorStr, "Value: L.MakeLens(")
assert.Contains(t, constructorStr, "I.FromZero")
}
func TestLensTemplatesWithOmitEmpty(t *testing.T) {
s := structInfo{
Name: "ConfigStruct",
Fields: []fieldInfo{
{Name: "Name", TypeName: "string", IsOptional: false},
{Name: "Value", TypeName: "string", IsOptional: true}, // non-pointer with omitempty
{Name: "Count", TypeName: "int", IsOptional: true}, // non-pointer with omitempty
{Name: "Pointer", TypeName: "*string", IsOptional: true}, // pointer
},
}
// Test struct template
var structBuf bytes.Buffer
err := structTmpl.Execute(&structBuf, s)
require.NoError(t, err)
structStr := structBuf.String()
assert.Contains(t, structStr, "type ConfigStructLenses struct")
assert.Contains(t, structStr, "Name L.Lens[ConfigStruct, string]")
assert.Contains(t, structStr, "Value LO.LensO[ConfigStruct, string]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Count LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should use LensO")
assert.Contains(t, structStr, "Pointer LO.LensO[ConfigStruct, *string]")
// Test constructor template
var constructorBuf bytes.Buffer
err = constructorTmpl.Execute(&constructorBuf, s)
require.NoError(t, err)
constructorStr := constructorBuf.String()
assert.Contains(t, constructorStr, "func MakeConfigStructLenses() ConfigStructLenses")
assert.Contains(t, constructorStr, "isoValue := I.FromZero[string]()")
assert.Contains(t, constructorStr, "isoCount := I.FromZero[int]()")
assert.Contains(t, constructorStr, "isoPointer := I.FromZero[*string]()")
}
func TestLensCommandFlags(t *testing.T) {
cmd := LensCommand()
assert.Equal(t, "lens", cmd.Name)
assert.Equal(t, "generate lens code for annotated structs", cmd.Usage)
assert.Contains(t, strings.ToLower(cmd.Description), "fp-go:lens")
assert.Contains(t, strings.ToLower(cmd.Description), "lenso")
// Check flags
assert.Len(t, cmd.Flags, 3)
var hasDir, hasFilename, hasVerbose bool
for _, flag := range cmd.Flags {
switch flag.Names()[0] {
case "dir":
hasDir = true
case "filename":
hasFilename = true
case "verbose":
hasVerbose = true
}
}
assert.True(t, hasDir, "should have dir flag")
assert.True(t, hasFilename, "should have filename flag")
assert.True(t, hasVerbose, "should have verbose flag")
}

376
v2/cli/monad.go Normal file
View File

@@ -0,0 +1,376 @@
// 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 (
"fmt"
"os"
"strings"
)
// Deprecated:
func tupleType(name string) func(i int) string {
return func(i int) string {
var buf strings.Builder
buf.WriteString(fmt.Sprintf("T.Tuple%d[", i))
for j := 0; j < i; j++ {
if j > 0 {
buf.WriteString(", ")
}
buf.WriteString(fmt.Sprintf("%s%d", name, j+1))
}
buf.WriteString("]")
return buf.String()
}
}
func tupleTypePlain(name string) func(i int) string {
return func(i int) string {
var buf strings.Builder
buf.WriteString(fmt.Sprintf("tuple.Tuple%d[", i))
for j := 0; j < i; j++ {
if j > 0 {
buf.WriteString(", ")
}
buf.WriteString(fmt.Sprintf("%s%d", name, j+1))
}
buf.WriteString("]")
return buf.String()
}
}
func monadGenerateSequenceTNonGeneric(
hkt func(string) string,
fmap func(string, string) string,
fap func(string, string) string,
) func(f *os.File, i int) {
return func(f *os.File, i int) {
tuple := tupleType("T")(i)
fmt.Fprintf(f, "SequenceT%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, "](")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d %s", j+1, hkt(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, ") %s {", hkt(tuple))
// the actual apply callback
fmt.Fprintf(f, " return apply.SequenceT%d(\n", i)
// map callback
curried := func(count int) string {
var buf strings.Builder
for j := count; j < i; j++ {
buf.WriteString(fmt.Sprintf("func(T%d)", j+1))
}
buf.WriteString(tuple)
return buf.String()
}
fmt.Fprintf(f, " %s,\n", fmap("T1", curried(1)))
for j := 1; j < i; j++ {
fmt.Fprintf(f, " %s,\n", fap(curried(j+1), fmt.Sprintf("T%d", j)))
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " T%d,\n", j+1)
}
fmt.Fprintf(f, " )\n")
fmt.Fprintf(f, "}\n")
}
}
func monadGenerateSequenceTGeneric(
hkt func(string) string,
fmap func(string, string) string,
fap func(string, string) string,
) func(f *os.File, i int) {
return func(f *os.File, i int) {
tuple := tupleType("T")(i)
fmt.Fprintf(f, "SequenceT%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, "](")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d %s", j+1, hkt(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, ") %s {", hkt(tuple))
// the actual apply callback
fmt.Fprintf(f, " return apply.SequenceT%d(\n", i)
// map callback
curried := func(count int) string {
var buf strings.Builder
for j := count; j < i; j++ {
buf.WriteString(fmt.Sprintf("func(T%d)", j+1))
}
buf.WriteString(tuple)
return buf.String()
}
fmt.Fprintf(f, " %s,\n", fmap("T1", curried(1)))
for j := 1; j < i; j++ {
fmt.Fprintf(f, " %s,\n", fap(curried(j+1), fmt.Sprintf("T%d", j)))
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " T%d,\n", j+1)
}
fmt.Fprintf(f, " )\n")
fmt.Fprintf(f, "}\n")
}
}
func generateTraverseTuple1(
hkt func(string) string,
infix string) func(f *os.File, i int) {
return func(f *os.File, i int) {
tuple := tupleType("T")(i)
fmt.Fprintf(f, "\n// TraverseTuple%d converts a [Tuple%d] of [A] via transformation functions transforming [A] to [%s] into a [%s].\n", i, i, hkt("A"), hkt(fmt.Sprintf("Tuple%d", i)))
fmt.Fprintf(f, "func TraverseTuple%d[", i)
// functions
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "F%d ~func(A%d) %s", j+1, j+1, hkt(fmt.Sprintf("T%d", j+1)))
}
if infix != "" {
fmt.Fprintf(f, ", %s", infix)
}
// types
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", A%d, T%d", j+1, j+1)
}
fmt.Fprintf(f, " any](")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "f%d F%d", j+1, j+1)
}
fmt.Fprintf(f, ") func (T.Tuple%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "A%d", j+1)
}
fmt.Fprintf(f, "]) %s {\n", hkt(tuple))
fmt.Fprintf(f, " return func(t T.Tuple%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "A%d", j+1)
}
fmt.Fprintf(f, "]) %s {\n", hkt(tuple))
fmt.Fprintf(f, " return A.TraverseTuple%d(\n", i)
// map
fmt.Fprintf(f, " Map[")
if infix != "" {
fmt.Fprintf(f, "%s, T1,", infix)
} else {
fmt.Fprintf(f, "T1,")
}
for j := 1; j < i; j++ {
fmt.Fprintf(f, " func(T%d)", j+1)
}
fmt.Fprintf(f, " %s],\n", tuple)
// applicatives
for j := 1; j < i; j++ {
fmt.Fprintf(f, " Ap[")
for k := j + 1; k < i; k++ {
if k > j+1 {
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "func(T%d)", k+1)
}
if j < i-1 {
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "%s", tuple)
if infix != "" {
fmt.Fprintf(f, ", %s", infix)
}
fmt.Fprintf(f, ", T%d],\n", j+1)
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " f%d,\n", j+1)
}
fmt.Fprintf(f, " t,\n")
fmt.Fprintf(f, " )\n")
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, "}\n")
}
}
func generateSequenceTuple1(
hkt func(string) string,
infix string) func(f *os.File, i int) {
return func(f *os.File, i int) {
tuple := tupleType("T")(i)
fmt.Fprintf(f, "\n// SequenceTuple%d converts a [Tuple%d] of [%s] into an [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
fmt.Fprintf(f, "func SequenceTuple%d[", i)
if infix != "" {
fmt.Fprintf(f, "%s", infix)
}
for j := 0; j < i; j++ {
if infix != "" || j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any](t T.Tuple%d[", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "%s", hkt(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, "]) %s {\n", hkt(tuple))
fmt.Fprintf(f, " return A.SequenceTuple%d(\n", i)
// map
fmt.Fprintf(f, " Map[")
if infix != "" {
fmt.Fprintf(f, "%s, T1,", infix)
} else {
fmt.Fprintf(f, "T1,")
}
for j := 1; j < i; j++ {
fmt.Fprintf(f, " func(T%d)", j+1)
}
fmt.Fprintf(f, " %s],\n", tuple)
// applicatives
for j := 1; j < i; j++ {
fmt.Fprintf(f, " Ap[")
for k := j + 1; k < i; k++ {
if k > j+1 {
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "func(T%d)", k+1)
}
if j < i-1 {
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "%s", tuple)
if infix != "" {
fmt.Fprintf(f, ", %s", infix)
}
fmt.Fprintf(f, ", T%d],\n", j+1)
}
fmt.Fprintf(f, " t,\n")
fmt.Fprintf(f, " )\n")
fmt.Fprintf(f, "}\n")
}
}
func generateSequenceT1(
hkt func(string) string,
infix string) func(f *os.File, i int) {
return func(f *os.File, i int) {
tuple := tupleType("T")(i)
fmt.Fprintf(f, "\n// SequenceT%d converts %d parameters of [%s] into a [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
fmt.Fprintf(f, "func SequenceT%d[", i)
if infix != "" {
fmt.Fprintf(f, "%s", infix)
}
for j := 0; j < i; j++ {
if infix != "" || j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
}
fmt.Fprintf(f, " any](")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d %s", j+1, hkt(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, ") %s {\n", hkt(tuple))
fmt.Fprintf(f, " return A.SequenceT%d(\n", i)
// map
fmt.Fprintf(f, " Map[")
if infix != "" {
fmt.Fprintf(f, "%s, T1,", infix)
} else {
fmt.Fprintf(f, "T1,")
}
for j := 1; j < i; j++ {
fmt.Fprintf(f, " func(T%d)", j+1)
}
fmt.Fprintf(f, " %s],\n", tuple)
// applicatives
for j := 1; j < i; j++ {
fmt.Fprintf(f, " Ap[")
for k := j + 1; k < i; k++ {
if k > j+1 {
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "func(T%d)", k+1)
}
if j < i-1 {
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "%s", tuple)
if infix != "" {
fmt.Fprintf(f, ", %s", infix)
}
fmt.Fprintf(f, ", T%d],\n", j+1)
}
for j := 0; j < i; j++ {
fmt.Fprintf(f, " t%d,\n", j+1)
}
fmt.Fprintf(f, " )\n")
fmt.Fprintf(f, "}\n")
}
}

413
v2/cli/monad2.go Normal file
View File

@@ -0,0 +1,413 @@
// 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 (
"fmt"
"os"
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
)
var (
concStrgs = A.Monoid[string]().Concat
intercalStrgs = A.Intercalate(S.Monoid)
concAllStrgs = A.ConcatAll(A.Monoid[string]())
)
func joinAll(middle string) func(all ...[]string) string {
ic := intercalStrgs(middle)
return func(all ...[]string) string {
return ic(concAllStrgs(all))
}
}
// Deprecated:
func deprecatedGenerateGenericSequenceT(
nonGenericType func(string) string,
genericType func(string) string,
extra []string,
) func(f, fg *os.File, i int) {
return func(f, fg *os.File, i int) {
// tuple
tuple := tupleType("T")(i)
// all types T
typesT := A.MakeBy(i, F.Flow2(
N.Inc[int],
S.Format[int]("T%d"),
))
// non generic version
fmt.Fprintf(f, "\n// SequenceT%d converts %d [%s] into a [%s]\n", i, i, nonGenericType("T"), nonGenericType(tuple))
fmt.Fprintf(f, "func SequenceT%d[%s any](\n", i, joinAll(", ")(extra, typesT))
for j := 0; j < i; j++ {
fmt.Fprintf(f, " t%d %s,\n", j+1, nonGenericType(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, ") %s {\n", nonGenericType(tuple))
fmt.Fprintf(f, " return G.SequenceT%d[\n", i)
fmt.Fprintf(f, " %s,\n", nonGenericType(tuple))
for j := 0; j < i; j++ {
fmt.Fprintf(f, " %s,\n", nonGenericType(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, " ](")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", j+1)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintf(f, "}\n")
// generic version
fmt.Fprintf(fg, "\n// SequenceT%d converts %d [%s] into a [%s]\n", i, i, genericType("T"), genericType(tuple))
fmt.Fprintf(fg, "func SequenceT%d[\n", i)
fmt.Fprintf(fg, " G_TUPLE%d ~%s,\n", i, genericType(tuple))
for j := 0; j < i; j++ {
fmt.Fprintf(fg, " G_T%d ~%s, \n", j+1, genericType(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(fg, " %s any](\n", joinAll(", ")(extra, typesT))
for j := 0; j < i; j++ {
fmt.Fprintf(fg, " t%d G_T%d,\n", j+1, j+1)
}
fmt.Fprintf(fg, ") G_TUPLE%d {\n", i)
fmt.Fprintf(fg, " return A.SequenceT%d(\n", i)
// map call
var cio string
cb := generateNestedCallbacks(1, i)
if i > 1 {
cio = genericType(cb)
} else {
cio = fmt.Sprintf("G_TUPLE%d", i)
}
fmt.Fprintf(fg, " Map[%s],\n", joinAll(", ")(A.From("G_T1", cio), extra, A.From("T1", cb)))
// the apply calls
for j := 1; j < i; j++ {
if j < i-1 {
cb := generateNestedCallbacks(j+1, i)
cio = genericType(cb)
} else {
cio = fmt.Sprintf("G_TUPLE%d", i)
}
fmt.Fprintf(fg, " Ap[%s, %s, G_T%d],\n", cio, genericType(generateNestedCallbacks(j, i)), j+1)
}
// function parameters
for j := 0; j < i; j++ {
fmt.Fprintf(fg, " t%d,\n", j+1)
}
fmt.Fprintf(fg, " )\n")
fmt.Fprintf(fg, "}\n")
}
}
// Deprecated:
func deprecatedGenerateGenericSequenceTuple(
nonGenericType func(string) string,
genericType func(string) string,
extra []string,
) func(f, fg *os.File, i int) {
return func(f, fg *os.File, i int) {
// tuple
tuple := tupleType("T")(i)
// all types T
typesT := A.MakeBy(i, F.Flow2(
N.Inc[int],
S.Format[int]("T%d"),
))
// non generic version
fmt.Fprintf(f, "\n// SequenceTuple%d converts a [T.Tuple%d[%s]] into a [%s]\n", i, i, nonGenericType("T"), nonGenericType(tuple))
fmt.Fprintf(f, "func SequenceTuple%d[%s any](t T.Tuple%d[", i, joinAll(", ")(extra, typesT), i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "%s", nonGenericType(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, "]) %s {\n", nonGenericType(tuple))
fmt.Fprintf(f, " return G.SequenceTuple%d[\n", i)
fmt.Fprintf(f, " %s,\n", nonGenericType(tuple))
for j := 0; j < i; j++ {
fmt.Fprintf(f, " %s,\n", nonGenericType(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, " ](t)\n")
fmt.Fprintf(f, "}\n")
// generic version
fmt.Fprintf(fg, "\n// SequenceTuple%d converts a [T.Tuple%d[%s]] into a [%s]\n", i, i, genericType("T"), genericType(tuple))
fmt.Fprintf(fg, "func SequenceTuple%d[\n", i)
fmt.Fprintf(fg, " G_TUPLE%d ~%s,\n", i, genericType(tuple))
for j := 0; j < i; j++ {
fmt.Fprintf(fg, " G_T%d ~%s, \n", j+1, genericType(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(fg, " %s any](t T.Tuple%d[", joinAll(", ")(extra, typesT), i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "G_T%d", j+1)
}
fmt.Fprintf(fg, "]) G_TUPLE%d {\n", i)
fmt.Fprintf(fg, " return A.SequenceTuple%d(\n", i)
// map call
var cio string
cb := generateNestedCallbacks(1, i)
if i > 1 {
cio = genericType(cb)
} else {
cio = fmt.Sprintf("G_TUPLE%d", i)
}
fmt.Fprintf(fg, " Map[%s],\n", joinAll(", ")(A.From("G_T1", cio), extra, A.From("T1", cb)))
// the apply calls
for j := 1; j < i; j++ {
if j < i-1 {
cb := generateNestedCallbacks(j+1, i)
cio = genericType(cb)
} else {
cio = fmt.Sprintf("G_TUPLE%d", i)
}
fmt.Fprintf(fg, " Ap[%s, %s, G_T%d],\n", cio, genericType(generateNestedCallbacks(j, i)), j+1)
}
// function parameters
fmt.Fprintf(fg, " t)\n")
fmt.Fprintf(fg, "}\n")
}
}
// Deprecated:
func deprecatedGenerateGenericTraverseTuple(
nonGenericType func(string) string,
genericType func(string) string,
extra []string,
) func(f, fg *os.File, i int) {
return func(f, fg *os.File, i int) {
// tuple
tupleT := tupleType("T")(i)
tupleA := tupleType("A")(i)
// all types T
typesT := A.MakeBy(i, F.Flow2(
N.Inc[int],
S.Format[int]("T%d"),
))
// all types A
typesA := A.MakeBy(i, F.Flow2(
N.Inc[int],
S.Format[int]("A%d"),
))
// all function types
typesF := A.MakeBy(i, F.Flow2(
N.Inc[int],
func(j int) string {
return fmt.Sprintf("F%d ~func(A%d) %s", j, j, nonGenericType(fmt.Sprintf("T%d", j)))
},
))
// non generic version
fmt.Fprintf(f, "\n// TraverseTuple%d converts a [T.Tuple%d[%s]] into a [%s]\n", i, i, nonGenericType("T"), nonGenericType(tupleT))
fmt.Fprintf(f, "func TraverseTuple%d[%s any](", i, joinAll(", ")(typesF, extra, typesA, typesT))
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "f%d F%d", j+1, j+1)
}
fmt.Fprintf(f, ") func(%s) %s {\n", tupleA, nonGenericType(tupleT))
fmt.Fprintf(f, " return G.TraverseTuple%d[%s](", i, nonGenericType(tupleT))
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "f%d", j+1)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintf(f, "}\n")
// generic version
fmt.Fprintf(fg, "\n// TraverseTuple%d converts a [T.Tuple%d[%s]] into a [%s]\n", i, i, genericType("T"), genericType(tupleT))
fmt.Fprintf(fg, "func TraverseTuple%d[\n", i)
fmt.Fprintf(fg, " G_TUPLE%d ~%s,\n", i, genericType(tupleT))
for j := 0; j < i; j++ {
fmt.Fprintf(fg, " F%d ~func(A%d) G_T%d,\n", j+1, j+1, j+1)
}
for j := 0; j < i; j++ {
fmt.Fprintf(fg, " G_T%d ~%s, \n", j+1, genericType(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(fg, " %s any](", joinAll(", ")(extra, typesA, typesT))
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(fg, ", ")
}
fmt.Fprintf(fg, "f%d F%d", j+1, j+1)
}
fmt.Fprintf(fg, ") func(%s) G_TUPLE%d {\n", tupleA, i)
fmt.Fprintf(fg, " return func(t %s) G_TUPLE%d {\n", tupleA, i)
fmt.Fprintf(fg, " return A.TraverseTuple%d(\n", i)
// map call
var cio string
cb := generateNestedCallbacks(1, i)
if i > 1 {
cio = genericType(cb)
} else {
cio = fmt.Sprintf("G_TUPLE%d", i)
}
fmt.Fprintf(fg, " Map[%s],\n", joinAll(", ")(A.From("G_T1", cio), extra, A.From("T1", cb)))
// the apply calls
for j := 1; j < i; j++ {
if j < i-1 {
cb := generateNestedCallbacks(j+1, i)
cio = genericType(cb)
} else {
cio = fmt.Sprintf("G_TUPLE%d", i)
}
fmt.Fprintf(fg, " Ap[%s, %s, G_T%d],\n", cio, genericType(generateNestedCallbacks(j, i)), j+1)
}
// function parameters
for j := 0; j < i; j++ {
fmt.Fprintf(fg, " f%d,\n", j+1)
}
// tuple parameter
fmt.Fprintf(fg, " t)\n")
fmt.Fprintf(fg, " }\n")
fmt.Fprintf(fg, "}\n")
}
}
func generateGenericSequenceT(
suffix string,
nonGenericType func(string) string,
extra []string,
) func(f *os.File, i int) {
return func(f *os.File, i int) {
// tuple
tuple := tupleTypePlain("T")(i)
// all types T
typesT := A.MakeBy(i, F.Flow2(
N.Inc[int],
S.Format[int]("T%d"),
))
// non generic version
fmt.Fprintf(f, "\n// Sequence%sT%d converts %d [%s] into a [%s]\n", suffix, i, i, nonGenericType("T"), nonGenericType(tuple))
fmt.Fprintf(f, "func Sequence%sT%d[%s any](\n", suffix, i, joinAll(", ")(extra, typesT))
for j := 0; j < i; j++ {
fmt.Fprintf(f, " t%d %s,\n", j+1, nonGenericType(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, ") %s {\n", nonGenericType(tuple))
fmt.Fprintf(f, " return apply.SequenceT%d(\n", i)
fmt.Fprintf(f, " Map[%s],\n", joinAll(", ")(extra, A.From("T1", generateNestedCallbacksPlain(1, i))))
// the apply calls
for j := 2; j <= i; j++ {
fmt.Fprintf(f, " Ap%s[%s],\n", suffix, joinAll(", ")(A.Of(generateNestedCallbacksPlain(j, i)), extra, A.Of(fmt.Sprintf("T%d", j))))
}
// function parameters
for j := 1; j <= i; j++ {
fmt.Fprintf(f, " t%d,\n", j)
}
fmt.Fprintf(f, " )\n")
fmt.Fprintf(f, "}\n")
}
}
func generateGenericSequenceTuple(
suffix string,
nonGenericType func(string) string,
extra []string,
) func(f *os.File, i int) {
return func(f *os.File, i int) {
// tuple
tuple := tupleTypePlain("T")(i)
// all types T
typesT := A.MakeBy(i, F.Flow2(
N.Inc[int],
S.Format[int]("T%d"),
))
// non generic version
fmt.Fprintf(f, "\n// Sequence%sTuple%d converts a [tuple.Tuple%d[%s]] into a [%s]\n", suffix, i, i, nonGenericType("T"), nonGenericType(tuple))
fmt.Fprintf(f, "func Sequence%sTuple%d[%s any](t tuple.Tuple%d[", suffix, i, joinAll(", ")(extra, typesT), i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "%s", nonGenericType(fmt.Sprintf("T%d", j+1)))
}
fmt.Fprintf(f, "]) %s {\n", nonGenericType(tuple))
fmt.Fprintf(f, " return apply.SequenceTuple%d(\n", i)
fmt.Fprintf(f, " Map[%s],\n", joinAll(", ")(extra, A.From("T1", generateNestedCallbacksPlain(1, i))))
// the apply calls
for j := 2; j <= i; j++ {
fmt.Fprintf(f, " Ap%s[%s],\n", suffix, joinAll(", ")(A.Of(generateNestedCallbacksPlain(j, i)), extra, A.Of(fmt.Sprintf("T%d", j))))
}
// function parameters
fmt.Fprintf(f, " t,\n")
// function parameters
fmt.Fprintf(f, " )\n")
fmt.Fprintf(f, "}\n")
}
}
func generateGenericTraverseTuple(
suffix string,
nonGenericType func(string) string,
extra []string,
) func(f *os.File, i int) {
return func(f *os.File, i int) {
// all types T
typesT := A.MakeBy(i, F.Flow2(
N.Inc[int],
S.Format[int]("T%d"),
))
// all types A
typesA := A.MakeBy(i, F.Flow2(
N.Inc[int],
S.Format[int]("A%d"),
))
// function types
typesF := A.MakeBy(i, func(j int) string {
return fmt.Sprintf("F%d ~func(A%d) %s", j+1, j+1, nonGenericType(fmt.Sprintf("T%d", j+1)))
})
paramF := A.MakeBy(i, func(j int) string {
return fmt.Sprintf("f%d F%d", j+1, j+1)
})
// return type
paramType := fmt.Sprintf("tuple.Tuple%d[%s]", i, joinAll(", ")(typesA))
returnType := nonGenericType(fmt.Sprintf("tuple.Tuple%d[%s]", i, joinAll(", ")(typesT)))
// non generic version
fmt.Fprintf(f, "\n// Traverse%sTuple%d converts a [%s] into a [%s]\n", suffix, i, paramType, returnType)
fmt.Fprintf(f, "func Traverse%sTuple%d[%s any](%s) func(%s) %s {\n", suffix, i, joinAll(", ")(extra, typesF, typesT, typesA), joinAll(", ")(paramF), paramType, returnType)
fmt.Fprintf(f, " return func(t %s) %s {\n", paramType, returnType)
fmt.Fprintf(f, " return apply.TraverseTuple%d(\n", i)
fmt.Fprintf(f, " Map[%s],\n", joinAll(", ")(extra, A.From("T1", generateNestedCallbacksPlain(1, i))))
// the apply calls
for j := 2; j <= i; j++ {
fmt.Fprintf(f, " Ap%s[%s],\n", suffix, joinAll(", ")(A.Of(generateNestedCallbacksPlain(j, i)), extra, A.Of(fmt.Sprintf("T%d", j))))
}
// the function parameters
for j := 1; j <= i; j++ {
fmt.Fprintf(f, " f%d,\n", j)
}
// function parameters
fmt.Fprintf(f, " t,\n")
// function parameters
fmt.Fprintf(f, " )\n")
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, "}\n")
}
}

210
v2/cli/option.go Normal file
View File

@@ -0,0 +1,210 @@
// 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 (
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
)
func optionHKT(typeA string) string {
return fmt.Sprintf("Option[%s]", typeA)
}
func generateOptionTraverseTuple(f *os.File, i int) {
generateTraverseTuple1(optionHKT, "")(f, i)
}
func generateOptionSequenceTuple(f *os.File, i int) {
generateSequenceTuple1(optionHKT, "")(f, i)
}
func generateOptionSequenceT(f *os.File, i int) {
generateSequenceT1(optionHKT, "")(f, i)
}
func generateOptionize(f *os.File, i int) {
// Create the optionize version
fmt.Fprintf(f, "\n// Optionize%d converts a function with %d parameters returning a tuple of a return value R and a boolean into a function with %d parameters returning an Option[R]\n", i, i, i)
fmt.Fprintf(f, "func Optionize%d[F ~func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") (R, bool)")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ", R any](f F) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") Option[R] {\n")
fmt.Fprintf(f, " return func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", j, j)
}
fmt.Fprintf(f, ") Option[R] {\n")
fmt.Fprintf(f, " return optionize(func() (R, bool) {\n")
fmt.Fprintf(f, " return f(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", j)
}
fmt.Fprintln(f, ")")
fmt.Fprintln(f, " })")
fmt.Fprintln(f, " }")
fmt.Fprintln(f, "}")
}
func generateUnoptionize(f *os.File, i int) {
// Create the optionize version
fmt.Fprintf(f, "\n// Unoptionize%d converts a function with %d parameters returning a tuple of a return value R and a boolean into a function with %d parameters returning an Option[R]\n", i, i, i)
fmt.Fprintf(f, "func Unoptionize%d[F ~func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") Option[R]")
for j := 0; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ", R any](f F) func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, ") (R, bool) {\n")
fmt.Fprintf(f, " return func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", j, j)
}
fmt.Fprintf(f, ") (R, bool) {\n")
fmt.Fprintf(f, " return Unwrap(f(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", j)
}
fmt.Fprintln(f, "))")
fmt.Fprintln(f, " }")
fmt.Fprintln(f, "}")
}
func generateOptionHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n\n", pkg)
fmt.Fprintf(f, `
import (
A "github.com/IBM/fp-go/v2/internal/apply"
T "github.com/IBM/fp-go/v2/tuple"
)
`)
// print out some helpers
fmt.Fprintf(f, `// optionize converts a nullary function to an option
func optionize[R any](f func() (R, bool)) Option[R] {
if r, ok := f(); ok {
return Some(r)
}
return None[R]()
}
`)
// zero level functions
// optionize
generateOptionize(f, 0)
// unoptionize
generateUnoptionize(f, 0)
for i := 1; i <= count; i++ {
// optionize
generateOptionize(f, i)
// unoptionize
generateUnoptionize(f, i)
// sequenceT
generateOptionSequenceT(f, i)
// sequenceTuple
generateOptionSequenceTuple(f, i)
// traverseTuple
generateOptionTraverseTuple(f, i)
}
return nil
}
func OptionCommand() *C.Command {
return &C.Command{
Name: "option",
Usage: "generate code for Option",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generateOptionHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

433
v2/cli/pipe.go Normal file
View File

@@ -0,0 +1,433 @@
// 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 (
"fmt"
"log"
"os"
"path/filepath"
"time"
C "github.com/urfave/cli/v2"
)
func generateUnsliced(f *os.File, i int) {
// Create the optionize version
fmt.Fprintf(f, "\n// Unsliced%d converts a function taking a slice parameter into a function with %d parameters\n", i, i)
fmt.Fprintf(f, "func Unsliced%d[F ~func([]T) R, T, R any](f F) func(", i)
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T")
}
fmt.Fprintf(f, ") R {\n")
fmt.Fprintf(f, " return func(")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", j+1)
}
if i > 0 {
fmt.Fprintf(f, " T")
}
fmt.Fprintf(f, ") R {\n")
fmt.Fprintf(f, " return f([]T{")
for j := 0; j < i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", j+1)
}
fmt.Fprintln(f, "})")
fmt.Fprintln(f, " }")
fmt.Fprintln(f, "}")
}
func generateVariadic(f *os.File, i int) {
// Create the nullary version
fmt.Fprintf(f, "\n// Variadic%d converts a function taking %d parameters and a final slice into a function with %d parameters but a final variadic argument\n", i, i, i)
fmt.Fprintf(f, "func Variadic%d[", i)
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "V, R any](f func(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "[]V) R) func(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "...V) R {\n")
fmt.Fprintf(f, " return func(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", j, j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "v ...V) R {\n")
fmt.Fprintf(f, " return f(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "v)\n")
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, "}\n")
}
func generateUnvariadic(f *os.File, i int) {
// Create the nullary version
fmt.Fprintf(f, "\n// Unvariadic%d converts a function taking %d parameters and a final variadic argument into a function with %d parameters but a final slice argument\n", i, i, i)
fmt.Fprintf(f, "func Unvariadic%d[", i)
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "V, R any](f func(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "...V) R) func(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "[]V) R {\n")
fmt.Fprintf(f, " return func(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", j, j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "v []V) R {\n")
fmt.Fprintf(f, " return f(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d", j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "v...)\n")
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, "}\n")
}
func generateNullary(f *os.File, i int) {
// Create the nullary version
fmt.Fprintf(f, "\n// Nullary%d creates a parameter less function from a parameter less function and %d functions. When executed the first parameter less function gets executed and then the result is piped through the remaining functions\n", i, i-1)
fmt.Fprintf(f, "func Nullary%d[F1 ~func() T1", i)
for j := 2; j <= i; j++ {
fmt.Fprintf(f, ", F%d ~func(T%d) T%d", j, j-1, j)
}
for j := 1; j <= i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, " any](f1 F1")
for j := 2; j <= i; j++ {
fmt.Fprintf(f, ", f%d F%d", j, j)
}
fmt.Fprintf(f, ") func() T%d {\n", i)
fmt.Fprintf(f, " return func() T%d {\n", i)
fmt.Fprintf(f, " return Pipe%d(f1()", i-1)
for j := 2; j <= i; j++ {
fmt.Fprintf(f, ", f%d", j)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintln(f, " }")
fmt.Fprintln(f, "}")
}
func generateFlow(f *os.File, i int) {
// Create the flow version
fmt.Fprintf(f, "\n// Flow%d creates a function that takes an initial value t0 and successively applies %d functions where the input of a function is the return value of the previous function\n// The final return value is the result of the last function application\n", i, i)
fmt.Fprintf(f, "func Flow%d[", i)
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "F%d ~func(T%d) T%d", j, j-1, j)
}
for j := 0; j <= i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, " any](")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "f%d F%d", j, j)
}
fmt.Fprintf(f, ") func(T0) T%d {\n", i)
fmt.Fprintf(f, " return func(t0 T0) T%d {\n", i)
fmt.Fprintf(f, " return Pipe%d(t0", i)
for j := 1; j <= i; j++ {
fmt.Fprintf(f, ", f%d", j)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintln(f, " }")
fmt.Fprintln(f, "}")
}
func generatePipe(f *os.File, i int) {
// Create the pipe version
fmt.Fprintf(f, "\n// Pipe%d takes an initial value t0 and successively applies %d functions where the input of a function is the return value of the previous function\n// The final return value is the result of the last function application\n", i, i)
fmt.Fprintf(f, "func Pipe%d[", i)
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "F%d ~func(T%d) T%d", j, j-1, j)
}
if i > 0 {
fmt.Fprintf(f, ", ")
}
for j := 0; j <= i; j++ {
if j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j)
}
fmt.Fprintf(f, " any](t0 T0")
for j := 1; j <= i; j++ {
fmt.Fprintf(f, ", f%d F%d", j, j)
}
fmt.Fprintf(f, ") T%d {\n", i)
fmt.Fprintf(f, " return ")
for j := i; j >= 1; j-- {
fmt.Fprintf(f, "f%d(", j)
}
fmt.Fprintf(f, "t0")
for j := 1; j <= i; j++ {
fmt.Fprintf(f, ")")
}
fmt.Fprintf(f, "\n")
fmt.Fprintln(f, "}")
}
func recurseCurry(f *os.File, indent string, total, count int) {
if count == 1 {
fmt.Fprintf(f, "%sreturn func(t%d T%d) T%d {\n", indent, total-1, total-1, total)
fmt.Fprintf(f, "%s return f(t0", indent)
for i := 1; i < total; i++ {
fmt.Fprintf(f, ", t%d", i)
}
fmt.Fprintf(f, ")\n")
fmt.Fprintf(f, "%s}\n", indent)
} else {
fmt.Fprintf(f, "%sreturn", indent)
for i := total - count + 1; i <= total; i++ {
fmt.Fprintf(f, " func(t%d T%d)", i-1, i-1)
}
fmt.Fprintf(f, " T%d {\n", total)
recurseCurry(f, fmt.Sprintf(" %s", indent), total, count-1)
fmt.Fprintf(f, "%s}\n", indent)
}
}
func generateCurry(f *os.File, i int) {
// Create the curry version
fmt.Fprintf(f, "\n// Curry%d takes a function with %d parameters and returns a cascade of functions each taking only one parameter.\n// The inverse function is [Uncurry%d]\n", i, i, i)
fmt.Fprintf(f, "func Curry%d[FCT ~func(T0", i)
for j := 1; j < i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, ") T%d", i)
// type arguments
for j := 0; j <= i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, " any](f FCT) func(T0)")
for j := 2; j <= i; j++ {
fmt.Fprintf(f, " func(T%d)", j-1)
}
fmt.Fprintf(f, " T%d {\n", i)
recurseCurry(f, " ", i, i)
fmt.Fprintf(f, "}\n")
}
func generateUncurry(f *os.File, i int) {
// Create the uncurry version
fmt.Fprintf(f, "\n// Uncurry%d takes a cascade of %d functions each taking only one parameter and returns a function with %d parameters .\n// The inverse function is [Curry%d]\n", i, i, i, i)
fmt.Fprintf(f, "func Uncurry%d[FCT ~func(T0)", i)
for j := 1; j < i; j++ {
fmt.Fprintf(f, " func(T%d)", j)
}
fmt.Fprintf(f, " T%d", i)
// the type parameters
for j := 0; j <= i; j++ {
fmt.Fprintf(f, ", T%d", j)
}
fmt.Fprintf(f, " any](f FCT) func(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j-1)
}
fmt.Fprintf(f, ") T%d {\n", i)
fmt.Fprintf(f, " return func(")
for j := 1; j <= i; j++ {
if j > 1 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "t%d T%d", j-1, j-1)
}
fmt.Fprintf(f, ") T%d {\n", i)
fmt.Fprintf(f, " return f")
for j := 1; j <= i; j++ {
fmt.Fprintf(f, "(t%d)", j-1)
}
fmt.Fprintln(f)
fmt.Fprintf(f, " }\n")
fmt.Fprintf(f, "}\n")
}
func generatePipeHelpers(filename string, count int) error {
dir, err := os.Getwd()
if err != nil {
return err
}
absDir, err := filepath.Abs(dir)
if err != nil {
return err
}
pkg := filepath.Base(absDir)
f, err := os.Create(filepath.Clean(filename))
if err != nil {
return err
}
defer f.Close()
// log
log.Printf("Generating code in [%s] for package [%s] with [%d] repetitions ...", filename, pkg, count)
// some header
fmt.Fprintln(f, "// Code generated by go generate; DO NOT EDIT.")
fmt.Fprintln(f, "// This file was generated by robots at")
fmt.Fprintf(f, "// %s\n\n", time.Now())
fmt.Fprintf(f, "package %s\n", pkg)
// pipe
generatePipe(f, 0)
// variadic
generateVariadic(f, 0)
// unvariadic
generateUnvariadic(f, 0)
// unsliced
generateUnsliced(f, 0)
for i := 1; i <= count; i++ {
// pipe
generatePipe(f, i)
// flow
generateFlow(f, i)
// nullary
generateNullary(f, i)
// curry
generateCurry(f, i)
// uncurry
generateUncurry(f, i)
// variadic
generateVariadic(f, i)
// unvariadic
generateUnvariadic(f, i)
// unsliced
generateUnsliced(f, i)
}
return nil
}
func PipeCommand() *C.Command {
return &C.Command{
Name: "pipe",
Usage: "generate code for pipe, flow, curry, etc",
Description: "Code generation for pipe, flow, curry, etc",
Flags: []C.Flag{
flagCount,
flagFilename,
},
Action: func(ctx *C.Context) error {
return generatePipeHelpers(
ctx.String(keyFilename),
ctx.Int(keyCount),
)
},
}
}

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