1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-17 23:37:41 +02:00
Files
fp-go/v2/endomorphism/from_test.go
Dr. Carsten Leue 34826d8c52 fix: Ask and add tests to retry
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-04 16:47:53 +01:00

440 lines
15 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"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
// TestFromSemigroup tests the FromSemigroup function with various semigroups
func TestFromSemigroup(t *testing.T) {
t.Run("integer addition semigroup", func(t *testing.T) {
// Create a semigroup for integer addition
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
// Convert to Kleisli arrow
addKleisli := FromSemigroup(addSemigroup)
// Create an endomorphism that adds 5
addFive := addKleisli(5)
// Test the endomorphism
assert.Equal(t, 15, addFive(10), "addFive(10) should equal 15")
assert.Equal(t, 5, addFive(0), "addFive(0) should equal 5")
assert.Equal(t, -5, addFive(-10), "addFive(-10) should equal -5")
})
t.Run("integer multiplication semigroup", func(t *testing.T) {
// Create a semigroup for integer multiplication
mulSemigroup := S.MakeSemigroup(func(a, b int) int {
return a * b
})
// Convert to Kleisli arrow
mulKleisli := FromSemigroup(mulSemigroup)
// Create an endomorphism that multiplies by 3
multiplyByThree := mulKleisli(3)
// Test the endomorphism
assert.Equal(t, 15, multiplyByThree(5), "multiplyByThree(5) should equal 15")
assert.Equal(t, 0, multiplyByThree(0), "multiplyByThree(0) should equal 0")
assert.Equal(t, -9, multiplyByThree(-3), "multiplyByThree(-3) should equal -9")
})
t.Run("string concatenation semigroup", func(t *testing.T) {
// Create a semigroup for string concatenation
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
// Convert to Kleisli arrow
concatKleisli := FromSemigroup(concatSemigroup)
// Create an endomorphism that appends "Hello, " (input is on the left)
appendHello := concatKleisli("Hello, ")
// Test the endomorphism - input is concatenated on the left, "Hello, " on the right
assert.Equal(t, "WorldHello, ", appendHello("World"), "appendHello('World') should equal 'WorldHello, '")
assert.Equal(t, "Hello, ", appendHello(""), "appendHello('') should equal 'Hello, '")
assert.Equal(t, "GoHello, ", appendHello("Go"), "appendHello('Go') should equal 'GoHello, '")
})
t.Run("slice concatenation semigroup", func(t *testing.T) {
// Create a semigroup for slice concatenation
sliceSemigroup := S.MakeSemigroup(func(a, b []int) []int {
result := make([]int, len(a)+len(b))
copy(result, a)
copy(result[len(a):], b)
return result
})
// Convert to Kleisli arrow
sliceKleisli := FromSemigroup(sliceSemigroup)
// Create an endomorphism that appends [1, 2] (input is on the left)
appendOneTwo := sliceKleisli([]int{1, 2})
// Test the endomorphism - input is concatenated on the left, [1,2] on the right
result1 := appendOneTwo([]int{3, 4, 5})
assert.Equal(t, []int{3, 4, 5, 1, 2}, result1, "appendOneTwo([3,4,5]) should equal [3,4,5,1,2]")
result2 := appendOneTwo([]int{})
assert.Equal(t, []int{1, 2}, result2, "appendOneTwo([]) should equal [1,2]")
result3 := appendOneTwo([]int{10})
assert.Equal(t, []int{10, 1, 2}, result3, "appendOneTwo([10]) should equal [10,1,2]")
})
t.Run("max semigroup", func(t *testing.T) {
// Create a semigroup for max operation
maxSemigroup := S.MakeSemigroup(func(a, b int) int {
if a > b {
return a
}
return b
})
// Convert to Kleisli arrow
maxKleisli := FromSemigroup(maxSemigroup)
// Create an endomorphism that takes max with 10
maxWithTen := maxKleisli(10)
// Test the endomorphism
assert.Equal(t, 15, maxWithTen(15), "maxWithTen(15) should equal 15")
assert.Equal(t, 10, maxWithTen(5), "maxWithTen(5) should equal 10")
assert.Equal(t, 10, maxWithTen(10), "maxWithTen(10) should equal 10")
assert.Equal(t, 10, maxWithTen(-5), "maxWithTen(-5) should equal 10")
})
t.Run("min semigroup", func(t *testing.T) {
// Create a semigroup for min operation
minSemigroup := S.MakeSemigroup(func(a, b int) int {
if a < b {
return a
}
return b
})
// Convert to Kleisli arrow
minKleisli := FromSemigroup(minSemigroup)
// Create an endomorphism that takes min with 10
minWithTen := minKleisli(10)
// Test the endomorphism
assert.Equal(t, 5, minWithTen(5), "minWithTen(5) should equal 5")
assert.Equal(t, 10, minWithTen(15), "minWithTen(15) should equal 10")
assert.Equal(t, 10, minWithTen(10), "minWithTen(10) should equal 10")
assert.Equal(t, -5, minWithTen(-5), "minWithTen(-5) should equal -5")
})
}
// TestFromSemigroupComposition tests that endomorphisms created from semigroups can be composed
func TestFromSemigroupComposition(t *testing.T) {
t.Run("compose addition endomorphisms", func(t *testing.T) {
// Create a semigroup for integer addition
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
// Create two endomorphisms
addFive := addKleisli(5)
addTen := addKleisli(10)
// Compose them (RIGHT-TO-LEFT execution)
composed := MonadCompose(addFive, addTen)
// Test composition: addTen first, then addFive
result := composed(3) // 3 + 10 = 13, then 13 + 5 = 18
assert.Equal(t, 18, result, "composed addition should work correctly")
})
t.Run("compose string endomorphisms", func(t *testing.T) {
// Create a semigroup for string concatenation
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
concatKleisli := FromSemigroup(concatSemigroup)
// Create two endomorphisms
appendHello := concatKleisli("Hello, ")
appendExclamation := concatKleisli("!")
// Compose them (RIGHT-TO-LEFT execution)
composed := MonadCompose(appendHello, appendExclamation)
// Test composition: appendExclamation first, then appendHello
// "World" + "!" = "World!", then "World!" + "Hello, " = "World!Hello, "
result := composed("World")
assert.Equal(t, "World!Hello, ", result, "composed string operations should work correctly")
})
}
// TestFromSemigroupWithMonoid tests using FromSemigroup-created endomorphisms with monoid operations
func TestFromSemigroupWithMonoid(t *testing.T) {
t.Run("monoid concat with addition endomorphisms", func(t *testing.T) {
// Create a semigroup for integer addition
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
// Create multiple endomorphisms
addOne := addKleisli(1)
addTwo := addKleisli(2)
addThree := addKleisli(3)
// Use monoid to combine them
monoid := Monoid[int]()
combined := monoid.Concat(monoid.Concat(addOne, addTwo), addThree)
// Test: RIGHT-TO-LEFT execution: addThree, then addTwo, then addOne
result := combined(10) // 10 + 3 = 13, 13 + 2 = 15, 15 + 1 = 16
assert.Equal(t, 16, result, "monoid combination should work correctly")
})
}
// TestFromSemigroupAssociativity tests that the semigroup associativity is preserved
func TestFromSemigroupAssociativity(t *testing.T) {
t.Run("addition associativity", func(t *testing.T) {
// Create a semigroup for integer addition
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
// Create three endomorphisms
addTwo := addKleisli(2)
addThree := addKleisli(3)
addFive := addKleisli(5)
// Test associativity: (a . b) . c = a . (b . c)
left := MonadCompose(MonadCompose(addTwo, addThree), addFive)
right := MonadCompose(addTwo, MonadCompose(addThree, addFive))
testValue := 10
assert.Equal(t, left(testValue), right(testValue), "composition should be associative")
// Both should equal: 10 + 5 + 3 + 2 = 20
assert.Equal(t, 20, left(testValue), "left composition should equal 20")
assert.Equal(t, 20, right(testValue), "right composition should equal 20")
})
t.Run("string concatenation associativity", func(t *testing.T) {
// Create a semigroup for string concatenation
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
concatKleisli := FromSemigroup(concatSemigroup)
// Create three endomorphisms
appendA := concatKleisli("A")
appendB := concatKleisli("B")
appendC := concatKleisli("C")
// Test associativity: (a . b) . c = a . (b . c)
left := MonadCompose(MonadCompose(appendA, appendB), appendC)
right := MonadCompose(appendA, MonadCompose(appendB, appendC))
testValue := "X"
assert.Equal(t, left(testValue), right(testValue), "string composition should be associative")
// Both should equal: "X" + "C" + "B" + "A" = "XCBA" (RIGHT-TO-LEFT composition)
assert.Equal(t, "XCBA", left(testValue), "left composition should equal 'XCBA'")
assert.Equal(t, "XCBA", right(testValue), "right composition should equal 'XCBA'")
})
}
// TestFromSemigroupEdgeCases tests edge cases and boundary conditions
func TestFromSemigroupEdgeCases(t *testing.T) {
t.Run("zero values", func(t *testing.T) {
// Test with addition and zero
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
addZero := addKleisli(0)
assert.Equal(t, 5, addZero(5), "adding zero should not change the value")
assert.Equal(t, 0, addZero(0), "adding zero to zero should be zero")
assert.Equal(t, -3, addZero(-3), "adding zero to negative should not change")
})
t.Run("empty string", func(t *testing.T) {
// Test with string concatenation and empty string
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
concatKleisli := FromSemigroup(concatSemigroup)
prependEmpty := concatKleisli("")
assert.Equal(t, "hello", prependEmpty("hello"), "prepending empty string should not change")
assert.Equal(t, "", prependEmpty(""), "prepending empty to empty should be empty")
})
t.Run("empty slice", func(t *testing.T) {
// Test with slice concatenation and empty slice
sliceSemigroup := S.MakeSemigroup(func(a, b []int) []int {
result := make([]int, len(a)+len(b))
copy(result, a)
copy(result[len(a):], b)
return result
})
sliceKleisli := FromSemigroup(sliceSemigroup)
prependEmpty := sliceKleisli([]int{})
result := prependEmpty([]int{1, 2, 3})
assert.Equal(t, []int{1, 2, 3}, result, "prepending empty slice should not change")
emptyResult := prependEmpty([]int{})
assert.Equal(t, []int{}, emptyResult, "prepending empty to empty should be empty")
})
}
// TestFromSemigroupDataLastPrinciple explicitly tests that FromSemigroup follows the "data last" principle
func TestFromSemigroupDataLastPrinciple(t *testing.T) {
t.Run("data last with string concatenation", func(t *testing.T) {
// Create a semigroup for string concatenation
// Concat(a, b) = a + b
concatSemigroup := S.MakeSemigroup(func(a, b string) string {
return a + b
})
// FromSemigroup uses Bind2of2, which binds the second parameter
// So FromSemigroup(s)(x) creates: func(input) = Concat(input, x)
// This is "data last" - the input data comes first, bound value comes last
kleisli := FromSemigroup(concatSemigroup)
// Bind "World" as the second parameter
appendWorld := kleisli("World")
// When we call appendWorld("Hello"), it computes Concat("Hello", "World")
// The input "Hello" is the first parameter (data), "World" is the second (bound value)
result := appendWorld("Hello")
assert.Equal(t, "HelloWorld", result, "Data last: Concat(input='Hello', bound='World') = 'HelloWorld'")
// Verify with different input
result2 := appendWorld("Goodbye")
assert.Equal(t, "GoodbyeWorld", result2, "Data last: Concat(input='Goodbye', bound='World') = 'GoodbyeWorld'")
})
t.Run("data last with integer addition", func(t *testing.T) {
// Create a semigroup for integer addition
// Concat(a, b) = a + b
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
// FromSemigroup binds the second parameter
// So FromSemigroup(s)(5) creates: func(input) = Concat(input, 5) = input + 5
kleisli := FromSemigroup(addSemigroup)
// Bind 5 as the second parameter
addFive := kleisli(5)
// When we call addFive(10), it computes Concat(10, 5) = 10 + 5 = 15
// The input 10 is the first parameter (data), 5 is the second (bound value)
result := addFive(10)
assert.Equal(t, 15, result, "Data last: Concat(input=10, bound=5) = 15")
})
t.Run("data last with non-commutative operation", func(t *testing.T) {
// Create a semigroup for a non-commutative operation to clearly show order
// Concat(a, b) = a - b (subtraction is not commutative)
subSemigroup := S.MakeSemigroup(func(a, b int) int {
return a - b
})
// FromSemigroup binds the second parameter
// So FromSemigroup(s)(5) creates: func(input) = Concat(input, 5) = input - 5
kleisli := FromSemigroup(subSemigroup)
// Bind 5 as the second parameter
subtractFive := kleisli(5)
// When we call subtractFive(10), it computes Concat(10, 5) = 10 - 5 = 5
// The input 10 is the first parameter (data), 5 is the second (bound value)
result := subtractFive(10)
assert.Equal(t, 5, result, "Data last: Concat(input=10, bound=5) = 10 - 5 = 5")
// If it were "data first" (binding first parameter), we would get:
// Concat(5, 10) = 5 - 10 = -5, which is NOT what we get
assert.NotEqual(t, -5, result, "Not data first: result is NOT Concat(bound=5, input=10) = 5 - 10 = -5")
})
t.Run("data last with list concatenation", func(t *testing.T) {
// Create a semigroup for list concatenation
// Concat(a, b) = a ++ b
listSemigroup := S.MakeSemigroup(func(a, b []int) []int {
result := make([]int, len(a)+len(b))
copy(result, a)
copy(result[len(a):], b)
return result
})
// FromSemigroup binds the second parameter
// So FromSemigroup(s)([3,4]) creates: func(input) = Concat(input, [3,4])
kleisli := FromSemigroup(listSemigroup)
// Bind [3, 4] as the second parameter
appendThreeFour := kleisli([]int{3, 4})
// When we call appendThreeFour([1,2]), it computes Concat([1,2], [3,4]) = [1,2,3,4]
// The input [1,2] is the first parameter (data), [3,4] is the second (bound value)
result := appendThreeFour([]int{1, 2})
assert.Equal(t, []int{1, 2, 3, 4}, result, "Data last: Concat(input=[1,2], bound=[3,4]) = [1,2,3,4]")
})
}
// BenchmarkFromSemigroup benchmarks the FromSemigroup function
func BenchmarkFromSemigroup(b *testing.B) {
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
addFive := addKleisli(5)
b.ResetTimer()
for b.Loop() {
_ = addFive(10)
}
}
// BenchmarkFromSemigroupComposition benchmarks composed endomorphisms from semigroups
func BenchmarkFromSemigroupComposition(b *testing.B) {
addSemigroup := S.MakeSemigroup(func(a, b int) int {
return a + b
})
addKleisli := FromSemigroup(addSemigroup)
addFive := addKleisli(5)
addTen := addKleisli(10)
composed := MonadCompose(addFive, addTen)
b.ResetTimer()
for b.Loop() {
_ = composed(3)
}
}