1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-29 10:36:04 +02:00

Compare commits

..

12 Commits

Author SHA1 Message Date
Dr. Carsten Leue
a7aa7e3560 fix: better DI example
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 22:45:17 +01:00
Dr. Carsten Leue
ff2a4299b2 fix: add some useful lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 17:39:34 +01:00
Dr. Carsten Leue
edd66d63e6 fix: more codec
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-27 14:51:35 +01:00
Dr. Carsten Leue
909aec8eba fix: better sequence iter
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-26 10:41:25 +01:00
Obed Tetteh
da0344f9bd feat(iterator): add Last function with Option return type (#155)
- Add Last function to retrieve the final element from an iterator,
  returning Some(element) for non-empty sequences and None for empty ones.
- Includes tests covering simple types and  complex types
- Add documentation including example code
2026-01-26 09:04:51 +01:00
Dr. Carsten Leue
cd79dd56b9 fix: simplify tests a bit
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 17:56:28 +01:00
Dr. Carsten Leue
df07599a9e fix: some docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:40:45 +01:00
Dr. Carsten Leue
30ad0e4dd8 doc: add validation docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:26:53 +01:00
Dr. Carsten Leue
2374d7f1e4 fix: support unexported fields for lenses
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:18:44 +01:00
Dr. Carsten Leue
eafc008798 fix: doc for lens generation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 16:00:11 +01:00
Dr. Carsten Leue
46bf065e34 fix: migrate CLI to github.com/urfave/v3
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-23 12:56:23 +01:00
renovate[bot]
b4e303423b chore(deps): update actions/checkout action to v6.0.2 (#153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-23 12:39:53 +01:00
91 changed files with 13230 additions and 233 deletions

View File

@@ -28,7 +28,7 @@ jobs:
fail-fast: false # Continue with other versions if one fails
steps:
# full checkout for semantic-release
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
@@ -66,7 +66,7 @@ jobs:
matrix:
go-version: ['1.24.x', '1.25.x']
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
@@ -126,7 +126,7 @@ jobs:
steps:
# full checkout for semantic-release
- name: Full checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

View File

@@ -465,7 +465,7 @@ func process() IOResult[string] {
- **ReaderIOResult** - Combine Reader, IO, and Result for complex workflows
- **Array** - Functional array operations
- **Record** - Functional record/map operations
- **Optics** - Lens, Prism, Optional, and Traversal for immutable updates
- **[Optics](./optics/README.md)** - Lens, Prism, Optional, and Traversal for immutable updates
#### Idiomatic Packages (Tuple-based, High Performance)
- **idiomatic/option** - Option monad using native Go `(value, bool)` tuples

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,6 +17,7 @@ package cli
import (
"bytes"
"context"
"go/ast"
"go/parser"
"go/token"
@@ -28,7 +29,7 @@ import (
"text/template"
S "github.com/IBM/fp-go/v2/string"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
const (
@@ -535,9 +536,9 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
fieldTypeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
fieldTypeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := fieldTypeName
@@ -697,9 +698,9 @@ func parseFile(filename string) ([]structInfo, string, error) {
continue
}
for _, name := range field.Names {
// Only export lenses for exported fields
if name.IsExported() {
typeName := getTypeName(field.Type)
// Generate lenses for both exported and unexported fields
typeName := getTypeName(field.Type)
if true { // Keep the block structure for minimal changes
isOptional := false
baseType := typeName
isComparable := false
@@ -934,12 +935,12 @@ func LensCommand() *C.Command {
flagVerbose,
flagIncludeTestFiles,
},
Action: func(ctx *C.Context) error {
Action: func(ctx context.Context, cmd *C.Command) error {
return generateLensHelpers(
ctx.String(keyLensDir),
ctx.String(keyFilename),
ctx.Bool(keyVerbose),
ctx.Bool(keyIncludeTestFile),
cmd.String(keyLensDir),
cmd.String(keyFilename),
cmd.Bool(keyVerbose),
cmd.Bool(keyIncludeTestFile),
)
},
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,168 @@
package readerreaderioresult
import (
"context"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
"github.com/IBM/fp-go/v2/result"
)
// Local modifies the outer environment before passing it to a computation.
// Useful for providing different configurations to sub-computations.
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.Local[context.Context, error, A](f)
}
// LocalIOK transforms the outer environment of a ReaderReaderIOResult using an IO-based Kleisli arrow.
// It allows you to modify the outer environment through an effectful computation before
// passing it to the ReaderReaderIOResult.
//
// This is useful when the outer environment transformation itself requires IO effects,
// such as reading from a file, making a network call, or accessing system resources,
// but these effects cannot fail (or failures are not relevant).
//
// The transformation happens in two stages:
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
// 2. The resulting R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalIOK[context.Context, error, A](f)
}
// LocalIOEitherK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
// It allows you to modify the outer environment through an effectful computation that can fail before
// passing it to the ReaderReaderIOResult.
//
// This is useful when the outer environment transformation itself requires IO effects that can fail,
// such as reading from a file that might not exist, making a network call that might timeout,
// or parsing data that might be invalid.
//
// The transformation happens in two stages:
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalIOEitherK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalIOEitherK[context.Context, A](f)
}
// LocalIOResultK transforms the outer environment of a ReaderReaderIOResult using an IOResult-based Kleisli arrow.
// This is a type-safe alias for LocalIOEitherK specialized for Result types (which use error as the error type).
//
// It allows you to modify the outer environment through an effectful computation that can fail before
// passing it to the ReaderReaderIOResult.
//
// The transformation happens in two stages:
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalIOEitherK[context.Context, A](f)
}
//go:inline
func LocalResultK[A, R1, R2 any](f result.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalEitherK[context.Context, A](f)
}
// LocalReaderIOEitherK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
// It allows you to modify the outer environment through a computation that depends on the inner context
// and can perform IO effects that may fail.
//
// This is useful when the outer environment transformation requires access to the inner context (e.g., context.Context)
// and may perform IO operations that can fail, such as database queries, API calls, or file operations.
//
// The transformation happens in three stages:
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalReaderIOEitherK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderIOEitherK[A](f)
}
// LocalReaderIOResultK transforms the outer environment of a ReaderReaderIOResult using a ReaderIOResult-based Kleisli arrow.
// This is a type-safe alias for LocalReaderIOEitherK specialized for Result types (which use error as the error type).
//
// It allows you to modify the outer environment through a computation that depends on the inner context
// and can perform IO effects that may fail.
//
// The transformation happens in three stages:
// 1. The ReaderIOResult effect f is executed with the R2 outer environment and inner context
// 2. If successful (Ok), the R1 value is passed as the outer environment to the ReaderReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderReaderIOResult
// - R1: The original outer environment type expected by the ReaderReaderIOResult
// - R2: The new input outer environment type
//
// Parameters:
// - f: A ReaderIOResult Kleisli arrow that transforms R2 to R1 with context-aware IO effects that can fail
//
// Returns:
// - A function that takes a ReaderReaderIOResult[R1, A] and returns a ReaderReaderIOResult[R2, A]
//
//go:inline
func LocalReaderIOResultK[A, R1, R2 any](f readerioresult.Kleisli[R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderIOEitherK[A](f)
}
//go:inline
func LocalReaderReaderIOEitherK[A, R1, R2 any](f Kleisli[R2, R2, R1]) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.LocalReaderReaderIOEitherK[A](f)
}

View File

@@ -0,0 +1,428 @@
// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package readerreaderioresult
import (
"context"
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type SimpleConfig struct {
Port int
}
type DetailedConfig struct {
Host string
Port int
}
// TestLocalIOK tests LocalIOK functionality
func TestLocalIOK(t *testing.T) {
ctx := context.Background()
t.Run("basic IO transformation", func(t *testing.T) {
// IO effect that loads config from a path
loadConfig := func(path string) io.IO[SimpleConfig] {
return func() SimpleConfig {
// Simulate loading config
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalIOK
adapted := LocalIOK[string](loadConfig)(useConfig)
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
})
t.Run("IO transformation with side effects", func(t *testing.T) {
var loadLog []string
loadData := func(key string) io.IO[int] {
return func() int {
loadLog = append(loadLog, "Loading: "+key)
return len(key) * 10
}
}
processData := func(n int) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Processed: %d", n))
}
}
}
adapted := LocalIOK[string](loadData)(processData)
res := adapted("test")(ctx)()
assert.Equal(t, result.Of("Processed: 40"), res)
assert.Equal(t, []string{"Loading: test"}, loadLog)
})
t.Run("error propagation in ReaderReaderIOResult", func(t *testing.T) {
loadConfig := func(path string) io.IO[SimpleConfig] {
return func() SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// ReaderReaderIOResult that returns an error
failingOperation := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Left[string](errors.New("operation failed"))
}
}
}
adapted := LocalIOK[string](loadConfig)(failingOperation)
res := adapted("config.json")(ctx)()
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOEitherK tests LocalIOEitherK functionality
func TestLocalIOEitherK(t *testing.T) {
ctx := context.Background()
t.Run("basic IOResult transformation", func(t *testing.T) {
// IOResult effect that loads config from a path (can fail)
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("error propagation from environment transformation", func(t *testing.T) {
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
return result.Left[SimpleConfig](errors.New("file not found"))
}
}
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
adapted := LocalIOEitherK[string](loadConfig)(useConfig)
res := adapted("missing.json")(ctx)()
// Error from loadConfig should propagate
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOResultK tests LocalIOResultK functionality
func TestLocalIOResultK(t *testing.T) {
ctx := context.Background()
t.Run("basic IOResult transformation", func(t *testing.T) {
// IOResult effect that loads config from a path (can fail)
loadConfig := func(path string) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalIOResultK
adapted := LocalIOResultK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("compose multiple LocalIOResultK", func(t *testing.T) {
// First transformation: string -> int (can fail)
parseID := func(s string) ioresult.IOResult[int] {
return func() result.Result[int] {
if s == "" {
return result.Left[int](errors.New("empty string"))
}
return result.Of(len(s) * 10)
}
}
// Second transformation: int -> SimpleConfig (can fail)
loadConfig := func(id int) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if id < 0 {
return result.Left[SimpleConfig](errors.New("invalid ID"))
}
return result.Of(SimpleConfig{Port: 8000 + id})
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose transformations
step1 := LocalIOResultK[string](loadConfig)(formatConfig)
step2 := LocalIOResultK[string](parseID)(step1)
// Success case
res := step2("test")(ctx)()
assert.Equal(t, result.Of("Port: 8040"), res)
// Failure in first transformation
resErr1 := step2("")(ctx)()
assert.True(t, result.IsLeft(resErr1))
})
}
// TestLocalReaderIOEitherK tests LocalReaderIOEitherK functionality
func TestLocalReaderIOEitherK(t *testing.T) {
ctx := context.Background()
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
// ReaderIOResult effect that loads config from a path (can fail, uses context)
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
// Could use context here for cancellation, logging, etc.
return result.Of(SimpleConfig{Port: 8080})
}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalReaderIOEitherK
adapted := LocalReaderIOEitherK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("context propagation", func(t *testing.T) {
type ctxKey string
const key ctxKey = "test-key"
// ReaderIOResult that reads from context
loadFromContext := func(path string) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
if val := ctx.Value(key); val != nil {
return result.Of(val.(string))
}
return result.Left[string](errors.New("key not found in context"))
}
}
}
// ReaderReaderIOResult that uses the loaded value
useValue := func(val string) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of("Loaded: " + val)
}
}
}
adapted := LocalReaderIOEitherK[string](loadFromContext)(useValue)
// With context value
ctxWithValue := context.WithValue(ctx, key, "test-value")
res := adapted("ignored")(ctxWithValue)()
assert.Equal(t, result.Of("Loaded: test-value"), res)
// Without context value
resErr := adapted("ignored")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
}
// TestLocalReaderIOResultK tests LocalReaderIOResultK functionality
func TestLocalReaderIOResultK(t *testing.T) {
ctx := context.Background()
t.Run("basic ReaderIOResult transformation", func(t *testing.T) {
// ReaderIOResult effect that loads config from a path (can fail, uses context)
loadConfig := func(path string) readerioresult.ReaderIOResult[SimpleConfig] {
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
}
// ReaderReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
}
// Compose using LocalReaderIOResultK
adapted := LocalReaderIOResultK[string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")(ctx)()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")(ctx)()
assert.True(t, result.IsLeft(resErr))
})
t.Run("real-world: load and validate config with context", func(t *testing.T) {
type ConfigFile struct {
Path string
}
// Read file with context (can fail, uses context for cancellation)
readFile := func(cf ConfigFile) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
// Check context cancellation
select {
case <-ctx.Done():
return result.Left[string](ctx.Err())
default:
}
if cf.Path == "" {
return result.Left[string](errors.New("empty path"))
}
return result.Of(`{"port":9000}`)
}
}
}
// Parse config with context (can fail)
parseConfig := func(content string) readerioresult.ReaderIOResult[SimpleConfig] {
return func(ctx context.Context) ioresult.IOResult[SimpleConfig] {
return func() result.Result[SimpleConfig] {
if content == "" {
return result.Left[SimpleConfig](errors.New("empty content"))
}
return result.Of(SimpleConfig{Port: 9000})
}
}
}
// Use the config
useConfig := func(cfg SimpleConfig) readerioresult.ReaderIOResult[string] {
return func(ctx context.Context) ioresult.IOResult[string] {
return func() result.Result[string] {
return result.Of(fmt.Sprintf("Using port: %d", cfg.Port))
}
}
}
// Compose the pipeline
step1 := LocalReaderIOResultK[string](parseConfig)(useConfig)
step2 := LocalReaderIOResultK[string](readFile)(step1)
// Success case
res := step2(ConfigFile{Path: "app.json"})(ctx)()
assert.Equal(t, result.Of("Using port: 9000"), res)
// Failure case
resErr := step2(ConfigFile{Path: ""})(ctx)()
assert.True(t, result.IsLeft(resErr))
})
}

View File

@@ -37,6 +37,7 @@ import (
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/readeroption"
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
"github.com/IBM/fp-go/v2/result"
)
// FromReaderOption converts a ReaderOption to a ReaderReaderIOResult.
@@ -170,6 +171,15 @@ func ChainEitherK[R, A, B any](f either.Kleisli[error, A, B]) Operator[R, A, B]
)
}
//go:inline
func ChainResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] {
return fromeither.ChainEitherK(
Chain[R, A, B],
FromEither[R, B],
f,
)
}
// MonadChainFirstEitherK chains a computation that returns an Either but preserves the original value.
// Useful for validation or side effects that may fail.
// This is the monadic version that takes the computation as the first parameter.
@@ -837,14 +847,6 @@ func MapLeft[R, A any](f Endmorphism[error]) Operator[R, A, A] {
return RRIOE.MapLeft[R, context.Context, A](f)
}
// Local modifies the outer environment before passing it to a computation.
// Useful for providing different configurations to sub-computations.
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOResult[R1, A]) ReaderReaderIOResult[R2, A] {
return RRIOE.Local[context.Context, error, A](f)
}
// Read provides a specific outer environment value to a computation.
// Converts ReaderReaderIOResult[R, A] to ReaderIOResult[context.Context, A].
//
@@ -892,3 +894,8 @@ func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderReaderIOResult[R, A]
func Delay[R, A any](delay time.Duration) Operator[R, A, A] {
return reader.Map[R](RIOE.Delay[A](delay))
}
//go:inline
func Defer[R, A any](fa Lazy[ReaderReaderIOResult[R, A]]) ReaderReaderIOResult[R, A] {
return RRIOE.Defer(fa)
}

View File

@@ -0,0 +1,9 @@
package readerreaderioresult
import (
RRIOE "github.com/IBM/fp-go/v2/readerreaderioeither"
)
func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
return RRIOE.TraverseArray(f)
}

View File

@@ -30,8 +30,8 @@ import (
type (
// InjectableFactory is a factory function that can create an untyped instance of a service based on its [Dependency] identifier
InjectableFactory = func(Dependency) IOResult[any]
ProviderFactory = func(InjectableFactory) IOResult[any]
InjectableFactory = ReaderIOResult[Dependency, any]
ProviderFactory = ReaderIOResult[InjectableFactory, any]
paramIndex = map[int]int
paramValue = map[int]any

View File

@@ -4,6 +4,7 @@ import (
"github.com/IBM/fp-go/v2/iooption"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/record"
)
@@ -12,4 +13,5 @@ type (
IOResult[T any] = ioresult.IOResult[T]
IOOption[T any] = iooption.IOOption[T]
Entry[K comparable, V any] = record.Entry[K, V]
ReaderIOResult[R, T any] = readerioresult.ReaderIOResult[R, T]
)

264
v2/effect/bind.go Normal file
View File

@@ -0,0 +1,264 @@
// 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 effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func Do[C, S any](
empty S,
) Effect[C, S] {
return readerreaderioresult.Of[C](empty)
}
//go:inline
func Bind[C, S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[C, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.Bind(setter, f)
}
//go:inline
func Let[C, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[C, S1, S2] {
return readerreaderioresult.Let[C](setter, f)
}
//go:inline
func LetTo[C, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[C, S1, S2] {
return readerreaderioresult.LetTo[C](setter, b)
}
//go:inline
func BindTo[C, S1, T any](
setter func(T) S1,
) Operator[C, T, S1] {
return readerreaderioresult.BindTo[C](setter)
}
//go:inline
func ApS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa Effect[C, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApS[C](setter, fa)
}
//go:inline
func ApSL[C, S, T any](
lens Lens[S, T],
fa Effect[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApSL[C](lens, fa)
}
//go:inline
func BindL[C, S, T any](
lens Lens[S, T],
f func(T) Effect[C, T],
) Operator[C, S, S] {
return readerreaderioresult.BindL[C](lens, f)
}
//go:inline
func LetL[C, S, T any](
lens Lens[S, T],
f func(T) T,
) Operator[C, S, S] {
return readerreaderioresult.LetL[C](lens, f)
}
//go:inline
func LetToL[C, S, T any](
lens Lens[S, T],
b T,
) Operator[C, S, S] {
return readerreaderioresult.LetToL[C](lens, b)
}
//go:inline
func BindIOEitherK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f ioeither.Kleisli[error, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindIOEitherK[C](setter, f)
}
//go:inline
func BindIOResultK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f ioresult.Kleisli[S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindIOResultK[C](setter, f)
}
//go:inline
func BindIOK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f io.Kleisli[S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindIOK[C](setter, f)
}
//go:inline
func BindReaderK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[C, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindReaderK[C](setter, f)
}
//go:inline
func BindReaderIOK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f readerio.Kleisli[C, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindReaderIOK[C](setter, f)
}
//go:inline
func BindEitherK[C, S1, S2, T any](
setter func(T) func(S1) S2,
f either.Kleisli[error, S1, T],
) Operator[C, S1, S2] {
return readerreaderioresult.BindEitherK[C](setter, f)
}
//go:inline
func BindIOEitherKL[C, S, T any](
lens Lens[S, T],
f ioeither.Kleisli[error, T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindIOEitherKL[C](lens, f)
}
//go:inline
func BindIOKL[C, S, T any](
lens Lens[S, T],
f io.Kleisli[T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindIOKL[C](lens, f)
}
//go:inline
func BindReaderKL[C, S, T any](
lens Lens[S, T],
f reader.Kleisli[C, T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindReaderKL[C](lens, f)
}
//go:inline
func BindReaderIOKL[C, S, T any](
lens Lens[S, T],
f readerio.Kleisli[C, T, T],
) Operator[C, S, S] {
return readerreaderioresult.BindReaderIOKL[C](lens, f)
}
//go:inline
func ApIOEitherS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa IOEither[error, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApIOEitherS[C](setter, fa)
}
//go:inline
func ApIOS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa IO[T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApIOS[C](setter, fa)
}
//go:inline
func ApReaderS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[C, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApReaderS[C](setter, fa)
}
//go:inline
func ApReaderIOS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIO[C, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApReaderIOS[C](setter, fa)
}
//go:inline
func ApEitherS[C, S1, S2, T any](
setter func(T) func(S1) S2,
fa Either[error, T],
) Operator[C, S1, S2] {
return readerreaderioresult.ApEitherS[C](setter, fa)
}
//go:inline
func ApIOEitherSL[C, S, T any](
lens Lens[S, T],
fa IOEither[error, T],
) Operator[C, S, S] {
return readerreaderioresult.ApIOEitherSL[C](lens, fa)
}
//go:inline
func ApIOSL[C, S, T any](
lens Lens[S, T],
fa IO[T],
) Operator[C, S, S] {
return readerreaderioresult.ApIOSL[C](lens, fa)
}
//go:inline
func ApReaderSL[C, S, T any](
lens Lens[S, T],
fa Reader[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApReaderSL[C](lens, fa)
}
//go:inline
func ApReaderIOSL[C, S, T any](
lens Lens[S, T],
fa ReaderIO[C, T],
) Operator[C, S, S] {
return readerreaderioresult.ApReaderIOSL[C](lens, fa)
}
//go:inline
func ApEitherSL[C, S, T any](
lens Lens[S, T],
fa Either[error, T],
) Operator[C, S, S] {
return readerreaderioresult.ApEitherSL[C](lens, fa)
}

768
v2/effect/bind_test.go Normal file
View File

@@ -0,0 +1,768 @@
// 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 effect
import (
"errors"
"testing"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/stretchr/testify/assert"
)
type BindState struct {
Name string
Age int
Email string
}
func TestDo(t *testing.T) {
t.Run("creates effect with initial state", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 30}
eff := Do[TestContext](initial)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, initial, result)
})
t.Run("creates effect with empty struct", func(t *testing.T) {
type Empty struct{}
eff := Do[TestContext](Empty{})
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, Empty{}, result)
})
}
func TestBind(t *testing.T) {
t.Run("binds effect result to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := Bind[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext, int](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("chains multiple binds", func(t *testing.T) {
initial := BindState{}
eff := Bind[TestContext](
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
return s
}
},
func(s BindState) Effect[TestContext, string] {
return Of[TestContext, string]("alice@example.com")
},
)(Bind[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext, int](30)
},
)(Bind[TestContext](
func(name string) func(BindState) BindState {
return func(s BindState) BindState {
s.Name = name
return s
}
},
func(s BindState) Effect[TestContext, string] {
return Of[TestContext, string]("Alice")
},
)(Do[TestContext](initial))))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
assert.Equal(t, "alice@example.com", result.Email)
})
t.Run("propagates errors", func(t *testing.T) {
expectedErr := errors.New("bind error")
initial := BindState{Name: "Alice"}
eff := Bind[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
},
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestLet(t *testing.T) {
t.Run("computes value and binds to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := Let[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) int {
return len(s.Name) * 10
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 50, result.Age) // len("Alice") * 10
})
t.Run("chains with Bind", func(t *testing.T) {
initial := BindState{Name: "Bob"}
eff := Let[TestContext](
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
return s
}
},
func(s BindState) string {
return s.Name + "@example.com"
},
)(Bind[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) Effect[TestContext, int] {
return Of[TestContext, int](25)
},
)(Do[TestContext](initial)))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Bob", result.Name)
assert.Equal(t, 25, result.Age)
assert.Equal(t, "Bob@example.com", result.Email)
})
}
func TestLetTo(t *testing.T) {
t.Run("binds constant value to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := LetTo[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
42,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 42, result.Age)
})
t.Run("chains multiple LetTo", func(t *testing.T) {
initial := BindState{}
eff := LetTo[TestContext](
func(email string) func(BindState) BindState {
return func(s BindState) BindState {
s.Email = email
return s
}
},
"test@example.com",
)(LetTo[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
30,
)(LetTo[TestContext](
func(name string) func(BindState) BindState {
return func(s BindState) BindState {
s.Name = name
return s
}
},
"Alice",
)(Do[TestContext](initial))))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
assert.Equal(t, "test@example.com", result.Email)
})
}
func TestBindTo(t *testing.T) {
t.Run("wraps value in state", func(t *testing.T) {
type SimpleState struct {
Value int
}
eff := BindTo[TestContext](func(v int) SimpleState {
return SimpleState{Value: v}
})(Of[TestContext, int](42))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result.Value)
})
t.Run("starts a bind chain", func(t *testing.T) {
type State struct {
X int
Y string
}
eff := Let[TestContext](
func(y string) func(State) State {
return func(s State) State {
s.Y = y
return s
}
},
func(s State) string {
return "computed"
},
)(BindTo[TestContext](func(x int) State {
return State{X: x}
})(Of[TestContext, int](10)))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
assert.Equal(t, "computed", result.Y)
})
}
func TestApS(t *testing.T) {
t.Run("applies effect and binds result to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
ageEffect := Of[TestContext, int](30)
eff := ApS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
ageEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("propagates errors from applied effect", func(t *testing.T) {
expectedErr := errors.New("aps error")
initial := BindState{Name: "Alice"}
ageEffect := Fail[TestContext, int](expectedErr)
eff := ApS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
ageEffect,
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestBindIOK(t *testing.T) {
t.Run("binds IO operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindIOK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) io.IO[int] {
return func() int {
return 30
}
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindIOEitherK(t *testing.T) {
t.Run("binds successful IOEither to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindIOEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) ioeither.IOEither[error, int] {
return ioeither.Of[error, int](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("propagates IOEither error", func(t *testing.T) {
expectedErr := errors.New("ioeither error")
initial := BindState{Name: "Alice"}
eff := BindIOEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) ioeither.IOEither[error, int] {
return ioeither.Left[int, error](expectedErr)
},
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestBindIOResultK(t *testing.T) {
t.Run("binds successful IOResult to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindIOResultK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) ioresult.IOResult[int] {
return ioresult.Of[int](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindReaderK(t *testing.T) {
t.Run("binds Reader operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindReaderK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) reader.Reader[TestContext, int] {
return func(ctx TestContext) int {
return 30
}
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindReaderIOK(t *testing.T) {
t.Run("binds ReaderIO operation to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindReaderIOK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) readerio.ReaderIO[TestContext, int] {
return func(ctx TestContext) io.IO[int] {
return func() int {
return 30
}
}
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestBindEitherK(t *testing.T) {
t.Run("binds successful Either to state", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eff := BindEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) either.Either[error, int] {
return either.Of[error, int](30)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("propagates Either error", func(t *testing.T) {
expectedErr := errors.New("either error")
initial := BindState{Name: "Alice"}
eff := BindEitherK[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
func(s BindState) either.Either[error, int] {
return either.Left[int, error](expectedErr)
},
)(Do[TestContext](initial))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestLensOperations(t *testing.T) {
// Create lenses for BindState
nameLens := lens.MakeLens(
func(s BindState) string { return s.Name },
func(s BindState, name string) BindState {
s.Name = name
return s
},
)
ageLens := lens.MakeLens(
func(s BindState) int { return s.Age },
func(s BindState, age int) BindState {
s.Age = age
return s
},
)
t.Run("ApSL applies effect using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
ageEffect := Of[TestContext, int](30)
eff := ApSL[TestContext](ageLens, ageEffect)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("BindL binds effect using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := BindL[TestContext](
ageLens,
func(age int) Effect[TestContext, int] {
return Of[TestContext, int](age + 5)
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 30, result.Age)
})
t.Run("LetL computes value using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := LetL[TestContext](
ageLens,
func(age int) int {
return age * 2
},
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 50, result.Age)
})
t.Run("LetToL sets constant using lens", func(t *testing.T) {
initial := BindState{Name: "Alice", Age: 25}
eff := LetToL[TestContext](ageLens, 100)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 100, result.Age)
})
t.Run("chains lens operations", func(t *testing.T) {
initial := BindState{}
eff := LetToL[TestContext](
ageLens,
30,
)(LetToL[TestContext](
nameLens,
"Bob",
)(Do[TestContext](initial)))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Bob", result.Name)
assert.Equal(t, 30, result.Age)
})
}
func TestApOperations(t *testing.T) {
t.Run("ApIOS applies IO effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
ioEffect := func() int { return 30 }
eff := ApIOS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
ioEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Age)
})
t.Run("ApReaderS applies Reader effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
readerEffect := func(ctx TestContext) int { return 30 }
eff := ApReaderS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
readerEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Age)
})
t.Run("ApEitherS applies Either effect", func(t *testing.T) {
initial := BindState{Name: "Alice"}
eitherEffect := either.Of[error, int](30)
eff := ApEitherS[TestContext](
func(age int) func(BindState) BindState {
return func(s BindState) BindState {
s.Age = age
return s
}
},
eitherEffect,
)(Do[TestContext](initial))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Age)
})
}
func TestComplexBindChain(t *testing.T) {
t.Run("builds complex state with multiple operations", func(t *testing.T) {
type ComplexState struct {
Name string
Age int
Email string
IsAdmin bool
Score int
}
eff := LetTo[TestContext](
func(score int) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Score = score
return s
}
},
100,
)(Let[TestContext](
func(isAdmin bool) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.IsAdmin = isAdmin
return s
}
},
func(s ComplexState) bool {
return s.Age >= 18
},
)(Let[TestContext](
func(email string) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Email = email
return s
}
},
func(s ComplexState) string {
return s.Name + "@example.com"
},
)(Bind[TestContext](
func(age int) func(ComplexState) ComplexState {
return func(s ComplexState) ComplexState {
s.Age = age
return s
}
},
func(s ComplexState) Effect[TestContext, int] {
return Of[TestContext, int](25)
},
)(BindTo[TestContext](func(name string) ComplexState {
return ComplexState{Name: name}
})(Of[TestContext, string]("Alice"))))))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Alice", result.Name)
assert.Equal(t, 25, result.Age)
assert.Equal(t, "Alice@example.com", result.Email)
assert.True(t, result.IsAdmin)
assert.Equal(t, 100, result.Score)
})
}

110
v2/effect/dependencies.go Normal file
View File

@@ -0,0 +1,110 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/result"
)
//go:inline
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
//go:inline
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
//go:inline
func LocalIOK[A, C1, C2 any](f io.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOK[A](f)
}
//go:inline
func LocalIOResultK[A, C1, C2 any](f ioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOResultK[A](f)
}
//go:inline
func LocalResultK[A, C1, C2 any](f result.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalResultK[A](f)
}
//go:inline
func LocalThunkK[A, C1, C2 any](f readerioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderIOResultK[A](f)
}
// LocalEffectK transforms the context of an Effect using an Effect-returning function.
// This is the most powerful context transformation function, allowing the transformation
// itself to be effectful (can fail, perform I/O, and access the outer context).
//
// LocalEffectK takes a Kleisli arrow that:
// - Accepts the outer context C2
// - Returns an Effect that produces the inner context C1
// - Can fail with an error during context transformation
// - Can perform I/O operations during transformation
//
// This is useful when:
// - Context transformation requires I/O (e.g., loading config from a file)
// - Context transformation can fail (e.g., validating or parsing context)
// - Context transformation needs to access the outer context
//
// Type Parameters:
// - A: The value type produced by the effect
// - C1: The inner context type (required by the original effect)
// - C2: The outer context type (provided to the transformed effect)
//
// Parameters:
// - f: A Kleisli arrow (C2 -> Effect[C2, C1]) that transforms C2 to C1 effectfully
//
// Returns:
// - A function that transforms Effect[C1, A] to Effect[C2, A]
//
// Example:
//
// type DatabaseConfig struct {
// ConnectionString string
// }
//
// type AppConfig struct {
// ConfigPath string
// }
//
// // Effect that needs DatabaseConfig
// dbEffect := effect.Of[DatabaseConfig, string]("query result")
//
// // Transform AppConfig to DatabaseConfig effectfully
// // (e.g., load config from file, which can fail)
// loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
// return effect.Chain[AppConfig](func(_ AppConfig) Effect[AppConfig, DatabaseConfig] {
// // Simulate loading config from file (can fail)
// return effect.Of[AppConfig, DatabaseConfig](DatabaseConfig{
// ConnectionString: "loaded from " + app.ConfigPath,
// })
// })(effect.Of[AppConfig, AppConfig](app))
// }
//
// // Apply the transformation
// transform := effect.LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
// appEffect := transform(dbEffect)
//
// // Run with AppConfig
// ioResult := effect.Provide(AppConfig{ConfigPath: "/etc/app.conf"})(appEffect)
// readerResult := effect.RunSync(ioResult)
// result, err := readerResult(context.Background())
//
// Comparison with other Local functions:
// - Local/Contramap: Pure context transformation (C2 -> C1)
// - LocalIOK: IO-based transformation (C2 -> IO[C1])
// - LocalIOResultK: IO with error handling (C2 -> IOResult[C1])
// - LocalReaderIOResultK: Reader-based with IO and errors (C2 -> ReaderIOResult[C1])
// - LocalEffectK: Full Effect transformation (C2 -> Effect[C2, C1])
//
//go:inline
func LocalEffectK[A, C1, C2 any](f Kleisli[C2, C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderReaderIOEitherK[A](f)
}

View File

@@ -0,0 +1,620 @@
// 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 effect
import (
"context"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/stretchr/testify/assert"
)
type OuterContext struct {
Value string
Number int
}
type InnerContext struct {
Value string
}
func TestLocal(t *testing.T) {
t.Run("transforms context for inner effect", func(t *testing.T) {
// Create an effect that uses InnerContext
innerEffect := Of[InnerContext, string]("result")
// Transform OuterContext to InnerContext
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
// Apply Local to transform the context
kleisli := Local[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("allows accessing outer context fields", func(t *testing.T) {
// Create an effect that reads from InnerContext
innerEffect := Chain[InnerContext](func(_ string) Effect[InnerContext, string] {
return Of[InnerContext, string]("inner value")
})(Of[InnerContext, string]("start"))
// Transform context
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value + " transformed"}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
Value: "original",
Number: 100,
})(outerEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner value", result)
})
t.Run("propagates errors from inner effect", func(t *testing.T) {
expectedErr := assert.AnError
innerEffect := Fail[InnerContext, string](expectedErr)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple Local transformations", func(t *testing.T) {
type Level1 struct {
A string
}
type Level2 struct {
B string
}
type Level3 struct {
C string
}
// Effect at deepest level
level3Effect := Of[Level3, string]("deep result")
// Transform Level2 -> Level3
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
return Level3{C: l2.B + "-c"}
})
// Transform Level1 -> Level2
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
return Level2{B: l1.A + "-b"}
})
// Compose transformations
level2Effect := local23(level3Effect)
level1Effect := local12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "deep result", result)
})
t.Run("works with complex context transformations", func(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
Database string
}
type AppConfig struct {
DB DatabaseConfig
APIKey string
Timeout int
}
// Effect that needs only DatabaseConfig
dbEffect := Of[DatabaseConfig, string]("connected")
// Extract DB config from AppConfig
accessor := func(app AppConfig) DatabaseConfig {
return app.DB
}
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
appEffect := kleisli(dbEffect)
// Run with full AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
DB: DatabaseConfig{
Host: "localhost",
Port: 5432,
Database: "mydb",
},
APIKey: "secret",
Timeout: 30,
})(appEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "connected", result)
})
}
func TestContramap(t *testing.T) {
t.Run("is equivalent to Local", func(t *testing.T) {
innerEffect := Of[InnerContext, int](42)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
// Test Local
localKleisli := Local[OuterContext, InnerContext, int](accessor)
localEffect := localKleisli(innerEffect)
// Test Contramap
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
contramapEffect := contramapKleisli(innerEffect)
outerCtx := OuterContext{Value: "test", Number: 100}
// Run both
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
localReader := RunSync[int](localIO)
localResult, localErr := localReader(context.Background())
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
contramapReader := RunSync[int](contramapIO)
contramapResult, contramapErr := contramapReader(context.Background())
assert.NoError(t, localErr)
assert.NoError(t, contramapErr)
assert.Equal(t, localResult, contramapResult)
})
t.Run("transforms context correctly", func(t *testing.T) {
innerEffect := Of[InnerContext, string]("success")
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value + " modified"}
}
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
Value: "original",
Number: 50,
})(outerEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "success", result)
})
t.Run("handles errors from inner effect", func(t *testing.T) {
expectedErr := assert.AnError
innerEffect := Fail[InnerContext, int](expectedErr)
accessor := func(outer OuterContext) InnerContext {
return InnerContext{Value: outer.Value}
}
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, int](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
readerResult := RunSync[int](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestLocalAndContramapInteroperability(t *testing.T) {
t.Run("can be used interchangeably", func(t *testing.T) {
type Config1 struct {
Value string
}
type Config2 struct {
Data string
}
type Config3 struct {
Info string
}
// Effect at deepest level
effect3 := Of[Config3, string]("result")
// Use Local for first transformation
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
return Config3{Info: c2.Data}
})
// Use Contramap for second transformation
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
return Config2{Data: c1.Value}
})
// Compose them
effect2 := local23(effect3)
effect1 := contramap12(effect2)
// Run
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
}
func TestLocalEffectK(t *testing.T) {
t.Run("transforms context using effectful function", func(t *testing.T) {
type DatabaseConfig struct {
ConnectionString string
}
type AppConfig struct {
ConfigPath string
}
// Effect that needs DatabaseConfig
dbEffect := Of[DatabaseConfig, string]("query result")
// Transform AppConfig to DatabaseConfig effectfully
loadConfig := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
ConnectionString: "loaded from " + app.ConfigPath,
})
}
// Apply the transformation
transform := LocalEffectK[string, DatabaseConfig, AppConfig](loadConfig)
appEffect := transform(dbEffect)
// Run with AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ConfigPath: "/etc/app.conf",
})(appEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "query result", result)
})
t.Run("propagates errors from context transformation", func(t *testing.T) {
type InnerCtx struct {
Value string
}
type OuterCtx struct {
Path string
}
innerEffect := Of[InnerCtx, string]("success")
expectedErr := assert.AnError
// Context transformation that fails
failingTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Fail[OuterCtx, InnerCtx](expectedErr)
}
transform := LocalEffectK[string, InnerCtx, OuterCtx](failingTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates errors from inner effect", func(t *testing.T) {
type InnerCtx struct {
Value string
}
type OuterCtx struct {
Path string
}
expectedErr := assert.AnError
innerEffect := Fail[InnerCtx, string](expectedErr)
// Successful context transformation
transform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Of[OuterCtx, InnerCtx](InnerCtx{Value: outer.Path})
}
transformK := LocalEffectK[string, InnerCtx, OuterCtx](transform)
outerEffect := transformK(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("allows effectful context transformation with IO operations", func(t *testing.T) {
type Config struct {
Data string
}
type AppContext struct {
ConfigFile string
}
// Effect that uses Config
configEffect := Chain[Config](func(cfg Config) Effect[Config, string] {
return Of[Config, string]("processed: " + cfg.Data)
})(readerreaderioresult.Ask[Config]())
// Effectful transformation that simulates loading config
loadConfigEffect := func(app AppContext) Effect[AppContext, Config] {
// Simulate IO operation (e.g., reading file)
return Of[AppContext, Config](Config{
Data: "loaded from " + app.ConfigFile,
})
}
transform := LocalEffectK[string, Config, AppContext](loadConfigEffect)
appEffect := transform(configEffect)
ioResult := Provide[AppContext, string](AppContext{
ConfigFile: "config.json",
})(appEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "processed: loaded from config.json", result)
})
t.Run("chains multiple LocalEffectK transformations", func(t *testing.T) {
type Level1 struct {
A string
}
type Level2 struct {
B string
}
type Level3 struct {
C string
}
// Effect at deepest level
level3Effect := Of[Level3, string]("deep result")
// Transform Level2 -> Level3 effectfully
transform23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2, Level3](Level3{C: l2.B + "-c"})
})
// Transform Level1 -> Level2 effectfully
transform12 := LocalEffectK[string, Level2, Level1](func(l1 Level1) Effect[Level1, Level2] {
return Of[Level1, Level2](Level2{B: l1.A + "-b"})
})
// Compose transformations
level2Effect := transform23(level3Effect)
level1Effect := transform12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "deep result", result)
})
t.Run("accesses outer context during transformation", func(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
}
type AppConfig struct {
Environment string
DBHost string
DBPort int
}
// Effect that needs DatabaseConfig
dbEffect := Chain[DatabaseConfig](func(cfg DatabaseConfig) Effect[DatabaseConfig, string] {
return Of[DatabaseConfig, string](fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
})(readerreaderioresult.Ask[DatabaseConfig]())
// Transform using outer context
transformWithContext := func(app AppConfig) Effect[AppConfig, DatabaseConfig] {
// Access outer context to build inner context
prefix := ""
if app.Environment == "prod" {
prefix = "prod-"
}
return Of[AppConfig, DatabaseConfig](DatabaseConfig{
Host: prefix + app.DBHost,
Port: app.DBPort,
})
}
transform := LocalEffectK[string, DatabaseConfig, AppConfig](transformWithContext)
appEffect := transform(dbEffect)
ioResult := Provide[AppConfig, string](AppConfig{
Environment: "prod",
DBHost: "localhost",
DBPort: 5432,
})(appEffect)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Contains(t, result, "prod-localhost")
})
t.Run("validates context during transformation", func(t *testing.T) {
type ValidatedConfig struct {
APIKey string
}
type RawConfig struct {
APIKey string
}
innerEffect := Of[ValidatedConfig, string]("success")
// Validation that can fail
validateConfig := func(raw RawConfig) Effect[RawConfig, ValidatedConfig] {
if raw.APIKey == "" {
return Fail[RawConfig, ValidatedConfig](assert.AnError)
}
return Of[RawConfig, ValidatedConfig](ValidatedConfig{
APIKey: raw.APIKey,
})
}
transform := LocalEffectK[string, ValidatedConfig, RawConfig](validateConfig)
outerEffect := transform(innerEffect)
// Test with invalid config
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
// Test with valid config
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
readerResult2 := RunSync[string](ioResult2)
result, err2 := readerResult2(context.Background())
assert.NoError(t, err2)
assert.Equal(t, "success", result)
})
t.Run("composes with other Local functions", func(t *testing.T) {
type Level1 struct {
Value string
}
type Level2 struct {
Data string
}
type Level3 struct {
Info string
}
// Effect at deepest level
effect3 := Of[Level3, string]("result")
// Use LocalEffectK for first transformation (effectful)
localEffectK23 := LocalEffectK[string, Level3, Level2](func(l2 Level2) Effect[Level2, Level3] {
return Of[Level2, Level3](Level3{Info: l2.Data})
})
// Use Local for second transformation (pure)
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
return Level2{Data: l1.Value}
})
// Compose them
effect2 := localEffectK23(effect3)
effect1 := local12(effect2)
// Run
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("handles complex nested effects in transformation", func(t *testing.T) {
type InnerCtx struct {
Value int
}
type OuterCtx struct {
Multiplier int
}
// Effect that uses InnerCtx
innerEffect := Chain[InnerCtx](func(ctx InnerCtx) Effect[InnerCtx, int] {
return Of[InnerCtx, int](ctx.Value * 2)
})(readerreaderioresult.Ask[InnerCtx]())
// Complex transformation with nested effects
complexTransform := func(outer OuterCtx) Effect[OuterCtx, InnerCtx] {
return Of[OuterCtx, InnerCtx](InnerCtx{
Value: outer.Multiplier * 10,
})
}
transform := LocalEffectK[int, InnerCtx, OuterCtx](complexTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
readerResult := RunSync[int](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 60, result) // 3 * 10 * 2
})
}

222
v2/effect/doc.go Normal file
View File

@@ -0,0 +1,222 @@
// 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 effect provides a functional effect system for managing side effects in Go.
# Overview
The effect package is a high-level abstraction for composing effectful computations
that may fail, require dependencies (context), and perform I/O operations. It is built
on top of ReaderReaderIOResult, providing a clean API for dependency injection and
error handling.
# Naming Conventions
The naming conventions in this package are modeled after effect-ts (https://effect.website/),
a popular TypeScript library for functional effect systems. This alignment helps developers
familiar with effect-ts to quickly understand and use this Go implementation.
# Core Type
The central type is Effect[C, A], which represents:
- C: The context/dependency type required by the effect
- A: The success value type produced by the effect
An Effect can:
- Succeed with a value of type A
- Fail with an error
- Require a context of type C
- Perform I/O operations
# Basic Operations
Creating Effects:
// Create a successful effect
effect.Succeed[MyContext, string]("hello")
// Create a failed effect
effect.Fail[MyContext, string](errors.New("failed"))
// Lift a pure value into an effect
effect.Of[MyContext, int](42)
Transforming Effects:
// Map over the success value
effect.Map[MyContext](func(x int) string {
return strconv.Itoa(x)
})
// Chain effects together (flatMap)
effect.Chain[MyContext](func(x int) Effect[MyContext, string] {
return effect.Succeed[MyContext, string](strconv.Itoa(x))
})
// Tap into an effect without changing its value
effect.Tap[MyContext](func(x int) Effect[MyContext, any] {
return effect.Succeed[MyContext, any](fmt.Println(x))
})
# Dependency Injection
Effects can access their required context:
// Transform the context before passing it to an effect
effect.Local[OuterCtx, InnerCtx](func(outer OuterCtx) InnerCtx {
return outer.Inner
})
// Provide a context to run an effect
effect.Provide[MyContext, string](myContext)
# Do Notation
The package provides "do notation" for composing effects in a sequential, imperative style:
type State struct {
X int
Y string
}
result := effect.Do[MyContext](State{}).
Bind(func(y string) func(State) State {
return func(s State) State {
s.Y = y
return s
}
}, fetchString).
Let(func(x int) func(State) State {
return func(s State) State {
s.X = x
return s
}
}, func(s State) int {
return len(s.Y)
})
# Bind Operations
The package provides various bind operations for integrating with other effect types:
- BindIOK: Bind an IO operation
- BindIOEitherK: Bind an IOEither operation
- BindIOResultK: Bind an IOResult operation
- BindReaderK: Bind a Reader operation
- BindReaderIOK: Bind a ReaderIO operation
- BindEitherK: Bind an Either operation
Each bind operation has a corresponding "L" variant for working with lenses:
- BindL, BindIOKL, BindReaderKL, etc.
# Applicative Operations
Apply effects in parallel:
// Apply a function effect to a value effect
effect.Ap[string, MyContext](valueEffect)(functionEffect)
// Apply effects to build up a structure
effect.ApS[MyContext](setter, effect1)
# Traversal
Traverse collections with effects:
// Map an array with an effectful function
effect.TraverseArray[MyContext](func(x int) Effect[MyContext, string] {
return effect.Succeed[MyContext, string](strconv.Itoa(x))
})
# Retry Logic
Retry effects with configurable policies:
effect.Retrying[MyContext, string](
retryPolicy,
func(status retry.RetryStatus) Effect[MyContext, string] {
return fetchData()
},
func(result Result[string]) bool {
return result.IsLeft() // retry on error
},
)
# Monoids
Combine effects using monoid operations:
// Combine effects using applicative semantics
effect.ApplicativeMonoid[MyContext](stringMonoid)
// Combine effects using alternative semantics (first success)
effect.AlternativeMonoid[MyContext](stringMonoid)
# Running Effects
To execute an effect:
// Provide the context
ioResult := effect.Provide[MyContext, string](myContext)(myEffect)
// Run synchronously
readerResult := effect.RunSync(ioResult)
// Execute with a context.Context
value, err := readerResult(ctx)
# Integration with Other Packages
The effect package integrates seamlessly with other fp-go packages:
- either: For error handling
- io: For I/O operations
- reader: For dependency injection
- result: For result types
- retry: For retry logic
- monoid: For combining effects
# Example
type Config struct {
APIKey string
BaseURL string
}
func fetchUser(id int) Effect[Config, User] {
return effect.Chain[Config](func(cfg Config) Effect[Config, User] {
// Use cfg.APIKey and cfg.BaseURL
return effect.Succeed[Config, User](User{ID: id})
})(effect.Of[Config, Config](Config{}))
}
func main() {
cfg := Config{APIKey: "key", BaseURL: "https://api.example.com"}
userEffect := fetchUser(42)
// Run the effect
ioResult := effect.Provide(cfg)(userEffect)
readerResult := effect.RunSync(ioResult)
user, err := readerResult(context.Background())
if err != nil {
log.Fatal(err)
}
fmt.Printf("User: %+v\n", user)
}
*/
package effect
//go:generate go run ../main.go lens --dir . --filename gen_lens.go --include-test-files

51
v2/effect/effect.go Normal file
View File

@@ -0,0 +1,51 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
func Succeed[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
func Fail[C, A any](err error) Effect[C, A] {
return readerreaderioresult.Left[C, A](err)
}
func Of[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
return readerreaderioresult.Map[C](f)
}
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.Chain(f)
}
func Ap[B, C, A any](fa Effect[C, A]) Operator[C, func(A) B, B] {
return readerreaderioresult.Ap[B](fa)
}
func Suspend[C, A any](fa Lazy[Effect[C, A]]) Effect[C, A] {
return readerreaderioresult.Defer(fa)
}
func Tap[C, A, ANY any](f Kleisli[C, A, ANY]) Operator[C, A, A] {
return readerreaderioresult.Tap(f)
}
func Ternary[C, A, B any](pred Predicate[A], onTrue, onFalse Kleisli[C, A, B]) Kleisli[C, A, B] {
return function.Ternary(pred, onTrue, onFalse)
}
func ChainResultK[C, A, B any](f result.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainResultK[C](f)
}
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
return readerreaderioresult.Read[A](c)
}

506
v2/effect/effect_test.go Normal file
View File

@@ -0,0 +1,506 @@
// 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 effect
import (
"context"
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
type TestContext struct {
Value string
}
func runEffect[A any](eff Effect[TestContext, A], ctx TestContext) (A, error) {
ioResult := Provide[TestContext, A](ctx)(eff)
readerResult := RunSync[A](ioResult)
return readerResult(context.Background())
}
func TestSucceed(t *testing.T) {
t.Run("creates successful effect with value", func(t *testing.T) {
eff := Succeed[TestContext, int](42)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("creates successful effect with string", func(t *testing.T) {
eff := Succeed[TestContext, string]("hello")
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "hello", result)
})
t.Run("creates successful effect with struct", func(t *testing.T) {
type User struct {
Name string
Age int
}
user := User{Name: "Alice", Age: 30}
eff := Succeed[TestContext, User](user)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, user, result)
})
}
func TestFail(t *testing.T) {
t.Run("creates failed effect with error", func(t *testing.T) {
expectedErr := errors.New("test error")
eff := Fail[TestContext, int](expectedErr)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("creates failed effect with custom error", func(t *testing.T) {
expectedErr := fmt.Errorf("custom error: %s", "details")
eff := Fail[TestContext, string](expectedErr)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestOf(t *testing.T) {
t.Run("lifts value into effect", func(t *testing.T) {
eff := Of[TestContext, int](100)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 100, result)
})
t.Run("is equivalent to Succeed", func(t *testing.T) {
value := "test value"
eff1 := Of[TestContext, string](value)
eff2 := Succeed[TestContext, string](value)
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
result2, err2 := runEffect(eff2, TestContext{Value: "test"})
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, result1, result2)
})
}
func TestMap(t *testing.T) {
t.Run("maps over successful effect", func(t *testing.T) {
eff := Of[TestContext, int](10)
mapped := Map[TestContext](func(x int) int {
return x * 2
})(eff)
result, err := runEffect(mapped, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
t.Run("maps to different type", func(t *testing.T) {
eff := Of[TestContext, int](42)
mapped := Map[TestContext](func(x int) string {
return fmt.Sprintf("value: %d", x)
})(eff)
result, err := runEffect(mapped, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "value: 42", result)
})
t.Run("preserves error in failed effect", func(t *testing.T) {
expectedErr := errors.New("original error")
eff := Fail[TestContext, int](expectedErr)
mapped := Map[TestContext](func(x int) int {
return x * 2
})(eff)
_, err := runEffect(mapped, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple maps", func(t *testing.T) {
eff := Of[TestContext, int](5)
result := Map[TestContext](func(x int) int {
return x + 1
})(Map[TestContext](func(x int) int {
return x * 2
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 11, value) // (5 * 2) + 1
})
}
func TestChain(t *testing.T) {
t.Run("chains successful effects", func(t *testing.T) {
eff := Of[TestContext, int](10)
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, result)
})
t.Run("chains to different type", func(t *testing.T) {
eff := Of[TestContext, int](42)
chained := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](fmt.Sprintf("number: %d", x))
})(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "number: 42", result)
})
t.Run("propagates first error", func(t *testing.T) {
expectedErr := errors.New("first error")
eff := Fail[TestContext, int](expectedErr)
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedErr := errors.New("second error")
eff := Of[TestContext, int](10)
chained := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
})(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple operations", func(t *testing.T) {
eff := Of[TestContext, int](5)
result := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x + 10)
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, value) // (5 * 2) + 10
})
}
func TestAp(t *testing.T) {
t.Run("applies function effect to value effect", func(t *testing.T) {
fn := Of[TestContext, func(int) int](func(x int) int {
return x * 2
})
value := Of[TestContext, int](21)
result := Ap[int](value)(fn)
val, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, val)
})
t.Run("applies function to different type", func(t *testing.T) {
fn := Of[TestContext, func(int) string](func(x int) string {
return fmt.Sprintf("value: %d", x)
})
value := Of[TestContext, int](42)
result := Ap[string](value)(fn)
val, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "value: 42", val)
})
t.Run("propagates error from function effect", func(t *testing.T) {
expectedErr := errors.New("function error")
fn := Fail[TestContext, func(int) int](expectedErr)
value := Of[TestContext, int](42)
result := Ap[int](value)(fn)
_, err := runEffect(result, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates error from value effect", func(t *testing.T) {
expectedErr := errors.New("value error")
fn := Of[TestContext, func(int) int](func(x int) int {
return x * 2
})
value := Fail[TestContext, int](expectedErr)
result := Ap[int](value)(fn)
_, err := runEffect(result, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestSuspend(t *testing.T) {
t.Run("suspends effect computation", func(t *testing.T) {
callCount := 0
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
callCount++
return Of[TestContext, int](42)
})
// Effect not executed yet
assert.Equal(t, 0, callCount)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
assert.Equal(t, 1, callCount)
})
t.Run("suspends failing effect", func(t *testing.T) {
expectedErr := errors.New("suspended error")
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
return Fail[TestContext, int](expectedErr)
})
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("allows lazy evaluation", func(t *testing.T) {
var value int
eff := Suspend[TestContext, int](func() Effect[TestContext, int] {
return Of[TestContext, int](value)
})
value = 10
result1, err1 := runEffect(eff, TestContext{Value: "test"})
value = 20
result2, err2 := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, 10, result1)
assert.Equal(t, 20, result2)
})
}
func TestTap(t *testing.T) {
t.Run("executes side effect without changing value", func(t *testing.T) {
sideEffectValue := 0
eff := Of[TestContext, int](42)
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
sideEffectValue = x * 2
return Of[TestContext, any](nil)
})(eff)
result, err := runEffect(tapped, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
assert.Equal(t, 84, sideEffectValue)
})
t.Run("propagates original error", func(t *testing.T) {
expectedErr := errors.New("original error")
eff := Fail[TestContext, int](expectedErr)
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
return Of[TestContext, any](nil)
})(eff)
_, err := runEffect(tapped, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates tap error", func(t *testing.T) {
expectedErr := errors.New("tap error")
eff := Of[TestContext, int](42)
tapped := Tap[TestContext](func(x int) Effect[TestContext, any] {
return Fail[TestContext, any](expectedErr)
})(eff)
_, err := runEffect(tapped, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple taps", func(t *testing.T) {
values := []int{}
eff := Of[TestContext, int](10)
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
values = append(values, x+2)
return Of[TestContext, any](nil)
})(Tap[TestContext](func(x int) Effect[TestContext, any] {
values = append(values, x+1)
return Of[TestContext, any](nil)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 10, value)
assert.Equal(t, []int{11, 12}, values)
})
}
func TestTernary(t *testing.T) {
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
kleisli := Ternary[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("less or equal")
},
)
result, err := runEffect(kleisli(15), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "greater", result)
})
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
kleisli := Ternary[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("less or equal")
},
)
result, err := runEffect(kleisli(5), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "less or equal", result)
})
t.Run("handles errors in onTrue branch", func(t *testing.T) {
expectedErr := errors.New("true branch error")
kleisli := Ternary[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Fail[TestContext, string](expectedErr)
},
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("less or equal")
},
)
_, err := runEffect(kleisli(15), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("handles errors in onFalse branch", func(t *testing.T) {
expectedErr := errors.New("false branch error")
kleisli := Ternary[TestContext, int, string](
func(x int) bool { return x > 10 },
func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("greater")
},
func(x int) Effect[TestContext, string] {
return Fail[TestContext, string](expectedErr)
},
)
_, err := runEffect(kleisli(5), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestEffectComposition(t *testing.T) {
t.Run("composes Map and Chain", func(t *testing.T) {
eff := Of[TestContext, int](5)
result := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](fmt.Sprintf("result: %d", x))
})(Map[TestContext](func(x int) int {
return x * 2
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "result: 10", value)
})
t.Run("composes Chain and Tap", func(t *testing.T) {
sideEffect := 0
eff := Of[TestContext, int](10)
result := Tap[TestContext](func(x int) Effect[TestContext, any] {
sideEffect = x
return Of[TestContext, any](nil)
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(eff))
value, err := runEffect(result, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 20, value)
assert.Equal(t, 20, sideEffect)
})
}
func TestEffectWithResult(t *testing.T) {
t.Run("converts result to effect", func(t *testing.T) {
res := result.Of[int](42)
// This demonstrates integration with result package
assert.True(t, result.IsRight(res))
})
}

118
v2/effect/gen_lens_test.go Normal file
View File

@@ -0,0 +1,118 @@
package effect
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2026-01-27 22:19:41.6840253 +0100 CET m=+0.008579701
import (
__lens "github.com/IBM/fp-go/v2/optics/lens"
__option "github.com/IBM/fp-go/v2/option"
__prism "github.com/IBM/fp-go/v2/optics/prism"
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
)
// ComplexServiceLenses provides lenses for accessing fields of ComplexService
type ComplexServiceLenses struct {
// mandatory fields
service1 __lens.Lens[ComplexService, Service1]
service2 __lens.Lens[ComplexService, Service2]
// optional fields
service1O __lens_option.LensO[ComplexService, Service1]
service2O __lens_option.LensO[ComplexService, Service2]
}
// ComplexServiceRefLenses provides lenses for accessing fields of ComplexService via a reference to ComplexService
type ComplexServiceRefLenses struct {
// mandatory fields
service1 __lens.Lens[*ComplexService, Service1]
service2 __lens.Lens[*ComplexService, Service2]
// optional fields
service1O __lens_option.LensO[*ComplexService, Service1]
service2O __lens_option.LensO[*ComplexService, Service2]
// prisms
service1P __prism.Prism[*ComplexService, Service1]
service2P __prism.Prism[*ComplexService, Service2]
}
// ComplexServicePrisms provides prisms for accessing fields of ComplexService
type ComplexServicePrisms struct {
service1 __prism.Prism[ComplexService, Service1]
service2 __prism.Prism[ComplexService, Service2]
}
// MakeComplexServiceLenses creates a new ComplexServiceLenses with lenses for all fields
func MakeComplexServiceLenses() ComplexServiceLenses {
// mandatory lenses
lensservice1 := __lens.MakeLensWithName(
func(s ComplexService) Service1 { return s.service1 },
func(s ComplexService, v Service1) ComplexService { s.service1 = v; return s },
"ComplexService.service1",
)
lensservice2 := __lens.MakeLensWithName(
func(s ComplexService) Service2 { return s.service2 },
func(s ComplexService, v Service2) ComplexService { s.service2 = v; return s },
"ComplexService.service2",
)
// optional lenses
lensservice1O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
lensservice2O := __lens_option.FromIso[ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
return ComplexServiceLenses{
// mandatory lenses
service1: lensservice1,
service2: lensservice2,
// optional lenses
service1O: lensservice1O,
service2O: lensservice2O,
}
}
// MakeComplexServiceRefLenses creates a new ComplexServiceRefLenses with lenses for all fields
func MakeComplexServiceRefLenses() ComplexServiceRefLenses {
// mandatory lenses
lensservice1 := __lens.MakeLensStrictWithName(
func(s *ComplexService) Service1 { return s.service1 },
func(s *ComplexService, v Service1) *ComplexService { s.service1 = v; return s },
"(*ComplexService).service1",
)
lensservice2 := __lens.MakeLensStrictWithName(
func(s *ComplexService) Service2 { return s.service2 },
func(s *ComplexService, v Service2) *ComplexService { s.service2 = v; return s },
"(*ComplexService).service2",
)
// optional lenses
lensservice1O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service1]())(lensservice1)
lensservice2O := __lens_option.FromIso[*ComplexService](__iso_option.FromZero[Service2]())(lensservice2)
return ComplexServiceRefLenses{
// mandatory lenses
service1: lensservice1,
service2: lensservice2,
// optional lenses
service1O: lensservice1O,
service2O: lensservice2O,
}
}
// MakeComplexServicePrisms creates a new ComplexServicePrisms with prisms for all fields
func MakeComplexServicePrisms() ComplexServicePrisms {
_fromNonZeroservice1 := __option.FromNonZero[Service1]()
_prismservice1 := __prism.MakePrismWithName(
func(s ComplexService) __option.Option[Service1] { return _fromNonZeroservice1(s.service1) },
func(v Service1) ComplexService {
return ComplexService{ service1: v }
},
"ComplexService.service1",
)
_fromNonZeroservice2 := __option.FromNonZero[Service2]()
_prismservice2 := __prism.MakePrismWithName(
func(s ComplexService) __option.Option[Service2] { return _fromNonZeroservice2(s.service2) },
func(v Service2) ComplexService {
return ComplexService{ service2: v }
},
"ComplexService.service2",
)
return ComplexServicePrisms {
service1: _prismservice1,
service2: _prismservice2,
}
}

207
v2/effect/injection_test.go Normal file
View File

@@ -0,0 +1,207 @@
// Package effect demonstrates dependency injection using the Effect pattern.
//
// This test file shows how to build a type-safe dependency injection system where:
// - An InjectionContainer can resolve services by ID (InjectionToken)
// - Services are generic effects that depend on the container
// - Lookup methods convert from untyped container to typed dependencies
// - Handler functions depend type-safely on specific service interfaces
package effect
import (
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/context/readerioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/result"
)
type (
// InjectionToken is a unique identifier for services in the container
InjectionToken string
// InjectionContainer is an Effect that resolves services by their token.
// It takes an InjectionToken and returns a Thunk that produces any type.
// This allows the container to store and retrieve services of different types.
InjectionContainer = Effect[InjectionToken, any]
// Service is a generic Effect that depends on the InjectionContainer.
// It represents a computation that needs access to the dependency injection
// container to resolve its dependencies before producing a string result.
Service[T any] = Effect[InjectionContainer, T]
// Service1 is an example service interface that can be resolved from the container
Service1 interface {
GetService1() string
}
// Service2 is another example service interface
Service2 interface {
GetService2() string
}
// impl1 is a concrete implementation of Service1
impl1 struct{}
// impl2 is a concrete implementation of Service2
impl2 struct{}
)
// ComplexService demonstrates a more complex dependency injection scenario
// where a service depends on multiple other services. This struct aggregates
// Service1 and Service2, showing how to compose dependencies.
// The fp-go:Lens directive generates lens functions for type-safe field access.
//
// fp-go:Lens
type ComplexService struct {
service1 Service1
service2 Service2
}
func (_ *impl1) GetService1() string {
return "service1"
}
func (_ *impl2) GetService2() string {
return "service2"
}
const (
// service1 is the injection token for Service1
service1 = InjectionToken("service1")
// service2 is the injection token for Service2
service2 = InjectionToken("service2")
)
var (
// complexServiceLenses provides type-safe accessors for ComplexService fields,
// generated by the fp-go:Lens directive. These lenses are used in applicative
// composition to build the ComplexService from individual dependencies.
complexServiceLenses = MakeComplexServiceLenses()
)
// makeSampleInjectionContainer creates an InjectionContainer that can resolve services by ID.
// The container maps InjectionTokens to their corresponding service implementations.
// It returns an error if a requested service is not available.
func makeSampleInjectionContainer() InjectionContainer {
return func(token InjectionToken) Thunk[any] {
switch token {
case service1:
return readerioresult.Of(any(&impl1{}))
case service2:
return readerioresult.Of(any(&impl2{}))
default:
return readerioresult.Left[any](errors.New("dependency not available"))
}
}
}
// handleService1 is an Effect that depends type-safely on Service1.
// It demonstrates how to write handlers that work with specific service interfaces
// rather than the untyped container, providing compile-time type safety.
func handleService1() Effect[Service1, string] {
return func(ctx Service1) ReaderIOResult[string] {
return readerioresult.Of(fmt.Sprintf("Service1: %s", ctx.GetService1()))
}
}
// handleComplexService is an Effect that depends on ComplexService, which itself
// aggregates multiple service dependencies (Service1 and Service2).
// This demonstrates how to work with composite dependencies in a type-safe manner.
func handleComplexService() Effect[ComplexService, string] {
return func(ctx ComplexService) ReaderIOResult[string] {
return readerioresult.Of(fmt.Sprintf("ComplexService: %s x %s", ctx.service1.GetService1(), ctx.service2.GetService2()))
}
}
// lookupService1 is a lookup method that converts from an untyped InjectionContainer
// to a typed Service1 dependency. It performs two steps:
// 1. Read[any](service1) - retrieves the service from the container by token
// 2. ChainResultK(result.InstanceOf[Service1]) - safely casts from any to Service1
// This conversion provides type safety when moving from the untyped container to typed handlers.
var lookupService1 = F.Flow2(
Read[any](service1),
readerioresult.ChainResultK(result.InstanceOf[Service1]),
)
// lookupService2 is a lookup method for Service2, following the same pattern as lookupService1.
// It retrieves Service2 from the container and safely casts it to the correct type.
var lookupService2 = F.Flow2(
Read[any](service2),
readerioresult.ChainResultK(result.InstanceOf[Service2]),
)
// lookupComplexService demonstrates applicative composition for complex dependency injection.
// It builds a ComplexService by composing multiple service lookups:
// 1. Do[InjectionContainer](ComplexService{}) - starts with an empty ComplexService in the Effect context
// 2. ApSL(complexServiceLenses.service1, lookupService1) - looks up Service1 and sets it using the lens
// 3. ApSL(complexServiceLenses.service2, lookupService2) - looks up Service2 and sets it using the lens
//
// This applicative style allows parallel composition of independent dependencies,
// building the complete ComplexService from its constituent parts in a type-safe way.
var lookupComplexService = F.Pipe2(
Do[InjectionContainer](ComplexService{}),
ApSL(complexServiceLenses.service1, lookupService1),
ApSL(complexServiceLenses.service2, lookupService2),
)
// handleResult is a curried function that combines results from two services.
// It demonstrates how to compose the outputs of multiple effects into a final result.
// The curried form allows it to be used with applicative composition (ApS).
func handleResult(s1 string) func(string) string {
return func(s2 string) string {
return fmt.Sprintf("Final Result: %s : %s", s1, s2)
}
}
// TestDependencyLookup demonstrates both simple and complex dependency injection patterns:
//
// Simple Pattern (handle1):
// 1. Create an InjectionContainer with registered services
// 2. Define a handler (handleService1) that depends on a single typed service interface
// 3. Use a lookup method (lookupService1) to resolve the dependency from the container
// 4. Compose the handler with the lookup using LocalThunkK to inject the dependency
//
// Complex Pattern (handleComplex):
// 1. Define a handler (handleComplexService) that depends on a composite service (ComplexService)
// 2. Use applicative composition (lookupComplexService) to build the composite from multiple lookups
// 3. Each sub-dependency is resolved independently and combined using lenses
// 4. LocalThunkK injects the complete composite dependency into the handler
//
// Service Composition:
// - ApS combines the results of handle1 and handleComplex using handleResult
// - This demonstrates how to compose multiple independent effects that share the same container
// - The final result aggregates outputs from both simple and complex dependency patterns
func TestDependencyLookup(t *testing.T) {
// Create the dependency injection container
container := makeSampleInjectionContainer()
// Simple dependency injection: single service lookup
// LocalThunkK transforms the handler to work with the container
handle1 := F.Pipe1(
handleService1(),
LocalThunkK[string](lookupService1),
)
// Complex dependency injection: composite service with multiple dependencies
// lookupComplexService uses applicative composition to build ComplexService
handleComplex := F.Pipe1(
handleComplexService(),
LocalThunkK[string](lookupComplexService),
)
// Compose both services using applicative style
// ApS applies handleResult to combine outputs from handle1 and handleComplex
result := F.Pipe1(
handle1,
ApS(handleResult, handleComplex),
)
// Execute: provide container, then context, then run the IO operation
res := result(container)(t.Context())()
fmt.Println(res)
}

14
v2/effect/monoid.go Normal file
View File

@@ -0,0 +1,14 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/monoid"
)
func ApplicativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.ApplicativeMonoid[C](m)
}
func AlternativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.AlternativeMonoid[C](m)
}

350
v2/effect/monoid_test.go Normal file
View File

@@ -0,0 +1,350 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package effect
import (
"errors"
"testing"
"github.com/IBM/fp-go/v2/monoid"
"github.com/stretchr/testify/assert"
)
func TestApplicativeMonoid(t *testing.T) {
t.Run("combines successful effects with string monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
eff1 := Of[TestContext, string]("Hello")
eff2 := Of[TestContext, string](" ")
eff3 := Of[TestContext, string]("World")
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Hello World", result)
})
t.Run("combines successful effects with int monoid", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
eff1 := Of[TestContext, int](10)
eff2 := Of[TestContext, int](20)
eff3 := Of[TestContext, int](30)
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 60, result)
})
t.Run("returns empty value for empty monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"empty",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "empty", result)
})
t.Run("propagates first error", func(t *testing.T) {
expectedErr := errors.New("first error")
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
eff1 := Fail[TestContext, string](expectedErr)
eff2 := Of[TestContext, string]("World")
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedErr := errors.New("second error")
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
eff1 := Of[TestContext, string]("Hello")
eff2 := Fail[TestContext, string](expectedErr)
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("combines multiple effects", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a * b },
1,
)
effectMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
effects := []Effect[TestContext, int]{
Of[TestContext, int](2),
Of[TestContext, int](3),
Of[TestContext, int](4),
Of[TestContext, int](5),
}
combined := effectMonoid.Empty()
for _, eff := range effects {
combined = effectMonoid.Concat(combined, eff)
}
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 120, result) // 1 * 2 * 3 * 4 * 5
})
t.Run("works with custom types", func(t *testing.T) {
type Counter struct {
Count int
}
counterMonoid := monoid.MakeMonoid(
func(a, b Counter) Counter {
return Counter{Count: a.Count + b.Count}
},
Counter{Count: 0},
)
effectMonoid := ApplicativeMonoid[TestContext, Counter](counterMonoid)
eff1 := Of[TestContext, Counter](Counter{Count: 5})
eff2 := Of[TestContext, Counter](Counter{Count: 10})
eff3 := Of[TestContext, Counter](Counter{Count: 15})
combined := effectMonoid.Concat(eff1, effectMonoid.Concat(eff2, eff3))
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result.Count)
})
}
func TestAlternativeMonoid(t *testing.T) {
t.Run("combines successful effects with monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
eff1 := Of[TestContext, string]("First")
eff2 := Of[TestContext, string]("Second")
combined := effectMonoid.Concat(eff1, eff2)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "FirstSecond", result) // Alternative still combines when both succeed
})
t.Run("tries second effect if first fails", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
eff1 := Fail[TestContext, string](errors.New("first failed"))
eff2 := Of[TestContext, string]("Second")
combined := effectMonoid.Concat(eff1, eff2)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "Second", result)
})
t.Run("returns error if all effects fail", func(t *testing.T) {
expectedErr := errors.New("second error")
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
eff1 := Fail[TestContext, string](errors.New("first error"))
eff2 := Fail[TestContext, string](expectedErr)
combined := effectMonoid.Concat(eff1, eff2)
_, err := runEffect(combined, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("returns empty value for empty monoid", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + b },
"default",
)
effectMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
result, err := runEffect(effectMonoid.Empty(), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "default", result)
})
t.Run("chains multiple alternatives", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
effectMonoid := AlternativeMonoid[TestContext, int](intMonoid)
eff1 := Fail[TestContext, int](errors.New("error 1"))
eff2 := Fail[TestContext, int](errors.New("error 2"))
eff3 := Of[TestContext, int](42)
eff4 := Of[TestContext, int](100)
combined := effectMonoid.Concat(
effectMonoid.Concat(eff1, eff2),
effectMonoid.Concat(eff3, eff4),
)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 142, result) // Combines successful values: 42 + 100
})
t.Run("works with custom types", func(t *testing.T) {
type Result struct {
Value string
Code int
}
resultMonoid := monoid.MakeMonoid(
func(a, b Result) Result {
return Result{Value: a.Value + b.Value, Code: a.Code + b.Code}
},
Result{Value: "", Code: 0},
)
effectMonoid := AlternativeMonoid[TestContext, Result](resultMonoid)
eff1 := Fail[TestContext, Result](errors.New("failed"))
eff2 := Of[TestContext, Result](Result{Value: "success", Code: 200})
eff3 := Of[TestContext, Result](Result{Value: "backup", Code: 201})
combined := effectMonoid.Concat(effectMonoid.Concat(eff1, eff2), eff3)
result, err := runEffect(combined, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "successbackup", result.Value) // Combines both successful values
assert.Equal(t, 401, result.Code) // 200 + 201
})
}
func TestMonoidComparison(t *testing.T) {
t.Run("ApplicativeMonoid vs AlternativeMonoid with all success", func(t *testing.T) {
stringMonoid := monoid.MakeMonoid(
func(a, b string) string { return a + "," + b },
"",
)
applicativeMonoid := ApplicativeMonoid[TestContext, string](stringMonoid)
alternativeMonoid := AlternativeMonoid[TestContext, string](stringMonoid)
eff1 := Of[TestContext, string]("A")
eff2 := Of[TestContext, string]("B")
// Applicative combines values
applicativeResult, err1 := runEffect(
applicativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
// Alternative takes first
alternativeResult, err2 := runEffect(
alternativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.Equal(t, "A,B", applicativeResult) // Combined with comma separator
assert.Equal(t, "A,B", alternativeResult) // Also combined (Alternative uses Alt semigroup)
})
t.Run("ApplicativeMonoid vs AlternativeMonoid with failures", func(t *testing.T) {
intMonoid := monoid.MakeMonoid(
func(a, b int) int { return a + b },
0,
)
applicativeMonoid := ApplicativeMonoid[TestContext, int](intMonoid)
alternativeMonoid := AlternativeMonoid[TestContext, int](intMonoid)
eff1 := Fail[TestContext, int](errors.New("error 1"))
eff2 := Of[TestContext, int](42)
// Applicative fails on first error
_, err1 := runEffect(
applicativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
// Alternative tries second on first failure
result2, err2 := runEffect(
alternativeMonoid.Concat(eff1, eff2),
TestContext{Value: "test"},
)
assert.Error(t, err1)
assert.NoError(t, err2)
assert.Equal(t, 42, result2)
})
}

14
v2/effect/retry.go Normal file
View File

@@ -0,0 +1,14 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/retry"
)
func Retrying[C, A any](
policy retry.RetryPolicy,
action Kleisli[C, retry.RetryStatus, A],
check Predicate[Result[A]],
) Effect[C, A] {
return readerreaderioresult.Retrying(policy, action, check)
}

377
v2/effect/retry_test.go Normal file
View File

@@ -0,0 +1,377 @@
// 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 effect
import (
"errors"
"testing"
"time"
"github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/retry"
"github.com/stretchr/testify/assert"
)
func TestRetrying(t *testing.T) {
t.Run("succeeds on first attempt", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Of[TestContext, string]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "success", result)
assert.Equal(t, 1, attemptCount)
})
t.Run("retries on failure and eventually succeeds", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 3 {
return Fail[TestContext, string](errors.New("temporary error"))
}
return Of[TestContext, string]("success after retries")
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "success after retries", result)
assert.Equal(t, 3, attemptCount)
})
t.Run("exhausts retry limit", func(t *testing.T) {
attemptCount := 0
maxRetries := uint(3)
policy := retry.LimitRetries(maxRetries)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Fail[TestContext, string](errors.New("persistent error"))
},
func(res Result[string]) bool {
return result.IsLeft(res) // retry on error
},
)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, int(maxRetries+1), attemptCount) // initial attempt + retries
})
t.Run("does not retry on success", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
return Of[TestContext, int](42)
},
func(res Result[int]) bool {
return result.IsLeft(res) // retry on error
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
assert.Equal(t, 1, attemptCount)
})
t.Run("uses custom retry predicate", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
return Of[TestContext, int](attemptCount * 10)
},
func(res Result[int]) bool {
// Retry if value is less than 30
if result.IsRight(res) {
val, _ := result.Unwrap(res)
return val < 30
}
return true
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 30, result)
assert.Equal(t, 3, attemptCount)
})
t.Run("tracks retry status", func(t *testing.T) {
var statuses []retry.RetryStatus
policy := retry.LimitRetries(3)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
statuses = append(statuses, status)
if len(statuses) < 3 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("done")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "done", result)
assert.Len(t, statuses, 3)
// First attempt has iteration 0
assert.Equal(t, uint(0), statuses[0].IterNumber)
assert.Equal(t, uint(1), statuses[1].IterNumber)
assert.Equal(t, uint(2), statuses[2].IterNumber)
})
t.Run("works with exponential backoff", func(t *testing.T) {
attemptCount := 0
policy := retry.Monoid.Concat(
retry.LimitRetries(3),
retry.ExponentialBackoff(10*time.Millisecond),
)
startTime := time.Now()
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 3 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
elapsed := time.Since(startTime)
assert.NoError(t, err)
assert.Equal(t, "success", result)
assert.Equal(t, 3, attemptCount)
// Should have some delay due to backoff
assert.Greater(t, elapsed, 10*time.Millisecond)
})
t.Run("combines with other effect operations", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
eff := Map[TestContext](func(s string) string {
return "mapped: " + s
})(Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 2 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("success")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "mapped: success", result)
assert.Equal(t, 2, attemptCount)
})
t.Run("retries with different error types", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
errors := []error{
errors.New("error 1"),
errors.New("error 2"),
errors.New("error 3"),
}
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
if attemptCount < len(errors) {
err := errors[attemptCount]
attemptCount++
return Fail[TestContext, string](err)
}
attemptCount++
return Of[TestContext, string]("finally succeeded")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "finally succeeded", result)
assert.Equal(t, 4, attemptCount)
})
t.Run("no retry when predicate returns false", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(5)
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
return Fail[TestContext, string](errors.New("error"))
},
func(res Result[string]) bool {
return false // never retry
},
)
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, 1, attemptCount) // only initial attempt
})
t.Run("retries with context access", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
ctx := TestContext{Value: "retry-context"}
eff := Retrying[TestContext, string](
policy,
func(status retry.RetryStatus) Effect[TestContext, string] {
attemptCount++
if attemptCount < 2 {
return Fail[TestContext, string](errors.New("retry"))
}
return Of[TestContext, string]("success with context")
},
func(res Result[string]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, ctx)
assert.NoError(t, err)
assert.Equal(t, "success with context", result)
assert.Equal(t, 2, attemptCount)
})
}
func TestRetryingWithComplexScenarios(t *testing.T) {
t.Run("retry with state accumulation", func(t *testing.T) {
type State struct {
Attempts []int
Value string
}
policy := retry.LimitRetries(4)
eff := Retrying[TestContext, State](
policy,
func(status retry.RetryStatus) Effect[TestContext, State] {
state := State{
Attempts: make([]int, status.IterNumber+1),
Value: "attempt",
}
for i := uint(0); i <= status.IterNumber; i++ {
state.Attempts[i] = int(i)
}
if status.IterNumber < 2 {
return Fail[TestContext, State](errors.New("retry"))
}
return Of[TestContext, State](state)
},
func(res Result[State]) bool {
return result.IsLeft(res)
},
)
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "attempt", result.Value)
assert.Equal(t, []int{0, 1, 2}, result.Attempts)
})
t.Run("retry with chain operations", func(t *testing.T) {
attemptCount := 0
policy := retry.LimitRetries(3)
eff := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("final: " + string(rune('0'+x)))
})(Retrying[TestContext, int](
policy,
func(status retry.RetryStatus) Effect[TestContext, int] {
attemptCount++
if attemptCount < 2 {
return Fail[TestContext, int](errors.New("retry"))
}
return Of[TestContext, int](attemptCount)
},
func(res Result[int]) bool {
return result.IsLeft(res)
},
))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Contains(t, result, "final:")
})
}

19
v2/effect/run.go Normal file
View File

@@ -0,0 +1,19 @@
package effect
import (
"context"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/idiomatic/context/readerresult"
"github.com/IBM/fp-go/v2/result"
)
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
return readerreaderioresult.Read[A](c)
}
func RunSync[A any](fa ReaderIOResult[A]) readerresult.ReaderResult[A] {
return func(ctx context.Context) (A, error) {
return result.Unwrap(fa(ctx)())
}
}

326
v2/effect/run_test.go Normal file
View File

@@ -0,0 +1,326 @@
// 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 effect
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestProvide(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
ctx := TestContext{Value: "test-value"}
eff := Of[TestContext, string]("result")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("provides context with specific values", func(t *testing.T) {
type Config struct {
Host string
Port int
}
cfg := Config{Host: "localhost", Port: 8080}
eff := Of[Config, string]("connected")
ioResult := Provide[Config, string](cfg)(eff)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "connected", result)
})
t.Run("propagates errors", func(t *testing.T) {
expectedErr := errors.New("provide error")
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("works with different context types", func(t *testing.T) {
type SimpleContext struct {
ID int
}
ctx := SimpleContext{ID: 42}
eff := Of[SimpleContext, int](100)
ioResult := Provide[SimpleContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, result)
})
t.Run("provides context to chained effects", func(t *testing.T) {
ctx := TestContext{Value: "base"}
eff := Chain[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string]("result")
})(Of[TestContext, int](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "result", result)
})
t.Run("provides context to mapped effects", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Map[TestContext](func(x int) string {
return "mapped"
})(Of[TestContext, int](42))
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, "mapped", result)
})
}
func TestRunSync(t *testing.T) {
t.Run("runs effect synchronously", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext, int](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("runs effect with context.Context", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext, string]("hello")
ioResult := Provide[TestContext, string](ctx)(eff)
readerResult := RunSync[string](ioResult)
bgCtx := context.Background()
result, err := readerResult(bgCtx)
assert.NoError(t, err)
assert.Equal(t, "hello", result)
})
t.Run("propagates errors synchronously", func(t *testing.T) {
expectedErr := errors.New("sync error")
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, int](expectedErr)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("runs complex effect chains", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x + 10)
})(Of[TestContext, int](5)))
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 30, result) // (5 + 10) * 2
})
t.Run("handles multiple sequential runs", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext, int](42)
ioResult := Provide[TestContext, int](ctx)(eff)
readerResult := RunSync[int](ioResult)
// Run multiple times
result1, err1 := readerResult(context.Background())
result2, err2 := readerResult(context.Background())
result3, err3 := readerResult(context.Background())
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
assert.Equal(t, 42, result1)
assert.Equal(t, 42, result2)
assert.Equal(t, 42, result3)
})
t.Run("works with different result types", func(t *testing.T) {
type User struct {
Name string
Age int
}
ctx := TestContext{Value: "test"}
user := User{Name: "Alice", Age: 30}
eff := Of[TestContext, User](user)
ioResult := Provide[TestContext, User](ctx)(eff)
readerResult := RunSync[User](ioResult)
result, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, user, result)
})
}
func TestProvideAndRunSyncIntegration(t *testing.T) {
t.Run("complete workflow with success", func(t *testing.T) {
type AppConfig struct {
APIKey string
Timeout int
}
cfg := AppConfig{APIKey: "secret", Timeout: 30}
// Create an effect that uses the config
eff := Of[AppConfig, string]("API call successful")
// Provide config and run
result, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "API call successful", result)
})
t.Run("complete workflow with error", func(t *testing.T) {
type AppConfig struct {
APIKey string
}
expectedErr := errors.New("API error")
cfg := AppConfig{APIKey: "secret"}
eff := Fail[AppConfig, string](expectedErr)
_, err := RunSync[string](Provide[AppConfig, string](cfg)(eff))(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("workflow with transformations", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Map[TestContext](func(x int) string {
return "final"
})(Chain[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(Of[TestContext, int](21)))
result, err := RunSync[string](Provide[TestContext, string](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "final", result)
})
t.Run("workflow with bind operations", func(t *testing.T) {
type State struct {
X int
Y int
}
ctx := TestContext{Value: "test"}
eff := Bind[TestContext](
func(y int) func(State) State {
return func(s State) State {
s.Y = y
return s
}
},
func(s State) Effect[TestContext, int] {
return Of[TestContext, int](s.X * 2)
},
)(BindTo[TestContext](func(x int) State {
return State{X: x}
})(Of[TestContext, int](10)))
result, err := RunSync[State](Provide[TestContext, State](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
assert.Equal(t, 20, result.Y)
})
t.Run("workflow with context transformation", func(t *testing.T) {
type OuterCtx struct {
Value string
}
type InnerCtx struct {
Data string
}
outerCtx := OuterCtx{Value: "outer"}
innerEff := Of[InnerCtx, string]("inner result")
// Transform context
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
return InnerCtx{Data: outer.Value + "-transformed"}
})(innerEff)
result, err := RunSync[string](Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner result", result)
})
t.Run("workflow with array traversal", func(t *testing.T) {
ctx := TestContext{Value: "test"}
input := []int{1, 2, 3, 4, 5}
eff := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})(input)
result, err := RunSync[[]int](Provide[TestContext, []int](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
})
}

7
v2/effect/traverse.go Normal file
View File

@@ -0,0 +1,7 @@
package effect
import "github.com/IBM/fp-go/v2/context/readerreaderioresult"
func TraverseArray[C, A, B any](f Kleisli[C, A, B]) Kleisli[C, []A, []B] {
return readerreaderioresult.TraverseArray(f)
}

266
v2/effect/traverse_test.go Normal file
View File

@@ -0,0 +1,266 @@
// 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 effect
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTraverseArray(t *testing.T) {
t.Run("traverses empty array", func(t *testing.T) {
input := []int{}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Empty(t, result)
})
t.Run("traverses array with single element", func(t *testing.T) {
input := []int{42}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []string{"42"}, result)
})
t.Run("traverses array with multiple elements", func(t *testing.T) {
input := []int{1, 2, 3, 4, 5}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []string{"1", "2", "3", "4", "5"}, result)
})
t.Run("transforms to different type", func(t *testing.T) {
input := []string{"hello", "world", "test"}
kleisli := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
return Of[TestContext, int](len(s))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []int{5, 5, 4}, result)
})
t.Run("stops on first error", func(t *testing.T) {
expectedErr := errors.New("traverse error")
input := []int{1, 2, 3, 4, 5}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
if x == 3 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("handles complex transformations", func(t *testing.T) {
type User struct {
ID int
Name string
}
input := []int{1, 2, 3}
kleisli := TraverseArray[TestContext](func(id int) Effect[TestContext, User] {
return Of[TestContext, User](User{
ID: id,
Name: fmt.Sprintf("User%d", id),
})
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Len(t, result, 3)
assert.Equal(t, 1, result[0].ID)
assert.Equal(t, "User1", result[0].Name)
assert.Equal(t, 2, result[1].ID)
assert.Equal(t, "User2", result[1].Name)
assert.Equal(t, 3, result[2].ID)
assert.Equal(t, "User3", result[2].Name)
})
t.Run("chains with other operations", func(t *testing.T) {
input := []int{1, 2, 3}
eff := Chain[TestContext](func(strings []string) Effect[TestContext, int] {
total := 0
for _, s := range strings {
val, _ := strconv.Atoi(s)
total += val
}
return Of[TestContext, int](total)
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x * 2))
})(input))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 12, result) // (1*2) + (2*2) + (3*2) = 2 + 4 + 6 = 12
})
t.Run("uses context in transformation", func(t *testing.T) {
input := []int{1, 2, 3}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Chain[TestContext](func(ctx TestContext) Effect[TestContext, string] {
return Of[TestContext, string](fmt.Sprintf("%s-%d", ctx.Value, x))
})(Of[TestContext, TestContext](TestContext{Value: "prefix"}))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []string{"prefix-1", "prefix-2", "prefix-3"}, result)
})
t.Run("preserves order", func(t *testing.T) {
input := []int{5, 3, 8, 1, 9, 2}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 10)
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []int{50, 30, 80, 10, 90, 20}, result)
})
t.Run("handles large arrays", func(t *testing.T) {
size := 1000
input := make([]int, size)
for i := 0; i < size; i++ {
input[i] = i
}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, int] {
return Of[TestContext, int](x * 2)
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Len(t, result, size)
assert.Equal(t, 0, result[0])
assert.Equal(t, 1998, result[999])
})
t.Run("composes multiple traversals", func(t *testing.T) {
input := []int{1, 2, 3}
// First traversal: int -> string
kleisli1 := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
// Second traversal: string -> int (length)
kleisli2 := TraverseArray[TestContext](func(s string) Effect[TestContext, int] {
return Of[TestContext, int](len(s))
})
eff := Chain[TestContext](kleisli2)(kleisli1(input))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, []int{1, 1, 1}, result) // All single-digit numbers have length 1
})
t.Run("handles nil array", func(t *testing.T) {
var input []int
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})
result, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.NoError(t, err)
assert.Empty(t, result) // TraverseArray returns empty slice for nil input
})
t.Run("works with Map for post-processing", func(t *testing.T) {
input := []int{1, 2, 3}
eff := Map[TestContext](func(strings []string) string {
result := ""
for _, s := range strings {
result += s + ","
}
return result
})(TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
return Of[TestContext, string](strconv.Itoa(x))
})(input))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "1,2,3,", result)
})
t.Run("error in middle of array", func(t *testing.T) {
expectedErr := errors.New("middle error")
input := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("error at end of array", func(t *testing.T) {
expectedErr := errors.New("end error")
input := []int{1, 2, 3, 4, 5}
kleisli := TraverseArray[TestContext](func(x int) Effect[TestContext, string] {
if x == 5 {
return Fail[TestContext, string](expectedErr)
}
return Of[TestContext, string](strconv.Itoa(x))
})
_, err := runEffect(kleisli(input), TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}

37
v2/effect/types.go Normal file
View File

@@ -0,0 +1,37 @@
package effect
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
)
type (
Either[E, A any] = either.Either[E, A]
Reader[R, A any] = reader.Reader[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
IO[A any] = io.IO[A]
IOEither[E, A any] = ioeither.IOEither[E, A]
Lazy[A any] = lazy.Lazy[A]
IOResult[A any] = ioresult.IOResult[A]
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
Monoid[A any] = monoid.Monoid[A]
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
Thunk[A any] = ReaderIOResult[A]
Predicate[A any] = predicate.Predicate[A]
Result[A any] = result.Result[A]
Lens[S, T any] = lens.Lens[S, T]
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
)

View File

@@ -4,14 +4,11 @@ go 1.24
require (
github.com/stretchr/testify v1.11.1
github.com/urfave/cli/v2 v2.27.7
github.com/urfave/cli/v3 v3.6.2
)
require (
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-20250705151800-55b8f293f342 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,17 +1,11 @@
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=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.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.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
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

@@ -19,6 +19,31 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
// MonadSequenceSegment sequences a segment of an array of effects using a divide-and-conquer approach.
// It recursively splits the array segment in half, sequences each half, and concatenates the results.
//
// This function is optimized for performance by using a divide-and-conquer strategy that reduces
// the depth of nested function calls compared to a linear fold approach.
//
// Type parameters:
// - HKTB: The higher-kinded type containing values (e.g., Option[B], Either[E, B])
// - HKTRB: The higher-kinded type containing an array of values (e.g., Option[[]B], Either[E, []B])
//
// Parameters:
// - fof: Function to lift a single HKTB into HKTRB
// - empty: The empty/identity value for HKTRB
// - concat: Function to concatenate two HKTRB values
// - fbs: The array of effects to sequence
// - start: The starting index of the segment (inclusive)
// - end: The ending index of the segment (exclusive)
//
// Returns:
// - HKTRB: The sequenced result for the segment
//
// The function handles three cases:
// - Empty segment (end - start == 0): returns empty
// - Single element (end - start == 1): returns fof(fbs[start])
// - Multiple elements: recursively divides and conquers
func MonadSequenceSegment[HKTB, HKTRB any](
fof func(HKTB) HKTRB,
empty HKTRB,
@@ -41,6 +66,23 @@ func MonadSequenceSegment[HKTB, HKTRB any](
}
}
// SequenceSegment creates a function that sequences a segment of an array of effects.
// Unlike MonadSequenceSegment, this returns a curried function that can be reused.
//
// This function builds a computation tree at construction time, which can be more efficient
// when the same sequencing pattern needs to be applied multiple times to arrays of the same length.
//
// Type parameters:
// - HKTB: The higher-kinded type containing values
// - HKTRB: The higher-kinded type containing an array of values
//
// Parameters:
// - fof: Function to lift a single HKTB into HKTRB
// - empty: The empty/identity value for HKTRB
// - concat: Function to concatenate two HKTRB values
//
// Returns:
// - A function that takes an array of HKTB and returns HKTRB
func SequenceSegment[HKTB, HKTRB any](
fof func(HKTB) HKTRB,
empty HKTRB,
@@ -85,14 +127,39 @@ func SequenceSegment[HKTB, HKTRB any](
}
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverse maps each element of an array to an effect, then sequences the results.
// This is the monadic version that takes the array as a direct parameter.
//
// Traverse combines mapping and sequencing in one operation. It's useful when you want to
// transform each element of an array into an effect (like Option, Either, IO, etc.) and
// then collect all those effects into a single effect containing an array.
//
// We need to pass the members of the applicative explicitly, because golang does neither
// support higher kinded types nor template methods on structs or interfaces.
//
// Type parameters:
// - GA: The input array type (e.g., []A)
// - GB: The output array type (e.g., []B)
// - A: The input element type
// - B: The output element type
// - HKTB: HKT<B> - The effect containing B (e.g., Option[B])
// - HKTAB: HKT<func(B)GB> - Intermediate applicative type
// - HKTRB: HKT<GB> - The effect containing the result array (e.g., Option[[]B])
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - ta: The input array to traverse
// - f: The function to apply to each element, producing an effect
//
// Returns:
// - HKTRB: An effect containing the array of transformed values
//
// Example:
//
// If any element produces None, the entire result is None.
// If all elements produce Some, the result is Some containing all values.
func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -103,14 +170,20 @@ func MonadTraverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
return MonadTraverseReduce(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverseWithIndex is like MonadTraverse but the transformation function also receives the index.
// This is useful when the transformation depends on the element's position in the array.
//
// Type parameters: Same as MonadTraverse
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - ta: The input array to traverse
// - f: The function to apply to each element with its index, producing an effect
//
// Returns:
// - HKTRB: An effect containing the array of transformed values
func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -121,6 +194,19 @@ func MonadTraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
return MonadTraverseReduceWithIndex(fof, fmap, fap, ta, f, Append[GB, B], Empty[GB]())
}
// Traverse creates a curried function that maps each element to an effect and sequences the results.
// This is the curried version of MonadTraverse, useful for partial application and composition.
//
// Type parameters: Same as MonadTraverse
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - f: The function to apply to each element, producing an effect
//
// Returns:
// - A function that takes an array and returns an effect containing the transformed array
func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -133,6 +219,19 @@ func Traverse[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
}
}
// TraverseWithIndex creates a curried function like Traverse but with index-aware transformation.
// This is the curried version of MonadTraverseWithIndex.
//
// Type parameters: Same as MonadTraverse
//
// Parameters:
// - fof: Function to lift a value into the effect (Of/Pure)
// - fmap: Function to map over the effect (Map)
// - fap: Function to apply an effect of a function to an effect of a value (Ap)
// - f: The function to apply to each element with its index, producing an effect
//
// Returns:
// - A function that takes an array and returns an effect containing the transformed array
func TraverseWithIndex[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -231,6 +330,16 @@ func TraverseReduce[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
}
}
// TraverseReduceWithIndex creates a curried function for index-aware custom reduction during traversal.
// This is the curried version of MonadTraverseReduceWithIndex.
//
// Type parameters: Same as MonadTraverseReduce
//
// Parameters: Same as TraverseReduce, except:
// - transform: Function that takes index and element, producing an effect
//
// Returns:
// - A function that takes an array and returns an effect containing the accumulated value
func TraverseReduceWithIndex[GA ~[]A, GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,

View File

@@ -1,10 +1,60 @@
// 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 iter provides functional programming utilities for working with Go 1.23+ iterators.
// It offers operations for reducing, mapping, concatenating, and transforming iterator sequences
// in a functional style, compatible with the range-over-func pattern.
package iter
import (
"slices"
F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
)
func From[A any](as ...A) Seq[A] {
return slices.Values(as)
}
// MonadReduceWithIndex reduces an iterator sequence to a single value using a reducer function
// that receives the current index, accumulated value, and current element.
//
// The function iterates through all elements in the sequence, applying the reducer function
// at each step with the element's index. This is useful when the position of elements matters
// in the reduction logic.
//
// Parameters:
// - fa: The iterator sequence to reduce
// - f: The reducer function that takes (index, accumulator, element) and returns the new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - The final accumulated value after processing all elements
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(10)
// yield(20)
// yield(30)
// }
// // Sum with index multiplier: 0*10 + 1*20 + 2*30 = 80
// result := MonadReduceWithIndex(iter, func(i, acc, val int) int {
// return acc + i*val
// }, 0)
func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(int, B, A) B, initial B) B {
current := initial
var i int
@@ -15,6 +65,29 @@ func MonadReduceWithIndex[GA ~func(yield func(A) bool), A, B any](fa GA, f func(
return current
}
// MonadReduce reduces an iterator sequence to a single value using a reducer function.
//
// This is similar to MonadReduceWithIndex but without index tracking, making it more
// efficient when the position of elements is not needed in the reduction logic.
//
// Parameters:
// - fa: The iterator sequence to reduce
// - f: The reducer function that takes (accumulator, element) and returns the new accumulator
// - initial: The initial value for the accumulator
//
// Returns:
// - The final accumulated value after processing all elements
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
// sum := MonadReduce(iter, func(acc, val int) int {
// return acc + val
// }, 0) // Returns: 6
func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B, initial B) B {
current := initial
for a := range fa {
@@ -23,7 +96,30 @@ func MonadReduce[GA ~func(yield func(A) bool), A, B any](fa GA, f func(B, A) B,
return current
}
// Concat concatenates two sequences, yielding all elements from left followed by all elements from right.
// Concat concatenates two iterator sequences, yielding all elements from left followed by all elements from right.
//
// The resulting iterator will first yield all elements from the left sequence, then all elements
// from the right sequence. If the consumer stops early (yield returns false), iteration stops
// immediately without processing remaining elements.
//
// Parameters:
// - left: The first iterator sequence
// - right: The second iterator sequence
//
// Returns:
// - A new iterator that yields elements from both sequences in order
//
// Example:
//
// left := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// right := func(yield func(int) bool) {
// yield(3)
// yield(4)
// }
// combined := Concat(left, right) // Yields: 1, 2, 3, 4
func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
return func(yield func(T) bool) {
for t := range left {
@@ -39,28 +135,129 @@ func Concat[GT ~func(yield func(T) bool), T any](left, right GT) GT {
}
}
// Of creates an iterator sequence containing a single element.
//
// This is the unit/return operation for the iterator monad, lifting a single value
// into the iterator context.
//
// Parameters:
// - a: The element to wrap in an iterator
//
// Returns:
// - An iterator that yields exactly one element
//
// Example:
//
// iter := Of[func(yield func(int) bool)](42)
// // Yields: 42
func Of[GA ~func(yield func(A) bool), A any](a A) GA {
return func(yield func(A) bool) {
yield(a)
}
}
// MonadAppend appends a single element to the end of an iterator sequence.
//
// This creates a new iterator that yields all elements from the original sequence
// followed by the tail element.
//
// Parameters:
// - f: The original iterator sequence
// - tail: The element to append
//
// Returns:
// - A new iterator with the tail element appended
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := MonadAppend(iter, 3) // Yields: 1, 2, 3
func MonadAppend[GA ~func(yield func(A) bool), A any](f GA, tail A) GA {
return Concat(f, Of[GA](tail))
}
// Append returns a function that appends a single element to the end of an iterator sequence.
//
// This is the curried version of MonadAppend, useful for partial application and composition.
//
// Parameters:
// - tail: The element to append
//
// Returns:
// - A function that takes an iterator and returns a new iterator with the tail element appended
//
// Example:
//
// appendThree := Append[func(yield func(int) bool)](3)
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := appendThree(iter) // Yields: 1, 2, 3
func Append[GA ~func(yield func(A) bool), A any](tail A) func(GA) GA {
return F.Bind2nd(Concat[GA], Of[GA](tail))
}
// Prepend returns a function that prepends a single element to the beginning of an iterator sequence.
//
// This is the curried version for prepending, useful for partial application and composition.
//
// Parameters:
// - head: The element to prepend
//
// Returns:
// - A function that takes an iterator and returns a new iterator with the head element prepended
//
// Example:
//
// prependZero := Prepend[func(yield func(int) bool)](0)
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := prependZero(iter) // Yields: 0, 1, 2
func Prepend[GA ~func(yield func(A) bool), A any](head A) func(GA) GA {
return F.Bind1st(Concat[GA], Of[GA](head))
}
// Empty creates an empty iterator sequence that yields no elements.
//
// This is the identity element for the Concat operation and represents an empty collection
// in the iterator context.
//
// Returns:
// - An iterator that yields no elements
//
// Example:
//
// iter := Empty[func(yield func(int) bool), int]()
// // Yields nothing
func Empty[GA ~func(yield func(A) bool), A any]() GA {
return func(_ func(A) bool) {}
}
// ToArray collects all elements from an iterator sequence into a slice.
//
// This eagerly evaluates the entire iterator sequence and materializes all elements
// into memory as a slice.
//
// Parameters:
// - fa: The iterator sequence to collect
//
// Returns:
// - A slice containing all elements from the iterator
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
// arr := ToArray[func(yield func(int) bool), []int](iter) // Returns: []int{1, 2, 3}
func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
bs := make(GB, 0)
for a := range fa {
@@ -69,6 +266,28 @@ func ToArray[GA ~func(yield func(A) bool), GB ~[]A, A any](fa GA) GB {
return bs
}
// MonadMapToArray maps each element of an iterator sequence through a function and collects the results into a slice.
//
// This combines mapping and collection into a single operation, eagerly evaluating the entire
// iterator sequence and materializing the transformed elements into memory.
//
// Parameters:
// - fa: The iterator sequence to map and collect
// - f: The mapping function to apply to each element
//
// Returns:
// - A slice containing the mapped elements
//
// Example:
//
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
// doubled := MonadMapToArray[func(yield func(int) bool), []int](iter, func(x int) int {
// return x * 2
// }) // Returns: []int{2, 4, 6}
func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(A) B) GB {
bs := make(GB, 0)
for a := range fa {
@@ -77,10 +296,54 @@ func MonadMapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f f
return bs
}
// MapToArray returns a function that maps each element through a function and collects the results into a slice.
//
// This is the curried version of MonadMapToArray, useful for partial application and composition.
//
// Parameters:
// - f: The mapping function to apply to each element
//
// Returns:
// - A function that takes an iterator and returns a slice of mapped elements
//
// Example:
//
// double := MapToArray[func(yield func(int) bool), []int](func(x int) int {
// return x * 2
// })
// iter := func(yield func(int) bool) {
// yield(1)
// yield(2)
// }
// result := double(iter) // Returns: []int{2, 4}
func MapToArray[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(A) B) func(GA) GB {
return F.Bind2nd(MonadMapToArray[GA, GB], f)
}
// MonadMapToArrayWithIndex maps each element of an iterator sequence through a function that receives
// the element's index, and collects the results into a slice.
//
// This is similar to MonadMapToArray but the mapping function also receives the zero-based index
// of each element, useful when the position matters in the transformation logic.
//
// Parameters:
// - fa: The iterator sequence to map and collect
// - f: The mapping function that takes (index, element) and returns the transformed element
//
// Returns:
// - A slice containing the mapped elements
//
// Example:
//
// iter := func(yield func(string) bool) {
// yield("a")
// yield("b")
// yield("c")
// }
// indexed := MonadMapToArrayWithIndex[func(yield func(string) bool), []string](iter,
// func(i int, s string) string {
// return fmt.Sprintf("%d:%s", i, s)
// }) // Returns: []string{"0:a", "1:b", "2:c"}
func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](fa GA, f func(int, A) B) GB {
bs := make(GB, 0)
var i int
@@ -91,10 +354,49 @@ func MonadMapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f
return bs
}
// MapToArrayWithIndex returns a function that maps each element through an indexed function
// and collects the results into a slice.
//
// This is the curried version of MonadMapToArrayWithIndex, useful for partial application and composition.
//
// Parameters:
// - f: The mapping function that takes (index, element) and returns the transformed element
//
// Returns:
// - A function that takes an iterator and returns a slice of mapped elements
//
// Example:
//
// addIndex := MapToArrayWithIndex[func(yield func(string) bool), []string](
// func(i int, s string) string {
// return fmt.Sprintf("%d:%s", i, s)
// })
// iter := func(yield func(string) bool) {
// yield("a")
// yield("b")
// }
// result := addIndex(iter) // Returns: []string{"0:a", "1:b"}
func MapToArrayWithIndex[GA ~func(yield func(A) bool), GB ~[]B, A, B any](f func(int, A) B) func(GA) GB {
return F.Bind2nd(MonadMapToArrayWithIndex[GA, GB], f)
}
// Monoid returns a Monoid instance for iterator sequences.
//
// The monoid uses Concat as the binary operation and Empty as the identity element,
// allowing iterator sequences to be combined in an associative way with a neutral element.
// This enables generic operations that work with any monoid, such as folding a collection
// of iterators into a single iterator.
//
// Returns:
// - A Monoid instance with Concat and Empty operations
//
// Example:
//
// m := Monoid[func(yield func(int) bool), int]()
// iter1 := func(yield func(int) bool) { yield(1); yield(2) }
// iter2 := func(yield func(int) bool) { yield(3); yield(4) }
// combined := m.Concat(iter1, iter2) // Yields: 1, 2, 3, 4
// empty := m.Empty() // Yields nothing
func Monoid[GA ~func(yield func(A) bool), A any]() M.Monoid[GA] {
return M.MakeMonoid(Concat[GA], Empty[GA]())
}

View File

@@ -21,18 +21,50 @@ import (
M "github.com/IBM/fp-go/v2/monoid"
)
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverse traverses an iterator sequence, applying an effectful function to each element
// and collecting the results in an applicative context.
//
// This is a fundamental operation in functional programming that allows you to "turn inside out"
// a structure containing effects. It maps each element through a function that produces an effect,
// then sequences all those effects together while preserving the iterator structure.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The output iterator type ~func(yield func(B) bool)
// - A: The input element type
// - B: The output element type
// - HKT_B: The higher-kinded type representing an effect containing B
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
// - HKT_GB: The higher-kinded type representing an effect containing GB (the result iterator)
//
// Parameters:
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
// - fof_gb: Lifts a GB value into the effect context (pure/of operation)
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
// - fap_gb: Applies an effectful function to an effectful value (ap operation)
// - ta: The input iterator sequence to traverse
// - f: The effectful function to apply to each element
//
// Returns:
// - An effect containing an iterator of transformed elements
//
// Note: We need to pass the applicative operations explicitly because Go doesn't support
// higher-kinded types or template methods on structs/interfaces.
//
// Example (conceptual with Option):
//
// // Traverse an iterator of strings, parsing each as an integer
// // If any parse fails, the whole result is None
// iter := func(yield func(string) bool) {
// yield("1")
// yield("2")
// yield("3")
// }
// result := MonadTraverse(..., iter, parseInt) // Some(iterator of [1,2,3]) or None
func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
fmap_b func(HKT_B, func(B) GB) HKT_GB,
fof_gb func(GB) HKT_GB,
fof_gb OfType[GB, HKT_GB],
fmap_gb func(HKT_GB, func(GB) func(GB) GB) HKT_GB_GB,
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
@@ -54,14 +86,43 @@ func MonadTraverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
}
// Traverse is the curried version of MonadTraverse, returning a function that traverses an iterator.
//
// This version uses type aliases for better readability and is more suitable for partial application
// and function composition. It returns a Kleisli arrow (a function from GA to HKT_GB).
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The output iterator type ~func(yield func(B) bool)
// - A: The input element type
// - B: The output element type
// - HKT_B: The higher-kinded type representing an effect containing B
// - HKT_GB_GB: The higher-kinded type for a function from GB to GB in the effect context
// - HKT_GB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fmap_b: Maps a function over HKT_B to produce HKT_GB
// - fof_gb: Lifts a GB value into the effect context
// - fmap_gb: Maps a function over HKT_GB to produce HKT_GB_GB
// - fap_gb: Applies an effectful function to an effectful value
// - f: The effectful function to apply to each element (Kleisli arrow)
//
// Returns:
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
//
// Example (conceptual):
//
// parseInts := Traverse[...](fmap, fof, fmap_gb, fap, parseInt)
// iter := func(yield func(string) bool) { yield("1"); yield("2") }
// result := parseInts(iter) // Effect containing iterator of integers
func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B, HKT_B, HKT_GB_GB, HKT_GB any](
fmap_b func(func(B) GB) func(HKT_B) HKT_GB,
fmap_b MapType[B, GB, HKT_B, HKT_GB],
fof_gb func(GB) HKT_GB,
fmap_gb func(func(GB) func(GB) GB) func(HKT_GB) HKT_GB_GB,
fap_gb func(HKT_GB_GB, HKT_GB) HKT_GB,
fof_gb OfType[GB, HKT_GB],
fmap_gb MapType[GB, Endomorphism[GB], HKT_GB, HKT_GB_GB],
fap_gb ApType[HKT_GB, HKT_GB, HKT_GB_GB],
f func(A) HKT_B) func(GA) HKT_GB {
f Kleisli[A, HKT_B]) Kleisli[GA, HKT_GB] {
fof := fmap_b(Of[GB])
empty := fof_gb(Empty[GB]())
@@ -69,18 +130,50 @@ func Traverse[GA ~func(yield func(A) bool), GB ~func(yield func(B) bool), A, B,
concat_gb := fmap_gb(cb)
concat := func(first, second HKT_GB) HKT_GB {
return fap_gb(concat_gb(first), second)
return fap_gb(second)(concat_gb(first))
}
return func(ma GA) HKT_GB {
// return INTA.SequenceSegment(fof, empty, concat)(MapToArray[GA, []HKT_B](f)(ma))
hktb := MonadMapToArray[GA, []HKT_B](ma, f)
return INTA.MonadSequenceSegment(fof, empty, concat, hktb, 0, len(hktb))
}
return F.Flow2(
MapToArray[GA, []HKT_B](f),
INTA.SequenceSegment(fof, empty, concat),
)
}
// MonadSequence sequences an iterator of effects into an effect containing an iterator.
//
// This is a special case of traverse where the transformation function is the identity.
// It "flips" the nesting of the iterator and effect types, collecting all effects into
// a single effect containing an iterator of values.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(HKTA) bool)
// - HKTA: The higher-kinded type representing an effect containing A
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
//
// Parameters:
// - fof: Lifts an HKTA value into the HKTRA context
// - m: A monoid for combining HKTRA values
// - ta: The input iterator of effects to sequence
//
// Returns:
// - An effect containing an iterator of values
//
// Example (conceptual with Option):
//
// iter := func(yield func(Option[int]) bool) {
// yield(Some(1))
// yield(Some(2))
// yield(Some(3))
// }
// result := MonadSequence(..., iter) // Some(iterator of [1,2,3])
//
// iter2 := func(yield func(Option[int]) bool) {
// yield(Some(1))
// yield(None)
// }
// result2 := MonadSequence(..., iter2) // None
func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
fof func(HKTA) HKTRA,
fof OfType[HKTA, HKTRA],
m M.Monoid[HKTRA],
ta GA) HKTRA {
@@ -90,14 +183,37 @@ func MonadSequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
}
/*
*
We need to pass the members of the applicative explicitly, because golang does neither support higher kinded types nor template methods on structs or interfaces
HKTRB = HKT<GB>
HKTB = HKT<B>
HKTAB = HKT<func(A)B>
*/
// MonadTraverseWithIndex traverses an iterator sequence with index tracking, applying an effectful
// function to each element along with its index.
//
// This is similar to MonadTraverse but the transformation function receives both the element's
// zero-based index and the element itself, useful when the position matters in the transformation.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - A: The input element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
//
// Parameters:
// - fof: Lifts an HKTB value into the HKTRB context
// - m: A monoid for combining HKTRB values
// - ta: The input iterator sequence to traverse
// - f: The effectful function that takes (index, element) and returns an effect
//
// Returns:
// - An effect containing an iterator of transformed elements
//
// Example (conceptual):
//
// iter := func(yield func(string) bool) {
// yield("a")
// yield("b")
// }
// // Add index prefix to each element
// result := MonadTraverseWithIndex(..., iter, func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// }) // Effect containing iterator of ["0:a", "1:b"]
func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
fof func(HKTB) HKTRB,
m M.Monoid[HKTRB],
@@ -110,8 +226,29 @@ func MonadTraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
return INTA.MonadSequenceSegment(fof, m.Empty(), m.Concat, hktb, 0, len(hktb))
}
// Sequence is the curried version of MonadSequence, returning a function that sequences an iterator of effects.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(HKTA) bool)
// - HKTA: The higher-kinded type representing an effect containing A
// - HKTRA: The higher-kinded type representing an effect containing an iterator of A
//
// Parameters:
// - fof: Lifts an HKTA value into the HKTRA context
// - m: A monoid for combining HKTRA values
//
// Returns:
// - A function that takes an iterator of effects and returns an effect containing an iterator
//
// Example (conceptual):
//
// sequenceOptions := Sequence[...](fof, monoid)
// iter := func(yield func(Option[int]) bool) { yield(Some(1)); yield(Some(2)) }
// result := sequenceOptions(iter) // Some(iterator of [1,2])
func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
fof func(HKTA) HKTRA,
fof OfType[HKTA, HKTRA],
m M.Monoid[HKTRA]) func(GA) HKTRA {
return func(ma GA) HKTRA {
@@ -119,6 +256,32 @@ func Sequence[GA ~func(yield func(HKTA) bool), HKTA, HKTRA any](
}
}
// TraverseWithIndex is the curried version of MonadTraverseWithIndex, returning a function that
// traverses an iterator with index tracking.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - A: The input element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTRB: The higher-kinded type representing an effect containing an iterator of B
//
// Parameters:
// - fof: Lifts an HKTB value into the HKTRB context
// - m: A monoid for combining HKTRB values
// - f: The effectful function that takes (index, element) and returns an effect
//
// Returns:
// - A function that takes an iterator and returns an effect containing an iterator of transformed elements
//
// Example (conceptual):
//
// addIndexPrefix := TraverseWithIndex[...](fof, monoid, func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// })
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
// result := addIndexPrefix(iter) // Effect containing iterator of ["0:a", "1:b"]
func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
fof func(HKTB) HKTRB,
m M.Monoid[HKTRB],
@@ -130,6 +293,39 @@ func TraverseWithIndex[GA ~func(yield func(A) bool), A, HKTB, HKTRB any](
}
}
// MonadTraverseReduce combines traversal with reduction, applying an effectful transformation
// and accumulating results using a reducer function.
//
// This is a more efficient operation when you want to both transform elements through effects
// and reduce them to a single accumulated value, avoiding intermediate collections.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - ta: The input iterator sequence to traverse and reduce
// - transform: The effectful function to apply to each element
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - An effect containing the final accumulated value
//
// Example (conceptual):
//
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
// // Parse strings to ints and sum them
// result := MonadTraverseReduce(..., iter, parseInt, add, 0)
// // Returns: Some(6) or None if any parse fails
func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -152,6 +348,44 @@ func MonadTraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HK
}, fof(initial))
}
// MonadTraverseReduceWithIndex combines indexed traversal with reduction, applying an effectful
// transformation that receives element indices and accumulating results using a reducer function.
//
// This is similar to MonadTraverseReduce but the transformation function also receives the
// zero-based index of each element, useful when position matters in the transformation logic.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - ta: The input iterator sequence to traverse and reduce
// - transform: The effectful function that takes (index, element) and returns an effect
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - An effect containing the final accumulated value
//
// Example (conceptual):
//
// iter := func(yield func(string) bool) { yield("a"); yield("b"); yield("c") }
// // Create indexed strings and concatenate
// result := MonadTraverseReduceWithIndex(..., iter,
// func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// },
// func(acc, s string) string { return acc + "," + s },
// "")
// // Returns: Effect containing "0:a,1:b,2:c"
func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -174,6 +408,36 @@ func MonadTraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB,
}, fof(initial))
}
// TraverseReduce is the curried version of MonadTraverseReduce, returning a function that
// traverses and reduces an iterator.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - transform: The effectful function to apply to each element
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - A function that takes an iterator and returns an effect containing the accumulated value
//
// Example (conceptual):
//
// sumParsedInts := TraverseReduce[...](fof, fmap, fap, parseInt, add, 0)
// iter := func(yield func(string) bool) { yield("1"); yield("2"); yield("3") }
// result := sumParsedInts(iter) // Some(6) or None if any parse fails
func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
@@ -188,6 +452,41 @@ func TraverseReduce[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB a
}
}
// TraverseReduceWithIndex is the curried version of MonadTraverseReduceWithIndex, returning a
// function that traverses and reduces an iterator with index tracking.
//
// This version is more suitable for partial application and function composition.
//
// Type Parameters:
// - GA: The input iterator type ~func(yield func(A) bool)
// - GB: The accumulator type
// - A: The input element type
// - B: The transformed element type
// - HKTB: The higher-kinded type representing an effect containing B
// - HKTAB: The higher-kinded type for a function from B to GB in the effect context
// - HKTRB: The higher-kinded type representing an effect containing GB
//
// Parameters:
// - fof: Lifts a GB value into the effect context
// - fmap: Maps a function over the effect to produce an effectful function
// - fap: Applies an effectful function to an effectful value
// - transform: The effectful function that takes (index, element) and returns an effect
// - reduce: The reducer function that combines the accumulator with a transformed element
// - initial: The initial accumulator value
//
// Returns:
// - A function that takes an iterator and returns an effect containing the accumulated value
//
// Example (conceptual):
//
// concatIndexed := TraverseReduceWithIndex[...](fof, fmap, fap,
// func(i int, s string) Effect[string] {
// return Pure(fmt.Sprintf("%d:%s", i, s))
// },
// func(acc, s string) string { return acc + "," + s },
// "")
// iter := func(yield func(string) bool) { yield("a"); yield("b") }
// result := concatIndexed(iter) // Effect containing "0:a,1:b"
func TraverseReduceWithIndex[GA ~func(yield func(A) bool), GB, A, B, HKTB, HKTAB, HKTRB any](
fof func(GB) HKTRB,
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,

View File

@@ -2,10 +2,23 @@ package iter
import (
I "iter"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/internal/pointed"
)
type (
// Seq represents Go's standard library iterator type for single values.
// It's an alias for iter.Seq[A] and provides interoperability with Go 1.23+ range-over-func.
Seq[A any] = I.Seq[A]
Endomorphism[A any] = endomorphism.Endomorphism[A]
OfType[A, HKT_A any] = pointed.OfType[A, HKT_A]
MapType[A, B, HKT_A, HKT_B any] = functor.MapType[A, B, HKT_A, HKT_B]
ApType[HKT_A, HKT_B, HKT_AB any] = apply.ApType[HKT_A, HKT_B, HKT_AB]
Kleisli[A, HKT_B any] = func(A) HKT_B
)

View File

@@ -61,18 +61,105 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
)
}
// TraverseIter applies an IO-returning function to each element of an iterator sequence
// and collects the results into an IO of an iterator sequence. Executes in parallel by default.
//
// This function is useful for processing lazy sequences where each element requires an IO operation.
// The resulting iterator is also lazy and will only execute IO operations when iterated.
//
// Type Parameters:
// - A: The input element type
// - B: The output element type
//
// Parameters:
// - f: A function that takes an element of type A and returns an IO computation producing B
//
// Returns:
// - A function that takes an iterator sequence of A and returns an IO of an iterator sequence of B
//
// Example:
//
// // Fetch user data for each ID in a sequence
// fetchUser := func(id int) io.IO[User] {
// return func() User {
// // Simulate fetching user from database
// return User{ID: id, Name: fmt.Sprintf("User%d", id)}
// }
// }
//
// // Create an iterator of user IDs
// userIDs := func(yield func(int) bool) {
// for _, id := range []int{1, 2, 3, 4, 5} {
// if !yield(id) { return }
// }
// }
//
// // Traverse the iterator, fetching each user
// fetchUsers := io.TraverseIter(fetchUser)
// usersIO := fetchUsers(userIDs)
//
// // Execute the IO to get the iterator of users
// users := usersIO()
// for user := range users {
// fmt.Printf("User: %v\n", user)
// }
func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
return INTI.Traverse[Seq[A]](
Map[B],
Of[Seq[B]],
Map[Seq[B]],
MonadAp[Seq[B]],
Ap[Seq[B]],
f,
)
}
// SequenceIter converts an iterator sequence of IO computations into an IO of an iterator sequence of results.
// All computations are executed in parallel by default when the resulting IO is invoked.
//
// This is a special case of TraverseIter where the transformation function is the identity.
// It "flips" the nesting of the iterator and IO types, executing all IO operations and collecting
// their results into a lazy iterator.
//
// Type Parameters:
// - A: The element type
//
// Parameters:
// - as: An iterator sequence where each element is an IO computation
//
// Returns:
// - An IO computation that, when executed, produces an iterator sequence of results
//
// Example:
//
// // Create an iterator of IO operations
// operations := func(yield func(io.IO[int]) bool) {
// yield(func() int { return 1 })
// yield(func() int { return 2 })
// yield(func() int { return 3 })
// }
//
// // Sequence the operations
// resultsIO := io.SequenceIter(operations)
//
// // Execute all IO operations and get the iterator of results
// results := resultsIO()
// for result := range results {
// fmt.Printf("Result: %d\n", result)
// }
//
// Note: The IO operations are executed when resultsIO() is called, not when iterating
// over the results. The resulting iterator is lazy but the computations have already
// been performed.
func SequenceIter[A any](as Seq[IO[A]]) IO[Seq[A]] {
return INTI.MonadSequence(
Map(INTI.Of[Seq[A]]),
ApplicativeMonoid(INTI.Monoid[Seq[A]]()),
as,
)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the index.
// Executes in parallel by default.
//

View File

@@ -1,9 +1,12 @@
package io
import (
"fmt"
"slices"
"strings"
"testing"
A "github.com/IBM/fp-go/v2/array"
"github.com/stretchr/testify/assert"
)
@@ -36,3 +39,264 @@ func TestTraverseCustomSlice(t *testing.T) {
assert.Equal(t, res(), []string{"A", "B"})
}
func TestTraverseIter(t *testing.T) {
t.Run("transforms all elements successfully", func(t *testing.T) {
// Create an iterator of strings
input := slices.Values(A.From("hello", "world", "test"))
// Transform each string to uppercase
transform := func(s string) IO[string] {
return Of(strings.ToUpper(s))
}
// Traverse the iterator
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
// Execute the IO and collect results
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Equal(t, []string{"HELLO", "WORLD", "TEST"}, collected)
})
t.Run("works with empty iterator", func(t *testing.T) {
// Create an empty iterator
input := func(yield func(string) bool) {}
transform := func(s string) IO[string] {
return Of(strings.ToUpper(s))
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Empty(t, collected)
})
t.Run("works with single element", func(t *testing.T) {
input := func(yield func(int) bool) {
yield(42)
}
transform := func(n int) IO[int] {
return Of(n * 2)
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []int
for n := range result {
collected = append(collected, n)
}
assert.Equal(t, []int{84}, collected)
})
t.Run("preserves order of elements", func(t *testing.T) {
input := func(yield func(int) bool) {
for i := 1; i <= 5; i++ {
if !yield(i) {
return
}
}
}
transform := func(n int) IO[string] {
return Of(fmt.Sprintf("item-%d", n))
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
expected := []string{"item-1", "item-2", "item-3", "item-4", "item-5"}
assert.Equal(t, expected, collected)
})
t.Run("handles complex transformations", func(t *testing.T) {
type User struct {
ID int
Name string
}
input := func(yield func(int) bool) {
for _, id := range []int{1, 2, 3} {
if !yield(id) {
return
}
}
}
transform := func(id int) IO[User] {
return Of(User{ID: id, Name: fmt.Sprintf("User%d", id)})
}
traverseFn := TraverseIter(transform)
resultIO := traverseFn(input)
result := resultIO()
var collected []User
for user := range result {
collected = append(collected, user)
}
expected := []User{
{ID: 1, Name: "User1"},
{ID: 2, Name: "User2"},
{ID: 3, Name: "User3"},
}
assert.Equal(t, expected, collected)
})
}
func TestSequenceIter(t *testing.T) {
t.Run("sequences multiple IO operations", func(t *testing.T) {
// Create an iterator of IO operations
input := slices.Values(A.From(Of(1), Of(2), Of(3)))
// Sequence the operations
resultIO := SequenceIter(input)
// Execute and collect results
result := resultIO()
var collected []int
for n := range result {
collected = append(collected, n)
}
assert.Equal(t, []int{1, 2, 3}, collected)
})
t.Run("works with empty iterator", func(t *testing.T) {
input := slices.Values(A.Empty[IO[string]]())
resultIO := SequenceIter(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Empty(t, collected)
})
t.Run("executes all IO operations", func(t *testing.T) {
// Track execution order
var executed []int
input := func(yield func(IO[int]) bool) {
yield(func() int {
executed = append(executed, 1)
return 10
})
yield(func() int {
executed = append(executed, 2)
return 20
})
yield(func() int {
executed = append(executed, 3)
return 30
})
}
resultIO := SequenceIter(input)
// Before execution, nothing should be executed
assert.Empty(t, executed)
// Execute the IO
result := resultIO()
// Collect results
var collected []int
for n := range result {
collected = append(collected, n)
}
// All operations should have been executed
assert.Equal(t, []int{1, 2, 3}, executed)
assert.Equal(t, []int{10, 20, 30}, collected)
})
t.Run("works with single IO operation", func(t *testing.T) {
input := func(yield func(IO[string]) bool) {
yield(Of("hello"))
}
resultIO := SequenceIter(input)
result := resultIO()
var collected []string
for s := range result {
collected = append(collected, s)
}
assert.Equal(t, []string{"hello"}, collected)
})
t.Run("preserves order of results", func(t *testing.T) {
input := func(yield func(IO[int]) bool) {
for i := 5; i >= 1; i-- {
n := i // capture loop variable
yield(func() int { return n * 10 })
}
}
resultIO := SequenceIter(input)
result := resultIO()
var collected []int
for n := range result {
collected = append(collected, n)
}
assert.Equal(t, []int{50, 40, 30, 20, 10}, collected)
})
t.Run("works with complex types", func(t *testing.T) {
type Result struct {
Value int
Label string
}
input := func(yield func(IO[Result]) bool) {
yield(Of(Result{Value: 1, Label: "first"}))
yield(Of(Result{Value: 2, Label: "second"}))
yield(Of(Result{Value: 3, Label: "third"}))
}
resultIO := SequenceIter(input)
result := resultIO()
var collected []Result
for r := range result {
collected = append(collected, r)
}
expected := []Result{
{Value: 1, Label: "first"},
{Value: 2, Label: "second"},
{Value: 3, Label: "third"},
}
assert.Equal(t, expected, collected)
})
}

54
v2/iterator/iter/last.go Normal file
View File

@@ -0,0 +1,54 @@
package iter
import (
"github.com/IBM/fp-go/v2/option"
)
// Last returns the last element from an [Iterator] wrapped in an [Option].
//
// This function retrieves the last element from the iterator by consuming the entire
// sequence. If the iterator contains at least one element, it returns Some(element).
// If the iterator is empty, it returns None.
//
// RxJS Equivalent: [last] - https://rxjs.dev/api/operators/last
//
// Type Parameters:
// - U: The type of elements in the iterator
//
// Parameters:
// - it: The input iterator to get the last element from
//
// Returns:
// - Option[U]: Some(last element) if the iterator is non-empty, None otherwise
//
// Example with non-empty sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// last := iter.Last(seq)
// // Returns: Some(5)
//
// Example with empty sequence:
//
// seq := iter.Empty[int]()
// last := iter.Last(seq)
// // Returns: None
//
// Example with filtered sequence:
//
// seq := iter.From(1, 2, 3, 4, 5)
// filtered := iter.Filter(func(x int) bool { return x < 4 })(seq)
// last := iter.Last(filtered)
// // Returns: Some(3)
func Last[U any](it Seq[U]) Option[U] {
var last U
found := false
for last = range it {
found = true
}
if !found {
return option.None[U]()
}
return option.Some(last)
}

View File

@@ -0,0 +1,305 @@
package iter
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestLast test getting the last element from a non-empty sequence
func TestLastSimple(t *testing.T) {
t.Run("returns last element from integer sequence", func(t *testing.T) {
seq := From(1, 2, 3)
last := Last(seq)
assert.Equal(t, O.Of(3), last)
})
t.Run("returns last element from string sequence", func(t *testing.T) {
seq := From("a", "b", "c")
last := Last(seq)
assert.Equal(t, O.Of("c"), last)
})
t.Run("returns last element from single element sequence", func(t *testing.T) {
seq := From(42)
last := Last(seq)
assert.Equal(t, O.Of(42), last)
})
t.Run("returns last element from large sequence", func(t *testing.T) {
seq := From(100, 200, 300, 400, 500)
last := Last(seq)
assert.Equal(t, O.Of(500), last)
})
}
// TestLastEmpty tests getting the last element from an empty sequence
func TestLastEmpty(t *testing.T) {
t.Run("returns None for empty integer sequence", func(t *testing.T) {
seq := Empty[int]()
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
t.Run("returns None for empty string sequence", func(t *testing.T) {
seq := Empty[string]()
last := Last(seq)
assert.Equal(t, O.None[string](), last)
})
t.Run("returns None for empty struct sequence", func(t *testing.T) {
type TestStruct struct {
Value int
}
seq := Empty[TestStruct]()
last := Last(seq)
assert.Equal(t, O.None[TestStruct](), last)
})
t.Run("returns None for empty sequence of functions", func(t *testing.T) {
type TestFunc func(int)
seq := Empty[TestFunc]()
last := Last(seq)
assert.Equal(t, O.None[TestFunc](), last)
})
}
// TestLastWithComplex tests Last with complex types
func TestLastWithComplex(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("returns last person", func(t *testing.T) {
seq := From(
Person{"Alice", 30},
Person{"Bob", 25},
Person{"Charlie", 35},
)
last := Last(seq)
expected := O.Of(Person{"Charlie", 35})
assert.Equal(t, expected, last)
})
t.Run("returns last pointer", func(t *testing.T) {
p1 := &Person{"Alice", 30}
p2 := &Person{"Bob", 25}
seq := From(p1, p2)
last := Last(seq)
assert.Equal(t, O.Of(p2), last)
})
}
func TestLastWithFunctions(t *testing.T) {
t.Run("return function", func(t *testing.T) {
want := "last"
f1 := function.Constant("first")
f2 := function.Constant("last")
seq := From(f1, f2)
getLast := function.Flow2(
Last,
O.Map(funcReader),
)
assert.Equal(t, O.Of(want), getLast(seq))
})
}
func funcReader(f func() string) string {
return f()
}
// TestLastWithChan tests Last with channels
func TestLastWithChan(t *testing.T) {
t.Run("return function", func(t *testing.T) {
want := 30
seq := From(intChan(10),
intChan(20),
intChan(want))
getLast := function.Flow2(
Last,
O.Map(chanReader[int]),
)
assert.Equal(t, O.Of(want), getLast(seq))
})
}
func chanReader[T any](c <-chan T) T {
return <-c
}
func intChan(val int) <-chan int {
ch := make(chan int, 1)
ch <- val
close(ch)
return ch
}
// TestLastWithChainedOperations tests Last with multiple chained operations
func TestLastWithChainedOperations(t *testing.T) {
t.Run("chains filter, map, and last", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
filtered := MonadFilter(seq, N.MoreThan(5))
mapped := MonadMap(filtered, N.Mul(10))
result := Last(mapped)
assert.Equal(t, O.Of(100), result)
})
t.Run("chains map and filter", func(t *testing.T) {
seq := From(1, 2, 3, 4, 5)
mapped := MonadMap(seq, N.Mul(2))
filtered := MonadFilter(mapped, N.MoreThan(5))
result := Last(filtered)
assert.Equal(t, O.Of(10), result)
})
}
// TestLastWithReplicate tests Last with replicated values
func TestLastWithReplicate(t *testing.T) {
t.Run("returns last from replicated sequence", func(t *testing.T) {
seq := Replicate(5, 42)
last := Last(seq)
assert.Equal(t, O.Of(42), last)
})
t.Run("returns None from zero replications", func(t *testing.T) {
seq := Replicate(0, 42)
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithMakeBy tests Last with MakeBy
func TestLastWithMakeBy(t *testing.T) {
t.Run("returns last generated element", func(t *testing.T) {
seq := MakeBy(5, func(i int) int { return i * i })
last := Last(seq)
assert.Equal(t, O.Of(16), last)
})
t.Run("returns None for zero elements", func(t *testing.T) {
seq := MakeBy(0, F.Identity[int])
last := Last(seq)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithPrepend tests Last with Prepend
func TestLastWithPrepend(t *testing.T) {
t.Run("returns last element, not prepended", func(t *testing.T) {
seq := From(2, 3, 4)
prepended := Prepend(1)(seq)
last := Last(prepended)
assert.Equal(t, O.Of(4), last)
})
t.Run("returns prepended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
prepended := Prepend(42)(seq)
last := Last(prepended)
assert.Equal(t, O.Of(42), last)
})
}
// TestLastWithAppend tests Last with Append
func TestLastWithAppend(t *testing.T) {
t.Run("returns appended element", func(t *testing.T) {
seq := From(1, 2, 3)
appended := Append(4)(seq)
last := Last(appended)
assert.Equal(t, O.Of(4), last)
})
t.Run("returns appended element from empty sequence", func(t *testing.T) {
seq := Empty[int]()
appended := Append(42)(seq)
last := Last(appended)
assert.Equal(t, O.Of(42), last)
})
}
// TestLastWithChain tests Last with Chain (flatMap)
func TestLastWithChain(t *testing.T) {
t.Run("returns last from chained sequence", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return From(x, x*10)
})
last := Last(chained)
assert.Equal(t, O.Of(30), last)
})
t.Run("returns None when chain produces empty", func(t *testing.T) {
seq := From(1, 2, 3)
chained := MonadChain(seq, func(x int) Seq[int] {
return Empty[int]()
})
last := Last(chained)
assert.Equal(t, O.None[int](), last)
})
}
// TestLastWithFlatten tests Last with Flatten
func TestLastWithFlatten(t *testing.T) {
t.Run("returns last from flattened sequence", func(t *testing.T) {
nested := From(From(1, 2), From(3, 4), From(5))
flattened := Flatten(nested)
last := Last(flattened)
assert.Equal(t, O.Of(5), last)
})
t.Run("returns None from empty nested sequence", func(t *testing.T) {
nested := Empty[Seq[int]]()
flattened := Flatten(nested)
last := Last(flattened)
assert.Equal(t, O.None[int](), last)
})
}
// Example tests for documentation
func ExampleLast() {
seq := From(1, 2, 3, 4, 5)
last := Last(seq)
if value, ok := O.Unwrap(last); ok {
fmt.Printf("Last element: %d\n", value)
}
// Output: Last element: 5
}
func ExampleLast_empty() {
seq := Empty[int]()
last := Last(seq)
if _, ok := O.Unwrap(last); !ok {
fmt.Println("Sequence is empty")
}
// Output: Sequence is empty
}
func ExampleLast_functions() {
f1 := function.Constant("first")
f2 := function.Constant("middle")
f3 := function.Constant("last")
seq := From(f1, f2, f3)
last := Last(seq)
if fn, ok := O.Unwrap(last); ok {
result := fn()
fmt.Printf("Last function result: %s\n", result)
}
// Output: Last function result: last
}

View File

@@ -17,23 +17,28 @@
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v2"
C "github.com/urfave/cli/v3"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
app := &C.App{
app := &C.Command{
Name: "fp-go",
Usage: "Code generation for fp-go",
Commands: cli.Commands(),
}
if err := app.Run(os.Args); err != nil {
if err := app.Run(ctx, os.Args); err != nil {
log.Fatal(err)
}
}

View File

@@ -221,7 +221,7 @@ Lenses can be automatically generated using the `fp-go` CLI tool and a simple an
1. **Annotate your struct** with the `fp-go:Lens` comment:
```go
//go:generate go run github.com/IBM/fp-go/v2/main.go lens --dir . --filename gen_lens.go
//go:generate go run github.com/IBM/fp-go/v2 lens --dir . --filename gen_lens.go
// fp-go:Lens
type Person struct {
@@ -230,8 +230,16 @@ type Person struct {
Email string
Phone *string // Optional field
}
// fp-go:Lens
type Config struct {
PublicField string
privateField int // Unexported fields are supported!
}
```
**Note:** The generator supports both exported (uppercase) and unexported (lowercase) fields. Generated lenses for unexported fields will have lowercase names and can only be used within the same package as the struct.
2. **Run `go generate`**:
```bash
@@ -268,6 +276,7 @@ The generator supports:
- ✅ Embedded structs (fields are promoted)
- ✅ Optional fields (pointers and `omitempty` tags)
- ✅ Custom package imports
-**Unexported fields** (lowercase names) - lenses will have lowercase names matching the field names
See [samples/lens](../samples/lens) for complete examples.
@@ -293,13 +302,23 @@ More specific optics can be converted to more general ones.
## 📦 Package Structure
### Core Optics
- **[optics/lens](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens)**: Lenses for product types (structs)
- **[optics/prism](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism)**: Prisms for sum types ([`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either), [`Result`](https://pkg.go.dev/github.com/IBM/fp-go/v2/result), etc.)
- **[optics/iso](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso)**: Isomorphisms for equivalent types
- **[optics/optional](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional)**: Optional optics for maybe values
- **[optics/traversal](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal)**: Traversals for multiple values
Each package includes specialized sub-packages for common patterns:
### Utilities
- **[optics/builder](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/builder)**: Builder pattern for constructing complex optics
- **[optics/codec](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/codec)**: Type-safe encoding/decoding with validation
- Provides `Type[A, O, I]` for bidirectional transformations with validation
- Includes codecs for primitives (String, Int, Bool), collections (Array), and sum types (Either)
- Supports refinement types and codec composition via `Pipe`
- Integrates validation errors with context tracking
### Specialized Sub-packages
Each core optics package includes specialized sub-packages for common patterns:
- **array**: Optics for arrays/slices
- **either**: Optics for [`Either`](https://pkg.go.dev/github.com/IBM/fp-go/v2/either) types
- **option**: Optics for [`Option`](https://pkg.go.dev/github.com/IBM/fp-go/v2/option) types

340
v2/optics/codec/codecs.go Normal file
View File

@@ -0,0 +1,340 @@
// 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 codec provides pre-built codec implementations for common types.
// This package includes codecs for URL parsing, date/time formatting, and other
// standard data transformations that require bidirectional encoding/decoding.
//
// The codecs in this package follow functional programming principles and integrate
// with the validation framework to provide type-safe, composable transformations.
package codec
import (
"net/url"
"regexp"
"strconv"
"time"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/reader"
)
// validateFromParser creates a validation function from a parser that may fail.
// It wraps a parser function that returns (A, error) into a Validate[I, A] function
// that integrates with the validation framework.
//
// The returned validation function:
// - Calls the parser with the input value
// - On success: returns a successful validation containing the parsed value
// - On failure: returns a validation failure with the error message and cause
//
// Type Parameters:
// - A: The target type to parse into
// - I: The input type to parse from
//
// Parameters:
// - parser: A function that attempts to parse input I into type A, returning an error on failure
//
// Returns:
// - A Validate[I, A] function that can be used in codec construction
//
// Example:
//
// // Create a validator for parsing integers from strings
// intValidator := validateFromParser(strconv.Atoi)
// // Use in a codec
// intCodec := MakeType("Int", Is[int](), intValidator, strconv.Itoa)
func validateFromParser[A, I any](parser func(I) (A, error)) Validate[I, A] {
return func(i I) Decode[Context, A] {
// Attempt to parse the input value
a, err := parser(i)
if err != nil {
// On error, create a validation failure with the error details
return validation.FailureWithError[A](i, err.Error())(err)
}
// On success, wrap the parsed value in a successful validation
return reader.Of[Context](validation.Success(a))
}
}
// URL creates a bidirectional codec for URL parsing and formatting.
// This codec can parse strings into *url.URL and encode *url.URL back to strings.
//
// The codec:
// - Decodes: Parses a string using url.Parse, validating URL syntax
// - Encodes: Converts a *url.URL to its string representation using String()
// - Validates: Ensures the input string is a valid URL format
//
// Returns:
// - A Type[*url.URL, string, string] codec that handles URL transformations
//
// Example:
//
// urlCodec := URL()
//
// // Decode a string to URL
// validation := urlCodec.Decode("https://example.com/path?query=value")
// // validation is Right(*url.URL{...})
//
// // Encode a URL to string
// u, _ := url.Parse("https://example.com")
// str := urlCodec.Encode(u)
// // str is "https://example.com"
//
// // Invalid URL fails validation
// validation := urlCodec.Decode("not a valid url")
// // validation is Left(ValidationError{...})
func URL() Type[*url.URL, string, string] {
return MakeType(
"URL",
Is[*url.URL](),
validateFromParser(url.Parse),
(*url.URL).String,
)
}
// Date creates a bidirectional codec for date/time parsing and formatting with a specific layout.
// This codec uses Go's time.Parse and time.Format with the provided layout string.
//
// The codec:
// - Decodes: Parses a string into time.Time using the specified layout
// - Encodes: Formats a time.Time back to a string using the same layout
// - Validates: Ensures the input string matches the expected date/time format
//
// Parameters:
// - layout: The time layout string (e.g., "2006-01-02", time.RFC3339)
// See time package documentation for layout format details
//
// Returns:
// - A Type[time.Time, string, string] codec that handles date/time transformations
//
// Example:
//
// // Create a codec for ISO 8601 dates
// dateCodec := Date("2006-01-02")
//
// // Decode a string to time.Time
// validation := dateCodec.Decode("2024-03-15")
// // validation is Right(time.Time{...})
//
// // Encode a time.Time to string
// t := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
// str := dateCodec.Encode(t)
// // str is "2024-03-15"
//
// // Create a codec for RFC3339 timestamps
// timestampCodec := Date(time.RFC3339)
// validation := timestampCodec.Decode("2024-03-15T10:30:00Z")
//
// // Invalid format fails validation
// validation := dateCodec.Decode("15-03-2024")
// // validation is Left(ValidationError{...})
func Date(layout string) Type[time.Time, string, string] {
return MakeType(
"Date",
Is[time.Time](),
validateFromParser(func(s string) (time.Time, error) { return time.Parse(layout, s) }),
F.Bind2nd(time.Time.Format, layout),
)
}
// Regex creates a bidirectional codec for regex pattern matching with capture groups.
// This codec can match strings against a regular expression pattern and extract capture groups,
// then reconstruct the original string from the match data.
//
// The codec uses prism.Match which contains:
// - Before: Text before the match
// - Groups: Capture groups (index 0 is the full match, 1+ are numbered capture groups)
// - After: Text after the match
//
// The codec:
// - Decodes: Attempts to match the regex against the input string
// - Encodes: Reconstructs the original string from a Match structure
// - Validates: Ensures the string matches the regex pattern
//
// Parameters:
// - re: A compiled regular expression pattern
//
// Returns:
// - A Type[prism.Match, string, string] codec that handles regex matching
//
// Example:
//
// // Create a codec for matching numbers in text
// numberRegex := regexp.MustCompile(`\d+`)
// numberCodec := Regex(numberRegex)
//
// // Decode a string with a number
// validation := numberCodec.Decode("Price: 42 dollars")
// // validation is Right(Match{Before: "Price: ", Groups: []string{"42"}, After: " dollars"})
//
// // Encode a Match back to string
// match := prism.Match{Before: "Price: ", Groups: []string{"42"}, After: " dollars"}
// str := numberCodec.Encode(match)
// // str is "Price: 42 dollars"
//
// // Non-matching string fails validation
// validation := numberCodec.Decode("no numbers here")
// // validation is Left(ValidationError{...})
func Regex(re *regexp.Regexp) Type[prism.Match, string, string] {
return FromRefinement(prism.RegexMatcher(re))
}
// RegexNamed creates a bidirectional codec for regex pattern matching with named capture groups.
// This codec can match strings against a regular expression with named groups and extract them
// by name, then reconstruct the original string from the match data.
//
// The codec uses prism.NamedMatch which contains:
// - Before: Text before the match
// - Groups: Map of named capture groups (name -> matched text)
// - Full: The complete matched text
// - After: Text after the match
//
// The codec:
// - Decodes: Attempts to match the regex against the input string
// - Encodes: Reconstructs the original string from a NamedMatch structure
// - Validates: Ensures the string matches the regex pattern with named groups
//
// Parameters:
// - re: A compiled regular expression with named capture groups (e.g., `(?P<name>pattern)`)
//
// Returns:
// - A Type[prism.NamedMatch, string, string] codec that handles named regex matching
//
// Example:
//
// // Create a codec for matching email addresses with named groups
// emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
// emailCodec := RegexNamed(emailRegex)
//
// // Decode an email string
// validation := emailCodec.Decode("john@example.com")
// // validation is Right(NamedMatch{
// // Before: "",
// // Groups: map[string]string{"user": "john", "domain": "example.com"},
// // Full: "john@example.com",
// // After: ""
// // })
//
// // Encode a NamedMatch back to string
// match := prism.NamedMatch{
// Before: "",
// Groups: map[string]string{"user": "john", "domain": "example.com"},
// Full: "john@example.com",
// After: "",
// }
// str := emailCodec.Encode(match)
// // str is "john@example.com"
//
// // Non-matching string fails validation
// validation := emailCodec.Decode("not-an-email")
// // validation is Left(ValidationError{...})
func RegexNamed(re *regexp.Regexp) Type[prism.NamedMatch, string, string] {
return FromRefinement(prism.RegexNamedMatcher(re))
}
// IntFromString creates a bidirectional codec for parsing integers from strings.
// This codec converts string representations of integers to int values and vice versa.
//
// The codec:
// - Decodes: Parses a string to an int using strconv.Atoi
// - Encodes: Converts an int to its string representation using strconv.Itoa
// - Validates: Ensures the string contains a valid integer (base 10)
//
// The codec accepts integers in base 10 format, with optional leading sign (+/-).
// It does not accept hexadecimal, octal, or other number formats.
//
// Returns:
// - A Type[int, string, string] codec that handles int/string conversions
//
// Example:
//
// intCodec := IntFromString()
//
// // Decode a valid integer string
// validation := intCodec.Decode("42")
// // validation is Right(42)
//
// // Decode negative integer
// validation := intCodec.Decode("-123")
// // validation is Right(-123)
//
// // Encode an integer to string
// str := intCodec.Encode(42)
// // str is "42"
//
// // Invalid integer string fails validation
// validation := intCodec.Decode("not a number")
// // validation is Left(ValidationError{...})
//
// // Floating point fails validation
// validation := intCodec.Decode("3.14")
// // validation is Left(ValidationError{...})
func IntFromString() Type[int, string, string] {
return MakeType(
"IntFromString",
Is[int](),
validateFromParser(strconv.Atoi),
strconv.Itoa,
)
}
// Int64FromString creates a bidirectional codec for parsing 64-bit integers from strings.
// This codec converts string representations of integers to int64 values and vice versa.
//
// The codec:
// - Decodes: Parses a string to an int64 using strconv.ParseInt with base 10
// - Encodes: Converts an int64 to its string representation
// - Validates: Ensures the string contains a valid 64-bit integer (base 10)
//
// The codec accepts integers in base 10 format, with optional leading sign (+/-).
// It supports the full range of int64 values (-9223372036854775808 to 9223372036854775807).
//
// Returns:
// - A Type[int64, string, string] codec that handles int64/string conversions
//
// Example:
//
// int64Codec := Int64FromString()
//
// // Decode a valid integer string
// validation := int64Codec.Decode("9223372036854775807")
// // validation is Right(9223372036854775807)
//
// // Decode negative integer
// validation := int64Codec.Decode("-9223372036854775808")
// // validation is Right(-9223372036854775808)
//
// // Encode an int64 to string
// str := int64Codec.Encode(42)
// // str is "42"
//
// // Invalid integer string fails validation
// validation := int64Codec.Decode("not a number")
// // validation is Left(ValidationError{...})
//
// // Out of range value fails validation
// validation := int64Codec.Decode("9223372036854775808")
// // validation is Left(ValidationError{...})
func Int64FromString() Type[int64, string, string] {
return MakeType(
"Int64FromString",
Is[int64](),
validateFromParser(func(s string) (int64, error) { return strconv.ParseInt(s, 10, 64) }),
prism.ParseInt64().ReverseGet,
)
}

View File

@@ -0,0 +1,908 @@
// 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 codec
import (
"net/url"
"regexp"
"testing"
"time"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/codec/validation"
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/reader"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestURL(t *testing.T) {
urlCodec := URL()
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, *url.URL](nil))
t.Run("decodes valid HTTP URL", func(t *testing.T) {
result := urlCodec.Decode("https://example.com/path?query=value")
assert.True(t, either.IsRight(result), "should successfully decode valid URL")
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "https", parsedURL.Scheme)
assert.Equal(t, "example.com", parsedURL.Host)
assert.Equal(t, "/path", parsedURL.Path)
assert.Equal(t, "query=value", parsedURL.RawQuery)
})
t.Run("decodes valid HTTP URL without path", func(t *testing.T) {
result := urlCodec.Decode("https://example.com")
assert.True(t, either.IsRight(result))
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "https", parsedURL.Scheme)
assert.Equal(t, "example.com", parsedURL.Host)
})
t.Run("decodes URL with port", func(t *testing.T) {
result := urlCodec.Decode("http://localhost:8080/api")
assert.True(t, either.IsRight(result))
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "http", parsedURL.Scheme)
assert.Equal(t, "localhost:8080", parsedURL.Host)
assert.Equal(t, "/api", parsedURL.Path)
})
t.Run("decodes URL with fragment", func(t *testing.T) {
result := urlCodec.Decode("https://example.com/page#section")
assert.True(t, either.IsRight(result))
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "section", parsedURL.Fragment)
})
t.Run("decodes relative URL", func(t *testing.T) {
result := urlCodec.Decode("/path/to/resource")
assert.True(t, either.IsRight(result))
parsedURL := getOrElseNull(result)
require.NotNil(t, parsedURL)
assert.Equal(t, "/path/to/resource", parsedURL.Path)
})
t.Run("fails to decode invalid URL", func(t *testing.T) {
result := urlCodec.Decode("not a valid url ://")
assert.True(t, either.IsLeft(result), "should fail to decode invalid URL")
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(*url.URL) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("fails to decode URL with invalid characters", func(t *testing.T) {
result := urlCodec.Decode("http://example.com/path with spaces")
// Note: url.Parse actually handles spaces, so let's test a truly invalid URL
result = urlCodec.Decode("ht!tp://invalid")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes URL to string", func(t *testing.T) {
parsedURL, err := url.Parse("https://example.com/path?query=value")
require.NoError(t, err)
encoded := urlCodec.Encode(parsedURL)
assert.Equal(t, "https://example.com/path?query=value", encoded)
})
t.Run("encodes URL with fragment", func(t *testing.T) {
parsedURL, err := url.Parse("https://example.com/page#section")
require.NoError(t, err)
encoded := urlCodec.Encode(parsedURL)
assert.Equal(t, "https://example.com/page#section", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "https://example.com/path?key=value&foo=bar#fragment"
// Decode
decodeResult := urlCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
parsedURL := getOrElseNull(decodeResult)
// Encode
encoded := urlCodec.Encode(parsedURL)
assert.Equal(t, original, encoded)
})
t.Run("codec has correct name", func(t *testing.T) {
assert.Equal(t, "URL", urlCodec.Name())
})
}
func TestDate(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors, time.Time](time.Time{}))
t.Run("ISO 8601 date format", func(t *testing.T) {
dateCodec := Date("2006-01-02")
t.Run("decodes valid date", func(t *testing.T) {
result := dateCodec.Decode("2024-03-15")
assert.True(t, either.IsRight(result))
parsedDate := getOrElseNull(result)
assert.Equal(t, 2024, parsedDate.Year())
assert.Equal(t, time.March, parsedDate.Month())
assert.Equal(t, 15, parsedDate.Day())
})
t.Run("fails to decode invalid date format", func(t *testing.T) {
result := dateCodec.Decode("15-03-2024")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(time.Time) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("fails to decode invalid date", func(t *testing.T) {
result := dateCodec.Decode("2024-13-45")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode non-date string", func(t *testing.T) {
result := dateCodec.Decode("not a date")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes date to string", func(t *testing.T) {
date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
encoded := dateCodec.Encode(date)
assert.Equal(t, "2024-03-15", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "2024-12-25"
// Decode
decodeResult := dateCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
parsedDate := getOrElseNull(decodeResult)
// Encode
encoded := dateCodec.Encode(parsedDate)
assert.Equal(t, original, encoded)
})
})
t.Run("RFC3339 timestamp format", func(t *testing.T) {
timestampCodec := Date(time.RFC3339)
t.Run("decodes valid RFC3339 timestamp", func(t *testing.T) {
result := timestampCodec.Decode("2024-03-15T10:30:00Z")
assert.True(t, either.IsRight(result))
parsedTime := getOrElseNull(result)
assert.Equal(t, 2024, parsedTime.Year())
assert.Equal(t, time.March, parsedTime.Month())
assert.Equal(t, 15, parsedTime.Day())
assert.Equal(t, 10, parsedTime.Hour())
assert.Equal(t, 30, parsedTime.Minute())
assert.Equal(t, 0, parsedTime.Second())
})
t.Run("decodes RFC3339 with timezone offset", func(t *testing.T) {
result := timestampCodec.Decode("2024-03-15T10:30:00+01:00")
assert.True(t, either.IsRight(result))
parsedTime := getOrElseNull(result)
assert.Equal(t, 2024, parsedTime.Year())
assert.Equal(t, time.March, parsedTime.Month())
assert.Equal(t, 15, parsedTime.Day())
})
t.Run("fails to decode invalid RFC3339", func(t *testing.T) {
result := timestampCodec.Decode("2024-03-15 10:30:00")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes timestamp to RFC3339 string", func(t *testing.T) {
timestamp := time.Date(2024, 3, 15, 10, 30, 0, 0, time.UTC)
encoded := timestampCodec.Encode(timestamp)
assert.Equal(t, "2024-03-15T10:30:00Z", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "2024-12-25T15:45:30Z"
// Decode
decodeResult := timestampCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
parsedTime := getOrElseNull(decodeResult)
// Encode
encoded := timestampCodec.Encode(parsedTime)
assert.Equal(t, original, encoded)
})
})
t.Run("custom date format", func(t *testing.T) {
customCodec := Date("02/01/2006")
t.Run("decodes custom format", func(t *testing.T) {
result := customCodec.Decode("15/03/2024")
assert.True(t, either.IsRight(result))
parsedDate := getOrElseNull(result)
assert.Equal(t, 2024, parsedDate.Year())
assert.Equal(t, time.March, parsedDate.Month())
assert.Equal(t, 15, parsedDate.Day())
})
t.Run("encodes to custom format", func(t *testing.T) {
date := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
encoded := customCodec.Encode(date)
assert.Equal(t, "15/03/2024", encoded)
})
})
t.Run("codec has correct name", func(t *testing.T) {
dateCodec := Date("2006-01-02")
assert.Equal(t, "Date", dateCodec.Name())
})
}
func TestValidateFromParser(t *testing.T) {
t.Run("successful parsing", func(t *testing.T) {
// Create a simple parser that always succeeds
parser := func(s string) (int, error) {
return 42, nil
}
validator := validateFromParser(parser)
decode := validator("test")
// Execute with empty context
result := decode(validation.Context{})
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("failed parsing", func(t *testing.T) {
// Create a parser that always fails
parser := func(s string) (int, error) {
return 0, assert.AnError
}
validator := validateFromParser(parser)
decode := validator("test")
// Execute with empty context
result := decode(validation.Context{})
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
// Check that the error contains the input value
if len(errors) > 0 {
assert.Equal(t, "test", errors[0].Value)
}
})
t.Run("parser with context", func(t *testing.T) {
parser := func(s string) (string, error) {
if s == "" {
return "", assert.AnError
}
return s, nil
}
validator := validateFromParser(parser)
// Test with context
ctx := validation.Context{
{Key: "field", Type: "string"},
}
decode := validator("")
result := decode(ctx)
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(string) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
// Verify context is preserved
if len(errors) > 0 {
assert.Equal(t, ctx, errors[0].Context)
}
})
}
func TestRegex(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](prism.Match{}))
t.Run("simple number pattern", func(t *testing.T) {
numberRegex := regexp.MustCompile(`\d+`)
regexCodec := Regex(numberRegex)
t.Run("decodes string with number", func(t *testing.T) {
result := regexCodec.Decode("Price: 42 dollars")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "Price: ", match.Before)
assert.Equal(t, []string{"42"}, match.Groups)
assert.Equal(t, " dollars", match.After)
})
t.Run("decodes number at start", func(t *testing.T) {
result := regexCodec.Decode("123 items")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, []string{"123"}, match.Groups)
assert.Equal(t, " items", match.After)
})
t.Run("decodes number at end", func(t *testing.T) {
result := regexCodec.Decode("Total: 999")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "Total: ", match.Before)
assert.Equal(t, []string{"999"}, match.Groups)
assert.Equal(t, "", match.After)
})
t.Run("fails to decode string without number", func(t *testing.T) {
result := regexCodec.Decode("no numbers here")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(prism.Match) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("encodes Match to string", func(t *testing.T) {
match := prism.Match{
Before: "Price: ",
Groups: []string{"42"},
After: " dollars",
}
encoded := regexCodec.Encode(match)
assert.Equal(t, "Price: 42 dollars", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "Count: 789 items"
// Decode
decodeResult := regexCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
match := getOrElseNull(decodeResult)
// Encode
encoded := regexCodec.Encode(match)
assert.Equal(t, original, encoded)
})
})
t.Run("pattern with capture groups", func(t *testing.T) {
// Pattern to match word followed by number
wordNumberRegex := regexp.MustCompile(`(\w+)(\d+)`)
regexCodec := Regex(wordNumberRegex)
t.Run("decodes with capture groups", func(t *testing.T) {
result := regexCodec.Decode("item42")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "", match.Before)
// Groups contains the full match and capture groups
require.NotEmpty(t, match.Groups)
assert.Equal(t, "item42", match.Groups[0])
// Verify we have capture groups
if len(match.Groups) > 1 {
assert.Contains(t, match.Groups[1], "item")
assert.Contains(t, match.Groups[len(match.Groups)-1], "2")
}
assert.Equal(t, "", match.After)
})
})
t.Run("codec name contains pattern info", func(t *testing.T) {
numberRegex := regexp.MustCompile(`\d+`)
regexCodec := Regex(numberRegex)
// The name is generated by FromRefinement and includes the pattern
assert.Contains(t, regexCodec.Name(), "FromRefinement")
})
}
func TestRegexNamed(t *testing.T) {
getOrElseNull := either.GetOrElse(reader.Of[validation.Errors](prism.NamedMatch{}))
t.Run("email pattern with named groups", func(t *testing.T) {
emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
emailCodec := RegexNamed(emailRegex)
t.Run("decodes valid email", func(t *testing.T) {
result := emailCodec.Decode("john@example.com")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, "john@example.com", match.Full)
assert.Equal(t, "", match.After)
require.NotNil(t, match.Groups)
assert.Equal(t, "john", match.Groups["user"])
assert.Equal(t, "example.com", match.Groups["domain"])
})
t.Run("decodes email with surrounding text", func(t *testing.T) {
result := emailCodec.Decode("Contact: alice@test.org for info")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "Contact: ", match.Before)
assert.Equal(t, "alice@test.org", match.Full)
assert.Equal(t, " for info", match.After)
assert.Equal(t, "alice", match.Groups["user"])
assert.Equal(t, "test.org", match.Groups["domain"])
})
t.Run("fails to decode invalid email", func(t *testing.T) {
result := emailCodec.Decode("not-an-email")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(prism.NamedMatch) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("encodes NamedMatch to string", func(t *testing.T) {
match := prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "bob", "domain": "example.com"},
Full: "bob@example.com",
After: "",
}
encoded := emailCodec.Encode(match)
assert.Equal(t, "Email: bob@example.com", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "Contact: support@company.io"
// Decode
decodeResult := emailCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
match := getOrElseNull(decodeResult)
// Encode
encoded := emailCodec.Encode(match)
assert.Equal(t, original, encoded)
})
})
t.Run("phone pattern with named groups", func(t *testing.T) {
phoneRegex := regexp.MustCompile(`(?P<area>\d{3})-(?P<prefix>\d{3})-(?P<line>\d{4})`)
phoneCodec := RegexNamed(phoneRegex)
t.Run("decodes valid phone number", func(t *testing.T) {
result := phoneCodec.Decode("555-123-4567")
assert.True(t, either.IsRight(result))
match := getOrElseNull(result)
assert.Equal(t, "555-123-4567", match.Full)
assert.Equal(t, "555", match.Groups["area"])
assert.Equal(t, "123", match.Groups["prefix"])
assert.Equal(t, "4567", match.Groups["line"])
})
t.Run("fails to decode invalid phone format", func(t *testing.T) {
result := phoneCodec.Decode("123-45-6789")
assert.True(t, either.IsLeft(result))
})
})
t.Run("codec name contains refinement info", func(t *testing.T) {
emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
emailCodec := RegexNamed(emailRegex)
// The name is generated by FromRefinement
assert.Contains(t, emailCodec.Name(), "FromRefinement")
})
}
func TestIntFromString(t *testing.T) {
intCodec := IntFromString()
t.Run("decodes positive integer", func(t *testing.T) {
result := intCodec.Decode("42")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 42, value)
})
t.Run("decodes negative integer", func(t *testing.T) {
result := intCodec.Decode("-123")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, -123, value)
})
t.Run("decodes zero", func(t *testing.T) {
result := intCodec.Decode("0")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return -1 },
F.Identity[int],
)
assert.Equal(t, 0, value)
})
t.Run("decodes integer with plus sign", func(t *testing.T) {
result := intCodec.Decode("+456")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
assert.Equal(t, 456, value)
})
t.Run("fails to decode floating point", func(t *testing.T) {
result := intCodec.Decode("3.14")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("fails to decode non-numeric string", func(t *testing.T) {
result := intCodec.Decode("not a number")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode empty string", func(t *testing.T) {
result := intCodec.Decode("")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode hexadecimal", func(t *testing.T) {
result := intCodec.Decode("0xFF")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode with whitespace", func(t *testing.T) {
result := intCodec.Decode(" 42 ")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes positive integer", func(t *testing.T) {
encoded := intCodec.Encode(42)
assert.Equal(t, "42", encoded)
})
t.Run("encodes negative integer", func(t *testing.T) {
encoded := intCodec.Encode(-123)
assert.Equal(t, "-123", encoded)
})
t.Run("encodes zero", func(t *testing.T) {
encoded := intCodec.Encode(0)
assert.Equal(t, "0", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "9876"
// Decode
decodeResult := intCodec.Decode(original)
require.True(t, either.IsRight(decodeResult))
value := either.MonadFold(decodeResult,
func(validation.Errors) int { return 0 },
F.Identity[int],
)
// Encode
encoded := intCodec.Encode(value)
assert.Equal(t, original, encoded)
})
t.Run("codec has correct name", func(t *testing.T) {
assert.Equal(t, "IntFromString", intCodec.Name())
})
}
func TestInt64FromString(t *testing.T) {
int64Codec := Int64FromString()
t.Run("decodes positive int64", func(t *testing.T) {
result := int64Codec.Decode("9223372036854775807")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int64 { return 0 },
F.Identity[int64],
)
assert.Equal(t, int64(9223372036854775807), value)
})
t.Run("decodes negative int64", func(t *testing.T) {
result := int64Codec.Decode("-9223372036854775808")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int64 { return 0 },
F.Identity[int64],
)
assert.Equal(t, int64(-9223372036854775808), value)
})
t.Run("decodes zero", func(t *testing.T) {
result := int64Codec.Decode("0")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int64 { return -1 },
F.Identity[int64],
)
assert.Equal(t, int64(0), value)
})
t.Run("decodes small int64", func(t *testing.T) {
result := int64Codec.Decode("42")
assert.True(t, either.IsRight(result))
value := either.MonadFold(result,
func(validation.Errors) int64 { return 0 },
F.Identity[int64],
)
assert.Equal(t, int64(42), value)
})
t.Run("fails to decode out of range positive", func(t *testing.T) {
result := int64Codec.Decode("9223372036854775808")
assert.True(t, either.IsLeft(result))
errors := either.MonadFold(result,
F.Identity[validation.Errors],
func(int64) validation.Errors { return nil },
)
require.NotNil(t, errors)
assert.NotEmpty(t, errors)
})
t.Run("fails to decode out of range negative", func(t *testing.T) {
result := int64Codec.Decode("-9223372036854775809")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode floating point", func(t *testing.T) {
result := int64Codec.Decode("3.14")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode non-numeric string", func(t *testing.T) {
result := int64Codec.Decode("not a number")
assert.True(t, either.IsLeft(result))
})
t.Run("fails to decode empty string", func(t *testing.T) {
result := int64Codec.Decode("")
assert.True(t, either.IsLeft(result))
})
t.Run("encodes positive int64", func(t *testing.T) {
encoded := int64Codec.Encode(9223372036854775807)
assert.Equal(t, "9223372036854775807", encoded)
})
t.Run("encodes negative int64", func(t *testing.T) {
encoded := int64Codec.Encode(-9223372036854775808)
assert.Equal(t, "-9223372036854775808", encoded)
})
t.Run("encodes zero", func(t *testing.T) {
encoded := int64Codec.Encode(0)
assert.Equal(t, "0", encoded)
})
t.Run("encodes small int64", func(t *testing.T) {
encoded := int64Codec.Encode(42)
assert.Equal(t, "42", encoded)
})
t.Run("round-trip encoding and decoding", func(t *testing.T) {
original := "1234567890123456"
// Decode
decodeResult := int64Codec.Decode(original)
require.True(t, either.IsRight(decodeResult))
value := either.MonadFold(decodeResult,
func(validation.Errors) int64 { return 0 },
F.Identity[int64],
)
// Encode
encoded := int64Codec.Encode(value)
assert.Equal(t, original, encoded)
})
t.Run("codec has correct name", func(t *testing.T) {
assert.Equal(t, "Int64FromString", int64Codec.Name())
})
}

264
v2/optics/lenses/doc.go Normal file
View File

@@ -0,0 +1,264 @@
// 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 lenses provides pre-built lens and prism implementations for common data structures.
//
// This package offers ready-to-use optics (lenses and prisms) for working with regex match
// structures and URL components in a functional programming style. Lenses enable immutable
// updates to nested data structures, while prisms provide safe optional access to fields.
//
// # Overview
//
// The package includes optics for:
// - Match structures: For working with regex match results (indexed capture groups)
// - NamedMatch structures: For working with regex matches with named capture groups
// - url.Userinfo: For working with URL authentication information
//
// Each structure has three variants of optics:
// - Value lenses: Work with value types (immutable updates)
// - Reference lenses: Work with pointer types (mutable updates)
// - Prisms: Provide optional access treating zero values as None
//
// # Lenses vs Prisms
//
// Lenses provide guaranteed access to a field within a structure:
// - Get: Extract a field value
// - Set: Update a field value (returns new structure for values, mutates for pointers)
//
// Prisms provide optional access to fields that may not be present:
// - GetOption: Try to extract a field, returning Option[T]
// - ReverseGet: Construct a structure from a field value
//
// # Match Structures
//
// Match represents a regex match with indexed capture groups:
//
// type Match struct {
// Before string // Text before the match
// Groups []string // Capture groups (index 0 is full match)
// After string // Text after the match
// }
//
// NamedMatch represents a regex match with named capture groups:
//
// type NamedMatch struct {
// Before string // Text before the match
// Groups map[string]string // Named capture groups
// Full string // Full matched text
// After string // Text after the match
// }
//
// # Usage Examples
//
// Working with Match (value-based):
//
// lenses := MakeMatchLenses()
// match := Match{
// Before: "Hello ",
// Groups: []string{"world", "world"},
// After: "!",
// }
//
// // Get a field
// before := lenses.Before.Get(match) // "Hello "
//
// // Update a field (returns new Match)
// updated := lenses.Before.Set(match, "Hi ")
// // updated.Before is "Hi ", original match unchanged
//
// // Use optional lens (treats empty string as None)
// emptyMatch := Match{Before: "", Groups: []string{}, After: ""}
// beforeOpt := lenses.BeforeO.GetOption(emptyMatch) // None
//
// Working with Match (reference-based):
//
// lenses := MakeMatchRefLenses()
// match := &Match{
// Before: "Hello ",
// Groups: []string{"world"},
// After: "!",
// }
//
// // Get a field
// before := lenses.Before.Get(match) // "Hello "
//
// // Update a field (mutates the pointer)
// lenses.Before.Set(match, "Hi ")
// // match.Before is now "Hi "
//
// // Use prism for optional access
// afterOpt := lenses.AfterP.GetOption(match) // Some("!")
//
// Working with NamedMatch:
//
// lenses := MakeNamedMatchLenses()
// match := NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{
// "user": "john",
// "domain": "example.com",
// },
// Full: "john@example.com",
// After: "",
// }
//
// // Get field values
// full := lenses.Full.Get(match) // "john@example.com"
// groups := lenses.Groups.Get(match) // map with user and domain
//
// // Update a field
// updated := lenses.Before.Set(match, "Contact: ")
//
// // Use optional lens
// afterOpt := lenses.AfterO.GetOption(match) // None (empty string)
//
// Working with url.Userinfo:
//
// lenses := MakeUserinfoRefLenses()
// userinfo := url.UserPassword("john", "secret123")
//
// // Get username
// username := lenses.Username.Get(userinfo) // "john"
//
// // Update password (returns new Userinfo)
// updated := lenses.Password.Set(userinfo, "newpass")
//
// // Use optional lens for password
// pwdOpt := lenses.PasswordO.GetOption(userinfo) // Some("secret123")
//
// // Handle userinfo without password
// userOnly := url.User("alice")
//
// Working with url.URL:
//
// lenses := MakeURLLenses()
// u := url.URL{
// Scheme: "https",
// Host: "example.com",
// Path: "/api/v1/users",
// }
//
// // Get field values
// scheme := lenses.Scheme.Get(u) // "https"
// host := lenses.Host.Get(u) // "example.com"
//
// // Update fields (returns new URL)
// updated := lenses.Path.Set("/api/v2/users")(u)
// // updated.Path is "/api/v2/users", original u unchanged
//
// // Use optional lens for query string
// queryOpt := lenses.RawQueryO.Get(u) // None (no query string)
//
// // Set query string
// withQuery := lenses.RawQuery.Set("page=1&limit=10")(u)
//
// Working with url.Error:
//
// lenses := MakeErrorLenses()
// urlErr := url.Error{
// Op: "Get",
// URL: "https://example.com",
// Err: errors.New("connection timeout"),
// }
//
// // Get field values
// op := lenses.Op.Get(urlErr) // "Get"
// urlStr := lenses.URL.Get(urlErr) // "https://example.com"
// err := lenses.Err.Get(urlErr) // error: "connection timeout"
//
// // Update fields (returns new Error)
// updated := lenses.Op.Set("Post")(urlErr)
// // updated.Op is "Post", original urlErr unchanged
// pwdOpt = lenses.PasswordO.GetOption(userOnly) // None
//
// # Composing Optics
//
// Lenses and prisms can be composed to access nested structures:
//
// // Compose lenses to access nested fields
// outerLens := MakeSomeLens()
// innerLens := MakeSomeOtherLens()
// composed := lens.Compose(outerLens, innerLens)
//
// // Compose prisms for optional nested access
// outerPrism := MakeSomePrism()
// innerPrism := MakeSomeOtherPrism()
// composed := prism.Compose(outerPrism, innerPrism)
//
// # Optional Lenses
//
// Optional lenses (suffixed with 'O') treat zero values as None:
// - Empty strings become None
// - Zero values of other types become None
// - Non-zero values become Some(value)
//
// This is useful for distinguishing between "field not set" and "field set to zero value":
//
// lenses := MakeMatchLenses()
// match := Match{Before: "", Groups: []string{"test"}, After: "!"}
//
// // Regular lens returns empty string
// before := lenses.Before.Get(match) // ""
//
// // Optional lens returns None
// beforeOpt := lenses.BeforeO.GetOption(match) // None
//
// // Setting None clears the field
// cleared := lenses.BeforeO.Set(match, option.None[string]())
// // cleared.Before is ""
//
// // Setting Some updates the field
// updated := lenses.BeforeO.Set(match, option.Some("prefix "))
// // updated.Before is "prefix "
//
// # Code Generation
//
// This package uses code generation for creating lens implementations.
// The generate directive at the top of this file triggers the lens generator:
//
// //go:generate go run ../../main.go lens --dir . --filename gen_lens.go
//
// To regenerate lenses after modifying structures, run:
//
// go generate ./optics/lenses
//
// # Performance Considerations
//
// Value-based lenses (MatchLenses, NamedMatchLenses):
// - Create new structures on each Set operation
// - Safe for concurrent use (immutable)
// - Suitable for functional programming patterns
//
// Reference-based lenses (MatchRefLenses, NamedMatchRefLenses):
// - Mutate existing structures
// - More efficient for repeated updates
// - Require careful handling in concurrent contexts
//
// # Related Packages
//
// - github.com/IBM/fp-go/v2/optics/lens: Core lens functionality
// - github.com/IBM/fp-go/v2/optics/prism: Core prism functionality
// - github.com/IBM/fp-go/v2/optics/iso: Isomorphisms for type conversions
// - github.com/IBM/fp-go/v2/option: Option type for optional values
//
// # See Also
//
// For more information on functional optics:
// - Lens laws: https://github.com/IBM/fp-go/blob/main/optics/lens/README.md
// - Prism laws: https://github.com/IBM/fp-go/blob/main/optics/prism/README.md
// - Optics tutorial: https://github.com/IBM/fp-go/blob/main/docs/optics.md
package lenses
//go:generate go run ../../main.go lens --dir . --filename gen_lens.go

627
v2/optics/lenses/matcher.go Normal file
View File

@@ -0,0 +1,627 @@
// 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 lenses
import (
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
__lens "github.com/IBM/fp-go/v2/optics/lens"
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
__prism "github.com/IBM/fp-go/v2/optics/prism"
__option "github.com/IBM/fp-go/v2/option"
)
// MatchLenses provides lenses for accessing and modifying fields of Match structures.
// Lenses enable functional updates to immutable data structures by providing
// composable getters and setters.
//
// This struct contains both mandatory lenses (for direct field access) and optional
// lenses (for fields that may be zero values, treating them as Option types).
//
// Fields:
// - Before: Lens for the text before the match
// - Groups: Lens for the capture groups array
// - After: Lens for the text after the match
// - BeforeO: Optional lens treating empty Before as None
// - AfterO: Optional lens treating empty After as None
//
// Example:
//
// lenses := MakeMatchLenses()
// match := Match{Before: "hello ", Groups: []string{"world"}, After: "!"}
//
// // Get a field value
// before := lenses.Before.Get(match) // "hello "
//
// // Set a field value (returns new Match)
// updated := lenses.Before.Set(match, "hi ")
// // updated.Before is now "hi "
type MatchLenses struct {
// mandatory fields
Before __lens.Lens[__prism.Match, string]
Groups __lens.Lens[__prism.Match, []string]
After __lens.Lens[__prism.Match, string]
// optional fields
BeforeO __lens_option.LensO[__prism.Match, string]
AfterO __lens_option.LensO[__prism.Match, string]
}
// MatchRefLenses provides lenses for accessing and modifying fields of Match structures
// via pointers. This is useful when working with mutable references to Match values.
//
// In addition to standard lenses, this struct also includes prisms for each field,
// which provide optional access patterns (useful for validation or conditional updates).
//
// Fields:
// - Before, Groups, After: Standard lenses for pointer-based access
// - BeforeO, AfterO: Optional lenses treating zero values as None
// - BeforeP, GroupsP, AfterP: Prisms for optional field access
//
// Example:
//
// lenses := MakeMatchRefLenses()
// match := &Match{Before: "hello ", Groups: []string{"world"}, After: "!"}
//
// // Get a field value
// before := lenses.Before.Get(match) // "hello "
//
// // Set a field value (mutates the pointer)
// lenses.Before.Set(match, "hi ")
// // match.Before is now "hi "
type MatchRefLenses struct {
// mandatory fields
Before __lens.Lens[*__prism.Match, string]
Groups __lens.Lens[*__prism.Match, []string]
After __lens.Lens[*__prism.Match, string]
// optional fields
BeforeO __lens_option.LensO[*__prism.Match, string]
AfterO __lens_option.LensO[*__prism.Match, string]
// prisms
BeforeP __prism.Prism[*__prism.Match, string]
GroupsP __prism.Prism[*__prism.Match, []string]
AfterP __prism.Prism[*__prism.Match, string]
}
// MatchPrisms provides prisms for accessing fields of Match structures.
// Prisms enable safe access to fields that may not be present (zero values),
// returning Option types instead of direct values.
//
// Fields:
// - Before: Prism for Before field (None if empty string)
// - Groups: Prism for Groups field (always Some)
// - After: Prism for After field (None if empty string)
//
// Example:
//
// prisms := MakeMatchPrisms()
// match := Match{Before: "", Groups: []string{"test"}, After: "!"}
//
// // Try to get Before (returns None because it's empty)
// beforeOpt := prisms.Before.GetOption(match) // None
//
// // Get After (returns Some because it's non-empty)
// afterOpt := prisms.After.GetOption(match) // Some("!")
type MatchPrisms struct {
Before __prism.Prism[__prism.Match, string]
Groups __prism.Prism[__prism.Match, []string]
After __prism.Prism[__prism.Match, string]
}
// MakeMatchLenses creates a new MatchLenses with lenses for all fields of Match.
// This function constructs both mandatory lenses (for direct field access) and
// optional lenses (for treating zero values as Option types).
//
// The returned lenses enable functional-style updates to Match structures,
// allowing you to get and set field values while maintaining immutability.
//
// Returns:
// - A MatchLenses struct with lenses for Before, Groups, After fields,
// plus optional lenses BeforeO and AfterO
//
// Example:
//
// lenses := MakeMatchLenses()
// match := Match{Before: "start ", Groups: []string{"middle"}, After: " end"}
//
// // Get field values
// before := lenses.Before.Get(match) // "start "
// groups := lenses.Groups.Get(match) // []string{"middle"}
//
// // Update a field (returns new Match)
// updated := lenses.After.Set(match, " finish")
// // updated is a new Match with After = " finish"
//
// // Use optional lens (treats empty string as None)
// emptyMatch := Match{Before: "", Groups: []string{}, After: ""}
// beforeOpt := lenses.BeforeO.GetOption(emptyMatch) // None
func MakeMatchLenses() MatchLenses {
// mandatory lenses
lensBefore := __lens.MakeLensWithName(
func(s __prism.Match) string { return s.Before },
func(s __prism.Match, v string) __prism.Match { s.Before = v; return s },
"Match.Before",
)
lensGroups := __lens.MakeLensWithName(
func(s __prism.Match) []string { return s.Groups },
func(s __prism.Match, v []string) __prism.Match { s.Groups = v; return s },
"Match.Groups",
)
lensAfter := __lens.MakeLensWithName(
func(s __prism.Match) string { return s.After },
func(s __prism.Match, v string) __prism.Match { s.After = v; return s },
"Match.After",
)
// optional lenses
lensBeforeO := __lens_option.FromIso[__prism.Match](__iso_option.FromZero[string]())(lensBefore)
lensAfterO := __lens_option.FromIso[__prism.Match](__iso_option.FromZero[string]())(lensAfter)
return MatchLenses{
// mandatory lenses
Before: lensBefore,
Groups: lensGroups,
After: lensAfter,
// optional lenses
BeforeO: lensBeforeO,
AfterO: lensAfterO,
}
}
// MakeMatchRefLenses creates a new MatchRefLenses with lenses for all fields of *Match.
// This function constructs lenses that work with pointers to Match structures,
// enabling both immutable-style updates and direct mutations.
//
// The returned lenses include:
// - Standard lenses for pointer-based field access
// - Optional lenses for treating zero values as Option types
// - Prisms for safe optional field access
//
// Returns:
// - A MatchRefLenses struct with lenses and prisms for all Match fields
//
// Example:
//
// lenses := MakeMatchRefLenses()
// match := &Match{Before: "prefix ", Groups: []string{"data"}, After: " suffix"}
//
// // Get field value
// before := lenses.Before.Get(match) // "prefix "
//
// // Set field value (mutates the pointer)
// lenses.Before.Set(match, "new ")
// // match.Before is now "new "
//
// // Use prism for optional access
// afterOpt := lenses.AfterP.GetOption(match) // Some(" suffix")
func MakeMatchRefLenses() MatchRefLenses {
// mandatory lenses
lensBefore := __lens.MakeLensStrictWithName(
func(s *__prism.Match) string { return s.Before },
func(s *__prism.Match, v string) *__prism.Match { s.Before = v; return s },
"(*Match).Before",
)
lensGroups := __lens.MakeLensRefWithName(
func(s *__prism.Match) []string { return s.Groups },
func(s *__prism.Match, v []string) *__prism.Match { s.Groups = v; return s },
"(*Match).Groups",
)
lensAfter := __lens.MakeLensStrictWithName(
func(s *__prism.Match) string { return s.After },
func(s *__prism.Match, v string) *__prism.Match { s.After = v; return s },
"(*Match).After",
)
// optional lenses
lensBeforeO := __lens_option.FromIso[*__prism.Match](__iso_option.FromZero[string]())(lensBefore)
lensAfterO := __lens_option.FromIso[*__prism.Match](__iso_option.FromZero[string]())(lensAfter)
return MatchRefLenses{
// mandatory lenses
Before: lensBefore,
Groups: lensGroups,
After: lensAfter,
// optional lenses
BeforeO: lensBeforeO,
AfterO: lensAfterO,
}
}
// MakeMatchPrisms creates a new MatchPrisms with prisms for all fields of Match.
// This function constructs prisms that provide safe optional access to Match fields,
// treating zero values (empty strings) as None.
//
// The returned prisms enable pattern matching on field presence:
// - Before and After prisms return None for empty strings
// - Groups prism always returns Some (even for empty slices)
//
// Returns:
// - A MatchPrisms struct with prisms for Before, Groups, and After fields
//
// Example:
//
// prisms := MakeMatchPrisms()
// match := Match{Before: "", Groups: []string{"data"}, After: "!"}
//
// // Try to get Before (returns None because it's empty)
// beforeOpt := prisms.Before.GetOption(match) // None
//
// // Get Groups (always returns Some)
// groupsOpt := prisms.Groups.GetOption(match) // Some([]string{"data"})
//
// // Get After (returns Some because it's non-empty)
// afterOpt := prisms.After.GetOption(match) // Some("!")
//
// // Construct a Match from a value using ReverseGet
// newMatch := prisms.Before.ReverseGet("prefix ")
// // newMatch is Match{Before: "prefix ", Groups: nil, After: ""}
func MakeMatchPrisms() MatchPrisms {
_fromNonZeroBefore := __option.FromNonZero[string]()
_prismBefore := __prism.MakePrismWithName(
func(s __prism.Match) __option.Option[string] { return _fromNonZeroBefore(s.Before) },
func(v string) __prism.Match {
return __prism.Match{Before: v}
},
"Match.Before",
)
_prismGroups := __prism.MakePrismWithName(
func(s __prism.Match) __option.Option[[]string] { return __option.Some(s.Groups) },
func(v []string) __prism.Match {
return __prism.Match{Groups: v}
},
"Match.Groups",
)
_fromNonZeroAfter := __option.FromNonZero[string]()
_prismAfter := __prism.MakePrismWithName(
func(s __prism.Match) __option.Option[string] { return _fromNonZeroAfter(s.After) },
func(v string) __prism.Match {
return __prism.Match{After: v}
},
"Match.After",
)
return MatchPrisms{
Before: _prismBefore,
Groups: _prismGroups,
After: _prismAfter,
}
}
// NamedMatchLenses provides lenses for accessing and modifying fields of NamedMatch structures.
// NamedMatch represents regex matches with named capture groups, and these lenses enable
// functional updates to its fields.
//
// This struct contains both mandatory lenses (for direct field access) and optional
// lenses (for fields that may be zero values, treating them as Option types).
//
// Fields:
// - Before: Lens for the text before the match
// - Groups: Lens for the named capture groups map
// - Full: Lens for the complete matched text
// - After: Lens for the text after the match
// - BeforeO, FullO, AfterO: Optional lenses treating empty strings as None
//
// Example:
//
// lenses := MakeNamedMatchLenses()
// match := NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{"user": "john", "domain": "example.com"},
// Full: "john@example.com",
// After: "",
// }
//
// // Get a field value
// full := lenses.Full.Get(match) // "john@example.com"
//
// // Set a field value (returns new NamedMatch)
// updated := lenses.Before.Set(match, "Contact: ")
// // updated.Before is now "Contact: "
type NamedMatchLenses struct {
// mandatory fields
Before __lens.Lens[__prism.NamedMatch, string]
Groups __lens.Lens[__prism.NamedMatch, map[string]string]
Full __lens.Lens[__prism.NamedMatch, string]
After __lens.Lens[__prism.NamedMatch, string]
// optional fields
BeforeO __lens_option.LensO[__prism.NamedMatch, string]
FullO __lens_option.LensO[__prism.NamedMatch, string]
AfterO __lens_option.LensO[__prism.NamedMatch, string]
}
// NamedMatchRefLenses provides lenses for accessing and modifying fields of NamedMatch
// structures via pointers. This is useful when working with mutable references to
// NamedMatch values.
//
// In addition to standard lenses, this struct also includes prisms for each field,
// which provide optional access patterns (useful for validation or conditional updates).
//
// Fields:
// - Before, Groups, Full, After: Standard lenses for pointer-based access
// - BeforeO, FullO, AfterO: Optional lenses treating zero values as None
// - BeforeP, GroupsP, FullP, AfterP: Prisms for optional field access
//
// Example:
//
// lenses := MakeNamedMatchRefLenses()
// match := &NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{"user": "alice", "domain": "test.org"},
// Full: "alice@test.org",
// After: " for info",
// }
//
// // Get a field value
// full := lenses.Full.Get(match) // "alice@test.org"
//
// // Set a field value (mutates the pointer)
// lenses.After.Set(match, " for contact")
// // match.After is now " for contact"
type NamedMatchRefLenses struct {
// mandatory fields
Before __lens.Lens[*__prism.NamedMatch, string]
Groups __lens.Lens[*__prism.NamedMatch, map[string]string]
Full __lens.Lens[*__prism.NamedMatch, string]
After __lens.Lens[*__prism.NamedMatch, string]
// optional fields
BeforeO __lens_option.LensO[*__prism.NamedMatch, string]
FullO __lens_option.LensO[*__prism.NamedMatch, string]
AfterO __lens_option.LensO[*__prism.NamedMatch, string]
// prisms
BeforeP __prism.Prism[*__prism.NamedMatch, string]
GroupsP __prism.Prism[*__prism.NamedMatch, map[string]string]
FullP __prism.Prism[*__prism.NamedMatch, string]
AfterP __prism.Prism[*__prism.NamedMatch, string]
}
// NamedMatchPrisms provides prisms for accessing fields of NamedMatch structures.
// Prisms enable safe access to fields that may not be present (zero values),
// returning Option types instead of direct values.
//
// Fields:
// - Before: Prism for Before field (None if empty string)
// - Groups: Prism for Groups field (always Some)
// - Full: Prism for Full field (None if empty string)
// - After: Prism for After field (None if empty string)
//
// Example:
//
// prisms := MakeNamedMatchPrisms()
// match := NamedMatch{
// Before: "",
// Groups: map[string]string{"user": "bob"},
// Full: "bob@example.com",
// After: "",
// }
//
// // Try to get Before (returns None because it's empty)
// beforeOpt := prisms.Before.GetOption(match) // None
//
// // Get Full (returns Some because it's non-empty)
// fullOpt := prisms.Full.GetOption(match) // Some("bob@example.com")
type NamedMatchPrisms struct {
Before __prism.Prism[__prism.NamedMatch, string]
Groups __prism.Prism[__prism.NamedMatch, map[string]string]
Full __prism.Prism[__prism.NamedMatch, string]
After __prism.Prism[__prism.NamedMatch, string]
}
// MakeNamedMatchLenses creates a new NamedMatchLenses with lenses for all fields of NamedMatch.
// This function constructs both mandatory lenses (for direct field access) and
// optional lenses (for treating zero values as Option types).
//
// The returned lenses enable functional-style updates to NamedMatch structures,
// allowing you to get and set field values while maintaining immutability.
//
// Returns:
// - A NamedMatchLenses struct with lenses for Before, Groups, Full, After fields,
// plus optional lenses BeforeO, FullO, and AfterO
//
// Example:
//
// lenses := MakeNamedMatchLenses()
// match := NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{"user": "john", "domain": "example.com"},
// Full: "john@example.com",
// After: "",
// }
//
// // Get field values
// full := lenses.Full.Get(match) // "john@example.com"
// groups := lenses.Groups.Get(match) // map[string]string{"user": "john", ...}
//
// // Update a field (returns new NamedMatch)
// updated := lenses.Before.Set(match, "Contact: ")
// // updated is a new NamedMatch with Before = "Contact: "
//
// // Use optional lens (treats empty string as None)
// afterOpt := lenses.AfterO.GetOption(match) // None (because After is empty)
func MakeNamedMatchLenses() NamedMatchLenses {
// mandatory lenses
lensBefore := __lens.MakeLensWithName(
func(s __prism.NamedMatch) string { return s.Before },
func(s __prism.NamedMatch, v string) __prism.NamedMatch { s.Before = v; return s },
"NamedMatch.Before",
)
lensGroups := __lens.MakeLensWithName(
func(s __prism.NamedMatch) map[string]string { return s.Groups },
func(s __prism.NamedMatch, v map[string]string) __prism.NamedMatch { s.Groups = v; return s },
"NamedMatch.Groups",
)
lensFull := __lens.MakeLensWithName(
func(s __prism.NamedMatch) string { return s.Full },
func(s __prism.NamedMatch, v string) __prism.NamedMatch { s.Full = v; return s },
"NamedMatch.Full",
)
lensAfter := __lens.MakeLensWithName(
func(s __prism.NamedMatch) string { return s.After },
func(s __prism.NamedMatch, v string) __prism.NamedMatch { s.After = v; return s },
"NamedMatch.After",
)
// optional lenses
lensBeforeO := __lens_option.FromIso[__prism.NamedMatch](__iso_option.FromZero[string]())(lensBefore)
lensFullO := __lens_option.FromIso[__prism.NamedMatch](__iso_option.FromZero[string]())(lensFull)
lensAfterO := __lens_option.FromIso[__prism.NamedMatch](__iso_option.FromZero[string]())(lensAfter)
return NamedMatchLenses{
// mandatory lenses
Before: lensBefore,
Groups: lensGroups,
Full: lensFull,
After: lensAfter,
// optional lenses
BeforeO: lensBeforeO,
FullO: lensFullO,
AfterO: lensAfterO,
}
}
// MakeNamedMatchRefLenses creates a new NamedMatchRefLenses with lenses for all fields of *NamedMatch.
// This function constructs lenses that work with pointers to NamedMatch structures,
// enabling both immutable-style updates and direct mutations.
//
// The returned lenses include:
// - Standard lenses for pointer-based field access
// - Optional lenses for treating zero values as Option types
// - Prisms for safe optional field access
//
// Returns:
// - A NamedMatchRefLenses struct with lenses and prisms for all NamedMatch fields
//
// Example:
//
// lenses := MakeNamedMatchRefLenses()
// match := &NamedMatch{
// Before: "Email: ",
// Groups: map[string]string{"user": "alice", "domain": "test.org"},
// Full: "alice@test.org",
// After: "",
// }
//
// // Get field value
// full := lenses.Full.Get(match) // "alice@test.org"
//
// // Set field value (mutates the pointer)
// lenses.Before.Set(match, "Contact: ")
// // match.Before is now "Contact: "
//
// // Use prism for optional access
// fullOpt := lenses.FullP.GetOption(match) // Some("alice@test.org")
func MakeNamedMatchRefLenses() NamedMatchRefLenses {
// mandatory lenses
lensBefore := __lens.MakeLensStrictWithName(
func(s *__prism.NamedMatch) string { return s.Before },
func(s *__prism.NamedMatch, v string) *__prism.NamedMatch { s.Before = v; return s },
"(*NamedMatch).Before",
)
lensGroups := __lens.MakeLensRefWithName(
func(s *__prism.NamedMatch) map[string]string { return s.Groups },
func(s *__prism.NamedMatch, v map[string]string) *__prism.NamedMatch { s.Groups = v; return s },
"(*NamedMatch).Groups",
)
lensFull := __lens.MakeLensStrictWithName(
func(s *__prism.NamedMatch) string { return s.Full },
func(s *__prism.NamedMatch, v string) *__prism.NamedMatch { s.Full = v; return s },
"(*NamedMatch).Full",
)
lensAfter := __lens.MakeLensStrictWithName(
func(s *__prism.NamedMatch) string { return s.After },
func(s *__prism.NamedMatch, v string) *__prism.NamedMatch { s.After = v; return s },
"(*NamedMatch).After",
)
// optional lenses
lensBeforeO := __lens_option.FromIso[*__prism.NamedMatch](__iso_option.FromZero[string]())(lensBefore)
lensFullO := __lens_option.FromIso[*__prism.NamedMatch](__iso_option.FromZero[string]())(lensFull)
lensAfterO := __lens_option.FromIso[*__prism.NamedMatch](__iso_option.FromZero[string]())(lensAfter)
return NamedMatchRefLenses{
// mandatory lenses
Before: lensBefore,
Groups: lensGroups,
Full: lensFull,
After: lensAfter,
// optional lenses
BeforeO: lensBeforeO,
FullO: lensFullO,
AfterO: lensAfterO,
}
}
// MakeNamedMatchPrisms creates a new NamedMatchPrisms with prisms for all fields of NamedMatch.
// This function constructs prisms that provide safe optional access to NamedMatch fields,
// treating zero values (empty strings) as None.
//
// The returned prisms enable pattern matching on field presence:
// - Before, Full, and After prisms return None for empty strings
// - Groups prism always returns Some (even for nil or empty maps)
//
// Returns:
// - A NamedMatchPrisms struct with prisms for Before, Groups, Full, and After fields
//
// Example:
//
// prisms := MakeNamedMatchPrisms()
// match := NamedMatch{
// Before: "",
// Groups: map[string]string{"user": "bob", "domain": "example.com"},
// Full: "bob@example.com",
// After: "",
// }
//
// // Try to get Before (returns None because it's empty)
// beforeOpt := prisms.Before.GetOption(match) // None
//
// // Get Groups (always returns Some)
// groupsOpt := prisms.Groups.GetOption(match)
// // Some(map[string]string{"user": "bob", "domain": "example.com"})
//
// // Get Full (returns Some because it's non-empty)
// fullOpt := prisms.Full.GetOption(match) // Some("bob@example.com")
//
// // Construct a NamedMatch from a value using ReverseGet
// newMatch := prisms.Full.ReverseGet("test@example.com")
// // newMatch is NamedMatch{Before: "", Groups: nil, Full: "test@example.com", After: ""}
func MakeNamedMatchPrisms() NamedMatchPrisms {
_fromNonZeroBefore := __option.FromNonZero[string]()
_prismBefore := __prism.MakePrismWithName(
func(s __prism.NamedMatch) __option.Option[string] { return _fromNonZeroBefore(s.Before) },
func(v string) __prism.NamedMatch {
return __prism.NamedMatch{Before: v}
},
"NamedMatch.Before",
)
_prismGroups := __prism.MakePrismWithName(
func(s __prism.NamedMatch) __option.Option[map[string]string] { return __option.Some(s.Groups) },
func(v map[string]string) __prism.NamedMatch {
return __prism.NamedMatch{Groups: v}
},
"NamedMatch.Groups",
)
_fromNonZeroFull := __option.FromNonZero[string]()
_prismFull := __prism.MakePrismWithName(
func(s __prism.NamedMatch) __option.Option[string] { return _fromNonZeroFull(s.Full) },
func(v string) __prism.NamedMatch {
return __prism.NamedMatch{Full: v}
},
"NamedMatch.Full",
)
_fromNonZeroAfter := __option.FromNonZero[string]()
_prismAfter := __prism.MakePrismWithName(
func(s __prism.NamedMatch) __option.Option[string] { return _fromNonZeroAfter(s.After) },
func(v string) __prism.NamedMatch {
return __prism.NamedMatch{After: v}
},
"NamedMatch.After",
)
return NamedMatchPrisms{
Before: _prismBefore,
Groups: _prismGroups,
Full: _prismFull,
After: _prismAfter,
}
}

View File

@@ -0,0 +1,558 @@
// 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 lenses
import (
"testing"
__prism "github.com/IBM/fp-go/v2/optics/prism"
__option "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestMatchLenses_Before tests the Before lens for Match
func TestMatchLenses_Before(t *testing.T) {
lenses := MakeMatchLenses()
match := __prism.Match{
Before: "prefix ",
Groups: []string{"match", "group1"},
After: " suffix",
}
// Test Get
before := lenses.Before.Get(match)
assert.Equal(t, "prefix ", before)
// Test Set (curried)
updated := lenses.Before.Set("new prefix ")(match)
assert.Equal(t, "new prefix ", updated.Before)
assert.Equal(t, match.Groups, updated.Groups) // Other fields unchanged
assert.Equal(t, match.After, updated.After)
assert.Equal(t, "prefix ", match.Before) // Original unchanged
}
// TestMatchLenses_Groups tests the Groups lens for Match
func TestMatchLenses_Groups(t *testing.T) {
lenses := MakeMatchLenses()
match := __prism.Match{
Before: "prefix ",
Groups: []string{"match", "group1"},
After: " suffix",
}
// Test Get
groups := lenses.Groups.Get(match)
assert.Equal(t, []string{"match", "group1"}, groups)
// Test Set (curried)
newGroups := []string{"new", "groups", "here"}
updated := lenses.Groups.Set(newGroups)(match)
assert.Equal(t, newGroups, updated.Groups)
assert.Equal(t, match.Before, updated.Before)
assert.Equal(t, match.After, updated.After)
}
// TestMatchLenses_After tests the After lens for Match
func TestMatchLenses_After(t *testing.T) {
lenses := MakeMatchLenses()
match := __prism.Match{
Before: "prefix ",
Groups: []string{"match"},
After: " suffix",
}
// Test Get
after := lenses.After.Get(match)
assert.Equal(t, " suffix", after)
// Test Set (curried)
updated := lenses.After.Set(" new suffix")(match)
assert.Equal(t, " new suffix", updated.After)
assert.Equal(t, match.Before, updated.Before)
assert.Equal(t, match.Groups, updated.Groups)
}
// TestMatchLenses_BeforeO tests the optional Before lens
func TestMatchLenses_BeforeO(t *testing.T) {
lenses := MakeMatchLenses()
t.Run("non-empty Before", func(t *testing.T) {
match := __prism.Match{Before: "prefix ", Groups: []string{}, After: ""}
opt := lenses.BeforeO.Get(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "prefix ", value)
})
t.Run("empty Before", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
opt := lenses.BeforeO.Get(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("set Some", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
updated := lenses.BeforeO.Set(__option.Some("new "))(match)
assert.Equal(t, "new ", updated.Before)
})
t.Run("set None", func(t *testing.T) {
match := __prism.Match{Before: "prefix ", Groups: []string{}, After: ""}
updated := lenses.BeforeO.Set(__option.None[string]())(match)
assert.Equal(t, "", updated.Before)
})
}
// TestMatchLenses_AfterO tests the optional After lens
func TestMatchLenses_AfterO(t *testing.T) {
lenses := MakeMatchLenses()
t.Run("non-empty After", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: " suffix"}
opt := lenses.AfterO.Get(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, " suffix", value)
})
t.Run("empty After", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
opt := lenses.AfterO.Get(match)
assert.True(t, __option.IsNone(opt))
})
}
// TestMatchRefLenses_Before tests the Before lens for *Match
func TestMatchRefLenses_Before(t *testing.T) {
lenses := MakeMatchRefLenses()
match := &__prism.Match{
Before: "prefix ",
Groups: []string{"match"},
After: " suffix",
}
// Test Get
before := lenses.Before.Get(match)
assert.Equal(t, "prefix ", before)
// Test Set (creates copy with MakeLensStrictWithName, curried)
result := lenses.Before.Set("new prefix ")(match)
assert.Equal(t, "new prefix ", result.Before)
assert.Equal(t, "prefix ", match.Before) // Original unchanged
assert.NotSame(t, match, result) // Returns new pointer
}
// TestMatchRefLenses_Groups tests the Groups lens for *Match
func TestMatchRefLenses_Groups(t *testing.T) {
lenses := MakeMatchRefLenses()
match := &__prism.Match{
Before: "prefix ",
Groups: []string{"match", "group1"},
After: " suffix",
}
// Test Get
groups := lenses.Groups.Get(match)
assert.Equal(t, []string{"match", "group1"}, groups)
// Test Set (creates copy with MakeLensRefWithName, curried)
newGroups := []string{"new", "groups"}
result := lenses.Groups.Set(newGroups)(match)
assert.Equal(t, newGroups, result.Groups)
assert.Equal(t, []string{"match", "group1"}, match.Groups) // Original unchanged
assert.NotSame(t, match, result)
}
// TestMatchRefLenses_After tests the After lens for *Match
func TestMatchRefLenses_After(t *testing.T) {
lenses := MakeMatchRefLenses()
match := &__prism.Match{
Before: "prefix ",
Groups: []string{"match"},
After: " suffix",
}
// Test Get
after := lenses.After.Get(match)
assert.Equal(t, " suffix", after)
// Test Set (creates copy with MakeLensStrictWithName, curried)
result := lenses.After.Set(" new suffix")(match)
assert.Equal(t, " new suffix", result.After)
assert.Equal(t, " suffix", match.After) // Original unchanged
assert.NotSame(t, match, result)
}
// TestMatchPrisms_Before tests the Before prism
func TestMatchPrisms_Before(t *testing.T) {
prisms := MakeMatchPrisms()
t.Run("GetOption with non-empty Before", func(t *testing.T) {
match := __prism.Match{Before: "prefix ", Groups: []string{}, After: ""}
opt := prisms.Before.GetOption(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "prefix ", value)
})
t.Run("GetOption with empty Before", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
opt := prisms.Before.GetOption(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("ReverseGet", func(t *testing.T) {
match := prisms.Before.ReverseGet("test ")
assert.Equal(t, "test ", match.Before)
assert.Nil(t, match.Groups)
assert.Equal(t, "", match.After)
})
}
// TestMatchPrisms_Groups tests the Groups prism
func TestMatchPrisms_Groups(t *testing.T) {
prisms := MakeMatchPrisms()
t.Run("GetOption always returns Some", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{"a", "b"}, After: ""}
opt := prisms.Groups.GetOption(match)
assert.True(t, __option.IsSome(opt))
groups := __option.GetOrElse(func() []string { return nil })(opt)
assert.Equal(t, []string{"a", "b"}, groups)
})
t.Run("GetOption with nil Groups", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: nil, After: ""}
opt := prisms.Groups.GetOption(match)
assert.True(t, __option.IsSome(opt))
})
t.Run("ReverseGet", func(t *testing.T) {
groups := []string{"test", "groups"}
match := prisms.Groups.ReverseGet(groups)
assert.Equal(t, groups, match.Groups)
assert.Equal(t, "", match.Before)
assert.Equal(t, "", match.After)
})
}
// TestMatchPrisms_After tests the After prism
func TestMatchPrisms_After(t *testing.T) {
prisms := MakeMatchPrisms()
t.Run("GetOption with non-empty After", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: " suffix"}
opt := prisms.After.GetOption(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, " suffix", value)
})
t.Run("GetOption with empty After", func(t *testing.T) {
match := __prism.Match{Before: "", Groups: []string{}, After: ""}
opt := prisms.After.GetOption(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("ReverseGet", func(t *testing.T) {
match := prisms.After.ReverseGet(" test")
assert.Equal(t, " test", match.After)
assert.Nil(t, match.Groups)
assert.Equal(t, "", match.Before)
})
}
// TestNamedMatchLenses_Before tests the Before lens for NamedMatch
func TestNamedMatchLenses_Before(t *testing.T) {
lenses := MakeNamedMatchLenses()
match := __prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john", "domain": "example.com"},
Full: "john@example.com",
After: "",
}
// Test Get
before := lenses.Before.Get(match)
assert.Equal(t, "Email: ", before)
// Test Set (curried)
updated := lenses.Before.Set("Contact: ")(match)
assert.Equal(t, "Contact: ", updated.Before)
assert.Equal(t, match.Groups, updated.Groups)
assert.Equal(t, match.Full, updated.Full)
assert.Equal(t, match.After, updated.After)
assert.Equal(t, "Email: ", match.Before) // Original unchanged
}
// TestNamedMatchLenses_Groups tests the Groups lens for NamedMatch
func TestNamedMatchLenses_Groups(t *testing.T) {
lenses := MakeNamedMatchLenses()
match := __prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john", "domain": "example.com"},
Full: "john@example.com",
After: "",
}
// Test Get
groups := lenses.Groups.Get(match)
assert.Equal(t, map[string]string{"user": "john", "domain": "example.com"}, groups)
// Test Set (curried)
newGroups := map[string]string{"user": "alice", "domain": "test.org"}
updated := lenses.Groups.Set(newGroups)(match)
assert.Equal(t, newGroups, updated.Groups)
assert.Equal(t, match.Before, updated.Before)
}
// TestNamedMatchLenses_Full tests the Full lens for NamedMatch
func TestNamedMatchLenses_Full(t *testing.T) {
lenses := MakeNamedMatchLenses()
match := __prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john"},
Full: "john@example.com",
After: "",
}
// Test Get
full := lenses.Full.Get(match)
assert.Equal(t, "john@example.com", full)
// Test Set (curried)
updated := lenses.Full.Set("alice@test.org")(match)
assert.Equal(t, "alice@test.org", updated.Full)
assert.Equal(t, match.Before, updated.Before)
}
// TestNamedMatchLenses_After tests the After lens for NamedMatch
func TestNamedMatchLenses_After(t *testing.T) {
lenses := MakeNamedMatchLenses()
match := __prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john"},
Full: "john@example.com",
After: " for contact",
}
// Test Get
after := lenses.After.Get(match)
assert.Equal(t, " for contact", after)
// Test Set (curried)
updated := lenses.After.Set(" for info")(match)
assert.Equal(t, " for info", updated.After)
assert.Equal(t, match.Before, updated.Before)
}
// TestNamedMatchLenses_Optional tests optional lenses for NamedMatch
func TestNamedMatchLenses_Optional(t *testing.T) {
lenses := MakeNamedMatchLenses()
t.Run("BeforeO with non-empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "prefix ", Groups: nil, Full: "", After: ""}
opt := lenses.BeforeO.Get(match)
assert.True(t, __option.IsSome(opt))
})
t.Run("BeforeO with empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt := lenses.BeforeO.Get(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("FullO with non-empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "test@example.com", After: ""}
opt := lenses.FullO.Get(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "test@example.com", value)
})
t.Run("FullO with empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt := lenses.FullO.Get(match)
assert.True(t, __option.IsNone(opt))
})
t.Run("AfterO with non-empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: " suffix"}
opt := lenses.AfterO.Get(match)
assert.True(t, __option.IsSome(opt))
})
t.Run("AfterO with empty", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt := lenses.AfterO.Get(match)
assert.True(t, __option.IsNone(opt))
})
}
// TestNamedMatchRefLenses_Immutability tests that reference lenses create copies
func TestNamedMatchRefLenses_Immutability(t *testing.T) {
lenses := MakeNamedMatchRefLenses()
match := &__prism.NamedMatch{
Before: "Email: ",
Groups: map[string]string{"user": "john"},
Full: "john@example.com",
After: "",
}
// Test Before (creates copy, curried)
updated1 := lenses.Before.Set("Contact: ")(match)
assert.Equal(t, "Contact: ", updated1.Before)
assert.Equal(t, "Email: ", match.Before) // Original unchanged
// Test Groups (creates copy, curried)
newGroups := map[string]string{"user": "alice"}
updated2 := lenses.Groups.Set(newGroups)(match)
assert.Equal(t, newGroups, updated2.Groups)
assert.Equal(t, map[string]string{"user": "john"}, match.Groups) // Original unchanged
// Test Full (creates copy, curried)
updated3 := lenses.Full.Set("alice@test.org")(match)
assert.Equal(t, "alice@test.org", updated3.Full)
assert.Equal(t, "john@example.com", match.Full) // Original unchanged
// Test After (creates copy, curried)
updated4 := lenses.After.Set(" for info")(match)
assert.Equal(t, " for info", updated4.After)
assert.Equal(t, "", match.After) // Original unchanged
}
// TestNamedMatchPrisms tests prisms for NamedMatch
func TestNamedMatchPrisms(t *testing.T) {
prisms := MakeNamedMatchPrisms()
t.Run("Before prism", func(t *testing.T) {
match := __prism.NamedMatch{Before: "prefix ", Groups: nil, Full: "", After: ""}
opt := prisms.Before.GetOption(match)
assert.True(t, __option.IsSome(opt))
emptyMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt = prisms.Before.GetOption(emptyMatch)
assert.True(t, __option.IsNone(opt))
constructed := prisms.Before.ReverseGet("test ")
assert.Equal(t, "test ", constructed.Before)
})
t.Run("Groups prism always returns Some", func(t *testing.T) {
match := __prism.NamedMatch{
Before: "",
Groups: map[string]string{"key": "value"},
Full: "",
After: "",
}
opt := prisms.Groups.GetOption(match)
assert.True(t, __option.IsSome(opt))
nilMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt = prisms.Groups.GetOption(nilMatch)
assert.True(t, __option.IsSome(opt))
})
t.Run("Full prism", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "test@example.com", After: ""}
opt := prisms.Full.GetOption(match)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "test@example.com", value)
emptyMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt = prisms.Full.GetOption(emptyMatch)
assert.True(t, __option.IsNone(opt))
constructed := prisms.Full.ReverseGet("alice@test.org")
assert.Equal(t, "alice@test.org", constructed.Full)
})
t.Run("After prism", func(t *testing.T) {
match := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: " suffix"}
opt := prisms.After.GetOption(match)
assert.True(t, __option.IsSome(opt))
emptyMatch := __prism.NamedMatch{Before: "", Groups: nil, Full: "", After: ""}
opt = prisms.After.GetOption(emptyMatch)
assert.True(t, __option.IsNone(opt))
constructed := prisms.After.ReverseGet(" test")
assert.Equal(t, " test", constructed.After)
})
}
// TestMatchLenses_Immutability verifies that value lenses don't mutate originals
func TestMatchLenses_Immutability(t *testing.T) {
lenses := MakeMatchLenses()
original := __prism.Match{
Before: "original ",
Groups: []string{"group1", "group2"},
After: " original",
}
// Make a copy to compare later
originalBefore := original.Before
originalGroups := make([]string, len(original.Groups))
copy(originalGroups, original.Groups)
originalAfter := original.After
// Perform multiple updates (curried)
updated1 := lenses.Before.Set("updated ")(original)
updated2 := lenses.Groups.Set([]string{"new"})(updated1)
updated3 := lenses.After.Set(" updated")(updated2)
// Verify original is unchanged
assert.Equal(t, originalBefore, original.Before)
assert.Equal(t, originalGroups, original.Groups)
assert.Equal(t, originalAfter, original.After)
// Verify updates worked
assert.Equal(t, "updated ", updated3.Before)
assert.Equal(t, []string{"new"}, updated3.Groups)
assert.Equal(t, " updated", updated3.After)
}
// TestNamedMatchLenses_Immutability verifies that value lenses don't mutate originals
func TestNamedMatchLenses_Immutability(t *testing.T) {
lenses := MakeNamedMatchLenses()
original := __prism.NamedMatch{
Before: "original ",
Groups: map[string]string{"key": "value"},
Full: "original@example.com",
After: " original",
}
// Make copies to compare later
originalBefore := original.Before
originalFull := original.Full
originalAfter := original.After
// Perform multiple updates (curried)
updated1 := lenses.Before.Set("updated ")(original)
updated2 := lenses.Full.Set("updated@test.org")(updated1)
updated3 := lenses.After.Set(" updated")(updated2)
// Verify original is unchanged
assert.Equal(t, originalBefore, original.Before)
assert.Equal(t, originalFull, original.Full)
assert.Equal(t, originalAfter, original.After)
// Verify updates worked
assert.Equal(t, "updated ", updated3.Before)
assert.Equal(t, "updated@test.org", updated3.Full)
assert.Equal(t, " updated", updated3.After)
}

409
v2/optics/lenses/url.go Normal file
View File

@@ -0,0 +1,409 @@
package lenses
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2026-01-27 16:08:47.5483589 +0100 CET m=+0.003380301
import (
url "net/url"
__iso_option "github.com/IBM/fp-go/v2/optics/iso/option"
__lens "github.com/IBM/fp-go/v2/optics/lens"
__lens_option "github.com/IBM/fp-go/v2/optics/lens/option"
__option "github.com/IBM/fp-go/v2/option"
)
// ErrorLenses provides lenses for accessing fields of url.Error
type ErrorLenses struct {
// mandatory fields
Op __lens.Lens[url.Error, string]
URL __lens.Lens[url.Error, string]
Err __lens.Lens[url.Error, error]
// optional fields
OpO __lens_option.LensO[url.Error, string]
URLO __lens_option.LensO[url.Error, string]
ErrO __lens_option.LensO[url.Error, error]
}
// ErrorRefLenses provides lenses for accessing fields of url.Error via a reference to url.Error
type ErrorRefLenses struct {
// mandatory fields
Op __lens.Lens[*url.Error, string]
URL __lens.Lens[*url.Error, string]
Err __lens.Lens[*url.Error, error]
// optional fields
OpO __lens_option.LensO[*url.Error, string]
URLO __lens_option.LensO[*url.Error, string]
ErrO __lens_option.LensO[*url.Error, error]
}
// MakeErrorLenses creates a new ErrorLenses with lenses for all fields
func MakeErrorLenses() ErrorLenses {
// mandatory lenses
lensOp := __lens.MakeLensWithName(
func(s url.Error) string { return s.Op },
func(s url.Error, v string) url.Error { s.Op = v; return s },
"Error.Op",
)
lensURL := __lens.MakeLensWithName(
func(s url.Error) string { return s.URL },
func(s url.Error, v string) url.Error { s.URL = v; return s },
"Error.URL",
)
lensErr := __lens.MakeLensWithName(
func(s url.Error) error { return s.Err },
func(s url.Error, v error) url.Error { s.Err = v; return s },
"Error.Err",
)
// optional lenses
lensOpO := __lens_option.FromIso[url.Error](__iso_option.FromZero[string]())(lensOp)
lensURLO := __lens_option.FromIso[url.Error](__iso_option.FromZero[string]())(lensURL)
lensErrO := __lens_option.FromIso[url.Error](__iso_option.FromZero[error]())(lensErr)
return ErrorLenses{
// mandatory lenses
Op: lensOp,
URL: lensURL,
Err: lensErr,
// optional lenses
OpO: lensOpO,
URLO: lensURLO,
ErrO: lensErrO,
}
}
// MakeErrorRefLenses creates a new ErrorRefLenses with lenses for all fields
func MakeErrorRefLenses() ErrorRefLenses {
// mandatory lenses
lensOp := __lens.MakeLensStrictWithName(
func(s *url.Error) string { return s.Op },
func(s *url.Error, v string) *url.Error { s.Op = v; return s },
"(*url.Error).Op",
)
lensURL := __lens.MakeLensStrictWithName(
func(s *url.Error) string { return s.URL },
func(s *url.Error, v string) *url.Error { s.URL = v; return s },
"(*url.Error).url.URL",
)
lensErr := __lens.MakeLensStrictWithName(
func(s *url.Error) error { return s.Err },
func(s *url.Error, v error) *url.Error { s.Err = v; return s },
"(*url.Error).Err",
)
// optional lenses
lensOpO := __lens_option.FromIso[*url.Error](__iso_option.FromZero[string]())(lensOp)
lensURLO := __lens_option.FromIso[*url.Error](__iso_option.FromZero[string]())(lensURL)
lensErrO := __lens_option.FromIso[*url.Error](__iso_option.FromZero[error]())(lensErr)
return ErrorRefLenses{
// mandatory lenses
Op: lensOp,
URL: lensURL,
Err: lensErr,
// optional lenses
OpO: lensOpO,
URLO: lensURLO,
ErrO: lensErrO,
}
}
// URLLenses provides lenses for accessing fields of url.URL
type URLLenses struct {
// mandatory fields
Scheme __lens.Lens[url.URL, string]
Opaque __lens.Lens[url.URL, string]
User __lens.Lens[url.URL, *url.Userinfo]
Host __lens.Lens[url.URL, string]
Path __lens.Lens[url.URL, string]
RawPath __lens.Lens[url.URL, string]
OmitHost __lens.Lens[url.URL, bool]
ForceQuery __lens.Lens[url.URL, bool]
RawQuery __lens.Lens[url.URL, string]
Fragment __lens.Lens[url.URL, string]
RawFragment __lens.Lens[url.URL, string]
// optional fields
SchemeO __lens_option.LensO[url.URL, string]
OpaqueO __lens_option.LensO[url.URL, string]
UserO __lens_option.LensO[url.URL, *url.Userinfo]
HostO __lens_option.LensO[url.URL, string]
PathO __lens_option.LensO[url.URL, string]
RawPathO __lens_option.LensO[url.URL, string]
OmitHostO __lens_option.LensO[url.URL, bool]
ForceQueryO __lens_option.LensO[url.URL, bool]
RawQueryO __lens_option.LensO[url.URL, string]
FragmentO __lens_option.LensO[url.URL, string]
RawFragmentO __lens_option.LensO[url.URL, string]
}
// URLRefLenses provides lenses for accessing fields of url.URL via a reference to url.URL
type URLRefLenses struct {
// mandatory fields
Scheme __lens.Lens[*url.URL, string]
Opaque __lens.Lens[*url.URL, string]
User __lens.Lens[*url.URL, *url.Userinfo]
Host __lens.Lens[*url.URL, string]
Path __lens.Lens[*url.URL, string]
RawPath __lens.Lens[*url.URL, string]
OmitHost __lens.Lens[*url.URL, bool]
ForceQuery __lens.Lens[*url.URL, bool]
RawQuery __lens.Lens[*url.URL, string]
Fragment __lens.Lens[*url.URL, string]
RawFragment __lens.Lens[*url.URL, string]
// optional fields
SchemeO __lens_option.LensO[*url.URL, string]
OpaqueO __lens_option.LensO[*url.URL, string]
UserO __lens_option.LensO[*url.URL, *url.Userinfo]
HostO __lens_option.LensO[*url.URL, string]
PathO __lens_option.LensO[*url.URL, string]
RawPathO __lens_option.LensO[*url.URL, string]
OmitHostO __lens_option.LensO[*url.URL, bool]
ForceQueryO __lens_option.LensO[*url.URL, bool]
RawQueryO __lens_option.LensO[*url.URL, string]
FragmentO __lens_option.LensO[*url.URL, string]
RawFragmentO __lens_option.LensO[*url.URL, string]
}
// MakeURLLenses creates a new URLLenses with lenses for all fields
func MakeURLLenses() URLLenses {
// mandatory lenses
lensScheme := __lens.MakeLensWithName(
func(s url.URL) string { return s.Scheme },
func(s url.URL, v string) url.URL { s.Scheme = v; return s },
"URL.Scheme",
)
lensOpaque := __lens.MakeLensWithName(
func(s url.URL) string { return s.Opaque },
func(s url.URL, v string) url.URL { s.Opaque = v; return s },
"URL.Opaque",
)
lensUser := __lens.MakeLensWithName(
func(s url.URL) *url.Userinfo { return s.User },
func(s url.URL, v *url.Userinfo) url.URL { s.User = v; return s },
"URL.User",
)
lensHost := __lens.MakeLensWithName(
func(s url.URL) string { return s.Host },
func(s url.URL, v string) url.URL { s.Host = v; return s },
"URL.Host",
)
lensPath := __lens.MakeLensWithName(
func(s url.URL) string { return s.Path },
func(s url.URL, v string) url.URL { s.Path = v; return s },
"URL.Path",
)
lensRawPath := __lens.MakeLensWithName(
func(s url.URL) string { return s.RawPath },
func(s url.URL, v string) url.URL { s.RawPath = v; return s },
"URL.RawPath",
)
lensOmitHost := __lens.MakeLensWithName(
func(s url.URL) bool { return s.OmitHost },
func(s url.URL, v bool) url.URL { s.OmitHost = v; return s },
"URL.OmitHost",
)
lensForceQuery := __lens.MakeLensWithName(
func(s url.URL) bool { return s.ForceQuery },
func(s url.URL, v bool) url.URL { s.ForceQuery = v; return s },
"URL.ForceQuery",
)
lensRawQuery := __lens.MakeLensWithName(
func(s url.URL) string { return s.RawQuery },
func(s url.URL, v string) url.URL { s.RawQuery = v; return s },
"URL.RawQuery",
)
lensFragment := __lens.MakeLensWithName(
func(s url.URL) string { return s.Fragment },
func(s url.URL, v string) url.URL { s.Fragment = v; return s },
"URL.Fragment",
)
lensRawFragment := __lens.MakeLensWithName(
func(s url.URL) string { return s.RawFragment },
func(s url.URL, v string) url.URL { s.RawFragment = v; return s },
"URL.RawFragment",
)
// optional lenses
lensSchemeO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensScheme)
lensOpaqueO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensOpaque)
lensUserO := __lens_option.FromIso[url.URL](__iso_option.FromZero[*url.Userinfo]())(lensUser)
lensHostO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensHost)
lensPathO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensPath)
lensRawPathO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawPath)
lensOmitHostO := __lens_option.FromIso[url.URL](__iso_option.FromZero[bool]())(lensOmitHost)
lensForceQueryO := __lens_option.FromIso[url.URL](__iso_option.FromZero[bool]())(lensForceQuery)
lensRawQueryO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawQuery)
lensFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensFragment)
lensRawFragmentO := __lens_option.FromIso[url.URL](__iso_option.FromZero[string]())(lensRawFragment)
return URLLenses{
// mandatory lenses
Scheme: lensScheme,
Opaque: lensOpaque,
User: lensUser,
Host: lensHost,
Path: lensPath,
RawPath: lensRawPath,
OmitHost: lensOmitHost,
ForceQuery: lensForceQuery,
RawQuery: lensRawQuery,
Fragment: lensFragment,
RawFragment: lensRawFragment,
// optional lenses
SchemeO: lensSchemeO,
OpaqueO: lensOpaqueO,
UserO: lensUserO,
HostO: lensHostO,
PathO: lensPathO,
RawPathO: lensRawPathO,
OmitHostO: lensOmitHostO,
ForceQueryO: lensForceQueryO,
RawQueryO: lensRawQueryO,
FragmentO: lensFragmentO,
RawFragmentO: lensRawFragmentO,
}
}
// MakeURLRefLenses creates a new URLRefLenses with lenses for all fields
func MakeURLRefLenses() URLRefLenses {
// mandatory lenses
lensScheme := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Scheme },
func(s *url.URL, v string) *url.URL { s.Scheme = v; return s },
"(*url.URL).Scheme",
)
lensOpaque := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Opaque },
func(s *url.URL, v string) *url.URL { s.Opaque = v; return s },
"(*url.URL).Opaque",
)
lensUser := __lens.MakeLensStrictWithName(
func(s *url.URL) *url.Userinfo { return s.User },
func(s *url.URL, v *url.Userinfo) *url.URL { s.User = v; return s },
"(*url.URL).User",
)
lensHost := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Host },
func(s *url.URL, v string) *url.URL { s.Host = v; return s },
"(*url.URL).Host",
)
lensPath := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Path },
func(s *url.URL, v string) *url.URL { s.Path = v; return s },
"(*url.URL).Path",
)
lensRawPath := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.RawPath },
func(s *url.URL, v string) *url.URL { s.RawPath = v; return s },
"(*url.URL).RawPath",
)
lensOmitHost := __lens.MakeLensStrictWithName(
func(s *url.URL) bool { return s.OmitHost },
func(s *url.URL, v bool) *url.URL { s.OmitHost = v; return s },
"(*url.URL).OmitHost",
)
lensForceQuery := __lens.MakeLensStrictWithName(
func(s *url.URL) bool { return s.ForceQuery },
func(s *url.URL, v bool) *url.URL { s.ForceQuery = v; return s },
"(*url.URL).ForceQuery",
)
lensRawQuery := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.RawQuery },
func(s *url.URL, v string) *url.URL { s.RawQuery = v; return s },
"(*url.URL).RawQuery",
)
lensFragment := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.Fragment },
func(s *url.URL, v string) *url.URL { s.Fragment = v; return s },
"(*url.URL).Fragment",
)
lensRawFragment := __lens.MakeLensStrictWithName(
func(s *url.URL) string { return s.RawFragment },
func(s *url.URL, v string) *url.URL { s.RawFragment = v; return s },
"(*url.URL).RawFragment",
)
// optional lenses
lensSchemeO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensScheme)
lensOpaqueO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensOpaque)
lensUserO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[*url.Userinfo]())(lensUser)
lensHostO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensHost)
lensPathO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensPath)
lensRawPathO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawPath)
lensOmitHostO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[bool]())(lensOmitHost)
lensForceQueryO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[bool]())(lensForceQuery)
lensRawQueryO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawQuery)
lensFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensFragment)
lensRawFragmentO := __lens_option.FromIso[*url.URL](__iso_option.FromZero[string]())(lensRawFragment)
return URLRefLenses{
// mandatory lenses
Scheme: lensScheme,
Opaque: lensOpaque,
User: lensUser,
Host: lensHost,
Path: lensPath,
RawPath: lensRawPath,
OmitHost: lensOmitHost,
ForceQuery: lensForceQuery,
RawQuery: lensRawQuery,
Fragment: lensFragment,
RawFragment: lensRawFragment,
// optional lenses
SchemeO: lensSchemeO,
OpaqueO: lensOpaqueO,
UserO: lensUserO,
HostO: lensHostO,
PathO: lensPathO,
RawPathO: lensRawPathO,
OmitHostO: lensOmitHostO,
ForceQueryO: lensForceQueryO,
RawQueryO: lensRawQueryO,
FragmentO: lensFragmentO,
RawFragmentO: lensRawFragmentO,
}
}
// UserinfoRefLenses provides lenses for accessing fields of url.Userinfo via a reference to url.Userinfo
type UserinfoRefLenses struct {
// mandatory fields
Username __lens.Lens[*url.Userinfo, string]
Password __lens.Lens[*url.Userinfo, string]
// optional fields
UsernameO __lens_option.LensO[*url.Userinfo, string]
PasswordO __lens_option.LensO[*url.Userinfo, string]
}
// MakeUserinfoRefLenses creates a new UserinfoRefLenses with lenses for all fields
func MakeUserinfoRefLenses() UserinfoRefLenses {
// mandatory lenses
lensUsername := __lens.MakeLensStrictWithName(
(*url.Userinfo).Username,
func(s *url.Userinfo, v string) *url.Userinfo {
pwd, ok := s.Password()
if ok {
return url.UserPassword(v, pwd)
}
return url.User(v)
},
"(*url.Userinfo).Username",
)
lensPassword := __lens.MakeLensStrictWithName(
func(s *url.Userinfo) string {
pwd, _ := s.Password()
return pwd
},
func(s *url.Userinfo, v string) *url.Userinfo { return url.UserPassword(s.Username(), v) },
"(*url.Userinfo).Password",
)
// optional lenses
lensUsernameO := __lens_option.FromIso[*url.Userinfo](__iso_option.FromZero[string]())(lensUsername)
lensPasswordO := __lens.MakeLensStrictWithName(
__option.FromValidation((*url.Userinfo).Password),
func(s *url.Userinfo, v __option.Option[string]) *url.Userinfo {
return __option.MonadFold(v, func() *url.Userinfo { return url.User(s.Username()) }, func(pwd string) *url.Userinfo { return url.UserPassword(s.Username(), pwd) })
},
"(*url.Userinfo).Password",
)
return UserinfoRefLenses{
// mandatory lenses
Username: lensUsername,
Password: lensPassword,
// optional lenses
UsernameO: lensUsernameO,
PasswordO: lensPasswordO,
}
}

View File

@@ -0,0 +1,655 @@
// 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 lenses
import (
"errors"
"net/url"
"testing"
"github.com/IBM/fp-go/v2/option"
__option "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestUserinfoRefLenses_Username tests the Username lens
func TestUserinfoRefLenses_Username(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Get username from UserPassword", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
username := lenses.Username.Get(userinfo)
assert.Equal(t, "john", username)
})
t.Run("Get username from User", func(t *testing.T) {
userinfo := url.User("alice")
username := lenses.Username.Get(userinfo)
assert.Equal(t, "alice", username)
})
t.Run("Set username with password", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
updated := lenses.Username.Set("bob")(userinfo)
assert.Equal(t, "bob", updated.Username())
// Password should be preserved
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "secret123", pwd)
})
t.Run("Set username without password", func(t *testing.T) {
userinfo := url.User("alice")
updated := lenses.Username.Set("bob")(userinfo)
assert.Equal(t, "bob", updated.Username())
// Should still have no password
_, ok := updated.Password()
assert.False(t, ok)
})
}
// TestUserinfoRefLenses_Password tests the Password lens
func TestUserinfoRefLenses_Password(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Get password when present", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
password := lenses.Password.Get(userinfo)
assert.Equal(t, "secret123", password)
})
t.Run("Get password when absent", func(t *testing.T) {
userinfo := url.User("alice")
password := lenses.Password.Get(userinfo)
assert.Equal(t, "", password)
})
t.Run("Set password", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
updated := lenses.Password.Set("newpass")(userinfo)
assert.Equal(t, "john", updated.Username())
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "newpass", pwd)
})
t.Run("Set password on user without password", func(t *testing.T) {
userinfo := url.User("alice")
updated := lenses.Password.Set("newpass")(userinfo)
assert.Equal(t, "alice", updated.Username())
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "newpass", pwd)
})
}
// TestUserinfoRefLenses_UsernameO tests the optional Username lens
func TestUserinfoRefLenses_UsernameO(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Get non-empty username", func(t *testing.T) {
userinfo := url.User("john")
opt := lenses.UsernameO.Get(userinfo)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "john", value)
})
t.Run("Get empty username", func(t *testing.T) {
userinfo := url.User("")
opt := lenses.UsernameO.Get(userinfo)
assert.True(t, __option.IsNone(opt))
})
t.Run("Set Some username", func(t *testing.T) {
userinfo := url.User("")
updated := lenses.UsernameO.Set(__option.Some("alice"))(userinfo)
assert.Equal(t, "alice", updated.Username())
})
t.Run("Set None username", func(t *testing.T) {
userinfo := url.User("john")
updated := lenses.UsernameO.Set(__option.None[string]())(userinfo)
assert.Equal(t, "", updated.Username())
})
}
// TestUserinfoRefLenses_PasswordO tests the optional Password lens
func TestUserinfoRefLenses_PasswordO(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Get Some password when present", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
opt := lenses.PasswordO.Get(userinfo)
assert.True(t, __option.IsSome(opt))
value := __option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "secret123", value)
})
t.Run("Get None password when absent", func(t *testing.T) {
userinfo := url.User("alice")
opt := lenses.PasswordO.Get(userinfo)
assert.True(t, __option.IsNone(opt))
})
t.Run("Set Some password", func(t *testing.T) {
userinfo := url.User("john")
updated := lenses.PasswordO.Set(__option.Some("newpass"))(userinfo)
assert.Equal(t, "john", updated.Username())
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "newpass", pwd)
})
t.Run("Set None password removes it", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
updated := lenses.PasswordO.Set(__option.None[string]())(userinfo)
assert.Equal(t, "john", updated.Username())
_, ok := updated.Password()
assert.False(t, ok)
})
t.Run("Set None password on user without password", func(t *testing.T) {
userinfo := url.User("alice")
updated := lenses.PasswordO.Set(__option.None[string]())(userinfo)
assert.Equal(t, "alice", updated.Username())
_, ok := updated.Password()
assert.False(t, ok)
})
}
// TestUserinfoRefLenses_Composition tests composing lens operations
func TestUserinfoRefLenses_Composition(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Update both username and password", func(t *testing.T) {
userinfo := url.UserPassword("john", "secret123")
// Update username first
updated1 := lenses.Username.Set("alice")(userinfo)
// Then update password
updated2 := lenses.Password.Set("newpass")(updated1)
assert.Equal(t, "alice", updated2.Username())
pwd, ok := updated2.Password()
assert.True(t, ok)
assert.Equal(t, "newpass", pwd)
})
t.Run("Add password to user without password", func(t *testing.T) {
userinfo := url.User("bob")
updated := lenses.PasswordO.Set(__option.Some("pass123"))(userinfo)
assert.Equal(t, "bob", updated.Username())
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "pass123", pwd)
})
t.Run("Remove password from user with password", func(t *testing.T) {
userinfo := url.UserPassword("charlie", "oldpass")
updated := lenses.PasswordO.Set(__option.None[string]())(userinfo)
assert.Equal(t, "charlie", updated.Username())
_, ok := updated.Password()
assert.False(t, ok)
})
}
// TestUserinfoRefLenses_EdgeCases tests edge cases
func TestUserinfoRefLenses_EdgeCases(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Empty username and password", func(t *testing.T) {
userinfo := url.UserPassword("", "")
username := lenses.Username.Get(userinfo)
assert.Equal(t, "", username)
password := lenses.Password.Get(userinfo)
assert.Equal(t, "", password)
})
t.Run("Special characters in username", func(t *testing.T) {
userinfo := url.User("user@domain.com")
username := lenses.Username.Get(userinfo)
assert.Equal(t, "user@domain.com", username)
updated := lenses.Username.Set("new@user.com")(userinfo)
assert.Equal(t, "new@user.com", updated.Username())
})
t.Run("Special characters in password", func(t *testing.T) {
userinfo := url.UserPassword("john", "p@$$w0rd!")
password := lenses.Password.Get(userinfo)
assert.Equal(t, "p@$$w0rd!", password)
updated := lenses.Password.Set("n3w!p@ss")(userinfo)
pwd, ok := updated.Password()
assert.True(t, ok)
assert.Equal(t, "n3w!p@ss", pwd)
})
t.Run("Very long username", func(t *testing.T) {
longUsername := "verylongusernamethatexceedsnormallengthbutshouldbehanded"
userinfo := url.User(longUsername)
username := lenses.Username.Get(userinfo)
assert.Equal(t, longUsername, username)
})
t.Run("Very long password", func(t *testing.T) {
longPassword := "verylongpasswordthatexceedsnormallengthbutshouldbehanded"
userinfo := url.UserPassword("john", longPassword)
password := lenses.Password.Get(userinfo)
assert.Equal(t, longPassword, password)
})
}
// TestUserinfoRefLenses_Immutability tests that operations return new instances
func TestUserinfoRefLenses_Immutability(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Setting username returns new instance", func(t *testing.T) {
original := url.User("john")
updated := lenses.Username.Set("alice")(original)
// Original should be unchanged
assert.Equal(t, "john", original.Username())
// Updated should have new value
assert.Equal(t, "alice", updated.Username())
// Should be different instances
assert.NotSame(t, original, updated)
})
t.Run("Setting password returns new instance", func(t *testing.T) {
original := url.UserPassword("john", "pass1")
updated := lenses.Password.Set("pass2")(original)
// Original should be unchanged
pwd, _ := original.Password()
assert.Equal(t, "pass1", pwd)
// Updated should have new value
pwd, _ = updated.Password()
assert.Equal(t, "pass2", pwd)
// Should be different instances
assert.NotSame(t, original, updated)
})
t.Run("Multiple updates create new instances", func(t *testing.T) {
original := url.UserPassword("john", "pass1")
updated1 := lenses.Username.Set("alice")(original)
updated2 := lenses.Password.Set("pass2")(updated1)
// Original unchanged
assert.Equal(t, "john", original.Username())
pwd, _ := original.Password()
assert.Equal(t, "pass1", pwd)
// First update has new username, old password
assert.Equal(t, "alice", updated1.Username())
pwd, _ = updated1.Password()
assert.Equal(t, "pass1", pwd)
// Second update has new username and password
assert.Equal(t, "alice", updated2.Username())
pwd, _ = updated2.Password()
assert.Equal(t, "pass2", pwd)
// All different instances
assert.NotSame(t, original, updated1)
assert.NotSame(t, updated1, updated2)
assert.NotSame(t, original, updated2)
})
}
// TestUserinfoRefLenses_PasswordPresence tests password presence detection
func TestUserinfoRefLenses_PasswordPresence(t *testing.T) {
lenses := MakeUserinfoRefLenses()
t.Run("Distinguish between no password and empty password", func(t *testing.T) {
// User with no password
userNoPass := url.User("john")
optNoPass := lenses.PasswordO.Get(userNoPass)
assert.True(t, __option.IsNone(optNoPass))
// User with empty password (still has password set)
userEmptyPass := url.UserPassword("john", "")
optEmptyPass := lenses.PasswordO.Get(userEmptyPass)
// Empty password is still Some (password is set, just empty)
assert.True(t, __option.IsSome(optEmptyPass))
value := __option.GetOrElse(func() string { return "default" })(optEmptyPass)
assert.Equal(t, "", value)
})
t.Run("Password lens returns empty string for no password", func(t *testing.T) {
userinfo := url.User("john")
password := lenses.Password.Get(userinfo)
assert.Equal(t, "", password)
// But PasswordO returns None
opt := lenses.PasswordO.Get(userinfo)
assert.True(t, __option.IsNone(opt))
})
}
// TestErrorLenses tests lenses for url.Error
func TestErrorLenses(t *testing.T) {
lenses := MakeErrorLenses()
t.Run("Get and Set Op field", func(t *testing.T) {
urlErr := url.Error{
Op: "Get",
URL: "https://example.com",
Err: assert.AnError,
}
// Test Get
op := lenses.Op.Get(urlErr)
assert.Equal(t, "Get", op)
// Test Set (curried, returns new Error)
updated := lenses.Op.Set("Post")(urlErr)
assert.Equal(t, "Post", updated.Op)
assert.Equal(t, "Get", urlErr.Op) // Original unchanged
})
t.Run("Get and Set URL field", func(t *testing.T) {
urlErr := url.Error{
Op: "Get",
URL: "https://example.com",
Err: assert.AnError,
}
// Test Get
urlStr := lenses.URL.Get(urlErr)
assert.Equal(t, "https://example.com", urlStr)
// Test Set (curried)
updated := lenses.URL.Set("https://newsite.com")(urlErr)
assert.Equal(t, "https://newsite.com", updated.URL)
assert.Equal(t, "https://example.com", urlErr.URL) // Original unchanged
})
t.Run("Get and Set Err field", func(t *testing.T) {
originalErr := assert.AnError
urlErr := url.Error{
Op: "Get",
URL: "https://example.com",
Err: originalErr,
}
// Test Get
err := lenses.Err.Get(urlErr)
assert.Equal(t, originalErr, err)
// Test Set (curried)
newErr := errors.New("new error")
updated := lenses.Err.Set(newErr)(urlErr)
assert.Equal(t, newErr, updated.Err)
assert.Equal(t, originalErr, urlErr.Err) // Original unchanged
})
t.Run("Optional lenses", func(t *testing.T) {
urlErr := url.Error{
Op: "Get",
URL: "https://example.com",
Err: assert.AnError,
}
// Test OpO
opOpt := lenses.OpO.Get(urlErr)
assert.True(t, __option.IsSome(opOpt))
// Test with empty Op
emptyErr := url.Error{Op: "", URL: "test", Err: nil}
opOpt = lenses.OpO.Get(emptyErr)
assert.True(t, __option.IsNone(opOpt))
// Test URLO
urlOpt := lenses.URLO.Get(urlErr)
assert.True(t, __option.IsSome(urlOpt))
// Test ErrO
errOpt := lenses.ErrO.Get(urlErr)
assert.True(t, __option.IsSome(errOpt))
// Test with nil error
nilErrErr := url.Error{Op: "Get", URL: "test", Err: nil}
errOpt = lenses.ErrO.Get(nilErrErr)
assert.True(t, __option.IsNone(errOpt))
})
}
// TestErrorRefLenses tests reference lenses for url.Error
func TestErrorRefLenses(t *testing.T) {
lenses := MakeErrorRefLenses()
t.Run("Get and Set creates new instance", func(t *testing.T) {
urlErr := &url.Error{
Op: "Get",
URL: "https://example.com",
Err: assert.AnError,
}
// Test Get
op := lenses.Op.Get(urlErr)
assert.Equal(t, "Get", op)
// Test Set (creates copy)
updated := lenses.Op.Set("Post")(urlErr)
assert.Equal(t, "Post", updated.Op)
assert.Equal(t, "Get", urlErr.Op) // Original unchanged
assert.NotSame(t, urlErr, updated)
})
}
// TestURLLenses tests lenses for url.URL
func TestURLLenses(t *testing.T) {
lenses := MakeURLLenses()
t.Run("Get and Set Scheme", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com"}
scheme := lenses.Scheme.Get(u)
assert.Equal(t, "https", scheme)
updated := lenses.Scheme.Set("http")(u)
assert.Equal(t, "http", updated.Scheme)
assert.Equal(t, "https", u.Scheme) // Original unchanged
})
t.Run("Get and Set Host", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com"}
host := lenses.Host.Get(u)
assert.Equal(t, "example.com", host)
updated := lenses.Host.Set("newsite.com")(u)
assert.Equal(t, "newsite.com", updated.Host)
assert.Equal(t, "example.com", u.Host)
})
t.Run("Get and Set Path", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", Path: "/api/v1"}
path := lenses.Path.Get(u)
assert.Equal(t, "/api/v1", path)
updated := lenses.Path.Set("/api/v2")(u)
assert.Equal(t, "/api/v2", updated.Path)
assert.Equal(t, "/api/v1", u.Path)
})
t.Run("Get and Set RawQuery", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", RawQuery: "page=1"}
query := lenses.RawQuery.Get(u)
assert.Equal(t, "page=1", query)
updated := lenses.RawQuery.Set("page=2&limit=10")(u)
assert.Equal(t, "page=2&limit=10", updated.RawQuery)
assert.Equal(t, "page=1", u.RawQuery)
})
t.Run("Get and Set Fragment", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", Fragment: "section1"}
fragment := lenses.Fragment.Get(u)
assert.Equal(t, "section1", fragment)
updated := lenses.Fragment.Set("section2")(u)
assert.Equal(t, "section2", updated.Fragment)
assert.Equal(t, "section1", u.Fragment)
})
t.Run("Get and Set User", func(t *testing.T) {
userinfo := url.User("john")
u := url.URL{Scheme: "https", Host: "example.com", User: userinfo}
user := lenses.User.Get(u)
assert.Equal(t, userinfo, user)
newUser := url.UserPassword("alice", "pass")
updated := lenses.User.Set(newUser)(u)
assert.Equal(t, newUser, updated.User)
assert.Equal(t, userinfo, u.User)
})
t.Run("Boolean fields", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", ForceQuery: true}
forceQuery := lenses.ForceQuery.Get(u)
assert.True(t, forceQuery)
updated := lenses.ForceQuery.Set(false)(u)
assert.False(t, updated.ForceQuery)
assert.True(t, u.ForceQuery)
})
t.Run("Optional lenses", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", Path: "/test"}
// Non-empty scheme
schemeOpt := lenses.SchemeO.Get(u)
assert.True(t, __option.IsSome(schemeOpt))
// Empty RawQuery
queryOpt := lenses.RawQueryO.Get(u)
assert.True(t, __option.IsNone(queryOpt))
// Set Some
withQuery := lenses.RawQueryO.Set(__option.Some("q=test"))(u)
assert.Equal(t, "q=test", withQuery.RawQuery)
// Set None
cleared := lenses.RawQueryO.Set(__option.None[string]())(withQuery)
assert.Equal(t, "", cleared.RawQuery)
})
}
// TestURLRefLenses tests reference lenses for url.URL
func TestURLRefLenses(t *testing.T) {
lenses := MakeURLRefLenses()
t.Run("Creates new instances", func(t *testing.T) {
u := &url.URL{Scheme: "https", Host: "example.com", Path: "/api"}
// Test Get
scheme := lenses.Scheme.Get(u)
assert.Equal(t, "https", scheme)
// Test Set (creates copy)
updated := lenses.Scheme.Set("http")(u)
assert.Equal(t, "http", updated.Scheme)
assert.Equal(t, "https", u.Scheme) // Original unchanged
assert.NotSame(t, u, updated)
})
t.Run("Multiple field updates", func(t *testing.T) {
u := &url.URL{Scheme: "https", Host: "example.com"}
updated1 := lenses.Path.Set("/api/v1")(u)
updated2 := lenses.RawQuery.Set("page=1")(updated1)
updated3 := lenses.Fragment.Set("top")(updated2)
// Original unchanged
assert.Equal(t, "", u.Path)
assert.Equal(t, "", u.RawQuery)
assert.Equal(t, "", u.Fragment)
// Final result has all updates
assert.Equal(t, "/api/v1", updated3.Path)
assert.Equal(t, "page=1", updated3.RawQuery)
assert.Equal(t, "top", updated3.Fragment)
})
}
// TestURLLenses_ComplexScenarios tests complex URL manipulation scenarios
func TestURLLenses_ComplexScenarios(t *testing.T) {
lenses := MakeURLLenses()
t.Run("Build URL incrementally", func(t *testing.T) {
u := url.URL{}
u = lenses.Scheme.Set("https")(u)
u = lenses.Host.Set("api.example.com")(u)
u = lenses.Path.Set("/v1/users")(u)
u = lenses.RawQuery.Set("limit=10&offset=0")(u)
u = lenses.Fragment.Set("results")(u)
assert.Equal(t, "https", u.Scheme)
assert.Equal(t, "api.example.com", u.Host)
assert.Equal(t, "/v1/users", u.Path)
assert.Equal(t, "limit=10&offset=0", u.RawQuery)
assert.Equal(t, "results", u.Fragment)
})
t.Run("Update URL with authentication", func(t *testing.T) {
u := url.URL{
Scheme: "https",
Host: "example.com",
Path: "/api",
}
userinfo := url.UserPassword("admin", "secret")
updated := lenses.User.Set(userinfo)(u)
assert.NotNil(t, updated.User)
assert.Equal(t, "admin", updated.User.Username())
pwd, ok := updated.User.Password()
assert.True(t, ok)
assert.Equal(t, "secret", pwd)
})
t.Run("Clear optional fields", func(t *testing.T) {
u := url.URL{
Scheme: "https",
Host: "example.com",
Path: "/api",
RawQuery: "page=1",
Fragment: "top",
}
// Clear query and fragment
u = lenses.RawQueryO.Set(option.None[string]())(u)
u = lenses.FragmentO.Set(option.None[string]())(u)
assert.Equal(t, "", u.RawQuery)
assert.Equal(t, "", u.Fragment)
assert.Equal(t, "https", u.Scheme) // Other fields unchanged
assert.Equal(t, "example.com", u.Host)
})
}

View File

@@ -430,6 +430,8 @@ func FromNonZero[T comparable]() Prism[T, T] {
// // Groups: []string{"123"},
// // After: "",
// // }
//
// fp-go:Lens
type Match struct {
Before string // Text before the match
Groups []string // Capture groups (index 0 is full match)
@@ -599,6 +601,8 @@ func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
// // Full: "user@example.com",
// // After: "",
// // }
//
// fp-go:Lens
type NamedMatch struct {
Before string
Groups map[string]string
@@ -1097,3 +1101,163 @@ func FromOption[T any]() Prism[Option[T], T] {
func NonEmptyString() Prism[string, string] {
return FromNonZero[string]()
}
// ErrorPrisms provides prisms for accessing fields of url.Error
type ErrorPrisms struct {
Op Prism[url.Error, string]
URL Prism[url.Error, string]
Err Prism[url.Error, error]
}
// MakeErrorPrisms creates a new ErrorPrisms with prisms for all fields
func MakeErrorPrisms() ErrorPrisms {
_fromNonZeroOp := option.FromNonZero[string]()
_prismOp := MakePrismWithName(
func(s url.Error) Option[string] { return _fromNonZeroOp(s.Op) },
func(v string) url.Error {
return url.Error{Op: v}
},
"Error.Op",
)
_fromNonZeroURL := option.FromNonZero[string]()
_prismURL := MakePrismWithName(
func(s url.Error) Option[string] { return _fromNonZeroURL(s.URL) },
func(v string) url.Error {
return url.Error{URL: v}
},
"Error.URL",
)
_fromNonZeroErr := option.FromNonZero[error]()
_prismErr := MakePrismWithName(
func(s url.Error) Option[error] { return _fromNonZeroErr(s.Err) },
func(v error) url.Error {
return url.Error{Err: v}
},
"Error.Err",
)
return ErrorPrisms{
Op: _prismOp,
URL: _prismURL,
Err: _prismErr,
}
}
// URLPrisms provides prisms for accessing fields of url.URL
type URLPrisms struct {
Scheme Prism[url.URL, string]
Opaque Prism[url.URL, string]
User Prism[url.URL, *url.Userinfo]
Host Prism[url.URL, string]
Path Prism[url.URL, string]
RawPath Prism[url.URL, string]
OmitHost Prism[url.URL, bool]
ForceQuery Prism[url.URL, bool]
RawQuery Prism[url.URL, string]
Fragment Prism[url.URL, string]
RawFragment Prism[url.URL, string]
}
// MakeURLPrisms creates a new URLPrisms with prisms for all fields
func MakeURLPrisms() URLPrisms {
_fromNonZeroScheme := option.FromNonZero[string]()
_prismScheme := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroScheme(s.Scheme) },
func(v string) url.URL {
return url.URL{Scheme: v}
},
"URL.Scheme",
)
_fromNonZeroOpaque := option.FromNonZero[string]()
_prismOpaque := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroOpaque(s.Opaque) },
func(v string) url.URL {
return url.URL{Opaque: v}
},
"URL.Opaque",
)
_fromNonZeroUser := option.FromNonZero[*url.Userinfo]()
_prismUser := MakePrismWithName(
func(s url.URL) Option[*url.Userinfo] { return _fromNonZeroUser(s.User) },
func(v *url.Userinfo) url.URL {
return url.URL{User: v}
},
"URL.User",
)
_fromNonZeroHost := option.FromNonZero[string]()
_prismHost := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroHost(s.Host) },
func(v string) url.URL {
return url.URL{Host: v}
},
"URL.Host",
)
_fromNonZeroPath := option.FromNonZero[string]()
_prismPath := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroPath(s.Path) },
func(v string) url.URL {
return url.URL{Path: v}
},
"URL.Path",
)
_fromNonZeroRawPath := option.FromNonZero[string]()
_prismRawPath := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroRawPath(s.RawPath) },
func(v string) url.URL {
return url.URL{RawPath: v}
},
"URL.RawPath",
)
_fromNonZeroOmitHost := option.FromNonZero[bool]()
_prismOmitHost := MakePrismWithName(
func(s url.URL) Option[bool] { return _fromNonZeroOmitHost(s.OmitHost) },
func(v bool) url.URL {
return url.URL{OmitHost: v}
},
"URL.OmitHost",
)
_fromNonZeroForceQuery := option.FromNonZero[bool]()
_prismForceQuery := MakePrismWithName(
func(s url.URL) Option[bool] { return _fromNonZeroForceQuery(s.ForceQuery) },
func(v bool) url.URL {
return url.URL{ForceQuery: v}
},
"URL.ForceQuery",
)
_fromNonZeroRawQuery := option.FromNonZero[string]()
_prismRawQuery := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroRawQuery(s.RawQuery) },
func(v string) url.URL {
return url.URL{RawQuery: v}
},
"URL.RawQuery",
)
_fromNonZeroFragment := option.FromNonZero[string]()
_prismFragment := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroFragment(s.Fragment) },
func(v string) url.URL {
return url.URL{Fragment: v}
},
"URL.Fragment",
)
_fromNonZeroRawFragment := option.FromNonZero[string]()
_prismRawFragment := MakePrismWithName(
func(s url.URL) Option[string] { return _fromNonZeroRawFragment(s.RawFragment) },
func(v string) url.URL {
return url.URL{RawFragment: v}
},
"URL.RawFragment",
)
return URLPrisms{
Scheme: _prismScheme,
Opaque: _prismOpaque,
User: _prismUser,
Host: _prismHost,
Path: _prismPath,
RawPath: _prismRawPath,
OmitHost: _prismOmitHost,
ForceQuery: _prismForceQuery,
RawQuery: _prismRawQuery,
Fragment: _prismFragment,
RawFragment: _prismRawFragment,
}
}

View File

@@ -18,6 +18,7 @@ package prism
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
@@ -127,4 +128,6 @@ type (
Operator[S, A, B any] = func(Prism[S, A]) Prism[S, B]
Predicate[A any] = predicate.Predicate[A]
Lens[S, A any] = lens.Lens[S, A]
)

106
v2/optics/prism/url_test.go Normal file
View File

@@ -0,0 +1,106 @@
package prism
import (
"net/url"
"testing"
"github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestURLPrisms tests prisms for url.URL
func TestURLPrisms(t *testing.T) {
prisms := MakeURLPrisms()
t.Run("Scheme prism", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com"}
opt := prisms.Scheme.GetOption(u)
assert.True(t, option.IsSome(opt))
emptyU := url.URL{Scheme: "", Host: "example.com"}
opt = prisms.Scheme.GetOption(emptyU)
assert.True(t, option.IsNone(opt))
// ReverseGet
constructed := prisms.Scheme.ReverseGet("ftp")
assert.Equal(t, "ftp", constructed.Scheme)
})
t.Run("Host prism", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com"}
opt := prisms.Host.GetOption(u)
assert.True(t, option.IsSome(opt))
value := option.GetOrElse(func() string { return "" })(opt)
assert.Equal(t, "example.com", value)
})
t.Run("Path prism", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", Path: "/api"}
opt := prisms.Path.GetOption(u)
assert.True(t, option.IsSome(opt))
emptyPath := url.URL{Scheme: "https", Host: "example.com", Path: ""}
opt = prisms.Path.GetOption(emptyPath)
assert.True(t, option.IsNone(opt))
})
t.Run("User prism", func(t *testing.T) {
userinfo := url.User("john")
u := url.URL{Scheme: "https", Host: "example.com", User: userinfo}
opt := prisms.User.GetOption(u)
assert.True(t, option.IsSome(opt))
noUser := url.URL{Scheme: "https", Host: "example.com", User: nil}
opt = prisms.User.GetOption(noUser)
assert.True(t, option.IsNone(opt))
})
t.Run("Boolean prisms", func(t *testing.T) {
u := url.URL{Scheme: "https", Host: "example.com", ForceQuery: true}
opt := prisms.ForceQuery.GetOption(u)
assert.True(t, option.IsSome(opt))
noForce := url.URL{Scheme: "https", Host: "example.com", ForceQuery: false}
opt = prisms.ForceQuery.GetOption(noForce)
assert.True(t, option.IsNone(opt))
})
}
// TestErrorPrisms tests prisms for url.Error
func TestErrorPrisms(t *testing.T) {
prisms := MakeErrorPrisms()
t.Run("Op prism", func(t *testing.T) {
urlErr := url.Error{Op: "Get", URL: "test", Err: nil}
opt := prisms.Op.GetOption(urlErr)
assert.True(t, option.IsSome(opt))
emptyErr := url.Error{Op: "", URL: "test", Err: nil}
opt = prisms.Op.GetOption(emptyErr)
assert.True(t, option.IsNone(opt))
// ReverseGet
constructed := prisms.Op.ReverseGet("Post")
assert.Equal(t, "Post", constructed.Op)
})
t.Run("URL prism", func(t *testing.T) {
urlErr := url.Error{Op: "Get", URL: "https://example.com", Err: nil}
opt := prisms.URL.GetOption(urlErr)
assert.True(t, option.IsSome(opt))
emptyErr := url.Error{Op: "Get", URL: "", Err: nil}
opt = prisms.URL.GetOption(emptyErr)
assert.True(t, option.IsNone(opt))
})
t.Run("Err prism", func(t *testing.T) {
urlErr := url.Error{Op: "Get", URL: "test", Err: assert.AnError}
opt := prisms.Err.GetOption(urlErr)
assert.True(t, option.IsSome(opt))
nilErr := url.Error{Op: "Get", URL: "test", Err: nil}
opt = prisms.Err.GetOption(nilErr)
assert.True(t, option.IsNone(opt))
})
}

View File

@@ -61,7 +61,7 @@ func TraverseIter[A, B any](f Kleisli[A, B]) Kleisli[Seq[A], Seq[B]] {
Of[Seq[B]],
Map[Seq[B]],
MonadAp[Seq[B]],
Ap[Seq[B]],
f,
)

121
v2/reader/iter.go Normal file
View File

@@ -0,0 +1,121 @@
// 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 reader
import (
INTI "github.com/IBM/fp-go/v2/internal/iter"
)
// TraverseIter traverses an iterator sequence, applying a Reader-producing function to each element
// and collecting the results in a Reader that produces an iterator.
//
// This function transforms a sequence of values through a function that produces Readers,
// then "flips" the nesting so that instead of having an iterator of Readers, you get a
// single Reader that produces an iterator of values. All Readers share the same environment R.
//
// This is particularly useful when you have a collection of values that each need to be
// transformed using environment-dependent logic, and you want to defer the environment
// injection until the final execution.
//
// Type Parameters:
// - R: The shared environment/context type
// - A: The input element type in the iterator
// - B: The output element type in the resulting iterator
//
// Parameters:
// - f: A Kleisli arrow that transforms each element A into a Reader[R, B]
//
// Returns:
// - A Kleisli arrow that takes an iterator of A and returns a Reader producing an iterator of B
//
// Example:
//
// type Config struct { Multiplier int }
//
// // Function that creates a Reader for each number
// multiplyByConfig := func(x int) reader.Reader[Config, int] {
// return func(c Config) int { return x * c.Multiplier }
// }
//
// // Create an iterator of numbers
// numbers := func(yield func(int) bool) {
// yield(1)
// yield(2)
// yield(3)
// }
//
// // Traverse the iterator
// traversed := reader.TraverseIter(multiplyByConfig)(numbers)
//
// // Execute with config
// result := traversed(Config{Multiplier: 10})
// // result is an iterator that yields: 10, 20, 30
func TraverseIter[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, Seq[A], Seq[B]] {
return INTI.Traverse[Seq[A]](
Map[R, B],
Of[R, Seq[B]],
Map[R, Seq[B]],
Ap[Seq[B]],
f,
)
}
// SequenceIter sequences an iterator of Readers into a Reader that produces an iterator.
//
// This function "flips" the nesting of an iterator and Reader types. Given an iterator
// where each element is a Reader[R, A], it produces a single Reader[R, Seq[A]] that,
// when executed with an environment, evaluates all the Readers with that environment
// and collects their results into an iterator.
//
// This is a special case of TraverseIter where the transformation function is the identity.
// All Readers in the input iterator share the same environment R and are evaluated with it.
//
// Type Parameters:
// - R: The shared environment/context type
// - A: The result type produced by each Reader
//
// Parameters:
// - as: An iterator sequence where each element is a Reader[R, A]
//
// Returns:
// - A Reader that, when executed, produces an iterator of all the Reader results
//
// Example:
//
// type Config struct { Base int }
//
// // Create an iterator of Readers
// readers := func(yield func(reader.Reader[Config, int]) bool) {
// yield(func(c Config) int { return c.Base + 1 })
// yield(func(c Config) int { return c.Base + 2 })
// yield(func(c Config) int { return c.Base + 3 })
// }
//
// // Sequence the iterator
// sequenced := reader.SequenceIter(readers)
//
// // Execute with config
// result := sequenced(Config{Base: 10})
// // result is an iterator that yields: 11, 12, 13
func SequenceIter[R, A any](as Seq[Reader[R, A]]) Reader[R, Seq[A]] {
return INTI.MonadSequence(
Map[R](INTI.Of[Seq[A]]),
ApplicativeMonoid[R](INTI.Monoid[Seq[A]]()),
as,
)
}

403
v2/reader/iter_test.go Normal file
View File

@@ -0,0 +1,403 @@
// 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 reader
import (
"fmt"
"slices"
"strconv"
"testing"
INTI "github.com/IBM/fp-go/v2/internal/iter"
"github.com/stretchr/testify/assert"
)
// Helper function to collect iterator values into a slice
func collectIter[A any](seq Seq[A]) []A {
return INTI.ToArray[Seq[A], []A](seq)
}
// Helper function to create an iterator from a slice
func fromSlice[A any](items []A) Seq[A] {
return slices.Values(items)
}
func TestTraverseIter(t *testing.T) {
type Config struct {
Multiplier int
Prefix string
}
t.Run("traverses empty iterator", func(t *testing.T) {
empty := INTI.Empty[Seq[int]]()
multiplyByConfig := func(x int) Reader[Config, int] {
return func(c Config) int { return x * c.Multiplier }
}
traversed := TraverseIter(multiplyByConfig)(empty)
result := traversed(Config{Multiplier: 10})
collected := collectIter(result)
assert.Empty(t, collected)
})
t.Run("traverses single element iterator", func(t *testing.T) {
single := INTI.Of[Seq[int]](5)
multiplyByConfig := func(x int) Reader[Config, int] {
return func(c Config) int { return x * c.Multiplier }
}
traversed := TraverseIter(multiplyByConfig)(single)
result := traversed(Config{Multiplier: 3})
collected := collectIter(result)
assert.Equal(t, []int{15}, collected)
})
t.Run("traverses multiple elements", func(t *testing.T) {
numbers := INTI.From(1, 2, 3, 4)
multiplyByConfig := func(x int) Reader[Config, int] {
return func(c Config) int { return x * c.Multiplier }
}
traversed := TraverseIter(multiplyByConfig)(numbers)
result := traversed(Config{Multiplier: 10})
collected := collectIter(result)
assert.Equal(t, []int{10, 20, 30, 40}, collected)
})
t.Run("transforms types during traversal", func(t *testing.T) {
numbers := INTI.From(1, 2, 3)
intToString := func(x int) Reader[Config, string] {
return func(c Config) string {
return fmt.Sprintf("%s%d", c.Prefix, x)
}
}
traversed := TraverseIter(intToString)(numbers)
result := traversed(Config{Prefix: "num-"})
collected := collectIter(result)
assert.Equal(t, []string{"num-1", "num-2", "num-3"}, collected)
})
t.Run("all readers share same environment", func(t *testing.T) {
numbers := INTI.From(1, 2, 3)
// Each reader accesses the same config
addBase := func(x int) Reader[Config, int] {
return func(c Config) int {
return x + c.Multiplier
}
}
traversed := TraverseIter(addBase)(numbers)
result := traversed(Config{Multiplier: 100})
collected := collectIter(result)
assert.Equal(t, []int{101, 102, 103}, collected)
})
t.Run("works with complex transformations", func(t *testing.T) {
words := INTI.From("hello", "world")
wordLength := func(s string) Reader[Config, int] {
return func(c Config) int {
return len(s) * c.Multiplier
}
}
traversed := TraverseIter(wordLength)(words)
result := traversed(Config{Multiplier: 2})
collected := collectIter(result)
assert.Equal(t, []int{10, 10}, collected) // "hello" = 5*2, "world" = 5*2
})
t.Run("preserves order of elements", func(t *testing.T) {
numbers := fromSlice([]int{5, 3, 8, 1, 9})
identity := func(x int) Reader[Config, int] {
return Of[Config](x)
}
traversed := TraverseIter(identity)(numbers)
result := traversed(Config{})
collected := collectIter(result)
assert.Equal(t, []int{5, 3, 8, 1, 9}, collected)
})
t.Run("can be used with different config types", func(t *testing.T) {
type StringConfig struct {
Suffix string
}
words := INTI.From("test", "data")
addSuffix := func(s string) Reader[StringConfig, string] {
return func(c StringConfig) string {
return s + c.Suffix
}
}
traversed := TraverseIter(addSuffix)(words)
result := traversed(StringConfig{Suffix: ".txt"})
collected := collectIter(result)
assert.Equal(t, []string{"test.txt", "data.txt"}, collected)
})
}
func TestSequenceIter(t *testing.T) {
type Config struct {
Base int
Multiplier int
}
t.Run("sequences empty iterator", func(t *testing.T) {
empty := func(yield func(Reader[Config, int]) bool) {}
sequenced := SequenceIter(empty)
result := sequenced(Config{Base: 10})
collected := collectIter(result)
assert.Empty(t, collected)
})
t.Run("sequences single reader", func(t *testing.T) {
single := func(yield func(Reader[Config, int]) bool) {
yield(func(c Config) int { return c.Base + 5 })
}
sequenced := SequenceIter(single)
result := sequenced(Config{Base: 10})
collected := collectIter(result)
assert.Equal(t, []int{15}, collected)
})
t.Run("sequences multiple readers", func(t *testing.T) {
readers := INTI.From(
func(c Config) int { return c.Base + 1 },
func(c Config) int { return c.Base + 2 },
func(c Config) int { return c.Base + 3 },
)
sequenced := SequenceIter(readers)
result := sequenced(Config{Base: 10})
collected := collectIter(result)
assert.Equal(t, []int{11, 12, 13}, collected)
})
t.Run("all readers receive same environment", func(t *testing.T) {
readers := INTI.From(
func(c Config) int { return c.Base * c.Multiplier },
func(c Config) int { return c.Base + c.Multiplier },
func(c Config) int { return c.Base - c.Multiplier },
)
sequenced := SequenceIter(readers)
result := sequenced(Config{Base: 10, Multiplier: 3})
collected := collectIter(result)
assert.Equal(t, []int{30, 13, 7}, collected)
})
t.Run("works with string readers", func(t *testing.T) {
type StringConfig struct {
Prefix string
Suffix string
}
readers := INTI.From(
func(c StringConfig) string { return c.Prefix + "first" },
func(c StringConfig) string { return c.Prefix + "second" },
func(c StringConfig) string { return "third" + c.Suffix },
)
sequenced := SequenceIter(readers)
result := sequenced(StringConfig{Prefix: "pre-", Suffix: "-post"})
collected := collectIter(result)
assert.Equal(t, []string{"pre-first", "pre-second", "third-post"}, collected)
})
t.Run("preserves order of readers", func(t *testing.T) {
readers := INTI.From(
Of[Config](5),
Of[Config](3),
Of[Config](8),
Of[Config](1),
)
sequenced := SequenceIter(readers)
result := sequenced(Config{})
collected := collectIter(result)
assert.Equal(t, []int{5, 3, 8, 1}, collected)
})
t.Run("works with complex reader logic", func(t *testing.T) {
readers := INTI.From(
func(c Config) string {
return strconv.Itoa(c.Base * 2)
},
func(c Config) string {
return fmt.Sprintf("mult-%d", c.Multiplier)
},
func(c Config) string {
return fmt.Sprintf("sum-%d", c.Base+c.Multiplier)
},
)
sequenced := SequenceIter(readers)
result := sequenced(Config{Base: 5, Multiplier: 3})
collected := collectIter(result)
assert.Equal(t, []string{"10", "mult-3", "sum-8"}, collected)
})
t.Run("can handle large number of readers", func(t *testing.T) {
readers := func(yield func(Reader[Config, int]) bool) {
for i := 0; i < 100; i++ {
i := i // capture loop variable
yield(func(c Config) int { return c.Base + i })
}
}
sequenced := SequenceIter(readers)
result := sequenced(Config{Base: 1000})
collected := collectIter(result)
assert.Len(t, collected, 100)
assert.Equal(t, 1000, collected[0])
assert.Equal(t, 1099, collected[99])
})
}
func TestTraverseIterAndSequenceIterRelationship(t *testing.T) {
type Config struct {
Value int
}
t.Run("SequenceIter is TraverseIter with identity", func(t *testing.T) {
// Create an iterator of readers
readers := INTI.From(
func(c Config) int { return c.Value + 1 },
func(c Config) int { return c.Value + 2 },
func(c Config) int { return c.Value + 3 },
)
// Using SequenceIter
sequenced := SequenceIter(readers)
sequencedResult := sequenced(Config{Value: 10})
// Using TraverseIter with identity function
identity := Asks[Config, int]
traversed := TraverseIter(identity)(readers)
traversedResult := traversed(Config{Value: 10})
// Both should produce the same results
sequencedCollected := collectIter(sequencedResult)
traversedCollected := collectIter(traversedResult)
assert.Equal(t, sequencedCollected, traversedCollected)
assert.Equal(t, []int{11, 12, 13}, sequencedCollected)
})
}
func TestIteratorIntegration(t *testing.T) {
type AppConfig struct {
DatabaseURL string
APIKey string
Port int
}
t.Run("real-world example: processing configuration values", func(t *testing.T) {
// Iterator of field names
fields := INTI.From(
"database",
"api",
"port",
)
// Function that creates a reader for each field
getConfigValue := func(field string) Reader[AppConfig, string] {
return func(c AppConfig) string {
switch field {
case "database":
return c.DatabaseURL
case "api":
return c.APIKey
case "port":
return strconv.Itoa(c.Port)
default:
return "unknown"
}
}
}
// Traverse to get all config values
traversed := TraverseIter(getConfigValue)(fields)
result := traversed(AppConfig{
DatabaseURL: "postgres://localhost",
APIKey: "secret-key",
Port: 8080,
})
collected := collectIter(result)
assert.Equal(t, []string{
"postgres://localhost",
"secret-key",
"8080",
}, collected)
})
t.Run("real-world example: batch processing with shared config", func(t *testing.T) {
type ProcessConfig struct {
Prefix string
Suffix string
}
// Iterator of items to process
items := fromSlice([]string{"item1", "item2", "item3"})
// Processing function that uses config
processItem := func(item string) Reader[ProcessConfig, string] {
return func(c ProcessConfig) string {
return c.Prefix + item + c.Suffix
}
}
// Process all items with shared config
traversed := TraverseIter(processItem)(items)
result := traversed(ProcessConfig{
Prefix: "[",
Suffix: "]",
})
collected := collectIter(result)
assert.Equal(t, []string{"[item1]", "[item2]", "[item3]"}, collected)
})
}

View File

@@ -15,7 +15,11 @@
package reader
import "github.com/IBM/fp-go/v2/tailrec"
import (
"iter"
"github.com/IBM/fp-go/v2/tailrec"
)
type (
// Reader represents a computation that depends on a shared environment of type R and produces a value of type A.
@@ -96,4 +100,7 @@ type (
// without stack overflow. It's used for implementing stack-safe recursive algorithms
// in the context of Reader computations.
Trampoline[B, L any] = tailrec.Trampoline[B, L]
// Seq represents an iterator sequence over values of type T.
Seq[T any] = iter.Seq[T]
)

View File

@@ -16,6 +16,7 @@
package readerio
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
)
@@ -234,3 +235,54 @@ func Local[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIO[R1, A], A] {
return reader.Contramap[IO[A]](f)
}
// LocalIOK transforms the environment of a ReaderIO using an IO-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation before
// passing it to the ReaderIO.
//
// This is useful when the environment transformation itself requires IO effects,
// such as reading from a file, making a network call, or accessing system resources.
//
// The transformation happens in two stages:
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
// 2. The resulting R1 value is passed to the ReaderIO[R1, A] to produce the final result
//
// Type Parameters:
// - A: The result type produced by the ReaderIO
// - R1: The original environment type expected by the ReaderIO
// - R2: The new input environment type
//
// Parameters:
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
//
// Returns:
// - A Kleisli arrow that takes a ReaderIO[R1, A] and returns a ReaderIO[R2, A]
//
// Example:
//
// // Transform a config path into a loaded config
// loadConfig := func(path string) IO[Config] {
// return func() Config {
// // Load config from file
// return parseConfig(readFile(path))
// }
// }
//
// // Use the config to perform some operation
// useConfig := func(cfg Config) IO[string] {
// return Of("Using: " + cfg.Name)
// }
//
// // Compose them using LocalIOK
// result := LocalIOK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config and uses it
//
//go:inline
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) Kleisli[R2, ReaderIO[R1, A], A] {
return func(ri ReaderIO[R1, A]) ReaderIO[R2, A] {
return F.Flow2(
f,
io.Chain(ri),
)
}
}

View File

@@ -612,3 +612,133 @@ func TestRealWorldScenarios(t *testing.T) {
assert.Equal(t, "[API] Status: 200, Body: OK", result)
})
}
// TestLocalIOK tests LocalIOK functionality
func TestLocalIOK(t *testing.T) {
t.Run("basic IO transformation", func(t *testing.T) {
// IO effect that loads config from a path
loadConfig := func(path string) io.IO[SimpleConfig] {
return func() SimpleConfig {
// Simulate loading config
return SimpleConfig{Host: "localhost", Port: 8080}
}
}
// ReaderIO that uses the config
useConfig := func(cfg SimpleConfig) io.IO[string] {
return io.Of(fmt.Sprintf("%s:%d", cfg.Host, cfg.Port))
}
// Compose using LocalIOK
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(useConfig)
result := adapted("config.json")()
assert.Equal(t, "localhost:8080", result)
})
t.Run("IO transformation with side effects", func(t *testing.T) {
var loadLog []string
loadData := func(key string) io.IO[int] {
return func() int {
loadLog = append(loadLog, "Loading: "+key)
return len(key) * 10
}
}
processData := func(n int) io.IO[string] {
return io.Of(fmt.Sprintf("Processed: %d", n))
}
adapted := LocalIOK[string, int, string](loadData)(processData)
result := adapted("test")()
assert.Equal(t, "Processed: 40", result)
assert.Equal(t, []string{"Loading: test"}, loadLog)
})
t.Run("compose multiple LocalIOK", func(t *testing.T) {
// First transformation: string -> int
parseID := func(s string) io.IO[int] {
return func() int {
id, _ := strconv.Atoi(s)
return id
}
}
// Second transformation: int -> UserEnv
loadUser := func(id int) io.IO[UserEnv] {
return func() UserEnv {
return UserEnv{UserID: id}
}
}
// Use the UserEnv
formatUser := func(env UserEnv) io.IO[string] {
return io.Of(fmt.Sprintf("User ID: %d", env.UserID))
}
// Compose transformations
step1 := LocalIOK[string, UserEnv, int](loadUser)(formatUser)
step2 := LocalIOK[string, int, string](parseID)(step1)
result := step2("42")()
assert.Equal(t, "User ID: 42", result)
})
t.Run("environment extraction with IO", func(t *testing.T) {
// Extract database config from app config
extractDB := func(app AppConfig) io.IO[DatabaseConfig] {
return func() DatabaseConfig {
// Could perform validation or default setting here
cfg := app.Database
if cfg.Host == "" {
cfg.Host = "localhost"
}
return cfg
}
}
// Use the database config
connectDB := func(cfg DatabaseConfig) io.IO[string] {
return io.Of(fmt.Sprintf("Connected to %s:%d", cfg.Host, cfg.Port))
}
adapted := LocalIOK[string, DatabaseConfig, AppConfig](extractDB)(connectDB)
result := adapted(AppConfig{
Database: DatabaseConfig{Host: "", Port: 5432},
})()
assert.Equal(t, "Connected to localhost:5432", result)
})
t.Run("real-world: load and parse config file", func(t *testing.T) {
type ConfigFile struct {
Path string
}
// Simulate reading file content
readFile := func(cf ConfigFile) io.IO[string] {
return func() string {
return `{"host":"example.com","port":9000}`
}
}
// Parse the content
parseConfig := func(content string) io.IO[SimpleConfig] {
return io.Of(SimpleConfig{Host: "example.com", Port: 9000})
}
// Use the parsed config
useConfig := func(cfg SimpleConfig) io.IO[string] {
return io.Of(fmt.Sprintf("Using %s:%d", cfg.Host, cfg.Port))
}
// Compose the pipeline
step1 := LocalIOK[string, SimpleConfig, string](parseConfig)(useConfig)
step2 := LocalIOK[string, string, ConfigFile](readFile)(step1)
result := step2(ConfigFile{Path: "app.json"})()
assert.Equal(t, "Using example.com:9000", result)
})
}

View File

@@ -16,8 +16,11 @@
package readerioeither
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
// Promap is the profunctor map operation that transforms both the input and output of a ReaderIOEither.
@@ -74,3 +77,117 @@ func Promap[R, E, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, E, ReaderIOE
func Contramap[E, A, R1, R2 any](f func(R2) R1) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
return reader.Contramap[IOEither[E, A]](f)
}
// LocalIOK transforms the environment of a ReaderIOEither using an IO-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation before
// passing it to the ReaderIOEither.
//
// This is useful when the environment transformation itself requires IO effects,
// such as reading from a file, making a network call, or accessing system resources,
// but these effects cannot fail (or failures are not relevant to the error type E).
//
// The transformation happens in two stages:
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
// 2. The resulting R1 value is passed to the ReaderIOEither[R1, E, A] to produce the final result
//
// Type Parameters:
// - E: The error type (unchanged through the transformation)
// - A: The success type produced by the ReaderIOEither
// - R1: The original environment type expected by the ReaderIOEither
// - R2: The new input environment type
//
// Parameters:
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOEither[R1, E, A] and returns a ReaderIOEither[R2, E, A]
//
// Example:
//
// // Transform a config path into a loaded config (infallible)
// loadConfig := func(path string) IO[Config] {
// return func() Config {
// return getDefaultConfig() // Always succeeds
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOEither[error, string] {
// return func() Either[error, string] {
// if cfg.Valid {
// return Right[error]("Success")
// }
// return Left[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOK
// result := LocalIOK[error, string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config and uses it
//
//go:inline
func LocalIOK[E, A, R1, R2 any](f io.Kleisli[R2, R1]) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
return readerio.LocalIOK[Either[E, A]](f)
}
// LocalIOEitherK transforms the environment of a ReaderIOEither using an IOEither-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation that can fail before
// passing it to the ReaderIOEither.
//
// This is useful when the environment transformation itself requires IO effects that can fail,
// such as reading from a file that might not exist, making a network call that might timeout,
// or parsing data that might be invalid.
//
// The transformation happens in two stages:
// 1. The IOEither effect f is executed with the R2 environment to produce Either[E, R1]
// 2. If successful (Right), the R1 value is passed to the ReaderIOEither[R1, E, A]
// 3. If failed (Left), the error E is propagated without executing the ReaderIOEither
//
// Type Parameters:
// - A: The success type produced by the ReaderIOEither
// - R1: The original environment type expected by the ReaderIOEither
// - R2: The new input environment type
// - E: The error type (shared by both the transformation and the ReaderIOEither)
//
// Parameters:
// - f: An IOEither Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOEither[R1, E, A] and returns a ReaderIOEither[R2, E, A]
//
// Example:
//
// // Transform a config path into a loaded config (can fail)
// loadConfig := func(path string) IOEither[error, Config] {
// return func() Either[error, Config] {
// cfg, err := readConfigFile(path)
// if err != nil {
// return Left[Config](err)
// }
// return Right[error](cfg)
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOEither[error, string] {
// return func() Either[error, string] {
// if cfg.Valid {
// return Right[error]("Success: " + cfg.Name)
// }
// return Left[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOEitherK
// result := LocalIOEitherK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config (might fail) and uses it (might fail)
//
//go:inline
func LocalIOEitherK[A, R1, R2, E any](f ioeither.Kleisli[E, R2, R1]) Kleisli[R2, E, ReaderIOEither[R1, E, A], A] {
return func(ri ReaderIOEither[R1, E, A]) ReaderIOEither[R2, E, A] {
return function.Flow2(
f,
ioeither.Chain(ri),
)
}
}

View File

@@ -131,3 +131,248 @@ func TestPromapWithIO(t *testing.T) {
assert.Equal(t, 1, counter) // Side effect occurred
})
}
// TestLocalIOK tests LocalIOK functionality
func TestLocalIOK(t *testing.T) {
t.Run("basic IO transformation", func(t *testing.T) {
// IO effect that loads config from a path
loadConfig := func(path string) IOE.IO[SimpleConfig] {
return func() SimpleConfig {
// Simulate loading config
return SimpleConfig{Port: 8080}
}
}
// ReaderIOEither that uses the config
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
// Compose using LocalIOK
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(useConfig)
result := adapted("config.json")()
assert.Equal(t, E.Of[string]("Port: 8080"), result)
})
t.Run("IO transformation with side effects", func(t *testing.T) {
var loadLog []string
loadData := func(key string) IOE.IO[int] {
return func() int {
loadLog = append(loadLog, "Loading: "+key)
return len(key) * 10
}
}
processData := func(n int) IOEither[string, string] {
return IOE.Of[string]("Processed: " + strconv.Itoa(n))
}
adapted := LocalIOK[string, string, int, string](loadData)(processData)
result := adapted("test")()
assert.Equal(t, E.Of[string]("Processed: 40"), result)
assert.Equal(t, []string{"Loading: test"}, loadLog)
})
t.Run("error propagation in ReaderIOEither", func(t *testing.T) {
loadConfig := func(path string) IOE.IO[SimpleConfig] {
return func() SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// ReaderIOEither that returns an error
failingOperation := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Left[string]("operation failed")
}
adapted := LocalIOK[string, string, SimpleConfig, string](loadConfig)(failingOperation)
result := adapted("config.json")()
assert.Equal(t, E.Left[string]("operation failed"), result)
})
t.Run("compose multiple LocalIOK", func(t *testing.T) {
// First transformation: string -> int
parseID := func(s string) IOE.IO[int] {
return func() int {
id, _ := strconv.Atoi(s)
return id
}
}
// Second transformation: int -> SimpleConfig
loadConfig := func(id int) IOE.IO[SimpleConfig] {
return func() SimpleConfig {
return SimpleConfig{Port: 8000 + id}
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
// Compose transformations
step1 := LocalIOK[string, string, SimpleConfig, int](loadConfig)(formatConfig)
step2 := LocalIOK[string, string, int, string](parseID)(step1)
result := step2("42")()
assert.Equal(t, E.Of[string]("Port: 8042"), result)
})
}
// TestLocalIOEitherK tests LocalIOEitherK functionality
func TestLocalIOEitherK(t *testing.T) {
t.Run("basic IOEither transformation", func(t *testing.T) {
// IOEither effect that loads config from a path (can fail)
loadConfig := func(path string) IOEither[string, SimpleConfig] {
return func() E.Either[string, SimpleConfig] {
if path == "" {
return E.Left[SimpleConfig]("empty path")
}
return E.Of[string](SimpleConfig{Port: 8080})
}
}
// ReaderIOEither that uses the config
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
// Success case
result := adapted("config.json")()
assert.Equal(t, E.Of[string]("Port: 8080"), result)
// Failure case
resultErr := adapted("")()
assert.Equal(t, E.Left[string]("empty path"), resultErr)
})
t.Run("error propagation from environment transformation", func(t *testing.T) {
loadConfig := func(path string) IOEither[string, SimpleConfig] {
return func() E.Either[string, SimpleConfig] {
return E.Left[SimpleConfig]("file not found")
}
}
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(useConfig)
result := adapted("missing.json")()
// Error from loadConfig should propagate
assert.Equal(t, E.Left[string]("file not found"), result)
})
t.Run("error propagation from ReaderIOEither", func(t *testing.T) {
loadConfig := func(path string) IOEither[string, SimpleConfig] {
return IOE.Of[string](SimpleConfig{Port: 8080})
}
// ReaderIOEither that returns an error
failingOperation := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Left[string]("operation failed")
}
adapted := LocalIOEitherK[string, SimpleConfig, string, string](loadConfig)(failingOperation)
result := adapted("config.json")()
// Error from ReaderIOEither should propagate
assert.Equal(t, E.Left[string]("operation failed"), result)
})
t.Run("compose multiple LocalIOEitherK", func(t *testing.T) {
// First transformation: string -> int (can fail)
parseID := func(s string) IOEither[string, int] {
return func() E.Either[string, int] {
id, err := strconv.Atoi(s)
if err != nil {
return E.Left[int]("invalid ID")
}
return E.Of[string](id)
}
}
// Second transformation: int -> SimpleConfig (can fail)
loadConfig := func(id int) IOEither[string, SimpleConfig] {
return func() E.Either[string, SimpleConfig] {
if id < 0 {
return E.Left[SimpleConfig]("invalid ID")
}
return E.Of[string](SimpleConfig{Port: 8000 + id})
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Port: " + strconv.Itoa(cfg.Port))
}
// Compose transformations
step1 := LocalIOEitherK[string, SimpleConfig, int, string](loadConfig)(formatConfig)
step2 := LocalIOEitherK[string, int, string, string](parseID)(step1)
// Success case
result := step2("42")()
assert.Equal(t, E.Of[string]("Port: 8042"), result)
// Failure in first transformation
resultErr1 := step2("invalid")()
assert.Equal(t, E.Left[string]("invalid ID"), resultErr1)
// Failure in second transformation
resultErr2 := step2("-5")()
assert.Equal(t, E.Left[string]("invalid ID"), resultErr2)
})
t.Run("real-world: load and validate config", func(t *testing.T) {
type ConfigFile struct {
Path string
}
// Read file (can fail)
readFile := func(cf ConfigFile) IOEither[string, string] {
return func() E.Either[string, string] {
if cf.Path == "" {
return E.Left[string]("empty path")
}
return E.Of[string](`{"port":9000}`)
}
}
// Parse config (can fail)
parseConfig := func(content string) IOEither[string, SimpleConfig] {
return func() E.Either[string, SimpleConfig] {
if content == "" {
return E.Left[SimpleConfig]("empty content")
}
return E.Of[string](SimpleConfig{Port: 9000})
}
}
// Use the config
useConfig := func(cfg SimpleConfig) IOEither[string, string] {
return IOE.Of[string]("Using port: " + strconv.Itoa(cfg.Port))
}
// Compose the pipeline
step1 := LocalIOEitherK[string, SimpleConfig, string, string](parseConfig)(useConfig)
step2 := LocalIOEitherK[string, string, ConfigFile, string](readFile)(step1)
// Success case
result := step2(ConfigFile{Path: "app.json"})()
assert.Equal(t, E.Of[string]("Using port: 9000"), result)
// Failure case
resultErr := step2(ConfigFile{Path: ""})()
assert.Equal(t, E.Left[string]("empty path"), resultErr)
})
}

View File

@@ -16,6 +16,9 @@
package readerioresult
import (
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
RIOE "github.com/IBM/fp-go/v2/readerioeither"
)
@@ -71,3 +74,165 @@ func Promap[R, A, D, B any](f func(D) R, g func(A) B) Kleisli[D, ReaderIOResult[
func Contramap[A, R1, R2 any](f func(R2) R1) Kleisli[R2, ReaderIOResult[R1, A], A] {
return RIOE.Contramap[error, A](f)
}
// LocalIOK transforms the environment of a ReaderIOResult using an IO-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation before
// passing it to the ReaderIOResult.
//
// This is useful when the environment transformation itself requires IO effects,
// such as reading from a file, making a network call, or accessing system resources,
// but these effects cannot fail (or failures are not relevant).
//
// The transformation happens in two stages:
// 1. The IO effect f is executed with the R2 environment to produce an R1 value
// 2. The resulting R1 value is passed to the ReaderIOResult[R1, A] to produce the final result
//
// Type Parameters:
// - A: The success type produced by the ReaderIOResult
// - R1: The original environment type expected by the ReaderIOResult
// - R2: The new input environment type
//
// Parameters:
// - f: An IO Kleisli arrow that transforms R2 to R1 with IO effects
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
//
// Example:
//
// // Transform a config path into a loaded config (infallible)
// loadConfig := func(path string) IO[Config] {
// return func() Config {
// return getDefaultConfig() // Always succeeds
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOResult[string] {
// return func() Result[string] {
// if cfg.Valid {
// return Ok[string]("Success")
// }
// return Err[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOK
// result := LocalIOK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config and uses it
//
//go:inline
func LocalIOK[A, R1, R2 any](f io.Kleisli[R2, R1]) Kleisli[R2, ReaderIOResult[R1, A], A] {
return RIOE.LocalIOK[error, A](f)
}
// LocalIOEitherK transforms the environment of a ReaderIOResult using an IOEither-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation that can fail before
// passing it to the ReaderIOResult.
//
// This is useful when the environment transformation itself requires IO effects that can fail,
// such as reading from a file that might not exist, making a network call that might timeout,
// or parsing data that might be invalid.
//
// The transformation happens in two stages:
// 1. The IOEither effect f is executed with the R2 environment to produce Either[error, R1]
// 2. If successful (Right), the R1 value is passed to the ReaderIOResult[R1, A]
// 3. If failed (Left), the error is propagated without executing the ReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderIOResult
// - R1: The original environment type expected by the ReaderIOResult
// - R2: The new input environment type
//
// Parameters:
// - f: An IOEither Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
//
// Example:
//
// // Transform a config path into a loaded config (can fail)
// loadConfig := func(path string) IOEither[error, Config] {
// return func() Either[error, Config] {
// cfg, err := readConfigFile(path)
// if err != nil {
// return Left[Config](err)
// }
// return Right[error](cfg)
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOResult[string] {
// return func() Result[string] {
// if cfg.Valid {
// return Ok[string]("Success: " + cfg.Name)
// }
// return Err[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOEitherK
// result := LocalIOEitherK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config (might fail) and uses it (might fail)
//
//go:inline
func LocalIOEitherK[A, R1, R2 any](f ioeither.Kleisli[error, R2, R1]) Kleisli[R2, ReaderIOResult[R1, A], A] {
return RIOE.LocalIOEitherK[A](f)
}
// LocalIOResultK transforms the environment of a ReaderIOResult using an IOResult-based Kleisli arrow.
// It allows you to modify the environment through an effectful computation that can fail before
// passing it to the ReaderIOResult.
//
// This is a type-safe alias for LocalIOEitherK specialized for error type, providing a more
// idiomatic API when working with Result types (which use error as the error type).
//
// The transformation happens in two stages:
// 1. The IOResult effect f is executed with the R2 environment to produce Result[R1]
// 2. If successful (Ok), the R1 value is passed to the ReaderIOResult[R1, A]
// 3. If failed (Err), the error is propagated without executing the ReaderIOResult
//
// Type Parameters:
// - A: The success type produced by the ReaderIOResult
// - R1: The original environment type expected by the ReaderIOResult
// - R2: The new input environment type
//
// Parameters:
// - f: An IOResult Kleisli arrow that transforms R2 to R1 with IO effects that can fail
//
// Returns:
// - A Kleisli arrow that takes a ReaderIOResult[R1, A] and returns a ReaderIOResult[R2, A]
//
// Example:
//
// // Transform a config path into a loaded config (can fail)
// loadConfig := func(path string) IOResult[Config] {
// return func() Result[Config] {
// cfg, err := readConfigFile(path)
// if err != nil {
// return Err[Config](err)
// }
// return Ok(cfg)
// }
// }
//
// // Use the config to perform an operation that might fail
// useConfig := func(cfg Config) IOResult[string] {
// return func() Result[string] {
// if cfg.Valid {
// return Ok("Success: " + cfg.Name)
// }
// return Err[string](errors.New("invalid config"))
// }
// }
//
// // Compose them using LocalIOResultK
// result := LocalIOResultK[string, Config, string](loadConfig)(useConfig)
// output := result("config.json")() // Loads config (might fail) and uses it (might fail)
//
//go:inline
func LocalIOResultK[A, R1, R2 any](f ioresult.Kleisli[R2, R1]) Kleisli[R2, ReaderIOResult[R1, A], A] {
return RIOE.LocalIOEitherK[A](f)
}

View File

@@ -17,10 +17,12 @@ package readerioresult
import (
"context"
"errors"
"fmt"
"log"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
R "github.com/IBM/fp-go/v2/reader"
@@ -323,3 +325,297 @@ func TestReadIO(t *testing.T) {
assert.Equal(t, "Processing user alice (ID: 123)", result.GetOrElse(func(error) string { return "" })(res))
})
}
// TestLocalIOK tests LocalIOK functionality
func TestLocalIOK(t *testing.T) {
type SimpleConfig struct {
Port int
}
t.Run("basic IO transformation", func(t *testing.T) {
// IO effect that loads config from a path
loadConfig := func(path string) IO[SimpleConfig] {
return func() SimpleConfig {
// Simulate loading config
return SimpleConfig{Port: 8080}
}
}
// ReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
// Compose using LocalIOK
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(useConfig)
res := adapted("config.json")()
assert.Equal(t, result.Of("Port: 8080"), res)
})
t.Run("IO transformation with side effects", func(t *testing.T) {
var loadLog []string
loadData := func(key string) IO[int] {
return func() int {
loadLog = append(loadLog, "Loading: "+key)
return len(key) * 10
}
}
processData := func(n int) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Processed: %d", n))
}
}
adapted := LocalIOK[string, int, string](loadData)(processData)
res := adapted("test")()
assert.Equal(t, result.Of("Processed: 40"), res)
assert.Equal(t, []string{"Loading: test"}, loadLog)
})
t.Run("error propagation in ReaderIOResult", func(t *testing.T) {
loadConfig := func(path string) IO[SimpleConfig] {
return func() SimpleConfig {
return SimpleConfig{Port: 8080}
}
}
// ReaderIOResult that returns an error
failingOperation := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Left[string](errors.New("operation failed"))
}
}
adapted := LocalIOK[string, SimpleConfig, string](loadConfig)(failingOperation)
res := adapted("config.json")()
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOEitherK tests LocalIOEitherK functionality
func TestLocalIOEitherK(t *testing.T) {
type SimpleConfig struct {
Port int
}
t.Run("basic IOEither transformation", func(t *testing.T) {
// IOEither effect that loads config from a path (can fail)
loadConfig := func(path string) IOEither[error, SimpleConfig] {
return func() Either[error, SimpleConfig] {
if path == "" {
return E.Left[SimpleConfig](errors.New("empty path"))
}
return E.Of[error](SimpleConfig{Port: 8080})
}
}
// ReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
// Compose using LocalIOEitherK
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")()
assert.True(t, result.IsLeft(resErr))
})
t.Run("error propagation from environment transformation", func(t *testing.T) {
loadConfig := func(path string) IOEither[error, SimpleConfig] {
return func() Either[error, SimpleConfig] {
return E.Left[SimpleConfig](errors.New("file not found"))
}
}
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(useConfig)
res := adapted("missing.json")()
// Error from loadConfig should propagate
assert.True(t, result.IsLeft(res))
})
t.Run("error propagation from ReaderIOResult", func(t *testing.T) {
loadConfig := func(path string) IOEither[error, SimpleConfig] {
return func() Either[error, SimpleConfig] {
return E.Of[error](SimpleConfig{Port: 8080})
}
}
// ReaderIOResult that returns an error
failingOperation := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Left[string](errors.New("operation failed"))
}
}
adapted := LocalIOEitherK[string, SimpleConfig, string](loadConfig)(failingOperation)
res := adapted("config.json")()
// Error from ReaderIOResult should propagate
assert.True(t, result.IsLeft(res))
})
}
// TestLocalIOResultK tests LocalIOResultK functionality
func TestLocalIOResultK(t *testing.T) {
type SimpleConfig struct {
Port int
}
t.Run("basic IOResult transformation", func(t *testing.T) {
// IOResult effect that loads config from a path (can fail)
loadConfig := func(path string) IOResult[SimpleConfig] {
return func() Result[SimpleConfig] {
if path == "" {
return result.Left[SimpleConfig](errors.New("empty path"))
}
return result.Of(SimpleConfig{Port: 8080})
}
}
// ReaderIOResult that uses the config
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
// Compose using LocalIOResultK
adapted := LocalIOResultK[string, SimpleConfig, string](loadConfig)(useConfig)
// Success case
res := adapted("config.json")()
assert.Equal(t, result.Of("Port: 8080"), res)
// Failure case
resErr := adapted("")()
assert.True(t, result.IsLeft(resErr))
})
t.Run("error propagation from environment transformation", func(t *testing.T) {
loadConfig := func(path string) IOResult[SimpleConfig] {
return func() Result[SimpleConfig] {
return result.Left[SimpleConfig](errors.New("file not found"))
}
}
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
adapted := LocalIOResultK[string, SimpleConfig, string](loadConfig)(useConfig)
res := adapted("missing.json")()
// Error from loadConfig should propagate
assert.True(t, result.IsLeft(res))
})
t.Run("compose multiple LocalIOResultK", func(t *testing.T) {
// First transformation: string -> int (can fail)
parseID := func(s string) IOResult[int] {
return func() Result[int] {
if s == "" {
return result.Left[int](errors.New("empty string"))
}
return result.Of(len(s) * 10)
}
}
// Second transformation: int -> SimpleConfig (can fail)
loadConfig := func(id int) IOResult[SimpleConfig] {
return func() Result[SimpleConfig] {
if id < 0 {
return result.Left[SimpleConfig](errors.New("invalid ID"))
}
return result.Of(SimpleConfig{Port: 8000 + id})
}
}
// Use the config
formatConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Port: %d", cfg.Port))
}
}
// Compose transformations
step1 := LocalIOResultK[string, SimpleConfig, int](loadConfig)(formatConfig)
step2 := LocalIOResultK[string, int, string](parseID)(step1)
// Success case
res := step2("test")()
assert.Equal(t, result.Of("Port: 8040"), res)
// Failure in first transformation
resErr1 := step2("")()
assert.True(t, result.IsLeft(resErr1))
})
t.Run("real-world: load and validate config", func(t *testing.T) {
type ConfigFile struct {
Path string
}
// Read file (can fail)
readFile := func(cf ConfigFile) IOResult[string] {
return func() Result[string] {
if cf.Path == "" {
return result.Left[string](errors.New("empty path"))
}
return result.Of(`{"port":9000}`)
}
}
// Parse config (can fail)
parseConfig := func(content string) IOResult[SimpleConfig] {
return func() Result[SimpleConfig] {
if content == "" {
return result.Left[SimpleConfig](errors.New("empty content"))
}
return result.Of(SimpleConfig{Port: 9000})
}
}
// Use the config
useConfig := func(cfg SimpleConfig) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Using port: %d", cfg.Port))
}
}
// Compose the pipeline
step1 := LocalIOResultK[string, SimpleConfig, string](parseConfig)(useConfig)
step2 := LocalIOResultK[string, string, ConfigFile](readFile)(step1)
// Success case
res := step2(ConfigFile{Path: "app.json"})()
assert.Equal(t, result.Of("Using port: 9000"), res)
// Failure case
resErr := step2(ConfigFile{Path: ""})()
assert.True(t, result.IsLeft(resErr))
})
}

View File

@@ -0,0 +1,72 @@
package readerreaderioeither
import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerioeither"
)
//go:inline
func Local[C, E, A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return reader.Local[ReaderIOEither[C, E, A]](f)
}
//go:inline
func LocalIOK[C, E, A, R1, R2 any](f io.Kleisli[R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow4(
f,
io.Map(rri),
readerioeither.FromIO[C],
readerioeither.Flatten,
)
}
}
//go:inline
func LocalIOEitherK[C, A, R1, R2, E any](f ioeither.Kleisli[E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow4(
f,
ioeither.Map[E](rri),
readerioeither.FromIOEither[C],
readerioeither.Flatten,
)
}
}
//go:inline
func LocalEitherK[C, A, R1, R2, E any](f either.Kleisli[E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow4(
f,
either.Map[E](rri),
readerioeither.FromEither[C],
readerioeither.Flatten,
)
}
}
//go:inline
func LocalReaderIOEitherK[A, C, E, R1, R2 any](f readerioeither.Kleisli[C, E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow3(
f,
readerioeither.Map[C, E](rri),
readerioeither.Flatten,
)
}
}
//go:inline
func LocalReaderReaderIOEitherK[A, C, E, R1, R2 any](f Kleisli[R2, C, E, R2, R1]) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return func(rri ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return F.Flow2(
reader.AsksReader(f),
readerioeither.Chain(rri),
)
}
}

View File

@@ -271,6 +271,15 @@ func ChainReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R,
)
}
//go:inline
func ChainReaderIOEitherK[C, R, E, A, B any](f RIOE.Kleisli[R, E, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
Chain[R, C, E, A, B],
FromReaderIOEither[C, E, R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -595,11 +604,6 @@ func MapLeft[R, C, A, E1, E2 any](f func(E1) E2) func(ReaderReaderIOEither[R, C,
return reader.Map[R](RIOE.MapLeft[C, A](f))
}
//go:inline
func Local[C, E, A, R1, R2 any](f func(R2) R1) func(ReaderReaderIOEither[R1, C, E, A]) ReaderReaderIOEither[R2, C, E, A] {
return reader.Local[ReaderIOEither[C, E, A]](f)
}
//go:inline
func Read[C, E, A, R any](r R) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return reader.Read[ReaderIOEither[C, E, A]](r)
@@ -659,3 +663,13 @@ func Delay[R, C, E, A any](delay time.Duration) Operator[R, C, E, A, A] {
func After[R, C, E, A any](timestamp time.Time) Operator[R, C, E, A, A] {
return reader.Map[R](RIOE.After[C, E, A](timestamp))
}
func Defer[R, C, E, A any](fa Lazy[ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return func(r R) ReaderIOEither[C, E, A] {
return func(c C) RIOE.IOEither[E, A] {
return func() IOE.Either[E, A] {
return fa()(r)(c)()
}
}
}
}

View File

@@ -0,0 +1,15 @@
package readerreaderioeither
import (
RA "github.com/IBM/fp-go/v2/internal/array"
)
func TraverseArray[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Kleisli[R, C, E, []A, []B] {
return RA.Traverse[[]A, []B](
Of,
Map,
Ap,
f,
)
}

View File

@@ -20,6 +20,8 @@
package result
import (
"fmt"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
@@ -603,3 +605,54 @@ func MonadAlt[A any](fa Result[A], that Lazy[Result[A]]) Result[A] {
func Zero[A any]() Result[A] {
return either.Zero[error, A]()
}
// InstanceOf attempts to perform a type assertion on an any value to convert it to type A.
// If the type assertion succeeds, it returns a Right containing the converted value.
// If the type assertion fails, it returns a Left containing an error describing the type mismatch.
//
// This function is useful for safely converting interface{}/any values to concrete types
// in a functional programming style, where type assertion failures are represented as
// Left values rather than panics or boolean checks.
//
// Type Parameters:
// - A: The target type to convert to
//
// Parameters:
// - a: The value of type any to be type-asserted
//
// Returns:
// - Result[A]: Right(value) if type assertion succeeds, Left(error) if it fails
//
// Example:
//
// // Successful type assertion
// var value any = 42
// result := result.InstanceOf[int](value) // Right(42)
//
// // Failed type assertion
// var value any = "hello"
// result := result.InstanceOf[int](value) // Left(error: "expected int, got string")
//
// // Using with pipe for safe type conversion
// var data any = 3.14
// result := F.Pipe1(
// data,
// result.InstanceOf[float64],
// ) // Right(3.14)
//
// // Chaining with other operations
// var value any = 10
// result := F.Pipe2(
// value,
// result.InstanceOf[int],
// result.Map(func(n int) int { return n * 2 }),
// ) // Right(20)
//
//go:inline
func InstanceOf[A any](a any) Result[A] {
var res, ok = a.(A)
if ok {
return Of(res)
}
return Left[A](fmt.Errorf("expected %T, got %T", res, a))
}

View File

@@ -183,3 +183,176 @@ func TestZeroEqualsDefaultInitialization(t *testing.T) {
assert.Equal(t, IsRight(defaultInit), IsRight(zero), "Both should be Right")
assert.Equal(t, IsLeft(defaultInit), IsLeft(zero), "Both should not be Left")
}
// TestInstanceOf tests the InstanceOf function for type assertions
func TestInstanceOf(t *testing.T) {
// Test successful type assertion with int
t.Run("successful int assertion", func(t *testing.T) {
var value any = 42
result := InstanceOf[int](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right(42), result)
})
// Test successful type assertion with string
t.Run("successful string assertion", func(t *testing.T) {
var value any = "hello"
result := InstanceOf[string](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right("hello"), result)
})
// Test successful type assertion with float64
t.Run("successful float64 assertion", func(t *testing.T) {
var value any = 3.14
result := InstanceOf[float64](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right(3.14), result)
})
// Test successful type assertion with pointer
t.Run("successful pointer assertion", func(t *testing.T) {
val := 42
var value any = &val
result := InstanceOf[*int](value)
assert.True(t, IsRight(result))
v, err := UnwrapError(result)
assert.NoError(t, err)
assert.Equal(t, 42, *v)
})
// Test successful type assertion with struct
t.Run("successful struct assertion", func(t *testing.T) {
type Person struct {
Name string
Age int
}
var value any = Person{Name: "Alice", Age: 30}
result := InstanceOf[Person](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right(Person{Name: "Alice", Age: 30}), result)
})
// Test failed type assertion - int to string
t.Run("failed int to string assertion", func(t *testing.T) {
var value any = 42
result := InstanceOf[string](value)
assert.True(t, IsLeft(result))
_, err := UnwrapError(result)
assert.Error(t, err)
assert.Contains(t, err.Error(), "expected")
assert.Contains(t, err.Error(), "got")
})
// Test failed type assertion - string to int
t.Run("failed string to int assertion", func(t *testing.T) {
var value any = "hello"
result := InstanceOf[int](value)
assert.True(t, IsLeft(result))
_, err := UnwrapError(result)
assert.Error(t, err)
})
// Test failed type assertion - int to float64
t.Run("failed int to float64 assertion", func(t *testing.T) {
var value any = 42
result := InstanceOf[float64](value)
assert.True(t, IsLeft(result))
})
// Test with nil value
t.Run("nil value assertion", func(t *testing.T) {
var value any = nil
result := InstanceOf[string](value)
assert.True(t, IsLeft(result))
})
// Test chaining with Map
t.Run("chaining with Map", func(t *testing.T) {
var value any = 10
result := F.Pipe2(
value,
InstanceOf[int],
Map(func(n int) int { return n * 2 }),
)
assert.Equal(t, Right(20), result)
})
// Test chaining with Map on failed assertion
t.Run("chaining with Map on failed assertion", func(t *testing.T) {
var value any = "not a number"
result := F.Pipe2(
value,
InstanceOf[int],
Map(func(n int) int { return n * 2 }),
)
assert.True(t, IsLeft(result))
})
// Test with Chain for dependent operations
t.Run("chaining with Chain", func(t *testing.T) {
var value any = 5
result := F.Pipe2(
value,
InstanceOf[int],
Chain(func(n int) Result[string] {
if n > 0 {
return Right(fmt.Sprintf("positive: %d", n))
}
return Left[string](errors.New("not positive"))
}),
)
assert.Equal(t, Right("positive: 5"), result)
})
// Test with GetOrElse for default value
t.Run("GetOrElse with failed assertion", func(t *testing.T) {
var value any = "not an int"
result := F.Pipe2(
value,
InstanceOf[int],
GetOrElse(func(err error) int { return -1 }),
)
assert.Equal(t, -1, result)
})
// Test with GetOrElse for successful assertion
t.Run("GetOrElse with successful assertion", func(t *testing.T) {
var value any = 42
result := F.Pipe2(
value,
InstanceOf[int],
GetOrElse(func(err error) int { return -1 }),
)
assert.Equal(t, 42, result)
})
// Test with interface type
t.Run("interface type assertion", func(t *testing.T) {
var value any = errors.New("test error")
result := InstanceOf[error](value)
assert.True(t, IsRight(result))
v, err := UnwrapError(result)
assert.NoError(t, err)
assert.Equal(t, "test error", v.Error())
})
// Test with slice type
t.Run("slice type assertion", func(t *testing.T) {
var value any = []int{1, 2, 3}
result := InstanceOf[[]int](value)
assert.True(t, IsRight(result))
assert.Equal(t, Right([]int{1, 2, 3}), result)
})
// Test with map type
t.Run("map type assertion", func(t *testing.T) {
var value any = map[string]int{"a": 1, "b": 2}
result := InstanceOf[map[string]int](value)
assert.True(t, IsRight(result))
v, err := UnwrapError(result)
assert.NoError(t, err)
assert.Equal(t, 1, v["a"])
assert.Equal(t, 2, v["b"])
})
}

View File

@@ -0,0 +1,235 @@
# 🏗️ Builder Pattern with fp-go
This package demonstrates a functional builder pattern using fp-go's optics library. It shows how to construct and validate objects using lenses, prisms, and codecs, separating the building phase from validation.
## 📋 Overview
The builder pattern here uses two key types:
- **`PartialPerson`** 🚧: An intermediate type with unvalidated fields (raw `string` and `int`)
- **`Person`** ✅: A validated type with refined fields (`NonEmptyString` and `AdultAge`)
The pattern provides two approaches for validation:
1. **Prism-based validation** 🔍 (simple, no error messages)
2. **Codec-based validation** 📝 (detailed error reporting)
## 🎯 Core Concepts
### 1. 🔧 Auto-Generated Lenses
The `fp-go:Lens` directive in `types.go` generates lens accessors for both types:
```go
// fp-go:Lens
type PartialPerson struct {
name string
age int
}
// fp-go:Lens
type Person struct {
Name NonEmptyString
Age AdultAge
}
```
This generates:
- `partialPersonLenses` with `.name` and `.age` lenses
- `personLenses` with `.Name` and `.Age` lenses
### 2. 🎁 Exporting Setters as `WithXXX` Methods
The lens setters are exported as builder methods:
```go
// WithName sets the Name field of a PartialPerson
WithName = partialPersonLenses.name.Set
// WithAge sets the Age field of a PartialPerson
WithAge = partialPersonLenses.age.Set
```
These return `Endomorphism[*PartialPerson]` functions that can be composed:
```go
builder := F.Pipe1(
A.From(
WithName("Alice"),
WithAge(25),
),
allOfPartialPerson,
)
partial := builder(&PartialPerson{})
```
Or use the convenience function:
```go
builder := MakePerson("Alice", 25)
```
## 🔍 Approach 1: Prism-Based Validation (No Error Messages)
### Creating Validation Prisms
Define prisms that validate individual fields:
> 💡 **Tip**: The `optics/prism` package provides many helpful out-of-the-box prisms for common validations, including:
> - `NonEmptyString()` - validates non-empty strings
> - `ParseInt()`, `ParseInt64()` - parses integers from strings
> - `ParseFloat32()`, `ParseFloat64()` - parses floats from strings
> - `ParseBool()` - parses booleans from strings
> - `ParseDate(layout)` - parses dates with custom layouts
> - `ParseURL()` - parses URLs
> - `FromZero()`, `FromNonZero()` - validates zero/non-zero values
> - `RegexMatcher()`, `RegexNamedMatcher()` - regex-based validation
> - `FromOption()`, `FromEither()`, `FromResult()` - extracts from monadic types
> - And many more! Check `optics/prism/prisms.go` for the full list.
>
> For custom validation logic, create your own prisms:
```go
namePrism = prism.MakePrismWithName(
func(s string) Option[NonEmptyString] {
if S.IsEmpty(s) {
return option.None[NonEmptyString]()
}
return option.Of(NonEmptyString(s))
},
func(ns NonEmptyString) string {
return string(ns)
},
"NonEmptyString",
)
agePrism = prism.MakePrismWithName(
func(a int) Option[AdultAge] {
if a < 18 {
return option.None[AdultAge]()
}
return option.Of(AdultAge(a))
},
func(aa AdultAge) int {
return int(aa)
},
"AdultAge",
)
```
### 🎭 Creating the PersonPrism
The `PersonPrism` converts between a builder and a validated `Person`:
```go
PersonPrism = prism.MakePrismWithName(
buildPerson(), // Forward: builder -> Option[*Person]
buildEndomorphism(), // Reverse: *Person -> builder
"Person",
)
```
**Forward direction** ➡️ (`buildPerson`):
1. Applies the builder to an empty `PartialPerson`
2. Validates each field using field prisms
3. Returns `Some(*Person)` if all validations pass, `None` otherwise
**Reverse direction** ⬅️ (`buildEndomorphism`):
1. Extracts validated fields from `Person`
2. Converts them back to raw types
3. Returns a builder that reconstructs the `PartialPerson`
### 💡 Usage Example
```go
// Create a builder
builder := MakePerson("Alice", 25)
// Validate and convert to Person
maybePerson := PersonPrism.GetOption(builder)
// maybePerson is Option[*Person]
// - Some(*Person) if validation succeeds ✅
// - None if validation fails (no error details) ❌
```
## 📝 Approach 2: Codec-Based Validation (With Error Messages)
### Creating Field Codecs
Convert prisms to codecs for detailed validation:
```go
nameCodec = codec.FromRefinement(namePrism)
ageCodec = codec.FromRefinement(agePrism)
```
### 🎯 Creating the PersonCodec
The `PersonCodec` provides bidirectional transformation with validation:
```go
func makePersonCodec() PersonCodec {
return codec.MakeType(
"Person",
codec.Is[*Person](),
makePersonValidate(), // Validation with error reporting
buildEndomorphism(), // Encoding (same as prism)
)
}
```
The `makePersonValidate` function:
1. Applies the builder to an empty `PartialPerson`
2. Validates each field using field codecs
3. Accumulates validation errors using applicative composition 📚
4. Returns `Validation[*Person]` (either errors or a valid `Person`)
### 💡 Usage Example
```go
// Create a builder
builder := MakePerson("", 15) // Invalid: empty name, age < 18
// Validate with detailed errors
personCodec := makePersonCodec()
validation := personCodec.Validate(builder)
// validation is Validation[*Person]
// - Right(*Person) if validation succeeds ✅
// - Left(ValidationErrors) with detailed error messages if validation fails ❌
```
## ⚖️ Key Differences
| Feature | Prism-Based 🔍 | Codec-Based 📝 |
|---------|-------------|-------------|
| Error Messages | No (returns `None`) ❌ | Yes (returns detailed errors) ✅ |
| Complexity | Simpler 🟢 | More complex 🟡 |
| Use Case | Simple validation | Production validation with user feedback |
| Return Type | `Option[*Person]` | `Validation[*Person]` |
## 📝 Pattern Summary
1. **Define types** 📐: Create `PartialPerson` (unvalidated) and `Person` (validated)
2. **Generate lenses** 🔧: Use `fp-go:Lens` directive
3. **Export setters** 🎁: Create `WithXXX` methods from lens setters
4. **Create validation prisms** 🎭: Define validation rules for each field
5. **Choose validation approach** ⚖️:
- **Simple** 🔍: Create a `Prism` for quick validation without errors
- **Detailed** 📝: Create a `Codec` for validation with error reporting
## ✨ Benefits
- **Type Safety** 🛡️: Validated types guarantee business rules at compile time
- **Composability** 🧩: Builders can be composed using monoid operations
- **Bidirectional** ↔️: Both prisms and codecs support encoding and decoding
- **Separation of Concerns** 🎯: Building and validation are separate phases
- **Functional** 🔄: Pure functions, no mutation, easy to test
## 📁 Files
- `types.go`: Type definitions and lens generation directives
- `builder.go`: Prism-based builder implementation
- `codec.go`: Codec-based validation implementation
- `codec_test.go`: Tests demonstrating usage patterns

View File

@@ -27,10 +27,10 @@ var (
personLenses = MakePersonRefLenses()
// emptyPartialPerson is a zero-value PartialPerson used as a starting point for building.
emptyPartialPerson = &PartialPerson{}
emptyPartialPerson = F.Zero[*PartialPerson]()
// emptyPerson is a zero-value Person used as a starting point for validation.
emptyPerson = &Person{}
emptyPerson = F.Zero[*Person]()
// monoidPartialPerson is a monoid for composing endomorphisms on PartialPerson.
// Allows combining multiple builder operations.
@@ -90,7 +90,7 @@ var (
// Example:
// builder := WithName("Alice")
// person := builder(&PartialPerson{})
WithName = partialPersonLenses.Name.Set
WithName = partialPersonLenses.name.Set
// WithAge is a builder function that sets the Age field of a PartialPerson.
// It returns an endomorphism that can be composed with other builder operations.
@@ -98,7 +98,7 @@ var (
// Example:
// builder := WithAge(25)
// person := builder(&PartialPerson{})
WithAge = partialPersonLenses.Age.Set
WithAge = partialPersonLenses.age.Set
// PersonPrism is a prism that converts between a builder pattern (Endomorphism[*PartialPerson])
// and a validated Person in both directions.
@@ -160,7 +160,7 @@ func buildPerson() ReaderOption[Endomorphism[*PartialPerson], *Person] {
// maybeName extracts the name from PartialPerson, validates it,
// and creates a setter for the Person's Name field if valid
maybeName := F.Flow3(
partialPersonLenses.Name.Get,
partialPersonLenses.name.Get,
namePrism.GetOption,
option.Map(personLenses.Name.Set),
)
@@ -168,7 +168,7 @@ func buildPerson() ReaderOption[Endomorphism[*PartialPerson], *Person] {
// maybeAge extracts the age from PartialPerson, validates it,
// and creates a setter for the Person's Age field if valid
maybeAge := F.Flow3(
partialPersonLenses.Age.Get,
partialPersonLenses.age.Get,
agePrism.GetOption,
option.Map(personLenses.Age.Set),
)
@@ -200,7 +200,7 @@ func buildEndomorphism() Reader[*Person, Endomorphism[*PartialPerson]] {
name := F.Flow3(
personLenses.Name.Get,
namePrism.ReverseGet,
partialPersonLenses.Name.Set,
partialPersonLenses.name.Set,
)
// age extracts the validated age, converts it to int,
@@ -208,7 +208,7 @@ func buildEndomorphism() Reader[*Person, Endomorphism[*PartialPerson]] {
age := F.Flow3(
personLenses.Age.Get,
agePrism.ReverseGet,
partialPersonLenses.Age.Set,
partialPersonLenses.age.Set,
)
// Combine the field extractors into a single builder

View File

@@ -81,7 +81,7 @@ func makePersonValidate() Validate[Endomorphism[*PartialPerson], *Person] {
// 2. Validate using nameCodec (ensures non-empty)
// 3. Map to a Person name setter if valid
valName := F.Flow3(
partialPersonLenses.Name.Get,
partialPersonLenses.name.Get,
nameCodec.Validate,
decode.Map[validation.Context](personLenses.Name.Set),
)
@@ -91,7 +91,7 @@ func makePersonValidate() Validate[Endomorphism[*PartialPerson], *Person] {
// 2. Validate using ageCodec (ensures >= 18)
// 3. Map to a Person age setter if valid
valAge := F.Flow3(
partialPersonLenses.Age.Get,
partialPersonLenses.age.Get,
ageCodec.Validate,
decode.Map[validation.Context](personLenses.Age.Set),
)

View File

@@ -151,8 +151,8 @@ func TestMakePersonCodec_Encode(t *testing.T) {
partial := builder(emptyPartialPerson)
// Assert
assert.Equal(t, "Eve", partial.Name)
assert.Equal(t, 28, partial.Age)
assert.Equal(t, "Eve", partial.name)
assert.Equal(t, 28, partial.age)
}
// TestMakePersonCodec_RoundTrip tests encoding and decoding round-trip

View File

@@ -2,7 +2,7 @@ package builder
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2026-01-21 17:40:56.5217758 +0100 CET m=+0.003738101
// 2026-01-27 10:18:05.2249315 +0100 CET m=+0.004416801
import (
__lens "github.com/IBM/fp-go/v2/optics/lens"
@@ -15,105 +15,105 @@ import (
// PartialPersonLenses provides lenses for accessing fields of PartialPerson
type PartialPersonLenses struct {
// mandatory fields
Name __lens.Lens[PartialPerson, string]
Age __lens.Lens[PartialPerson, int]
name __lens.Lens[PartialPerson, string]
age __lens.Lens[PartialPerson, int]
// optional fields
NameO __lens_option.LensO[PartialPerson, string]
AgeO __lens_option.LensO[PartialPerson, int]
nameO __lens_option.LensO[PartialPerson, string]
ageO __lens_option.LensO[PartialPerson, int]
}
// PartialPersonRefLenses provides lenses for accessing fields of PartialPerson via a reference to PartialPerson
type PartialPersonRefLenses struct {
// mandatory fields
Name __lens.Lens[*PartialPerson, string]
Age __lens.Lens[*PartialPerson, int]
name __lens.Lens[*PartialPerson, string]
age __lens.Lens[*PartialPerson, int]
// optional fields
NameO __lens_option.LensO[*PartialPerson, string]
AgeO __lens_option.LensO[*PartialPerson, int]
nameO __lens_option.LensO[*PartialPerson, string]
ageO __lens_option.LensO[*PartialPerson, int]
// prisms
NameP __prism.Prism[*PartialPerson, string]
AgeP __prism.Prism[*PartialPerson, int]
nameP __prism.Prism[*PartialPerson, string]
ageP __prism.Prism[*PartialPerson, int]
}
// PartialPersonPrisms provides prisms for accessing fields of PartialPerson
type PartialPersonPrisms struct {
Name __prism.Prism[PartialPerson, string]
Age __prism.Prism[PartialPerson, int]
name __prism.Prism[PartialPerson, string]
age __prism.Prism[PartialPerson, int]
}
// MakePartialPersonLenses creates a new PartialPersonLenses with lenses for all fields
func MakePartialPersonLenses() PartialPersonLenses {
// mandatory lenses
lensName := __lens.MakeLensWithName(
func(s PartialPerson) string { return s.Name },
func(s PartialPerson, v string) PartialPerson { s.Name = v; return s },
"PartialPerson.Name",
lensname := __lens.MakeLensWithName(
func(s PartialPerson) string { return s.name },
func(s PartialPerson, v string) PartialPerson { s.name = v; return s },
"PartialPerson.name",
)
lensAge := __lens.MakeLensWithName(
func(s PartialPerson) int { return s.Age },
func(s PartialPerson, v int) PartialPerson { s.Age = v; return s },
"PartialPerson.Age",
lensage := __lens.MakeLensWithName(
func(s PartialPerson) int { return s.age },
func(s PartialPerson, v int) PartialPerson { s.age = v; return s },
"PartialPerson.age",
)
// optional lenses
lensNameO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[string]())(lensName)
lensAgeO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[int]())(lensAge)
lensnameO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[string]())(lensname)
lensageO := __lens_option.FromIso[PartialPerson](__iso_option.FromZero[int]())(lensage)
return PartialPersonLenses{
// mandatory lenses
Name: lensName,
Age: lensAge,
name: lensname,
age: lensage,
// optional lenses
NameO: lensNameO,
AgeO: lensAgeO,
nameO: lensnameO,
ageO: lensageO,
}
}
// MakePartialPersonRefLenses creates a new PartialPersonRefLenses with lenses for all fields
func MakePartialPersonRefLenses() PartialPersonRefLenses {
// mandatory lenses
lensName := __lens.MakeLensStrictWithName(
func(s *PartialPerson) string { return s.Name },
func(s *PartialPerson, v string) *PartialPerson { s.Name = v; return s },
"(*PartialPerson).Name",
lensname := __lens.MakeLensStrictWithName(
func(s *PartialPerson) string { return s.name },
func(s *PartialPerson, v string) *PartialPerson { s.name = v; return s },
"(*PartialPerson).name",
)
lensAge := __lens.MakeLensStrictWithName(
func(s *PartialPerson) int { return s.Age },
func(s *PartialPerson, v int) *PartialPerson { s.Age = v; return s },
"(*PartialPerson).Age",
lensage := __lens.MakeLensStrictWithName(
func(s *PartialPerson) int { return s.age },
func(s *PartialPerson, v int) *PartialPerson { s.age = v; return s },
"(*PartialPerson).age",
)
// optional lenses
lensNameO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[string]())(lensName)
lensAgeO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[int]())(lensAge)
lensnameO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[string]())(lensname)
lensageO := __lens_option.FromIso[*PartialPerson](__iso_option.FromZero[int]())(lensage)
return PartialPersonRefLenses{
// mandatory lenses
Name: lensName,
Age: lensAge,
name: lensname,
age: lensage,
// optional lenses
NameO: lensNameO,
AgeO: lensAgeO,
nameO: lensnameO,
ageO: lensageO,
}
}
// MakePartialPersonPrisms creates a new PartialPersonPrisms with prisms for all fields
func MakePartialPersonPrisms() PartialPersonPrisms {
_fromNonZeroName := __option.FromNonZero[string]()
_prismName := __prism.MakePrismWithName(
func(s PartialPerson) __option.Option[string] { return _fromNonZeroName(s.Name) },
_fromNonZeroname := __option.FromNonZero[string]()
_prismname := __prism.MakePrismWithName(
func(s PartialPerson) __option.Option[string] { return _fromNonZeroname(s.name) },
func(v string) PartialPerson {
return PartialPerson{ Name: v }
return PartialPerson{ name: v }
},
"PartialPerson.Name",
"PartialPerson.name",
)
_fromNonZeroAge := __option.FromNonZero[int]()
_prismAge := __prism.MakePrismWithName(
func(s PartialPerson) __option.Option[int] { return _fromNonZeroAge(s.Age) },
_fromNonZeroage := __option.FromNonZero[int]()
_prismage := __prism.MakePrismWithName(
func(s PartialPerson) __option.Option[int] { return _fromNonZeroage(s.age) },
func(v int) PartialPerson {
return PartialPerson{ Age: v }
return PartialPerson{ age: v }
},
"PartialPerson.Age",
"PartialPerson.age",
)
return PartialPersonPrisms {
Name: _prismName,
Age: _prismAge,
name: _prismname,
age: _prismage,
}
}

View File

@@ -38,13 +38,37 @@ type (
// a value of type A. It is an alias for reader.Reader[R, A].
Reader[R, A any] = reader.Reader[R, A]
// Prism represents an optic that focuses on a subset of values of type S that can be
// converted to type A. It provides bidirectional transformation with validation.
// It is an alias for prism.Prism[S, A].
Prism[S, A any] = prism.Prism[S, A]
Lens[S, A any] = lens.Lens[S, A]
Type[A, O, I any] = codec.Type[A, O, I]
// Lens represents an optic that focuses on a field of type A within a structure of type S.
// It provides getter and setter operations for immutable updates.
// It is an alias for lens.Lens[S, A].
Lens[S, A any] = lens.Lens[S, A]
// Type represents a codec that handles bidirectional transformation between types.
// A: The validated target type
// O: The output encoding type
// I: The input decoding type
// It is an alias for codec.Type[A, O, I].
Type[A, O, I any] = codec.Type[A, O, I]
// Validate represents a validation function that transforms input I into a validated result A.
// It returns a Validation that contains either the validated value or validation errors.
// It is an alias for validate.Validate[I, A].
Validate[I, A any] = validate.Validate[I, A]
Validation[A any] = validation.Validation[A]
Encode[A, O any] = codec.Encode[A, O]
// Validation represents the result of a validation operation.
// It contains either a validated value of type A (Right) or validation errors (Left).
// It is an alias for validation.Validation[A].
Validation[A any] = validation.Validation[A]
// Encode represents an encoding function that transforms a value of type A into type O.
// It is used in codecs for the reverse direction of validation.
// It is an alias for codec.Encode[A, O].
Encode[A, O any] = codec.Encode[A, O]
// NonEmptyString is a string type that represents a validated non-empty string.
// It is used to ensure that string fields contain meaningful data.
@@ -64,11 +88,11 @@ type (
//
// fp-go:Lens
type PartialPerson struct {
// Name is the person's name as a raw string, which may be empty or invalid.
Name string
// name is the person's name as a raw string, which may be empty or invalid.
name string
// Age is the person's age as a raw integer, which may be negative or otherwise invalid.
Age int
// age is the person's age as a raw integer, which may be negative or otherwise invalid.
age int
}
// Person represents a person record with validated fields.

View File

@@ -107,7 +107,7 @@ func TestHeterogeneousHttpRequests(t *testing.T) {
// BenchmarkHeterogeneousHttpRequests shows how to execute multiple HTTP requests in parallel when
// the response structure of these requests is different. We use [R.TraverseTuple2] to account for the different types
func BenchmarkHeterogeneousHttpRequests(b *testing.B) {
for n := 0; n < b.N; n++ {
heterogeneousHTTPRequests()(context.Background())()
for b.Loop() {
heterogeneousHTTPRequests()(b.Context())()
}
}

View File

@@ -15,7 +15,9 @@
package lens
import "github.com/IBM/fp-go/v2/optics/lens/option"
import (
"github.com/IBM/fp-go/v2/optics/lens/option"
)
//go:generate go run ../../main.go lens --dir . --filename gen_lens.go
@@ -64,3 +66,9 @@ type WithGeneric[T any] struct {
Name string
Value T
}
// fp-go:Lens
type DataBuilder struct {
name string
value string
}

View File

@@ -21,6 +21,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
@@ -341,3 +342,125 @@ func TestCompanyRefLensesOptionalIdempotent(t *testing.T) {
assert.Equal(t, &newWebsiteValue, differentWebsite.Website)
assert.Equal(t, &websiteValue, company.Website, "Original should be unchanged")
}
func TestDataBuilderLensWithUnexportedFields(t *testing.T) {
// Test that lenses can access and modify unexported fields
// This demonstrates that the lens generator now supports unexported fields
// Create a DataBuilder with unexported fields
builder := DataBuilder{
name: "initial-name",
value: "initial-value",
}
// Create lenses
lenses := MakeDataBuilderLenses()
// Test Get on unexported fields
assert.Equal(t, "initial-name", lenses.name.Get(builder))
assert.Equal(t, "initial-value", lenses.value.Get(builder))
// Test Set on unexported fields
updatedName := lenses.name.Set("updated-name")(builder)
assert.Equal(t, "updated-name", updatedName.name)
assert.Equal(t, "initial-value", updatedName.value) // Other field unchanged
assert.Equal(t, "initial-name", builder.name) // Original unchanged
updatedValue := lenses.value.Set("updated-value")(builder)
assert.Equal(t, "initial-name", updatedValue.name) // Other field unchanged
assert.Equal(t, "updated-value", updatedValue.value)
assert.Equal(t, "initial-value", builder.value) // Original unchanged
// Test Modify on unexported fields
modifyName := F.Pipe1(
lenses.name,
L.Modify[DataBuilder](S.Append("-modified")),
)
modified := modifyName(builder)
assert.Equal(t, "initial-name-modified", modified.name)
assert.Equal(t, "initial-name", builder.name) // Original unchanged
// Test composition of modifications
updatedBoth := F.Pipe2(
builder,
lenses.name.Set("new-name"),
lenses.value.Set("new-value"),
)
assert.Equal(t, "new-name", updatedBoth.name)
assert.Equal(t, "new-value", updatedBoth.value)
assert.Equal(t, "initial-name", builder.name) // Original unchanged
assert.Equal(t, "initial-value", builder.value) // Original unchanged
}
func TestDataBuilderRefLensesWithUnexportedFields(t *testing.T) {
// Test that ref lenses work with unexported fields and maintain idempotency
builder := &DataBuilder{
name: "test-name",
value: "test-value",
}
refLenses := MakeDataBuilderRefLenses()
// Test Get on unexported fields
assert.Equal(t, "test-name", refLenses.name.Get(builder))
assert.Equal(t, "test-value", refLenses.value.Get(builder))
// Test idempotency - setting same value should return same pointer
sameName := refLenses.name.Set("test-name")(builder)
assert.Same(t, builder, sameName, "Setting name to same value should return identical pointer")
sameValue := refLenses.value.Set("test-value")(builder)
assert.Same(t, builder, sameValue, "Setting value to same value should return identical pointer")
// Test that setting different value creates new pointer
differentName := refLenses.name.Set("different-name")(builder)
assert.NotSame(t, builder, differentName, "Setting name to different value should return new pointer")
assert.Equal(t, "different-name", differentName.name)
assert.Equal(t, "test-name", builder.name, "Original should be unchanged")
differentValue := refLenses.value.Set("different-value")(builder)
assert.NotSame(t, builder, differentValue, "Setting value to different value should return new pointer")
assert.Equal(t, "different-value", differentValue.value)
assert.Equal(t, "test-value", builder.value, "Original should be unchanged")
}
func TestDataBuilderOptionalLensesWithUnexportedFields(t *testing.T) {
// Test optional lenses (LensO) with unexported fields
builder := DataBuilder{
name: "test",
value: "data",
}
lenses := MakeDataBuilderLenses()
// Test getting non-zero values as Some
nameOpt := lenses.nameO.Get(builder)
assert.True(t, O.IsSome(nameOpt))
assert.Equal(t, "test", O.GetOrElse(F.Zero[string])(nameOpt))
valueOpt := lenses.valueO.Get(builder)
assert.True(t, O.IsSome(valueOpt))
assert.Equal(t, "data", O.GetOrElse(F.Zero[string])(valueOpt))
// Test setting to Some
updatedName := lenses.nameO.Set(O.Some("new-test"))(builder)
assert.Equal(t, "new-test", updatedName.name)
// Test setting to None (zero value for string is "")
clearedName := lenses.nameO.Set(O.None[string]())(builder)
assert.Equal(t, "", clearedName.name)
// Test with zero value
emptyBuilder := DataBuilder{
name: "",
value: "",
}
emptyNameOpt := lenses.nameO.Get(emptyBuilder)
assert.True(t, O.IsNone(emptyNameOpt), "Empty string should be None")
emptyValueOpt := lenses.valueO.Get(emptyBuilder)
assert.True(t, O.IsNone(emptyValueOpt), "Empty string should be None")
}

View File

@@ -2,7 +2,7 @@ package lens
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2025-12-16 15:41:28.7198645 +0100 CET m=+0.003889201
// 2026-01-27 10:33:42.2879434 +0100 CET m=+0.002788201
import (
__lens "github.com/IBM/fp-go/v2/optics/lens"
@@ -946,3 +946,108 @@ func MakeWithGenericPrisms[T any]() WithGenericPrisms[T] {
Value: _prismValue,
}
}
// DataBuilderLenses provides lenses for accessing fields of DataBuilder
type DataBuilderLenses struct {
// mandatory fields
name __lens.Lens[DataBuilder, string]
value __lens.Lens[DataBuilder, string]
// optional fields
nameO __lens_option.LensO[DataBuilder, string]
valueO __lens_option.LensO[DataBuilder, string]
}
// DataBuilderRefLenses provides lenses for accessing fields of DataBuilder via a reference to DataBuilder
type DataBuilderRefLenses struct {
// mandatory fields
name __lens.Lens[*DataBuilder, string]
value __lens.Lens[*DataBuilder, string]
// optional fields
nameO __lens_option.LensO[*DataBuilder, string]
valueO __lens_option.LensO[*DataBuilder, string]
// prisms
nameP __prism.Prism[*DataBuilder, string]
valueP __prism.Prism[*DataBuilder, string]
}
// DataBuilderPrisms provides prisms for accessing fields of DataBuilder
type DataBuilderPrisms struct {
name __prism.Prism[DataBuilder, string]
value __prism.Prism[DataBuilder, string]
}
// MakeDataBuilderLenses creates a new DataBuilderLenses with lenses for all fields
func MakeDataBuilderLenses() DataBuilderLenses {
// mandatory lenses
lensname := __lens.MakeLensWithName(
func(s DataBuilder) string { return s.name },
func(s DataBuilder, v string) DataBuilder { s.name = v; return s },
"DataBuilder.name",
)
lensvalue := __lens.MakeLensWithName(
func(s DataBuilder) string { return s.value },
func(s DataBuilder, v string) DataBuilder { s.value = v; return s },
"DataBuilder.value",
)
// optional lenses
lensnameO := __lens_option.FromIso[DataBuilder](__iso_option.FromZero[string]())(lensname)
lensvalueO := __lens_option.FromIso[DataBuilder](__iso_option.FromZero[string]())(lensvalue)
return DataBuilderLenses{
// mandatory lenses
name: lensname,
value: lensvalue,
// optional lenses
nameO: lensnameO,
valueO: lensvalueO,
}
}
// MakeDataBuilderRefLenses creates a new DataBuilderRefLenses with lenses for all fields
func MakeDataBuilderRefLenses() DataBuilderRefLenses {
// mandatory lenses
lensname := __lens.MakeLensStrictWithName(
func(s *DataBuilder) string { return s.name },
func(s *DataBuilder, v string) *DataBuilder { s.name = v; return s },
"(*DataBuilder).name",
)
lensvalue := __lens.MakeLensStrictWithName(
func(s *DataBuilder) string { return s.value },
func(s *DataBuilder, v string) *DataBuilder { s.value = v; return s },
"(*DataBuilder).value",
)
// optional lenses
lensnameO := __lens_option.FromIso[*DataBuilder](__iso_option.FromZero[string]())(lensname)
lensvalueO := __lens_option.FromIso[*DataBuilder](__iso_option.FromZero[string]())(lensvalue)
return DataBuilderRefLenses{
// mandatory lenses
name: lensname,
value: lensvalue,
// optional lenses
nameO: lensnameO,
valueO: lensvalueO,
}
}
// MakeDataBuilderPrisms creates a new DataBuilderPrisms with prisms for all fields
func MakeDataBuilderPrisms() DataBuilderPrisms {
_fromNonZeroname := __option.FromNonZero[string]()
_prismname := __prism.MakePrismWithName(
func(s DataBuilder) __option.Option[string] { return _fromNonZeroname(s.name) },
func(v string) DataBuilder {
return DataBuilder{ name: v }
},
"DataBuilder.name",
)
_fromNonZerovalue := __option.FromNonZero[string]()
_prismvalue := __prism.MakePrismWithName(
func(s DataBuilder) __option.Option[string] { return _fromNonZerovalue(s.value) },
func(v string) DataBuilder {
return DataBuilder{ value: v }
},
"DataBuilder.value",
)
return DataBuilderPrisms {
name: _prismname,
value: _prismvalue,
}
}