mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
fix: better endo and lens
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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]]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
640
v2/optics/lens/lens_laws_test.go
Normal file
640
v2/optics/lens/lens_laws_test.go
Normal file
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user