From af271e7d107c620a18fd1b0fab46f1a22815bffe Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Wed, 12 Nov 2025 15:03:55 +0100 Subject: [PATCH] fix: better endo and lens Signed-off-by: Dr. Carsten Leue --- v2/endomorphism/curry.go | 4 +- v2/endomorphism/endo.go | 176 ++++++-- v2/endomorphism/endomorphism_test.go | 283 ++++++++++-- v2/endomorphism/types.go | 4 +- v2/optics/lens/doc.go | 2 + v2/optics/lens/lens_laws_test.go | 640 +++++++++++++++++++++++++++ v2/optics/lens/option/from.go | 44 +- 7 files changed, 1056 insertions(+), 97 deletions(-) create mode 100644 v2/optics/lens/lens_laws_test.go diff --git a/v2/endomorphism/curry.go b/v2/endomorphism/curry.go index 9be6ce6..7c5fa92 100644 --- a/v2/endomorphism/curry.go +++ b/v2/endomorphism/curry.go @@ -41,7 +41,7 @@ import ( // curriedAdd := endomorphism.Curry2(add) // addFive := curriedAdd(5) // Returns an endomorphism that adds 5 // result := addFive(10) // Returns: 15 -func Curry2[FCT ~func(T0, T1) T1, T0, T1 any](f FCT) Kleisli[T0, T1] { +func Curry2[FCT ~func(T0, T1) T1, T0, T1 any](f FCT) func(T0) Endomorphism[T1] { return function.Curry2(f) } @@ -68,6 +68,6 @@ func Curry2[FCT ~func(T0, T1) T1, T0, T1 any](f FCT) Kleisli[T0, T1] { // curriedCombine := endomorphism.Curry3(combine) // addTen := curriedCombine(5)(5) // Returns an endomorphism that adds 10 // result := addTen(20) // Returns: 30 -func Curry3[FCT ~func(T0, T1, T2) T2, T0, T1, T2 any](f FCT) func(T0) Kleisli[T1, T2] { +func Curry3[FCT ~func(T0, T1, T2) T2, T0, T1, T2 any](f FCT) func(T0) func(T1) Endomorphism[T2] { return function.Curry3(f) } diff --git a/v2/endomorphism/endo.go b/v2/endomorphism/endo.go index 2f776f1..8275d01 100644 --- a/v2/endomorphism/endo.go +++ b/v2/endomorphism/endo.go @@ -17,47 +17,58 @@ package endomorphism import ( "github.com/IBM/fp-go/v2/function" - "github.com/IBM/fp-go/v2/identity" ) -// MonadAp applies an endomorphism to a value in a monadic context. +// MonadAp applies an endomorphism in a function to an endomorphism value. // -// This function applies the endomorphism fab to the value fa, returning the result. -// It's the monadic application operation for endomorphisms. +// For endomorphisms, Ap composes two endomorphisms using RIGHT-TO-LEFT composition. +// This is the applicative functor operation for endomorphisms. +// +// IMPORTANT: Execution order is RIGHT-TO-LEFT (same as MonadCompose): +// - fa is applied first to the input +// - fab is applied to the result // // Parameters: -// - fab: An endomorphism to apply -// - fa: The value to apply the endomorphism to +// - fab: An endomorphism to apply (outer function) +// - fa: An endomorphism to apply first (inner function) // // Returns: -// - The result of applying fab to fa +// - A new endomorphism that applies fa, then fab // // Example: // // double := func(x int) int { return x * 2 } -// result := endomorphism.MonadAp(double, 5) // Returns: 10 -func MonadAp[A any](fab Endomorphism[A], fa A) A { - return identity.MonadAp(fab, fa) +// increment := func(x int) int { return x + 1 } +// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment +// // result(5) = double(increment(5)) = double(6) = 12 +func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] { + return MonadCompose(fab, fa) } -// Ap returns a function that applies a value to an endomorphism. +// Ap returns a function that applies an endomorphism to another endomorphism. // -// This is the curried version of MonadAp. It takes a value and returns a function -// that applies that value to any endomorphism. +// This is the curried version of MonadAp. It takes an endomorphism fa and returns +// a function that composes any endomorphism with fa using RIGHT-TO-LEFT composition. +// +// IMPORTANT: Execution order is RIGHT-TO-LEFT: +// - fa is applied first to the input +// - The endomorphism passed to the returned function is applied to the result // // Parameters: -// - fa: The value to be applied +// - fa: The first endomorphism to apply (inner function) // // Returns: -// - A function that takes an endomorphism and applies fa to it +// - A function that takes an endomorphism and composes it with fa (right-to-left) // // Example: // -// applyFive := endomorphism.Ap(5) +// increment := func(x int) int { return x + 1 } +// applyIncrement := endomorphism.Ap(increment) // double := func(x int) int { return x * 2 } -// result := applyFive(double) // Returns: 10 -func Ap[A any](fa A) func(Endomorphism[A]) A { - return identity.Ap[A](fa) +// composed := applyIncrement(double) // double ∘ increment +// // composed(5) = double(increment(5)) = double(6) = 12 +func Ap[A any](fa Endomorphism[A]) Operator[A] { + return Compose(fa) } // MonadCompose composes two endomorphisms, executing them from right to left. @@ -94,6 +105,32 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] { return function.Flow2(g, f) } +// MonadMap maps an endomorphism over another endomorphism using function composition. +// +// For endomorphisms, Map is equivalent to Compose (RIGHT-TO-LEFT composition). +// This is the functor map operation for endomorphisms. +// +// IMPORTANT: Execution order is RIGHT-TO-LEFT: +// - g is applied first to the input +// - f is applied to the result +// +// Parameters: +// - f: The function to map (outer function) +// - g: The endomorphism to map over (inner function) +// +// Returns: +// - A new endomorphism that applies g, then f +// +// Example: +// +// double := func(x int) int { return x * 2 } +// increment := func(x int) int { return x + 1 } +// mapped := endomorphism.MonadMap(double, increment) +// // mapped(5) = double(increment(5)) = double(6) = 12 +func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] { + return MonadCompose(f, g) +} + // Compose returns a function that composes an endomorphism with another, executing right to left. // // This is the curried version of MonadCompose. It takes an endomorphism g and returns @@ -126,24 +163,52 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] { // chainWithIncrement := endomorphism.Chain(increment) // chained := chainWithIncrement(double) // result2 := chained(5) // (5 * 2) + 1 = 11 -func Compose[A any](g Endomorphism[A]) Endomorphism[Endomorphism[A]] { +func Compose[A any](g Endomorphism[A]) Operator[A] { return function.Bind2nd(MonadCompose, g) } +// Map returns a function that maps an endomorphism over another endomorphism. +// +// This is the curried version of MonadMap. It takes an endomorphism f and returns +// a function that maps f over any endomorphism using RIGHT-TO-LEFT composition. +// +// IMPORTANT: Execution order is RIGHT-TO-LEFT (same as Compose): +// - The endomorphism passed to the returned function is applied first +// - f is applied to the result +// +// For endomorphisms, Map is equivalent to Compose. +// +// Parameters: +// - f: The function to map (outer function) +// +// Returns: +// - A function that takes an endomorphism and maps f over it (right-to-left) +// +// Example: +// +// double := func(x int) int { return x * 2 } +// mapDouble := endomorphism.Map(double) +// increment := func(x int) int { return x + 1 } +// mapped := mapDouble(increment) +// // mapped(5) = double(increment(5)) = double(6) = 12 +func Map[A any](f Endomorphism[A]) Operator[A] { + return Compose(f) +} + // MonadChain chains two endomorphisms together, executing them from left to right. // -// This is the monadic bind operation for endomorphisms. It composes two endomorphisms -// ma and f, returning a new endomorphism that applies ma first, then f. +// This is the monadic bind operation for endomorphisms. For endomorphisms, bind is +// simply left-to-right function composition: ma is applied first, then f. // // IMPORTANT: The execution order is LEFT-TO-RIGHT: -// - f is applied first to the input -// - g is applied to the result of ma +// - ma is applied first to the input +// - f is applied to the result of ma // -// This is different from Compose which executes RIGHT-TO-LEFT. +// This is different from MonadCompose which executes RIGHT-TO-LEFT. // // Parameters: -// - f: The first endomorphism to apply -// - g: The second endomorphism to apply +// - ma: The first endomorphism to apply +// - f: The second endomorphism to apply // // Returns: // - A new endomorphism that applies ma, then f @@ -157,11 +222,58 @@ func Compose[A any](g Endomorphism[A]) Endomorphism[Endomorphism[A]] { // chained := endomorphism.MonadChain(double, increment) // result := chained(5) // (5 * 2) + 1 = 11 // -// // Compare with Compose which executes RIGHT-TO-LEFT: -// composed := endomorphism.Compose(increment, double) +// // Compare with MonadCompose which executes RIGHT-TO-LEFT: +// composed := endomorphism.MonadCompose(increment, double) // result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order) -func MonadChain[A any](f Endomorphism[A], g Endomorphism[A]) Endomorphism[A] { - return function.Flow2(f, g) +func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] { + return function.Flow2(ma, f) +} + +// MonadChainFirst chains two endomorphisms but returns the result of the first. +// +// This applies ma first, then f, but discards the result of f and returns the result of ma. +// Useful for performing side-effects while preserving the original value. +// +// Parameters: +// - ma: The endomorphism whose result to keep +// - f: The endomorphism to apply for its effect +// +// Returns: +// - A new endomorphism that applies both but returns ma's result +// +// Example: +// +// double := func(x int) int { return x * 2 } +// log := func(x int) int { fmt.Println(x); return x } +// chained := endomorphism.MonadChainFirst(double, log) +// result := chained(5) // Prints 10, returns 10 +func MonadChainFirst[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] { + return func(a A) A { + result := ma(a) + f(result) // Apply f for its effect + return result // But return ma's result + } +} + +// ChainFirst returns a function that chains for effect but preserves the original result. +// +// This is the curried version of MonadChainFirst. +// +// Parameters: +// - f: The endomorphism to apply for its effect +// +// Returns: +// - A function that takes an endomorphism and chains it with f, keeping the first result +// +// Example: +// +// log := func(x int) int { fmt.Println(x); return x } +// chainLog := endomorphism.ChainFirst(log) +// double := func(x int) int { return x * 2 } +// chained := chainLog(double) +// result := chained(5) // Prints 10, returns 10 +func ChainFirst[A any](f Endomorphism[A]) Operator[A] { + return function.Bind2nd(MonadChainFirst, f) } // Chain returns a function that chains an endomorphism with another, executing left to right. @@ -189,6 +301,6 @@ func MonadChain[A any](f Endomorphism[A], g Endomorphism[A]) Endomorphism[A] { // // Chains double (first) with increment (second) // chained := chainWithIncrement(double) // result := chained(5) // (5 * 2) + 1 = 11 -func Chain[A any](f Endomorphism[A]) Endomorphism[Endomorphism[A]] { +func Chain[A any](f Endomorphism[A]) Operator[A] { return function.Bind2nd(MonadChain, f) } diff --git a/v2/endomorphism/endomorphism_test.go b/v2/endomorphism/endomorphism_test.go index ccb8f17..78abd04 100644 --- a/v2/endomorphism/endomorphism_test.go +++ b/v2/endomorphism/endomorphism_test.go @@ -76,29 +76,43 @@ func TestCurry3(t *testing.T) { // TestMonadAp tests the MonadAp function func TestMonadAp(t *testing.T) { - result := MonadAp(double, 5) - assert.Equal(t, 10, result, "MonadAp should apply endomorphism to value") + // MonadAp composes two endomorphisms (RIGHT-TO-LEFT) + // MonadAp(double, increment) means: increment first, then double + composed := MonadAp(double, increment) + result := composed(5) + assert.Equal(t, 12, result, "MonadAp should compose right-to-left: (5 + 1) * 2 = 12") - result2 := MonadAp(increment, 10) - assert.Equal(t, 11, result2, "MonadAp should work with different endomorphisms") + // Test with different order + composed2 := MonadAp(increment, double) + result2 := composed2(5) + assert.Equal(t, 11, result2, "MonadAp should compose right-to-left: (5 * 2) + 1 = 11") - result3 := MonadAp(square, 4) - assert.Equal(t, 16, result3, "MonadAp should work with square function") + // Test with square + composed3 := MonadAp(square, increment) + result3 := composed3(5) + assert.Equal(t, 36, result3, "MonadAp should compose right-to-left: (5 + 1) ^ 2 = 36") } // TestAp tests the Ap function func TestAp(t *testing.T) { - applyFive := Ap(5) + // Ap is the curried version of MonadAp + // Ap(increment) returns a function that composes with increment (RIGHT-TO-LEFT) + applyIncrement := Ap(increment) - result := applyFive(double) - assert.Equal(t, 10, result, "Ap should apply value to endomorphism") + composed := applyIncrement(double) + result := composed(5) + assert.Equal(t, 12, result, "Ap should compose right-to-left: (5 + 1) * 2 = 12") - result2 := applyFive(increment) - assert.Equal(t, 6, result2, "Ap should work with different endomorphisms") + // Test with different endomorphism + composed2 := applyIncrement(square) + result2 := composed2(5) + assert.Equal(t, 36, result2, "Ap should compose right-to-left: (5 + 1) ^ 2 = 36") - applyTen := Ap(10) - result3 := applyTen(square) - assert.Equal(t, 100, result3, "Ap should work with different values") + // Test with different base endomorphism + applyDouble := Ap(double) + composed3 := applyDouble(increment) + result3 := composed3(5) + assert.Equal(t, 11, result3, "Ap should compose right-to-left: (5 * 2) + 1 = 11") } // TestMonadCompose tests the MonadCompose function @@ -409,30 +423,25 @@ func TestComplexCompositions(t *testing.T) { // TestOperatorType tests the Operator type func TestOperatorType(t *testing.T) { - // Create an operator that lifts an int endomorphism to work on the length of strings - lengthOperator := func(f Endomorphism[int]) Endomorphism[string] { - return func(s string) string { - newLen := f(len(s)) - if newLen > len(s) { - // Pad with spaces - for i := len(s); i < newLen; i++ { - s += " " - } - } else if newLen < len(s) { - // Truncate - s = s[:newLen] - } - return s + // Create an operator that transforms int endomorphisms + // This operator takes an endomorphism and returns a new one that applies it twice + applyTwice := func(f Endomorphism[int]) Endomorphism[int] { + return func(x int) int { + return f(f(x)) } } // Use the operator - var op Operator[int, string] = lengthOperator - doubleLength := op(double) + var op Operator[int] = applyTwice + doubleDouble := op(double) - result := doubleLength("hello") // len("hello") = 5, 5 * 2 = 10 - assert.Equal(t, 10, len(result), "Operator should transform endomorphisms correctly") - assert.Equal(t, "hello ", result, "Operator should pad string correctly") + result := doubleDouble(5) // double(double(5)) = double(10) = 20 + assert.Equal(t, 20, result, "Operator should transform endomorphisms correctly") + + // Test with increment + incrementTwice := op(increment) + result2 := incrementTwice(5) // increment(increment(5)) = increment(6) = 7 + assert.Equal(t, 7, result2, "Operator should work with different endomorphisms") } // BenchmarkCompose benchmarks the Compose function @@ -504,3 +513,211 @@ func BenchmarkChain(b *testing.B) { _ = chained(5) } } + +// TestFunctorLaws tests that endomorphisms satisfy the functor laws +func TestFunctorLaws(t *testing.T) { + // Functor Law 1: Identity + // map(id) = id + t.Run("Identity", func(t *testing.T) { + id := Identity[int]() + endo := double + + // map(id)(endo) should equal endo + mapped := MonadMap(id, endo) + testValue := 5 + assert.Equal(t, endo(testValue), mapped(testValue), "map(id) should equal id") + }) + + // Functor Law 2: Composition + // map(f . g) = map(f) . map(g) + t.Run("Composition", func(t *testing.T) { + f := double + g := increment + endo := square + + // Left side: map(f . g)(endo) + composed := MonadCompose(f, g) + left := MonadMap(composed, endo) + + // Right side: map(f)(map(g)(endo)) + mappedG := MonadMap(g, endo) + right := MonadMap(f, mappedG) + + testValue := 3 + assert.Equal(t, left(testValue), right(testValue), "map(f . g) should equal map(f) . map(g)") + }) +} + +// TestApplicativeLaws tests that endomorphisms satisfy the applicative functor laws +func TestApplicativeLaws(t *testing.T) { + // Applicative Law 1: Identity + // ap(id, v) = v + t.Run("Identity", func(t *testing.T) { + id := Identity[int]() + v := double + + applied := MonadAp(id, v) + testValue := 5 + assert.Equal(t, v(testValue), applied(testValue), "ap(id, v) should equal v") + }) + + // Applicative Law 2: Composition + // ap(ap(ap(compose, u), v), w) = ap(u, ap(v, w)) + t.Run("Composition", func(t *testing.T) { + u := double + v := increment + w := square + + // For endomorphisms, ap is just composition + // Left side: ap(ap(ap(compose, u), v), w) = compose(compose(u, v), w) + left := MonadCompose(MonadCompose(u, v), w) + + // Right side: ap(u, ap(v, w)) = compose(u, compose(v, w)) + right := MonadCompose(u, MonadCompose(v, w)) + + testValue := 3 + assert.Equal(t, left(testValue), right(testValue), "Applicative composition law") + }) + + // Applicative Law 3: Homomorphism + // ap(pure(f), pure(x)) = pure(f(x)) + t.Run("Homomorphism", func(t *testing.T) { + // For endomorphisms, "pure" is just the identity function that returns a constant + // This law is trivially satisfied for endomorphisms + f := double + x := 5 + + // ap(f, id) applied to x should equal f(x) + id := Identity[int]() + applied := MonadAp(f, id) + assert.Equal(t, f(x), applied(x), "Homomorphism law") + }) +} + +// TestMonadLaws tests that endomorphisms satisfy the monad laws +func TestMonadLaws(t *testing.T) { + // Monad Law 1: Left Identity + // chain(pure(a), f) = f(a) + t.Run("LeftIdentity", func(t *testing.T) { + // For endomorphisms, "pure" is the identity function + // chain(id, f) = f + id := Identity[int]() + f := double + + chained := MonadChain(id, f) + testValue := 5 + assert.Equal(t, f(testValue), chained(testValue), "chain(id, f) should equal f") + }) + + // Monad Law 2: Right Identity + // chain(m, pure) = m + t.Run("RightIdentity", func(t *testing.T) { + m := double + id := Identity[int]() + + chained := MonadChain(m, id) + testValue := 5 + assert.Equal(t, m(testValue), chained(testValue), "chain(m, id) should equal m") + }) + + // Monad Law 3: Associativity + // chain(chain(m, f), g) = chain(m, x => chain(f(x), g)) + t.Run("Associativity", func(t *testing.T) { + m := square + f := double + g := increment + + // Left side: chain(chain(m, f), g) + left := MonadChain(MonadChain(m, f), g) + + // Right side: chain(m, chain(f, g)) + // For simple endomorphisms (not Kleisli arrows), this simplifies to: + right := MonadChain(m, MonadChain(f, g)) + + testValue := 3 + assert.Equal(t, left(testValue), right(testValue), "Monad associativity law") + }) +} + +// TestMonadComposeVsMonadChain verifies the relationship between Compose and Chain +func TestMonadComposeVsMonadChain(t *testing.T) { + f := double + g := increment + + // MonadCompose(f, g) should equal MonadChain(g, f) + // Because Compose is right-to-left and Chain is left-to-right + composed := MonadCompose(f, g) + chained := MonadChain(g, f) + + testValue := 5 + assert.Equal(t, composed(testValue), chained(testValue), + "MonadCompose(f, g) should equal MonadChain(g, f)") +} + +// TestMapEqualsCompose verifies that Map is equivalent to Compose for endomorphisms +func TestMapEqualsCompose(t *testing.T) { + f := double + g := increment + + // MonadMap(f, g) should equal MonadCompose(f, g) + mapped := MonadMap(f, g) + composed := MonadCompose(f, g) + + testValue := 5 + assert.Equal(t, composed(testValue), mapped(testValue), + "MonadMap should equal MonadCompose for endomorphisms") + + // Curried versions + mapF := Map(f) + composeF := Compose(f) + + mappedG := mapF(g) + composedG := composeF(g) + + assert.Equal(t, composedG(testValue), mappedG(testValue), + "Map should equal Compose for endomorphisms (curried)") +} + +// TestApEqualsCompose verifies that Ap is equivalent to Compose for endomorphisms +func TestApEqualsCompose(t *testing.T) { + f := double + g := increment + + // MonadAp(f, g) should equal MonadCompose(f, g) + applied := MonadAp(f, g) + composed := MonadCompose(f, g) + + testValue := 5 + assert.Equal(t, composed(testValue), applied(testValue), + "MonadAp should equal MonadCompose for endomorphisms") + + // Curried versions + apG := Ap(g) + composeG := Compose(g) + + appliedF := apG(f) + composedF := composeG(f) + + assert.Equal(t, composedF(testValue), appliedF(testValue), + "Ap should equal Compose for endomorphisms (curried)") +} + +// TestChainFirst tests the ChainFirst operation +func TestChainFirst(t *testing.T) { + double := func(x int) int { return x * 2 } + + // Track side effect + var sideEffect int + logEffect := func(x int) int { + sideEffect = x + return x + 100 // This result should be discarded + } + + chained := MonadChainFirst(double, logEffect) + result := chained(5) + + // Should return double's result (10), not logEffect's result + assert.Equal(t, 10, result, "ChainFirst should return first result") + // But side effect should have been executed with double's result + assert.Equal(t, 10, sideEffect, "ChainFirst should execute second function for effect") +} diff --git a/v2/endomorphism/types.go b/v2/endomorphism/types.go index e0c2f26..a0f99c2 100644 --- a/v2/endomorphism/types.go +++ b/v2/endomorphism/types.go @@ -37,7 +37,7 @@ type ( // var g endomorphism.Endomorphism[int] = increment Endomorphism[A any] = func(A) A - Kleisli[A, B any] = func(A) Endomorphism[B] + Kleisli[A any] = func(A) Endomorphism[A] // Operator represents a transformation from one endomorphism to another. // @@ -54,5 +54,5 @@ type ( // return strconv.Itoa(result) // } // } - Operator[A, B any] = Kleisli[Endomorphism[A], B] + Operator[A any] = Endomorphism[Endomorphism[A]] ) diff --git a/v2/optics/lens/doc.go b/v2/optics/lens/doc.go index ed870a5..5c1c24d 100644 --- a/v2/optics/lens/doc.go +++ b/v2/optics/lens/doc.go @@ -453,6 +453,8 @@ Core Lens Creation: - MakeLensCurried: Create a lens with curried setter - MakeLensRef: Create a lens for pointer-based structures - MakeLensRefCurried: Create a lens for pointers with curried setter + - MakeLensWithEq: Create a lens with equality optimization for pointer structures + - MakeLensStrict: Create a lens with strict equality optimization for pointer structures - Id: Create an identity lens - IdRef: Create an identity lens for pointers diff --git a/v2/optics/lens/lens_laws_test.go b/v2/optics/lens/lens_laws_test.go new file mode 100644 index 0000000..b18b882 --- /dev/null +++ b/v2/optics/lens/lens_laws_test.go @@ -0,0 +1,640 @@ +// 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 lens + +import ( + "testing" + + EQ "github.com/IBM/fp-go/v2/eq" + F "github.com/IBM/fp-go/v2/function" + "github.com/stretchr/testify/assert" +) + +// TestModify tests the Modify function +func TestModify(t *testing.T) { + type Counter struct { + Value int + } + + valueLens := MakeLens( + func(c Counter) int { return c.Value }, + func(c Counter, v int) Counter { + c.Value = v + return c + }, + ) + + counter := Counter{Value: 5} + + // Test increment + increment := func(v int) int { return v + 1 } + modifyIncrement := Modify[Counter](increment)(valueLens) + incremented := modifyIncrement(counter) + assert.Equal(t, 6, incremented.Value) + assert.Equal(t, 5, counter.Value) // Original unchanged + + // Test double + double := func(v int) int { return v * 2 } + modifyDouble := Modify[Counter](double)(valueLens) + doubled := modifyDouble(counter) + assert.Equal(t, 10, doubled.Value) + assert.Equal(t, 5, counter.Value) // Original unchanged + + // Test identity (no change) + identity := func(v int) int { return v } + modifyIdentity := Modify[Counter](identity)(valueLens) + unchanged := modifyIdentity(counter) + assert.Equal(t, counter, unchanged) +} + +func TestModifyRef(t *testing.T) { + valueLens := MakeLensRef( + func(s *Street) int { return s.num }, + func(s *Street, num int) *Street { + s.num = num + return s + }, + ) + + street := &Street{num: 10, name: "Main"} + + // Test increment + increment := func(v int) int { return v + 1 } + modifyIncrement := Modify[*Street](increment)(valueLens) + incremented := modifyIncrement(street) + assert.Equal(t, 11, incremented.num) + assert.Equal(t, 10, street.num) // Original unchanged +} + +// Lens Laws Tests + +func TestMakeLensLaws(t *testing.T) { + nameLens := MakeLens( + func(s Street) string { return s.name }, + func(s Street, name string) Street { + s.name = name + return s + }, + ) + + street := Street{num: 1, name: "Main"} + newName := "Oak" + + // Law 1: GetSet - lens.Set(lens.Get(s))(s) == s + t.Run("GetSet", func(t *testing.T) { + result := nameLens.Set(nameLens.Get(street))(street) + assert.Equal(t, street, result) + }) + + // Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a + t.Run("SetGet", func(t *testing.T) { + result := nameLens.Get(nameLens.Set(newName)(street)) + assert.Equal(t, newName, result) + }) + + // Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s) + t.Run("SetSet", func(t *testing.T) { + result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street)) + result2 := nameLens.Set("Elm")(street) + assert.Equal(t, result2, result1) + }) +} + +func TestMakeLensRefLaws(t *testing.T) { + nameLens := MakeLensRef( + (*Street).GetName, + (*Street).SetName, + ) + + street := &Street{num: 1, name: "Main"} + newName := "Oak" + + // Law 1: GetSet - lens.Set(lens.Get(s))(s) == s + t.Run("GetSet", func(t *testing.T) { + result := nameLens.Set(nameLens.Get(street))(street) + assert.Equal(t, street.name, result.name) + assert.Equal(t, street.num, result.num) + }) + + // Law 2: SetGet - lens.Get(lens.Set(a)(s)) == a + t.Run("SetGet", func(t *testing.T) { + result := nameLens.Get(nameLens.Set(newName)(street)) + assert.Equal(t, newName, result) + }) + + // Law 3: SetSet - lens.Set(a2)(lens.Set(a1)(s)) == lens.Set(a2)(s) + t.Run("SetSet", func(t *testing.T) { + result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street)) + result2 := nameLens.Set("Elm")(street) + assert.Equal(t, result2.name, result1.name) + assert.Equal(t, result2.num, result1.num) + }) +} + +func TestMakeLensCurriedLaws(t *testing.T) { + nameLens := MakeLensCurried( + func(s Street) string { return s.name }, + func(name string) func(Street) Street { + return func(s Street) Street { + s.name = name + return s + } + }, + ) + + street := Street{num: 1, name: "Main"} + newName := "Oak" + + // Law 1: GetSet + t.Run("GetSet", func(t *testing.T) { + result := nameLens.Set(nameLens.Get(street))(street) + assert.Equal(t, street, result) + }) + + // Law 2: SetGet + t.Run("SetGet", func(t *testing.T) { + result := nameLens.Get(nameLens.Set(newName)(street)) + assert.Equal(t, newName, result) + }) + + // Law 3: SetSet + t.Run("SetSet", func(t *testing.T) { + result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street)) + result2 := nameLens.Set("Elm")(street) + assert.Equal(t, result2, result1) + }) +} + +func TestMakeLensRefCurriedLaws(t *testing.T) { + nameLens := MakeLensRefCurried( + func(s *Street) string { return s.name }, + func(name string) func(*Street) *Street { + return func(s *Street) *Street { + s.name = name + return s + } + }, + ) + + street := &Street{num: 1, name: "Main"} + newName := "Oak" + + // Law 1: GetSet + t.Run("GetSet", func(t *testing.T) { + result := nameLens.Set(nameLens.Get(street))(street) + assert.Equal(t, street.name, result.name) + assert.Equal(t, street.num, result.num) + }) + + // Law 2: SetGet + t.Run("SetGet", func(t *testing.T) { + result := nameLens.Get(nameLens.Set(newName)(street)) + assert.Equal(t, newName, result) + }) + + // Law 3: SetSet + t.Run("SetSet", func(t *testing.T) { + result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street)) + result2 := nameLens.Set("Elm")(street) + assert.Equal(t, result2.name, result1.name) + assert.Equal(t, result2.num, result1.num) + }) +} + +func TestMakeLensWithEqLaws(t *testing.T) { + nameLens := MakeLensWithEq( + EQ.FromStrictEquals[string](), + func(s *Street) string { return s.name }, + func(s *Street, name string) *Street { + s.name = name + return s + }, + ) + + street := &Street{num: 1, name: "Main"} + newName := "Oak" + + // Law 1: GetSet + t.Run("GetSet", func(t *testing.T) { + result := nameLens.Set(nameLens.Get(street))(street) + assert.Equal(t, street.name, result.name) + assert.Equal(t, street.num, result.num) + // With Eq optimization, should return same pointer + assert.Same(t, street, result) + }) + + // Law 2: SetGet + t.Run("SetGet", func(t *testing.T) { + result := nameLens.Get(nameLens.Set(newName)(street)) + assert.Equal(t, newName, result) + }) + + // Law 3: SetSet + t.Run("SetSet", func(t *testing.T) { + result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street)) + result2 := nameLens.Set("Elm")(street) + assert.Equal(t, result2.name, result1.name) + assert.Equal(t, result2.num, result1.num) + }) +} + +func TestMakeLensStrictLaws(t *testing.T) { + nameLens := MakeLensStrict( + func(s *Street) string { return s.name }, + func(s *Street, name string) *Street { + s.name = name + return s + }, + ) + + street := &Street{num: 1, name: "Main"} + newName := "Oak" + + // Law 1: GetSet + t.Run("GetSet", func(t *testing.T) { + result := nameLens.Set(nameLens.Get(street))(street) + assert.Equal(t, street.name, result.name) + assert.Equal(t, street.num, result.num) + // With strict equality optimization, should return same pointer + assert.Same(t, street, result) + }) + + // Law 2: SetGet + t.Run("SetGet", func(t *testing.T) { + result := nameLens.Get(nameLens.Set(newName)(street)) + assert.Equal(t, newName, result) + }) + + // Law 3: SetSet + t.Run("SetSet", func(t *testing.T) { + result1 := nameLens.Set("Elm")(nameLens.Set(newName)(street)) + result2 := nameLens.Set("Elm")(street) + assert.Equal(t, result2.name, result1.name) + assert.Equal(t, result2.num, result1.num) + }) +} + +func TestIdLaws(t *testing.T) { + idLens := Id[Street]() + street := Street{num: 1, name: "Main"} + newStreet := Street{num: 2, name: "Oak"} + + // Law 1: GetSet + t.Run("GetSet", func(t *testing.T) { + result := idLens.Set(idLens.Get(street))(street) + assert.Equal(t, street, result) + }) + + // Law 2: SetGet + t.Run("SetGet", func(t *testing.T) { + result := idLens.Get(idLens.Set(newStreet)(street)) + assert.Equal(t, newStreet, result) + }) + + // Law 3: SetSet + t.Run("SetSet", func(t *testing.T) { + anotherStreet := Street{num: 3, name: "Elm"} + result1 := idLens.Set(anotherStreet)(idLens.Set(newStreet)(street)) + result2 := idLens.Set(anotherStreet)(street) + assert.Equal(t, result2, result1) + }) +} + +func TestIdRefLaws(t *testing.T) { + idLens := IdRef[Street]() + street := &Street{num: 1, name: "Main"} + newStreet := &Street{num: 2, name: "Oak"} + + // Law 1: GetSet + t.Run("GetSet", func(t *testing.T) { + result := idLens.Set(idLens.Get(street))(street) + assert.Equal(t, street.name, result.name) + assert.Equal(t, street.num, result.num) + }) + + // Law 2: SetGet + t.Run("SetGet", func(t *testing.T) { + result := idLens.Get(idLens.Set(newStreet)(street)) + assert.Equal(t, newStreet, result) + }) + + // Law 3: SetSet + t.Run("SetSet", func(t *testing.T) { + anotherStreet := &Street{num: 3, name: "Elm"} + result1 := idLens.Set(anotherStreet)(idLens.Set(newStreet)(street)) + result2 := idLens.Set(anotherStreet)(street) + assert.Equal(t, result2, result1) + }) +} + +func TestComposeLaws(t *testing.T) { + streetLens := MakeLensRef((*Street).GetName, (*Street).SetName) + addrLens := MakeLensRef((*Address).GetStreet, (*Address).SetStreet) + + // Compose to get street name from address + streetNameLens := Compose[*Address](streetLens)(addrLens) + + sampleStreet := Street{num: 220, name: "Schönaicherstr"} + sampleAddress := Address{city: "Böblingen", street: &sampleStreet} + newName := "Böblingerstr" + + // Law 1: GetSet + t.Run("GetSet", func(t *testing.T) { + result := streetNameLens.Set(streetNameLens.Get(&sampleAddress))(&sampleAddress) + assert.Equal(t, sampleAddress.street.name, result.street.name) + assert.Equal(t, sampleAddress.street.num, result.street.num) + }) + + // Law 2: SetGet + t.Run("SetGet", func(t *testing.T) { + result := streetNameLens.Get(streetNameLens.Set(newName)(&sampleAddress)) + assert.Equal(t, newName, result) + }) + + // Law 3: SetSet + t.Run("SetSet", func(t *testing.T) { + result1 := streetNameLens.Set("Elm St")(streetNameLens.Set(newName)(&sampleAddress)) + result2 := streetNameLens.Set("Elm St")(&sampleAddress) + assert.Equal(t, result2.street.name, result1.street.name) + }) +} + +func TestComposeRefLaws(t *testing.T) { + streetLens := MakeLensRef((*Street).GetName, (*Street).SetName) + addrLens := MakeLensRef((*Address).GetStreet, (*Address).SetStreet) + + // Compose using ComposeRef + streetNameLens := ComposeRef[Address](streetLens)(addrLens) + + sampleStreet := Street{num: 220, name: "Schönaicherstr"} + sampleAddress := Address{city: "Böblingen", street: &sampleStreet} + newName := "Böblingerstr" + + // Law 1: GetSet + t.Run("GetSet", func(t *testing.T) { + result := streetNameLens.Set(streetNameLens.Get(&sampleAddress))(&sampleAddress) + assert.Equal(t, sampleAddress.street.name, result.street.name) + assert.Equal(t, sampleAddress.street.num, result.street.num) + }) + + // Law 2: SetGet + t.Run("SetGet", func(t *testing.T) { + result := streetNameLens.Get(streetNameLens.Set(newName)(&sampleAddress)) + assert.Equal(t, newName, result) + }) + + // Law 3: SetSet + t.Run("SetSet", func(t *testing.T) { + result1 := streetNameLens.Set("Elm St")(streetNameLens.Set(newName)(&sampleAddress)) + result2 := streetNameLens.Set("Elm St")(&sampleAddress) + assert.Equal(t, result2.street.name, result1.street.name) + }) +} + +func TestIMapLaws(t *testing.T) { + type Celsius float64 + type Fahrenheit float64 + + celsiusToFahrenheit := func(c Celsius) Fahrenheit { + return Fahrenheit(c*9/5 + 32) + } + + fahrenheitToCelsius := func(f Fahrenheit) Celsius { + return Celsius((f - 32) * 5 / 9) + } + + type Weather struct { + Temperature Celsius + } + + tempCelsiusLens := MakeLens( + func(w Weather) Celsius { return w.Temperature }, + func(w Weather, t Celsius) Weather { + w.Temperature = t + return w + }, + ) + + // Create a lens that works with Fahrenheit + tempFahrenheitLens := F.Pipe1( + tempCelsiusLens, + IMap[Weather](celsiusToFahrenheit, fahrenheitToCelsius), + ) + + weather := Weather{Temperature: 20} // 20°C + newTempF := Fahrenheit(86) // 86°F (30°C) + + // Law 1: GetSet + t.Run("GetSet", func(t *testing.T) { + result := tempFahrenheitLens.Set(tempFahrenheitLens.Get(weather))(weather) + // Allow small floating point differences + assert.InDelta(t, float64(weather.Temperature), float64(result.Temperature), 0.0001) + }) + + // Law 2: SetGet + t.Run("SetGet", func(t *testing.T) { + result := tempFahrenheitLens.Get(tempFahrenheitLens.Set(newTempF)(weather)) + assert.InDelta(t, float64(newTempF), float64(result), 0.0001) + }) + + // Law 3: SetSet + t.Run("SetSet", func(t *testing.T) { + anotherTempF := Fahrenheit(95) // 95°F (35°C) + result1 := tempFahrenheitLens.Set(anotherTempF)(tempFahrenheitLens.Set(newTempF)(weather)) + result2 := tempFahrenheitLens.Set(anotherTempF)(weather) + assert.InDelta(t, float64(result2.Temperature), float64(result1.Temperature), 0.0001) + }) +} + +func TestIMapIdentity(t *testing.T) { + // IMap with identity functions should behave like the original lens + type S struct { + a int + } + + originalLens := MakeLens( + func(s S) int { return s.a }, + func(s S, a int) S { + s.a = a + return s + }, + ) + + // Apply IMap with identity functions + identityMappedLens := F.Pipe1( + originalLens, + IMap[S](F.Identity[int], F.Identity[int]), + ) + + s := S{a: 42} + + // Both lenses should behave identically + assert.Equal(t, originalLens.Get(s), identityMappedLens.Get(s)) + assert.Equal(t, originalLens.Set(100)(s), identityMappedLens.Set(100)(s)) +} + +func TestIMapComposition(t *testing.T) { + // IMap(f, g) ∘ IMap(h, k) = IMap(f ∘ h, k ∘ g) + type S struct { + value int + } + + baseLens := MakeLens( + func(s S) int { return s.value }, + func(s S, v int) S { + s.value = v + return s + }, + ) + + // First transformation: int -> float64 + intToFloat := func(i int) float64 { return float64(i) } + floatToInt := func(f float64) int { return int(f) } + + // Second transformation: float64 -> string + floatToString := func(f float64) string { return F.Pipe1(f, func(x float64) string { return "value" }) } + stringToFloat := func(s string) float64 { return 42.0 } + + // Compose IMap twice + lens1 := F.Pipe1(baseLens, IMap[S](intToFloat, floatToInt)) + lens2 := F.Pipe1(lens1, IMap[S](floatToString, stringToFloat)) + + // Direct composition + lens3 := F.Pipe1( + baseLens, + IMap[S]( + F.Flow2(intToFloat, floatToString), + F.Flow2(stringToFloat, floatToInt), + ), + ) + + s := S{value: 10} + + // Both should produce the same results + assert.Equal(t, lens2.Get(s), lens3.Get(s)) + assert.Equal(t, lens2.Set("test")(s), lens3.Set("test")(s)) +} + +func TestModifyLaws(t *testing.T) { + // Modify should satisfy: Modify(id) = id + // and: Modify(f ∘ g) = Modify(f) ∘ Modify(g) + + type S struct { + value int + } + + lens := MakeLens( + func(s S) int { return s.value }, + func(s S, v int) S { + s.value = v + return s + }, + ) + + s := S{value: 10} + + // Modify with identity should return the same value + t.Run("ModifyIdentity", func(t *testing.T) { + modifyIdentity := Modify[S](F.Identity[int])(lens) + result := modifyIdentity(s) + assert.Equal(t, s, result) + }) + + // Modify composition: Modify(f ∘ g) = Modify(f) ∘ Modify(g) + t.Run("ModifyComposition", func(t *testing.T) { + f := func(x int) int { return x * 2 } + g := func(x int) int { return x + 3 } + + // Modify(f ∘ g) + composed := F.Flow2(g, f) + modifyComposed := Modify[S](composed)(lens) + result1 := modifyComposed(s) + + // Modify(f) ∘ Modify(g) + modifyG := Modify[S](g)(lens) + intermediate := modifyG(s) + modifyF := Modify[S](f)(lens) + result2 := modifyF(intermediate) + + assert.Equal(t, result1, result2) + }) +} + +func TestComposeAssociativity(t *testing.T) { + // Test that lens composition is associative: + // (l1 ∘ l2) ∘ l3 = l1 ∘ (l2 ∘ l3) + + type Level3 struct { + value string + } + + type Level2 struct { + level3 Level3 + } + + type Level1 struct { + level2 Level2 + } + + lens12 := MakeLens( + func(l1 Level1) Level2 { return l1.level2 }, + func(l1 Level1, l2 Level2) Level1 { + l1.level2 = l2 + return l1 + }, + ) + + lens23 := MakeLens( + func(l2 Level2) Level3 { return l2.level3 }, + func(l2 Level2, l3 Level3) Level2 { + l2.level3 = l3 + return l2 + }, + ) + + lens3Value := MakeLens( + func(l3 Level3) string { return l3.value }, + func(l3 Level3, v string) Level3 { + l3.value = v + return l3 + }, + ) + + // (lens12 ∘ lens23) ∘ lens3Value + composed1 := F.Pipe2( + lens12, + Compose[Level1](lens23), + Compose[Level1](lens3Value), + ) + + // lens12 ∘ (lens23 ∘ lens3Value) + composed2 := F.Pipe1( + lens12, + Compose[Level1](F.Pipe1(lens23, Compose[Level2](lens3Value))), + ) + + l1 := Level1{ + level2: Level2{ + level3: Level3{value: "test"}, + }, + } + + // Both compositions should behave identically + 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/from.go b/v2/optics/lens/option/from.go index 5728eee..7c29709 100644 --- a/v2/optics/lens/option/from.go +++ b/v2/optics/lens/option/from.go @@ -1,7 +1,6 @@ package option import ( - EM "github.com/IBM/fp-go/v2/endomorphism" F "github.com/IBM/fp-go/v2/function" "github.com/IBM/fp-go/v2/optics/lens" O "github.com/IBM/fp-go/v2/option" @@ -9,30 +8,23 @@ import ( // 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 fromPredicate[GET ~func(S) Option[A], SET ~func(S, Option[A]) S, S, A any](creator func(get GET, set SET) LensO[S, A], pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] { +func fromPredicate[GET ~func(S) Option[A], SET ~func(Option[A]) Endomorphism[S], S, A any](creator func(get GET, set SET) LensO[S, A], pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] { fromPred := O.FromPredicate(pred) return func(sa Lens[S, A]) LensO[S, A] { - fold := O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set) - return creator(F.Flow2(sa.Get, fromPred), func(s S, a Option[A]) S { - return F.Pipe2( - a, - fold, - EM.Ap(s), - ) - }) + return creator(F.Flow2(sa.Get, fromPred), O.Fold(F.Bind1of1(sa.Set)(nilValue), sa.Set)) } } // 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 FromPredicate[S, A any](pred func(A) bool, nilValue A) func(sa Lens[S, A]) LensO[S, A] { - return fromPredicate(lens.MakeLens[func(S) Option[A], func(S, Option[A]) S], pred, nilValue) + 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]] { - return fromPredicate(lens.MakeLensRef[func(*S) Option[A], func(*S, Option[A]) *S], pred, nilValue) + 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 @@ -48,45 +40,41 @@ func FromNillableRef[S, A any](sa Lens[*S, *A]) Lens[*S, Option[*A]] { } // 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(S, A) 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 func(A) Option[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( sa.Get, isNullable, - O.GetOrElse(F.Constant(defaultValue)), - ), func(s S, a A) S { - return sa.Set(a)(s) - }, - ) + orElse, + ), sa.Set) } } // 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] { - return fromNullableProp(lens.MakeLens[func(S) A, func(S, A) S], isNullable, defaultValue) + 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] { - return fromNullableProp(lens.MakeLensRef[func(*S) A, func(*S, A) *S], isNullable, defaultValue) + 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(S, A) 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(sa 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( sa.Get, - O.GetOrElse(F.Constant(defaultValue)), - ), func(s S, a A) S { - return sa.Set(O.Some(a))(s) - }, - ) + orElse, + ), F.Flow2(O.Of[A], sa.Set)) } } // 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] { - return fromOption(lens.MakeLens[func(S) A, func(S, A) S], defaultValue) + return fromOption(lens.MakeLensCurried[func(S) A, func(A) Endomorphism[S]], defaultValue) } // FromOptionRef creates a lens from an Option property with a default value for pointer structures. @@ -105,5 +93,5 @@ 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] { - return fromOption(lens.MakeLensRef[func(*S) A, func(*S, A) *S], defaultValue) + return fromOption(lens.MakeLensRefCurried[S, A], defaultValue) }