mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
fix: make a distinction between Chain and Compose for endomorphism
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
30
v2/README.md
30
v2/README.md
@@ -197,6 +197,36 @@ pair := MakePair(1, "hello")
|
|||||||
result := Map(func(s string) string { return s + "!" })(pair) // Pair(1, "hello!")
|
result := Map(func(s string) string { return s + "!" })(pair) // Pair(1, "hello!")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 4. Endomorphism Compose Semantics
|
||||||
|
|
||||||
|
The `Compose` function for endomorphisms now follows **mathematical function composition** (right-to-left execution), aligning with standard functional programming conventions.
|
||||||
|
|
||||||
|
**V1:**
|
||||||
|
```go
|
||||||
|
// Compose executed left-to-right
|
||||||
|
double := func(x int) int { return x * 2 }
|
||||||
|
increment := func(x int) int { return x + 1 }
|
||||||
|
composed := Compose(double, increment)
|
||||||
|
result := composed(5) // (5 * 2) + 1 = 11
|
||||||
|
```
|
||||||
|
|
||||||
|
**V2:**
|
||||||
|
```go
|
||||||
|
// Compose executes RIGHT-TO-LEFT (mathematical composition)
|
||||||
|
double := func(x int) int { return x * 2 }
|
||||||
|
increment := func(x int) int { return x + 1 }
|
||||||
|
composed := Compose(double, increment)
|
||||||
|
result := composed(5) // (5 + 1) * 2 = 12
|
||||||
|
|
||||||
|
// Use MonadChain for LEFT-TO-RIGHT execution
|
||||||
|
chained := MonadChain(double, increment)
|
||||||
|
result2 := chained(5) // (5 * 2) + 1 = 11
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Difference:**
|
||||||
|
- `Compose(f, g)` now means `f ∘ g`, which applies `g` first, then `f` (right-to-left)
|
||||||
|
- `MonadChain(f, g)` applies `f` first, then `g` (left-to-right)
|
||||||
|
|
||||||
## ✨ Key Improvements
|
## ✨ Key Improvements
|
||||||
|
|
||||||
### 1. Simplified Type Declarations
|
### 1. Simplified Type Declarations
|
||||||
|
|||||||
@@ -39,13 +39,18 @@
|
|||||||
// double := func(x int) int { return x * 2 }
|
// double := func(x int) int { return x * 2 }
|
||||||
// increment := func(x int) int { return x + 1 }
|
// increment := func(x int) int { return x + 1 }
|
||||||
//
|
//
|
||||||
// // Compose them
|
// // Compose them (RIGHT-TO-LEFT execution)
|
||||||
// doubleAndIncrement := endomorphism.Compose(double, increment)
|
// composed := endomorphism.Compose(double, increment)
|
||||||
// result := doubleAndIncrement(5) // (5 * 2) + 1 = 11
|
// result := composed(5) // increment(5) then double: (5 + 1) * 2 = 12
|
||||||
|
//
|
||||||
|
// // Chain them (LEFT-TO-RIGHT execution)
|
||||||
|
// chained := endomorphism.MonadChain(double, increment)
|
||||||
|
// result2 := chained(5) // double(5) then increment: (5 * 2) + 1 = 11
|
||||||
//
|
//
|
||||||
// # Monoid Operations
|
// # Monoid Operations
|
||||||
//
|
//
|
||||||
// Endomorphisms form a monoid, which means you can combine multiple endomorphisms:
|
// Endomorphisms form a monoid, which means you can combine multiple endomorphisms.
|
||||||
|
// The monoid uses Compose, which executes RIGHT-TO-LEFT:
|
||||||
//
|
//
|
||||||
// import (
|
// import (
|
||||||
// "github.com/IBM/fp-go/v2/endomorphism"
|
// "github.com/IBM/fp-go/v2/endomorphism"
|
||||||
@@ -55,22 +60,39 @@
|
|||||||
// // Get the monoid for int endomorphisms
|
// // Get the monoid for int endomorphisms
|
||||||
// monoid := endomorphism.Monoid[int]()
|
// monoid := endomorphism.Monoid[int]()
|
||||||
//
|
//
|
||||||
// // Combine multiple endomorphisms
|
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||||
// combined := M.ConcatAll(monoid)(
|
// combined := M.ConcatAll(monoid)(
|
||||||
// func(x int) int { return x * 2 },
|
// func(x int) int { return x * 2 }, // applied third
|
||||||
// func(x int) int { return x + 1 },
|
// func(x int) int { return x + 1 }, // applied second
|
||||||
// func(x int) int { return x * 3 },
|
// func(x int) int { return x * 3 }, // applied first
|
||||||
// )
|
// )
|
||||||
// result := combined(5) // ((5 * 2) + 1) * 3 = 33
|
// result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
|
||||||
//
|
//
|
||||||
// # Monad Operations
|
// # Monad Operations
|
||||||
//
|
//
|
||||||
// The package also provides monadic operations for endomorphisms:
|
// The package also provides monadic operations for endomorphisms.
|
||||||
|
// MonadChain executes LEFT-TO-RIGHT, unlike Compose:
|
||||||
//
|
//
|
||||||
// // Chain allows sequencing of endomorphisms
|
// // Chain allows sequencing of endomorphisms (LEFT-TO-RIGHT)
|
||||||
// f := func(x int) int { return x * 2 }
|
// f := func(x int) int { return x * 2 }
|
||||||
// g := func(x int) int { return x + 1 }
|
// g := func(x int) int { return x + 1 }
|
||||||
// chained := endomorphism.MonadChain(f, g)
|
// chained := endomorphism.MonadChain(f, g) // f first, then g
|
||||||
|
// result := chained(5) // (5 * 2) + 1 = 11
|
||||||
|
//
|
||||||
|
// # Compose vs Chain
|
||||||
|
//
|
||||||
|
// The key difference between Compose and Chain/MonadChain is execution order:
|
||||||
|
//
|
||||||
|
// double := func(x int) int { return x * 2 }
|
||||||
|
// increment := func(x int) int { return x + 1 }
|
||||||
|
//
|
||||||
|
// // Compose: RIGHT-TO-LEFT (mathematical composition)
|
||||||
|
// composed := endomorphism.Compose(double, increment)
|
||||||
|
// result1 := composed(5) // increment(5) * 2 = (5 + 1) * 2 = 12
|
||||||
|
//
|
||||||
|
// // MonadChain: LEFT-TO-RIGHT (sequential application)
|
||||||
|
// chained := endomorphism.MonadChain(double, increment)
|
||||||
|
// result2 := chained(5) // double(5) + 1 = (5 * 2) + 1 = 11
|
||||||
//
|
//
|
||||||
// # Type Safety
|
// # Type Safety
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -60,70 +60,133 @@ func Ap[A any](fa A) func(Endomorphism[A]) A {
|
|||||||
return identity.Ap[A](fa)
|
return identity.Ap[A](fa)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose composes two endomorphisms into a single endomorphism.
|
// MonadCompose composes two endomorphisms, executing them from right to left.
|
||||||
//
|
//
|
||||||
// Given two endomorphisms f1 and f2, Compose returns a new endomorphism that
|
// MonadCompose creates a new endomorphism that applies f2 first, then f1.
|
||||||
// applies f1 first, then applies f2 to the result. This is function composition:
|
// This follows the mathematical notation of function composition: (f1 ∘ f2)(x) = f1(f2(x))
|
||||||
// Compose(f1, f2)(x) = f2(f1(x))
|
|
||||||
//
|
//
|
||||||
// Composition is associative: Compose(Compose(f, g), h) = Compose(f, Compose(g, h))
|
// IMPORTANT: The execution order is RIGHT-TO-LEFT:
|
||||||
|
// - f2 is applied first to the input
|
||||||
|
// - f1 is applied to the result of f2
|
||||||
|
//
|
||||||
|
// This is different from Chain/MonadChain which executes LEFT-TO-RIGHT.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - f1: The first endomorphism to apply
|
// - f1: The second function to apply (outer function)
|
||||||
// - f2: The second endomorphism to apply
|
// - f2: The first function to apply (inner function)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - A new endomorphism that is the composition of f1 and f2
|
// - A new endomorphism that applies f2, then f1
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// double := func(x int) int { return x * 2 }
|
// double := func(x int) int { return x * 2 }
|
||||||
// increment := func(x int) int { return x + 1 }
|
// increment := func(x int) int { return x + 1 }
|
||||||
// doubleAndIncrement := endomorphism.Compose(double, increment)
|
//
|
||||||
// result := doubleAndIncrement(5) // (5 * 2) + 1 = 11
|
// // MonadCompose executes RIGHT-TO-LEFT: increment first, then double
|
||||||
func Compose[A any](f1, f2 Endomorphism[A]) Endomorphism[A] {
|
// composed := endomorphism.MonadCompose(double, increment)
|
||||||
return function.Flow2(f1, f2)
|
// result := composed(5) // (5 + 1) * 2 = 12
|
||||||
|
//
|
||||||
|
// // Compare with Chain which executes LEFT-TO-RIGHT:
|
||||||
|
// chained := endomorphism.MonadChain(double, increment)
|
||||||
|
// result2 := chained(5) // (5 * 2) + 1 = 11
|
||||||
|
func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||||
|
return function.Flow2(g, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MonadChain chains two endomorphisms together.
|
// 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
|
||||||
|
// a function that composes any endomorphism with g, applying g first (inner function),
|
||||||
|
// then the input endomorphism (outer function).
|
||||||
|
//
|
||||||
|
// IMPORTANT: Execution order is RIGHT-TO-LEFT (mathematical composition):
|
||||||
|
// - g is applied first to the input
|
||||||
|
// - The endomorphism passed to the returned function is applied to the result of g
|
||||||
|
//
|
||||||
|
// This follows the mathematical composition notation where Compose(g)(f) = f ∘ g
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - g: The first endomorphism to apply (inner function)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - A function that takes an endomorphism f and composes it with g (right-to-left)
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// increment := func(x int) int { return x + 1 }
|
||||||
|
// composeWithIncrement := endomorphism.Compose(increment)
|
||||||
|
// double := func(x int) int { return x * 2 }
|
||||||
|
//
|
||||||
|
// // Composes double with increment (RIGHT-TO-LEFT: increment first, then double)
|
||||||
|
// composed := composeWithIncrement(double)
|
||||||
|
// result := composed(5) // (5 + 1) * 2 = 12
|
||||||
|
//
|
||||||
|
// // Compare with Chain which executes LEFT-TO-RIGHT:
|
||||||
|
// chainWithIncrement := endomorphism.Chain(increment)
|
||||||
|
// chained := chainWithIncrement(double)
|
||||||
|
// result2 := chained(5) // (5 * 2) + 1 = 11
|
||||||
|
func Compose[A any](g Endomorphism[A]) Endomorphism[Endomorphism[A]] {
|
||||||
|
return function.Bind2nd(MonadCompose, g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonadChain chains two endomorphisms together, executing them from left to right.
|
||||||
//
|
//
|
||||||
// This is the monadic bind operation for endomorphisms. It composes two endomorphisms
|
// 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.
|
// ma and f, returning a new endomorphism that applies ma first, then f.
|
||||||
// MonadChain is equivalent to Compose.
|
//
|
||||||
|
// IMPORTANT: The execution order is LEFT-TO-RIGHT:
|
||||||
|
// - f is applied first to the input
|
||||||
|
// - g is applied to the result of ma
|
||||||
|
//
|
||||||
|
// This is different from Compose which executes RIGHT-TO-LEFT.
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - ma: The first endomorphism in the chain
|
// - f: The first endomorphism to apply
|
||||||
// - f: The second endomorphism in the chain
|
// - g: The second endomorphism to apply
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - A new endomorphism that chains ma and f
|
// - A new endomorphism that applies ma, then f
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// double := func(x int) int { return x * 2 }
|
// double := func(x int) int { return x * 2 }
|
||||||
// increment := func(x int) int { return x + 1 }
|
// increment := func(x int) int { return x + 1 }
|
||||||
|
//
|
||||||
|
// // MonadChain executes LEFT-TO-RIGHT: double first, then increment
|
||||||
// chained := endomorphism.MonadChain(double, increment)
|
// chained := endomorphism.MonadChain(double, increment)
|
||||||
// result := chained(5) // (5 * 2) + 1 = 11
|
// result := chained(5) // (5 * 2) + 1 = 11
|
||||||
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
|
//
|
||||||
return Compose(ma, f)
|
// // Compare with Compose which executes RIGHT-TO-LEFT:
|
||||||
|
// composed := endomorphism.Compose(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chain returns a function that chains an endomorphism with another.
|
// Chain returns a function that chains an endomorphism with another, executing left to right.
|
||||||
//
|
//
|
||||||
// This is the curried version of MonadChain. It takes an endomorphism f and returns
|
// This is the curried version of MonadChain. It takes an endomorphism f and returns
|
||||||
// a function that chains any endomorphism with f.
|
// a function that chains any endomorphism with f, applying the input endomorphism first,
|
||||||
|
// then f.
|
||||||
|
//
|
||||||
|
// IMPORTANT: Execution order is LEFT-TO-RIGHT:
|
||||||
|
// - The endomorphism passed to the returned function is applied first
|
||||||
|
// - f is applied to the result
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - f: The endomorphism to chain with
|
// - f: The second endomorphism to apply
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - A function that takes an endomorphism and chains it with f
|
// - A function that takes an endomorphism and chains it with f (left-to-right)
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
// increment := func(x int) int { return x + 1 }
|
// increment := func(x int) int { return x + 1 }
|
||||||
// chainWithIncrement := endomorphism.Chain(increment)
|
// chainWithIncrement := endomorphism.Chain(increment)
|
||||||
// double := func(x int) int { return x * 2 }
|
// double := func(x int) int { return x * 2 }
|
||||||
|
//
|
||||||
|
// // Chains double (first) with increment (second)
|
||||||
// chained := chainWithIncrement(double)
|
// chained := chainWithIncrement(double)
|
||||||
// result := chained(5) // (5 * 2) + 1 = 11
|
// result := chained(5) // (5 * 2) + 1 = 11
|
||||||
func Chain[A any](f Endomorphism[A]) Endomorphism[Endomorphism[A]] {
|
func Chain[A any](f Endomorphism[A]) Endomorphism[Endomorphism[A]] {
|
||||||
|
|||||||
@@ -101,59 +101,113 @@ func TestAp(t *testing.T) {
|
|||||||
assert.Equal(t, 100, result3, "Ap should work with different values")
|
assert.Equal(t, 100, result3, "Ap should work with different values")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCompose tests the Compose function
|
// TestMonadCompose tests the MonadCompose function
|
||||||
func TestCompose(t *testing.T) {
|
func TestMonadCompose(t *testing.T) {
|
||||||
// Test basic composition: (5 * 2) + 1 = 11
|
// Test basic composition: RIGHT-TO-LEFT execution
|
||||||
doubleAndIncrement := Compose(double, increment)
|
// MonadCompose(double, increment) means: increment first, then double
|
||||||
result := doubleAndIncrement(5)
|
composed := MonadCompose(double, increment)
|
||||||
assert.Equal(t, 11, result, "Compose should compose endomorphisms correctly")
|
result := composed(5)
|
||||||
|
assert.Equal(t, 12, result, "MonadCompose should execute right-to-left: (5 + 1) * 2 = 12")
|
||||||
|
|
||||||
// Test composition order: (5 + 1) * 2 = 12
|
// Test composition order: RIGHT-TO-LEFT execution
|
||||||
incrementAndDouble := Compose(increment, double)
|
// MonadCompose(increment, double) means: double first, then increment
|
||||||
result2 := incrementAndDouble(5)
|
composed2 := MonadCompose(increment, double)
|
||||||
assert.Equal(t, 12, result2, "Compose should respect order of composition")
|
result2 := composed2(5)
|
||||||
|
assert.Equal(t, 11, result2, "MonadCompose should execute right-to-left: (5 * 2) + 1 = 11")
|
||||||
|
|
||||||
// Test with three compositions: ((5 * 2) + 1) * ((5 * 2) + 1) = 121
|
// Test with three compositions: RIGHT-TO-LEFT execution
|
||||||
complex := Compose(Compose(double, increment), square)
|
// MonadCompose(MonadCompose(double, increment), square) means: square, then increment, then double
|
||||||
|
complex := MonadCompose(MonadCompose(double, increment), square)
|
||||||
result3 := complex(5)
|
result3 := complex(5)
|
||||||
assert.Equal(t, 121, result3, "Compose should work with nested compositions")
|
// 5 -> square -> 25 -> increment -> 26 -> double -> 52
|
||||||
|
assert.Equal(t, 52, result3, "MonadCompose should work with nested compositions: square(5)=25, +1=26, *2=52")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMonadChain tests the MonadChain function
|
// TestMonadChain tests the MonadChain function
|
||||||
func TestMonadChain(t *testing.T) {
|
func TestMonadChain(t *testing.T) {
|
||||||
// MonadChain should behave like Compose
|
// MonadChain executes LEFT-TO-RIGHT (first arg first, second arg second)
|
||||||
chained := MonadChain(double, increment)
|
chained := MonadChain(double, increment)
|
||||||
result := chained(5)
|
result := chained(5)
|
||||||
assert.Equal(t, 11, result, "MonadChain should chain endomorphisms correctly")
|
assert.Equal(t, 11, result, "MonadChain should execute left-to-right: (5 * 2) + 1 = 11")
|
||||||
|
|
||||||
chained2 := MonadChain(increment, double)
|
chained2 := MonadChain(increment, double)
|
||||||
result2 := chained2(5)
|
result2 := chained2(5)
|
||||||
assert.Equal(t, 12, result2, "MonadChain should respect order")
|
assert.Equal(t, 12, result2, "MonadChain should execute left-to-right: (5 + 1) * 2 = 12")
|
||||||
|
|
||||||
// Test with negative values
|
// Test with negative values
|
||||||
chained3 := MonadChain(negate, increment)
|
chained3 := MonadChain(negate, increment)
|
||||||
result3 := chained3(5)
|
result3 := chained3(5)
|
||||||
assert.Equal(t, -4, result3, "MonadChain should work with negative values")
|
assert.Equal(t, -4, result3, "MonadChain should execute left-to-right: -(5) + 1 = -4")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestChain tests the Chain function
|
// TestChain tests the Chain function
|
||||||
func TestChain(t *testing.T) {
|
func TestChain(t *testing.T) {
|
||||||
|
// Chain(f) returns a function that applies its argument first, then f
|
||||||
chainWithIncrement := Chain(increment)
|
chainWithIncrement := Chain(increment)
|
||||||
|
|
||||||
|
// chainWithIncrement(double) means: double first, then increment
|
||||||
chained := chainWithIncrement(double)
|
chained := chainWithIncrement(double)
|
||||||
result := chained(5)
|
result := chained(5)
|
||||||
assert.Equal(t, 11, result, "Chain should create chaining function correctly")
|
assert.Equal(t, 11, result, "Chain should execute left-to-right: (5 * 2) + 1 = 11")
|
||||||
|
|
||||||
chainWithDouble := Chain(double)
|
chainWithDouble := Chain(double)
|
||||||
|
// chainWithDouble(increment) means: increment first, then double
|
||||||
chained2 := chainWithDouble(increment)
|
chained2 := chainWithDouble(increment)
|
||||||
result2 := chained2(5)
|
result2 := chained2(5)
|
||||||
assert.Equal(t, 12, result2, "Chain should work with different endomorphisms")
|
assert.Equal(t, 12, result2, "Chain should execute left-to-right: (5 + 1) * 2 = 12")
|
||||||
|
|
||||||
// Test chaining with square
|
// Test chaining with square
|
||||||
chainWithSquare := Chain(square)
|
chainWithSquare := Chain(square)
|
||||||
|
// chainWithSquare(double) means: double first, then square
|
||||||
chained3 := chainWithSquare(double)
|
chained3 := chainWithSquare(double)
|
||||||
result3 := chained3(3)
|
result3 := chained3(3)
|
||||||
assert.Equal(t, 36, result3, "Chain should work with square function")
|
assert.Equal(t, 36, result3, "Chain should execute left-to-right: (3 * 2) ^ 2 = 36")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCompose tests the curried Compose function
|
||||||
|
func TestCompose(t *testing.T) {
|
||||||
|
// Compose(g) returns a function that applies g first, then its argument
|
||||||
|
composeWithIncrement := Compose(increment)
|
||||||
|
|
||||||
|
// composeWithIncrement(double) means: increment first, then double
|
||||||
|
composed := composeWithIncrement(double)
|
||||||
|
result := composed(5)
|
||||||
|
assert.Equal(t, 12, result, "Compose should execute right-to-left: (5 + 1) * 2 = 12")
|
||||||
|
|
||||||
|
composeWithDouble := Compose(double)
|
||||||
|
// composeWithDouble(increment) means: double first, then increment
|
||||||
|
composed2 := composeWithDouble(increment)
|
||||||
|
result2 := composed2(5)
|
||||||
|
assert.Equal(t, 11, result2, "Compose should execute right-to-left: (5 * 2) + 1 = 11")
|
||||||
|
|
||||||
|
// Test composing with square
|
||||||
|
composeWithSquare := Compose(square)
|
||||||
|
// composeWithSquare(double) means: square first, then double
|
||||||
|
composed3 := composeWithSquare(double)
|
||||||
|
result3 := composed3(3)
|
||||||
|
assert.Equal(t, 18, result3, "Compose should execute right-to-left: (3 ^ 2) * 2 = 18")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMonadComposeVsCompose demonstrates the relationship between MonadCompose and Compose
|
||||||
|
func TestMonadComposeVsCompose(t *testing.T) {
|
||||||
|
double := func(x int) int { return x * 2 }
|
||||||
|
increment := func(x int) int { return x + 1 }
|
||||||
|
|
||||||
|
// MonadCompose takes both functions at once
|
||||||
|
monadComposed := MonadCompose(double, increment)
|
||||||
|
result1 := monadComposed(5) // (5 + 1) * 2 = 12
|
||||||
|
|
||||||
|
// Compose is the curried version - takes one function, returns a function
|
||||||
|
curriedCompose := Compose(increment)
|
||||||
|
composed := curriedCompose(double)
|
||||||
|
result2 := composed(5) // (5 + 1) * 2 = 12
|
||||||
|
|
||||||
|
assert.Equal(t, result1, result2, "MonadCompose and Compose should produce the same result")
|
||||||
|
assert.Equal(t, 12, result1, "Both should execute right-to-left: (5 + 1) * 2 = 12")
|
||||||
|
|
||||||
|
// Demonstrate that Compose(g)(f) is equivalent to MonadCompose(f, g)
|
||||||
|
assert.Equal(t, MonadCompose(double, increment)(5), Compose(increment)(double)(5),
|
||||||
|
"Compose(g)(f) should equal MonadCompose(f, g)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOf tests the Of function
|
// TestOf tests the Of function
|
||||||
@@ -191,12 +245,14 @@ func TestIdentity(t *testing.T) {
|
|||||||
assert.Equal(t, 0, id(0), "Identity should work with zero")
|
assert.Equal(t, 0, id(0), "Identity should work with zero")
|
||||||
assert.Equal(t, -10, id(-10), "Identity should work with negative values")
|
assert.Equal(t, -10, id(-10), "Identity should work with negative values")
|
||||||
|
|
||||||
// Identity should be neutral for composition
|
// Identity should be neutral for composition (RIGHT-TO-LEFT)
|
||||||
composed1 := Compose(id, double)
|
// Compose(id, double) means: double first, then id
|
||||||
assert.Equal(t, 10, composed1(5), "Identity should be right neutral for composition")
|
composed1 := MonadCompose(id, double)
|
||||||
|
assert.Equal(t, 10, composed1(5), "Identity should be left neutral: double(5) = 10")
|
||||||
|
|
||||||
composed2 := Compose(double, id)
|
// Compose(double, id) means: id first, then double
|
||||||
assert.Equal(t, 10, composed2(5), "Identity should be left neutral for composition")
|
composed2 := MonadCompose(double, id)
|
||||||
|
assert.Equal(t, 10, composed2(5), "Identity should be right neutral: id(5) then double = 10")
|
||||||
|
|
||||||
// Test with strings
|
// Test with strings
|
||||||
idStr := Identity[string]()
|
idStr := Identity[string]()
|
||||||
@@ -207,10 +263,11 @@ func TestIdentity(t *testing.T) {
|
|||||||
func TestSemigroup(t *testing.T) {
|
func TestSemigroup(t *testing.T) {
|
||||||
sg := Semigroup[int]()
|
sg := Semigroup[int]()
|
||||||
|
|
||||||
// Test basic concat
|
// Test basic concat (RIGHT-TO-LEFT execution via Compose)
|
||||||
|
// Concat(double, increment) means: increment first, then double
|
||||||
combined := sg.Concat(double, increment)
|
combined := sg.Concat(double, increment)
|
||||||
result := combined(5)
|
result := combined(5)
|
||||||
assert.Equal(t, 11, result, "Semigroup concat should compose endomorphisms")
|
assert.Equal(t, 12, result, "Semigroup concat should execute right-to-left: (5 + 1) * 2 = 12")
|
||||||
|
|
||||||
// Test associativity: (f . g) . h = f . (g . h)
|
// Test associativity: (f . g) . h = f . (g . h)
|
||||||
f := double
|
f := double
|
||||||
@@ -223,10 +280,12 @@ func TestSemigroup(t *testing.T) {
|
|||||||
testValue := 3
|
testValue := 3
|
||||||
assert.Equal(t, left(testValue), right(testValue), "Semigroup should be associative")
|
assert.Equal(t, left(testValue), right(testValue), "Semigroup should be associative")
|
||||||
|
|
||||||
// Test with ConcatAll from semigroup package
|
// Test with ConcatAll from semigroup package (RIGHT-TO-LEFT)
|
||||||
|
// ConcatAll(double)(increment, square) means: square, then increment, then double
|
||||||
combined2 := S.ConcatAll(sg)(double)([]Endomorphism[int]{increment, square})
|
combined2 := S.ConcatAll(sg)(double)([]Endomorphism[int]{increment, square})
|
||||||
result2 := combined2(5)
|
result2 := combined2(5)
|
||||||
assert.Equal(t, 121, result2, "Semigroup should work with ConcatAll")
|
// 5 -> square -> 25 -> increment -> 26 -> double -> 52
|
||||||
|
assert.Equal(t, 52, result2, "Semigroup ConcatAll should execute right-to-left: square(5)=25, +1=26, *2=52")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMonoid tests the Monoid function
|
// TestMonoid tests the Monoid function
|
||||||
@@ -237,19 +296,21 @@ func TestMonoid(t *testing.T) {
|
|||||||
empty := monoid.Empty()
|
empty := monoid.Empty()
|
||||||
assert.Equal(t, 42, empty(42), "Monoid empty should be identity")
|
assert.Equal(t, 42, empty(42), "Monoid empty should be identity")
|
||||||
|
|
||||||
// Test right identity: x . empty = x
|
// Test right identity: x . empty = x (RIGHT-TO-LEFT: empty first, then x)
|
||||||
|
// Concat(double, empty) means: empty first, then double
|
||||||
rightIdentity := monoid.Concat(double, empty)
|
rightIdentity := monoid.Concat(double, empty)
|
||||||
assert.Equal(t, 10, rightIdentity(5), "Monoid should satisfy right identity")
|
assert.Equal(t, 10, rightIdentity(5), "Monoid should satisfy right identity: empty(5) then double = 10")
|
||||||
|
|
||||||
// Test left identity: empty . x = x
|
// Test left identity: empty . x = x (RIGHT-TO-LEFT: x first, then empty)
|
||||||
|
// Concat(empty, double) means: double first, then empty
|
||||||
leftIdentity := monoid.Concat(empty, double)
|
leftIdentity := monoid.Concat(empty, double)
|
||||||
assert.Equal(t, 10, leftIdentity(5), "Monoid should satisfy left identity")
|
assert.Equal(t, 10, leftIdentity(5), "Monoid should satisfy left identity: double(5) then empty = 10")
|
||||||
|
|
||||||
// Test ConcatAll with multiple endomorphisms
|
// Test ConcatAll with multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||||
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
|
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
|
||||||
result := combined(5)
|
result := combined(5)
|
||||||
// (5 * 2) = 10, (10 + 1) = 11, (11 * 11) = 121
|
// RIGHT-TO-LEFT: square(5) = 25, increment(25) = 26, double(26) = 52
|
||||||
assert.Equal(t, 121, result, "Monoid should work with ConcatAll")
|
assert.Equal(t, 52, result, "Monoid ConcatAll should execute right-to-left: square(5)=25, +1=26, *2=52")
|
||||||
|
|
||||||
// Test ConcatAll with empty list should return identity
|
// Test ConcatAll with empty list should return identity
|
||||||
emptyResult := M.ConcatAll(monoid)([]Endomorphism[int]{})
|
emptyResult := M.ConcatAll(monoid)([]Endomorphism[int]{})
|
||||||
@@ -294,19 +355,20 @@ func TestMonoidLaws(t *testing.T) {
|
|||||||
|
|
||||||
// TestEndomorphismWithDifferentTypes tests endomorphisms with different types
|
// TestEndomorphismWithDifferentTypes tests endomorphisms with different types
|
||||||
func TestEndomorphismWithDifferentTypes(t *testing.T) {
|
func TestEndomorphismWithDifferentTypes(t *testing.T) {
|
||||||
// Test with strings
|
// Test with strings (RIGHT-TO-LEFT execution)
|
||||||
toUpper := func(s string) string {
|
addExclamation := func(s string) string {
|
||||||
return s + "!"
|
return s + "!"
|
||||||
}
|
}
|
||||||
addPrefix := func(s string) string {
|
addPrefix := func(s string) string {
|
||||||
return "Hello, " + s
|
return "Hello, " + s
|
||||||
}
|
}
|
||||||
|
|
||||||
strComposed := Compose(toUpper, addPrefix)
|
// Compose(addExclamation, addPrefix) means: addPrefix first, then addExclamation
|
||||||
|
strComposed := MonadCompose(addExclamation, addPrefix)
|
||||||
result := strComposed("World")
|
result := strComposed("World")
|
||||||
assert.Equal(t, "Hello, World!", result, "Endomorphism should work with strings")
|
assert.Equal(t, "Hello, World!", result, "Compose should execute right-to-left with strings")
|
||||||
|
|
||||||
// Test with float64
|
// Test with float64 (RIGHT-TO-LEFT execution)
|
||||||
doubleFloat := func(x float64) float64 {
|
doubleFloat := func(x float64) float64 {
|
||||||
return x * 2.0
|
return x * 2.0
|
||||||
}
|
}
|
||||||
@@ -314,31 +376,35 @@ func TestEndomorphismWithDifferentTypes(t *testing.T) {
|
|||||||
return x + 1.0
|
return x + 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
floatComposed := Compose(doubleFloat, addOne)
|
// Compose(doubleFloat, addOne) means: addOne first, then doubleFloat
|
||||||
|
floatComposed := MonadCompose(doubleFloat, addOne)
|
||||||
resultFloat := floatComposed(5.5)
|
resultFloat := floatComposed(5.5)
|
||||||
assert.Equal(t, 12.0, resultFloat, "Endomorphism should work with float64")
|
// 5.5 + 1.0 = 6.5, 6.5 * 2.0 = 13.0
|
||||||
|
assert.Equal(t, 13.0, resultFloat, "Compose should execute right-to-left: (5.5 + 1.0) * 2.0 = 13.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestComplexCompositions tests more complex composition scenarios
|
// TestComplexCompositions tests more complex composition scenarios
|
||||||
func TestComplexCompositions(t *testing.T) {
|
func TestComplexCompositions(t *testing.T) {
|
||||||
// Create a pipeline of transformations
|
// Create a pipeline of transformations (RIGHT-TO-LEFT execution)
|
||||||
pipeline := Compose(
|
// Innermost Compose is evaluated first in the composition chain
|
||||||
Compose(
|
pipeline := MonadCompose(
|
||||||
Compose(double, increment),
|
MonadCompose(
|
||||||
|
MonadCompose(double, increment),
|
||||||
square,
|
square,
|
||||||
),
|
),
|
||||||
negate,
|
negate,
|
||||||
)
|
)
|
||||||
|
|
||||||
// (5 * 2) = 10, (10 + 1) = 11, (11 * 11) = 121, -(121) = -121
|
// RIGHT-TO-LEFT: negate(5) = -5, square(-5) = 25, increment(25) = 26, double(26) = 52
|
||||||
result := pipeline(5)
|
result := pipeline(5)
|
||||||
assert.Equal(t, -121, result, "Complex composition should work correctly")
|
assert.Equal(t, 52, result, "Complex composition should execute right-to-left")
|
||||||
|
|
||||||
// Test using monoid to build the same pipeline
|
// Test using monoid to build the same pipeline (RIGHT-TO-LEFT)
|
||||||
monoid := Monoid[int]()
|
monoid := Monoid[int]()
|
||||||
pipelineMonoid := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square, negate})
|
pipelineMonoid := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square, negate})
|
||||||
resultMonoid := pipelineMonoid(5)
|
resultMonoid := pipelineMonoid(5)
|
||||||
assert.Equal(t, -121, resultMonoid, "Monoid-based pipeline should match composition")
|
// RIGHT-TO-LEFT: negate(5) = -5, square(-5) = 25, increment(25) = 26, double(26) = 52
|
||||||
|
assert.Equal(t, 52, resultMonoid, "Monoid-based pipeline should match composition (right-to-left)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOperatorType tests the Operator type
|
// TestOperatorType tests the Operator type
|
||||||
@@ -371,7 +437,7 @@ func TestOperatorType(t *testing.T) {
|
|||||||
|
|
||||||
// BenchmarkCompose benchmarks the Compose function
|
// BenchmarkCompose benchmarks the Compose function
|
||||||
func BenchmarkCompose(b *testing.B) {
|
func BenchmarkCompose(b *testing.B) {
|
||||||
composed := Compose(double, increment)
|
composed := MonadCompose(double, increment)
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_ = composed(5)
|
_ = composed(5)
|
||||||
@@ -379,6 +445,47 @@ func BenchmarkCompose(b *testing.B) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkMonoidConcatAll benchmarks ConcatAll with monoid
|
// BenchmarkMonoidConcatAll benchmarks ConcatAll with monoid
|
||||||
|
// TestComposeVsChain demonstrates the key difference between Compose and Chain
|
||||||
|
func TestComposeVsChain(t *testing.T) {
|
||||||
|
double := func(x int) int { return x * 2 }
|
||||||
|
increment := func(x int) int { return x + 1 }
|
||||||
|
|
||||||
|
// Compose executes RIGHT-TO-LEFT
|
||||||
|
// Compose(double, increment) means: increment first, then double
|
||||||
|
composed := MonadCompose(double, increment)
|
||||||
|
composedResult := composed(5) // (5 + 1) * 2 = 12
|
||||||
|
|
||||||
|
// MonadChain executes LEFT-TO-RIGHT
|
||||||
|
// MonadChain(double, increment) means: double first, then increment
|
||||||
|
chained := MonadChain(double, increment)
|
||||||
|
chainedResult := chained(5) // (5 * 2) + 1 = 11
|
||||||
|
|
||||||
|
assert.Equal(t, 12, composedResult, "Compose should execute right-to-left")
|
||||||
|
assert.Equal(t, 11, chainedResult, "MonadChain should execute left-to-right")
|
||||||
|
assert.NotEqual(t, composedResult, chainedResult, "Compose and Chain should produce different results with non-commutative operations")
|
||||||
|
|
||||||
|
// To get the same result with Compose, we need to reverse the order
|
||||||
|
composedReversed := MonadCompose(increment, double)
|
||||||
|
assert.Equal(t, chainedResult, composedReversed(5), "Compose with reversed args should match Chain")
|
||||||
|
|
||||||
|
// Demonstrate with a more complex example
|
||||||
|
square := func(x int) int { return x * x }
|
||||||
|
|
||||||
|
// Compose: RIGHT-TO-LEFT
|
||||||
|
composed3 := MonadCompose(MonadCompose(square, increment), double)
|
||||||
|
// double(5) = 10, increment(10) = 11, square(11) = 121
|
||||||
|
result1 := composed3(5)
|
||||||
|
|
||||||
|
// MonadChain: LEFT-TO-RIGHT
|
||||||
|
chained3 := MonadChain(MonadChain(double, increment), square)
|
||||||
|
// double(5) = 10, increment(10) = 11, square(11) = 121
|
||||||
|
result2 := chained3(5)
|
||||||
|
|
||||||
|
assert.Equal(t, 121, result1, "Compose should execute right-to-left")
|
||||||
|
assert.Equal(t, 121, result2, "MonadChain should execute left-to-right")
|
||||||
|
assert.Equal(t, result1, result2, "Both should produce same result when operations are in correct order")
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkMonoidConcatAll(b *testing.B) {
|
func BenchmarkMonoidConcatAll(b *testing.B) {
|
||||||
monoid := Monoid[int]()
|
monoid := Monoid[int]()
|
||||||
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
|
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
|
||||||
|
|||||||
@@ -88,11 +88,15 @@ func Identity[A any]() Endomorphism[A] {
|
|||||||
// For endomorphisms, this operation is composition (Compose). This means:
|
// For endomorphisms, this operation is composition (Compose). This means:
|
||||||
// - Concat(f, Concat(g, h)) = Concat(Concat(f, g), h)
|
// - Concat(f, Concat(g, h)) = Concat(Concat(f, g), h)
|
||||||
//
|
//
|
||||||
|
// IMPORTANT: Concat uses Compose, which executes RIGHT-TO-LEFT:
|
||||||
|
// - Concat(f, g) applies g first, then f
|
||||||
|
// - This is equivalent to Compose(f, g)
|
||||||
|
//
|
||||||
// The returned semigroup can be used with semigroup operations to combine
|
// The returned semigroup can be used with semigroup operations to combine
|
||||||
// multiple endomorphisms.
|
// multiple endomorphisms.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - A Semigroup[Endomorphism[A]] where concat is composition
|
// - A Semigroup[Endomorphism[A]] where concat is composition (right-to-left)
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
@@ -102,11 +106,11 @@ func Identity[A any]() Endomorphism[A] {
|
|||||||
// double := func(x int) int { return x * 2 }
|
// double := func(x int) int { return x * 2 }
|
||||||
// increment := func(x int) int { return x + 1 }
|
// increment := func(x int) int { return x + 1 }
|
||||||
//
|
//
|
||||||
// // Combine using the semigroup
|
// // Combine using the semigroup (RIGHT-TO-LEFT execution)
|
||||||
// combined := sg.Concat(double, increment)
|
// combined := sg.Concat(double, increment)
|
||||||
// result := combined(5) // (5 * 2) + 1 = 11
|
// result := combined(5) // (5 + 1) * 2 = 12 (increment first, then double)
|
||||||
func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
|
func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
|
||||||
return S.MakeSemigroup(Compose[A])
|
return S.MakeSemigroup(MonadCompose[A])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monoid returns a Monoid for endomorphisms where concat is composition and empty is identity.
|
// Monoid returns a Monoid for endomorphisms where concat is composition and empty is identity.
|
||||||
@@ -115,6 +119,10 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
|
|||||||
// - The binary operation is composition (Compose)
|
// - The binary operation is composition (Compose)
|
||||||
// - The identity element is the identity function (Identity)
|
// - The identity element is the identity function (Identity)
|
||||||
//
|
//
|
||||||
|
// IMPORTANT: Concat uses Compose, which executes RIGHT-TO-LEFT:
|
||||||
|
// - Concat(f, g) applies g first, then f
|
||||||
|
// - ConcatAll applies functions from right to left
|
||||||
|
//
|
||||||
// This satisfies the monoid laws:
|
// This satisfies the monoid laws:
|
||||||
// - Right identity: Concat(x, Empty) = x
|
// - Right identity: Concat(x, Empty) = x
|
||||||
// - Left identity: Concat(Empty, x) = x
|
// - Left identity: Concat(Empty, x) = x
|
||||||
@@ -124,7 +132,7 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
|
|||||||
// combine multiple endomorphisms.
|
// combine multiple endomorphisms.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - A Monoid[Endomorphism[A]] with composition and identity
|
// - A Monoid[Endomorphism[A]] with composition (right-to-left) and identity
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
@@ -135,9 +143,9 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
|
|||||||
// increment := func(x int) int { return x + 1 }
|
// increment := func(x int) int { return x + 1 }
|
||||||
// square := func(x int) int { return x * x }
|
// square := func(x int) int { return x * x }
|
||||||
//
|
//
|
||||||
// // Combine multiple endomorphisms
|
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||||
// combined := M.ConcatAll(monoid)(double, increment, square)
|
// combined := M.ConcatAll(monoid)(double, increment, square)
|
||||||
// result := combined(5) // ((5 * 2) + 1) * ((5 * 2) + 1) = 121
|
// result := combined(5) // square(increment(double(5))) = square(increment(10)) = square(11) = 121
|
||||||
func Monoid[A any]() M.Monoid[Endomorphism[A]] {
|
func Monoid[A any]() M.Monoid[Endomorphism[A]] {
|
||||||
return M.MakeMonoid(Compose[A], Identity[A]())
|
return M.MakeMonoid(MonadCompose[A], Identity[A]())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user