diff --git a/v2/cli/lens.go b/v2/cli/lens.go index 3154e2a..74318e4 100644 --- a/v2/cli/lens.go +++ b/v2/cli/lens.go @@ -76,15 +76,25 @@ type templateData struct { const lensStructTemplate = ` // {{.Name}}Lenses provides lenses for accessing fields of {{.Name}} type {{.Name}}Lenses struct { + // mandatory fields {{- range .Fields}} - {{.Name}} {{if .IsOptional}}LO.LensO[{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[{{$.Name}}, {{.TypeName}}]{{end}} + {{.Name}} L.Lens[{{$.Name}}, {{.TypeName}}] +{{- end}} + // optional fields +{{- range .Fields}} + {{.Name}}O LO.LensO[{{$.Name}}, {{.TypeName}}] {{- end}} } // {{.Name}}RefLenses provides lenses for accessing fields of {{.Name}} via a reference to {{.Name}} type {{.Name}}RefLenses struct { + // mandatory fields {{- range .Fields}} - {{.Name}} {{if .IsOptional}}LO.LensO[*{{$.Name}}, {{.TypeName}}]{{else}}L.Lens[*{{$.Name}}, {{.TypeName}}]{{end}} + {{.Name}} L.Lens[*{{$.Name}}, {{.TypeName}}] +{{- end}} + // optional fields +{{- range .Fields}} + {{.Name}}O LO.LensO[*{{$.Name}}, {{.TypeName}}] {{- end}} } ` @@ -92,57 +102,57 @@ type {{.Name}}RefLenses struct { const lensConstructorTemplate = ` // Make{{.Name}}Lenses creates a new {{.Name}}Lenses with lenses for all fields func Make{{.Name}}Lenses() {{.Name}}Lenses { + // mandatory lenses {{- range .Fields}} -{{- if .IsOptional}} - iso{{.Name}} := I.FromZero[{{.TypeName}}]() + lens{{.Name}} := L.MakeLens( + func(s {{$.Name}}) {{.TypeName}} { return s.{{.Name}} }, + func(s {{$.Name}}, v {{.TypeName}}) {{$.Name}} { s.{{.Name}} = v; return s }, + ) {{- end}} + // optional lenses +{{- range .Fields}} + lens{{.Name}}O := LO.FromIso[{{$.Name}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}}) {{- end}} return {{.Name}}Lenses{ + // mandatory 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 }, - ), + {{.Name}}: lens{{.Name}}, {{- end}} + // optional lenses +{{- range .Fields}} + {{.Name}}O: lens{{.Name}}O, {{- end}} } } // Make{{.Name}}RefLenses creates a new {{.Name}}RefLenses with lenses for all fields func Make{{.Name}}RefLenses() {{.Name}}RefLenses { - return {{.Name}}RefLenses{ + // mandatory lenses {{- range .Fields}} -{{- if .IsOptional}} {{- if .IsComparable}} - {{.Name}}: LO.FromIso[*{{$.Name}}](I.FromZero[{{.TypeName}}]())(L.MakeLensStrict( - func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} }, - func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s }, - )), + lens{{.Name}} := L.MakeLensStrict( + func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} }, + func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s }, + ) {{- else}} - {{.Name}}: LO.FromIso[*{{$.Name}}](I.FromZero[{{.TypeName}}]())(L.MakeLensRef( - func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} }, - func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s }, - )), -{{- end}} -{{- else}} -{{- if .IsComparable}} - {{.Name}}: L.MakeLensStrict( - func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} }, - func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = 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 }, - ), + lens{{.Name}} := L.MakeLensRef( + func(s *{{$.Name}}) {{.TypeName}} { return s.{{.Name}} }, + func(s *{{$.Name}}, v {{.TypeName}}) *{{$.Name}} { s.{{.Name}} = v; return s }, + ) {{- end}} {{- end}} + // optional lenses +{{- range .Fields}} + lens{{.Name}}O := LO.FromIso[*{{$.Name}}](IO.FromZero[{{.TypeName}}]())(lens{{.Name}}) +{{- end}} + return {{.Name}}RefLenses{ + // mandatory lenses +{{- range .Fields}} + {{.Name}}: lens{{.Name}}, +{{- end}} + // optional lenses +{{- range .Fields}} + {{.Name}}O: lens{{.Name}}O, {{- end}} } } @@ -362,6 +372,7 @@ func isComparableType(expr ast.Expr) bool { } } // For other generic types, conservatively assume not comparable + log.Printf("Not comparable type: %v\n", t) return false case *ast.ChanType: // Channel types are comparable @@ -463,6 +474,7 @@ func parseFile(filename string) ([]structInfo, string, error) { // Check if the type is comparable (for non-optional fields) // For optional fields, we don't need to check since they use LensO isComparable = isComparableType(field.Type) + // log.Printf("field %s, type: %v, isComparable: %b\n", name, field.Type, isComparable) // Extract imports from this field's type fieldImports := make(map[string]string) @@ -590,8 +602,8 @@ func generateLensHelpers(dir, filename string, verbose bool) error { // 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") + // f.WriteString("\tO \"github.com/IBM/fp-go/v2/option\"\n") + f.WriteString("\tIO \"github.com/IBM/fp-go/v2/optics/iso/option\"\n") // Add additional imports collected from field types for importPath, alias := range allImports { diff --git a/v2/cli/lens_test.go b/v2/cli/lens_test.go index 3816a72..41f865e 100644 --- a/v2/cli/lens_test.go +++ b/v2/cli/lens_test.go @@ -514,15 +514,15 @@ func TestLensRefTemplatesWithComparable(t *testing.T) { assert.Contains(t, constructorStr, "func MakeTestStructRefLenses() TestStructRefLenses") // Name field - comparable, should use MakeLensStrict - assert.Contains(t, constructorStr, "Name: L.MakeLensStrict(", + assert.Contains(t, constructorStr, "lensName := L.MakeLensStrict(", "comparable field Name should use MakeLensStrict in RefLenses") // Age field - comparable, should use MakeLensStrict - assert.Contains(t, constructorStr, "Age: L.MakeLensStrict(", + assert.Contains(t, constructorStr, "lensAge := L.MakeLensStrict(", "comparable field Age should use MakeLensStrict in RefLenses") // Data field - not comparable, should use MakeLensRef - assert.Contains(t, constructorStr, "Data: L.MakeLensRef(", + assert.Contains(t, constructorStr, "lensData := L.MakeLensRef(", "non-comparable field Data should use MakeLensRef in RefLenses") } @@ -573,12 +573,12 @@ type TestStruct struct { "non-comparable fields should use MakeLensRef in RefLenses") // Verify the pattern appears for Name field (comparable) - namePattern := "Name: L.MakeLensStrict(" + namePattern := "lensName := L.MakeLensStrict(" assert.Contains(t, contentStr, namePattern, "Name field should use MakeLensStrict") // Verify the pattern appears for Data field (not comparable) - dataPattern := "Data: L.MakeLensRef(" + dataPattern := "lensData := L.MakeLensRef(" assert.Contains(t, contentStr, dataPattern, "Data field should use MakeLensRef") } @@ -619,11 +619,11 @@ type TestStruct struct { // 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, "TestStructLenses") + assert.Contains(t, contentStr, "MakeTestStructLenses") assert.Contains(t, contentStr, "L.Lens[TestStruct, string]") assert.Contains(t, contentStr, "LO.LensO[TestStruct, *int]") - assert.Contains(t, contentStr, "I.FromZero") + assert.Contains(t, contentStr, "IO.FromZero") } func TestGenerateLensHelpersNoAnnotations(t *testing.T) { @@ -670,7 +670,9 @@ func TestLensTemplates(t *testing.T) { 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]") + assert.Contains(t, structStr, "NameO LO.LensO[TestStruct, string]") + assert.Contains(t, structStr, "Value L.Lens[TestStruct, *int]") + assert.Contains(t, structStr, "ValueO LO.LensO[TestStruct, *int]") // Test constructor template var constructorBuf bytes.Buffer @@ -680,9 +682,11 @@ func TestLensTemplates(t *testing.T) { 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") + assert.Contains(t, constructorStr, "Name: lensName,") + assert.Contains(t, constructorStr, "NameO: lensNameO,") + assert.Contains(t, constructorStr, "Value: lensValue,") + assert.Contains(t, constructorStr, "ValueO: lensValueO,") + assert.Contains(t, constructorStr, "IO.FromZero") } func TestLensTemplatesWithOmitEmpty(t *testing.T) { @@ -704,9 +708,13 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) { 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]") + assert.Contains(t, structStr, "NameO LO.LensO[ConfigStruct, string]") + assert.Contains(t, structStr, "Value L.Lens[ConfigStruct, string]") + assert.Contains(t, structStr, "ValueO LO.LensO[ConfigStruct, string]", "non-pointer with omitempty should have optional lens") + assert.Contains(t, structStr, "Count L.Lens[ConfigStruct, int]") + assert.Contains(t, structStr, "CountO LO.LensO[ConfigStruct, int]", "non-pointer with omitempty should have optional lens") + assert.Contains(t, structStr, "Pointer L.Lens[ConfigStruct, *string]") + assert.Contains(t, structStr, "PointerO LO.LensO[ConfigStruct, *string]") // Test constructor template var constructorBuf bytes.Buffer @@ -715,9 +723,9 @@ func TestLensTemplatesWithOmitEmpty(t *testing.T) { 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]()") + assert.Contains(t, constructorStr, "IO.FromZero[string]()") + assert.Contains(t, constructorStr, "IO.FromZero[int]()") + assert.Contains(t, constructorStr, "IO.FromZero[*string]()") } func TestLensCommandFlags(t *testing.T) { @@ -726,7 +734,7 @@ func TestLensCommandFlags(t *testing.T) { 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") + assert.Contains(t, strings.ToLower(cmd.Description), "lenso", "Description should mention LensO for optional lenses") // Check flags assert.Len(t, cmd.Flags, 3) diff --git a/v2/optics/README.md b/v2/optics/README.md index 71b4e96..ba6e343 100644 --- a/v2/optics/README.md +++ b/v2/optics/README.md @@ -1,4 +1,234 @@ # Optics -Refer to [Introduction to optics: lenses and prisms](https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe) for an introduction about functional optics. +Functional optics for composable data access and manipulation in Go. +## Overview + +Optics are first-class, composable references to parts of data structures. They provide a uniform interface for reading, writing, and transforming nested immutable data without verbose boilerplate code. + +## Quick Start + +```go +import ( + "github.com/IBM/fp-go/v2/optics/lens" + F "github.com/IBM/fp-go/v2/function" +) + +type Person struct { + Name string + Age int +} + +// Create a lens for the Name field +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} + +// Get the name +name := nameLens.Get(person) // "Alice" + +// Set a new name (returns a new Person) +updated := nameLens.Set("Bob")(person) +// person.Name is still "Alice", updated.Name is "Bob" +``` + +## Core Optics Types + +### Lens - Product Types (Structs) +Focus on a single field within a struct. Provides get and set operations. + +**Use when:** Working with struct fields that always exist. + +```go +ageLens := lens.MakeLens( + func(p Person) int { return p.Age }, + func(p Person, age int) Person { + p.Age = age + return p + }, +) +``` + +### Prism - Sum Types (Variants) +Focus on one variant of a sum type. Provides optional get and definite set. + +**Use when:** Working with Either, Result, or custom sum types. + +```go +import "github.com/IBM/fp-go/v2/optics/prism" + +successPrism := prism.MakePrism( + func(r Result) option.Option[int] { + if s, ok := r.(Success); ok { + return option.Some(s.Value) + } + return option.None[int]() + }, + func(v int) Result { return Success{Value: v} }, +) +``` + +### Iso - Isomorphisms +Bidirectional transformation between equivalent types with no information loss. + +**Use when:** Converting between equivalent representations (e.g., Celsius ↔ Fahrenheit). + +```go +import "github.com/IBM/fp-go/v2/optics/iso" + +celsiusToFahrenheit := iso.MakeIso( + func(c float64) float64 { return c*9/5 + 32 }, + func(f float64) float64 { return (f - 32) * 5 / 9 }, +) +``` + +### Optional - Maybe Values +Focus on a value that may or may not exist. + +**Use when:** Working with nullable fields or values that may be absent. + +```go +import "github.com/IBM/fp-go/v2/optics/optional" + +timeoutOptional := optional.MakeOptional( + func(c Config) option.Option[*int] { + return option.FromNillable(c.Timeout) + }, + func(c Config, t *int) Config { + c.Timeout = t + return c + }, +) +``` + +### Traversal - Multiple Values +Focus on multiple values simultaneously, allowing batch operations. + +**Use when:** Working with collections or updating multiple fields at once. + +```go +import ( + "github.com/IBM/fp-go/v2/optics/traversal" + TA "github.com/IBM/fp-go/v2/optics/traversal/array" +) + +numbers := []int{1, 2, 3, 4, 5} + +// Double all elements +doubled := F.Pipe2( + numbers, + TA.Traversal[int](), + traversal.Modify[[]int, int](func(n int) int { return n * 2 }), +) +// Result: [2, 4, 6, 8, 10] +``` + +## Composition + +The real power of optics comes from composition: + +```go +type Company struct { + Name string + Address Address +} + +type Address struct { + Street string + City string +} + +// Individual lenses +addressLens := lens.MakeLens( + func(c Company) Address { return c.Address }, + func(c Company, a Address) Company { + c.Address = a + return c + }, +) + +cityLens := lens.MakeLens( + func(a Address) string { return a.City }, + func(a Address, city string) Address { + a.City = city + return a + }, +) + +// Compose to access city directly from company +companyCityLens := F.Pipe1( + addressLens, + lens.Compose[Company](cityLens), +) + +company := Company{ + Name: "Acme Corp", + Address: Address{Street: "Main St", City: "NYC"}, +} + +city := companyCityLens.Get(company) // "NYC" +updated := companyCityLens.Set("Boston")(company) +``` + +## Optics Hierarchy + +``` +Iso[S, A] + ↓ +Lens[S, A] + ↓ +Optional[S, A] + ↓ +Traversal[S, A] + +Prism[S, A] + ↓ +Optional[S, A] + ↓ +Traversal[S, A] +``` + +More specific optics can be converted to more general ones. + +## Package Structure + +- **optics/lens**: Lenses for product types (structs) +- **optics/prism**: Prisms for sum types (Either, Result, etc.) +- **optics/iso**: Isomorphisms for equivalent types +- **optics/optional**: Optional optics for maybe values +- **optics/traversal**: Traversals for multiple values + +Each package includes specialized sub-packages for common patterns: +- **array**: Optics for arrays/slices +- **either**: Optics for Either types +- **option**: Optics for Option types +- **record**: Optics for maps + +## Documentation + +For detailed documentation on each optic type, see: +- [Main Package Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics) +- [Lens Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/lens) +- [Prism Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/prism) +- [Iso Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/iso) +- [Optional Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/optional) +- [Traversal Documentation](https://pkg.go.dev/github.com/IBM/fp-go/v2/optics/traversal) + +## Further Reading + +For an introduction to functional optics concepts: +- [Introduction to optics: lenses and prisms](https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe) by Giulio Canti + +## Examples + +See the [samples/lens](../samples/lens) directory for complete working examples. + +## License + +Apache License 2.0 - See LICENSE file for details. diff --git a/v2/optics/iso/option/doc.go b/v2/optics/iso/option/doc.go new file mode 100644 index 0000000..1542f86 --- /dev/null +++ b/v2/optics/iso/option/doc.go @@ -0,0 +1,305 @@ +// 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. + +# Overview + +This package offers utilities to convert between regular values and Option-wrapped values, +particularly useful for handling zero values and optional data. It provides isomorphisms +that treat certain values (like zero values) as representing absence, mapping them to None, +while other values map to Some. + +# Core Functionality + +The main function in this package is FromZero, which creates an isomorphism between a +comparable type T and Option[T], treating the zero value as None. + +# FromZero Isomorphism + +FromZero creates a bidirectional transformation where: + - Forward (Get): T → Option[T] + - Zero value → None + - Non-zero value → Some(value) + - Reverse (ReverseGet): Option[T] → T + - None → Zero value + - Some(value) → value + +# Basic Usage + +Working with integers: + + import ( + "github.com/IBM/fp-go/v2/optics/iso/option" + O "github.com/IBM/fp-go/v2/option" + ) + + isoInt := option.FromZero[int]() + + // Convert zero to None + opt := isoInt.Get(0) // None[int] + + // Convert non-zero to Some + opt = isoInt.Get(42) // Some(42) + + // Convert None to zero + val := isoInt.ReverseGet(O.None[int]()) // 0 + + // Convert Some to value + val = isoInt.ReverseGet(O.Some(42)) // 42 + +# Use Cases + +## Database Nullable Columns + +Convert between database NULL and Go zero values: + + type User struct { + ID int + Name string + Age *int // NULL in database + Email *string + } + + ageIso := option.FromZero[*int]() + + // Reading from database + var dbAge *int = nil + optAge := ageIso.Get(dbAge) // None[*int] + + // Writing to database + userAge := 25 + dbAge = ageIso.ReverseGet(O.Some(&userAge)) // &25 + +## Configuration with Defaults + +Handle optional configuration values: + + type Config struct { + Port int + Timeout int + MaxConn int + } + + portIso := option.FromZero[int]() + + // Use zero as "not configured" + config := Config{Port: 0, Timeout: 30, MaxConn: 100} + portOpt := portIso.Get(config.Port) // None[int] (use default) + + // Set explicit value + config.Port = portIso.ReverseGet(O.Some(8080)) // 8080 + +## API Response Handling + +Work with APIs that use zero values to indicate absence: + + type APIResponse struct { + UserID int // 0 means not set + Score float64 // 0.0 means not available + Message string // "" means no message + } + + userIDIso := option.FromZero[int]() + scoreIso := option.FromZero[float64]() + messageIso := option.FromZero[string]() + + response := APIResponse{UserID: 0, Score: 0.0, Message: ""} + + userID := userIDIso.Get(response.UserID) // None[int] + score := scoreIso.Get(response.Score) // None[float64] + message := messageIso.Get(response.Message) // None[string] + +## Validation Logic + +Simplify required vs optional field validation: + + type FormData struct { + Name string // Required + Email string // Required + Phone string // Optional (empty = not provided) + Comments string // Optional + } + + phoneIso := option.FromZero[string]() + commentsIso := option.FromZero[string]() + + form := FormData{ + Name: "Alice", + Email: "alice@example.com", + Phone: "", + Comments: "", + } + + // Check optional fields + phone := phoneIso.Get(form.Phone) // None[string] + comments := commentsIso.Get(form.Comments) // None[string] + + // Validate: required fields must be non-empty + if form.Name == "" || form.Email == "" { + // Validation error + } + +# Working with Different Types + +## Strings + + strIso := option.FromZero[string]() + + opt := strIso.Get("") // None[string] + opt = strIso.Get("hello") // Some("hello") + + val := strIso.ReverseGet(O.None[string]()) // "" + val = strIso.ReverseGet(O.Some("world")) // "world" + +## Pointers + + ptrIso := option.FromZero[*int]() + + opt := ptrIso.Get(nil) // None[*int] + num := 42 + opt = ptrIso.Get(&num) // Some(&num) + + val := ptrIso.ReverseGet(O.None[*int]()) // nil + val = ptrIso.ReverseGet(O.Some(&num)) // &num + +## Floating Point Numbers + + floatIso := option.FromZero[float64]() + + opt := floatIso.Get(0.0) // None[float64] + opt = floatIso.Get(3.14) // Some(3.14) + + val := floatIso.ReverseGet(O.None[float64]()) // 0.0 + val = floatIso.ReverseGet(O.Some(2.71)) // 2.71 + +## Booleans + + boolIso := option.FromZero[bool]() + + opt := boolIso.Get(false) // None[bool] + opt = boolIso.Get(true) // Some(true) + + val := boolIso.ReverseGet(O.None[bool]()) // false + val = boolIso.ReverseGet(O.Some(true)) // true + +# Composition with Other Optics + +Combine with lenses for nested structures: + + import ( + L "github.com/IBM/fp-go/v2/optics/lens" + I "github.com/IBM/fp-go/v2/optics/iso" + ) + + type Settings struct { + Volume int // 0 means muted + } + + volumeLens := L.MakeLens( + func(s Settings) int { return s.Volume }, + func(s Settings, v int) Settings { + s.Volume = v + return s + }, + ) + + volumeIso := option.FromZero[int]() + + // Compose lens with iso + volumeOptLens := F.Pipe1( + volumeLens, + L.IMap[Settings](volumeIso.Get, volumeIso.ReverseGet), + ) + + settings := Settings{Volume: 0} + vol := volumeOptLens.Get(settings) // None[int] (muted) + + // Set volume + updated := volumeOptLens.Set(O.Some(75))(settings) + // updated.Volume == 75 + +# Isomorphism Laws + +FromZero satisfies the isomorphism round-trip laws: + +1. **ReverseGet(Get(t)) == t** for all t: T + + isoInt := option.FromZero[int]() + value := 42 + result := isoInt.ReverseGet(isoInt.Get(value)) + // result == 42 + +2. **Get(ReverseGet(opt)) == opt** for all opt: Option[T] + + isoInt := option.FromZero[int]() + opt := O.Some(42) + result := isoInt.Get(isoInt.ReverseGet(opt)) + // result == Some(42) + +These laws ensure that the transformation is truly reversible with no information loss. + +# Performance Considerations + +The FromZero isomorphism is very efficient: + - No allocations for the iso structure itself + - Simple equality comparison for zero check + - Direct value unwrapping for ReverseGet + - No reflection or runtime type assertions + +# Type Safety + +The isomorphism is fully type-safe: + - Compile-time type checking ensures T is comparable + - Generic type parameters prevent type mismatches + - No runtime type assertions needed + - The compiler enforces correct usage + +# Limitations + +The FromZero isomorphism has some limitations to be aware of: + +1. **Zero Value Ambiguity**: Cannot distinguish between "intentionally zero" and "absent" + - For int: 0 always maps to None, even if 0 is a valid value + - For string: "" always maps to None, even if empty string is valid + - Solution: Use a different representation (e.g., pointers) if zero is meaningful + +2. **Comparable Constraint**: Only works with comparable types + - Cannot use with slices, maps, or functions + - Cannot use with structs containing non-comparable fields + - Solution: Use pointers to such types, or custom isomorphisms + +3. **Boolean Limitation**: false always maps to None + - Cannot represent "explicitly false" vs "not set" + - Solution: Use *bool or a custom type if this distinction matters + +# Related Packages + + - github.com/IBM/fp-go/v2/optics/iso: Core isomorphism functionality + - github.com/IBM/fp-go/v2/option: Option type and operations + - github.com/IBM/fp-go/v2/optics/lens: Lenses for focused access + - github.com/IBM/fp-go/v2/optics/lens/option: Lenses for optional values + +# See Also + +For more information on isomorphisms and optics: + - optics/iso package documentation + - optics package overview + - option package documentation +*/ +package option + +// Made with Bob diff --git a/v2/optics/lens/iso/doc.go b/v2/optics/lens/iso/doc.go new file mode 100644 index 0000000..3e90159 --- /dev/null +++ b/v2/optics/lens/iso/doc.go @@ -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 iso provides utilities for composing lenses with isomorphisms. + +# Overview + +This package bridges lenses and isomorphisms, allowing you to transform the focus type +of a lens using an isomorphism. It provides functions to compose lenses with isomorphisms +and to create isomorphisms for common patterns like nullable pointers. + +The key insight is that if you have a Lens[S, A] and an Iso[A, B], you can create a +Lens[S, B] by composing them. This allows you to work with transformed views of your +data without changing the underlying structure. + +# Core Functions + +## FromNillable + +Creates an isomorphism between a nullable pointer and an Option type: + + type Config struct { + Timeout *int + } + + // Create isomorphism: *int ↔ Option[int] + timeoutIso := iso.FromNillable[int]() + + // nil → None, &value → Some(value) + opt := timeoutIso.Get(nil) // None[int] + num := 42 + opt = timeoutIso.Get(&num) // Some(42) + + // None → nil, Some(value) → &value + ptr := timeoutIso.ReverseGet(O.None[int]()) // nil + ptr = timeoutIso.ReverseGet(O.Some(42)) // &42 + +## Compose + +Composes a lens with an isomorphism to transform the focus type: + + type Person struct { + Name string + Age int + } + + type Celsius float64 + type Fahrenheit float64 + + type Weather struct { + Temperature Celsius + } + + // Lens to access temperature + tempLens := L.MakeLens( + func(w Weather) Celsius { return w.Temperature }, + func(w Weather, t Celsius) Weather { + w.Temperature = t + return w + }, + ) + + // Isomorphism: Celsius ↔ Fahrenheit + celsiusToFahrenheit := I.MakeIso( + func(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }, + func(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }, + ) + + // Compose to work with Fahrenheit + tempFahrenheitLens := F.Pipe1( + tempLens, + iso.Compose[Weather, Celsius, Fahrenheit](celsiusToFahrenheit), + ) + + weather := Weather{Temperature: 20} // 20°C + tempF := tempFahrenheitLens.Get(weather) // 68°F + updated := tempFahrenheitLens.Set(86)(weather) // Set to 86°F (30°C) + +# Use Cases + +## Working with Nullable Fields + +Convert between nullable pointers and Option types: + + type DatabaseConfig struct { + Host string + Port int + Username string + Password *string // Nullable + } + + type AppConfig struct { + Database *DatabaseConfig + } + + // Lens to database config + dbLens := L.MakeLens( + func(c AppConfig) *DatabaseConfig { return c.Database }, + func(c AppConfig, db *DatabaseConfig) AppConfig { + c.Database = db + return c + }, + ) + + // Isomorphism for nullable pointer + dbIso := iso.FromNillable[DatabaseConfig]() + + // Compose to work with Option + dbOptLens := F.Pipe1( + dbLens, + iso.Compose[AppConfig, *DatabaseConfig, O.Option[DatabaseConfig]](dbIso), + ) + + config := AppConfig{Database: nil} + dbOpt := dbOptLens.Get(config) // None[DatabaseConfig] + + // Set with Some + newDB := DatabaseConfig{Host: "localhost", Port: 5432} + updated := dbOptLens.Set(O.Some(newDB))(config) + +## Unit Conversions + +Work with different units of measurement: + + type Distance struct { + Meters float64 + } + + type Kilometers float64 + type Miles float64 + + // Lens to meters + metersLens := L.MakeLens( + func(d Distance) float64 { return d.Meters }, + func(d Distance, m float64) Distance { + d.Meters = m + return d + }, + ) + + // Isomorphism: meters ↔ kilometers + metersToKm := I.MakeIso( + func(m float64) Kilometers { return Kilometers(m / 1000) }, + func(km Kilometers) float64 { return float64(km * 1000) }, + ) + + // Compose to work with kilometers + kmLens := F.Pipe1( + metersLens, + iso.Compose[Distance, float64, Kilometers](metersToKm), + ) + + distance := Distance{Meters: 5000} + km := kmLens.Get(distance) // 5 km + updated := kmLens.Set(Kilometers(10))(distance) // 10000 meters + +## Type Wrappers + +Work with newtype wrappers: + + type UserId int + type User struct { + ID UserId + Name string + } + + // Lens to user ID + idLens := L.MakeLens( + func(u User) UserId { return u.ID }, + func(u User, id UserId) User { + u.ID = id + return u + }, + ) + + // Isomorphism: UserId ↔ int + userIdIso := I.MakeIso( + func(id UserId) int { return int(id) }, + func(i int) UserId { return UserId(i) }, + ) + + // Compose to work with raw int + idIntLens := F.Pipe1( + idLens, + iso.Compose[User, UserId, int](userIdIso), + ) + + user := User{ID: 42, Name: "Alice"} + rawId := idIntLens.Get(user) // 42 (int) + updated := idIntLens.Set(100)(user) // UserId(100) + +## Nested Nullable Fields + +Safely navigate through nullable nested structures: + + type Address struct { + Street string + City string + } + + type Person struct { + Name string + Address *Address + } + + type Company struct { + Name string + CEO *Person + } + + // Lens to CEO + ceoLens := L.MakeLens( + func(c Company) *Person { return c.CEO }, + func(c Company, p *Person) Company { + c.CEO = p + return c + }, + ) + + // Isomorphism for nullable person + personIso := iso.FromNillable[Person]() + + // Compose to work with Option[Person] + ceoOptLens := F.Pipe1( + ceoLens, + iso.Compose[Company, *Person, O.Option[Person]](personIso), + ) + + company := Company{Name: "Acme Corp", CEO: nil} + ceo := ceoOptLens.Get(company) // None[Person] + + // Set CEO + newCEO := Person{Name: "Alice", Address: nil} + updated := ceoOptLens.Set(O.Some(newCEO))(company) + +# Composition Patterns + +## Chaining Multiple Isomorphisms + + type Meters float64 + type Kilometers float64 + type Miles float64 + + type Journey struct { + Distance Meters + } + + // Lens to distance + distLens := L.MakeLens( + func(j Journey) Meters { return j.Distance }, + func(j Journey, d Meters) Journey { + j.Distance = d + return j + }, + ) + + // Isomorphisms + metersToKm := I.MakeIso( + func(m Meters) Kilometers { return Kilometers(m / 1000) }, + func(km Kilometers) Meters { return Meters(km * 1000) }, + ) + + kmToMiles := I.MakeIso( + func(km Kilometers) Miles { return Miles(km * 0.621371) }, + func(mi Miles) Kilometers { return Kilometers(mi / 0.621371) }, + ) + + // Compose lens with chained isomorphisms + milesLens := F.Pipe2( + distLens, + iso.Compose[Journey, Meters, Kilometers](metersToKm), + iso.Compose[Journey, Kilometers, Miles](kmToMiles), + ) + + journey := Journey{Distance: 5000} // 5000 meters + miles := milesLens.Get(journey) // ~3.11 miles + +## Combining with Optional Lenses + + type Config struct { + Database *DatabaseConfig + } + + type DatabaseConfig struct { + Port int + } + + // Lens to database (nullable) + dbLens := L.MakeLens( + func(c Config) *DatabaseConfig { return c.Database }, + func(c Config, db *DatabaseConfig) Config { + c.Database = db + return c + }, + ) + + // Convert to Option lens + dbIso := iso.FromNillable[DatabaseConfig]() + dbOptLens := F.Pipe1( + dbLens, + iso.Compose[Config, *DatabaseConfig, O.Option[DatabaseConfig]](dbIso), + ) + + // Now compose with lens to port + portLens := L.MakeLens( + func(db DatabaseConfig) int { return db.Port }, + func(db DatabaseConfig, port int) DatabaseConfig { + db.Port = port + return db + }, + ) + + // Use ComposeOption to handle the Option + defaultDB := DatabaseConfig{Port: 5432} + configPortLens := F.Pipe1( + dbOptLens, + L.ComposeOption[Config, int](defaultDB)(portLens), + ) + +# Performance Considerations + +Composing lenses with isomorphisms is efficient: + - No additional allocations beyond the lens and iso structures + - Composition creates function closures but is still performant + - The isomorphism transformations are applied on-demand + - Consider caching composed lenses for frequently used paths + +# Type Safety + +All operations are fully type-safe: + - Compile-time type checking ensures correct composition + - Generic type parameters prevent type mismatches + - No runtime type assertions needed + - The compiler enforces that isomorphisms are properly reversible + +# Related Packages + + - github.com/IBM/fp-go/v2/optics/lens: Core lens functionality + - github.com/IBM/fp-go/v2/optics/iso: Core isomorphism functionality + - github.com/IBM/fp-go/v2/optics/iso/lens: Convert isomorphisms to lenses + - github.com/IBM/fp-go/v2/option: Option type and operations + - github.com/IBM/fp-go/v2/function: Function composition utilities + +# See Also + +For more information on lenses and isomorphisms: + - optics/lens package documentation + - optics/iso package documentation + - optics package overview +*/ +package iso + +// Made with Bob diff --git a/v2/optics/optional/doc.go b/v2/optics/optional/doc.go new file mode 100644 index 0000000..c7c9332 --- /dev/null +++ b/v2/optics/optional/doc.go @@ -0,0 +1,479 @@ +// 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 optional provides optional optics for focusing on values that may not exist. + +# Overview + +An Optional is an optic that focuses on a subpart of a data structure that may or may not +be present. Unlike lenses which always focus on an existing field, optionals handle cases +where the target value might be absent, returning Option[A] instead of A. + +Optionals are the bridge between lenses (which always succeed) and prisms (which may fail +to match). They combine aspects of both: + - Like lenses: Focus on a specific location in a structure + - Like prisms: The value at that location may not exist + +Optionals are essential for: + - Working with nullable fields (pointers that may be nil) + - Accessing nested optional values + - Conditional updates based on value presence + - Safe navigation through potentially missing data + +# Mathematical Foundation + +An Optional[S, A] consists of two operations: + - GetOption: S → Option[A] (try to extract A from S, may return None) + - Set: A → S → S (update A in S, may be a no-op if value doesn't exist) + +Optionals must satisfy the optional laws: + 1. GetOptionSet: if GetOption(s) == Some(a), then GetOption(Set(a)(s)) == Some(a) + 2. SetGetOption: if GetOption(s) == Some(a), then Set(a)(s) preserves other parts of s + 3. SetSet: Set(a2)(Set(a1)(s)) == Set(a2)(s) + +# Basic Usage + +Creating an optional for a nullable field: + + type Config struct { + Timeout *int + MaxSize *int + } + + timeoutOptional := optional.MakeOptional( + func(c Config) option.Option[*int] { + return option.FromNillable(c.Timeout) + }, + func(c Config, t *int) Config { + c.Timeout = t + return c + }, + ) + + config := Config{Timeout: nil, MaxSize: ptr(100)} + + // Get returns None for nil + timeout := timeoutOptional.GetOption(config) // None[*int] + + // Set updates the value + newTimeout := 30 + updated := timeoutOptional.Set(&newTimeout)(config) + // updated.Timeout points to 30 + +# Working with Pointers + +For pointer-based structures, use MakeOptionalRef which handles copying automatically: + + type Database struct { + Host string + Port int + } + + type Config struct { + Database *Database + } + + dbOptional := optional.MakeOptionalRef( + func(c *Config) option.Option[*Database] { + return option.FromNillable(c.Database) + }, + func(c *Config, db *Database) *Config { + c.Database = db + return c + }, + ) + + config := &Config{Database: nil} + + // Get returns None when database is nil + db := dbOptional.GetOption(config) // None[*Database] + + // Set creates a new config with the database + newDB := &Database{Host: "localhost", Port: 5432} + updated := dbOptional.Set(newDB)(config) + // config.Database is still nil, updated.Database points to newDB + +# Identity Optional + +The identity optional focuses on the entire structure: + + idOpt := optional.Id[Config]() + + config := Config{Timeout: ptr(30)} + value := idOpt.GetOption(config) // Some(config) + updated := idOpt.Set(Config{Timeout: ptr(60)})(config) + +# Composing Optionals + +Optionals can be composed to navigate through nested optional structures: + + type Address struct { + Street string + City string + } + + type Person struct { + Name string + Address *Address + } + + addressOpt := optional.MakeOptional( + func(p Person) option.Option[*Address] { + return option.FromNillable(p.Address) + }, + func(p Person, a *Address) Person { + p.Address = a + return p + }, + ) + + cityOpt := optional.MakeOptionalRef( + func(a *Address) option.Option[string] { + if a == nil { + return option.None[string]() + } + return option.Some(a.City) + }, + func(a *Address, city string) *Address { + a.City = city + return a + }, + ) + + // Compose to access city from person + personCityOpt := F.Pipe1( + addressOpt, + optional.Compose[Person, *Address, string](cityOpt), + ) + + person := Person{Name: "Alice", Address: nil} + + // Get returns None when address is nil + city := personCityOpt.GetOption(person) // None[string] + + // Set updates the city if address exists + withAddress := Person{ + Name: "Alice", + Address: &Address{Street: "Main St", City: "NYC"}, + } + updated := personCityOpt.Set("Boston")(withAddress) + // updated.Address.City == "Boston" + +# From Predicate + +Create an optional that only focuses on values satisfying a predicate: + + type User struct { + Age int + } + + ageOpt := optional.FromPredicate[User, int]( + func(age int) bool { return age >= 18 }, + )( + func(u User) int { return u.Age }, + func(u User, age int) User { + u.Age = age + return u + }, + ) + + adult := User{Age: 25} + age := ageOpt.GetOption(adult) // Some(25) + + minor := User{Age: 15} + minorAge := ageOpt.GetOption(minor) // None[int] + + // Set only works if predicate is satisfied + updated := ageOpt.Set(30)(adult) // Age becomes 30 + unchanged := ageOpt.Set(30)(minor) // Age stays 15 (predicate fails) + +# Modifying Values + +Use ModifyOption to transform values that exist: + + type Counter struct { + Value *int + } + + valueOpt := optional.MakeOptional( + func(c Counter) option.Option[*int] { + return option.FromNillable(c.Value) + }, + func(c Counter, v *int) Counter { + c.Value = v + return c + }, + ) + + counter := Counter{Value: ptr(5)} + + // Increment if value exists + incremented := F.Pipe3( + counter, + valueOpt, + optional.ModifyOption[Counter, *int](func(v *int) *int { + newVal := *v + 1 + return &newVal + }), + option.GetOrElse(F.Constant(counter)), + ) + // incremented.Value points to 6 + + // No change if value is nil + nilCounter := Counter{Value: nil} + result := F.Pipe3( + nilCounter, + valueOpt, + optional.ModifyOption[Counter, *int](func(v *int) *int { + newVal := *v + 1 + return &newVal + }), + option.GetOrElse(F.Constant(nilCounter)), + ) + // result.Value is still nil + +# Bidirectional Mapping + +Transform the focus type of an optional: + + type Celsius float64 + type Fahrenheit float64 + + type Weather struct { + Temperature *Celsius + } + + tempCelsiusOpt := optional.MakeOptional( + func(w Weather) option.Option[*Celsius] { + return option.FromNillable(w.Temperature) + }, + func(w Weather, t *Celsius) Weather { + w.Temperature = t + return w + }, + ) + + // Create optional that works with Fahrenheit + tempFahrenheitOpt := F.Pipe1( + tempCelsiusOpt, + optional.IMap[Weather, *Celsius, *Fahrenheit]( + func(c *Celsius) *Fahrenheit { + f := Fahrenheit(*c*9/5 + 32) + return &f + }, + func(f *Fahrenheit) *Celsius { + c := Celsius((*f - 32) * 5 / 9) + return &c + }, + ), + ) + + celsius := Celsius(20) + weather := Weather{Temperature: &celsius} + + tempF := tempFahrenheitOpt.GetOption(weather) // Some(68°F) + +# Real-World Example: Configuration with Defaults + + type DatabaseConfig struct { + Host string + Port int + Username string + Password string + } + + type AppConfig struct { + Database *DatabaseConfig + Debug bool + } + + dbOpt := optional.MakeOptional( + func(c AppConfig) option.Option[*DatabaseConfig] { + return option.FromNillable(c.Database) + }, + func(c AppConfig, db *DatabaseConfig) AppConfig { + c.Database = db + return c + }, + ) + + dbHostOpt := optional.MakeOptionalRef( + func(db *DatabaseConfig) option.Option[string] { + if db == nil { + return option.None[string]() + } + return option.Some(db.Host) + }, + func(db *DatabaseConfig, host string) *DatabaseConfig { + db.Host = host + return db + }, + ) + + // Compose to access database host + appDbHostOpt := F.Pipe1( + dbOpt, + optional.Compose[AppConfig, *DatabaseConfig, string](dbHostOpt), + ) + + config := AppConfig{Database: nil, Debug: true} + + // Get returns None when database is not configured + host := appDbHostOpt.GetOption(config) // None[string] + + // Set creates database if needed + withDB := AppConfig{ + Database: &DatabaseConfig{Host: "localhost", Port: 5432}, + Debug: true, + } + updated := appDbHostOpt.Set("prod.example.com")(withDB) + // updated.Database.Host == "prod.example.com" + +# Real-World Example: Safe Navigation + + type Company struct { + Name string + CEO *Person + } + + type Person struct { + Name string + Address *Address + } + + type Address struct { + City string + } + + ceoOpt := optional.MakeOptional( + func(c Company) option.Option[*Person] { + return option.FromNillable(c.CEO) + }, + func(c Company, p *Person) Company { + c.CEO = p + return c + }, + ) + + addressOpt := optional.MakeOptionalRef( + func(p *Person) option.Option[*Address] { + return option.FromNillable(p.Address) + }, + func(p *Person, a *Address) *Person { + p.Address = a + return p + }, + ) + + cityOpt := optional.MakeOptionalRef( + func(a *Address) option.Option[string] { + if a == nil { + return option.None[string]() + } + return option.Some(a.City) + }, + func(a *Address, city string) *Address { + a.City = city + return a + }, + ) + + // Compose all optionals for safe navigation + ceoCityOpt := F.Pipe2( + ceoOpt, + optional.Compose[Company, *Person, *Address](addressOpt), + optional.Compose[Company, *Address, string](cityOpt), + ) + + company := Company{Name: "Acme Corp", CEO: nil} + + // Safe navigation returns None at any missing level + city := ceoCityOpt.GetOption(company) // None[string] + +# Optionals in the Optics Hierarchy + +Optionals sit between lenses and traversals in the optics hierarchy: + + Lens[S, A] + ↓ + Optional[S, A] + ↓ + Traversal[S, A] + + Prism[S, A] + ↓ + Optional[S, A] + +This means: + - Every Lens can be converted to an Optional (value always exists) + - Every Prism can be converted to an Optional (variant may not match) + - Every Optional can be converted to a Traversal (0 or 1 values) + +# Performance Considerations + +Optionals are efficient: + - No reflection - all operations are type-safe at compile time + - Minimal allocations - optionals themselves are lightweight + - GetOption short-circuits on None + - Set operations create new copies (immutability) + +For best performance: + - Use MakeOptionalRef for pointer structures to ensure proper copying + - Cache composed optionals rather than recomposing + - Consider batch operations when updating multiple optional values + +# Type Safety + +Optionals are fully type-safe: + - Compile-time type checking + - No runtime type assertions + - Generic type parameters ensure correctness + - Composition maintains type relationships + +# Function Reference + +Core Optional Creation: + - MakeOptional: Create an optional from getter and setter functions + - MakeOptionalRef: Create an optional for pointer-based structures + - Id: Create an identity optional + - IdRef: Create an identity optional for pointers + +Composition: + - Compose: Compose two optionals + - ComposeRef: Compose optionals for pointer structures + +Transformation: + - ModifyOption: Transform a value through an optional (returns Option[S]) + - SetOption: Set a value through an optional (returns Option[S]) + - IMap: Bidirectionally map an optional + - IChain: Bidirectionally map with optional results + - IChainAny: Map to/from any type + +Predicate-Based: + - FromPredicate: Create optional from predicate + - FromPredicateRef: Create optional from predicate (ref version) + +# Related Packages + + - github.com/IBM/fp-go/v2/optics/lens: Lenses for fields that always exist + - github.com/IBM/fp-go/v2/optics/prism: Prisms for sum types + - github.com/IBM/fp-go/v2/optics/traversal: Traversals for multiple values + - github.com/IBM/fp-go/v2/option: Optional values + - github.com/IBM/fp-go/v2/endomorphism: Endomorphisms (A → A functions) +*/ +package optional + +// Made with Bob diff --git a/v2/optics/traversal/doc.go b/v2/optics/traversal/doc.go new file mode 100644 index 0000000..9d19b9b --- /dev/null +++ b/v2/optics/traversal/doc.go @@ -0,0 +1,495 @@ +// 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 traversal provides traversals - optics for focusing on multiple values simultaneously. + +# Overview + +A Traversal is an optic that focuses on zero or more values within a data structure, +allowing you to view, modify, or fold over multiple elements at once. Unlike lenses +which focus on a single field, or prisms which focus on one variant, traversals can +target collections, multiple fields, or any number of values. + +Traversals are the most general optic and sit at the bottom of the optics hierarchy. +They are essential for: + - Working with collections (arrays, slices, maps) + - Batch operations on multiple fields + - Filtering and transforming multiple values + - Aggregating data from multiple sources + - Applying the same operation to all matching elements + +# Mathematical Foundation + +A Traversal[S, A] is defined using higher-kinded types and applicative functors. +In practical terms, it provides operations to: + - Modify: Apply a function to all focused values + - Set: Replace all focused values with a constant + - FoldMap: Map each value to a monoid and combine results + - GetAll: Collect all focused values into a list + +Traversals must satisfy the traversal laws: + 1. Identity: traverse(Identity, id) == Identity + 2. Composition: traverse(Compose(F, G), f) == Compose(traverse(F, traverse(G, f))) + +These laws ensure that traversals compose properly and behave consistently. + +# Basic Usage + +Creating a traversal for array elements: + + import ( + A "github.com/IBM/fp-go/v2/array" + T "github.com/IBM/fp-go/v2/optics/traversal" + TA "github.com/IBM/fp-go/v2/optics/traversal/array" + ) + + numbers := []int{1, 2, 3, 4, 5} + + // Get all elements + all := T.GetAll(numbers)(TA.Traversal[int]()) + // Result: [1, 2, 3, 4, 5] + + // Modify all elements + doubled := F.Pipe2( + numbers, + TA.Traversal[int](), + T.Modify[[]int, int](func(n int) int { return n * 2 }), + ) + // Result: [2, 4, 6, 8, 10] + + // Set all elements to a constant + allTens := F.Pipe2( + numbers, + TA.Traversal[int](), + T.Set[[]int, int](10), + ) + // Result: [10, 10, 10, 10, 10] + +# Identity Traversal + +The identity traversal focuses on the entire structure: + + idTrav := T.Id[int, int]() + + value := 42 + result := T.Modify[int, int](func(n int) int { return n * 2 })(idTrav)(value) + // Result: 84 + +# Folding with Traversals + +Aggregate values using monoids: + + import ( + M "github.com/IBM/fp-go/v2/monoid" + N "github.com/IBM/fp-go/v2/number" + ) + + numbers := []int{1, 2, 3, 4, 5} + + // Sum all elements + sum := F.Pipe2( + numbers, + TA.Traversal[int](), + T.FoldMap[int, []int, int](F.Identity[int]), + )(N.MonoidSum[int]()) + // Result: 15 + + // Product of all elements + product := F.Pipe2( + numbers, + TA.Traversal[int](), + T.FoldMap[int, []int, int](F.Identity[int]), + )(N.MonoidProduct[int]()) + // Result: 120 + +# Composing Traversals + +Traversals can be composed to focus on nested collections: + + type Person struct { + Name string + Friends []string + } + + people := []Person{ + {Name: "Alice", Friends: []string{"Bob", "Charlie"}}, + {Name: "Bob", Friends: []string{"Alice", "David"}}, + } + + // Traversal for people array + peopleTrav := TA.Traversal[Person]() + + // Traversal for friends array within a person + friendsTrav := T.MakeTraversal(func(p Person) []string { + return p.Friends + }) + + // Compose to access all friends of all people + allFriendsTrav := F.Pipe1( + peopleTrav, + T.Compose[[]Person, Person, string, ...](friendsTrav), + ) + + // Get all friends + allFriends := T.GetAll(people)(allFriendsTrav) + // Result: ["Bob", "Charlie", "Alice", "David"] + +# Working with Records (Maps) + +Traverse over map values: + + import TR "github.com/IBM/fp-go/v2/optics/traversal/record" + + scores := map[string]int{ + "Alice": 85, + "Bob": 92, + "Charlie": 78, + } + + // Get all scores + allScores := F.Pipe2( + scores, + TR.Traversal[string, int](), + T.GetAll[map[string]int, int], + ) + // Result: [85, 92, 78] (order may vary) + + // Increase all scores by 5 + boosted := F.Pipe2( + scores, + TR.Traversal[string, int](), + T.Modify[map[string]int, int](func(score int) int { + return score + 5 + }), + ) + // Result: {"Alice": 90, "Bob": 97, "Charlie": 83} + +# Working with Either Types + +Traverse over the Right values: + + import ( + E "github.com/IBM/fp-go/v2/either" + TE "github.com/IBM/fp-go/v2/optics/traversal/either" + ) + + results := []E.Either[string, int]{ + E.Right[string](10), + E.Left[int]("error"), + E.Right[string](20), + } + + // Traversal for array of Either + arrayTrav := TA.Traversal[E.Either[string, int]]() + + // Traversal for Right values + rightTrav := TE.Traversal[string, int]() + + // Compose to access all Right values + allRightsTrav := F.Pipe1( + arrayTrav, + T.Compose[[]E.Either[string, int], E.Either[string, int], int, ...](rightTrav), + ) + + // Get all Right values + rights := T.GetAll(results)(allRightsTrav) + // Result: [10, 20] + + // Double all Right values + doubled := F.Pipe2( + results, + allRightsTrav, + T.Modify[[]E.Either[string, int], int](func(n int) int { return n * 2 }), + ) + // Result: [Right(20), Left("error"), Right(40)] + +# Working with Option Types + +Traverse over Some values: + + import ( + O "github.com/IBM/fp-go/v2/option" + TO "github.com/IBM/fp-go/v2/optics/traversal/option" + ) + + values := []O.Option[int]{ + O.Some(1), + O.None[int](), + O.Some(2), + O.None[int](), + O.Some(3), + } + + // Compose array and option traversals + allSomesTrav := F.Pipe1( + TA.Traversal[O.Option[int]](), + T.Compose[[]O.Option[int], O.Option[int], int, ...](TO.Traversal[int]()), + ) + + // Get all Some values + somes := T.GetAll(values)(allSomesTrav) + // Result: [1, 2, 3] + + // Increment all Some values + incremented := F.Pipe2( + values, + allSomesTrav, + T.Modify[[]O.Option[int], int](func(n int) int { return n + 1 }), + ) + // Result: [Some(2), None, Some(3), None, Some(4)] + +# Real-World Example: Nested Data Structures + + type Department struct { + Name string + Employees []Employee + } + + type Employee struct { + Name string + Salary int + } + + company := []Department{ + { + Name: "Engineering", + Employees: []Employee{ + {Name: "Alice", Salary: 100000}, + {Name: "Bob", Salary: 95000}, + }, + }, + { + Name: "Sales", + Employees: []Employee{ + {Name: "Charlie", Salary: 80000}, + {Name: "David", Salary: 85000}, + }, + }, + } + + // Traversal for departments + deptTrav := TA.Traversal[Department]() + + // Traversal for employees within a department + empTrav := T.MakeTraversal(func(d Department) []Employee { + return d.Employees + }) + + // Traversal for employee array + empArrayTrav := TA.Traversal[Employee]() + + // Compose to access all employees + allEmpTrav := F.Pipe2( + deptTrav, + T.Compose[[]Department, Department, []Employee, ...](empTrav), + T.Compose[[]Department, []Employee, Employee, ...](empArrayTrav), + ) + + // Get all employee names + names := F.Pipe2( + company, + allEmpTrav, + T.FoldMap[[]string, []Department, Employee](func(e Employee) []string { + return []string{e.Name} + }), + )(A.Monoid[string]()) + // Result: ["Alice", "Bob", "Charlie", "David"] + + // Give everyone a 10% raise + withRaises := F.Pipe2( + company, + allEmpTrav, + T.Modify[[]Department, Employee](func(e Employee) Employee { + e.Salary = int(float64(e.Salary) * 1.1) + return e + }), + ) + +# Real-World Example: Filtering with Traversals + + type Product struct { + Name string + Price float64 + InStock bool + } + + products := []Product{ + {Name: "Laptop", Price: 999.99, InStock: true}, + {Name: "Mouse", Price: 29.99, InStock: false}, + {Name: "Keyboard", Price: 79.99, InStock: true}, + } + + // Create a traversal that only focuses on in-stock products + inStockTrav := T.MakeTraversal(func(ps []Product) []Product { + return A.Filter(func(p Product) bool { + return p.InStock + })(ps) + }) + + // Apply discount to in-stock items + discounted := F.Pipe2( + products, + inStockTrav, + T.Modify[[]Product, Product](func(p Product) Product { + p.Price = p.Price * 0.9 + return p + }), + ) + // Only Laptop and Keyboard prices are reduced + +# Real-World Example: Data Aggregation + + type Order struct { + ID string + Items []OrderItem + Status string + } + + type OrderItem struct { + Product string + Quantity int + Price float64 + } + + orders := []Order{ + { + ID: "001", + Items: []OrderItem{ + {Product: "Widget", Quantity: 2, Price: 10.0}, + {Product: "Gadget", Quantity: 1, Price: 25.0}, + }, + Status: "completed", + }, + { + ID: "002", + Items: []OrderItem{ + {Product: "Widget", Quantity: 5, Price: 10.0}, + }, + Status: "completed", + }, + } + + // Traversal for orders + orderTrav := TA.Traversal[Order]() + + // Traversal for items within an order + itemsTrav := T.MakeTraversal(func(o Order) []OrderItem { + return o.Items + }) + + // Traversal for item array + itemArrayTrav := TA.Traversal[OrderItem]() + + // Compose to access all items + allItemsTrav := F.Pipe2( + orderTrav, + T.Compose[[]Order, Order, []OrderItem, ...](itemsTrav), + T.Compose[[]Order, []OrderItem, OrderItem, ...](itemArrayTrav), + ) + + // Calculate total revenue + totalRevenue := F.Pipe2( + orders, + allItemsTrav, + T.FoldMap[float64, []Order, OrderItem](func(item OrderItem) float64 { + return float64(item.Quantity) * item.Price + }), + )(N.MonoidSum[float64]()) + // Result: 95.0 (2*10 + 1*25 + 5*10) + +# Traversals in the Optics Hierarchy + +Traversals are the most general optic: + + Iso[S, A] + ↓ + Lens[S, A] + ↓ + Optional[S, A] + ↓ + Traversal[S, A] + + Prism[S, A] + ↓ + Optional[S, A] + ↓ + Traversal[S, A] + +This means: + - Every Iso, Lens, Prism, and Optional can be converted to a Traversal + - Traversals are the most flexible but least specific optic + - Use more specific optics when possible for better type safety + +# Performance Considerations + +Traversals can be efficient but consider: + - Each traversal operation may iterate over all elements + - Composition creates nested iterations + - FoldMap is often more efficient than GetAll followed by reduction + - Modify creates new copies (immutability) + +For best performance: + - Use specialized traversals (array, record, etc.) when available + - Avoid unnecessary composition + - Consider batch operations + - Cache composed traversals + +# Type Safety + +Traversals are fully type-safe: + - Compile-time type checking + - Generic type parameters ensure correctness + - Composition maintains type relationships + - No runtime type assertions + +# Function Reference + +Core Functions: + - Id: Create an identity traversal + - Modify: Apply a function to all focused values + - Set: Replace all focused values with a constant + - Compose: Compose two traversals + +Aggregation: + - FoldMap: Map each value to a monoid and combine + - Fold: Fold over all values using a monoid + - GetAll: Collect all focused values into a list + +# Specialized Traversals + +The package includes specialized sub-packages for common patterns: + - array: Traversals for arrays and slices + - record: Traversals for maps + - either: Traversals for Either types + - option: Traversals for Option types + +Each specialized package provides optimized implementations for its data type. + +# Related Packages + + - github.com/IBM/fp-go/v2/optics/lens: Lenses for single fields + - github.com/IBM/fp-go/v2/optics/prism: Prisms for sum types + - github.com/IBM/fp-go/v2/optics/optional: Optionals for maybe values + - github.com/IBM/fp-go/v2/optics/traversal/array: Array traversals + - github.com/IBM/fp-go/v2/optics/traversal/record: Record/map traversals + - github.com/IBM/fp-go/v2/optics/traversal/either: Either traversals + - github.com/IBM/fp-go/v2/optics/traversal/option: Option traversals + - github.com/IBM/fp-go/v2/array: Array utilities + - github.com/IBM/fp-go/v2/monoid: Monoid type class +*/ +package traversal + +// Made with Bob diff --git a/v2/samples/lens/example.go b/v2/samples/lens/example.go index 5776bae..9be50c1 100644 --- a/v2/samples/lens/example.go +++ b/v2/samples/lens/example.go @@ -47,6 +47,12 @@ type Company struct { Website *string } +// fp-go:Lens +type CompanyExtended struct { + Company + Extended string +} + // fp-go:Lens type CheckOption struct { Name option.Option[string] diff --git a/v2/samples/lens/example_test.go b/v2/samples/lens/example_test.go index 81e731a..8a8e417 100644 --- a/v2/samples/lens/example_test.go +++ b/v2/samples/lens/example_test.go @@ -214,7 +214,7 @@ func TestPersonRefLensesOptionalIdempotent(t *testing.T) { refLenses := MakePersonRefLenses() // Test that setting Phone to the same value returns the same pointer - samePhone := refLenses.Phone.Set(O.Some(&phoneValue))(person) + samePhone := refLenses.PhoneO.Set(O.Some(&phoneValue))(person) assert.Same(t, person, samePhone, "Setting Phone to same value should return identical pointer") // Test with Phone field set to nil @@ -226,24 +226,24 @@ func TestPersonRefLensesOptionalIdempotent(t *testing.T) { } // Setting Phone to None when it's already nil should return same pointer - sameNilPhone := refLenses.Phone.Set(O.None[*string]())(personNoPhone) + sameNilPhone := refLenses.PhoneO.Set(O.None[*string]())(personNoPhone) assert.Same(t, personNoPhone, sameNilPhone, "Setting Phone to None when already nil should return identical pointer") // Test that setting to a different value creates a new pointer newPhoneValue := "555-5678" - differentPhone := refLenses.Phone.Set(O.Some(&newPhoneValue))(person) + differentPhone := refLenses.PhoneO.Set(O.Some(&newPhoneValue))(person) assert.NotSame(t, person, differentPhone, "Setting Phone to different value should return new pointer") assert.Equal(t, &newPhoneValue, differentPhone.Phone) assert.Equal(t, &phoneValue, person.Phone, "Original should be unchanged") // Test setting from nil to Some creates new pointer - somePhone := refLenses.Phone.Set(O.Some(&phoneValue))(personNoPhone) + somePhone := refLenses.PhoneO.Set(O.Some(&phoneValue))(personNoPhone) assert.NotSame(t, personNoPhone, somePhone, "Setting Phone from nil to Some should return new pointer") assert.Equal(t, &phoneValue, somePhone.Phone) assert.Nil(t, personNoPhone.Phone, "Original should be unchanged") // Test setting from Some to None creates new pointer - nonePhone := refLenses.Phone.Set(O.None[*string]())(person) + nonePhone := refLenses.PhoneO.Set(O.None[*string]())(person) assert.NotSame(t, person, nonePhone, "Setting Phone from Some to None should return new pointer") assert.Nil(t, nonePhone.Phone) assert.Equal(t, &phoneValue, person.Phone, "Original should be unchanged") @@ -264,7 +264,7 @@ func TestAddressRefLensesOptionalIdempotent(t *testing.T) { refLenses := MakeAddressRefLenses() // Test that setting State to the same value returns the same pointer - sameState := refLenses.State.Set(O.Some(&stateValue))(address) + sameState := refLenses.StateO.Set(O.Some(&stateValue))(address) assert.Same(t, address, sameState, "Setting State to same value should return identical pointer") // Test with State field set to nil @@ -277,12 +277,12 @@ func TestAddressRefLensesOptionalIdempotent(t *testing.T) { } // Setting State to None when it's already nil should return same pointer - sameNilState := refLenses.State.Set(O.None[*string]())(addressNoState) + sameNilState := refLenses.StateO.Set(O.None[*string]())(addressNoState) assert.Same(t, addressNoState, sameNilState, "Setting State to None when already nil should return identical pointer") // Test that setting to a different value creates a new pointer newStateValue := "New York" - differentState := refLenses.State.Set(O.Some(&newStateValue))(address) + differentState := refLenses.StateO.Set(O.Some(&newStateValue))(address) assert.NotSame(t, address, differentState, "Setting State to different value should return new pointer") assert.Equal(t, &newStateValue, differentState.State) assert.Equal(t, &stateValue, address.State, "Original should be unchanged") @@ -311,7 +311,7 @@ func TestCompanyRefLensesOptionalIdempotent(t *testing.T) { refLenses := MakeCompanyRefLenses() // Test that setting Website to the same value returns the same pointer - sameWebsite := refLenses.Website.Set(O.Some(&websiteValue))(company) + sameWebsite := refLenses.WebsiteO.Set(O.Some(&websiteValue))(company) assert.Same(t, company, sameWebsite, "Setting Website to same value should return identical pointer") // Test with Website field set to nil @@ -331,12 +331,12 @@ func TestCompanyRefLensesOptionalIdempotent(t *testing.T) { } // Setting Website to None when it's already nil should return same pointer - sameNilWebsite := refLenses.Website.Set(O.None[*string]())(companyNoWebsite) + sameNilWebsite := refLenses.WebsiteO.Set(O.None[*string]())(companyNoWebsite) assert.Same(t, companyNoWebsite, sameNilWebsite, "Setting Website to None when already nil should return identical pointer") // Test that setting to a different value creates a new pointer newWebsiteValue := "https://newsite.com" - differentWebsite := refLenses.Website.Set(O.Some(&newWebsiteValue))(company) + differentWebsite := refLenses.WebsiteO.Set(O.Some(&newWebsiteValue))(company) assert.NotSame(t, company, differentWebsite, "Setting Website to different value should return new pointer") assert.Equal(t, &newWebsiteValue, differentWebsite.Website) assert.Equal(t, &websiteValue, company.Website, "Original should be unchanged") diff --git a/v2/samples/lens/gen_lens.go b/v2/samples/lens/gen_lens.go index b557082..6b10dee 100644 --- a/v2/samples/lens/gen_lens.go +++ b/v2/samples/lens/gen_lens.go @@ -2,246 +2,459 @@ package lens // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2025-11-12 18:15:07.69943 +0100 CET m=+0.005345401 +// 2025-11-13 09:05:02.7405925 +0100 CET m=+0.003158301 import ( + IO "github.com/IBM/fp-go/v2/optics/iso/option" L "github.com/IBM/fp-go/v2/optics/lens" LO "github.com/IBM/fp-go/v2/optics/lens/option" - O "github.com/IBM/fp-go/v2/option" - I "github.com/IBM/fp-go/v2/optics/iso/option" option "github.com/IBM/fp-go/v2/optics/lens/option" ) // PersonLenses provides lenses for accessing fields of Person type PersonLenses struct { - Name L.Lens[Person, string] - Age L.Lens[Person, int] + // mandatory fields + Name L.Lens[Person, string] + Age L.Lens[Person, int] Email L.Lens[Person, string] - Phone LO.LensO[Person, *string] + Phone L.Lens[Person, *string] + // optional fields + NameO LO.LensO[Person, string] + AgeO LO.LensO[Person, int] + EmailO LO.LensO[Person, string] + PhoneO LO.LensO[Person, *string] } // PersonRefLenses provides lenses for accessing fields of Person via a reference to Person type PersonRefLenses struct { - Name L.Lens[*Person, string] - Age L.Lens[*Person, int] + // mandatory fields + Name L.Lens[*Person, string] + Age L.Lens[*Person, int] Email L.Lens[*Person, string] - Phone LO.LensO[*Person, *string] + Phone L.Lens[*Person, *string] + // optional fields + NameO LO.LensO[*Person, string] + AgeO LO.LensO[*Person, int] + EmailO LO.LensO[*Person, string] + PhoneO LO.LensO[*Person, *string] } // MakePersonLenses creates a new PersonLenses with lenses for all fields func MakePersonLenses() PersonLenses { - isoPhone := I.FromZero[*string]() + // mandatory lenses + lensName := L.MakeLens( + func(s Person) string { return s.Name }, + func(s Person, v string) Person { s.Name = v; return s }, + ) + lensAge := L.MakeLens( + func(s Person) int { return s.Age }, + func(s Person, v int) Person { s.Age = v; return s }, + ) + lensEmail := L.MakeLens( + func(s Person) string { return s.Email }, + func(s Person, v string) Person { s.Email = v; return s }, + ) + lensPhone := L.MakeLens( + func(s Person) *string { return s.Phone }, + func(s Person, v *string) Person { s.Phone = v; return s }, + ) + // optional lenses + lensNameO := LO.FromIso[Person](IO.FromZero[string]())(lensName) + lensAgeO := LO.FromIso[Person](IO.FromZero[int]())(lensAge) + lensEmailO := LO.FromIso[Person](IO.FromZero[string]())(lensEmail) + lensPhoneO := LO.FromIso[Person](IO.FromZero[*string]())(lensPhone) return PersonLenses{ - Name: L.MakeLens( - func(s Person) string { return s.Name }, - func(s Person, v string) Person { s.Name = v; return s }, - ), - Age: L.MakeLens( - func(s Person) int { return s.Age }, - func(s Person, v int) Person { s.Age = v; return s }, - ), - Email: L.MakeLens( - func(s Person) string { return s.Email }, - func(s Person, v string) Person { s.Email = v; return s }, - ), - Phone: L.MakeLens( - func(s Person) O.Option[*string] { return isoPhone.Get(s.Phone) }, - func(s Person, v O.Option[*string]) Person { s.Phone = isoPhone.ReverseGet(v); return s }, - ), + // mandatory lenses + Name: lensName, + Age: lensAge, + Email: lensEmail, + Phone: lensPhone, + // optional lenses + NameO: lensNameO, + AgeO: lensAgeO, + EmailO: lensEmailO, + PhoneO: lensPhoneO, } } // MakePersonRefLenses creates a new PersonRefLenses with lenses for all fields func MakePersonRefLenses() PersonRefLenses { + // mandatory lenses + lensName := L.MakeLensStrict( + func(s *Person) string { return s.Name }, + func(s *Person, v string) *Person { s.Name = v; return s }, + ) + lensAge := L.MakeLensStrict( + func(s *Person) int { return s.Age }, + func(s *Person, v int) *Person { s.Age = v; return s }, + ) + lensEmail := L.MakeLensStrict( + func(s *Person) string { return s.Email }, + func(s *Person, v string) *Person { s.Email = v; return s }, + ) + lensPhone := L.MakeLensStrict( + func(s *Person) *string { return s.Phone }, + func(s *Person, v *string) *Person { s.Phone = v; return s }, + ) + // optional lenses + lensNameO := LO.FromIso[*Person](IO.FromZero[string]())(lensName) + lensAgeO := LO.FromIso[*Person](IO.FromZero[int]())(lensAge) + lensEmailO := LO.FromIso[*Person](IO.FromZero[string]())(lensEmail) + lensPhoneO := LO.FromIso[*Person](IO.FromZero[*string]())(lensPhone) return PersonRefLenses{ - Name: L.MakeLensStrict( - func(s *Person) string { return s.Name }, - func(s *Person, v string) *Person { s.Name = v; return s }, - ), - Age: L.MakeLensStrict( - func(s *Person) int { return s.Age }, - func(s *Person, v int) *Person { s.Age = v; return s }, - ), - Email: L.MakeLensStrict( - func(s *Person) string { return s.Email }, - func(s *Person, v string) *Person { s.Email = v; return s }, - ), - Phone: LO.FromIso[*Person](I.FromZero[*string]())(L.MakeLensStrict( - func(s *Person) *string { return s.Phone }, - func(s *Person, v *string) *Person { s.Phone = v; return s }, - )), + // mandatory lenses + Name: lensName, + Age: lensAge, + Email: lensEmail, + Phone: lensPhone, + // optional lenses + NameO: lensNameO, + AgeO: lensAgeO, + EmailO: lensEmailO, + PhoneO: lensPhoneO, } } // AddressLenses provides lenses for accessing fields of Address type AddressLenses struct { - Street L.Lens[Address, string] - City L.Lens[Address, string] + // mandatory fields + Street L.Lens[Address, string] + City L.Lens[Address, string] ZipCode L.Lens[Address, string] Country L.Lens[Address, string] - State LO.LensO[Address, *string] + State L.Lens[Address, *string] + // optional fields + StreetO LO.LensO[Address, string] + CityO LO.LensO[Address, string] + ZipCodeO LO.LensO[Address, string] + CountryO LO.LensO[Address, string] + StateO LO.LensO[Address, *string] } // AddressRefLenses provides lenses for accessing fields of Address via a reference to Address type AddressRefLenses struct { - Street L.Lens[*Address, string] - City L.Lens[*Address, string] + // mandatory fields + Street L.Lens[*Address, string] + City L.Lens[*Address, string] ZipCode L.Lens[*Address, string] Country L.Lens[*Address, string] - State LO.LensO[*Address, *string] + State L.Lens[*Address, *string] + // optional fields + StreetO LO.LensO[*Address, string] + CityO LO.LensO[*Address, string] + ZipCodeO LO.LensO[*Address, string] + CountryO LO.LensO[*Address, string] + StateO LO.LensO[*Address, *string] } // MakeAddressLenses creates a new AddressLenses with lenses for all fields func MakeAddressLenses() AddressLenses { - isoState := I.FromZero[*string]() + // mandatory lenses + lensStreet := L.MakeLens( + func(s Address) string { return s.Street }, + func(s Address, v string) Address { s.Street = v; return s }, + ) + lensCity := L.MakeLens( + func(s Address) string { return s.City }, + func(s Address, v string) Address { s.City = v; return s }, + ) + lensZipCode := L.MakeLens( + func(s Address) string { return s.ZipCode }, + func(s Address, v string) Address { s.ZipCode = v; return s }, + ) + lensCountry := L.MakeLens( + func(s Address) string { return s.Country }, + func(s Address, v string) Address { s.Country = v; return s }, + ) + lensState := L.MakeLens( + func(s Address) *string { return s.State }, + func(s Address, v *string) Address { s.State = v; return s }, + ) + // optional lenses + lensStreetO := LO.FromIso[Address](IO.FromZero[string]())(lensStreet) + lensCityO := LO.FromIso[Address](IO.FromZero[string]())(lensCity) + lensZipCodeO := LO.FromIso[Address](IO.FromZero[string]())(lensZipCode) + lensCountryO := LO.FromIso[Address](IO.FromZero[string]())(lensCountry) + lensStateO := LO.FromIso[Address](IO.FromZero[*string]())(lensState) return AddressLenses{ - Street: L.MakeLens( - func(s Address) string { return s.Street }, - func(s Address, v string) Address { s.Street = v; return s }, - ), - City: L.MakeLens( - func(s Address) string { return s.City }, - func(s Address, v string) Address { s.City = v; return s }, - ), - ZipCode: L.MakeLens( - func(s Address) string { return s.ZipCode }, - func(s Address, v string) Address { s.ZipCode = v; return s }, - ), - Country: L.MakeLens( - func(s Address) string { return s.Country }, - func(s Address, v string) Address { s.Country = v; return s }, - ), - State: L.MakeLens( - func(s Address) O.Option[*string] { return isoState.Get(s.State) }, - func(s Address, v O.Option[*string]) Address { s.State = isoState.ReverseGet(v); return s }, - ), + // mandatory lenses + Street: lensStreet, + City: lensCity, + ZipCode: lensZipCode, + Country: lensCountry, + State: lensState, + // optional lenses + StreetO: lensStreetO, + CityO: lensCityO, + ZipCodeO: lensZipCodeO, + CountryO: lensCountryO, + StateO: lensStateO, } } // MakeAddressRefLenses creates a new AddressRefLenses with lenses for all fields func MakeAddressRefLenses() AddressRefLenses { + // mandatory lenses + lensStreet := L.MakeLensStrict( + func(s *Address) string { return s.Street }, + func(s *Address, v string) *Address { s.Street = v; return s }, + ) + lensCity := L.MakeLensStrict( + func(s *Address) string { return s.City }, + func(s *Address, v string) *Address { s.City = v; return s }, + ) + lensZipCode := L.MakeLensStrict( + func(s *Address) string { return s.ZipCode }, + func(s *Address, v string) *Address { s.ZipCode = v; return s }, + ) + lensCountry := L.MakeLensStrict( + func(s *Address) string { return s.Country }, + func(s *Address, v string) *Address { s.Country = v; return s }, + ) + lensState := L.MakeLensStrict( + func(s *Address) *string { return s.State }, + func(s *Address, v *string) *Address { s.State = v; return s }, + ) + // optional lenses + lensStreetO := LO.FromIso[*Address](IO.FromZero[string]())(lensStreet) + lensCityO := LO.FromIso[*Address](IO.FromZero[string]())(lensCity) + lensZipCodeO := LO.FromIso[*Address](IO.FromZero[string]())(lensZipCode) + lensCountryO := LO.FromIso[*Address](IO.FromZero[string]())(lensCountry) + lensStateO := LO.FromIso[*Address](IO.FromZero[*string]())(lensState) return AddressRefLenses{ - Street: L.MakeLensStrict( - func(s *Address) string { return s.Street }, - func(s *Address, v string) *Address { s.Street = v; return s }, - ), - City: L.MakeLensStrict( - func(s *Address) string { return s.City }, - func(s *Address, v string) *Address { s.City = v; return s }, - ), - ZipCode: L.MakeLensStrict( - func(s *Address) string { return s.ZipCode }, - func(s *Address, v string) *Address { s.ZipCode = v; return s }, - ), - Country: L.MakeLensStrict( - func(s *Address) string { return s.Country }, - func(s *Address, v string) *Address { s.Country = v; return s }, - ), - State: LO.FromIso[*Address](I.FromZero[*string]())(L.MakeLensStrict( - func(s *Address) *string { return s.State }, - func(s *Address, v *string) *Address { s.State = v; return s }, - )), + // mandatory lenses + Street: lensStreet, + City: lensCity, + ZipCode: lensZipCode, + Country: lensCountry, + State: lensState, + // optional lenses + StreetO: lensStreetO, + CityO: lensCityO, + ZipCodeO: lensZipCodeO, + CountryO: lensCountryO, + StateO: lensStateO, } } // CompanyLenses provides lenses for accessing fields of Company type CompanyLenses struct { - Name L.Lens[Company, string] + // mandatory fields + Name L.Lens[Company, string] Address L.Lens[Company, Address] - CEO L.Lens[Company, Person] - Website LO.LensO[Company, *string] + CEO L.Lens[Company, Person] + Website L.Lens[Company, *string] + // optional fields + NameO LO.LensO[Company, string] + AddressO LO.LensO[Company, Address] + CEOO LO.LensO[Company, Person] + WebsiteO LO.LensO[Company, *string] } // CompanyRefLenses provides lenses for accessing fields of Company via a reference to Company type CompanyRefLenses struct { - Name L.Lens[*Company, string] + // mandatory fields + Name L.Lens[*Company, string] Address L.Lens[*Company, Address] - CEO L.Lens[*Company, Person] - Website LO.LensO[*Company, *string] + CEO L.Lens[*Company, Person] + Website L.Lens[*Company, *string] + // optional fields + NameO LO.LensO[*Company, string] + AddressO LO.LensO[*Company, Address] + CEOO LO.LensO[*Company, Person] + WebsiteO LO.LensO[*Company, *string] } // MakeCompanyLenses creates a new CompanyLenses with lenses for all fields func MakeCompanyLenses() CompanyLenses { - isoWebsite := I.FromZero[*string]() + // mandatory lenses + lensName := L.MakeLens( + func(s Company) string { return s.Name }, + func(s Company, v string) Company { s.Name = v; return s }, + ) + lensAddress := L.MakeLens( + func(s Company) Address { return s.Address }, + func(s Company, v Address) Company { s.Address = v; return s }, + ) + lensCEO := L.MakeLens( + func(s Company) Person { return s.CEO }, + func(s Company, v Person) Company { s.CEO = v; return s }, + ) + lensWebsite := L.MakeLens( + func(s Company) *string { return s.Website }, + func(s Company, v *string) Company { s.Website = v; return s }, + ) + // optional lenses + lensNameO := LO.FromIso[Company](IO.FromZero[string]())(lensName) + lensAddressO := LO.FromIso[Company](IO.FromZero[Address]())(lensAddress) + lensCEOO := LO.FromIso[Company](IO.FromZero[Person]())(lensCEO) + lensWebsiteO := LO.FromIso[Company](IO.FromZero[*string]())(lensWebsite) return CompanyLenses{ - Name: L.MakeLens( - func(s Company) string { return s.Name }, - func(s Company, v string) Company { s.Name = v; return s }, - ), - Address: L.MakeLens( - func(s Company) Address { return s.Address }, - func(s Company, v Address) Company { s.Address = v; return s }, - ), - CEO: L.MakeLens( - func(s Company) Person { return s.CEO }, - func(s Company, v Person) Company { s.CEO = v; return s }, - ), - Website: L.MakeLens( - func(s Company) O.Option[*string] { return isoWebsite.Get(s.Website) }, - func(s Company, v O.Option[*string]) Company { s.Website = isoWebsite.ReverseGet(v); return s }, - ), + // mandatory lenses + Name: lensName, + Address: lensAddress, + CEO: lensCEO, + Website: lensWebsite, + // optional lenses + NameO: lensNameO, + AddressO: lensAddressO, + CEOO: lensCEOO, + WebsiteO: lensWebsiteO, } } // MakeCompanyRefLenses creates a new CompanyRefLenses with lenses for all fields func MakeCompanyRefLenses() CompanyRefLenses { + // mandatory lenses + lensName := L.MakeLensStrict( + func(s *Company) string { return s.Name }, + func(s *Company, v string) *Company { s.Name = v; return s }, + ) + lensAddress := L.MakeLensStrict( + func(s *Company) Address { return s.Address }, + func(s *Company, v Address) *Company { s.Address = v; return s }, + ) + lensCEO := L.MakeLensStrict( + func(s *Company) Person { return s.CEO }, + func(s *Company, v Person) *Company { s.CEO = v; return s }, + ) + lensWebsite := L.MakeLensStrict( + func(s *Company) *string { return s.Website }, + func(s *Company, v *string) *Company { s.Website = v; return s }, + ) + // optional lenses + lensNameO := LO.FromIso[*Company](IO.FromZero[string]())(lensName) + lensAddressO := LO.FromIso[*Company](IO.FromZero[Address]())(lensAddress) + lensCEOO := LO.FromIso[*Company](IO.FromZero[Person]())(lensCEO) + lensWebsiteO := LO.FromIso[*Company](IO.FromZero[*string]())(lensWebsite) return CompanyRefLenses{ - Name: L.MakeLensStrict( - func(s *Company) string { return s.Name }, - func(s *Company, v string) *Company { s.Name = v; return s }, - ), - Address: L.MakeLensStrict( - func(s *Company) Address { return s.Address }, - func(s *Company, v Address) *Company { s.Address = v; return s }, - ), - CEO: L.MakeLensStrict( - func(s *Company) Person { return s.CEO }, - func(s *Company, v Person) *Company { s.CEO = v; return s }, - ), - Website: LO.FromIso[*Company](I.FromZero[*string]())(L.MakeLensStrict( - func(s *Company) *string { return s.Website }, - func(s *Company, v *string) *Company { s.Website = v; return s }, - )), + // mandatory lenses + Name: lensName, + Address: lensAddress, + CEO: lensCEO, + Website: lensWebsite, + // optional lenses + NameO: lensNameO, + AddressO: lensAddressO, + CEOO: lensCEOO, + WebsiteO: lensWebsiteO, + } +} + +// CompanyExtendedLenses provides lenses for accessing fields of CompanyExtended +type CompanyExtendedLenses struct { + // mandatory fields + Extended L.Lens[CompanyExtended, string] + // optional fields + ExtendedO LO.LensO[CompanyExtended, string] +} + +// CompanyExtendedRefLenses provides lenses for accessing fields of CompanyExtended via a reference to CompanyExtended +type CompanyExtendedRefLenses struct { + // mandatory fields + Extended L.Lens[*CompanyExtended, string] + // optional fields + ExtendedO LO.LensO[*CompanyExtended, string] +} + +// MakeCompanyExtendedLenses creates a new CompanyExtendedLenses with lenses for all fields +func MakeCompanyExtendedLenses() CompanyExtendedLenses { + // mandatory lenses + lensExtended := L.MakeLens( + func(s CompanyExtended) string { return s.Extended }, + func(s CompanyExtended, v string) CompanyExtended { s.Extended = v; return s }, + ) + // optional lenses + lensExtendedO := LO.FromIso[CompanyExtended](IO.FromZero[string]())(lensExtended) + return CompanyExtendedLenses{ + // mandatory lenses + Extended: lensExtended, + // optional lenses + ExtendedO: lensExtendedO, + } +} + +// MakeCompanyExtendedRefLenses creates a new CompanyExtendedRefLenses with lenses for all fields +func MakeCompanyExtendedRefLenses() CompanyExtendedRefLenses { + // mandatory lenses + lensExtended := L.MakeLensStrict( + func(s *CompanyExtended) string { return s.Extended }, + func(s *CompanyExtended, v string) *CompanyExtended { s.Extended = v; return s }, + ) + // optional lenses + lensExtendedO := LO.FromIso[*CompanyExtended](IO.FromZero[string]())(lensExtended) + return CompanyExtendedRefLenses{ + // mandatory lenses + Extended: lensExtended, + // optional lenses + ExtendedO: lensExtendedO, } } // CheckOptionLenses provides lenses for accessing fields of CheckOption type CheckOptionLenses struct { - Name L.Lens[CheckOption, option.Option[string]] - Value LO.LensO[CheckOption, string] + // mandatory fields + Name L.Lens[CheckOption, option.Option[string]] + Value L.Lens[CheckOption, string] + // optional fields + NameO LO.LensO[CheckOption, option.Option[string]] + ValueO LO.LensO[CheckOption, string] } // CheckOptionRefLenses provides lenses for accessing fields of CheckOption via a reference to CheckOption type CheckOptionRefLenses struct { - Name L.Lens[*CheckOption, option.Option[string]] - Value LO.LensO[*CheckOption, string] + // mandatory fields + Name L.Lens[*CheckOption, option.Option[string]] + Value L.Lens[*CheckOption, string] + // optional fields + NameO LO.LensO[*CheckOption, option.Option[string]] + ValueO LO.LensO[*CheckOption, string] } // MakeCheckOptionLenses creates a new CheckOptionLenses with lenses for all fields func MakeCheckOptionLenses() CheckOptionLenses { - isoValue := I.FromZero[string]() + // mandatory lenses + lensName := L.MakeLens( + func(s CheckOption) option.Option[string] { return s.Name }, + func(s CheckOption, v option.Option[string]) CheckOption { s.Name = v; return s }, + ) + lensValue := L.MakeLens( + func(s CheckOption) string { return s.Value }, + func(s CheckOption, v string) CheckOption { s.Value = v; return s }, + ) + // optional lenses + lensNameO := LO.FromIso[CheckOption](IO.FromZero[option.Option[string]]())(lensName) + lensValueO := LO.FromIso[CheckOption](IO.FromZero[string]())(lensValue) return CheckOptionLenses{ - Name: L.MakeLens( - func(s CheckOption) option.Option[string] { return s.Name }, - func(s CheckOption, v option.Option[string]) CheckOption { s.Name = v; return s }, - ), - Value: L.MakeLens( - func(s CheckOption) O.Option[string] { return isoValue.Get(s.Value) }, - func(s CheckOption, v O.Option[string]) CheckOption { s.Value = isoValue.ReverseGet(v); return s }, - ), + // mandatory lenses + Name: lensName, + Value: lensValue, + // optional lenses + NameO: lensNameO, + ValueO: lensValueO, } } // MakeCheckOptionRefLenses creates a new CheckOptionRefLenses with lenses for all fields func MakeCheckOptionRefLenses() CheckOptionRefLenses { + // mandatory lenses + lensName := L.MakeLensRef( + func(s *CheckOption) option.Option[string] { return s.Name }, + func(s *CheckOption, v option.Option[string]) *CheckOption { s.Name = v; return s }, + ) + lensValue := L.MakeLensStrict( + func(s *CheckOption) string { return s.Value }, + func(s *CheckOption, v string) *CheckOption { s.Value = v; return s }, + ) + // optional lenses + lensNameO := LO.FromIso[*CheckOption](IO.FromZero[option.Option[string]]())(lensName) + lensValueO := LO.FromIso[*CheckOption](IO.FromZero[string]())(lensValue) return CheckOptionRefLenses{ - Name: L.MakeLensRef( - func(s *CheckOption) option.Option[string] { return s.Name }, - func(s *CheckOption, v option.Option[string]) *CheckOption { s.Name = v; return s }, - ), - Value: LO.FromIso[*CheckOption](I.FromZero[string]())(L.MakeLensStrict( - func(s *CheckOption) string { return s.Value }, - func(s *CheckOption, v string) *CheckOption { s.Value = v; return s }, - )), + // mandatory lenses + Name: lensName, + Value: lensValue, + // optional lenses + NameO: lensNameO, + ValueO: lensValueO, } }