From 567315a31c8f406a8c664d0ed1fca9282d080663 Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Wed, 12 Nov 2025 13:51:00 +0100 Subject: [PATCH] fix: make a distinction between Chain and Compose for endomorphism Signed-off-by: Dr. Carsten Leue --- v2/README.md | 30 ++++ v2/endomorphism/doc.go | 46 ++++-- v2/endomorphism/endo.go | 109 +++++++++++--- v2/endomorphism/endomorphism_test.go | 211 ++++++++++++++++++++------- v2/endomorphism/monoid.go | 24 ++- 5 files changed, 325 insertions(+), 95 deletions(-) diff --git a/v2/README.md b/v2/README.md index a38bbef..e333f77 100644 --- a/v2/README.md +++ b/v2/README.md @@ -197,6 +197,36 @@ pair := MakePair(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 ### 1. Simplified Type Declarations diff --git a/v2/endomorphism/doc.go b/v2/endomorphism/doc.go index 27249cc..bd44436 100644 --- a/v2/endomorphism/doc.go +++ b/v2/endomorphism/doc.go @@ -39,13 +39,18 @@ // double := func(x int) int { return x * 2 } // increment := func(x int) int { return x + 1 } // -// // Compose them -// doubleAndIncrement := endomorphism.Compose(double, increment) -// result := doubleAndIncrement(5) // (5 * 2) + 1 = 11 +// // Compose them (RIGHT-TO-LEFT execution) +// composed := endomorphism.Compose(double, increment) +// 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 // -// 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 ( // "github.com/IBM/fp-go/v2/endomorphism" @@ -55,22 +60,39 @@ // // Get the monoid for int endomorphisms // monoid := endomorphism.Monoid[int]() // -// // Combine multiple endomorphisms +// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution) // combined := M.ConcatAll(monoid)( -// func(x int) int { return x * 2 }, -// func(x int) int { return x + 1 }, -// func(x int) int { return x * 3 }, +// func(x int) int { return x * 2 }, // applied third +// func(x int) int { return x + 1 }, // applied second +// 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 // -// 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 } // 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 // diff --git a/v2/endomorphism/endo.go b/v2/endomorphism/endo.go index 028fba1..2f776f1 100644 --- a/v2/endomorphism/endo.go +++ b/v2/endomorphism/endo.go @@ -60,70 +60,133 @@ func Ap[A any](fa A) func(Endomorphism[A]) A { 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 -// applies f1 first, then applies f2 to the result. This is function composition: -// Compose(f1, f2)(x) = f2(f1(x)) +// MonadCompose creates a new endomorphism that applies f2 first, then f1. +// This follows the mathematical notation of function composition: (f1 ∘ f2)(x) = f1(f2(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: -// - f1: The first endomorphism to apply -// - f2: The second endomorphism to apply +// - f1: The second function to apply (outer function) +// - f2: The first function to apply (inner function) // // Returns: -// - A new endomorphism that is the composition of f1 and f2 +// - A new endomorphism that applies f2, then f1 // // Example: // // double := func(x int) int { return x * 2 } // increment := func(x int) int { return x + 1 } -// doubleAndIncrement := endomorphism.Compose(double, increment) -// result := doubleAndIncrement(5) // (5 * 2) + 1 = 11 -func Compose[A any](f1, f2 Endomorphism[A]) Endomorphism[A] { - return function.Flow2(f1, f2) +// +// // MonadCompose executes RIGHT-TO-LEFT: increment first, then double +// composed := endomorphism.MonadCompose(double, increment) +// 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 // 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: -// - ma: The first endomorphism in the chain -// - f: The second endomorphism in the chain +// - f: The first endomorphism to apply +// - g: The second endomorphism to apply // // Returns: -// - A new endomorphism that chains ma and f +// - A new endomorphism that applies ma, then f // // Example: // // double := func(x int) int { return x * 2 } // increment := func(x int) int { return x + 1 } +// +// // MonadChain executes LEFT-TO-RIGHT: double first, then increment // chained := endomorphism.MonadChain(double, increment) // 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 -// 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: -// - f: The endomorphism to chain with +// - f: The second endomorphism to apply // // 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: // // increment := func(x int) int { return x + 1 } // chainWithIncrement := endomorphism.Chain(increment) // double := func(x int) int { return x * 2 } +// +// // 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]] { diff --git a/v2/endomorphism/endomorphism_test.go b/v2/endomorphism/endomorphism_test.go index 8c2d9e0..ccb8f17 100644 --- a/v2/endomorphism/endomorphism_test.go +++ b/v2/endomorphism/endomorphism_test.go @@ -101,59 +101,113 @@ func TestAp(t *testing.T) { assert.Equal(t, 100, result3, "Ap should work with different values") } -// TestCompose tests the Compose function -func TestCompose(t *testing.T) { - // Test basic composition: (5 * 2) + 1 = 11 - doubleAndIncrement := Compose(double, increment) - result := doubleAndIncrement(5) - assert.Equal(t, 11, result, "Compose should compose endomorphisms correctly") +// TestMonadCompose tests the MonadCompose function +func TestMonadCompose(t *testing.T) { + // Test basic composition: RIGHT-TO-LEFT execution + // MonadCompose(double, increment) means: increment first, then double + composed := MonadCompose(double, increment) + 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 - incrementAndDouble := Compose(increment, double) - result2 := incrementAndDouble(5) - assert.Equal(t, 12, result2, "Compose should respect order of composition") + // Test composition order: RIGHT-TO-LEFT execution + // MonadCompose(increment, double) means: double first, then increment + composed2 := MonadCompose(increment, double) + 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 - complex := Compose(Compose(double, increment), square) + // Test with three compositions: RIGHT-TO-LEFT execution + // MonadCompose(MonadCompose(double, increment), square) means: square, then increment, then double + complex := MonadCompose(MonadCompose(double, increment), square) 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 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) 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) 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 chained3 := MonadChain(negate, increment) 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 func TestChain(t *testing.T) { + // Chain(f) returns a function that applies its argument first, then f chainWithIncrement := Chain(increment) + // chainWithIncrement(double) means: double first, then increment chained := chainWithIncrement(double) 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(increment) means: increment first, then double chained2 := chainWithDouble(increment) 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 chainWithSquare := Chain(square) + // chainWithSquare(double) means: double first, then square chained3 := chainWithSquare(double) 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 @@ -191,12 +245,14 @@ func TestIdentity(t *testing.T) { assert.Equal(t, 0, id(0), "Identity should work with zero") assert.Equal(t, -10, id(-10), "Identity should work with negative values") - // Identity should be neutral for composition - composed1 := Compose(id, double) - assert.Equal(t, 10, composed1(5), "Identity should be right neutral for composition") + // Identity should be neutral for composition (RIGHT-TO-LEFT) + // Compose(id, double) means: double first, then id + composed1 := MonadCompose(id, double) + assert.Equal(t, 10, composed1(5), "Identity should be left neutral: double(5) = 10") - composed2 := Compose(double, id) - assert.Equal(t, 10, composed2(5), "Identity should be left neutral for composition") + // Compose(double, id) means: id first, then double + composed2 := MonadCompose(double, id) + assert.Equal(t, 10, composed2(5), "Identity should be right neutral: id(5) then double = 10") // Test with strings idStr := Identity[string]() @@ -207,10 +263,11 @@ func TestIdentity(t *testing.T) { func TestSemigroup(t *testing.T) { 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) 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) f := double @@ -223,10 +280,12 @@ func TestSemigroup(t *testing.T) { testValue := 3 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}) 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 @@ -237,19 +296,21 @@ func TestMonoid(t *testing.T) { empty := monoid.Empty() 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) - 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) - 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}) result := combined(5) - // (5 * 2) = 10, (10 + 1) = 11, (11 * 11) = 121 - assert.Equal(t, 121, result, "Monoid should work with ConcatAll") + // RIGHT-TO-LEFT: square(5) = 25, increment(25) = 26, double(26) = 52 + 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 emptyResult := M.ConcatAll(monoid)([]Endomorphism[int]{}) @@ -294,19 +355,20 @@ func TestMonoidLaws(t *testing.T) { // TestEndomorphismWithDifferentTypes tests endomorphisms with different types func TestEndomorphismWithDifferentTypes(t *testing.T) { - // Test with strings - toUpper := func(s string) string { + // Test with strings (RIGHT-TO-LEFT execution) + addExclamation := func(s string) string { return s + "!" } addPrefix := func(s string) string { return "Hello, " + s } - strComposed := Compose(toUpper, addPrefix) + // Compose(addExclamation, addPrefix) means: addPrefix first, then addExclamation + strComposed := MonadCompose(addExclamation, addPrefix) 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 { return x * 2.0 } @@ -314,31 +376,35 @@ func TestEndomorphismWithDifferentTypes(t *testing.T) { return x + 1.0 } - floatComposed := Compose(doubleFloat, addOne) + // Compose(doubleFloat, addOne) means: addOne first, then doubleFloat + floatComposed := MonadCompose(doubleFloat, addOne) 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 func TestComplexCompositions(t *testing.T) { - // Create a pipeline of transformations - pipeline := Compose( - Compose( - Compose(double, increment), + // Create a pipeline of transformations (RIGHT-TO-LEFT execution) + // Innermost Compose is evaluated first in the composition chain + pipeline := MonadCompose( + MonadCompose( + MonadCompose(double, increment), square, ), 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) - 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]() pipelineMonoid := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square, negate}) 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 @@ -371,7 +437,7 @@ func TestOperatorType(t *testing.T) { // BenchmarkCompose benchmarks the Compose function func BenchmarkCompose(b *testing.B) { - composed := Compose(double, increment) + composed := MonadCompose(double, increment) b.ResetTimer() for i := 0; i < b.N; i++ { _ = composed(5) @@ -379,6 +445,47 @@ func BenchmarkCompose(b *testing.B) { } // 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) { monoid := Monoid[int]() combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square}) diff --git a/v2/endomorphism/monoid.go b/v2/endomorphism/monoid.go index bf26cc8..4af3e38 100644 --- a/v2/endomorphism/monoid.go +++ b/v2/endomorphism/monoid.go @@ -88,11 +88,15 @@ func Identity[A any]() Endomorphism[A] { // For endomorphisms, this operation is composition (Compose). This means: // - 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 // multiple endomorphisms. // // Returns: -// - A Semigroup[Endomorphism[A]] where concat is composition +// - A Semigroup[Endomorphism[A]] where concat is composition (right-to-left) // // Example: // @@ -102,11 +106,11 @@ func Identity[A any]() Endomorphism[A] { // double := func(x int) int { return x * 2 } // 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) -// 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]] { - return S.MakeSemigroup(Compose[A]) + return S.MakeSemigroup(MonadCompose[A]) } // 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 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: // - Right identity: Concat(x, Empty) = x // - Left identity: Concat(Empty, x) = x @@ -124,7 +132,7 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] { // combine multiple endomorphisms. // // Returns: -// - A Monoid[Endomorphism[A]] with composition and identity +// - A Monoid[Endomorphism[A]] with composition (right-to-left) and identity // // Example: // @@ -135,9 +143,9 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] { // increment := func(x int) int { return x + 1 } // 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) -// 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]] { - return M.MakeMonoid(Compose[A], Identity[A]()) + return M.MakeMonoid(MonadCompose[A], Identity[A]()) }