mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
725 lines
24 KiB
Go
725 lines
24 KiB
Go
// 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 endomorphism
|
|
|
|
import (
|
|
"testing"
|
|
|
|
M "github.com/IBM/fp-go/v2/monoid"
|
|
N "github.com/IBM/fp-go/v2/number"
|
|
S "github.com/IBM/fp-go/v2/semigroup"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// Test helper functions
|
|
func double(x int) int {
|
|
return x * 2
|
|
}
|
|
|
|
func increment(x int) int {
|
|
return x + 1
|
|
}
|
|
|
|
func square(x int) int {
|
|
return x * x
|
|
}
|
|
|
|
func negate(x int) int {
|
|
return -x
|
|
}
|
|
|
|
// TestCurry2 tests the Curry2 function
|
|
func TestCurry2(t *testing.T) {
|
|
add := func(x, y int) int {
|
|
return x + y
|
|
}
|
|
|
|
curriedAdd := Curry2(add)
|
|
addFive := curriedAdd(5)
|
|
|
|
result := addFive(10)
|
|
assert.Equal(t, 15, result, "Curry2 should curry binary function correctly")
|
|
|
|
// Test with different values
|
|
addTen := curriedAdd(10)
|
|
assert.Equal(t, 25, addTen(15), "Curry2 should work with different values")
|
|
}
|
|
|
|
// TestCurry3 tests the Curry3 function
|
|
func TestCurry3(t *testing.T) {
|
|
combine := func(x, y, z int) int {
|
|
return x + y + z
|
|
}
|
|
|
|
curriedCombine := Curry3(combine)
|
|
addTen := curriedCombine(5)(5)
|
|
|
|
result := addTen(20)
|
|
assert.Equal(t, 30, result, "Curry3 should curry ternary function correctly")
|
|
|
|
// Test with different values
|
|
addFifteen := curriedCombine(5)(10)
|
|
assert.Equal(t, 35, addFifteen(20), "Curry3 should work with different values")
|
|
}
|
|
|
|
// TestMonadAp tests the MonadAp function
|
|
func TestMonadAp(t *testing.T) {
|
|
// 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")
|
|
|
|
// 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")
|
|
|
|
// 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) {
|
|
// Ap is the curried version of MonadAp
|
|
// Ap(increment) returns a function that composes with increment (RIGHT-TO-LEFT)
|
|
applyIncrement := Ap(increment)
|
|
|
|
composed := applyIncrement(double)
|
|
result := composed(5)
|
|
assert.Equal(t, 12, result, "Ap should compose right-to-left: (5 + 1) * 2 = 12")
|
|
|
|
// 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")
|
|
|
|
// 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
|
|
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: 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: RIGHT-TO-LEFT execution
|
|
// MonadCompose(MonadCompose(double, increment), square) means: square, then increment, then double
|
|
complex := MonadCompose(MonadCompose(double, increment), square)
|
|
result3 := complex(5)
|
|
// 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 executes LEFT-TO-RIGHT (first arg first, second arg second)
|
|
chained := MonadChain(double, increment)
|
|
result := chained(5)
|
|
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 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 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 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 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 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 := N.Mul(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
|
|
func TestOf(t *testing.T) {
|
|
endo := Of(double)
|
|
result := endo(5)
|
|
assert.Equal(t, 10, result, "Of should convert function to endomorphism")
|
|
|
|
endo2 := Of(increment)
|
|
result2 := endo2(10)
|
|
assert.Equal(t, 11, result2, "Of should work with different functions")
|
|
}
|
|
|
|
// TestWrap tests the Wrap function (deprecated)
|
|
func TestWrap(t *testing.T) {
|
|
endo := Wrap(double)
|
|
result := endo(5)
|
|
assert.Equal(t, 10, result, "Wrap should convert function to endomorphism")
|
|
}
|
|
|
|
// TestUnwrap tests the Unwrap function (deprecated)
|
|
func TestUnwrap(t *testing.T) {
|
|
endo := Of(double)
|
|
unwrapped := Unwrap[func(int) int](endo)
|
|
result := unwrapped(5)
|
|
assert.Equal(t, 10, result, "Unwrap should convert endomorphism to function")
|
|
}
|
|
|
|
// TestIdentity tests the Identity function
|
|
func TestIdentity(t *testing.T) {
|
|
id := Identity[int]()
|
|
|
|
// Identity should return input unchanged
|
|
assert.Equal(t, 42, id(42), "Identity should return input unchanged")
|
|
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 (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")
|
|
|
|
// 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]()
|
|
assert.Equal(t, "hello", idStr("hello"), "Identity should work with strings")
|
|
}
|
|
|
|
// TestSemigroup tests the Semigroup function
|
|
func TestSemigroup(t *testing.T) {
|
|
sg := Semigroup[int]()
|
|
|
|
// 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, 12, result, "Semigroup concat should execute right-to-left: (5 + 1) * 2 = 12")
|
|
|
|
// Test associativity: (f . g) . h = f . (g . h)
|
|
f := double
|
|
g := increment
|
|
h := square
|
|
|
|
left := sg.Concat(sg.Concat(f, g), h)
|
|
right := sg.Concat(f, sg.Concat(g, h))
|
|
|
|
testValue := 3
|
|
assert.Equal(t, left(testValue), right(testValue), "Semigroup should be associative")
|
|
|
|
// 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)
|
|
// 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
|
|
func TestMonoid(t *testing.T) {
|
|
monoid := Monoid[int]()
|
|
|
|
// Test that empty is identity
|
|
empty := monoid.Empty()
|
|
assert.Equal(t, 42, empty(42), "Monoid empty should be identity")
|
|
|
|
// 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: empty(5) then double = 10")
|
|
|
|
// 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: double(5) then empty = 10")
|
|
|
|
// Test ConcatAll with multiple endomorphisms (RIGHT-TO-LEFT execution)
|
|
combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
|
|
result := combined(5)
|
|
// 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]{})
|
|
assert.Equal(t, 42, emptyResult(42), "ConcatAll with no args should return identity")
|
|
}
|
|
|
|
// TestMonoidLaws tests that the Monoid satisfies monoid laws
|
|
func TestMonoidLaws(t *testing.T) {
|
|
monoid := Monoid[int]()
|
|
empty := monoid.Empty()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
f Endomorphism[int]
|
|
g Endomorphism[int]
|
|
h Endomorphism[int]
|
|
}{
|
|
{"basic", double, increment, square},
|
|
{"with negate", negate, double, increment},
|
|
{"with identity", Identity[int](), double, increment},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
testValue := 5
|
|
|
|
// Right identity: x . empty = x
|
|
rightId := monoid.Concat(tc.f, empty)
|
|
assert.Equal(t, tc.f(testValue), rightId(testValue), "Right identity law")
|
|
|
|
// Left identity: empty . x = x
|
|
leftId := monoid.Concat(empty, tc.f)
|
|
assert.Equal(t, tc.f(testValue), leftId(testValue), "Left identity law")
|
|
|
|
// Associativity: (f . g) . h = f . (g . h)
|
|
left := monoid.Concat(monoid.Concat(tc.f, tc.g), tc.h)
|
|
right := monoid.Concat(tc.f, monoid.Concat(tc.g, tc.h))
|
|
assert.Equal(t, left(testValue), right(testValue), "Associativity law")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEndomorphismWithDifferentTypes tests endomorphisms with different types
|
|
func TestEndomorphismWithDifferentTypes(t *testing.T) {
|
|
// Test with strings (RIGHT-TO-LEFT execution)
|
|
addExclamation := func(s string) string {
|
|
return s + "!"
|
|
}
|
|
addPrefix := func(s string) string {
|
|
return "Hello, " + s
|
|
}
|
|
|
|
// Compose(addExclamation, addPrefix) means: addPrefix first, then addExclamation
|
|
strComposed := MonadCompose(addExclamation, addPrefix)
|
|
result := strComposed("World")
|
|
assert.Equal(t, "Hello, World!", result, "Compose should execute right-to-left with strings")
|
|
|
|
// Test with float64 (RIGHT-TO-LEFT execution)
|
|
doubleFloat := func(x float64) float64 {
|
|
return x * 2.0
|
|
}
|
|
addOne := func(x float64) float64 {
|
|
return x + 1.0
|
|
}
|
|
|
|
// Compose(doubleFloat, addOne) means: addOne first, then doubleFloat
|
|
floatComposed := MonadCompose(doubleFloat, addOne)
|
|
resultFloat := floatComposed(5.5)
|
|
// 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 (RIGHT-TO-LEFT execution)
|
|
// Innermost Compose is evaluated first in the composition chain
|
|
pipeline := MonadCompose(
|
|
MonadCompose(
|
|
MonadCompose(double, increment),
|
|
square,
|
|
),
|
|
negate,
|
|
)
|
|
|
|
// RIGHT-TO-LEFT: negate(5) = -5, square(-5) = 25, increment(25) = 26, double(26) = 52
|
|
result := pipeline(5)
|
|
assert.Equal(t, 52, result, "Complex composition should execute right-to-left")
|
|
|
|
// 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)
|
|
// 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
|
|
func TestOperatorType(t *testing.T) {
|
|
// 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] = applyTwice
|
|
doubleDouble := op(double)
|
|
|
|
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
|
|
func BenchmarkCompose(b *testing.B) {
|
|
composed := MonadCompose(double, increment)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
_ = composed(5)
|
|
}
|
|
}
|
|
|
|
// BenchmarkMonoidConcatAll benchmarks ConcatAll with monoid
|
|
// TestComposeVsChain demonstrates the key difference between Compose and Chain
|
|
func TestComposeVsChain(t *testing.T) {
|
|
double := N.Mul(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})
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
_ = combined(5)
|
|
}
|
|
}
|
|
|
|
// BenchmarkChain benchmarks the Chain function
|
|
func BenchmarkChain(b *testing.B) {
|
|
chainWithIncrement := Chain(increment)
|
|
chained := chainWithIncrement(double)
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
_ = 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 := N.Mul(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")
|
|
}
|