From 3c3bb7c16625066546888e0d23de1c87aa5f6056 Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Thu, 13 Nov 2025 12:15:52 +0100 Subject: [PATCH] fix: improve lens implementation Signed-off-by: Dr. Carsten Leue --- v2/endomorphism/endo.go | 82 +++ v2/optics/iso/lens/doc.go | 2 - v2/optics/iso/lens/lens_test.go | 2 - v2/optics/iso/option/doc.go | 2 - v2/optics/lens/iso/doc.go | 2 - v2/optics/lens/lens.go | 35 +- v2/optics/lens/lens_laws_test.go | 2 - v2/optics/lens/option/compose.go | 107 ++-- v2/optics/lens/option/compose_test.go | 841 ++++++++++++++++++++++++++ v2/optics/lens/option/from.go | 36 +- v2/optics/lens/option/from_test.go | 2 - v2/optics/lens/option/option.go | 3 +- v2/optics/lens/option/types.go | 4 + v2/optics/optional/doc.go | 2 - v2/optics/traversal/doc.go | 2 - v2/result/variadic.go | 10 + v2/samples/lens/gen_lens.go | 2 +- 17 files changed, 1028 insertions(+), 108 deletions(-) create mode 100644 v2/optics/lens/option/compose_test.go diff --git a/v2/endomorphism/endo.go b/v2/endomorphism/endo.go index 8275d01..4039b31 100644 --- a/v2/endomorphism/endo.go +++ b/v2/endomorphism/endo.go @@ -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) + } +} diff --git a/v2/optics/iso/lens/doc.go b/v2/optics/iso/lens/doc.go index 6a16c6b..60462cd 100644 --- a/v2/optics/iso/lens/doc.go +++ b/v2/optics/iso/lens/doc.go @@ -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 diff --git a/v2/optics/iso/lens/lens_test.go b/v2/optics/iso/lens/lens_test.go index 974b76b..76b0fea 100644 --- a/v2/optics/iso/lens/lens_test.go +++ b/v2/optics/iso/lens/lens_test.go @@ -397,5 +397,3 @@ func TestIsoAsLensTypeConversion(t *testing.T) { assert.Equal(t, 3, len(updated)) }) } - -// Made with Bob diff --git a/v2/optics/iso/option/doc.go b/v2/optics/iso/option/doc.go index 1542f86..72bac1b 100644 --- a/v2/optics/iso/option/doc.go +++ b/v2/optics/iso/option/doc.go @@ -301,5 +301,3 @@ For more information on isomorphisms and optics: - option package documentation */ package option - -// Made with Bob diff --git a/v2/optics/lens/iso/doc.go b/v2/optics/lens/iso/doc.go index 3e90159..7735ad8 100644 --- a/v2/optics/lens/iso/doc.go +++ b/v2/optics/lens/iso/doc.go @@ -362,5 +362,3 @@ For more information on lenses and isomorphisms: - optics package overview */ package iso - -// Made with Bob diff --git a/v2/optics/lens/lens.go b/v2/optics/lens/lens.go index 77ab21e..d1f111f 100644 --- a/v2/optics/lens/lens.go +++ b/v2/optics/lens/lens.go @@ -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)) } } diff --git a/v2/optics/lens/lens_laws_test.go b/v2/optics/lens/lens_laws_test.go index b18b882..13759e7 100644 --- a/v2/optics/lens/lens_laws_test.go +++ b/v2/optics/lens/lens_laws_test.go @@ -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 diff --git a/v2/optics/lens/option/compose.go b/v2/optics/lens/option/compose.go index 1c2aca7..d5ac811 100644 --- a/v2/optics/lens/option/compose.go +++ b/v2/optics/lens/option/compose.go @@ -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, - // optB is None - func() S { - return O.MonadFold( - optA, - // optA is None - no-op - F.Constant(s), - // optA is Some - unset B in A - func(a A) S { - return setSomeA(abSetNone(a))(s) - }, - ) - }, - // optB is Some - func(b B) S { - setB := ab.Set(O.Some(b)) - return O.MonadFold( - optA, - // optA is None - create with defaultA - func() S { - return setSomeA(setB(defaultA))(s) - }, - // optA is Some - update B in A - func(a A) S { - return setSomeA(setB(a))(s) - }, - ) - }, - ) - } - }, + F.Flow2( + O.Fold( + // optB is None + lazy.Of(F.Flow2( + saGet, + O.Fold(endomorphism.Identity[S], F.Flow2(abSetNone, setSomeA)), + )), + // optB is Some + func(b B) func(S) Endomorphism[S] { + setB := ab.Set(O.Some(b)) + 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( - // optB is None - remove A entirely - F.Constant(setNoneA(s)), - // optB is Some - set B - func(b B) S { - optA := saGet(s) - return O.Fold( - // optA is None - create with defaultA - func() S { - return setSomeA(abSet(b)(defaultA))(s) - }, - // optA is Some - update B in A - func(a A) S { - return setSomeA(abSet(b)(a))(s) - }, - )(optA) - }, - )(optB) - }, + setSomeA := F.Flow2(O.Some[A], saSet) + return lens.MakeLensCurried( + F.Flow2(saGet, O.Map(abGet)), + O.Fold( + // optB is None - remove A entirely + lazy.Of(setNoneA), + // optB is Some - set B + func(b B) Endomorphism[S] { + absetB := abSet(b) + abSetA := absetB(defaultA) + return endomorphism.Join(F.Flow3( + saGet, + O.Fold(lazy.Of(abSetA), absetB), + setSomeA, + )) + }, + ), ) } } diff --git a/v2/optics/lens/option/compose_test.go b/v2/optics/lens/option/compose_test.go new file mode 100644 index 0000000..0efc1f3 --- /dev/null +++ b/v2/optics/lens/option/compose_test.go @@ -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) + }) +} diff --git a/v2/optics/lens/option/from.go b/v2/optics/lens/option/from.go index 5502da5..3d9dc5c 100644 --- a/v2/optics/lens/option/from.go +++ b/v2/optics/lens/option/from.go @@ -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) } diff --git a/v2/optics/lens/option/from_test.go b/v2/optics/lens/option/from_test.go index 941f5ce..11e0280 100644 --- a/v2/optics/lens/option/from_test.go +++ b/v2/optics/lens/option/from_test.go @@ -477,5 +477,3 @@ func TestFromIsoMultipleFields(t *testing.T) { assert.Equal(t, 3, updated.retries) }) } - -// Made with Bob diff --git a/v2/optics/lens/option/option.go b/v2/optics/lens/option/option.go index 7ef7596..a769fc9 100644 --- a/v2/optics/lens/option/option.go +++ b/v2/optics/lens/option/option.go @@ -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]) } diff --git a/v2/optics/lens/option/types.go b/v2/optics/lens/option/types.go index 3864892..bc8947d 100644 --- a/v2/optics/lens/option/types.go +++ b/v2/optics/lens/option/types.go @@ -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] ) diff --git a/v2/optics/optional/doc.go b/v2/optics/optional/doc.go index c7c9332..79edfb3 100644 --- a/v2/optics/optional/doc.go +++ b/v2/optics/optional/doc.go @@ -475,5 +475,3 @@ Predicate-Based: - github.com/IBM/fp-go/v2/endomorphism: Endomorphisms (A → A functions) */ package optional - -// Made with Bob diff --git a/v2/optics/traversal/doc.go b/v2/optics/traversal/doc.go index 9d19b9b..331752f 100644 --- a/v2/optics/traversal/doc.go +++ b/v2/optics/traversal/doc.go @@ -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 diff --git a/v2/result/variadic.go b/v2/result/variadic.go index b4d4c68..3607ed6 100644 --- a/v2/result/variadic.go +++ b/v2/result/variadic.go @@ -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) diff --git a/v2/samples/lens/gen_lens.go b/v2/samples/lens/gen_lens.go index c5b1bc6..0555f5a 100644 --- a/v2/samples/lens/gen_lens.go +++ b/v2/samples/lens/gen_lens.go @@ -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"