mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
fix: improve lens implementation
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
@@ -304,3 +304,85 @@ func ChainFirst[A any](f Endomorphism[A]) Operator[A] {
|
||||
func Chain[A any](f Endomorphism[A]) Operator[A] {
|
||||
return function.Bind2nd(MonadChain, f)
|
||||
}
|
||||
|
||||
// Flatten collapses a nested endomorphism into a single endomorphism.
|
||||
//
|
||||
// Given an endomorphism that transforms endomorphisms (Endomorphism[Endomorphism[A]]),
|
||||
// Flatten produces a simple endomorphism by applying the outer transformation to the
|
||||
// identity function. This is the monadic join operation for the Endomorphism monad.
|
||||
//
|
||||
// The function applies the nested endomorphism to Identity[A] to extract the inner
|
||||
// endomorphism, effectively "flattening" the two layers into one.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type being transformed by the endomorphisms
|
||||
//
|
||||
// Parameters:
|
||||
// - mma: A nested endomorphism that transforms endomorphisms
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that applies the transformation directly to values of type A
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Counter struct {
|
||||
// Value int
|
||||
// }
|
||||
//
|
||||
// // An endomorphism that wraps another endomorphism
|
||||
// addThenDouble := func(endo Endomorphism[Counter]) Endomorphism[Counter] {
|
||||
// return func(c Counter) Counter {
|
||||
// c = endo(c) // Apply the input endomorphism
|
||||
// c.Value = c.Value * 2 // Then double
|
||||
// return c
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// flattened := Flatten(addThenDouble)
|
||||
// result := flattened(Counter{Value: 5}) // Counter{Value: 10}
|
||||
func Flatten[A any](mma Endomorphism[Endomorphism[A]]) Endomorphism[A] {
|
||||
return mma(function.Identity[A])
|
||||
}
|
||||
|
||||
// Join performs self-application of a function that produces endomorphisms.
|
||||
//
|
||||
// Given a function that takes a value and returns an endomorphism of that same type,
|
||||
// Join creates an endomorphism that applies the value to itself through the function.
|
||||
// This operation is also known as the W combinator (warbler) in combinatory logic,
|
||||
// or diagonal application.
|
||||
//
|
||||
// The resulting endomorphism evaluates f(a)(a), applying the same value a to both
|
||||
// the function f and the resulting endomorphism.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type being transformed
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A function that takes a value and returns an endomorphism of that type
|
||||
//
|
||||
// Returns:
|
||||
// - An endomorphism that performs self-application: f(a)(a)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Point struct {
|
||||
// X, Y int
|
||||
// }
|
||||
//
|
||||
// // Create an endomorphism based on the input point
|
||||
// scaleBy := func(p Point) Endomorphism[Point] {
|
||||
// return func(p2 Point) Point {
|
||||
// return Point{
|
||||
// X: p2.X * p.X,
|
||||
// Y: p2.Y * p.Y,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// selfScale := Join(scaleBy)
|
||||
// result := selfScale(Point{X: 3, Y: 4}) // Point{X: 9, Y: 16}
|
||||
func Join[A any](f Kleisli[A]) Endomorphism[A] {
|
||||
return func(a A) A {
|
||||
return f(a)(a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,5 +229,3 @@ via ReverseGet, so other fields may be reset to default values. For partial
|
||||
updates, use regular lenses instead.
|
||||
*/
|
||||
package lens
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -397,5 +397,3 @@ func TestIsoAsLensTypeConversion(t *testing.T) {
|
||||
assert.Equal(t, 3, len(updated))
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -301,5 +301,3 @@ For more information on isomorphisms and optics:
|
||||
- option package documentation
|
||||
*/
|
||||
package option
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -362,5 +362,3 @@ For more information on lenses and isomorphisms:
|
||||
- optics package overview
|
||||
*/
|
||||
package iso
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package lens
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
)
|
||||
@@ -43,7 +44,7 @@ func setCopyWithEq[GET ~func(*S) A, SET ~func(*S, A) *S, S, A any](pred EQ.Eq[A]
|
||||
|
||||
// setCopyCurried wraps a setter for a pointer into a setter that first creates a copy before
|
||||
// modifying that copy
|
||||
func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(a A) Endomorphism[*S] {
|
||||
func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(A) Endomorphism[*S] {
|
||||
return func(a A) Endomorphism[*S] {
|
||||
seta := setter(a)
|
||||
return func(s *S) *S {
|
||||
@@ -374,7 +375,7 @@ func IdRef[S any]() Lens[*S, *S] {
|
||||
}
|
||||
|
||||
// Compose combines two lenses and allows to narrow down the focus to a sub-lens
|
||||
func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GET, set SET) Lens[S, B], ab Lens[A, B]) func(Lens[S, A]) Lens[S, B] {
|
||||
func compose[GET ~func(S) B, SET ~func(B) func(S) S, S, A, B any](creator func(get GET, set SET) Lens[S, B], ab Lens[A, B]) Operator[S, A, B] {
|
||||
abget := ab.Get
|
||||
abset := ab.Set
|
||||
return func(sa Lens[S, A]) Lens[S, B] {
|
||||
@@ -382,8 +383,12 @@ func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GE
|
||||
saset := sa.Set
|
||||
return creator(
|
||||
F.Flow2(saget, abget),
|
||||
func(s S, b B) S {
|
||||
return saset(abset(b)(saget(s)))(s)
|
||||
func(b B) func(S) S {
|
||||
return endomorphism.Join(F.Flow3(
|
||||
saget,
|
||||
abset(b),
|
||||
saset,
|
||||
))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -436,7 +441,7 @@ func compose[GET ~func(S) B, SET ~func(S, B) S, S, A, B any](creator func(get GE
|
||||
// street := personStreetLens.Get(person) // "Main St"
|
||||
// updated := personStreetLens.Set("Oak Ave")(person)
|
||||
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)
|
||||
return compose(MakeLensCurried[func(S) B, func(B) func(S) S], ab)
|
||||
}
|
||||
|
||||
// ComposeRef combines two lenses for pointer-based structures.
|
||||
@@ -478,11 +483,7 @@ func Compose[S, A, B any](ab Lens[A, B]) Operator[S, A, B] {
|
||||
//
|
||||
// personStreetLens := F.Pipe1(addressLens, lens.ComposeRef[Person](streetLens))
|
||||
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)
|
||||
}
|
||||
|
||||
func modify[FCT ~func(A) A, S, A any](f FCT, sa Lens[S, A], s S) S {
|
||||
return sa.Set(f(sa.Get(s)))(s)
|
||||
return compose(MakeLensRefCurried[S, B], ab)
|
||||
}
|
||||
|
||||
// Modify transforms a value through a lens using a transformation F.
|
||||
@@ -531,7 +532,13 @@ func modify[FCT ~func(A) A, S, A any](f FCT, sa Lens[S, A], s S) S {
|
||||
// )
|
||||
// // doubled.Value == 10
|
||||
func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S] {
|
||||
return F.Curry3(modify[FCT, S, A])(f)
|
||||
return func(la Lens[S, A]) Endomorphism[S] {
|
||||
return endomorphism.Join(F.Flow3(
|
||||
la.Get,
|
||||
f,
|
||||
la.Set,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// IMap transforms the focus type of a lens using an isomorphism.
|
||||
@@ -585,8 +592,8 @@ func Modify[S any, FCT ~func(A) A, A any](f FCT) func(Lens[S, A]) Endomorphism[S
|
||||
// weather := Weather{Temperature: 20} // 20°C
|
||||
// tempF := tempFahrenheitLens.Get(weather) // 68°F
|
||||
// updated := tempFahrenheitLens.Set(86)(weather) // Set to 86°F (30°C)
|
||||
func IMap[E any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Lens[E, A]) Lens[E, B] {
|
||||
return func(ea Lens[E, A]) Lens[E, B] {
|
||||
return Lens[E, B]{Get: F.Flow2(ea.Get, ab), Set: F.Flow2(ba, ea.Set)}
|
||||
func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[S, A, B] {
|
||||
return func(ea Lens[S, A]) Lens[S, B] {
|
||||
return MakeLensCurried(F.Flow2(ea.Get, ab), F.Flow2(ba, ea.Set))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,5 +636,3 @@ func TestComposeAssociativity(t *testing.T) {
|
||||
assert.Equal(t, composed1.Get(l1), composed2.Get(l1))
|
||||
assert.Equal(t, composed1.Set("new")(l1), composed2.Set("new")(l1))
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
@@ -51,9 +53,9 @@ import (
|
||||
// defaultSettings := &Settings{}
|
||||
// configRetriesLens := F.Pipe1(settingsLens,
|
||||
// lens.Compose[Config, *int](defaultSettings)(retriesLens))
|
||||
func Compose[S, B, A any](defaultA A) func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
func Compose[S, B, A any](defaultA A) func(LensO[A, B]) Operator[S, A, B] {
|
||||
noneb := O.None[B]()
|
||||
return func(ab LensO[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
return func(ab LensO[A, B]) Operator[S, A, B] {
|
||||
abGet := ab.Get
|
||||
abSetNone := ab.Set(noneb)
|
||||
return func(sa LensO[S, A]) LensO[S, B] {
|
||||
@@ -62,41 +64,24 @@ func Compose[S, B, A any](defaultA A) func(ab LensO[A, B]) func(LensO[S, A]) Len
|
||||
setSomeA := F.Flow2(O.Some[A], sa.Set)
|
||||
return lens.MakeLensCurried(
|
||||
F.Flow2(saGet, O.Chain(abGet)),
|
||||
func(optB Option[B]) Endomorphism[S] {
|
||||
return func(s S) S {
|
||||
optA := saGet(s)
|
||||
return O.MonadFold(
|
||||
optB,
|
||||
F.Flow2(
|
||||
O.Fold(
|
||||
// optB is None
|
||||
func() S {
|
||||
return O.MonadFold(
|
||||
optA,
|
||||
// optA is None - no-op
|
||||
F.Constant(s),
|
||||
// optA is Some - unset B in A
|
||||
func(a A) S {
|
||||
return setSomeA(abSetNone(a))(s)
|
||||
},
|
||||
)
|
||||
},
|
||||
lazy.Of(F.Flow2(
|
||||
saGet,
|
||||
O.Fold(endomorphism.Identity[S], F.Flow2(abSetNone, setSomeA)),
|
||||
)),
|
||||
// optB is Some
|
||||
func(b B) S {
|
||||
func(b B) func(S) Endomorphism[S] {
|
||||
setB := ab.Set(O.Some(b))
|
||||
return O.MonadFold(
|
||||
optA,
|
||||
// optA is None - create with defaultA
|
||||
func() S {
|
||||
return setSomeA(setB(defaultA))(s)
|
||||
},
|
||||
// optA is Some - update B in A
|
||||
func(a A) S {
|
||||
return setSomeA(setB(a))(s)
|
||||
},
|
||||
return F.Flow2(
|
||||
saGet,
|
||||
O.Fold(lazy.Of(setSomeA(setB(defaultA))), F.Flow2(setB, setSomeA)),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
endomorphism.Join[S],
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -150,8 +135,8 @@ func Compose[S, B, A any](defaultA A) func(ab LensO[A, B]) func(LensO[S, A]) Len
|
||||
// port := configPortLens.Get(config) // None[int]
|
||||
// updated := configPortLens.Set(O.Some(3306))(config)
|
||||
// // updated.Database.Port == 3306, Host == "localhost" (from default)
|
||||
func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
return func(ab Lens[A, B]) func(LensO[S, A]) LensO[S, B] {
|
||||
func ComposeOption[S, B, A any](defaultA A) func(Lens[A, B]) Operator[S, A, B] {
|
||||
return func(ab Lens[A, B]) Operator[S, A, B] {
|
||||
abGet := ab.Get
|
||||
abSet := ab.Set
|
||||
return func(sa LensO[S, A]) LensO[S, B] {
|
||||
@@ -159,33 +144,23 @@ func ComposeOption[S, B, A any](defaultA A) func(ab Lens[A, B]) func(LensO[S, A]
|
||||
saSet := sa.Set
|
||||
// Pre-compute setters
|
||||
setNoneA := saSet(O.None[A]())
|
||||
setSomeA := func(a A) Endomorphism[S] {
|
||||
return saSet(O.Some(a))
|
||||
}
|
||||
return lens.MakeLens(
|
||||
func(s S) Option[B] {
|
||||
return O.Map(abGet)(saGet(s))
|
||||
},
|
||||
func(s S, optB Option[B]) S {
|
||||
return O.Fold(
|
||||
setSomeA := F.Flow2(O.Some[A], saSet)
|
||||
return lens.MakeLensCurried(
|
||||
F.Flow2(saGet, O.Map(abGet)),
|
||||
O.Fold(
|
||||
// optB is None - remove A entirely
|
||||
F.Constant(setNoneA(s)),
|
||||
lazy.Of(setNoneA),
|
||||
// optB is Some - set B
|
||||
func(b B) S {
|
||||
optA := saGet(s)
|
||||
return O.Fold(
|
||||
// optA is None - create with defaultA
|
||||
func() S {
|
||||
return setSomeA(abSet(b)(defaultA))(s)
|
||||
},
|
||||
// optA is Some - update B in A
|
||||
func(a A) S {
|
||||
return setSomeA(abSet(b)(a))(s)
|
||||
},
|
||||
)(optA)
|
||||
},
|
||||
)(optB)
|
||||
func(b B) Endomorphism[S] {
|
||||
absetB := abSet(b)
|
||||
abSetA := absetB(defaultA)
|
||||
return endomorphism.Join(F.Flow3(
|
||||
saGet,
|
||||
O.Fold(lazy.Of(abSetA), absetB),
|
||||
setSomeA,
|
||||
))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
841
v2/optics/lens/option/compose_test.go
Normal file
841
v2/optics/lens/option/compose_test.go
Normal file
@@ -0,0 +1,841 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package option
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
EQT "github.com/IBM/fp-go/v2/eq/testing"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test types for ComposeOption - using unique names to avoid conflicts
|
||||
type (
|
||||
DatabaseCfg struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
ServerConfig struct {
|
||||
Database *DatabaseCfg
|
||||
}
|
||||
|
||||
AppSettings struct {
|
||||
MaxRetries int
|
||||
Timeout int
|
||||
}
|
||||
|
||||
ApplicationConfig struct {
|
||||
Settings *AppSettings
|
||||
}
|
||||
)
|
||||
|
||||
// Helper methods for DatabaseCfg
|
||||
func (db *DatabaseCfg) GetPort() int {
|
||||
return db.Port
|
||||
}
|
||||
|
||||
func (db *DatabaseCfg) SetPort(port int) *DatabaseCfg {
|
||||
db.Port = port
|
||||
return db
|
||||
}
|
||||
|
||||
// Helper methods for ServerConfig
|
||||
func (c ServerConfig) GetDatabase() *DatabaseCfg {
|
||||
return c.Database
|
||||
}
|
||||
|
||||
func (c ServerConfig) SetDatabase(db *DatabaseCfg) ServerConfig {
|
||||
c.Database = db
|
||||
return c
|
||||
}
|
||||
|
||||
// Helper methods for AppSettings
|
||||
func (s *AppSettings) GetMaxRetries() int {
|
||||
return s.MaxRetries
|
||||
}
|
||||
|
||||
func (s *AppSettings) SetMaxRetries(retries int) *AppSettings {
|
||||
s.MaxRetries = retries
|
||||
return s
|
||||
}
|
||||
|
||||
// Helper methods for ApplicationConfig
|
||||
func (ac ApplicationConfig) GetSettings() *AppSettings {
|
||||
return ac.Settings
|
||||
}
|
||||
|
||||
func (ac ApplicationConfig) SetSettings(s *AppSettings) ApplicationConfig {
|
||||
ac.Settings = s
|
||||
return ac
|
||||
}
|
||||
|
||||
// TestComposeOptionBasicOperations tests basic get/set operations
|
||||
func TestComposeOptionBasicOperations(t *testing.T) {
|
||||
// Create lenses
|
||||
dbLens := FromNillable(L.MakeLens(ServerConfig.GetDatabase, ServerConfig.SetDatabase))
|
||||
portLens := L.MakeLensRef((*DatabaseCfg).GetPort, (*DatabaseCfg).SetPort)
|
||||
|
||||
defaultDB := &DatabaseCfg{Host: "localhost", Port: 5432}
|
||||
configPortLens := F.Pipe1(dbLens, ComposeOption[ServerConfig, int](defaultDB)(portLens))
|
||||
|
||||
t.Run("Get from empty config returns None", func(t *testing.T) {
|
||||
config := ServerConfig{Database: nil}
|
||||
result := configPortLens.Get(config)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get from config with database returns Some", func(t *testing.T) {
|
||||
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
|
||||
result := configPortLens.Get(config)
|
||||
assert.Equal(t, O.Some(3306), result)
|
||||
})
|
||||
|
||||
t.Run("Set Some on empty config creates database with default", func(t *testing.T) {
|
||||
config := ServerConfig{Database: nil}
|
||||
updated := configPortLens.Set(O.Some(3306))(config)
|
||||
assert.NotNil(t, updated.Database)
|
||||
assert.Equal(t, 3306, updated.Database.Port)
|
||||
assert.Equal(t, "localhost", updated.Database.Host) // From default
|
||||
})
|
||||
|
||||
t.Run("Set Some on existing database updates port", func(t *testing.T) {
|
||||
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 5432}}
|
||||
updated := configPortLens.Set(O.Some(8080))(config)
|
||||
assert.NotNil(t, updated.Database)
|
||||
assert.Equal(t, 8080, updated.Database.Port)
|
||||
assert.Equal(t, "example.com", updated.Database.Host) // Preserved
|
||||
})
|
||||
|
||||
t.Run("Set None removes database entirely", func(t *testing.T) {
|
||||
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
|
||||
updated := configPortLens.Set(O.None[int]())(config)
|
||||
assert.Nil(t, updated.Database)
|
||||
})
|
||||
|
||||
t.Run("Set None on empty config is no-op", func(t *testing.T) {
|
||||
config := ServerConfig{Database: nil}
|
||||
updated := configPortLens.Set(O.None[int]())(config)
|
||||
assert.Nil(t, updated.Database)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeOptionLensLawsDetailed verifies that ComposeOption satisfies lens laws
|
||||
func TestComposeOptionLensLawsDetailed(t *testing.T) {
|
||||
// Setup
|
||||
defaultDB := &DatabaseCfg{Host: "localhost", Port: 5432}
|
||||
dbLens := FromNillable(L.MakeLens(ServerConfig.GetDatabase, ServerConfig.SetDatabase))
|
||||
portLens := L.MakeLensRef((*DatabaseCfg).GetPort, (*DatabaseCfg).SetPort)
|
||||
configPortLens := F.Pipe1(dbLens, ComposeOption[ServerConfig, int](defaultDB)(portLens))
|
||||
|
||||
// Equality predicates
|
||||
eqInt := EQT.Eq[int]()
|
||||
eqOptInt := O.Eq(eqInt)
|
||||
eqServerConfig := func(a, b ServerConfig) bool {
|
||||
if a.Database == nil && b.Database == nil {
|
||||
return true
|
||||
}
|
||||
if a.Database == nil || b.Database == nil {
|
||||
return false
|
||||
}
|
||||
return a.Database.Host == b.Database.Host && a.Database.Port == b.Database.Port
|
||||
}
|
||||
|
||||
// Test structures
|
||||
configNil := ServerConfig{Database: nil}
|
||||
config3306 := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
|
||||
config5432 := ServerConfig{Database: &DatabaseCfg{Host: "test.com", Port: 5432}}
|
||||
|
||||
// Law 1: GetSet - lens.Get(lens.Set(a)(s)) == a
|
||||
t.Run("Law1_GetSet_WithSome", func(t *testing.T) {
|
||||
// Setting Some(8080) and getting back should return Some(8080)
|
||||
result := configPortLens.Get(configPortLens.Set(O.Some(8080))(config3306))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(8080)),
|
||||
"Get(Set(Some(8080))(s)) should equal Some(8080)")
|
||||
})
|
||||
|
||||
t.Run("Law1_GetSet_WithNone", func(t *testing.T) {
|
||||
// Setting None and getting back should return None
|
||||
result := configPortLens.Get(configPortLens.Set(O.None[int]())(config3306))
|
||||
assert.True(t, eqOptInt.Equals(result, O.None[int]()),
|
||||
"Get(Set(None)(s)) should equal None")
|
||||
})
|
||||
|
||||
t.Run("Law1_GetSet_OnEmptyWithSome", func(t *testing.T) {
|
||||
// Setting Some on empty config and getting back
|
||||
result := configPortLens.Get(configPortLens.Set(O.Some(9000))(configNil))
|
||||
assert.True(t, eqOptInt.Equals(result, O.Some(9000)),
|
||||
"Get(Set(Some(9000))(empty)) should equal Some(9000)")
|
||||
})
|
||||
|
||||
// Law 2: SetGet - lens.Set(lens.Get(s))(s) == s
|
||||
t.Run("Law2_SetGet_WithDatabase", func(t *testing.T) {
|
||||
// Setting what we get should return the same structure
|
||||
result := configPortLens.Set(configPortLens.Get(config3306))(config3306)
|
||||
assert.True(t, eqServerConfig(result, config3306),
|
||||
"Set(Get(s))(s) should equal s")
|
||||
})
|
||||
|
||||
t.Run("Law2_SetGet_WithoutDatabase", func(t *testing.T) {
|
||||
// Setting what we get from empty should return the same structure
|
||||
result := configPortLens.Set(configPortLens.Get(configNil))(configNil)
|
||||
assert.True(t, eqServerConfig(result, configNil),
|
||||
"Set(Get(empty))(empty) should equal empty")
|
||||
})
|
||||
|
||||
t.Run("Law2_SetGet_DifferentConfigs", func(t *testing.T) {
|
||||
// Test with another config
|
||||
result := configPortLens.Set(configPortLens.Get(config5432))(config5432)
|
||||
assert.True(t, eqServerConfig(result, config5432),
|
||||
"Set(Get(s))(s) should equal s for any s")
|
||||
})
|
||||
|
||||
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
|
||||
t.Run("Law3_SetSet_BothSome", func(t *testing.T) {
|
||||
// Setting twice with Some should be same as setting once
|
||||
setTwice := configPortLens.Set(O.Some(9000))(configPortLens.Set(O.Some(8080))(config3306))
|
||||
setOnce := configPortLens.Set(O.Some(9000))(config3306)
|
||||
assert.True(t, eqServerConfig(setTwice, setOnce),
|
||||
"Set(a2)(Set(a1)(s)) should equal Set(a2)(s)")
|
||||
})
|
||||
|
||||
t.Run("Law3_SetSet_BothNone", func(t *testing.T) {
|
||||
// Setting None twice should be same as setting once
|
||||
setTwice := configPortLens.Set(O.None[int]())(configPortLens.Set(O.None[int]())(config3306))
|
||||
setOnce := configPortLens.Set(O.None[int]())(config3306)
|
||||
assert.True(t, eqServerConfig(setTwice, setOnce),
|
||||
"Set(None)(Set(None)(s)) should equal Set(None)(s)")
|
||||
})
|
||||
|
||||
t.Run("Law3_SetSet_SomeThenNone", func(t *testing.T) {
|
||||
// Setting None after Some should be same as setting None directly
|
||||
setTwice := configPortLens.Set(O.None[int]())(configPortLens.Set(O.Some(8080))(config3306))
|
||||
setOnce := configPortLens.Set(O.None[int]())(config3306)
|
||||
assert.True(t, eqServerConfig(setTwice, setOnce),
|
||||
"Set(None)(Set(Some)(s)) should equal Set(None)(s)")
|
||||
})
|
||||
|
||||
t.Run("Law3_SetSet_NoneThenSome", func(t *testing.T) {
|
||||
// Setting Some after None creates a new database with default values
|
||||
// This is different from setting Some directly which preserves existing fields
|
||||
setTwice := configPortLens.Set(O.Some(8080))(configPortLens.Set(O.None[int]())(config3306))
|
||||
// After setting None, the database is removed, so setting Some creates it with defaults
|
||||
assert.NotNil(t, setTwice.Database)
|
||||
assert.Equal(t, 8080, setTwice.Database.Port)
|
||||
assert.Equal(t, "localhost", setTwice.Database.Host) // From default, not "example.com"
|
||||
|
||||
// This demonstrates that ComposeOption's behavior when setting None then Some
|
||||
// uses the default value for the intermediate structure
|
||||
setOnce := configPortLens.Set(O.Some(8080))(config3306)
|
||||
assert.Equal(t, 8080, setOnce.Database.Port)
|
||||
assert.Equal(t, "example.com", setOnce.Database.Host) // Preserved from original
|
||||
|
||||
// They are NOT equal because the Host field differs
|
||||
assert.False(t, eqServerConfig(setTwice, setOnce),
|
||||
"Set(Some)(Set(None)(s)) uses default, Set(Some)(s) preserves fields")
|
||||
})
|
||||
|
||||
t.Run("Law3_SetSet_OnEmpty", func(t *testing.T) {
|
||||
// Setting twice on empty config
|
||||
setTwice := configPortLens.Set(O.Some(9000))(configPortLens.Set(O.Some(8080))(configNil))
|
||||
setOnce := configPortLens.Set(O.Some(9000))(configNil)
|
||||
assert.True(t, eqServerConfig(setTwice, setOnce),
|
||||
"Set(a2)(Set(a1)(empty)) should equal Set(a2)(empty)")
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeOptionWithModify tests the Modify operation
|
||||
func TestComposeOptionWithModify(t *testing.T) {
|
||||
defaultDB := &DatabaseCfg{Host: "localhost", Port: 5432}
|
||||
dbLens := FromNillable(L.MakeLens(ServerConfig.GetDatabase, ServerConfig.SetDatabase))
|
||||
portLens := L.MakeLensRef((*DatabaseCfg).GetPort, (*DatabaseCfg).SetPort)
|
||||
configPortLens := F.Pipe1(dbLens, ComposeOption[ServerConfig, int](defaultDB)(portLens))
|
||||
|
||||
t.Run("Modify with identity returns same structure", func(t *testing.T) {
|
||||
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
|
||||
result := L.Modify[ServerConfig](F.Identity[Option[int]])(configPortLens)(config)
|
||||
assert.Equal(t, config.Database.Port, result.Database.Port)
|
||||
assert.Equal(t, config.Database.Host, result.Database.Host)
|
||||
})
|
||||
|
||||
t.Run("Modify with Some transformation", func(t *testing.T) {
|
||||
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
|
||||
// Double the port if it exists
|
||||
doublePort := O.Map(func(p int) int { return p * 2 })
|
||||
result := L.Modify[ServerConfig](doublePort)(configPortLens)(config)
|
||||
assert.Equal(t, 6612, result.Database.Port)
|
||||
assert.Equal(t, "example.com", result.Database.Host)
|
||||
})
|
||||
|
||||
t.Run("Modify on empty config with Some transformation", func(t *testing.T) {
|
||||
config := ServerConfig{Database: nil}
|
||||
doublePort := O.Map(func(p int) int { return p * 2 })
|
||||
result := L.Modify[ServerConfig](doublePort)(configPortLens)(config)
|
||||
// Should remain empty since there's nothing to modify
|
||||
assert.Nil(t, result.Database)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeOptionComposition tests composing multiple ComposeOption lenses
|
||||
func TestComposeOptionComposition(t *testing.T) {
|
||||
type Level3 struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
type Level2 struct {
|
||||
Level3 *Level3
|
||||
}
|
||||
|
||||
type Level1 struct {
|
||||
Level2 *Level2
|
||||
}
|
||||
|
||||
// Create lenses
|
||||
level2Lens := FromNillable(L.MakeLens(
|
||||
func(l1 Level1) *Level2 { return l1.Level2 },
|
||||
func(l1 Level1, l2 *Level2) Level1 { l1.Level2 = l2; return l1 },
|
||||
))
|
||||
|
||||
level3Lens := L.MakeLensRef(
|
||||
func(l2 *Level2) *Level3 { return l2.Level3 },
|
||||
func(l2 *Level2, l3 *Level3) *Level2 { l2.Level3 = l3; return l2 },
|
||||
)
|
||||
|
||||
valueLens := L.MakeLensRef(
|
||||
func(l3 *Level3) int { return l3.Value },
|
||||
func(l3 *Level3, v int) *Level3 { l3.Value = v; return l3 },
|
||||
)
|
||||
|
||||
// Compose: Level1 -> Option[Level2] -> Option[Level3] -> Option[int]
|
||||
defaultLevel2 := &Level2{Level3: &Level3{Value: 0}}
|
||||
defaultLevel3 := &Level3{Value: 0}
|
||||
|
||||
// First composition: Level1 -> Option[Level3]
|
||||
level1ToLevel3 := F.Pipe1(level2Lens, ComposeOption[Level1, *Level3](defaultLevel2)(level3Lens))
|
||||
|
||||
// Second composition: Level1 -> Option[int]
|
||||
level1ToValue := F.Pipe1(level1ToLevel3, ComposeOption[Level1, int](defaultLevel3)(valueLens))
|
||||
|
||||
t.Run("Get from fully populated structure", func(t *testing.T) {
|
||||
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: 42}}}
|
||||
result := level1ToValue.Get(l1)
|
||||
assert.Equal(t, O.Some(42), result)
|
||||
})
|
||||
|
||||
t.Run("Get from empty structure", func(t *testing.T) {
|
||||
l1 := Level1{Level2: nil}
|
||||
result := level1ToValue.Get(l1)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Set on empty structure creates all levels", func(t *testing.T) {
|
||||
l1 := Level1{Level2: nil}
|
||||
updated := level1ToValue.Set(O.Some(100))(l1)
|
||||
assert.NotNil(t, updated.Level2)
|
||||
assert.NotNil(t, updated.Level2.Level3)
|
||||
assert.Equal(t, 100, updated.Level2.Level3.Value)
|
||||
})
|
||||
|
||||
t.Run("Set None removes top level", func(t *testing.T) {
|
||||
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: 42}}}
|
||||
updated := level1ToValue.Set(O.None[int]())(l1)
|
||||
assert.Nil(t, updated.Level2)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeOptionEdgeCasesExtended tests additional edge cases
|
||||
func TestComposeOptionEdgeCasesExtended(t *testing.T) {
|
||||
defaultSettings := &AppSettings{MaxRetries: 3, Timeout: 30}
|
||||
settingsLens := FromNillable(L.MakeLens(ApplicationConfig.GetSettings, ApplicationConfig.SetSettings))
|
||||
retriesLens := L.MakeLensRef((*AppSettings).GetMaxRetries, (*AppSettings).SetMaxRetries)
|
||||
configRetriesLens := F.Pipe1(settingsLens, ComposeOption[ApplicationConfig, int](defaultSettings)(retriesLens))
|
||||
|
||||
t.Run("Multiple sets with different values", func(t *testing.T) {
|
||||
config := ApplicationConfig{Settings: nil}
|
||||
// Set multiple times
|
||||
config = configRetriesLens.Set(O.Some(5))(config)
|
||||
assert.Equal(t, 5, config.Settings.MaxRetries)
|
||||
|
||||
config = configRetriesLens.Set(O.Some(10))(config)
|
||||
assert.Equal(t, 10, config.Settings.MaxRetries)
|
||||
|
||||
config = configRetriesLens.Set(O.None[int]())(config)
|
||||
assert.Nil(t, config.Settings)
|
||||
})
|
||||
|
||||
t.Run("Get after Set maintains consistency", func(t *testing.T) {
|
||||
config := ApplicationConfig{Settings: nil}
|
||||
updated := configRetriesLens.Set(O.Some(7))(config)
|
||||
retrieved := configRetriesLens.Get(updated)
|
||||
assert.Equal(t, O.Some(7), retrieved)
|
||||
})
|
||||
|
||||
t.Run("Default values are used correctly", func(t *testing.T) {
|
||||
config := ApplicationConfig{Settings: nil}
|
||||
updated := configRetriesLens.Set(O.Some(15))(config)
|
||||
// Check that default timeout is used
|
||||
assert.Equal(t, 30, updated.Settings.Timeout)
|
||||
assert.Equal(t, 15, updated.Settings.MaxRetries)
|
||||
})
|
||||
|
||||
t.Run("Preserves other fields when updating", func(t *testing.T) {
|
||||
config := ApplicationConfig{Settings: &AppSettings{MaxRetries: 5, Timeout: 60}}
|
||||
updated := configRetriesLens.Set(O.Some(10))(config)
|
||||
assert.Equal(t, 10, updated.Settings.MaxRetries)
|
||||
assert.Equal(t, 60, updated.Settings.Timeout) // Preserved
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeOptionWithZeroValues tests behavior with zero values
|
||||
func TestComposeOptionWithZeroValues(t *testing.T) {
|
||||
defaultDB := &DatabaseCfg{Host: "", Port: 0}
|
||||
dbLens := FromNillable(L.MakeLens(ServerConfig.GetDatabase, ServerConfig.SetDatabase))
|
||||
portLens := L.MakeLensRef((*DatabaseCfg).GetPort, (*DatabaseCfg).SetPort)
|
||||
configPortLens := F.Pipe1(dbLens, ComposeOption[ServerConfig, int](defaultDB)(portLens))
|
||||
|
||||
t.Run("Set zero value", func(t *testing.T) {
|
||||
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 3306}}
|
||||
updated := configPortLens.Set(O.Some(0))(config)
|
||||
assert.Equal(t, 0, updated.Database.Port)
|
||||
assert.Equal(t, "example.com", updated.Database.Host)
|
||||
})
|
||||
|
||||
t.Run("Get zero value returns Some(0)", func(t *testing.T) {
|
||||
config := ServerConfig{Database: &DatabaseCfg{Host: "example.com", Port: 0}}
|
||||
result := configPortLens.Get(config)
|
||||
assert.Equal(t, O.Some(0), result)
|
||||
})
|
||||
|
||||
t.Run("Default with zero values", func(t *testing.T) {
|
||||
config := ServerConfig{Database: nil}
|
||||
updated := configPortLens.Set(O.Some(8080))(config)
|
||||
assert.Equal(t, "", updated.Database.Host) // From default
|
||||
assert.Equal(t, 8080, updated.Database.Port)
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests for Compose function (both lenses return Option values)
|
||||
// ============================================================================
|
||||
|
||||
// TestComposeBasicOperations tests basic get/set operations for Compose
|
||||
func TestComposeBasicOperations(t *testing.T) {
|
||||
type Value struct {
|
||||
Data *string
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
Value *Value
|
||||
}
|
||||
|
||||
// Create lenses
|
||||
valueLens := FromNillable(L.MakeLens(
|
||||
func(c Container) *Value { return c.Value },
|
||||
func(c Container, v *Value) Container { c.Value = v; return c },
|
||||
))
|
||||
|
||||
dataLens := L.MakeLensRef(
|
||||
func(v *Value) *string { return v.Data },
|
||||
func(v *Value, d *string) *Value { v.Data = d; return v },
|
||||
)
|
||||
|
||||
defaultValue := &Value{Data: nil}
|
||||
composedLens := F.Pipe1(valueLens, Compose[Container, *string](defaultValue)(
|
||||
FromNillable(dataLens),
|
||||
))
|
||||
|
||||
t.Run("Get from empty container returns None", func(t *testing.T) {
|
||||
container := Container{Value: nil}
|
||||
result := composedLens.Get(container)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get from container with nil data returns None", func(t *testing.T) {
|
||||
container := Container{Value: &Value{Data: nil}}
|
||||
result := composedLens.Get(container)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get from container with data returns Some", func(t *testing.T) {
|
||||
data := "test"
|
||||
container := Container{Value: &Value{Data: &data}}
|
||||
result := composedLens.Get(container)
|
||||
assert.True(t, O.IsSome(result))
|
||||
assert.Equal(t, &data, O.GetOrElse(func() *string { return nil })(result))
|
||||
})
|
||||
|
||||
t.Run("Set Some on empty container creates structure with default", func(t *testing.T) {
|
||||
container := Container{Value: nil}
|
||||
data := "new"
|
||||
updated := composedLens.Set(O.Some(&data))(container)
|
||||
assert.NotNil(t, updated.Value)
|
||||
assert.NotNil(t, updated.Value.Data)
|
||||
assert.Equal(t, "new", *updated.Value.Data)
|
||||
})
|
||||
|
||||
t.Run("Set Some on existing container updates data", func(t *testing.T) {
|
||||
oldData := "old"
|
||||
container := Container{Value: &Value{Data: &oldData}}
|
||||
newData := "new"
|
||||
updated := composedLens.Set(O.Some(&newData))(container)
|
||||
assert.NotNil(t, updated.Value)
|
||||
assert.NotNil(t, updated.Value.Data)
|
||||
assert.Equal(t, "new", *updated.Value.Data)
|
||||
})
|
||||
|
||||
t.Run("Set None when container is empty is no-op", func(t *testing.T) {
|
||||
container := Container{Value: nil}
|
||||
updated := composedLens.Set(O.None[*string]())(container)
|
||||
assert.Nil(t, updated.Value)
|
||||
})
|
||||
|
||||
t.Run("Set None when container exists unsets data", func(t *testing.T) {
|
||||
data := "test"
|
||||
container := Container{Value: &Value{Data: &data}}
|
||||
updated := composedLens.Set(O.None[*string]())(container)
|
||||
assert.NotNil(t, updated.Value)
|
||||
assert.Nil(t, updated.Value.Data)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeLensLawsDetailed verifies that Compose satisfies lens laws
|
||||
func TestComposeLensLawsDetailed(t *testing.T) {
|
||||
type Inner struct {
|
||||
Value *int
|
||||
Extra string
|
||||
}
|
||||
|
||||
type Outer struct {
|
||||
Inner *Inner
|
||||
}
|
||||
|
||||
// Setup
|
||||
defaultInner := &Inner{Value: nil, Extra: "default"}
|
||||
innerLens := FromNillable(L.MakeLens(
|
||||
func(o Outer) *Inner { return o.Inner },
|
||||
func(o Outer, i *Inner) Outer { o.Inner = i; return o },
|
||||
))
|
||||
valueLens := L.MakeLensRef(
|
||||
func(i *Inner) *int { return i.Value },
|
||||
func(i *Inner, v *int) *Inner { i.Value = v; return i },
|
||||
)
|
||||
composedLens := F.Pipe1(innerLens, Compose[Outer, *int](defaultInner)(
|
||||
FromNillable(valueLens),
|
||||
))
|
||||
|
||||
// Equality predicates
|
||||
eqIntPtr := EQT.Eq[*int]()
|
||||
eqOptIntPtr := O.Eq(eqIntPtr)
|
||||
eqOuter := func(a, b Outer) bool {
|
||||
if a.Inner == nil && b.Inner == nil {
|
||||
return true
|
||||
}
|
||||
if a.Inner == nil || b.Inner == nil {
|
||||
return false
|
||||
}
|
||||
aVal := a.Inner.Value
|
||||
bVal := b.Inner.Value
|
||||
if aVal == nil && bVal == nil {
|
||||
return a.Inner.Extra == b.Inner.Extra
|
||||
}
|
||||
if aVal == nil || bVal == nil {
|
||||
return false
|
||||
}
|
||||
return *aVal == *bVal && a.Inner.Extra == b.Inner.Extra
|
||||
}
|
||||
|
||||
// Test structures
|
||||
val42 := 42
|
||||
val100 := 100
|
||||
outerNil := Outer{Inner: nil}
|
||||
outerWithNilValue := Outer{Inner: &Inner{Value: nil, Extra: "test"}}
|
||||
outer42 := Outer{Inner: &Inner{Value: &val42, Extra: "test"}}
|
||||
|
||||
// Law 1: GetSet - lens.Get(lens.Set(a)(s)) == a
|
||||
t.Run("Law1_GetSet_WithSome", func(t *testing.T) {
|
||||
result := composedLens.Get(composedLens.Set(O.Some(&val100))(outer42))
|
||||
assert.True(t, eqOptIntPtr.Equals(result, O.Some(&val100)),
|
||||
"Get(Set(Some(100))(s)) should equal Some(100)")
|
||||
})
|
||||
|
||||
t.Run("Law1_GetSet_WithNone", func(t *testing.T) {
|
||||
result := composedLens.Get(composedLens.Set(O.None[*int]())(outer42))
|
||||
assert.True(t, eqOptIntPtr.Equals(result, O.None[*int]()),
|
||||
"Get(Set(None)(s)) should equal None")
|
||||
})
|
||||
|
||||
t.Run("Law1_GetSet_OnEmpty", func(t *testing.T) {
|
||||
result := composedLens.Get(composedLens.Set(O.Some(&val100))(outerNil))
|
||||
assert.True(t, eqOptIntPtr.Equals(result, O.Some(&val100)),
|
||||
"Get(Set(Some(100))(empty)) should equal Some(100)")
|
||||
})
|
||||
|
||||
// Law 2: SetGet - lens.Set(lens.Get(s))(s) == s
|
||||
t.Run("Law2_SetGet_WithValue", func(t *testing.T) {
|
||||
result := composedLens.Set(composedLens.Get(outer42))(outer42)
|
||||
assert.True(t, eqOuter(result, outer42),
|
||||
"Set(Get(s))(s) should equal s")
|
||||
})
|
||||
|
||||
t.Run("Law2_SetGet_WithNilValue", func(t *testing.T) {
|
||||
result := composedLens.Set(composedLens.Get(outerWithNilValue))(outerWithNilValue)
|
||||
assert.True(t, eqOuter(result, outerWithNilValue),
|
||||
"Set(Get(s))(s) should equal s when value is nil")
|
||||
})
|
||||
|
||||
t.Run("Law2_SetGet_WithNilInner", func(t *testing.T) {
|
||||
result := composedLens.Set(composedLens.Get(outerNil))(outerNil)
|
||||
assert.True(t, eqOuter(result, outerNil),
|
||||
"Set(Get(empty))(empty) should equal empty")
|
||||
})
|
||||
|
||||
// Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s)
|
||||
t.Run("Law3_SetSet_BothSome", func(t *testing.T) {
|
||||
val200 := 200
|
||||
setTwice := composedLens.Set(O.Some(&val200))(composedLens.Set(O.Some(&val100))(outer42))
|
||||
setOnce := composedLens.Set(O.Some(&val200))(outer42)
|
||||
assert.True(t, eqOuter(setTwice, setOnce),
|
||||
"Set(a2)(Set(a1)(s)) should equal Set(a2)(s)")
|
||||
})
|
||||
|
||||
t.Run("Law3_SetSet_BothNone", func(t *testing.T) {
|
||||
setTwice := composedLens.Set(O.None[*int]())(composedLens.Set(O.None[*int]())(outer42))
|
||||
setOnce := composedLens.Set(O.None[*int]())(outer42)
|
||||
assert.True(t, eqOuter(setTwice, setOnce),
|
||||
"Set(None)(Set(None)(s)) should equal Set(None)(s)")
|
||||
})
|
||||
|
||||
t.Run("Law3_SetSet_SomeThenNone", func(t *testing.T) {
|
||||
setTwice := composedLens.Set(O.None[*int]())(composedLens.Set(O.Some(&val100))(outer42))
|
||||
setOnce := composedLens.Set(O.None[*int]())(outer42)
|
||||
assert.True(t, eqOuter(setTwice, setOnce),
|
||||
"Set(None)(Set(Some)(s)) should equal Set(None)(s)")
|
||||
})
|
||||
|
||||
t.Run("Law3_SetSet_NoneThenSome", func(t *testing.T) {
|
||||
// This case is interesting: setting None then Some uses default
|
||||
setTwice := composedLens.Set(O.Some(&val100))(composedLens.Set(O.None[*int]())(outer42))
|
||||
// After None, inner still exists but value is nil
|
||||
// Then setting Some updates the value
|
||||
assert.NotNil(t, setTwice.Inner)
|
||||
assert.NotNil(t, setTwice.Inner.Value)
|
||||
assert.Equal(t, 100, *setTwice.Inner.Value)
|
||||
assert.Equal(t, "test", setTwice.Inner.Extra) // Preserved from original
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeWithModify tests the Modify operation for Compose
|
||||
func TestComposeWithModify(t *testing.T) {
|
||||
type Data struct {
|
||||
Count *int
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
Data *Data
|
||||
}
|
||||
|
||||
defaultData := &Data{Count: nil}
|
||||
dataLens := FromNillable(L.MakeLens(
|
||||
func(s Store) *Data { return s.Data },
|
||||
func(s Store, d *Data) Store { s.Data = d; return s },
|
||||
))
|
||||
countLens := L.MakeLensRef(
|
||||
func(d *Data) *int { return d.Count },
|
||||
func(d *Data, c *int) *Data { d.Count = c; return d },
|
||||
)
|
||||
composedLens := F.Pipe1(dataLens, Compose[Store, *int](defaultData)(
|
||||
FromNillable(countLens),
|
||||
))
|
||||
|
||||
t.Run("Modify with identity returns same structure", func(t *testing.T) {
|
||||
count := 5
|
||||
store := Store{Data: &Data{Count: &count}}
|
||||
result := L.Modify[Store](F.Identity[Option[*int]])(composedLens)(store)
|
||||
assert.Equal(t, 5, *result.Data.Count)
|
||||
})
|
||||
|
||||
t.Run("Modify with Some transformation", func(t *testing.T) {
|
||||
count := 5
|
||||
store := Store{Data: &Data{Count: &count}}
|
||||
// Double the count if it exists
|
||||
doubleCount := O.Map(func(c *int) *int {
|
||||
doubled := *c * 2
|
||||
return &doubled
|
||||
})
|
||||
result := L.Modify[Store](doubleCount)(composedLens)(store)
|
||||
assert.Equal(t, 10, *result.Data.Count)
|
||||
})
|
||||
|
||||
t.Run("Modify on empty store", func(t *testing.T) {
|
||||
store := Store{Data: nil}
|
||||
doubleCount := O.Map(func(c *int) *int {
|
||||
doubled := *c * 2
|
||||
return &doubled
|
||||
})
|
||||
result := L.Modify[Store](doubleCount)(composedLens)(store)
|
||||
// Should remain empty since there's nothing to modify
|
||||
assert.Nil(t, result.Data)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeMultiLevel tests composing multiple Compose operations
|
||||
func TestComposeMultiLevel(t *testing.T) {
|
||||
type Level3 struct {
|
||||
Value *string
|
||||
}
|
||||
|
||||
type Level2 struct {
|
||||
Level3 *Level3
|
||||
}
|
||||
|
||||
type Level1 struct {
|
||||
Level2 *Level2
|
||||
}
|
||||
|
||||
// Create lenses
|
||||
level2Lens := FromNillable(L.MakeLens(
|
||||
func(l1 Level1) *Level2 { return l1.Level2 },
|
||||
func(l1 Level1, l2 *Level2) Level1 { l1.Level2 = l2; return l1 },
|
||||
))
|
||||
|
||||
level3Lens := L.MakeLensRef(
|
||||
func(l2 *Level2) *Level3 { return l2.Level3 },
|
||||
func(l2 *Level2, l3 *Level3) *Level2 { l2.Level3 = l3; return l2 },
|
||||
)
|
||||
|
||||
valueLens := L.MakeLensRef(
|
||||
func(l3 *Level3) *string { return l3.Value },
|
||||
func(l3 *Level3, v *string) *Level3 { l3.Value = v; return l3 },
|
||||
)
|
||||
|
||||
// Compose: Level1 -> Option[Level2] -> Option[Level3] -> Option[string]
|
||||
defaultLevel2 := &Level2{Level3: nil}
|
||||
defaultLevel3 := &Level3{Value: nil}
|
||||
|
||||
// First composition: Level1 -> Option[Level3]
|
||||
level1ToLevel3 := F.Pipe1(level2Lens, Compose[Level1, *Level3](defaultLevel2)(
|
||||
FromNillable(level3Lens),
|
||||
))
|
||||
|
||||
// Second composition: Level1 -> Option[string]
|
||||
level1ToValue := F.Pipe1(level1ToLevel3, Compose[Level1, *string](defaultLevel3)(
|
||||
FromNillable(valueLens),
|
||||
))
|
||||
|
||||
t.Run("Get from fully populated structure", func(t *testing.T) {
|
||||
value := "test"
|
||||
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: &value}}}
|
||||
result := level1ToValue.Get(l1)
|
||||
assert.True(t, O.IsSome(result))
|
||||
})
|
||||
|
||||
t.Run("Get from partially populated structure", func(t *testing.T) {
|
||||
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: nil}}}
|
||||
result := level1ToValue.Get(l1)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Get from empty structure", func(t *testing.T) {
|
||||
l1 := Level1{Level2: nil}
|
||||
result := level1ToValue.Get(l1)
|
||||
assert.True(t, O.IsNone(result))
|
||||
})
|
||||
|
||||
t.Run("Set on empty structure creates all levels", func(t *testing.T) {
|
||||
l1 := Level1{Level2: nil}
|
||||
value := "new"
|
||||
updated := level1ToValue.Set(O.Some(&value))(l1)
|
||||
assert.NotNil(t, updated.Level2)
|
||||
assert.NotNil(t, updated.Level2.Level3)
|
||||
assert.NotNil(t, updated.Level2.Level3.Value)
|
||||
assert.Equal(t, "new", *updated.Level2.Level3.Value)
|
||||
})
|
||||
|
||||
t.Run("Set None when structure exists unsets value", func(t *testing.T) {
|
||||
value := "test"
|
||||
l1 := Level1{Level2: &Level2{Level3: &Level3{Value: &value}}}
|
||||
updated := level1ToValue.Set(O.None[*string]())(l1)
|
||||
assert.NotNil(t, updated.Level2)
|
||||
assert.NotNil(t, updated.Level2.Level3)
|
||||
assert.Nil(t, updated.Level2.Level3.Value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestComposeEdgeCasesExtended tests additional edge cases for Compose
|
||||
func TestComposeEdgeCasesExtended(t *testing.T) {
|
||||
type Metadata struct {
|
||||
Tags *[]string
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
Metadata *Metadata
|
||||
}
|
||||
|
||||
defaultMetadata := &Metadata{Tags: nil}
|
||||
metadataLens := FromNillable(L.MakeLens(
|
||||
func(d Document) *Metadata { return d.Metadata },
|
||||
func(d Document, m *Metadata) Document { d.Metadata = m; return d },
|
||||
))
|
||||
tagsLens := L.MakeLensRef(
|
||||
func(m *Metadata) *[]string { return m.Tags },
|
||||
func(m *Metadata, t *[]string) *Metadata { m.Tags = t; return m },
|
||||
)
|
||||
composedLens := F.Pipe1(metadataLens, Compose[Document, *[]string](defaultMetadata)(
|
||||
FromNillable(tagsLens),
|
||||
))
|
||||
|
||||
t.Run("Multiple sets with different values", func(t *testing.T) {
|
||||
doc := Document{Metadata: nil}
|
||||
tags1 := []string{"tag1"}
|
||||
tags2 := []string{"tag2", "tag3"}
|
||||
|
||||
// Set first value
|
||||
doc = composedLens.Set(O.Some(&tags1))(doc)
|
||||
assert.NotNil(t, doc.Metadata)
|
||||
assert.NotNil(t, doc.Metadata.Tags)
|
||||
assert.Equal(t, 1, len(*doc.Metadata.Tags))
|
||||
|
||||
// Set second value
|
||||
doc = composedLens.Set(O.Some(&tags2))(doc)
|
||||
assert.Equal(t, 2, len(*doc.Metadata.Tags))
|
||||
|
||||
// Set None
|
||||
doc = composedLens.Set(O.None[*[]string]())(doc)
|
||||
assert.NotNil(t, doc.Metadata)
|
||||
assert.Nil(t, doc.Metadata.Tags)
|
||||
})
|
||||
|
||||
t.Run("Get after Set maintains consistency", func(t *testing.T) {
|
||||
doc := Document{Metadata: nil}
|
||||
tags := []string{"test"}
|
||||
updated := composedLens.Set(O.Some(&tags))(doc)
|
||||
retrieved := composedLens.Get(updated)
|
||||
assert.True(t, O.IsSome(retrieved))
|
||||
})
|
||||
|
||||
t.Run("Default values are used when creating structure", func(t *testing.T) {
|
||||
doc := Document{Metadata: nil}
|
||||
tags := []string{"new"}
|
||||
updated := composedLens.Set(O.Some(&tags))(doc)
|
||||
// Metadata should be created with default (Tags: nil initially, then set)
|
||||
assert.NotNil(t, updated.Metadata)
|
||||
assert.NotNil(t, updated.Metadata.Tags)
|
||||
assert.Equal(t, []string{"new"}, *updated.Metadata.Tags)
|
||||
})
|
||||
}
|
||||
@@ -18,30 +18,38 @@ func fromPredicate[GET ~func(S) Option[A], SET ~func(Option[A]) Endomorphism[S],
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicate[S, A any](pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] {
|
||||
return fromPredicate(lens.MakeLensCurried[func(S) Option[A], func(Option[A]) Endomorphism[S]], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicateRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the nil value will be set instead
|
||||
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) Lens[*S, Option[A]] {
|
||||
//
|
||||
//go:inline
|
||||
func FromPredicateRef[S, A any](pred func(A) bool, nilValue A) func(sa Lens[*S, A]) LensO[*S, A] {
|
||||
return fromPredicate(lens.MakeLensRefCurried[S, Option[A]], pred, nilValue)
|
||||
}
|
||||
|
||||
// FromPredicate returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillable[S, A any](sa Lens[S, *A]) Lens[S, Option[*A]] {
|
||||
//
|
||||
//go:inline
|
||||
func FromNillable[S, A any](sa Lens[S, *A]) LensO[S, *A] {
|
||||
return FromPredicate[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// FromNillableRef returns a `Lens` for a property accessibly as a getter and setter that can be optional
|
||||
// if the optional value is set then the `nil` value will be set instead
|
||||
func FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, Option[*A]] {
|
||||
//
|
||||
//go:inline
|
||||
func FromNillableRef[S, A any](sa Lens[*S, *A]) LensO[*S, *A] {
|
||||
return FromPredicateRef[S](F.IsNonNil[A], nil)(sa)
|
||||
}
|
||||
|
||||
// fromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func fromNullableProp[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
func fromNullableProp[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], isNullable O.Kleisli[A, A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
orElse := O.GetOrElse(F.Constant(defaultValue))
|
||||
return func(sa Lens[S, A]) Lens[S, A] {
|
||||
return creator(F.Flow3(
|
||||
@@ -53,17 +61,21 @@ func fromNullableProp[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](cr
|
||||
}
|
||||
|
||||
// FromNullableProp returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullableProp[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[S, A]) Lens[S, A] {
|
||||
//
|
||||
//go:inline
|
||||
func FromNullableProp[S, A any](isNullable O.Kleisli[A, A], defaultValue A) lens.Operator[S, A, A] {
|
||||
return fromNullableProp(lens.MakeLensCurried[func(S) A, func(A) Endomorphism[S]], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// FromNullablePropRef returns a `Lens` from a property that may be optional. The getter returns a default value for these items
|
||||
func FromNullablePropRef[S, A any](isNullable func(A) Option[A], defaultValue A) func(sa Lens[*S, A]) Lens[*S, A] {
|
||||
//
|
||||
//go:inline
|
||||
func FromNullablePropRef[S, A any](isNullable O.Kleisli[A, A], defaultValue A) lens.Operator[*S, A, A] {
|
||||
return fromNullableProp(lens.MakeLensRefCurried[S, A], isNullable, defaultValue)
|
||||
}
|
||||
|
||||
// fromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func fromOption[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
|
||||
func fromOption[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator func(get GET, set SET) Lens[S, A], defaultValue A) func(LensO[S, A]) Lens[S, A] {
|
||||
orElse := O.GetOrElse(F.Constant(defaultValue))
|
||||
return func(sa LensO[S, A]) Lens[S, A] {
|
||||
return creator(F.Flow2(
|
||||
@@ -74,7 +86,9 @@ func fromOption[GET ~func(S) A, SET ~func(A) Endomorphism[S], S, A any](creator
|
||||
}
|
||||
|
||||
// FromOption returns a `Lens` from an option property. The getter returns a default value the setter will always set the some option
|
||||
func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
|
||||
//
|
||||
//go:inline
|
||||
func FromOption[S, A any](defaultValue A) func(LensO[S, A]) Lens[S, A] {
|
||||
return fromOption(lens.MakeLensCurried[func(S) A, func(A) Endomorphism[S]], defaultValue)
|
||||
}
|
||||
|
||||
@@ -93,7 +107,9 @@ func FromOption[S, A any](defaultValue A) func(sa LensO[S, A]) Lens[S, A] {
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[*S, Option[A]] and returns a Lens[*S, A]
|
||||
func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, Option[A]]) Lens[*S, A] {
|
||||
//
|
||||
//go:inline
|
||||
func FromOptionRef[S, A any](defaultValue A) func(LensO[*S, A]) Lens[*S, A] {
|
||||
return fromOption(lens.MakeLensRefCurried[S, A], defaultValue)
|
||||
}
|
||||
|
||||
@@ -159,6 +175,8 @@ func FromOptionRef[S, A any](defaultValue A) func(sa Lens[*S, Option[A]]) Lens[*
|
||||
// - FromPredicate: For predicate-based optional conversion
|
||||
// - FromNillable: For pointer-based optional conversion
|
||||
// - FromOption: For converting from optional to non-optional with defaults
|
||||
//
|
||||
//go:inline
|
||||
func FromIso[S, A any](iso Iso[A, Option[A]]) func(Lens[S, A]) LensO[S, A] {
|
||||
return LI.Compose[S](iso)
|
||||
}
|
||||
|
||||
@@ -477,5 +477,3 @@ func TestFromIsoMultipleFields(t *testing.T) {
|
||||
assert.Equal(t, 3, updated.retries)
|
||||
})
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
package option
|
||||
|
||||
import (
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
LG "github.com/IBM/fp-go/v2/optics/lens/generic"
|
||||
T "github.com/IBM/fp-go/v2/optics/traversal/option"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
@@ -60,6 +59,6 @@ import (
|
||||
// // Now can use traversal operations
|
||||
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
|
||||
// // Apply operations across all configs using the traversal
|
||||
func AsTraversal[S, A any]() func(L.Lens[S, A]) T.Traversal[S, A] {
|
||||
func AsTraversal[S, A any]() func(Lens[S, A]) T.Traversal[S, A] {
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/IBM/fp-go/v2/optics/iso"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -93,5 +94,8 @@ type (
|
||||
// // optLens is a LensO[*Config, *int]
|
||||
LensO[S, A any] = Lens[S, Option[A]]
|
||||
|
||||
Kleisli[S, A, B any] = reader.Reader[A, LensO[S, B]]
|
||||
Operator[S, A, B any] = Kleisli[S, LensO[S, A], B]
|
||||
|
||||
Iso[S, A any] = iso.Iso[S, A]
|
||||
)
|
||||
|
||||
@@ -475,5 +475,3 @@ Predicate-Based:
|
||||
- github.com/IBM/fp-go/v2/endomorphism: Endomorphisms (A → A functions)
|
||||
*/
|
||||
package optional
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -491,5 +491,3 @@ Each specialized package provides optimized implementations for its data type.
|
||||
- github.com/IBM/fp-go/v2/monoid: Monoid type class
|
||||
*/
|
||||
package traversal
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -28,60 +28,70 @@ import "github.com/IBM/fp-go/v2/either"
|
||||
// }
|
||||
// variadicSum := either.Variadic0(sum)
|
||||
// result := variadicSum(1, 2, 3) // Right(6)
|
||||
//
|
||||
//go:inline
|
||||
func Variadic0[V, R any](f func([]V) (R, error)) func(...V) Result[R] {
|
||||
return either.Variadic0(f)
|
||||
}
|
||||
|
||||
// Variadic1 converts a function with 1 fixed parameter and a slice into a variadic function returning Either.
|
||||
//
|
||||
//go:inline
|
||||
func Variadic1[T1, V, R any](f func(T1, []V) (R, error)) func(T1, ...V) Result[R] {
|
||||
return either.Variadic1(f)
|
||||
}
|
||||
|
||||
// Variadic2 converts a function with 2 fixed parameters and a slice into a variadic function returning Either.
|
||||
//
|
||||
//go:inline
|
||||
func Variadic2[T1, T2, V, R any](f func(T1, T2, []V) (R, error)) func(T1, T2, ...V) Result[R] {
|
||||
return either.Variadic2(f)
|
||||
}
|
||||
|
||||
// Variadic3 converts a function with 3 fixed parameters and a slice into a variadic function returning Either.
|
||||
//
|
||||
//go:inline
|
||||
func Variadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, []V) (R, error)) func(T1, T2, T3, ...V) Result[R] {
|
||||
return either.Variadic3(f)
|
||||
}
|
||||
|
||||
// Variadic4 converts a function with 4 fixed parameters and a slice into a variadic function returning Either.
|
||||
//
|
||||
//go:inline
|
||||
func Variadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, []V) (R, error)) func(T1, T2, T3, T4, ...V) Result[R] {
|
||||
return either.Variadic4(f)
|
||||
}
|
||||
|
||||
// Unvariadic0 converts a variadic function returning (R, error) into a function taking a slice and returning Either.
|
||||
//
|
||||
//go:inline
|
||||
func Unvariadic0[V, R any](f func(...V) (R, error)) func([]V) Result[R] {
|
||||
return either.Unvariadic0(f)
|
||||
}
|
||||
|
||||
// Unvariadic1 converts a variadic function with 1 fixed parameter into a function taking a slice and returning Either.
|
||||
//
|
||||
//go:inline
|
||||
func Unvariadic1[T1, V, R any](f func(T1, ...V) (R, error)) func(T1, []V) Result[R] {
|
||||
return either.Unvariadic1(f)
|
||||
}
|
||||
|
||||
// Unvariadic2 converts a variadic function with 2 fixed parameters into a function taking a slice and returning Either.
|
||||
//
|
||||
//go:inline
|
||||
func Unvariadic2[T1, T2, V, R any](f func(T1, T2, ...V) (R, error)) func(T1, T2, []V) Result[R] {
|
||||
return either.Unvariadic2(f)
|
||||
}
|
||||
|
||||
// Unvariadic3 converts a variadic function with 3 fixed parameters into a function taking a slice and returning Either.
|
||||
//
|
||||
//go:inline
|
||||
func Unvariadic3[T1, T2, T3, V, R any](f func(T1, T2, T3, ...V) (R, error)) func(T1, T2, T3, []V) Result[R] {
|
||||
return either.Unvariadic3(f)
|
||||
}
|
||||
|
||||
// Unvariadic4 converts a variadic function with 4 fixed parameters into a function taking a slice and returning Either.
|
||||
//
|
||||
//go:inline
|
||||
func Unvariadic4[T1, T2, T3, T4, V, R any](f func(T1, T2, T3, T4, ...V) (R, error)) func(T1, T2, T3, T4, []V) Result[R] {
|
||||
return either.Unvariadic4(f)
|
||||
|
||||
@@ -2,7 +2,7 @@ package lens
|
||||
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at
|
||||
// 2025-11-13 09:35:28.5803707 +0100 CET m=+0.006324101
|
||||
// 2025-11-13 09:53:07.1489139 +0100 CET m=+0.005967001
|
||||
|
||||
import (
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
|
||||
Reference in New Issue
Block a user