1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-23 22:14:53 +02:00
Files
fp-go/v2/semigroup/semigroup_test.go
Carsten Leue 3385c705dc Implement v2 using type aliases (#141)
* fix: initial checkin of v2

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: slowly migrate IO

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: migrate MonadTraverseArray and TraverseArray

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: migrate traversal

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: complete migration of IO

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: migrate ioeither

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: refactorY

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: next step in migration

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: adjust IO generation code

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: get rid of more IO methods

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: get rid of more IO

* fix: convert iooption

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: convert reader

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: convert a bit of reader

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: new build script

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: cleanup

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: reformat

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: simplify

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: some cleanup

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: adjust Pair to Haskell semantic

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: documentation and testcases

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: some performance optimizations

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: remove coverage

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

* fix: better doc

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>

---------

Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-06 09:27:00 +01:00

447 lines
12 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 semigroup
import (
"testing"
M "github.com/IBM/fp-go/v2/magma"
"github.com/stretchr/testify/assert"
)
// Test basic First semigroup
func TestFirst(t *testing.T) {
first := First[int]()
assert.Equal(t, 1, first.Concat(1, 2))
assert.Equal(t, 10, first.Concat(10, 20))
assert.Equal(t, "a", First[string]().Concat("a", "b"))
}
// Test basic Last semigroup
func TestLast(t *testing.T) {
last := Last[int]()
assert.Equal(t, 2, last.Concat(1, 2))
assert.Equal(t, 20, last.Concat(10, 20))
assert.Equal(t, "b", Last[string]().Concat("a", "b"))
}
// Test MakeSemigroup
func TestMakeSemigroup(t *testing.T) {
// Integer addition semigroup
add := MakeSemigroup(func(a, b int) int { return a + b })
assert.Equal(t, 5, add.Concat(2, 3))
assert.Equal(t, 10, add.Concat(4, 6))
// String concatenation semigroup
concat := MakeSemigroup(func(a, b string) string { return a + b })
assert.Equal(t, "hello", concat.Concat("hel", "lo"))
assert.Equal(t, "foobar", concat.Concat("foo", "bar"))
// Max semigroup
max := MakeSemigroup(func(a, b int) int {
if a > b {
return a
}
return b
})
assert.Equal(t, 10, max.Concat(5, 10))
assert.Equal(t, 20, max.Concat(20, 15))
}
// Test Reverse semigroup
func TestReverse(t *testing.T) {
// Subtraction is not commutative, so reverse changes the result
sub := MakeSemigroup(func(a, b int) int { return a - b })
reversed := Reverse(sub)
assert.Equal(t, 7, sub.Concat(10, 3)) // 10 - 3 = 7
assert.Equal(t, -7, reversed.Concat(10, 3)) // 3 - 10 = -7
// String concatenation
concat := MakeSemigroup(func(a, b string) string { return a + b })
reversedConcat := Reverse(concat)
assert.Equal(t, "ab", concat.Concat("a", "b"))
assert.Equal(t, "ba", reversedConcat.Concat("a", "b"))
}
// Test FunctionSemigroup
func TestFunctionSemigroup(t *testing.T) {
// Base semigroup for integers (addition)
add := MakeSemigroup(func(a, b int) int { return a + b })
// Lift to functions
funcSG := FunctionSemigroup[string](add)
// Create two functions
f := func(s string) int { return len(s) }
g := func(s string) int { return len(s) * 2 }
// Combine functions
combined := funcSG.Concat(f, g)
// Test with different strings
assert.Equal(t, 15, combined("hello")) // 5 + 10 = 15
assert.Equal(t, 9, combined("abc")) // 3 + 6 = 9
assert.Equal(t, 0, combined("")) // 0 + 0 = 0
}
// Test FunctionSemigroup with different types
func TestFunctionSemigroupMultipleTypes(t *testing.T) {
// String concatenation semigroup
concat := MakeSemigroup(func(a, b string) string { return a + b })
// Lift to functions from int to string
funcSG := FunctionSemigroup[int](concat)
f := func(n int) string { return "a" }
g := func(n int) string { return "b" }
combined := funcSG.Concat(f, g)
assert.Equal(t, "ab", combined(42))
}
// Test ToMagma conversion
func TestToMagma(t *testing.T) {
sg := MakeSemigroup(func(a, b int) int { return a + b })
magma := ToMagma(sg)
// Should work as a magma
assert.Equal(t, 5, magma.Concat(2, 3))
assert.Equal(t, 10, magma.Concat(4, 6))
// Verify it's a Magma interface
var _ M.Magma[int] = magma
}
// Test ConcatAll
func TestConcatAll(t *testing.T) {
add := MakeSemigroup(func(a, b int) int { return a + b })
concatAll := ConcatAll(add)
// Test with various arrays
assert.Equal(t, 10, concatAll(0)([]int{1, 2, 3, 4}))
assert.Equal(t, 20, concatAll(10)([]int{1, 2, 3, 4}))
assert.Equal(t, 5, concatAll(5)([]int{}))
assert.Equal(t, 15, concatAll(0)([]int{15}))
// Test with string concatenation
concat := MakeSemigroup(func(a, b string) string { return a + b })
concatAllStr := ConcatAll(concat)
assert.Equal(t, "hello", concatAllStr("")([]string{"h", "e", "l", "l", "o"}))
assert.Equal(t, "prefix_abc", concatAllStr("prefix_")([]string{"a", "b", "c"}))
}
// Test MonadConcatAll
func TestMonadConcatAll(t *testing.T) {
add := MakeSemigroup(func(a, b int) int { return a + b })
monadConcatAll := MonadConcatAll(add)
// Test with various arrays
assert.Equal(t, 10, monadConcatAll([]int{1, 2, 3, 4}, 0))
assert.Equal(t, 20, monadConcatAll([]int{1, 2, 3, 4}, 10))
assert.Equal(t, 5, monadConcatAll([]int{}, 5))
assert.Equal(t, 15, monadConcatAll([]int{15}, 0))
// Test with multiplication
mul := MakeSemigroup(func(a, b int) int { return a * b })
monadConcatAllMul := MonadConcatAll(mul)
assert.Equal(t, 24, monadConcatAllMul([]int{2, 3, 4}, 1))
assert.Equal(t, 120, monadConcatAllMul([]int{2, 3, 4, 5}, 1))
}
// Test GenericConcatAll with custom slice type
func TestGenericConcatAll(t *testing.T) {
type MyInts []int
add := MakeSemigroup(func(a, b int) int { return a + b })
concatAll := GenericConcatAll[MyInts](add)
assert.Equal(t, 6, concatAll(0)(MyInts{1, 2, 3}))
assert.Equal(t, 16, concatAll(10)(MyInts{1, 2, 3}))
assert.Equal(t, 5, concatAll(5)(MyInts{}))
}
// Test GenericMonadConcatAll with custom slice type
func TestGenericMonadConcatAll(t *testing.T) {
type MyInts []int
add := MakeSemigroup(func(a, b int) int { return a + b })
monadConcatAll := GenericMonadConcatAll[MyInts](add)
assert.Equal(t, 6, monadConcatAll(MyInts{1, 2, 3}, 0))
assert.Equal(t, 16, monadConcatAll(MyInts{1, 2, 3}, 10))
assert.Equal(t, 5, monadConcatAll(MyInts{}, 5))
}
// Test ApplySemigroup
func TestApplySemigroup(t *testing.T) {
// Base semigroup for integers
add := MakeSemigroup(func(a, b int) int { return a + b })
// Simple HKT simulation using slices
type HKT []int
fmap := func(hkt HKT, f func(int) func(int) int) []func(int) int {
result := make([]func(int) int, len(hkt))
for i, v := range hkt {
result[i] = f(v)
}
return result
}
fap := func(fs []func(int) int, hkt HKT) HKT {
result := make(HKT, 0)
for _, f := range fs {
for _, v := range hkt {
result = append(result, f(v))
}
}
return result
}
applySG := ApplySemigroup[int, HKT, []func(int) int](fmap, fap, add)
hkt1 := HKT{1, 2}
hkt2 := HKT{3, 4}
result := applySG.Concat(hkt1, hkt2)
// Should apply the semigroup operation to all combinations
assert.NotEmpty(t, result)
}
// Test AltSemigroup
func TestAltSemigroup(t *testing.T) {
// Simple HKT simulation using Option-like type
type Option[A any] struct {
value A
hasValue bool
}
falt := func(first Option[int], second func() Option[int]) Option[int] {
if first.hasValue {
return first
}
return second()
}
altSG := AltSemigroup[Option[int], func() Option[int]](falt)
some := Option[int]{value: 42, hasValue: true}
none := Option[int]{hasValue: false}
other := Option[int]{value: 100, hasValue: true}
// First has value, should return first
result1 := altSG.Concat(some, none)
assert.True(t, result1.hasValue)
assert.Equal(t, 42, result1.value)
// First is none, should return second
result2 := altSG.Concat(none, other)
assert.True(t, result2.hasValue)
assert.Equal(t, 100, result2.value)
// Both have values, should return first
result3 := altSG.Concat(some, other)
assert.True(t, result3.hasValue)
assert.Equal(t, 42, result3.value)
}
// Test associativity law for various semigroups
func TestAssociativityLaw(t *testing.T) {
testCases := []struct {
name string
sg Semigroup[int]
a, b, c int
}{
{"Addition", MakeSemigroup(func(a, b int) int { return a + b }), 1, 2, 3},
{"Multiplication", MakeSemigroup(func(a, b int) int { return a * b }), 2, 3, 4},
{"Max", MakeSemigroup(func(a, b int) int {
if a > b {
return a
}
return b
}), 5, 10, 3},
{"Min", MakeSemigroup(func(a, b int) int {
if a < b {
return a
}
return b
}), 5, 10, 3},
{"First", First[int](), 1, 2, 3},
{"Last", Last[int](), 1, 2, 3},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// (a • b) • c
left := tc.sg.Concat(tc.sg.Concat(tc.a, tc.b), tc.c)
// a • (b • c)
right := tc.sg.Concat(tc.a, tc.sg.Concat(tc.b, tc.c))
assert.Equal(t, left, right, "Associativity law failed for %s", tc.name)
})
}
}
// Test associativity law for string semigroups
func TestAssociativityLawString(t *testing.T) {
concat := MakeSemigroup(func(a, b string) string { return a + b })
a, b, c := "hello", " ", "world"
left := concat.Concat(concat.Concat(a, b), c)
right := concat.Concat(a, concat.Concat(b, c))
assert.Equal(t, left, right)
assert.Equal(t, "hello world", left)
}
// Test complex types
func TestComplexTypes(t *testing.T) {
type Config struct {
Timeout int
Retries int
}
configSG := MakeSemigroup(func(a, b Config) Config {
maxTimeout := a.Timeout
if b.Timeout > maxTimeout {
maxTimeout = b.Timeout
}
return Config{
Timeout: maxTimeout,
Retries: a.Retries + b.Retries,
}
})
c1 := Config{Timeout: 30, Retries: 3}
c2 := Config{Timeout: 60, Retries: 5}
c3 := Config{Timeout: 45, Retries: 2}
result := configSG.Concat(configSG.Concat(c1, c2), c3)
assert.Equal(t, 60, result.Timeout)
assert.Equal(t, 10, result.Retries)
// Test associativity
left := configSG.Concat(configSG.Concat(c1, c2), c3)
right := configSG.Concat(c1, configSG.Concat(c2, c3))
assert.Equal(t, left, right)
}
// Test with slices
func TestSliceSemigroup(t *testing.T) {
sliceConcat := MakeSemigroup(func(a, b []int) []int {
result := make([]int, len(a)+len(b))
copy(result, a)
copy(result[len(a):], b)
return result
})
s1 := []int{1, 2, 3}
s2 := []int{4, 5}
s3 := []int{6}
result := sliceConcat.Concat(sliceConcat.Concat(s1, s2), s3)
assert.Equal(t, []int{1, 2, 3, 4, 5, 6}, result)
// Test associativity
left := sliceConcat.Concat(sliceConcat.Concat(s1, s2), s3)
right := sliceConcat.Concat(s1, sliceConcat.Concat(s2, s3))
assert.Equal(t, left, right)
}
// Test with maps
func TestMapSemigroup(t *testing.T) {
mapMerge := MakeSemigroup(func(a, b map[string]int) map[string]int {
result := make(map[string]int)
for k, v := range a {
result[k] = v
}
for k, v := range b {
result[k] = v // Later values override
}
return result
})
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"b": 3, "c": 4}
m3 := map[string]int{"c": 5, "d": 6}
result := mapMerge.Concat(mapMerge.Concat(m1, m2), m3)
assert.Equal(t, 1, result["a"])
assert.Equal(t, 3, result["b"])
assert.Equal(t, 5, result["c"])
assert.Equal(t, 6, result["d"])
}
// Benchmark tests
func BenchmarkFirst(b *testing.B) {
first := First[int]()
for i := 0; i < b.N; i++ {
first.Concat(1, 2)
}
}
func BenchmarkLast(b *testing.B) {
last := Last[int]()
for i := 0; i < b.N; i++ {
last.Concat(1, 2)
}
}
func BenchmarkMakeSemigroupAdd(b *testing.B) {
add := MakeSemigroup(func(a, b int) int { return a + b })
for i := 0; i < b.N; i++ {
add.Concat(1, 2)
}
}
func BenchmarkReverse(b *testing.B) {
sub := MakeSemigroup(func(a, b int) int { return a - b })
reversed := Reverse(sub)
for i := 0; i < b.N; i++ {
reversed.Concat(10, 3)
}
}
func BenchmarkConcatAll(b *testing.B) {
add := MakeSemigroup(func(a, b int) int { return a + b })
concatAll := ConcatAll(add)
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
b.ResetTimer()
for i := 0; i < b.N; i++ {
concatAll(0)(arr)
}
}
func BenchmarkFunctionSemigroup(b *testing.B) {
add := MakeSemigroup(func(a, b int) int { return a + b })
funcSG := FunctionSemigroup[string](add)
f := func(s string) int { return len(s) }
g := func(s string) int { return len(s) * 2 }
combined := funcSG.Concat(f, g)
b.ResetTimer()
for i := 0; i < b.N; i++ {
combined("hello")
}
}