1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-23 22:14:53 +02:00

fix: improve lens handling

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
Dr. Carsten Leue
2025-11-12 18:23:57 +01:00
parent 6f7ec0768d
commit d2dbce6e8b
11 changed files with 747 additions and 42 deletions

View File

@@ -116,18 +116,20 @@ func Make{{.Name}}Lenses() {{.Name}}Lenses {
// 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 },
),
{{- 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 },
)),
{{- 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(
@@ -460,9 +462,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
if !isOptional {
isComparable = isComparableType(field.Type)
}
isComparable = isComparableType(field.Type)
// Extract imports from this field's type
fieldImports := make(map[string]string)

View File

@@ -472,7 +472,6 @@ type TypeTest struct {
assert.Equal(t, "Pointer", typeTest.Fields[2].Name)
assert.Equal(t, "*string", typeTest.Fields[2].TypeName)
assert.True(t, typeTest.Fields[2].IsOptional)
assert.False(t, typeTest.Fields[2].IsComparable, "IsComparable not set for optional fields")
// Slice - not comparable
assert.Equal(t, "Slice", typeTest.Fields[3].Name)
@@ -526,9 +525,6 @@ func TestLensRefTemplatesWithComparable(t *testing.T) {
assert.Contains(t, constructorStr, "Data: L.MakeLensRef(",
"non-comparable field Data should use MakeLensRef in RefLenses")
// Pointer field - optional, should use MakeLensRef
assert.Contains(t, constructorStr, "Pointer: L.MakeLensRef(",
"optional field Pointer should use MakeLensRef in RefLenses")
}
func TestGenerateLensHelpersWithComparable(t *testing.T) {

View File

@@ -24,18 +24,18 @@ import (
)
// FromNillable converts a nillable value to an option and back
func FromNillable[T any]() I.Iso[*T, O.Option[T]] {
func FromNillable[T any]() Iso[*T, Option[T]] {
return I.MakeIso(F.Flow2(
O.FromPredicate(F.IsNonNil[T]),
O.Map(F.Deref[T]),
),
O.Fold(F.Constant((*T)(nil)), F.Ref[T]),
O.Fold(F.ConstNil[T], F.Ref[T]),
)
}
// Compose converts a Lens to a property of `A` into a lens to a property of type `B`
// the transformation is done via an ISO
func Compose[S, A, B any](ab I.Iso[A, B]) func(sa L.Lens[S, A]) L.Lens[S, B] {
func Compose[S, A, B any](ab Iso[A, B]) Operator[S, A, B] {
return F.Pipe2(
ab,
IL.IsoAsLens[A, B],

View File

@@ -0,0 +1,14 @@
package iso
import (
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
)
type (
Option[A any] = option.Option[A]
Iso[S, A any] = iso.Iso[S, A]
Lens[S, A any] = lens.Lens[S, A]
Operator[S, A, B any] = lens.Operator[S, A, B]
)

View File

@@ -435,7 +435,7 @@ func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GE
// 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] {
func Compose[S, A, B any](ab Lens[A, B]) Operator[S, A, B] {
return compose(MakeLens[func(S) B, func(S, B) S], ab)
}
@@ -477,7 +477,7 @@ func Compose[S, A, B any](ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
// )
//
// 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] {
func ComposeRef[S, A, B any](ab Lens[A, B]) Operator[*S, A, B] {
return compose(MakeLensRef[func(*S) B, func(*S, B) *S], ab)
}

View File

@@ -3,6 +3,7 @@ package option
import (
F "github.com/IBM/fp-go/v2/function"
"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"
)
@@ -95,3 +96,69 @@ func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, Option[A]]) Lens[*S, A] {
return fromOption(lens.MakeLensRefCurried[S, A], defaultValue)
}
// FromIso converts a Lens[S, A] to a LensO[S, A] using an isomorphism.
//
// This function takes an isomorphism between A and Option[A] and uses it to
// transform a regular lens into an optional lens. It's particularly useful when
// you have a custom isomorphism that defines how to convert between a value
// and its optional representation.
//
// The isomorphism must satisfy the round-trip laws:
// 1. iso.ReverseGet(iso.Get(a)) == a for all a: A
// 2. iso.Get(iso.ReverseGet(opt)) == opt for all opt: Option[A]
//
// Type Parameters:
// - S: The structure type containing the field
// - A: The type of the field being focused on
//
// Parameters:
// - iso: An isomorphism between A and Option[A] that defines the conversion
//
// Returns:
// - A function that takes a Lens[S, A] and returns a LensO[S, A]
//
// Example:
//
// type Config struct {
// timeout int
// }
//
// // Create a lens to the timeout field
// timeoutLens := lens.MakeLens(
// func(c Config) int { return c.timeout },
// func(c Config, t int) Config { c.timeout = t; return c },
// )
//
// // Create an isomorphism that treats 0 as None
// zeroAsNone := iso.MakeIso(
// func(t int) option.Option[int] {
// if t == 0 {
// return option.None[int]()
// }
// return option.Some(t)
// },
// func(opt option.Option[int]) int {
// return option.GetOrElse(func() int { return 0 })(opt)
// },
// )
//
// // Convert to optional lens
// optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
//
// config := Config{timeout: 0}
// opt := optTimeoutLens.Get(config) // None[int]()
// updated := optTimeoutLens.Set(option.Some(30))(config) // Config{timeout: 30}
//
// Common Use Cases:
// - Converting between sentinel values (like 0, -1, "") and Option
// - Applying custom validation logic when converting to/from Option
// - Integrating with existing isomorphisms like FromNillable
//
// See also:
// - FromPredicate: For predicate-based optional conversion
// - FromNillable: For pointer-based optional conversion
// - FromOption: For converting from optional to non-optional with defaults
func FromIso[S, A any](iso Iso[A, Option[A]]) func(Lens[S, A]) LensO[S, A] {
return LI.Compose[S](iso)
}

View File

@@ -0,0 +1,481 @@
// 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"
ISO "github.com/IBM/fp-go/v2/optics/iso"
L "github.com/IBM/fp-go/v2/optics/lens"
LT "github.com/IBM/fp-go/v2/optics/lens/testing"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// Test types
type Config struct {
timeout int
retries int
}
type Settings struct {
maxConnections int
bufferSize int
}
// TestFromIsoBasic tests basic functionality of FromIso
func TestFromIsoBasic(t *testing.T) {
// Create an isomorphism that treats 0 as None
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
// Create a lens to the timeout field
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
// Convert to optional lens using FromIso
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
t.Run("GetNone", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
result := optTimeoutLens.Get(config)
assert.True(t, O.IsNone(result))
})
t.Run("GetSome", func(t *testing.T) {
config := Config{timeout: 30, retries: 3}
result := optTimeoutLens.Get(config)
assert.True(t, O.IsSome(result))
assert.Equal(t, 30, O.GetOrElse(F.Constant(0))(result))
})
t.Run("SetNone", func(t *testing.T) {
config := Config{timeout: 30, retries: 3}
updated := optTimeoutLens.Set(O.None[int]())(config)
assert.Equal(t, 0, updated.timeout)
assert.Equal(t, 3, updated.retries) // Other fields unchanged
})
t.Run("SetSome", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
updated := optTimeoutLens.Set(O.Some(60))(config)
assert.Equal(t, 60, updated.timeout)
assert.Equal(t, 3, updated.retries) // Other fields unchanged
})
t.Run("SetPreservesOriginal", func(t *testing.T) {
original := Config{timeout: 30, retries: 3}
_ = optTimeoutLens.Set(O.Some(60))(original)
// Original should be unchanged
assert.Equal(t, 30, original.timeout)
assert.Equal(t, 3, original.retries)
})
}
// TestFromIsoWithNegativeSentinel tests using -1 as a sentinel value
func TestFromIsoWithNegativeSentinel(t *testing.T) {
// Create an isomorphism that treats -1 as None
negativeOneAsNone := ISO.MakeIso(
func(n int) O.Option[int] {
if n == -1 {
return O.None[int]()
}
return O.Some(n)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(-1))(opt)
},
)
retriesLens := L.MakeLens(
func(c Config) int { return c.retries },
func(c Config, r int) Config { c.retries = r; return c },
)
optRetriesLens := FromIso[Config, int](negativeOneAsNone)(retriesLens)
t.Run("GetNoneForNegativeOne", func(t *testing.T) {
config := Config{timeout: 30, retries: -1}
result := optRetriesLens.Get(config)
assert.True(t, O.IsNone(result))
})
t.Run("GetSomeForZero", func(t *testing.T) {
config := Config{timeout: 30, retries: 0}
result := optRetriesLens.Get(config)
assert.True(t, O.IsSome(result))
assert.Equal(t, 0, O.GetOrElse(F.Constant(-1))(result))
})
t.Run("SetNoneToNegativeOne", func(t *testing.T) {
config := Config{timeout: 30, retries: 5}
updated := optRetriesLens.Set(O.None[int]())(config)
assert.Equal(t, -1, updated.retries)
})
}
// TestFromIsoLaws verifies that FromIso satisfies lens laws
func TestFromIsoLaws(t *testing.T) {
// Create an isomorphism
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
eqOptInt := O.Eq(EQT.Eq[int]())
eqConfig := EQT.Eq[Config]()
config := Config{timeout: 30, retries: 3}
newValue := O.Some(60)
// Law 1: GetSet - lens.Set(lens.Get(s))(s) == s
t.Run("GetSetLaw", func(t *testing.T) {
result := optTimeoutLens.Set(optTimeoutLens.Get(config))(config)
assert.True(t, eqConfig.Equals(config, result))
})
// Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a
t.Run("SetGetLaw", func(t *testing.T) {
result := optTimeoutLens.Get(optTimeoutLens.Set(newValue)(config))
assert.True(t, eqOptInt.Equals(newValue, result))
})
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
t.Run("SetSetLaw", func(t *testing.T) {
a1 := O.Some(60)
a2 := O.None[int]()
result1 := optTimeoutLens.Set(a2)(optTimeoutLens.Set(a1)(config))
result2 := optTimeoutLens.Set(a2)(config)
assert.True(t, eqConfig.Equals(result1, result2))
})
// Use the testing helper to verify all laws
t.Run("AllLaws", func(t *testing.T) {
laws := LT.AssertLaws(t, eqOptInt, eqConfig)(optTimeoutLens)
assert.True(t, laws(config, O.Some(100)))
assert.True(t, laws(Config{timeout: 0, retries: 5}, O.None[int]()))
})
}
// TestFromIsoComposition tests composing FromIso with other lenses
func TestFromIsoComposition(t *testing.T) {
type Application struct {
config Config
}
// Isomorphism for zero as none
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
// Lens to config field
configLens := L.MakeLens(
func(a Application) Config { return a.config },
func(a Application, c Config) Application { a.config = c; return a },
)
// Lens to timeout field
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
// Compose: Application -> Config -> timeout (as Option)
optTimeoutFromConfig := FromIso[Config, int](zeroAsNone)(timeoutLens)
optTimeoutFromApp := F.Pipe1(
configLens,
L.Compose[Application](optTimeoutFromConfig),
)
app := Application{config: Config{timeout: 0, retries: 3}}
t.Run("ComposedGet", func(t *testing.T) {
result := optTimeoutFromApp.Get(app)
assert.True(t, O.IsNone(result))
})
t.Run("ComposedSet", func(t *testing.T) {
updated := optTimeoutFromApp.Set(O.Some(45))(app)
assert.Equal(t, 45, updated.config.timeout)
assert.Equal(t, 3, updated.config.retries)
})
}
// TestFromIsoModify tests using Modify with FromIso-based lenses
func TestFromIsoModify(t *testing.T) {
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
t.Run("ModifyNoneToSome", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
// Map None to Some(10)
modified := L.Modify[Config](O.Map(func(x int) int { return x + 10 }))(optTimeoutLens)(config)
// Since it was None, Map doesn't apply, stays None (0)
assert.Equal(t, 0, modified.timeout)
})
t.Run("ModifySomeValue", func(t *testing.T) {
config := Config{timeout: 30, retries: 3}
// Double the timeout value
modified := L.Modify[Config](O.Map(func(x int) int { return x * 2 }))(optTimeoutLens)(config)
assert.Equal(t, 60, modified.timeout)
})
t.Run("ModifyWithAlt", func(t *testing.T) {
config := Config{timeout: 0, retries: 3}
// Use Alt to provide a default
modified := L.Modify[Config](func(opt O.Option[int]) O.Option[int] {
return O.Alt(F.Constant(O.Some(10)))(opt)
})(optTimeoutLens)(config)
assert.Equal(t, 10, modified.timeout)
})
}
// TestFromIsoWithStringEmpty tests using empty string as None
func TestFromIsoWithStringEmpty(t *testing.T) {
type User struct {
name string
email string
}
// Isomorphism that treats empty string as None
emptyAsNone := ISO.MakeIso(
func(s string) O.Option[string] {
if s == "" {
return O.None[string]()
}
return O.Some(s)
},
func(opt O.Option[string]) string {
return O.GetOrElse(F.Constant(""))(opt)
},
)
emailLens := L.MakeLens(
func(u User) string { return u.email },
func(u User, e string) User { u.email = e; return u },
)
optEmailLens := FromIso[User, string](emptyAsNone)(emailLens)
t.Run("EmptyStringAsNone", func(t *testing.T) {
user := User{name: "Alice", email: ""}
result := optEmailLens.Get(user)
assert.True(t, O.IsNone(result))
})
t.Run("NonEmptyStringAsSome", func(t *testing.T) {
user := User{name: "Alice", email: "alice@example.com"}
result := optEmailLens.Get(user)
assert.True(t, O.IsSome(result))
assert.Equal(t, "alice@example.com", O.GetOrElse(F.Constant(""))(result))
})
t.Run("SetNoneToEmpty", func(t *testing.T) {
user := User{name: "Alice", email: "alice@example.com"}
updated := optEmailLens.Set(O.None[string]())(user)
assert.Equal(t, "", updated.email)
})
}
// TestFromIsoRoundTrip tests round-trip conversions
func TestFromIsoRoundTrip(t *testing.T) {
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
maxConnectionsLens := L.MakeLens(
func(s Settings) int { return s.maxConnections },
func(s Settings, m int) Settings { s.maxConnections = m; return s },
)
optMaxConnectionsLens := FromIso[Settings, int](zeroAsNone)(maxConnectionsLens)
t.Run("RoundTripThroughGet", func(t *testing.T) {
settings := Settings{maxConnections: 100, bufferSize: 1024}
// Get the value, then Set it back
opt := optMaxConnectionsLens.Get(settings)
restored := optMaxConnectionsLens.Set(opt)(settings)
assert.Equal(t, settings, restored)
})
t.Run("RoundTripThroughSet", func(t *testing.T) {
settings := Settings{maxConnections: 0, bufferSize: 1024}
// Set a new value, then Get it
newOpt := O.Some(200)
updated := optMaxConnectionsLens.Set(newOpt)(settings)
retrieved := optMaxConnectionsLens.Get(updated)
assert.True(t, O.Eq(EQT.Eq[int]()).Equals(newOpt, retrieved))
})
t.Run("RoundTripWithNone", func(t *testing.T) {
settings := Settings{maxConnections: 100, bufferSize: 1024}
// Set None, then get it back
updated := optMaxConnectionsLens.Set(O.None[int]())(settings)
retrieved := optMaxConnectionsLens.Get(updated)
assert.True(t, O.IsNone(retrieved))
})
}
// TestFromIsoChaining tests chaining multiple FromIso transformations
func TestFromIsoChaining(t *testing.T) {
// Create two different isomorphisms
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
config := Config{timeout: 30, retries: 3}
t.Run("ChainedOperations", func(t *testing.T) {
// Chain multiple operations
result := F.Pipe2(
config,
optTimeoutLens.Set(O.Some(60)),
optTimeoutLens.Set(O.None[int]()),
)
assert.Equal(t, 0, result.timeout)
})
}
// TestFromIsoMultipleFields tests using FromIso on multiple fields
func TestFromIsoMultipleFields(t *testing.T) {
zeroAsNone := ISO.MakeIso(
func(t int) O.Option[int] {
if t == 0 {
return O.None[int]()
}
return O.Some(t)
},
func(opt O.Option[int]) int {
return O.GetOrElse(F.Constant(0))(opt)
},
)
timeoutLens := L.MakeLens(
func(c Config) int { return c.timeout },
func(c Config, t int) Config { c.timeout = t; return c },
)
retriesLens := L.MakeLens(
func(c Config) int { return c.retries },
func(c Config, r int) Config { c.retries = r; return c },
)
optTimeoutLens := FromIso[Config, int](zeroAsNone)(timeoutLens)
optRetriesLens := FromIso[Config, int](zeroAsNone)(retriesLens)
t.Run("IndependentFields", func(t *testing.T) {
config := Config{timeout: 0, retries: 5}
// Get both fields
timeoutOpt := optTimeoutLens.Get(config)
retriesOpt := optRetriesLens.Get(config)
assert.True(t, O.IsNone(timeoutOpt))
assert.True(t, O.IsSome(retriesOpt))
assert.Equal(t, 5, O.GetOrElse(F.Constant(0))(retriesOpt))
})
t.Run("SetBothFields", func(t *testing.T) {
config := Config{timeout: 0, retries: 0}
// Set both fields
updated := F.Pipe2(
config,
optTimeoutLens.Set(O.Some(30)),
optRetriesLens.Set(O.Some(3)),
)
assert.Equal(t, 30, updated.timeout)
assert.Equal(t, 3, updated.retries)
})
}
// Made with Bob

View File

@@ -17,6 +17,7 @@ package option
import (
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/optics/iso"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/option"
)
@@ -91,4 +92,6 @@ type (
// optLens := lens.FromNillableRef(timeoutLens)
// // optLens is a LensO[*Config, *int]
LensO[S, A any] = Lens[S, Option[A]]
Iso[S, A any] = iso.Iso[S, A]
)

View File

@@ -80,4 +80,7 @@ type (
// with the focused value updated to a. The original structure is never modified.
Set func(a A) Endomorphism[S]
}
Kleisli[S, A, B any] = func(A) Lens[S, B]
Operator[S, A, B any] = Kleisli[S, Lens[S, A], B]
)

View File

@@ -20,6 +20,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/optics/lens"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
@@ -196,3 +197,147 @@ func TestPersonRefLensesIdempotent(t *testing.T) {
assert.Equal(t, "bob@example.com", differentEmail.Email)
assert.Equal(t, "alice@example.com", person.Email, "Original should be unchanged")
}
func TestPersonRefLensesOptionalIdempotent(t *testing.T) {
// Test that setting an optional field to the same value returns the identical pointer
// This is important for performance and correctness in functional programming
// Test with Phone field set to a value
phoneValue := "555-1234"
person := &Person{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
Phone: &phoneValue,
}
refLenses := MakePersonRefLenses()
// Test that setting Phone to the same value returns the same pointer
samePhone := refLenses.Phone.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
personNoPhone := &Person{
Name: "Bob",
Age: 25,
Email: "bob@example.com",
Phone: nil,
}
// Setting Phone to None when it's already nil should return same pointer
sameNilPhone := refLenses.Phone.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)
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)
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)
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")
}
func TestAddressRefLensesOptionalIdempotent(t *testing.T) {
// Test Address.State optional field idempotency
stateValue := "California"
address := &Address{
Street: "123 Main St",
City: "Los Angeles",
ZipCode: "90001",
Country: "USA",
State: &stateValue,
}
refLenses := MakeAddressRefLenses()
// Test that setting State to the same value returns the same pointer
sameState := refLenses.State.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
addressNoState := &Address{
Street: "456 Oak Ave",
City: "Boston",
ZipCode: "02101",
Country: "USA",
State: nil,
}
// Setting State to None when it's already nil should return same pointer
sameNilState := refLenses.State.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)
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")
}
func TestCompanyRefLensesOptionalIdempotent(t *testing.T) {
// Test Company.Website optional field idempotency
websiteValue := "https://example.com"
company := &Company{
Name: "Tech Inc",
Address: Address{
Street: "789 Tech Blvd",
City: "San Francisco",
ZipCode: "94102",
Country: "USA",
},
CEO: Person{
Name: "Jane Doe",
Age: 45,
Email: "jane@techinc.com",
},
Website: &websiteValue,
}
refLenses := MakeCompanyRefLenses()
// Test that setting Website to the same value returns the same pointer
sameWebsite := refLenses.Website.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
companyNoWebsite := &Company{
Name: "Startup LLC",
Address: Address{
Street: "101 Innovation Way",
City: "Austin",
ZipCode: "78701",
Country: "USA",
},
CEO: Person{
Name: "John Smith",
Age: 35,
Email: "john@startup.com",
},
}
// Setting Website to None when it's already nil should return same pointer
sameNilWebsite := refLenses.Website.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)
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")
}

View File

@@ -2,7 +2,7 @@ package lens
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at
// 2025-11-12 17:16:40.1431921 +0100 CET m=+0.003694701
// 2025-11-12 18:15:07.69943 +0100 CET m=+0.005345401
import (
L "github.com/IBM/fp-go/v2/optics/lens"
@@ -53,7 +53,6 @@ func MakePersonLenses() PersonLenses {
// MakePersonRefLenses creates a new PersonRefLenses with lenses for all fields
func MakePersonRefLenses() PersonRefLenses {
isoPhone := I.FromZero[*string]()
return PersonRefLenses{
Name: L.MakeLensStrict(
func(s *Person) string { return s.Name },
@@ -67,10 +66,10 @@ func MakePersonRefLenses() PersonRefLenses {
func(s *Person) string { return s.Email },
func(s *Person, v string) *Person { s.Email = v; return s },
),
Phone: L.MakeLensRef(
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 },
),
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 },
)),
}
}
@@ -121,7 +120,6 @@ func MakeAddressLenses() AddressLenses {
// MakeAddressRefLenses creates a new AddressRefLenses with lenses for all fields
func MakeAddressRefLenses() AddressRefLenses {
isoState := I.FromZero[*string]()
return AddressRefLenses{
Street: L.MakeLensStrict(
func(s *Address) string { return s.Street },
@@ -139,10 +137,10 @@ func MakeAddressRefLenses() AddressRefLenses {
func(s *Address) string { return s.Country },
func(s *Address, v string) *Address { s.Country = v; return s },
),
State: L.MakeLensRef(
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 },
),
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 },
)),
}
}
@@ -187,7 +185,6 @@ func MakeCompanyLenses() CompanyLenses {
// MakeCompanyRefLenses creates a new CompanyRefLenses with lenses for all fields
func MakeCompanyRefLenses() CompanyRefLenses {
isoWebsite := I.FromZero[*string]()
return CompanyRefLenses{
Name: L.MakeLensStrict(
func(s *Company) string { return s.Name },
@@ -201,10 +198,10 @@ func MakeCompanyRefLenses() CompanyRefLenses {
func(s *Company) Person { return s.CEO },
func(s *Company, v Person) *Company { s.CEO = v; return s },
),
Website: L.MakeLensRef(
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 },
),
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 },
)),
}
}
@@ -237,15 +234,14 @@ func MakeCheckOptionLenses() CheckOptionLenses {
// MakeCheckOptionRefLenses creates a new CheckOptionRefLenses with lenses for all fields
func MakeCheckOptionRefLenses() CheckOptionRefLenses {
isoValue := I.FromZero[string]()
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: L.MakeLensRef(
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 },
),
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 },
)),
}
}