1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-23 22:14:53 +02:00
Files
fp-go/v2/monoid/monoid_test.go
Dr. Carsten Leue ed108812d6 fix: modernize codebase
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-15 17:00:22 +01:00

717 lines
17 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 monoid
import (
"math"
"testing"
S "github.com/IBM/fp-go/v2/semigroup"
"github.com/stretchr/testify/assert"
)
// assertMonoidLaws checks monoid laws for a given value
func assertMonoidLaws[A any](t *testing.T, m Monoid[A], a A) bool {
e := m.Empty()
return assert.Equal(t, a, m.Concat(a, e), "Monoid right identity") &&
assert.Equal(t, a, m.Concat(e, a), "Monoid left identity")
}
// assertMonoidLawsForAll checks monoid laws for all values in a slice
func assertMonoidLawsForAll[A any](t *testing.T, m Monoid[A], data []A) bool {
result := true
for _, value := range data {
result = result && assertMonoidLaws(t, m, value)
}
return result
}
// Test MakeMonoid creates a valid monoid
func TestMakeMonoid(t *testing.T) {
// Integer addition monoid
addMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
assert.Equal(t, 8, addMonoid.Concat(5, 3))
assert.Equal(t, 0, addMonoid.Empty())
// Integer multiplication monoid
mulMonoid := MakeMonoid(
func(a, b int) int { return a * b },
1,
)
assert.Equal(t, 15, mulMonoid.Concat(5, 3))
assert.Equal(t, 1, mulMonoid.Empty())
// String concatenation monoid
stringMonoid := MakeMonoid(
func(a, b string) string { return a + b },
"",
)
assert.Equal(t, "HelloWorld", stringMonoid.Concat("Hello", "World"))
assert.Equal(t, "", stringMonoid.Empty())
}
// Test monoid laws for integer addition
func TestMonoidLaws_IntAddition(t *testing.T) {
addMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
testData := []int{0, 1, -1, 5, 10, -10, 100}
// Test using helper function
assert.True(t, assertMonoidLawsForAll(t, addMonoid, testData))
// Explicit law tests
for _, x := range testData {
// Left identity: Empty() + x = x
assert.Equal(t, x, addMonoid.Concat(addMonoid.Empty(), x),
"Left identity failed for %d", x)
// Right identity: x + Empty() = x
assert.Equal(t, x, addMonoid.Concat(x, addMonoid.Empty()),
"Right identity failed for %d", x)
}
// Associativity (from Semigroup)
assert.Equal(t,
addMonoid.Concat(addMonoid.Concat(1, 2), 3),
addMonoid.Concat(1, addMonoid.Concat(2, 3)),
)
}
// Test monoid laws for integer multiplication
func TestMonoidLaws_IntMultiplication(t *testing.T) {
mulMonoid := MakeMonoid(
func(a, b int) int { return a * b },
1,
)
testData := []int{1, 2, 3, 5, 10}
assert.True(t, assertMonoidLawsForAll(t, mulMonoid, testData))
// Explicit tests
for _, x := range testData {
assert.Equal(t, x, mulMonoid.Concat(mulMonoid.Empty(), x))
assert.Equal(t, x, mulMonoid.Concat(x, mulMonoid.Empty()))
}
}
// Test monoid laws for string concatenation
func TestMonoidLaws_String(t *testing.T) {
stringMonoid := MakeMonoid(
func(a, b string) string { return a + b },
"",
)
testData := []string{"", "a", "hello", "world", "test"}
assert.True(t, assertMonoidLawsForAll(t, stringMonoid, testData))
}
// Test monoid laws for boolean AND
func TestMonoidLaws_BooleanAnd(t *testing.T) {
andMonoid := MakeMonoid(
func(a, b bool) bool { return a && b },
true,
)
testData := []bool{true, false}
assert.True(t, assertMonoidLawsForAll(t, andMonoid, testData))
// Specific tests
assert.Equal(t, true, andMonoid.Concat(true, true))
assert.Equal(t, false, andMonoid.Concat(true, false))
assert.Equal(t, false, andMonoid.Concat(false, true))
assert.Equal(t, false, andMonoid.Concat(false, false))
assert.Equal(t, true, andMonoid.Empty())
}
// Test monoid laws for boolean OR
func TestMonoidLaws_BooleanOr(t *testing.T) {
orMonoid := MakeMonoid(
func(a, b bool) bool { return a || b },
false,
)
testData := []bool{true, false}
assert.True(t, assertMonoidLawsForAll(t, orMonoid, testData))
// Specific tests
assert.Equal(t, true, orMonoid.Concat(true, true))
assert.Equal(t, true, orMonoid.Concat(true, false))
assert.Equal(t, true, orMonoid.Concat(false, true))
assert.Equal(t, false, orMonoid.Concat(false, false))
assert.Equal(t, false, orMonoid.Empty())
}
// Test Reverse swaps argument order
func TestReverse(t *testing.T) {
// Subtraction is not commutative, so reverse will give different results
subMonoid := MakeMonoid(
func(a, b int) int { return a - b },
0,
)
reversedMonoid := Reverse(subMonoid)
// Original: 10 - 3 = 7
assert.Equal(t, 7, subMonoid.Concat(10, 3))
// Reversed: 3 - 10 = -7
assert.Equal(t, -7, reversedMonoid.Concat(10, 3))
// Empty should be the same
assert.Equal(t, subMonoid.Empty(), reversedMonoid.Empty())
// Test with string concatenation
stringMonoid := MakeMonoid(
func(a, b string) string { return a + b },
"",
)
reversedString := Reverse(stringMonoid)
assert.Equal(t, "HelloWorld", stringMonoid.Concat("Hello", "World"))
assert.Equal(t, "WorldHello", reversedString.Concat("Hello", "World"))
}
// Test Reverse preserves monoid laws
func TestReverse_PreservesLaws(t *testing.T) {
addMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
reversedMonoid := Reverse(addMonoid)
testData := []int{0, 1, 5, 10}
// Reversed monoid should still satisfy monoid laws
assert.True(t, assertMonoidLawsForAll(t, reversedMonoid, testData))
}
// Test ToSemigroup conversion
func TestToSemigroup(t *testing.T) {
addMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
sg := ToSemigroup(addMonoid)
// Should work as a semigroup
assert.Equal(t, 8, sg.Concat(5, 3))
assert.Equal(t, 15, sg.Concat(10, 5))
// Verify it's the same underlying operation
assert.Equal(t, addMonoid.Concat(5, 3), sg.Concat(5, 3))
}
// Test ConcatAll with various inputs
func TestConcatAll(t *testing.T) {
addMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
concatAll := ConcatAll(addMonoid)
tests := []struct {
name string
input []int
expected int
}{
{"empty slice", []int{}, 0},
{"single element", []int{5}, 5},
{"multiple elements", []int{1, 2, 3, 4, 5}, 15},
{"with zeros", []int{0, 5, 0, 3}, 8},
{"negative numbers", []int{-1, -2, -3}, -6},
{"mixed signs", []int{10, -5, 3, -2}, 6},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := concatAll(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// Test ConcatAll with multiplication
func TestConcatAll_Multiplication(t *testing.T) {
mulMonoid := MakeMonoid(
func(a, b int) int { return a * b },
1,
)
concatAll := ConcatAll(mulMonoid)
tests := []struct {
name string
input []int
expected int
}{
{"empty slice", []int{}, 1},
{"single element", []int{5}, 5},
{"multiple elements", []int{2, 3, 4}, 24},
{"with one", []int{1, 5, 1, 3}, 15},
{"with zero", []int{2, 0, 3}, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := concatAll(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// Test ConcatAll with strings
func TestConcatAll_String(t *testing.T) {
stringMonoid := MakeMonoid(
func(a, b string) string { return a + b },
"",
)
concatAll := ConcatAll(stringMonoid)
tests := []struct {
name string
input []string
expected string
}{
{"empty slice", []string{}, ""},
{"single element", []string{"hello"}, "hello"},
{"multiple elements", []string{"Hello", " ", "World", "!"}, "Hello World!"},
{"with empty strings", []string{"a", "", "b", "", "c"}, "abc"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := concatAll(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// Test Fold (alias for ConcatAll)
func TestFold(t *testing.T) {
addMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
fold := Fold(addMonoid)
concatAll := ConcatAll(addMonoid)
input := []int{1, 2, 3, 4, 5}
// Fold and ConcatAll should produce the same result
assert.Equal(t, concatAll(input), fold(input))
assert.Equal(t, 15, fold(input))
}
// Test GenericConcatAll with custom slice type
func TestGenericConcatAll(t *testing.T) {
type IntSlice []int
addMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
concatAll := GenericConcatAll[IntSlice](addMonoid)
tests := []struct {
name string
input IntSlice
expected int
}{
{"empty slice", IntSlice{}, 0},
{"single element", IntSlice{5}, 5},
{"multiple elements", IntSlice{1, 2, 3, 4, 5}, 15},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := concatAll(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// Test FunctionMonoid
func TestFunctionMonoid(t *testing.T) {
// Create a monoid for functions that return integers
intAddMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
funcMonoid := FunctionMonoid[string](intAddMonoid)
// Create some functions
f1 := func(s string) int { return len(s) }
f2 := func(s string) int { return len(s) * 2 }
f3 := func(s string) int { return 10 }
// Combine functions
combined := funcMonoid.Concat(f1, f2)
// Test combined function
assert.Equal(t, 15, combined("hello")) // len("hello") + len("hello")*2 = 5 + 10 = 15
assert.Equal(t, 9, combined("abc")) // len("abc") + len("abc")*2 = 3 + 6 = 9
// Test with three functions
combined3 := funcMonoid.Concat(funcMonoid.Concat(f1, f2), f3)
assert.Equal(t, 25, combined3("hello")) // 5 + 10 + 10 = 25
// Test empty function
emptyFunc := funcMonoid.Empty()
assert.Equal(t, 0, emptyFunc("anything"))
assert.Equal(t, 0, emptyFunc(""))
// Test identity laws
input := "test"
assert.Equal(t, f1(input), funcMonoid.Concat(funcMonoid.Empty(), f1)(input))
assert.Equal(t, f1(input), funcMonoid.Concat(f1, funcMonoid.Empty())(input))
}
// Test FunctionMonoid with different types
func TestFunctionMonoid_DifferentTypes(t *testing.T) {
// Monoid for functions that return booleans (OR)
boolOrMonoid := MakeMonoid(
func(a, b bool) bool { return a || b },
false,
)
funcMonoid := FunctionMonoid[int](boolOrMonoid)
isEven := func(n int) bool { return n%2 == 0 }
isPositive := func(n int) bool { return n > 0 }
// Combine: returns true if number is even OR positive
combined := funcMonoid.Concat(isEven, isPositive)
assert.True(t, combined(4)) // even and positive
assert.True(t, combined(3)) // odd but positive
assert.True(t, combined(-2)) // even but negative
assert.False(t, combined(-3)) // odd and negative
}
// Test ApplicativeMonoid with a simple applicative
func TestApplicativeMonoid(t *testing.T) {
// Simple Option-like type for testing
type Option[A any] struct {
value *A
}
some := func(a int) Option[int] {
return Option[int]{value: &a}
}
none := func() Option[int] {
return Option[int]{value: nil}
}
fmap := func(opt Option[int], f func(int) func(int) int) Option[func(int) int] {
if opt.value == nil {
return Option[func(int) int]{value: nil}
}
fn := f(*opt.value)
return Option[func(int) int]{value: &fn}
}
fap := func(optF Option[func(int) int], opt Option[int]) Option[int] {
if optF.value == nil || opt.value == nil {
return none()
}
result := (*optF.value)(*opt.value)
return some(result)
}
intAddMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
optMonoid := ApplicativeMonoid(some, fmap, fap, intAddMonoid)
// Test concat of Some values
opt1 := some(5)
opt2 := some(3)
result := optMonoid.Concat(opt1, opt2)
assert.NotNil(t, result.value)
assert.Equal(t, 8, *result.value)
// Test empty
empty := optMonoid.Empty()
assert.NotNil(t, empty.value)
assert.Equal(t, 0, *empty.value)
// Test identity laws
assert.Equal(t, opt1, optMonoid.Concat(optMonoid.Empty(), opt1))
assert.Equal(t, opt1, optMonoid.Concat(opt1, optMonoid.Empty()))
}
// Test AlternativeMonoid
func TestAlternativeMonoid(t *testing.T) {
// Simple Option-like type for testing
type Option[A any] struct {
value *A
}
some := func(a int) Option[int] {
return Option[int]{value: &a}
}
none := func() Option[int] {
return Option[int]{value: nil}
}
fmap := func(opt Option[int], f func(int) func(int) int) Option[func(int) int] {
if opt.value == nil {
return Option[func(int) int]{value: nil}
}
fn := f(*opt.value)
return Option[func(int) int]{value: &fn}
}
fap := func(optF Option[func(int) int], opt Option[int]) Option[int] {
if optF.value == nil || opt.value == nil {
return none()
}
result := (*optF.value)(*opt.value)
return some(result)
}
falt := func(first Option[int], second func() Option[int]) Option[int] {
if first.value != nil {
return first
}
return second()
}
intAddMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
optMonoid := AlternativeMonoid(some, fmap, fap, falt, intAddMonoid)
// Test concat of Some values - should add them
opt1 := some(5)
opt2 := some(3)
result := optMonoid.Concat(opt1, opt2)
assert.NotNil(t, result.value)
assert.Equal(t, 8, *result.value)
// Test concat with None - should fall back
result2 := optMonoid.Concat(none(), opt2)
assert.NotNil(t, result2.value)
assert.Equal(t, 3, *result2.value)
// Test concat: first Some wins in alternative
result3 := optMonoid.Concat(opt1, opt2)
assert.NotNil(t, result3.value)
// The result should be 8 (5 + 3) from applicative behavior
// Test empty
empty := optMonoid.Empty()
assert.NotNil(t, empty.value)
assert.Equal(t, 0, *empty.value)
// Test identity laws
assert.Equal(t, opt1, optMonoid.Concat(optMonoid.Empty(), opt1))
assert.Equal(t, opt1, optMonoid.Concat(opt1, optMonoid.Empty()))
}
// Test AltMonoid
func TestAltMonoid(t *testing.T) {
// Simple Option-like type
type Option[A any] struct {
value *A
}
some := func(a int) Option[int] {
return Option[int]{value: &a}
}
none := func() Option[int] {
return Option[int]{value: nil}
}
falt := func(first Option[int], second func() Option[int]) Option[int] {
if first.value != nil {
return first
}
return second()
}
optMonoid := AltMonoid(none, falt)
// Test concat: first Some wins
opt1 := some(5)
opt2 := some(3)
result := optMonoid.Concat(opt1, opt2)
assert.NotNil(t, result.value)
assert.Equal(t, 5, *result.value)
// Test concat: None falls back to second
result2 := optMonoid.Concat(none(), opt2)
assert.NotNil(t, result2.value)
assert.Equal(t, 3, *result2.value)
// Test concat: both None
result3 := optMonoid.Concat(none(), none())
assert.Nil(t, result3.value)
// Test empty
empty := optMonoid.Empty()
assert.Nil(t, empty.value)
}
// Test monoid with max operation
func TestMonoid_Max(t *testing.T) {
maxMonoid := MakeMonoid(
func(a, b int) int {
if a > b {
return a
}
return b
},
math.MinInt,
)
numbers := []int{3, 7, 2, 9, 1, 5}
maximum := ConcatAll(maxMonoid)(numbers)
assert.Equal(t, 9, maximum)
// Empty slice returns identity (MinInt)
emptyMax := ConcatAll(maxMonoid)([]int{})
assert.Equal(t, math.MinInt, emptyMax)
}
// Test monoid with min operation
func TestMonoid_Min(t *testing.T) {
minMonoid := MakeMonoid(
func(a, b int) int {
if a < b {
return a
}
return b
},
math.MaxInt,
)
numbers := []int{3, 7, 2, 9, 1, 5}
minimum := ConcatAll(minMonoid)(numbers)
assert.Equal(t, 1, minimum)
// Empty slice returns identity (MaxInt)
emptyMin := ConcatAll(minMonoid)([]int{})
assert.Equal(t, math.MaxInt, emptyMin)
}
// Test monoid with list concatenation
func TestMonoid_ListConcat(t *testing.T) {
listMonoid := MakeMonoid(
func(a, b []int) []int {
result := make([]int, len(a)+len(b))
copy(result, a)
copy(result[len(a):], b)
return result
},
[]int{},
)
lists := [][]int{{1, 2}, {3, 4}, {5}}
flattened := ConcatAll(listMonoid)(lists)
assert.Equal(t, []int{1, 2, 3, 4, 5}, flattened)
// Empty slice returns empty list
emptyList := ConcatAll(listMonoid)([][]int{})
assert.Equal(t, []int{}, emptyList)
// Test identity laws
list := []int{1, 2, 3}
assert.Equal(t, list, listMonoid.Concat(listMonoid.Empty(), list))
assert.Equal(t, list, listMonoid.Concat(list, listMonoid.Empty()))
}
// Test that monoid interface is properly implemented
func TestMonoidInterface(t *testing.T) {
addMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
// Verify it implements Monoid interface
var _ Monoid[int] = addMonoid
// Verify it also implements Semigroup interface (through embedding)
var _ S.Semigroup[int] = addMonoid
}
// Benchmark ConcatAll
func BenchmarkConcatAll(b *testing.B) {
addMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
concatAll := ConcatAll(addMonoid)
numbers := make([]int, 1000)
for i := range numbers {
numbers[i] = i
}
b.ResetTimer()
for b.Loop() {
_ = concatAll(numbers)
}
}
// Benchmark FunctionMonoid
func BenchmarkFunctionMonoid(b *testing.B) {
intAddMonoid := MakeMonoid(
func(a, b int) int { return a + b },
0,
)
funcMonoid := FunctionMonoid[string](intAddMonoid)
f1 := func(s string) int { return len(s) }
f2 := func(s string) int { return len(s) * 2 }
combined := funcMonoid.Concat(f1, f2)
b.ResetTimer()
for b.Loop() {
_ = combined("benchmark")
}
}