1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-09 23:11:40 +02:00

Compare commits

...

5 Commits

Author SHA1 Message Date
Dr. Carsten Leue
7e7cc06f11 fix: add more Kleisli definitions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 17:42:54 +01:00
Dr. Carsten Leue
54d5dbd04a fix: more tests for iso and prism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 17:31:27 +01:00
Dr. Carsten Leue
51adce0c95 fix: better package import
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 16:15:16 +01:00
Dr. Carsten Leue
aa5e908810 fix: introduce Kleisli type
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-07 14:35:46 +01:00
Dr. Carsten Leue
b3bd5e9ad3 fix: bind docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 16:18:15 +01:00
114 changed files with 14039 additions and 5209 deletions

View File

@@ -21,14 +21,56 @@ import (
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
// result := generic.Do[[]State, State](State{})
func Do[GS ~[]S, S any](
empty S,
) GS {
return Of[GS](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps.
// For arrays, this produces the cartesian product where later steps can use values from earlier steps.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
//
// result := F.Pipe2(
// generic.Do[[]State, State](State{}),
// generic.Bind[[]State, []State, []int, State, State, int](
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// func(s State) []int {
// return []int{1, 2, 3}
// },
// ),
// generic.Bind[[]State, []State, []int, State, State, int](
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) []int {
// // This can access s.X from the previous step
// return []int{s.X * 10, s.X * 20}
// },
// ),
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
func Bind[GS1 ~[]S1, GS2 ~[]S2, GT ~[]T, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) GT,

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,20 +19,75 @@ import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// result := readerioeither.Do(State{})
//
//go:inline
func Do[S any](
empty S,
) ReaderIOEither[S] {
return Of(empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access the context.Context from the environment.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerioeither.ReaderIOEither[User] {
// return func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// },
// ),
// readerioeither.Bind(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) readerioeither.ReaderIOEither[Config] {
// // This can access s.User from the previous step
// return func(ctx context.Context) ioeither.IOEither[error, Config] {
// return ioeither.TryCatch(func() (Config, error) {
// return fetchConfigForUser(ctx, s.User.ID)
// })
// }
// },
// ),
// )
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderIOEither[T],
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
f Kleisli[S1, T],
) Operator[S1, S2] {
return chain.Bind(
Chain[S1, S2],
Map[T, S2],
@@ -42,10 +97,12 @@ func Bind[S1, S2, T any](
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
//
//go:inline
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
) Operator[S1, S2] {
return functor.Let(
Map[S1, S2],
setter,
@@ -54,10 +111,12 @@ func Let[S1, S2, T any](
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
//
//go:inline
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
) Operator[S1, S2] {
return functor.LetTo(
Map[S1, S2],
setter,
@@ -66,6 +125,8 @@ func LetTo[S1, S2, T any](
}
// BindTo initializes a new state [S1] from a value [T]
//
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
@@ -116,10 +177,12 @@ func BindTo[S1, T any](
// getConfig,
// ),
// )
//
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIOEither[T],
) func(ReaderIOEither[S1]) ReaderIOEither[S2] {
) Operator[S1, S2] {
return apply.ApS(
Ap[S2, T],
Map[S1, func(T) S2],
@@ -127,3 +190,152 @@ func ApS[S1, S2, T any](
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// getUser := func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// result := F.Pipe2(
// readerioeither.Of(State{}),
// readerioeither.ApSL(userLens, getUser),
// )
//
//go:inline
func ApSL[S, T any](
lens L.Lens[S, T],
fa ReaderIOEither[T],
) Operator[S, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a ReaderIOEither computation that produces an updated value.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.BindL(userLens, func(user User) readerioeither.ReaderIOEither[User] {
// return func(ctx context.Context) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser(ctx)
// })
// }
// }),
// )
//
//go:inline
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return Bind[S, S, T](lens.Set, func(s S) ReaderIOEither[T] {
return f(lens.Get(s))
})
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new value (without wrapping in a ReaderIOEither).
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readerioeither.Do(State{User: User{Name: "Alice"}}),
// readerioeither.LetL(userLens, func(user User) User {
// user.Name = "Bob"
// return user
// }),
// )
//
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Operator[S, S] {
return Let[S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The value b is set directly to the focused field.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// newUser := User{Name: "Bob", ID: 123}
// result := F.Pipe2(
// readerioeither.Do(State{}),
// readerioeither.LetToL(userLens, newUser),
// )
//
//go:inline
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Operator[S, S] {
return LetTo[S, S, T](lens.Set, b)
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -71,8 +71,8 @@ import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
LA "github.com/IBM/fp-go/v2/optics/lens/array"
LO "github.com/IBM/fp-go/v2/optics/lens/option"
LRG "github.com/IBM/fp-go/v2/optics/lens/record/generic"
RG "github.com/IBM/fp-go/v2/record/generic"
)
@@ -136,7 +136,7 @@ var (
// element of a string array, returning an Option[string].
composeHead = F.Pipe1(
LA.AtHead[string](),
L.ComposeOptions[http.Header, string](A.Empty[string]()),
LO.Compose[http.Header, string](A.Empty[string]()),
)
// AtValue is a Lens that focuses on the first value of a specific header.

View File

@@ -21,14 +21,55 @@ import (
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
// result := identity.Do(State{})
func Do[S any](
empty S,
) S {
return empty
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
//
// result := F.Pipe2(
// identity.Do(State{}),
// identity.Bind(
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// func(s State) int {
// return 42
// },
// ),
// identity.Bind(
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) int {
// // This can access s.X from the previous step
// return s.X * 2
// },
// ),
// ) // State{X: 42, Y: 84}
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,16 +19,62 @@ import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
// result := ioeither.Do[error](State{})
func Do[E, S any](
empty S,
) IOEither[E, S] {
return Of[E](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
//
// result := F.Pipe2(
// ioeither.Do[error](State{}),
// ioeither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) ioeither.IOEither[error, User] {
// return ioeither.TryCatch(func() (User, error) {
// return fetchUser()
// })
// },
// ),
// ioeither.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) ioeither.IOEither[error, []Post] {
// // This can access s.User from the previous step
// return ioeither.TryCatch(func() ([]Post, error) {
// return fetchPostsForUser(s.User.ID)
// })
// },
// ),
// )
func Bind[E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) IOEither[E, T],
@@ -119,3 +165,139 @@ func ApS[E, S1, S2, T any](
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type Config struct {
// Host string
// Port int
// }
//
// portLens := lens.MakeLens(
// func(c Config) int { return c.Port },
// func(c Config, p int) Config { c.Port = p; return c },
// )
//
// result := F.Pipe2(
// ioeither.Of[error](Config{Host: "localhost"}),
// ioeither.ApSL(portLens, ioeither.Of[error](8080)),
// )
func ApSL[E, S, T any](
lens L.Lens[S, T],
fa IOEither[E, T],
) Operator[E, S, S] {
return ApS(lens.Set, fa)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// an IOEither that produces the new value.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// increment := func(v int) ioeither.IOEither[error, int] {
// return ioeither.TryCatch(func() (int, error) {
// if v >= 100 {
// return 0, errors.New("overflow")
// }
// return v + 1, nil
// })
// }
//
// result := F.Pipe1(
// ioeither.Of[error](Counter{Value: 42}),
// ioeither.BindL(valueLens, increment),
// )
func BindL[E, S, T any](
lens L.Lens[S, T],
f func(T) IOEither[E, T],
) Operator[E, S, S] {
return Bind[E, S, S, T](lens.Set, func(s S) IOEither[E, T] {
return f(lens.Get(s))
})
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in IOEither).
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// ioeither.Of[error](Counter{Value: 21}),
// ioeither.LetL(valueLens, double),
// )
func LetL[E, S, T any](
lens L.Lens[S, T],
f func(T) T,
) Operator[E, S, S] {
return Let[E, S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// ioeither.Of[error](Config{Debug: true, Timeout: 30}),
// ioeither.LetToL(debugLens, false),
// )
func LetToL[E, S, T any](
lens L.Lens[S, T],
b T,
) Operator[E, S, S] {
return LetTo[E, S, S, T](lens.Set, b)
}

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@ import "github.com/IBM/fp-go/v2/function"
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource
func WithResource[
R, A, ANY any](onCreate IOOption[R], onRelease func(R) IOOption[ANY]) func(func(R) IOOption[A]) IOOption[A] {
R, A, ANY any](onCreate IOOption[R], onRelease func(R) IOOption[ANY]) Kleisli[Kleisli[R, A], A] {
// simply map to implementation of bracket
return function.Bind13of3(Bracket[R, A, ANY])(onCreate, function.Ignore2of2[Option[A]](onRelease))
}

View File

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

View File

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

View File

@@ -19,18 +19,60 @@ import (
G "github.com/IBM/fp-go/v2/iterator/stateless/generic"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
// result := stateless.Do(State{})
func Do[S any](
empty S,
) Iterator[S] {
return G.Do[Iterator[S]](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps.
// For iterators, this produces the cartesian product of all values.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
//
// result := F.Pipe2(
// stateless.Do(State{}),
// stateless.Bind(
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// func(s State) stateless.Iterator[int] {
// return stateless.Of(1, 2, 3)
// },
// ),
// stateless.Bind(
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) stateless.Iterator[int] {
// // This can access s.X from the previous step
// return stateless.Of(s.X * 10, s.X * 20)
// },
// ),
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) Iterator[T],
) func(Iterator[S1]) Iterator[S2] {
f Kleisli[S1, T],
) Kleisli[Iterator[S1], S2] {
return G.Bind[Iterator[S1], Iterator[S2], Iterator[T], S1, S2, T](setter, f)
}
@@ -38,7 +80,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(Iterator[S1]) Iterator[S2] {
) Kleisli[Iterator[S1], S2] {
return G.Let[Iterator[S1], Iterator[S2], S1, S2, T](setter, f)
}
@@ -46,14 +88,14 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(Iterator[S1]) Iterator[S2] {
) Kleisli[Iterator[S1], S2] {
return G.LetTo[Iterator[S1], Iterator[S2], S1, S2, T](setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[S1, T any](
setter func(T) S1,
) func(Iterator[T]) Iterator[S1] {
) Kleisli[Iterator[T], S1] {
return G.BindTo[Iterator[S1], Iterator[T], S1, T](setter)
}
@@ -93,6 +135,6 @@ func BindTo[S1, T any](
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Iterator[T],
) func(Iterator[S1]) Iterator[S2] {
) Kleisli[Iterator[S1], S2] {
return G.ApS[Iterator[func(T) S2], Iterator[S1], Iterator[S2], Iterator[T], S1, S2, T](setter, fa)
}

View File

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

View File

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

View File

@@ -23,14 +23,56 @@ import (
P "github.com/IBM/fp-go/v2/pair"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
// result := generic.Do[Iterator[State]](State{})
func Do[GS ~func() O.Option[P.Pair[GS, S]], S any](
empty S,
) GS {
return Of[GS](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps.
// For iterators, this produces the cartesian product where later steps can use values from earlier steps.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// X int
// Y int
// }
//
// result := F.Pipe2(
// generic.Do[Iterator[State]](State{}),
// generic.Bind[Iterator[State], Iterator[State], Iterator[int], State, State, int](
// func(x int) func(State) State {
// return func(s State) State { s.X = x; return s }
// },
// func(s State) Iterator[int] {
// return generic.Of[Iterator[int]](1, 2, 3)
// },
// ),
// generic.Bind[Iterator[State], Iterator[State], Iterator[int], State, State, int](
// func(y int) func(State) State {
// return func(s State) State { s.Y = y; return s }
// },
// func(s State) Iterator[int] {
// // This can access s.X from the previous step
// return generic.Of[Iterator[int]](s.X * 10, s.X * 20)
// },
// ),
// ) // Produces: {1,10}, {1,20}, {2,20}, {2,40}, {3,30}, {3,60}
func Bind[GS1 ~func() O.Option[P.Pair[GS1, S1]], GS2 ~func() O.Option[P.Pair[GS2, S2]], GA ~func() O.Option[P.Pair[GA, A]], S1, S2, A any](
setter func(A) func(S1) S2,
f func(S1) GA,

View File

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

View File

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

View File

@@ -16,21 +16,64 @@
package lazy
import (
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/io"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// Config Config
// Data Data
// }
// result := lazy.Do(State{})
func Do[S any](
empty S,
) Lazy[S] {
return io.Do(empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// Config Config
// Data Data
// }
//
// result := F.Pipe2(
// lazy.Do(State{}),
// lazy.Bind(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) lazy.Lazy[Config] {
// return lazy.MakeLazy(func() Config { return loadConfig() })
// },
// ),
// lazy.Bind(
// func(data Data) func(State) State {
// return func(s State) State { s.Data = data; return s }
// },
// func(s State) lazy.Lazy[Data] {
// // This can access s.Config from the previous step
// return lazy.MakeLazy(func() Data { return loadData(s.Config) })
// },
// ),
// )
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) Lazy[T],
) func(Lazy[S1]) Lazy[S2] {
f Kleisli[S1, T],
) Kleisli[Lazy[S1], S2] {
return io.Bind(setter, f)
}
@@ -38,7 +81,7 @@ func Bind[S1, S2, T any](
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) func(Lazy[S1]) Lazy[S2] {
) Kleisli[Lazy[S1], S2] {
return io.Let(setter, f)
}
@@ -46,14 +89,14 @@ func Let[S1, S2, T any](
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) func(Lazy[S1]) Lazy[S2] {
) Kleisli[Lazy[S1], S2] {
return io.LetTo(setter, b)
}
// BindTo initializes a new state [S1] from a value [T]
func BindTo[S1, T any](
setter func(T) S1,
) func(Lazy[T]) Lazy[S1] {
) Kleisli[Lazy[T], S1] {
return io.BindTo(setter)
}
@@ -93,6 +136,143 @@ func BindTo[S1, T any](
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Lazy[T],
) func(Lazy[S1]) Lazy[S2] {
) Kleisli[Lazy[S1], S2] {
return io.ApS(setter, fa)
}
// ApSL is a variant of ApS that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. This allows you to work with nested fields without manually managing
// the update logic.
//
// Example:
//
// type Config struct {
// Host string
// Port int
// }
// type State struct {
// Config Config
// Data string
// }
//
// configLens := L.Prop[State, Config]("Config")
// getConfig := lazy.MakeLazy(func() Config { return Config{Host: "localhost", Port: 8080} })
//
// result := F.Pipe2(
// lazy.Do(State{}),
// lazy.ApSL(configLens, getConfig),
// )
func ApSL[S, T any](
lens L.Lens[S, T],
fa Lazy[T],
) Kleisli[Lazy[S], S] {
return io.ApSL(lens, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new computation that produces an updated value.
//
// Example:
//
// type Config struct {
// Host string
// Port int
// }
// type State struct {
// Config Config
// Data string
// }
//
// configLens := L.Prop[State, Config]("Config")
//
// result := F.Pipe2(
// lazy.Do(State{Config: Config{Host: "localhost"}}),
// lazy.BindL(configLens, func(cfg Config) lazy.Lazy[Config] {
// return lazy.MakeLazy(func() Config {
// cfg.Port = 8080
// return cfg
// })
// }),
// )
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Kleisli[Lazy[S], S] {
return io.BindL(lens, f)
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new value (without wrapping in a monad).
//
// Example:
//
// type Config struct {
// Host string
// Port int
// }
// type State struct {
// Config Config
// Data string
// }
//
// configLens := L.Prop[State, Config]("Config")
//
// result := F.Pipe2(
// lazy.Do(State{Config: Config{Host: "localhost"}}),
// lazy.LetL(configLens, func(cfg Config) Config {
// cfg.Port = 8080
// return cfg
// }),
// )
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Kleisli[Lazy[S], S] {
return io.LetL(lens, f)
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The value b is set directly to the focused field.
//
// Example:
//
// type Config struct {
// Host string
// Port int
// }
// type State struct {
// Config Config
// Data string
// }
//
// configLens := L.Prop[State, Config]("Config")
// newConfig := Config{Host: "localhost", Port: 8080}
//
// result := F.Pipe2(
// lazy.Do(State{}),
// lazy.LetToL(configLens, newConfig),
// )
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Kleisli[Lazy[S], S] {
return io.LetToL(lens, b)
}

View File

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

View File

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

View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,109 @@
package option
import (
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
)
// fromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the nil value will be set instead
func fromPredicate[GET ~func(S) Option[A], SET ~func(S, Option[A]) S, S, A any](creator func(get GET, set SET) LensO[S, A], pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
fromPred := O.FromPredicate(pred)
return func(sa Lens[S, A]) LensO[S, A] {
fold := O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set)
return creator(F.Flow2(sa.Get, fromPred), func(s S, a Option[A]) S {
return F.Pipe2(
a,
fold,
EM.Ap(s),
)
})
}
}
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the nil value will be set instead
func FromPredicate[S, A any](pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
return fromPredicate(lens.MakeLens[func(S) Option[A], func(S, Option[A]) S], pred, nilValue)
}
// FromPredicateRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the nil value will be set instead
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) Lens[*S, Option[A]] {
return fromPredicate(lens.MakeLensRef[func(*S) Option[A], func(*S, Option[A]) *S], pred, nilValue)
}
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the `nil` value will be set instead
func FromNillable[S, A any](sa Lens[S, *A]) Lens[S, Option[*A]] {
return FromPredicate[S](F.IsNonNil[A], nil)(sa)
}
// FromNillableRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
// if the optional value is set then the `nil` value will be set instead
func FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, Option[*A]] {
return FromPredicateRef[S](F.IsNonNil[A], nil)(sa)
}
// fromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
func fromNullableProp[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
return func(sa Lens[S, A]) Lens[S, A] {
return creator(F.Flow3(
sa.Get,
isNullable,
O.GetOrElse(F.Constant(defaultValue)),
), func(s S, a A) S {
return sa.Set(a)(s)
},
)
}
}
// FromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
func FromNullableProp[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
return fromNullableProp(lens.MakeLens[func(S) A, func(S, A) S], isNullable, defaultValue)
}
// FromNullablePropRef returns a `Lens` from a property that may be optional. The getter returns a default value for these items
func FromNullablePropRef[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[*S, A]) Lens[*S, A] {
return fromNullableProp(lens.MakeLensRef[func(*S) A, func(*S, A) *S], isNullable, defaultValue)
}
// fromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
func fromOption[GET ~func(S) A, SET ~func(S, A) S, S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
return func(sa LensO[S, A]) Lens[S, A] {
return creator(F.Flow2(
sa.Get,
O.GetOrElse(F.Constant(defaultValue)),
), func(s S, a A) S {
return sa.Set(O.Some(a))(s)
},
)
}
}
// FromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
return fromOption(lens.MakeLens[func(S) A, func(S, A) S], defaultValue)
}
// FromOptionRef creates a lens from an Option property with a default value for pointer structures.
//
// This is the pointer version of [FromOption], with automatic copying to ensure immutability.
// The getter returns the value inside Some[A], or the defaultValue if it's None[A].
// The setter always wraps the value in Some[A].
//
// Type Parameters:
// - S: Structure type (will be used as *S)
// - A: Focus type
//
// Parameters:
// - defaultValue: Value to return when the Option is None
//
// Returns:
// - A function that takes a Lens[*S, Option[A]] and returns a Lens[*S, A]
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, Option[A]]) Lens[*S, A] {
return fromOption(lens.MakeLensRef[func(*S) A, func(*S, A) *S], defaultValue)
}

View File

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

View File

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

View File

@@ -0,0 +1,267 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testing
import (
"testing"
EQT "github.com/IBM/fp-go/v2/eq/testing"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/identity"
L "github.com/IBM/fp-go/v2/optics/lens"
LI "github.com/IBM/fp-go/v2/optics/lens/iso"
LO "github.com/IBM/fp-go/v2/optics/lens/option"
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
type (
Street struct {
num int
name string
}
Address struct {
city string
street *Street
}
Inner struct {
Value int
Foo string
}
InnerOpt struct {
Value *int
Foo *string
}
Outer struct {
inner *Inner
}
OuterOpt struct {
inner *InnerOpt
}
)
func (outer *OuterOpt) GetInner() *InnerOpt {
return outer.inner
}
func (outer *OuterOpt) SetInner(inner *InnerOpt) *OuterOpt {
outer.inner = inner
return outer
}
func (inner *InnerOpt) GetValue() *int {
return inner.Value
}
func (inner *InnerOpt) SetValue(value *int) *InnerOpt {
inner.Value = value
return inner
}
func (outer *Outer) GetInner() *Inner {
return outer.inner
}
func (outer *Outer) SetInner(inner *Inner) *Outer {
outer.inner = inner
return outer
}
func (inner *Inner) GetValue() int {
return inner.Value
}
func (inner *Inner) SetValue(value int) *Inner {
inner.Value = value
return inner
}
func (street *Street) GetName() string {
return street.name
}
func (street *Street) SetName(name string) *Street {
street.name = name
return street
}
func (addr *Address) GetStreet() *Street {
return addr.street
}
func (addr *Address) SetStreet(s *Street) *Address {
addr.street = s
return addr
}
var (
streetLens = L.MakeLensRef((*Street).GetName, (*Street).SetName)
addrLens = L.MakeLensRef((*Address).GetStreet, (*Address).SetStreet)
outerLens = LO.FromNillableRef(L.MakeLensRef((*Outer).GetInner, (*Outer).SetInner))
valueLens = L.MakeLensRef((*Inner).GetValue, (*Inner).SetValue)
outerOptLens = LO.FromNillableRef(L.MakeLensRef((*OuterOpt).GetInner, (*OuterOpt).SetInner))
valueOptLens = L.MakeLensRef((*InnerOpt).GetValue, (*InnerOpt).SetValue)
sampleStreet = Street{num: 220, name: "Schönaicherstr"}
sampleAddress = Address{city: "Böblingen", street: &sampleStreet}
sampleStreet2 = Street{num: 220, name: "Neue Str"}
defaultInner = Inner{
Value: -1,
Foo: "foo",
}
emptyOuter = Outer{}
defaultInnerOpt = InnerOpt{
Value: &defaultInner.Value,
Foo: &defaultInner.Foo,
}
emptyOuterOpt = OuterOpt{}
)
func TestStreetLensLaws(t *testing.T) {
// some comparison
eqs := EQT.Eq[*Street]()
eqa := EQT.Eq[string]()
laws := LT.AssertLaws(
t,
eqa,
eqs,
)(streetLens)
cpy := sampleStreet
assert.True(t, laws(&sampleStreet, "Neue Str."))
assert.Equal(t, cpy, sampleStreet)
}
func TestAddrLensLaws(t *testing.T) {
// some comparison
eqs := EQT.Eq[*Address]()
eqa := EQT.Eq[*Street]()
laws := LT.AssertLaws(
t,
eqa,
eqs,
)(addrLens)
cpyAddr := sampleAddress
cpyStreet := sampleStreet2
assert.True(t, laws(&sampleAddress, &sampleStreet2))
assert.Equal(t, cpyAddr, sampleAddress)
assert.Equal(t, cpyStreet, sampleStreet2)
}
func TestCompose(t *testing.T) {
// some comparison
eqs := EQT.Eq[*Address]()
eqa := EQT.Eq[string]()
streetName := L.Compose[*Address](streetLens)(addrLens)
laws := LT.AssertLaws(
t,
eqa,
eqs,
)(streetName)
cpyAddr := sampleAddress
cpyStreet := sampleStreet
assert.True(t, laws(&sampleAddress, "Neue Str."))
assert.Equal(t, cpyAddr, sampleAddress)
assert.Equal(t, cpyStreet, sampleStreet)
}
func TestOuterLensLaws(t *testing.T) {
// some equal predicates
eqValue := EQT.Eq[int]()
eqOptValue := O.Eq(eqValue)
// lens to access a value from outer
valueFromOuter := LO.ComposeOption[*Outer, int](&defaultInner)(valueLens)(outerLens)
// try to access the value, this should get an option
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
// update the object
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuter)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuter), O.None[int]()))
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
// updating with none should remove the inner
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
// check if this meets the laws
eqOuter := EQT.Eq[*Outer]()
laws := LT.AssertLaws(
t,
eqOptValue,
eqOuter,
)(valueFromOuter)
assert.True(t, laws(&emptyOuter, O.Some(2)))
assert.True(t, laws(&emptyOuter, O.None[int]()))
assert.True(t, laws(withValue, O.Some(2)))
assert.True(t, laws(withValue, O.None[int]()))
}
func TestOuterOptLensLaws(t *testing.T) {
// some equal predicates
eqValue := EQT.Eq[int]()
eqOptValue := O.Eq(eqValue)
intIso := LI.FromNillable[int]()
// lens to access a value from outer
valueFromOuter := F.Pipe3(
valueOptLens,
LI.Compose[*InnerOpt](intIso),
LO.Compose[*OuterOpt, int](&defaultInnerOpt),
I.Ap[L.Lens[*OuterOpt, O.Option[int]]](outerOptLens),
)
// try to access the value, this should get an option
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
// update the object
withValue := valueFromOuter.Set(O.Some(1))(&emptyOuterOpt)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(&emptyOuterOpt), O.None[int]()))
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(withValue), O.Some(1)))
// updating with none should remove the inner
nextValue := valueFromOuter.Set(O.None[int]())(withValue)
assert.True(t, eqOptValue.Equals(valueFromOuter.Get(nextValue), O.None[int]()))
// check if this meets the laws
eqOuter := EQT.Eq[*OuterOpt]()
laws := LT.AssertLaws(
t,
eqOptValue,
eqOuter,
)(valueFromOuter)
assert.True(t, laws(&emptyOuterOpt, O.Some(2)))
assert.True(t, laws(&emptyOuterOpt, O.None[int]()))
assert.True(t, laws(withValue, O.Some(2)))
assert.True(t, laws(withValue, O.None[int]()))
}

View File

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

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ package prism
import (
"encoding/base64"
"net/url"
"regexp"
"time"
"github.com/IBM/fp-go/v2/either"
@@ -310,3 +311,349 @@ func Deref[T any]() Prism[*T, *T] {
func FromEither[E, T any]() Prism[Either[E, T], T] {
return MakePrism(either.ToOption[E, T], either.Of[E, T])
}
// FromZero creates a prism that matches zero values of comparable types.
// It provides a safe way to work with zero values, handling non-zero values
// gracefully through the Option type.
//
// The prism's GetOption returns Some(t) if the value equals the zero value
// of type T; otherwise, it returns None.
//
// The prism's ReverseGet is the identity function, returning the value unchanged.
//
// Type Parameters:
// - T: A comparable type (must support == and != operators)
//
// Returns:
// - A Prism[T, T] that matches zero values
//
// Example:
//
// // Create a prism for zero integers
// zeroPrism := FromZero[int]()
//
// // Match zero value
// result := zeroPrism.GetOption(0) // Some(0)
//
// // Non-zero returns None
// result = zeroPrism.GetOption(42) // None[int]()
//
// // ReverseGet is identity
// value := zeroPrism.ReverseGet(0) // 0
//
// // Use with Set to update zero values
// setter := Set[int, int](100)
// result := setter(zeroPrism)(0) // 100
// result = setter(zeroPrism)(42) // 42 (unchanged)
//
// Common use cases:
// - Validating that values are zero/default
// - Filtering zero values in data pipelines
// - Working with optional fields that use zero as "not set"
// - Replacing zero values with defaults
func FromZero[T comparable]() Prism[T, T] {
var zero T
return MakePrism(option.FromPredicate(func(t T) bool { return t == zero }), F.Identity[T])
}
// Match represents a regex match result with full reconstruction capability.
// It contains everything needed to reconstruct the original string, making it
// suitable for use in a prism that maintains bidirectionality.
//
// Fields:
// - Before: Text before the match
// - Groups: Capture groups (index 0 is the full match, 1+ are capture groups)
// - After: Text after the match
//
// Example:
//
// // For string "hello world 123" with regex `\d+`:
// // Match{
// // Before: "hello world ",
// // Groups: []string{"123"},
// // After: "",
// // }
type Match struct {
Before string // Text before the match
Groups []string // Capture groups (index 0 is full match)
After string // Text after the match
}
// Reconstruct builds the original string from a Match.
// This is the inverse operation of regex matching, allowing full round-trip conversion.
//
// Returns:
// - The original string that was matched
//
// Example:
//
// match := Match{
// Before: "hello ",
// Groups: []string{"world"},
// After: "!",
// }
// original := match.Reconstruct() // "hello world!"
func (m Match) Reconstruct() string {
return m.Before + m.Groups[0] + m.After
}
// FullMatch returns the complete matched text (the entire regex match).
// This is equivalent to Groups[0] and represents what the regex matched.
//
// Returns:
// - The full matched text
//
// Example:
//
// match := Match{
// Before: "price: ",
// Groups: []string{"$99.99", "99.99"},
// After: " USD",
// }
// full := match.FullMatch() // "$99.99"
func (m Match) FullMatch() string {
return m.Groups[0]
}
// Group returns the nth capture group from the match (1-indexed).
// Capture group 0 is the full match, groups 1+ are the parenthesized captures.
// Returns an empty string if the group index is out of bounds.
//
// Parameters:
// - n: The capture group index (1-indexed)
//
// Returns:
// - The captured text, or empty string if index is invalid
//
// Example:
//
// // Regex: `(\w+)@(\w+\.\w+)` matching "user@example.com"
// match := Match{
// Groups: []string{"user@example.com", "user", "example.com"},
// }
// username := match.Group(1) // "user"
// domain := match.Group(2) // "example.com"
// invalid := match.Group(5) // ""
func (m Match) Group(n int) string {
if n < len(m.Groups) {
return m.Groups[n]
}
return ""
}
// RegexMatcher creates a prism for regex pattern matching with full reconstruction.
// It provides a safe way to match strings against a regex pattern, extracting
// match information while maintaining the ability to reconstruct the original string.
//
// The prism's GetOption attempts to match the regex against the string.
// If a match is found, it returns Some(Match) with all capture groups and context;
// if no match is found, it returns None.
//
// The prism's ReverseGet reconstructs the original string from a Match.
//
// Parameters:
// - re: A compiled regular expression
//
// Returns:
// - A Prism[string, Match] that safely handles regex matching
//
// Example:
//
// // Create a prism for matching numbers
// numRegex := regexp.MustCompile(`\d+`)
// numPrism := RegexMatcher(numRegex)
//
// // Match a string
// match := numPrism.GetOption("price: 42 dollars")
// // Some(Match{Before: "price: ", Groups: ["42"], After: " dollars"})
//
// // No match returns None
// noMatch := numPrism.GetOption("no numbers here") // None[Match]()
//
// // Reconstruct original string
// if m, ok := option.IsSome(match); ok {
// original := numPrism.ReverseGet(m) // "price: 42 dollars"
// }
//
// // Extract capture groups
// emailRegex := regexp.MustCompile(`(\w+)@(\w+\.\w+)`)
// emailPrism := RegexMatcher(emailRegex)
// match = emailPrism.GetOption("contact: user@example.com")
// // Match.Group(1) = "user", Match.Group(2) = "example.com"
//
// Common use cases:
// - Parsing structured text with regex patterns
// - Extracting and validating data from strings
// - Text transformation pipelines
// - Pattern-based string manipulation with reconstruction
//
// Note: This prism is bijective - you can always reconstruct the original
// string from a Match, making it suitable for round-trip transformations.
func RegexMatcher(re *regexp.Regexp) Prism[string, Match] {
noMatch := option.None[Match]()
return MakePrism(
// String -> Option[Match]
func(s string) Option[Match] {
loc := re.FindStringSubmatchIndex(s)
if loc == nil {
return noMatch
}
// Extract all capture groups
groups := make([]string, 0)
for i := 0; i < len(loc); i += 2 {
if loc[i] >= 0 {
groups = append(groups, s[loc[i]:loc[i+1]])
} else {
groups = append(groups, "")
}
}
match := Match{
Before: s[:loc[0]],
Groups: groups,
After: s[loc[1]:],
}
return option.Some(match)
},
Match.Reconstruct,
)
}
// NamedMatch represents a regex match result with named capture groups.
// It provides access to captured text by name rather than by index, making
// regex patterns more readable and maintainable.
//
// Fields:
// - Before: Text before the match
// - Groups: Map of capture group names to their matched text
// - Full: The complete matched text
// - After: Text after the match
//
// Example:
//
// // For regex `(?P<user>\w+)@(?P<domain>\w+\.\w+)` matching "user@example.com":
// // NamedMatch{
// // Before: "",
// // Groups: map[string]string{"user": "user", "domain": "example.com"},
// // Full: "user@example.com",
// // After: "",
// // }
type NamedMatch struct {
Before string
Groups map[string]string
Full string // The full matched text
After string
}
// Reconstruct builds the original string from a NamedMatch.
// This is the inverse operation of regex matching, allowing full round-trip conversion.
//
// Returns:
// - The original string that was matched
//
// Example:
//
// match := NamedMatch{
// Before: "email: ",
// Full: "user@example.com",
// Groups: map[string]string{"user": "user", "domain": "example.com"},
// After: "",
// }
// original := match.Reconstruct() // "email: user@example.com"
func (nm NamedMatch) Reconstruct() string {
return nm.Before + nm.Full + nm.After
}
// RegexNamedMatcher creates a prism for regex pattern matching with named capture groups.
// It provides a safe way to match strings against a regex pattern with named groups,
// making it easier to extract specific parts of the match by name rather than index.
//
// The prism's GetOption attempts to match the regex against the string.
// If a match is found, it returns Some(NamedMatch) with all named capture groups;
// if no match is found, it returns None.
//
// The prism's ReverseGet reconstructs the original string from a NamedMatch.
//
// Parameters:
// - re: A compiled regular expression with named capture groups
//
// Returns:
// - A Prism[string, NamedMatch] that safely handles regex matching with named groups
//
// Example:
//
// // Create a prism for matching email addresses with named groups
// emailRegex := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
// emailPrism := RegexNamedMatcher(emailRegex)
//
// // Match a string
// match := emailPrism.GetOption("contact: user@example.com")
// // Some(NamedMatch{
// // Before: "contact: ",
// // Groups: {"user": "user", "domain": "example.com"},
// // Full: "user@example.com",
// // After: "",
// // })
//
// // Access named groups
// if m, ok := option.IsSome(match); ok {
// username := m.Groups["user"] // "user"
// domain := m.Groups["domain"] // "example.com"
// }
//
// // No match returns None
// noMatch := emailPrism.GetOption("invalid-email") // None[NamedMatch]()
//
// // Reconstruct original string
// if m, ok := option.IsSome(match); ok {
// original := emailPrism.ReverseGet(m) // "contact: user@example.com"
// }
//
// // More complex example with date parsing
// dateRegex := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
// datePrism := RegexNamedMatcher(dateRegex)
// match = datePrism.GetOption("Date: 2024-03-15")
// // Access: match.Groups["year"], match.Groups["month"], match.Groups["day"]
//
// Common use cases:
// - Parsing structured text with meaningful field names
// - Extracting and validating data from formatted strings
// - Log parsing with named fields
// - Configuration file parsing
// - URL route parameter extraction
//
// Note: Only named capture groups appear in the Groups map. Unnamed groups
// are not included. The Full field always contains the complete matched text.
func RegexNamedMatcher(re *regexp.Regexp) Prism[string, NamedMatch] {
names := re.SubexpNames()
noMatch := option.None[NamedMatch]()
return MakePrism(
func(s string) Option[NamedMatch] {
loc := re.FindStringSubmatchIndex(s)
if loc == nil {
return noMatch
}
groups := make(map[string]string)
for i := 1; i < len(loc)/2; i++ {
if names[i] != "" && loc[2*i] >= 0 {
groups[names[i]] = s[loc[2*i]:loc[2*i+1]]
}
}
match := NamedMatch{
Before: s[:loc[0]],
Groups: groups,
Full: s[loc[0]:loc[1]],
After: s[loc[1]:],
}
return option.Some(match)
},
NamedMatch.Reconstruct,
)
}

View File

@@ -0,0 +1,534 @@
// 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 prism
import (
"regexp"
"testing"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestFromZero tests the FromZero prism with various comparable types
func TestFromZero(t *testing.T) {
t.Run("int - match zero", func(t *testing.T) {
prism := FromZero[int]()
result := prism.GetOption(0)
assert.True(t, O.IsSome(result))
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("int - non-zero returns None", func(t *testing.T) {
prism := FromZero[int]()
result := prism.GetOption(42)
assert.True(t, O.IsNone(result))
})
t.Run("string - match empty string", func(t *testing.T) {
prism := FromZero[string]()
result := prism.GetOption("")
assert.True(t, O.IsSome(result))
assert.Equal(t, "", O.GetOrElse(F.Constant("default"))(result))
})
t.Run("string - non-empty returns None", func(t *testing.T) {
prism := FromZero[string]()
result := prism.GetOption("hello")
assert.True(t, O.IsNone(result))
})
t.Run("bool - match false", func(t *testing.T) {
prism := FromZero[bool]()
result := prism.GetOption(false)
assert.True(t, O.IsSome(result))
assert.False(t, O.GetOrElse(F.Constant(true))(result))
})
t.Run("bool - true returns None", func(t *testing.T) {
prism := FromZero[bool]()
result := prism.GetOption(true)
assert.True(t, O.IsNone(result))
})
t.Run("float64 - match 0.0", func(t *testing.T) {
prism := FromZero[float64]()
result := prism.GetOption(0.0)
assert.True(t, O.IsSome(result))
assert.Equal(t, 0.0, O.GetOrElse(F.Constant(-1.0))(result))
})
t.Run("float64 - non-zero returns None", func(t *testing.T) {
prism := FromZero[float64]()
result := prism.GetOption(3.14)
assert.True(t, O.IsNone(result))
})
t.Run("pointer - match nil", func(t *testing.T) {
prism := FromZero[*int]()
var nilPtr *int
result := prism.GetOption(nilPtr)
assert.True(t, O.IsSome(result))
})
t.Run("pointer - non-nil returns None", func(t *testing.T) {
prism := FromZero[*int]()
value := 42
result := prism.GetOption(&value)
assert.True(t, O.IsNone(result))
})
t.Run("reverse get is identity", func(t *testing.T) {
prism := FromZero[int]()
assert.Equal(t, 0, prism.ReverseGet(0))
assert.Equal(t, 42, prism.ReverseGet(42))
})
}
// TestFromZeroWithSet tests using Set with FromZero prism
func TestFromZeroWithSet(t *testing.T) {
t.Run("set on zero value", func(t *testing.T) {
prism := FromZero[int]()
setter := Set[int, int](100)
result := setter(prism)(0)
assert.Equal(t, 100, result)
})
t.Run("set on non-zero returns original", func(t *testing.T) {
prism := FromZero[int]()
setter := Set[int, int](100)
result := setter(prism)(42)
assert.Equal(t, 42, result)
})
}
// TestFromZeroPrismLaws tests that FromZero satisfies prism laws
func TestFromZeroPrismLaws(t *testing.T) {
t.Run("law 1: GetOption(ReverseGet(a)) == Some(a) for zero", func(t *testing.T) {
prism := FromZero[int]()
reversed := prism.ReverseGet(0)
extracted := prism.GetOption(reversed)
assert.True(t, O.IsSome(extracted))
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(extracted))
})
t.Run("law 2: if GetOption(s) == Some(a), then ReverseGet(a) == s", func(t *testing.T) {
prism := FromZero[string]()
extracted := prism.GetOption("")
if O.IsSome(extracted) {
value := O.GetOrElse(F.Constant("default"))(extracted)
reconstructed := prism.ReverseGet(value)
assert.Equal(t, "", reconstructed)
}
})
}
// TestRegexMatcher tests the RegexMatcher prism
func TestRegexMatcher(t *testing.T) {
t.Run("simple number match", func(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
result := prism.GetOption("price: 42 dollars")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "price: ", match.Before)
assert.Equal(t, "42", match.FullMatch())
assert.Equal(t, " dollars", match.After)
})
t.Run("no match returns None", func(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
result := prism.GetOption("no numbers here")
assert.True(t, O.IsNone(result))
})
t.Run("match with capture groups", func(t *testing.T) {
re := regexp.MustCompile(`(\w+)@(\w+\.\w+)`)
prism := RegexMatcher(re)
result := prism.GetOption("contact: user@example.com")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "contact: ", match.Before)
assert.Equal(t, "user@example.com", match.FullMatch())
assert.Equal(t, "user", match.Group(1))
assert.Equal(t, "example.com", match.Group(2))
assert.Equal(t, "", match.After)
})
t.Run("match at beginning", func(t *testing.T) {
re := regexp.MustCompile(`^\d+`)
prism := RegexMatcher(re)
result := prism.GetOption("123 test")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, "123", match.FullMatch())
assert.Equal(t, " test", match.After)
})
t.Run("match at end", func(t *testing.T) {
re := regexp.MustCompile(`\d+$`)
prism := RegexMatcher(re)
result := prism.GetOption("test 123")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "test ", match.Before)
assert.Equal(t, "123", match.FullMatch())
assert.Equal(t, "", match.After)
})
t.Run("reconstruct original string", func(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
original := "price: 42 dollars"
result := prism.GetOption(original)
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
reconstructed := match.Reconstruct()
assert.Equal(t, original, reconstructed)
})
t.Run("reverse get reconstructs", func(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
match := Match{
Before: "price: ",
Groups: []string{"42"},
After: " dollars",
}
reconstructed := prism.ReverseGet(match)
assert.Equal(t, "price: 42 dollars", reconstructed)
})
t.Run("Group with invalid index returns empty", func(t *testing.T) {
match := Match{
Groups: []string{"full", "group1"},
}
assert.Equal(t, "full", match.Group(0))
assert.Equal(t, "group1", match.Group(1))
assert.Equal(t, "", match.Group(5))
})
t.Run("empty string match", func(t *testing.T) {
re := regexp.MustCompile(`.*`)
prism := RegexMatcher(re)
result := prism.GetOption("")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, "", match.FullMatch())
assert.Equal(t, "", match.After)
})
}
// TestRegexMatcherPrismLaws tests that RegexMatcher satisfies prism laws
func TestRegexMatcherPrismLaws(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
t.Run("law 1: GetOption(ReverseGet(match)) reconstructs", func(t *testing.T) {
match := Match{
Before: "test ",
Groups: []string{"123"},
After: " end",
}
str := prism.ReverseGet(match)
result := prism.GetOption(str)
assert.True(t, O.IsSome(result))
reconstructed := O.GetOrElse(F.Constant(Match{}))(result)
assert.Equal(t, match.Before, reconstructed.Before)
assert.Equal(t, match.Groups[0], reconstructed.Groups[0])
assert.Equal(t, match.After, reconstructed.After)
})
t.Run("law 2: ReverseGet(GetOption(s)) == s for matching strings", func(t *testing.T) {
original := "value: 42 units"
extracted := prism.GetOption(original)
if O.IsSome(extracted) {
match := O.GetOrElse(F.Constant(Match{}))(extracted)
reconstructed := prism.ReverseGet(match)
assert.Equal(t, original, reconstructed)
}
})
}
// TestRegexNamedMatcher tests the RegexNamedMatcher prism
func TestRegexNamedMatcher(t *testing.T) {
t.Run("email with named groups", func(t *testing.T) {
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("contact: user@example.com")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "contact: ", match.Before)
assert.Equal(t, "user@example.com", match.Full)
assert.Equal(t, "", match.After)
assert.Equal(t, "user", match.Groups["user"])
assert.Equal(t, "example.com", match.Groups["domain"])
})
t.Run("date with named groups", func(t *testing.T) {
re := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("Date: 2024-03-15")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "Date: ", match.Before)
assert.Equal(t, "2024-03-15", match.Full)
assert.Equal(t, "2024", match.Groups["year"])
assert.Equal(t, "03", match.Groups["month"])
assert.Equal(t, "15", match.Groups["day"])
})
t.Run("no match returns None", func(t *testing.T) {
re := regexp.MustCompile(`(?P<num>\d+)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("no numbers")
assert.True(t, O.IsNone(result))
})
t.Run("reconstruct original string", func(t *testing.T) {
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
prism := RegexNamedMatcher(re)
original := "email: admin@site.com here"
result := prism.GetOption(original)
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
reconstructed := match.Reconstruct()
assert.Equal(t, original, reconstructed)
})
t.Run("reverse get reconstructs", func(t *testing.T) {
re := regexp.MustCompile(`(?P<num>\d+)`)
prism := RegexNamedMatcher(re)
match := NamedMatch{
Before: "value: ",
Full: "42",
Groups: map[string]string{"num": "42"},
After: " end",
}
reconstructed := prism.ReverseGet(match)
assert.Equal(t, "value: 42 end", reconstructed)
})
t.Run("unnamed groups not in map", func(t *testing.T) {
// Mix of named and unnamed groups - use non-greedy match for clarity
re := regexp.MustCompile(`(?P<name>[a-z]+)(\d+)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("user123")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "user123", match.Full)
assert.Equal(t, "user", match.Groups["name"])
// Only named groups should be in the map, not unnamed ones
assert.Equal(t, 1, len(match.Groups))
})
t.Run("empty string match", func(t *testing.T) {
re := regexp.MustCompile(`(?P<all>.*)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "", match.Before)
assert.Equal(t, "", match.Full)
assert.Equal(t, "", match.After)
})
t.Run("multiple matches - only first", func(t *testing.T) {
re := regexp.MustCompile(`(?P<num>\d+)`)
prism := RegexNamedMatcher(re)
result := prism.GetOption("first 123 second 456")
assert.True(t, O.IsSome(result))
match := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, "first ", match.Before)
assert.Equal(t, "123", match.Full)
assert.Equal(t, " second 456", match.After)
assert.Equal(t, "123", match.Groups["num"])
})
}
// TestRegexNamedMatcherPrismLaws tests that RegexNamedMatcher satisfies prism laws
func TestRegexNamedMatcherPrismLaws(t *testing.T) {
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
prism := RegexNamedMatcher(re)
t.Run("law 1: GetOption(ReverseGet(match)) reconstructs", func(t *testing.T) {
match := NamedMatch{
Before: "email: ",
Full: "user@example.com",
Groups: map[string]string{
"user": "user",
"domain": "example.com",
},
After: "",
}
str := prism.ReverseGet(match)
result := prism.GetOption(str)
assert.True(t, O.IsSome(result))
reconstructed := O.GetOrElse(F.Constant(NamedMatch{}))(result)
assert.Equal(t, match.Before, reconstructed.Before)
assert.Equal(t, match.Full, reconstructed.Full)
assert.Equal(t, match.After, reconstructed.After)
})
t.Run("law 2: ReverseGet(GetOption(s)) == s for matching strings", func(t *testing.T) {
original := "contact: admin@site.com"
extracted := prism.GetOption(original)
if O.IsSome(extracted) {
match := O.GetOrElse(F.Constant(NamedMatch{}))(extracted)
reconstructed := prism.ReverseGet(match)
assert.Equal(t, original, reconstructed)
}
})
}
// TestRegexMatcherWithSet tests using Set with RegexMatcher
func TestRegexMatcherWithSet(t *testing.T) {
re := regexp.MustCompile(`\d+`)
prism := RegexMatcher(re)
t.Run("set on matching string", func(t *testing.T) {
original := "price: 42 dollars"
newMatch := Match{
Before: "price: ",
Groups: []string{"100"},
After: " dollars",
}
setter := Set[string, Match](newMatch)
result := setter(prism)(original)
assert.Equal(t, "price: 100 dollars", result)
})
t.Run("set on non-matching string returns original", func(t *testing.T) {
original := "no numbers"
newMatch := Match{
Before: "",
Groups: []string{"42"},
After: "",
}
setter := Set[string, Match](newMatch)
result := setter(prism)(original)
assert.Equal(t, original, result)
})
}
// TestRegexNamedMatcherWithSet tests using Set with RegexNamedMatcher
func TestRegexNamedMatcherWithSet(t *testing.T) {
re := regexp.MustCompile(`(?P<user>\w+)@(?P<domain>\w+\.\w+)`)
prism := RegexNamedMatcher(re)
t.Run("set on matching string", func(t *testing.T) {
original := "email: user@example.com"
newMatch := NamedMatch{
Before: "email: ",
Full: "admin@newsite.com",
Groups: map[string]string{
"user": "admin",
"domain": "newsite.com",
},
After: "",
}
setter := Set[string, NamedMatch](newMatch)
result := setter(prism)(original)
assert.Equal(t, "email: admin@newsite.com", result)
})
t.Run("set on non-matching string returns original", func(t *testing.T) {
original := "no email here"
newMatch := NamedMatch{
Before: "",
Full: "test@test.com",
Groups: map[string]string{
"user": "test",
"domain": "test.com",
},
After: "",
}
setter := Set[string, NamedMatch](newMatch)
result := setter(prism)(original)
assert.Equal(t, original, result)
})
}

View File

@@ -33,7 +33,7 @@ import (
// }
// result := TraverseArrayG[[]string, []int](parse)([]string{"1", "2", "3"}) // Some([1, 2, 3])
// result := TraverseArrayG[[]string, []int](parse)([]string{"1", "x", "3"}) // None
func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f func(A) Option[B]) func(GA) Option[GB] {
func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f Kleisli[A, B]) Kleisli[GA, GB] {
return RA.Traverse[GA](
Of[GB],
Map[GB, func(B) GB],
@@ -54,7 +54,7 @@ func TraverseArrayG[GA ~[]A, GB ~[]B, A, B any](f func(A) Option[B]) func(GA) Op
// }
// result := TraverseArray(validate)([]int{1, 2, 3}) // Some([2, 4, 6])
// result := TraverseArray(validate)([]int{1, -1, 3}) // None
func TraverseArray[A, B any](f func(A) Option[B]) func([]A) Option[[]B] {
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return TraverseArrayG[[]A, []B](f)
}
@@ -68,7 +68,7 @@ func TraverseArray[A, B any](f func(A) Option[B]) func([]A) Option[[]B] {
// return Some(fmt.Sprintf("%d:%s", i, s))
// }
// result := TraverseArrayWithIndexG[[]string, []string](f)([]string{"a", "b"}) // Some(["0:a", "1:b"])
func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) Option[B]) func(GA) Option[GB] {
func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) Option[B]) Kleisli[GA, GB] {
return RA.TraverseWithIndex[GA](
Of[GB],
Map[GB, func(B) GB],
@@ -88,7 +88,7 @@ func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) Option[B
// return None[int]()
// }
// result := TraverseArrayWithIndex(f)([]int{1, 2, 3}) // Some([1, 2, 3])
func TraverseArrayWithIndex[A, B any](f func(int, A) Option[B]) func([]A) Option[[]B] {
func TraverseArrayWithIndex[A, B any](f func(int, A) Option[B]) Kleisli[[]A, []B] {
return TraverseArrayWithIndexG[[]A, []B](f)
}

View File

@@ -19,6 +19,7 @@ import (
A "github.com/IBM/fp-go/v2/internal/apply"
C "github.com/IBM/fp-go/v2/internal/chain"
F "github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type S to be used with the Bind operation.
@@ -51,8 +52,8 @@ func Do[S any](
// )
func Bind[S1, S2, A any](
setter func(A) func(S1) S2,
f func(S1) Option[A],
) func(Option[S1]) Option[S2] {
f Kleisli[S1, A],
) Kleisli[Option[S1], S2] {
return C.Bind(
Chain[S1, S2],
Map[A, S2],
@@ -76,7 +77,7 @@ func Bind[S1, S2, A any](
func Let[S1, S2, B any](
key func(B) func(S1) S2,
f func(S1) B,
) func(Option[S1]) Option[S2] {
) Kleisli[Option[S1], S2] {
return F.Let(
Map[S1, S2],
key,
@@ -98,7 +99,7 @@ func Let[S1, S2, B any](
func LetTo[S1, S2, B any](
key func(B) func(S1) S2,
b B,
) func(Option[S1]) Option[S2] {
) Kleisli[Option[S1], S2] {
return F.LetTo(
Map[S1, S2],
key,
@@ -118,7 +119,7 @@ func LetTo[S1, S2, B any](
// )
func BindTo[S1, T any](
setter func(T) S1,
) func(Option[T]) Option[S1] {
) Kleisli[Option[T], S1] {
return C.BindTo(
Map[T, S1],
setter,
@@ -140,7 +141,7 @@ func BindTo[S1, T any](
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa Option[T],
) func(Option[S1]) Option[S2] {
) Kleisli[Option[S1], S2] {
return A.ApS(
Ap[S2, T],
Map[S1, func(T) S2],
@@ -148,3 +149,158 @@ func ApS[S1, S2, T any](
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type Address struct {
// Street string
// City string
// }
//
// type Person struct {
// Name string
// Address Address
// }
//
// // Create a lens for the Address field
// addressLens := lens.MakeLens(
// func(p Person) Address { return p.Address },
// func(p Person, a Address) Person { p.Address = a; return p },
// )
//
// // Use ApSL to update the address
// result := F.Pipe2(
// option.Some(Person{Name: "Alice"}),
// option.ApSL(
// addressLens,
// option.Some(Address{Street: "Main St", City: "NYC"}),
// ),
// )
func ApSL[S, T any](
lens L.Lens[S, T],
fa Option[T],
) Kleisli[Option[S], S] {
return ApS(lens.Set, fa)
}
// BindL attaches the result of a computation to a context using a lens-based setter.
// This is a convenience function that combines Bind with a lens, allowing you to use
// optics to update nested structures based on their current values.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The computation function f receives the current value of the focused field and returns
// an Option that produces the new value.
//
// Unlike ApSL, BindL uses monadic sequencing, meaning the computation f can depend on
// the current value of the focused field.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Increment the counter, but return None if it would exceed 100
// increment := func(v int) option.Option[int] {
// if v >= 100 {
// return option.None[int]()
// }
// return option.Some(v + 1)
// }
//
// result := F.Pipe1(
// option.Some(Counter{Value: 42}),
// option.BindL(valueLens, increment),
// ) // Some(Counter{Value: 43})
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Kleisli[Option[S], S] {
return Bind[S, S, T](lens.Set, func(s S) Option[T] {
return f(lens.Get(s))
})
}
// LetL attaches the result of a pure computation to a context using a lens-based setter.
// This is a convenience function that combines Let with a lens, allowing you to use
// optics to update nested structures with pure transformations.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// The transformation function f receives the current value of the focused field and returns
// the new value directly (not wrapped in Option).
//
// This is useful for pure transformations that cannot fail, such as mathematical operations,
// string manipulations, or other deterministic updates.
//
// Example:
//
// type Counter struct {
// Value int
// }
//
// valueLens := lens.MakeLens(
// func(c Counter) int { return c.Value },
// func(c Counter, v int) Counter { c.Value = v; return c },
// )
//
// // Double the counter value
// double := func(v int) int { return v * 2 }
//
// result := F.Pipe1(
// option.Some(Counter{Value: 21}),
// option.LetL(valueLens, double),
// ) // Some(Counter{Value: 42})
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
) Kleisli[Option[S], S] {
return Let[S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL attaches a constant value to a context using a lens-based setter.
// This is a convenience function that combines LetTo with a lens, allowing you to use
// optics to set nested fields to specific values.
//
// The lens parameter provides the setter for a field within the structure S.
// Unlike LetL which transforms the current value, LetToL simply replaces it with
// the provided constant value b.
//
// This is useful for resetting fields, initializing values, or setting fields to
// predetermined constants.
//
// Example:
//
// type Config struct {
// Debug bool
// Timeout int
// }
//
// debugLens := lens.MakeLens(
// func(c Config) bool { return c.Debug },
// func(c Config, d bool) Config { c.Debug = d; return c },
// )
//
// result := F.Pipe1(
// option.Some(Config{Debug: true, Timeout: 30}),
// option.LetToL(debugLens, false),
// ) // Some(Config{Debug: false, Timeout: 30})
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Kleisli[Option[S], S] {
return LetTo[S, S, T](lens.Set, b)
}

View File

@@ -43,6 +43,11 @@ type Option[A any] struct {
value A
}
type (
Kleisli[A, B any] = func(A) Option[B]
Operator[A, B any] = Kleisli[Option[A], B]
)
// optString prints some debug info for the object
//
//go:noinline

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ import (
L "github.com/IBM/fp-go/v2/logging"
)
func _log[A any](left func(string, ...any), right func(string, ...any), prefix string) func(Option[A]) Option[A] {
func _log[A any](left func(string, ...any), right func(string, ...any), prefix string) Kleisli[Option[A], A] {
return Fold(
func() Option[A] {
left("%s", prefix)
@@ -55,9 +55,9 @@ func _log[A any](left func(string, ...any), right func(string, ...any), prefix s
// None[int](),
// logger("step1"), // logs "step1"
// ) // None
func Logger[A any](loggers ...*log.Logger) func(string) func(Option[A]) Option[A] {
func Logger[A any](loggers ...*log.Logger) func(string) Kleisli[Option[A], A] {
left, right := L.LoggingCallbacks(loggers...)
return func(prefix string) func(Option[A]) Option[A] {
return func(prefix string) Kleisli[Option[A], A] {
delegate := _log[A](left, right, prefix)
return func(ma Option[A]) Option[A] {
return F.Pipe1(

View File

@@ -25,11 +25,11 @@ func (o *optionMonad[A, B]) Of(a A) Option[A] {
return Of[A](a)
}
func (o *optionMonad[A, B]) Map(f func(A) B) func(Option[A]) Option[B] {
func (o *optionMonad[A, B]) Map(f func(A) B) Operator[A, B] {
return Map[A, B](f)
}
func (o *optionMonad[A, B]) Chain(f func(A) Option[B]) func(Option[A]) Option[B] {
func (o *optionMonad[A, B]) Chain(f Kleisli[A, B]) Operator[A, B] {
return Chain[A, B](f)
}

View File

@@ -39,7 +39,7 @@ func fromPredicate[A any](a A, pred func(A) bool) Option[A] {
// isPositive := FromPredicate(func(n int) bool { return n > 0 })
// result := isPositive(5) // Some(5)
// result := isPositive(-1) // None
func FromPredicate[A any](pred func(A) bool) func(A) Option[A] {
func FromPredicate[A any](pred func(A) bool) Kleisli[A, A] {
return F.Bind2nd(fromPredicate[A], pred)
}
@@ -66,7 +66,7 @@ func FromNillable[A any](a *A) Option[*A] {
// return n, err == nil
// })
// result := parseNum("42") // Some(42)
func FromValidation[A, B any](f func(A) (B, bool)) func(A) Option[B] {
func FromValidation[A, B any](f func(A) (B, bool)) Kleisli[A, B] {
return Optionize1(f)
}
@@ -94,7 +94,7 @@ func MonadAp[B, A any](fab Option[func(A) B], fa Option[A]) Option[B] {
// applyTo5 := Ap[int](fa)
// fab := Some(func(x int) int { return x * 2 })
// result := applyTo5(fab) // Some(10)
func Ap[B, A any](fa Option[A]) func(Option[func(A) B]) Option[B] {
func Ap[B, A any](fa Option[A]) Operator[func(A) B, B] {
return F.Bind2nd(MonadAp[B, A], fa)
}
@@ -117,7 +117,7 @@ func MonadMap[A, B any](fa Option[A], f func(A) B) Option[B] {
// double := Map(func(x int) int { return x * 2 })
// result := double(Some(5)) // Some(10)
// result := double(None[int]()) // None
func Map[A, B any](f func(a A) B) func(Option[A]) Option[B] {
func Map[A, B any](f func(a A) B) Operator[A, B] {
return Chain(F.Flow2(f, Some[B]))
}
@@ -138,7 +138,7 @@ func MonadMapTo[A, B any](fa Option[A], b B) Option[B] {
//
// replaceWith42 := MapTo[string, int](42)
// result := replaceWith42(Some("hello")) // Some(42)
func MapTo[A, B any](b B) func(Option[A]) Option[B] {
func MapTo[A, B any](b B) Operator[A, B] {
return F.Bind2nd(MonadMapTo[A, B], b)
}
@@ -207,7 +207,7 @@ func GetOrElse[A any](onNone func() A) func(Option[A]) A {
// if x > 0 { return Some(x * 2) }
// return None[int]()
// }) // Some(10)
func MonadChain[A, B any](fa Option[A], f func(A) Option[B]) Option[B] {
func MonadChain[A, B any](fa Option[A], f Kleisli[A, B]) Option[B] {
return MonadFold(fa, None[B], f)
}
@@ -221,7 +221,7 @@ func MonadChain[A, B any](fa Option[A], f func(A) Option[B]) Option[B] {
// return None[int]()
// })
// result := validate(Some(5)) // Some(10)
func Chain[A, B any](f func(A) Option[B]) func(Option[A]) Option[B] {
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return Fold(None[B], f)
}
@@ -241,7 +241,7 @@ func MonadChainTo[A, B any](_ Option[A], mb Option[B]) Option[B] {
//
// replaceWith := ChainTo(Some("hello"))
// result := replaceWith(Some(42)) // Some("hello")
func ChainTo[A, B any](mb Option[B]) func(Option[A]) Option[B] {
func ChainTo[A, B any](mb Option[B]) Operator[A, B] {
return F.Bind2nd(MonadChainTo[A, B], mb)
}
@@ -253,7 +253,7 @@ func ChainTo[A, B any](mb Option[B]) func(Option[A]) Option[B] {
// result := MonadChainFirst(Some(5), func(x int) Option[string] {
// return Some(fmt.Sprintf("%d", x))
// }) // Some(5) - original value is kept
func MonadChainFirst[A, B any](ma Option[A], f func(A) Option[B]) Option[A] {
func MonadChainFirst[A, B any](ma Option[A], f Kleisli[A, B]) Option[A] {
return C.MonadChainFirst(
MonadChain[A, A],
MonadMap[B, A],
@@ -271,7 +271,7 @@ func MonadChainFirst[A, B any](ma Option[A], f func(A) Option[B]) Option[A] {
// return Some("logged")
// })
// result := logAndKeep(Some(5)) // Some(5)
func ChainFirst[A, B any](f func(A) Option[B]) func(Option[A]) Option[A] {
func ChainFirst[A, B any](f Kleisli[A, B]) Kleisli[Option[A], A] {
return C.ChainFirst(
Chain[A, A],
Map[B, A],
@@ -309,7 +309,7 @@ func MonadAlt[A any](fa Option[A], that func() Option[A]) Option[A] {
// withDefault := Alt(func() Option[int] { return Some(0) })
// result := withDefault(Some(5)) // Some(5)
// result := withDefault(None[int]()) // Some(0)
func Alt[A any](that func() Option[A]) func(Option[A]) Option[A] {
func Alt[A any](that func() Option[A]) Kleisli[Option[A], A] {
return Fold(that, Of[A])
}
@@ -361,7 +361,7 @@ func Reduce[A, B any](f func(B, A) B, initial B) func(Option[A]) B {
// result := isPositive(Some(5)) // Some(5)
// result := isPositive(Some(-1)) // None
// result := isPositive(None[int]()) // None
func Filter[A any](pred func(A) bool) func(Option[A]) Option[A] {
func Filter[A any](pred func(A) bool) Kleisli[Option[A], A] {
return Fold(None[A], F.Ternary(pred, Of[A], F.Ignore1of1[A](None[A])))
}
@@ -383,6 +383,6 @@ func MonadFlap[B, A any](fab Option[func(A) B], a A) Option[B] {
// applyFive := Flap[int](5)
// fab := Some(func(x int) int { return x * 2 })
// result := applyFive(fab) // Some(10)
func Flap[B, A any](a A) func(Option[func(A) B]) Option[B] {
func Flap[B, A any](a A) Operator[func(A) B, B] {
return FC.Flap(Map[func(A) B, B], a)
}

View File

@@ -32,7 +32,7 @@ import (
// }
// input := map[string]int{"a": 1, "b": 2}
// result := TraverseRecordG[map[string]int, map[string]int](validate)(input) // Some(map[a:2 b:4])
func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f func(A) Option[B]) func(GA) Option[GB] {
func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f Kleisli[A, B]) Kleisli[GA, GB] {
return RR.Traverse[GA](
Of[GB],
Map[GB, func(B) GB],
@@ -53,7 +53,7 @@ func TraverseRecordG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f func(A)
// }
// input := map[string]int{"a": 1, "b": 2}
// result := TraverseRecord(validate)(input) // Some(map[a:"1" b:"2"])
func TraverseRecord[K comparable, A, B any](f func(A) Option[B]) func(map[K]A) Option[map[K]B] {
func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, map[K]B] {
return TraverseRecordG[map[K]A, map[K]B](f)
}
@@ -68,7 +68,7 @@ func TraverseRecord[K comparable, A, B any](f func(A) Option[B]) func(map[K]A) O
// }
// input := map[string]int{"a": 1, "b": 2}
// result := TraverseRecordWithIndexG[map[string]int, map[string]string](f)(input) // Some(map[a:"a:1" b:"b:2"])
func TraverseRecordWithIndexG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f func(K, A) Option[B]) func(GA) Option[GB] {
func TraverseRecordWithIndexG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](f func(K, A) Option[B]) Kleisli[GA, GB] {
return RR.TraverseWithIndex[GA](
Of[GB],
Map[GB, func(B) GB],
@@ -89,7 +89,7 @@ func TraverseRecordWithIndexG[GA ~map[K]A, GB ~map[K]B, K comparable, A, B any](
// }
// input := map[string]int{"a": 1, "b": 2}
// result := TraverseRecordWithIndex(f)(input) // Some(map[a:1 b:2])
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) Option[B]) func(map[K]A) Option[map[K]B] {
func TraverseRecordWithIndex[K comparable, A, B any](f func(K, A) Option[B]) Kleisli[map[K]A, map[K]B] {
return TraverseRecordWithIndexG[map[K]A, map[K]B](f)
}

View File

@@ -35,7 +35,7 @@ import (
// )
func Sequence[A, HKTA, HKTOA any](
mof func(Option[A]) HKTOA,
mmap func(func(A) Option[A]) func(HKTA) HKTOA,
mmap func(Kleisli[A, A]) func(HKTA) HKTOA,
) func(Option[HKTA]) HKTOA {
return Fold(F.Nullary2(None[A], mof), mmap(Some[A]))
}
@@ -59,7 +59,7 @@ func Sequence[A, HKTA, HKTOA any](
// )
func Traverse[A, B, HKTB, HKTOB any](
mof func(Option[B]) HKTOB,
mmap func(func(B) Option[B]) func(HKTB) HKTOB,
mmap func(Kleisli[B, B]) func(HKTB) HKTOB,
) func(func(A) HKTB) func(Option[A]) HKTOB {
onNone := F.Nullary2(None[B], mof)
onSome := mmap(Some[B])

View File

@@ -37,7 +37,7 @@ import (
// }
// r := reader.MonadTraverseArray(numbers, addPrefix)
// result := r(Config{Prefix: "num"}) // ["num1", "num2", "num3"]
func MonadTraverseArray[R, A, B any](ma []A, f func(A) Reader[R, B]) Reader[R, []B] {
func MonadTraverseArray[R, A, B any](ma []A, f Kleisli[R, A, B]) Reader[R, []B] {
return array.MonadTraverse[[]A](
Of[R, []B],
Map[R, []B, func(B) []B],
@@ -62,7 +62,7 @@ func MonadTraverseArray[R, A, B any](ma []A, f func(A) Reader[R, B]) Reader[R, [
// transform := reader.TraverseArray(multiply)
// r := transform([]int{1, 2, 3})
// result := r(Config{Multiplier: 10}) // [10, 20, 30]
func TraverseArray[R, A, B any](f func(A) Reader[R, B]) func([]A) Reader[R, []B] {
func TraverseArray[R, A, B any](f Kleisli[R, A, B]) func([]A) Reader[R, []B] {
return array.Traverse[[]A](
Of[R, []B],
Map[R, []B, func(B) []B],

View File

@@ -19,6 +19,7 @@ import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Do creates an empty context of type [S] to be used with the [Bind] operation.
@@ -82,7 +83,7 @@ func Do[R, S any](
// )
func Bind[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) Reader[R, T],
f Kleisli[R, S1, T],
) Operator[R, S1, S2] {
return chain.Bind(
Chain[R, S1, S2],
@@ -208,3 +209,158 @@ func ApS[R, S1, S2, T any](
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type State struct {
// Host string
// Port int
// }
// type Config struct {
// DefaultHost string
// DefaultPort int
// }
//
// portLens := lens.MakeLens(
// func(s State) int { return s.Port },
// func(s State, p int) State { s.Port = p; return s },
// )
//
// getPort := reader.Asks(func(c Config) int { return c.DefaultPort })
// result := F.Pipe2(
// reader.Of[Config](State{Host: "localhost"}),
// reader.ApSL(portLens, getPort),
// )
func ApSL[R, S, T any](
lens L.Lens[S, T],
fa Reader[R, T],
) Operator[R, S, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a Reader computation that produces an updated value.
//
// Example:
//
// type State struct {
// Config ConfigData
// Status string
// }
// type ConfigData struct {
// Host string
// Port int
// }
// type Env struct {
// DefaultHost string
// DefaultPort int
// }
//
// configLens := lens.MakeLens(
// func(s State) ConfigData { return s.Config },
// func(s State, c ConfigData) State { s.Config = c; return s },
// )
//
// result := F.Pipe2(
// reader.Do[Env](State{}),
// reader.BindL(configLens, func(cfg ConfigData) reader.Reader[Env, ConfigData] {
// return reader.Asks(func(e Env) ConfigData {
// return ConfigData{Host: e.DefaultHost, Port: e.DefaultPort}
// })
// }),
// )
func BindL[R, S, T any](
lens L.Lens[S, T],
f Kleisli[R, T, T],
) Operator[R, S, S] {
return Bind[R, S, S, T](lens.Set, func(s S) Reader[R, T] {
return f(lens.Get(s))
})
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new value (without wrapping in a Reader).
//
// Example:
//
// type State struct {
// Config ConfigData
// Status string
// }
// type ConfigData struct {
// Host string
// Port int
// }
//
// configLens := lens.MakeLens(
// func(s State) ConfigData { return s.Config },
// func(s State, c ConfigData) State { s.Config = c; return s },
// )
//
// result := F.Pipe2(
// reader.Do[any](State{Config: ConfigData{Host: "localhost"}}),
// reader.LetL(configLens, func(cfg ConfigData) ConfigData {
// cfg.Port = 8080
// return cfg
// }),
// )
func LetL[R, S, T any](
lens L.Lens[S, T],
f func(T) T,
) Operator[R, S, S] {
return Let[R, S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The value b is set directly to the focused field.
//
// Example:
//
// type State struct {
// Config ConfigData
// Status string
// }
// type ConfigData struct {
// Host string
// Port int
// }
//
// configLens := lens.MakeLens(
// func(s State) ConfigData { return s.Config },
// func(s State, c ConfigData) State { s.Config = c; return s },
// )
//
// newConfig := ConfigData{Host: "localhost", Port: 8080}
// result := F.Pipe2(
// reader.Do[any](State{}),
// reader.LetToL(configLens, newConfig),
// )
func LetToL[R, S, T any](
lens L.Lens[S, T],
b T,
) Operator[R, S, S] {
return LetTo[R, S, S, T](lens.Set, b)
}

View File

@@ -48,7 +48,7 @@ func Curry0[R, A any](f func(R) A) Reader[R, A] {
// curried := reader.Curry1(addPrefix)
// r := curried("hello")
// result := r(Config{Prefix: ">> "}) // ">> hello"
func Curry1[R, T1, A any](f func(R, T1) A) func(T1) Reader[R, A] {
func Curry1[R, T1, A any](f func(R, T1) A) Kleisli[R, T1, A] {
return G.Curry1[Reader[R, A]](f)
}
@@ -121,7 +121,7 @@ func Uncurry0[R, A any](f Reader[R, A]) func(R) A {
// }
// f := reader.Uncurry1(curried)
// result := f(Config{Prefix: ">> "}, "hello") // ">> hello"
func Uncurry1[R, T1, A any](f func(T1) Reader[R, A]) func(R, T1) A {
func Uncurry1[R, T1, A any](f Kleisli[R, T1, A]) func(R, T1) A {
return G.Uncurry1(f)
}

View File

@@ -154,7 +154,7 @@ func Of[R, A any](a A) Reader[R, A] {
// }
// r := reader.MonadChain(getUser, getUserName)
// name := r(Config{UserId: 42}) // "User42"
func MonadChain[R, A, B any](ma Reader[R, A], f func(A) Reader[R, B]) Reader[R, B] {
func MonadChain[R, A, B any](ma Reader[R, A], f Kleisli[R, A, B]) Reader[R, B] {
return func(r R) B {
return f(ma(r))(r)
}
@@ -172,7 +172,7 @@ func MonadChain[R, A, B any](ma Reader[R, A], f func(A) Reader[R, B]) Reader[R,
// }
// r := reader.Chain(getUserName)(getUser)
// name := r(Config{UserId: 42}) // "User42"
func Chain[R, A, B any](f func(A) Reader[R, B]) Operator[R, A, B] {
func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
return function.Bind2nd(MonadChain[R, A, B], f)
}
@@ -187,7 +187,7 @@ func Chain[R, A, B any](f func(A) Reader[R, B]) Operator[R, A, B] {
// }
// flat := reader.Flatten(nested)
// result := flat(Config{Value: 5}) // 10 (5 + 5)
func Flatten[R, A any](mma func(R) Reader[R, A]) Reader[R, A] {
func Flatten[R, A any](mma Reader[R, Reader[R, A]]) Reader[R, A] {
return MonadChain(mma, function.Identity[Reader[R, A]])
}

View File

@@ -47,6 +47,8 @@ type (
// apiKey := getAPIKey(config) // "secret"
Reader[R, A any] = func(R) A
Kleisli[R, A, B any] = func(A) Reader[R, B]
// Operator represents a transformation from one Reader to another.
// It takes a Reader[R, A] and produces a Reader[R, B], where both readers
// share the same environment type R.
@@ -69,5 +71,5 @@ type (
// getNumber := reader.Asks(func(c Config) int { return c.Multiplier })
// getString := intToString(getNumber)
// result := getString(Config{Multiplier: 42}) // "42"
Operator[R, A, B any] = func(Reader[R, A]) Reader[R, B]
Operator[R, A, B any] = Kleisli[R, Reader[R, A], B]
)

View File

@@ -16,17 +16,72 @@
package readereither
import (
L "github.com/IBM/fp-go/v2/optics/lens"
G "github.com/IBM/fp-go/v2/readereither/generic"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
// result := readereither.Do[Env, error](State{})
func Do[R, E, S any](
empty S,
) ReaderEither[R, E, S] {
return G.Do[ReaderEither[R, E, S], R, E, S](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access the shared environment.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readereither.ReaderEither[Env, error, User] {
// return readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// },
// ),
// readereither.Bind(
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) readereither.ReaderEither[Env, error, Config] {
// // This can access s.User from the previous step
// return readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfigForUser(s.User.ID)
// })
// },
// ),
// )
func Bind[R, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderEither[R, E, T],
@@ -104,3 +159,148 @@ func ApS[R, E, S1, S2, T any](
) func(ReaderEither[R, E, S1]) ReaderEither[R, E, S2] {
return G.ApS[ReaderEither[R, E, S1], ReaderEither[R, E, S2], ReaderEither[R, E, T], R, E, S1, S2, T](setter, fa)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// configLens := lens.MakeLens(
// func(s State) Config { return s.Config },
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// getConfig := readereither.Asks(func(env Env) either.Either[error, Config] {
// return env.ConfigService.GetConfig()
// })
// result := F.Pipe2(
// readereither.Of[Env, error](State{}),
// readereither.ApSL(configLens, getConfig),
// )
func ApSL[R, E, S, T any](
lens L.Lens[S, T],
fa ReaderEither[R, E, T],
) func(ReaderEither[R, E, S]) ReaderEither[R, E, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a ReaderEither computation that produces an updated value.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
// type Env struct {
// UserService UserService
// ConfigService ConfigService
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readereither.Do[Env, error](State{}),
// readereither.BindL(userLens, func(user User) readereither.ReaderEither[Env, error, User] {
// return readereither.Asks(func(env Env) either.Either[error, User] {
// return env.UserService.GetUser()
// })
// }),
// )
func BindL[R, E, S, T any](
lens L.Lens[S, T],
f func(T) ReaderEither[R, E, T],
) func(ReaderEither[R, E, S]) ReaderEither[R, E, S] {
return Bind[R, E, S, S, T](lens.Set, func(s S) ReaderEither[R, E, T] {
return f(lens.Get(s))
})
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new value (without wrapping in a ReaderEither).
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// configLens := lens.MakeLens(
// func(s State) Config { return s.Config },
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// result := F.Pipe2(
// readereither.Do[any, error](State{Config: Config{Host: "localhost"}}),
// readereither.LetL(configLens, func(cfg Config) Config {
// cfg.Port = 8080
// return cfg
// }),
// )
func LetL[R, E, S, T any](
lens L.Lens[S, T],
f func(T) T,
) func(ReaderEither[R, E, S]) ReaderEither[R, E, S] {
return Let[R, E, S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The value b is set directly to the focused field.
//
// Example:
//
// type State struct {
// User User
// Config Config
// }
//
// configLens := lens.MakeLens(
// func(s State) Config { return s.Config },
// func(s State, c Config) State { s.Config = c; return s },
// )
//
// newConfig := Config{Host: "localhost", Port: 8080}
// result := F.Pipe2(
// readereither.Do[any, error](State{}),
// readereither.LetToL(configLens, newConfig),
// )
func LetToL[R, E, S, T any](
lens L.Lens[S, T],
b T,
) func(ReaderEither[R, E, S]) ReaderEither[R, E, S] {
return LetTo[R, E, S, S, T](lens.Set, b)
}

View File

@@ -22,14 +22,68 @@ import (
F "github.com/IBM/fp-go/v2/internal/functor"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// Config Config
// User User
// }
// type Env struct {
// ConfigService ConfigService
// UserService UserService
// }
// result := generic.Do[ReaderEither[Env, error, State], Env, error, State](State{})
func Do[GS ~func(R) ET.Either[E, S], R, E, S any](
empty S,
) GS {
return Of[GS, E, R, S](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access the shared environment.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// Config Config
// User User
// }
// type Env struct {
// ConfigService ConfigService
// UserService UserService
// }
//
// result := F.Pipe2(
// generic.Do[ReaderEither[Env, error, State], Env, error, State](State{}),
// generic.Bind[ReaderEither[Env, error, State], ReaderEither[Env, error, State], ReaderEither[Env, error, Config], Env, error, State, State, Config](
// func(cfg Config) func(State) State {
// return func(s State) State { s.Config = cfg; return s }
// },
// func(s State) ReaderEither[Env, error, Config] {
// return func(env Env) either.Either[error, Config] {
// return env.ConfigService.Load()
// }
// },
// ),
// generic.Bind[ReaderEither[Env, error, State], ReaderEither[Env, error, State], ReaderEither[Env, error, User], Env, error, State, State, User](
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) ReaderEither[Env, error, User] {
// // This can access s.Config from the previous step
// return func(env Env) either.Either[error, User] {
// return env.UserService.GetUserForConfig(s.Config)
// }
// },
// ),
// )
func Bind[GS1 ~func(R) ET.Either[E, S1], GS2 ~func(R) ET.Either[E, S2], GT ~func(R) ET.Either[E, T], R, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) GT,

View File

@@ -19,16 +19,71 @@ import (
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/functor"
L "github.com/IBM/fp-go/v2/optics/lens"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// Host string
// Port int
// }
// type Config struct {
// DefaultHost string
// DefaultPort int
// }
// result := readerio.Do[Config](State{})
func Do[R, S any](
empty S,
) ReaderIO[R, S] {
return Of[R](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access the shared environment.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// Host string
// Port int
// }
// type Config struct {
// DefaultHost string
// DefaultPort int
// }
//
// result := F.Pipe2(
// readerio.Do[Config](State{}),
// readerio.Bind(
// func(host string) func(State) State {
// return func(s State) State { s.Host = host; return s }
// },
// func(s State) readerio.ReaderIO[Config, string] {
// return readerio.Asks(func(c Config) io.IO[string] {
// return io.Of(c.DefaultHost)
// })
// },
// ),
// readerio.Bind(
// func(port int) func(State) State {
// return func(s State) State { s.Port = port; return s }
// },
// func(s State) readerio.ReaderIO[Config, int] {
// // This can access s.Host from the previous step
// return readerio.Asks(func(c Config) io.IO[int] {
// return io.Of(c.DefaultPort)
// })
// },
// ),
// )
func Bind[R, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderIO[R, T],
@@ -127,3 +182,146 @@ func ApS[R, S1, S2, T any](
fa,
)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type State struct {
// Host string
// Port int
// }
// type Config struct {
// DefaultHost string
// DefaultPort int
// }
//
// portLens := lens.MakeLens(
// func(s State) int { return s.Port },
// func(s State, p int) State { s.Port = p; return s },
// )
//
// getPort := readerio.Asks(func(c Config) io.IO[int] {
// return io.Of(c.DefaultPort)
// })
// result := F.Pipe2(
// readerio.Of[Config](State{Host: "localhost"}),
// readerio.ApSL(portLens, getPort),
// )
func ApSL[R, S, T any](
lens L.Lens[S, T],
fa ReaderIO[R, T],
) func(ReaderIO[R, S]) ReaderIO[R, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a ReaderIO computation that produces an updated value.
//
// Example:
//
// type State struct {
// Host string
// Port int
// }
// type Config struct {
// DefaultHost string
// DefaultPort int
// }
//
// portLens := lens.MakeLens(
// func(s State) int { return s.Port },
// func(s State, p int) State { s.Port = p; return s },
// )
//
// result := F.Pipe2(
// readerio.Do[Config](State{Host: "localhost"}),
// readerio.BindL(portLens, func(port int) readerio.ReaderIO[Config, int] {
// return readerio.Asks(func(c Config) io.IO[int] {
// return io.Of(c.DefaultPort)
// })
// }),
// )
func BindL[R, S, T any](
lens L.Lens[S, T],
f func(T) ReaderIO[R, T],
) func(ReaderIO[R, S]) ReaderIO[R, S] {
return Bind[R, S, S, T](lens.Set, func(s S) ReaderIO[R, T] {
return f(lens.Get(s))
})
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new value (without wrapping in a ReaderIO).
//
// Example:
//
// type State struct {
// Host string
// Port int
// }
//
// portLens := lens.MakeLens(
// func(s State) int { return s.Port },
// func(s State, p int) State { s.Port = p; return s },
// )
//
// result := F.Pipe2(
// readerio.Do[any](State{Host: "localhost", Port: 8080}),
// readerio.LetL(portLens, func(port int) int {
// return port + 1
// }),
// )
func LetL[R, S, T any](
lens L.Lens[S, T],
f func(T) T,
) func(ReaderIO[R, S]) ReaderIO[R, S] {
return Let[R, S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The value b is set directly to the focused field.
//
// Example:
//
// type State struct {
// Host string
// Port int
// }
//
// portLens := lens.MakeLens(
// func(s State) int { return s.Port },
// func(s State, p int) State { s.Port = p; return s },
// )
//
// result := F.Pipe2(
// readerio.Do[any](State{Host: "localhost"}),
// readerio.LetToL(portLens, 8080),
// )
func LetToL[R, S, T any](
lens L.Lens[S, T],
b T,
) func(ReaderIO[R, S]) ReaderIO[R, S] {
return LetTo[R, S, S, T](lens.Set, b)
}

View File

@@ -17,17 +17,76 @@ package readerioeither
import (
IOE "github.com/IBM/fp-go/v2/ioeither"
L "github.com/IBM/fp-go/v2/optics/lens"
G "github.com/IBM/fp-go/v2/readerioeither/generic"
)
// Bind creates an empty context of type [S] to be used with the [Bind] operation
// Do creates an empty context of type [S] to be used with the [Bind] operation.
// This is the starting point for do-notation style composition.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
// type Env struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
// result := readerioeither.Do[Env, error](State{})
//
//go:inline
func Do[R, E, S any](
empty S,
) ReaderIOEither[R, E, S] {
return G.Do[ReaderIOEither[R, E, S], IOE.IOEither[E, S], R, E, S](empty)
}
// Bind attaches the result of a computation to a context [S1] to produce a context [S2]
// Bind attaches the result of a computation to a context [S1] to produce a context [S2].
// This enables sequential composition where each step can depend on the results of previous steps
// and access the shared environment.
//
// The setter function takes the result of the computation and returns a function that
// updates the context from S1 to S2.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
// type Env struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
//
// result := F.Pipe2(
// readerioeither.Do[Env, error](State{}),
// readerioeither.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerioeither.ReaderIOEither[Env, error, User] {
// return readerioeither.Asks(func(env Env) ioeither.IOEither[error, User] {
// return env.UserRepo.FindUser()
// })
// },
// ),
// readerioeither.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readerioeither.ReaderIOEither[Env, error, []Post] {
// // This can access s.User from the previous step
// return readerioeither.Asks(func(env Env) ioeither.IOEither[error, []Post] {
// return env.PostRepo.FindPostsByUser(s.User.ID)
// })
// },
// ),
// )
//
//go:inline
func Bind[R, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) ReaderIOEither[R, E, T],
@@ -36,6 +95,8 @@ func Bind[R, E, S1, S2, T any](
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
//
//go:inline
func Let[R, E, S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
@@ -44,6 +105,8 @@ func Let[R, E, S1, S2, T any](
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
//
//go:inline
func LetTo[R, E, S1, S2, T any](
setter func(T) func(S1) S2,
b T,
@@ -52,6 +115,8 @@ func LetTo[R, E, S1, S2, T any](
}
// BindTo initializes a new state [S1] from a value [T]
//
//go:inline
func BindTo[R, E, S1, T any](
setter func(T) S1,
) Operator[R, E, T, S1] {
@@ -99,9 +164,164 @@ func BindTo[R, E, S1, T any](
// getPosts,
// ),
// )
//
//go:inline
func ApS[R, E, S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderIOEither[R, E, T],
) Operator[R, E, S1, S2] {
return G.ApS[ReaderIOEither[R, E, func(T) S2], ReaderIOEither[R, E, S1], ReaderIOEither[R, E, S2], ReaderIOEither[R, E, T], IOE.IOEither[E, func(T) S2], IOE.IOEither[E, S1], IOE.IOEither[E, S2], IOE.IOEither[E, T], R, E, S1, S2, T](setter, fa)
}
// ApSL attaches a value to a context using a lens-based setter.
// This is a convenience function that combines ApS with a lens, allowing you to use
// optics to update nested structures in a more composable way.
//
// The lens parameter provides both the getter and setter for a field within the structure S.
// This eliminates the need to manually write setter functions.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
// type Env struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// getUser := readerioeither.Asks(func(env Env) ioeither.IOEither[error, User] {
// return env.UserRepo.FindUser()
// })
// result := F.Pipe2(
// readerioeither.Of[Env, error](State{}),
// readerioeither.ApSL(userLens, getUser),
// )
//
//go:inline
func ApSL[R, E, S, T any](
lens L.Lens[S, T],
fa ReaderIOEither[R, E, T],
) Operator[R, E, S, S] {
return ApS(lens.Set, fa)
}
// BindL is a variant of Bind that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a ReaderIOEither computation that produces an updated value.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
// type Env struct {
// UserRepo UserRepository
// PostRepo PostRepository
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readerioeither.Do[Env, error](State{}),
// readerioeither.BindL(userLens, func(user User) readerioeither.ReaderIOEither[Env, error, User] {
// return readerioeither.Asks(func(env Env) ioeither.IOEither[error, User] {
// return env.UserRepo.FindUser()
// })
// }),
// )
//
//go:inline
func BindL[R, E, S, T any](
lens L.Lens[S, T],
f func(T) ReaderIOEither[R, E, T],
) Operator[R, E, S, S] {
return Bind[R, E, S, S, T](lens.Set, func(s S) ReaderIOEither[R, E, T] {
return f(lens.Get(s))
})
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The function f receives the current value of the focused field and
// returns a new value (without wrapping in a ReaderIOEither).
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// result := F.Pipe2(
// readerioeither.Do[any, error](State{User: User{Name: "Alice"}}),
// readerioeither.LetL(userLens, func(user User) User {
// user.Name = "Bob"
// return user
// }),
// )
//
//go:inline
func LetL[R, E, S, T any](
lens L.Lens[S, T],
f func(T) T,
) Operator[R, E, S, S] {
return Let[R, E, S, S, T](lens.Set, func(s S) T {
return f(lens.Get(s))
})
}
// LetToL is a variant of LetTo that uses a lens to focus on a specific part of the context.
// This provides a more ergonomic API when working with nested structures, eliminating
// the need to manually write setter functions.
//
// The lens parameter provides both a getter and setter for a field of type T within
// the context S. The value b is set directly to the focused field.
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
//
// userLens := lens.MakeLens(
// func(s State) User { return s.User },
// func(s State, u User) State { s.User = u; return s },
// )
//
// newUser := User{Name: "Bob", ID: 123}
// result := F.Pipe2(
// readerioeither.Do[any, error](State{}),
// readerioeither.LetToL(userLens, newUser),
// )
//
//go:inline
func LetToL[R, E, S, T any](
lens L.Lens[S, T],
b T,
) Operator[R, E, S, S] {
return LetTo[R, E, S, S, T](lens.Set, b)
}

View File

@@ -22,6 +22,8 @@ import (
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
// whether the body action returns and error or not.
//
//go:inline
func Bracket[
R, E, A, B, ANY any](

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