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:
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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],
|
||||
|
||||
14
v2/optics/lens/iso/types.go
Normal file
14
v2/optics/lens/iso/types.go
Normal 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]
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
481
v2/optics/lens/option/from_test.go
Normal file
481
v2/optics/lens/option/from_test.go
Normal 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
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user