1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-01-15 00:53:10 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Dr. Carsten Leue
7f2e76dd94 fix: implement monoid for Pair
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 22:32:33 +01:00
Dr. Carsten Leue
77965a12ff fix: implement monoid for Pair
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-01-12 22:32:04 +01:00
4 changed files with 1283 additions and 0 deletions

230
v2/pair/monoid.go Normal file
View File

@@ -0,0 +1,230 @@
// Copyright (c) 2024 - 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 pair
import (
F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
)
// ApplicativeMonoid creates a monoid for [Pair] using applicative functor operations on the tail.
//
// This is an alias for [ApplicativeMonoidTail], which lifts the right (tail) monoid into the
// Pair applicative functor. The left monoid provides the semigroup for combining head values
// during applicative operations.
//
// IMPORTANT: The three monoid constructors (ApplicativeMonoid/ApplicativeMonoidTail and
// ApplicativeMonoidHead) produce DIFFERENT results:
// - ApplicativeMonoidTail: Combines head values in REVERSE order (right-to-left)
// - ApplicativeMonoidHead: Combines tail values in REVERSE order (right-to-left)
// - The "focused" component (tail for Tail, head for Head) combines in normal order (left-to-right)
//
// This difference is significant for non-commutative operations like string concatenation.
//
// Parameters:
// - l: A monoid for the head (left) values of type L
// - r: A monoid for the tail (right) values of type R
//
// Returns:
// - A Monoid[Pair[L, R]] that combines pairs using applicative operations on the tail
//
// Example:
//
// import (
// N "github.com/IBM/fp-go/v2/number"
// S "github.com/IBM/fp-go/v2/string"
// )
//
// intAdd := N.MonoidSum[int]()
// strConcat := S.Monoid
//
// pairMonoid := pair.ApplicativeMonoid(intAdd, strConcat)
//
// p1 := pair.MakePair(10, "foo")
// p2 := pair.MakePair(20, "bar")
//
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[int, string]{30, "foobar"}
// // Note: head combines normally (10+20), tail combines normally ("foo"+"bar")
//
// empty := pairMonoid.Empty()
// // empty is Pair[int, string]{0, ""}
//
//go:inline
func ApplicativeMonoid[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L, R]] {
return ApplicativeMonoidTail(l, r)
}
// ApplicativeMonoidTail creates a monoid for [Pair] by lifting the tail monoid into the applicative functor.
//
// This function constructs a monoid using the applicative structure of Pair, focusing on
// the tail (right) value. The head values are combined using the left monoid's semigroup
// operation during applicative application.
//
// CRITICAL BEHAVIOR: Due to the applicative functor implementation, the HEAD values are
// combined in REVERSE order (right-to-left), while TAIL values combine in normal order
// (left-to-right). This matters for non-commutative operations:
//
// strConcat := S.Monoid
// pairMonoid := pair.ApplicativeMonoidTail(strConcat, strConcat)
// p1 := pair.MakePair("hello", "foo")
// p2 := pair.MakePair(" world", "bar")
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[string, string]{" worldhello", "foobar"}
// // ^^^^^^^^^^^^^^ ^^^^^^
// // REVERSED! normal
//
// The resulting monoid satisfies the standard monoid laws:
// - Associativity: Concat(Concat(p1, p2), p3) = Concat(p1, Concat(p2, p3))
// - Left identity: Concat(Empty(), p) = p
// - Right identity: Concat(p, Empty()) = p
//
// Parameters:
// - l: A monoid for the head (left) values of type L
// - r: A monoid for the tail (right) values of type R
//
// Returns:
// - A Monoid[Pair[L, R]] that combines pairs component-wise
//
// Example:
//
// import (
// N "github.com/IBM/fp-go/v2/number"
// M "github.com/IBM/fp-go/v2/monoid"
// )
//
// intAdd := N.MonoidSum[int]()
// intMul := N.MonoidProduct[int]()
//
// pairMonoid := pair.ApplicativeMonoidTail(intAdd, intMul)
//
// p1 := pair.MakePair(5, 3)
// p2 := pair.MakePair(10, 4)
//
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[int, int]{15, 12} (5+10, 3*4)
// // Note: Addition is commutative, so order doesn't matter for head
//
// empty := pairMonoid.Empty()
// // empty is Pair[int, int]{0, 1}
//
// Example with different types:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// boolAnd := M.MakeMonoid(func(a, b bool) bool { return a && b }, true)
// strConcat := S.Monoid
//
// pairMonoid := pair.ApplicativeMonoidTail(boolAnd, strConcat)
//
// p1 := pair.MakePair(true, "hello")
// p2 := pair.MakePair(true, " world")
//
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[bool, string]{true, "hello world"}
// // Note: Boolean AND is commutative, so order doesn't matter for head
//
//go:inline
func ApplicativeMonoidTail[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L, R]] {
return M.ApplicativeMonoid(
FromHead[R](l.Empty()),
MonadMapTail[L, R, func(R) R],
F.Bind1of3(MonadApTail[L, R, R])(l),
r)
}
// ApplicativeMonoidHead creates a monoid for [Pair] by lifting the head monoid into the applicative functor.
//
// This function constructs a monoid using the applicative structure of Pair, focusing on
// the head (left) value. The tail values are combined using the right monoid's semigroup
// operation during applicative application.
//
// This is the dual of [ApplicativeMonoidTail], operating on the head instead of the tail.
//
// CRITICAL BEHAVIOR: Due to the applicative functor implementation, the TAIL values are
// combined in REVERSE order (right-to-left), while HEAD values combine in normal order
// (left-to-right). This is the opposite of ApplicativeMonoidTail:
//
// strConcat := S.Monoid
// pairMonoid := pair.ApplicativeMonoidHead(strConcat, strConcat)
// p1 := pair.MakePair("hello", "foo")
// p2 := pair.MakePair(" world", "bar")
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[string, string]{"hello world", "barfoo"}
// // ^^^^^^^^^^^^ ^^^^^^^^
// // normal REVERSED!
//
// The resulting monoid satisfies the standard monoid laws:
// - Associativity: Concat(Concat(p1, p2), p3) = Concat(p1, Concat(p2, p3))
// - Left identity: Concat(Empty(), p) = p
// - Right identity: Concat(p, Empty()) = p
//
// Parameters:
// - l: A monoid for the head (left) values of type L
// - r: A monoid for the tail (right) values of type R
//
// Returns:
// - A Monoid[Pair[L, R]] that combines pairs component-wise
//
// Example:
//
// import (
// N "github.com/IBM/fp-go/v2/number"
// M "github.com/IBM/fp-go/v2/monoid"
// )
//
// intMul := N.MonoidProduct[int]()
// intAdd := N.MonoidSum[int]()
//
// pairMonoid := pair.ApplicativeMonoidHead(intMul, intAdd)
//
// p1 := pair.MakePair(3, 5)
// p2 := pair.MakePair(4, 10)
//
// result := pairMonoid.Concat(p1, p2)
// // result is Pair[int, int]{12, 15} (3*4, 5+10)
// // Note: Both operations are commutative, so order doesn't matter
//
// empty := pairMonoid.Empty()
// // empty is Pair[int, int]{1, 0}
//
// Example comparing Head vs Tail with non-commutative operations:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// strConcat := S.Monoid
//
// // Using ApplicativeMonoidHead - tail values REVERSED
// headMonoid := pair.ApplicativeMonoidHead(strConcat, strConcat)
// p1 := pair.MakePair("hello", "foo")
// p2 := pair.MakePair(" world", "bar")
// result := headMonoid.Concat(p1, p2)
// // result is Pair[string, string]{"hello world", "barfoo"}
//
// // Using ApplicativeMonoidTail - head values REVERSED
// tailMonoid := pair.ApplicativeMonoidTail(strConcat, strConcat)
// result2 := tailMonoid.Concat(p1, p2)
// // result2 is Pair[string, string]{" worldhello", "foobar"}
// // DIFFERENT result! Head and tail are swapped in their reversal behavior
//
//go:inline
func ApplicativeMonoidHead[L, R any](l M.Monoid[L], r M.Monoid[R]) M.Monoid[Pair[L, R]] {
return M.ApplicativeMonoid(
FromTail[L](r.Empty()),
MonadMapHead[R, L, func(L) L],
F.Bind1of3(MonadApHead[R, L, L])(r),
l)
}

497
v2/pair/monoid_test.go Normal file
View File

@@ -0,0 +1,497 @@
// Copyright (c) 2024 - 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 pair
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/string"
"github.com/stretchr/testify/assert"
)
// TestApplicativeMonoidTail tests the ApplicativeMonoidTail implementation
func TestApplicativeMonoidTail(t *testing.T) {
t.Run("integer addition and string concatenation", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
p1 := MakePair(5, "hello")
p2 := MakePair(3, " world")
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 8, Head(result))
assert.Equal(t, "hello world", Tail(result))
})
t.Run("integer multiplication and addition", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidTail(intMul, intAdd)
p1 := MakePair(3, 5)
p2 := MakePair(4, 10)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 12, Head(result)) // 3 * 4
assert.Equal(t, 15, Tail(result)) // 5 + 10
})
t.Run("boolean AND and OR", func(t *testing.T) {
boolAnd := M.MakeMonoid(func(a, b bool) bool { return a && b }, true)
boolOr := M.MakeMonoid(func(a, b bool) bool { return a || b }, false)
pairMonoid := ApplicativeMonoidTail(boolAnd, boolOr)
p1 := MakePair(true, false)
p2 := MakePair(true, true)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, true, Head(result)) // true && true
assert.Equal(t, true, Tail(result)) // false || true
})
t.Run("empty value", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
empty := pairMonoid.Empty()
assert.Equal(t, 0, Head(empty))
assert.Equal(t, "", Tail(empty))
})
t.Run("left identity law", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
p := MakePair(5, "test")
result := pairMonoid.Concat(pairMonoid.Empty(), p)
assert.Equal(t, p, result)
})
t.Run("right identity law", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
p := MakePair(5, "test")
result := pairMonoid.Concat(p, pairMonoid.Empty())
assert.Equal(t, p, result)
})
t.Run("associativity law", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
p1 := MakePair(1, "a")
p2 := MakePair(2, "b")
p3 := MakePair(3, "c")
left := pairMonoid.Concat(pairMonoid.Concat(p1, p2), p3)
right := pairMonoid.Concat(p1, pairMonoid.Concat(p2, p3))
assert.Equal(t, left, right)
assert.Equal(t, 6, Head(left))
assert.Equal(t, "abc", Tail(left))
})
t.Run("multiple concatenations", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
pairs := []Pair[int, int]{
MakePair(1, 2),
MakePair(3, 4),
MakePair(5, 6),
}
result := pairMonoid.Empty()
for _, p := range pairs {
result = pairMonoid.Concat(result, p)
}
assert.Equal(t, 9, Head(result)) // 0 + 1 + 3 + 5
assert.Equal(t, 48, Tail(result)) // 1 * 2 * 4 * 6
})
}
// TestApplicativeMonoidHead tests the ApplicativeMonoidHead implementation
func TestApplicativeMonoidHead(t *testing.T) {
t.Run("integer multiplication and addition", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
p1 := MakePair(3, 5)
p2 := MakePair(4, 10)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 12, Head(result)) // 3 * 4
assert.Equal(t, 15, Tail(result)) // 5 + 10
})
t.Run("string concatenation and boolean OR", func(t *testing.T) {
strConcat := S.Monoid
boolOr := M.MakeMonoid(func(a, b bool) bool { return a || b }, false)
pairMonoid := ApplicativeMonoidHead(strConcat, boolOr)
p1 := MakePair("hello", false)
p2 := MakePair(" world", true)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, "hello world", Head(result))
assert.Equal(t, true, Tail(result))
})
t.Run("empty value", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
empty := pairMonoid.Empty()
assert.Equal(t, 1, Head(empty))
assert.Equal(t, 0, Tail(empty))
})
t.Run("left identity law", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
p := MakePair(5, 10)
result := pairMonoid.Concat(pairMonoid.Empty(), p)
assert.Equal(t, p, result)
})
t.Run("right identity law", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
p := MakePair(5, 10)
result := pairMonoid.Concat(p, pairMonoid.Empty())
assert.Equal(t, p, result)
})
t.Run("associativity law", func(t *testing.T) {
intMul := N.MonoidProduct[int]()
intAdd := N.MonoidSum[int]()
pairMonoid := ApplicativeMonoidHead(intMul, intAdd)
p1 := MakePair(2, 1)
p2 := MakePair(3, 2)
p3 := MakePair(4, 3)
left := pairMonoid.Concat(pairMonoid.Concat(p1, p2), p3)
right := pairMonoid.Concat(p1, pairMonoid.Concat(p2, p3))
assert.Equal(t, left, right)
assert.Equal(t, 24, Head(left)) // 2 * 3 * 4
assert.Equal(t, 6, Tail(left)) // 1 + 2 + 3
})
t.Run("multiple concatenations", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidHead(intAdd, intMul)
pairs := []Pair[int, int]{
MakePair(1, 2),
MakePair(3, 4),
MakePair(5, 6),
}
result := pairMonoid.Empty()
for _, p := range pairs {
result = pairMonoid.Concat(result, p)
}
assert.Equal(t, 9, Head(result)) // 0 + 1 + 3 + 5
assert.Equal(t, 48, Tail(result)) // 1 * 2 * 4 * 6
})
}
// TestApplicativeMonoid tests the ApplicativeMonoid alias
func TestApplicativeMonoid(t *testing.T) {
t.Run("is alias for ApplicativeMonoidTail", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
monoid1 := ApplicativeMonoid(intAdd, strConcat)
monoid2 := ApplicativeMonoidTail(intAdd, strConcat)
p1 := MakePair(5, "hello")
p2 := MakePair(3, " world")
result1 := monoid1.Concat(p1, p2)
result2 := monoid2.Concat(p1, p2)
assert.Equal(t, result1, result2)
assert.Equal(t, 8, Head(result1))
assert.Equal(t, "hello world", Tail(result1))
})
t.Run("empty values are identical", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
monoid1 := ApplicativeMonoid(intAdd, strConcat)
monoid2 := ApplicativeMonoidTail(intAdd, strConcat)
assert.Equal(t, monoid1.Empty(), monoid2.Empty())
})
}
// TestMonoidHeadVsTail compares ApplicativeMonoidHead and ApplicativeMonoidTail
func TestMonoidHeadVsTail(t *testing.T) {
t.Run("same result with commutative operations", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
headMonoid := ApplicativeMonoidHead(intMul, intAdd)
tailMonoid := ApplicativeMonoidTail(intMul, intAdd)
p1 := MakePair(2, 3)
p2 := MakePair(4, 5)
resultHead := headMonoid.Concat(p1, p2)
resultTail := tailMonoid.Concat(p1, p2)
// Both should give same result since operations are commutative
assert.Equal(t, 8, Head(resultHead)) // 2 * 4
assert.Equal(t, 8, Tail(resultHead)) // 3 + 5
assert.Equal(t, 8, Head(resultTail)) // 2 * 4
assert.Equal(t, 8, Tail(resultTail)) // 3 + 5
})
t.Run("different empty values", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
headMonoid := ApplicativeMonoidHead(intMul, intAdd)
tailMonoid := ApplicativeMonoidTail(intAdd, intMul)
emptyHead := headMonoid.Empty()
emptyTail := tailMonoid.Empty()
assert.Equal(t, 1, Head(emptyHead)) // intMul empty
assert.Equal(t, 0, Tail(emptyHead)) // intAdd empty
assert.Equal(t, 0, Head(emptyTail)) // intAdd empty
assert.Equal(t, 1, Tail(emptyTail)) // intMul empty
})
}
// TestMonoidLaws verifies monoid laws for all implementations
func TestMonoidLaws(t *testing.T) {
testCases := []struct {
name string
monoid M.Monoid[Pair[int, int]]
p1, p2, p3 Pair[int, int]
}{
{
name: "ApplicativeMonoidTail",
monoid: ApplicativeMonoidTail(N.MonoidSum[int](), N.MonoidProduct[int]()),
p1: MakePair(1, 2),
p2: MakePair(3, 4),
p3: MakePair(5, 6),
},
{
name: "ApplicativeMonoidHead",
monoid: ApplicativeMonoidHead(N.MonoidProduct[int](), N.MonoidSum[int]()),
p1: MakePair(2, 1),
p2: MakePair(3, 2),
p3: MakePair(4, 3),
},
{
name: "ApplicativeMonoid",
monoid: ApplicativeMonoid(N.MonoidSum[int](), N.MonoidSum[int]()),
p1: MakePair(1, 2),
p2: MakePair(3, 4),
p3: MakePair(5, 6),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Run("associativity", func(t *testing.T) {
left := tc.monoid.Concat(tc.monoid.Concat(tc.p1, tc.p2), tc.p3)
right := tc.monoid.Concat(tc.p1, tc.monoid.Concat(tc.p2, tc.p3))
assert.Equal(t, left, right)
})
t.Run("left identity", func(t *testing.T) {
result := tc.monoid.Concat(tc.monoid.Empty(), tc.p1)
assert.Equal(t, tc.p1, result)
})
t.Run("right identity", func(t *testing.T) {
result := tc.monoid.Concat(tc.p1, tc.monoid.Empty())
assert.Equal(t, tc.p1, result)
})
})
}
}
// TestMonoidEdgeCases tests edge cases for monoid operations
func TestMonoidEdgeCases(t *testing.T) {
t.Run("concatenating empty with empty", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(intAdd, strConcat)
result := pairMonoid.Concat(pairMonoid.Empty(), pairMonoid.Empty())
assert.Equal(t, pairMonoid.Empty(), result)
})
t.Run("chain of operations", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
result := pairMonoid.Concat(
pairMonoid.Concat(
pairMonoid.Concat(MakePair(1, 2), MakePair(2, 3)),
MakePair(3, 4),
),
MakePair(4, 5),
)
assert.Equal(t, 10, Head(result)) // 1 + 2 + 3 + 4
assert.Equal(t, 120, Tail(result)) // 2 * 3 * 4 * 5
})
t.Run("zero values", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
p1 := MakePair(0, 0)
p2 := MakePair(5, 10)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 5, Head(result))
assert.Equal(t, 0, Tail(result)) // 0 * 10 = 0
})
t.Run("negative values", func(t *testing.T) {
intAdd := N.MonoidSum[int]()
intMul := N.MonoidProduct[int]()
pairMonoid := ApplicativeMonoidTail(intAdd, intMul)
p1 := MakePair(-5, -2)
p2 := MakePair(3, 4)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, -2, Head(result)) // -5 + 3
assert.Equal(t, -8, Tail(result)) // -2 * 4
})
}
// TestMonoidWithDifferentTypes tests monoids with various type combinations
func TestMonoidWithDifferentTypes(t *testing.T) {
t.Run("string and boolean", func(t *testing.T) {
strConcat := S.Monoid
boolAnd := M.MakeMonoid(func(a, b bool) bool { return a && b }, true)
pairMonoid := ApplicativeMonoidTail(strConcat, boolAnd)
p1 := MakePair("hello", true)
p2 := MakePair(" world", true)
result := pairMonoid.Concat(p1, p2)
// Note: The order depends on the applicative implementation
assert.Equal(t, " worldhello", Head(result))
assert.Equal(t, true, Tail(result))
})
t.Run("boolean and string", func(t *testing.T) {
boolOr := M.MakeMonoid(func(a, b bool) bool { return a || b }, false)
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(boolOr, strConcat)
p1 := MakePair(false, "foo")
p2 := MakePair(true, "bar")
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, true, Head(result))
assert.Equal(t, "foobar", Tail(result))
})
t.Run("float64 addition and multiplication", func(t *testing.T) {
floatAdd := N.MonoidSum[float64]()
floatMul := N.MonoidProduct[float64]()
pairMonoid := ApplicativeMonoidTail(floatAdd, floatMul)
p1 := MakePair(1.5, 2.0)
p2 := MakePair(2.5, 3.0)
result := pairMonoid.Concat(p1, p2)
assert.Equal(t, 4.0, Head(result))
assert.Equal(t, 6.0, Tail(result))
})
}
// TestMonoidCommutativity tests behavior with non-commutative operations
func TestMonoidCommutativity(t *testing.T) {
t.Run("string concatenation is not commutative", func(t *testing.T) {
strConcat := S.Monoid
pairMonoid := ApplicativeMonoidTail(strConcat, strConcat)
p1 := MakePair("hello", "foo")
p2 := MakePair(" world", "bar")
result1 := pairMonoid.Concat(p1, p2)
result2 := pairMonoid.Concat(p2, p1)
// The applicative implementation reverses the order for head values
assert.Equal(t, " worldhello", Head(result1))
assert.Equal(t, "foobar", Tail(result1))
assert.Equal(t, "hello world", Head(result2))
assert.Equal(t, "barfoo", Tail(result2))
assert.NotEqual(t, result1, result2)
})
}

View File

@@ -52,3 +52,61 @@ func AlternativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
func AltMonoid[A any](zero Lazy[Result[A]]) Monoid[A] {
return either.AltMonoid(zero)
}
// FirstMonoid creates a Monoid for Result[A] that returns the first Ok (Right) value.
// This monoid prefers the left operand when it is Ok, otherwise returns the right operand.
// The empty value is provided as a lazy computation.
//
// This is equivalent to AltMonoid but implemented more directly.
//
// Truth table:
//
// | x | y | concat(x, y) |
// | --------- | --------- | ------------ |
// | err(e1) | err(e2) | err(e2) |
// | ok(a) | err(e) | ok(a) |
// | err(e) | ok(b) | ok(b) |
// | ok(a) | ok(b) | ok(a) |
//
// Example:
//
// import "errors"
// zero := func() result.Result[int] { return result.Error[int](errors.New("empty")) }
// m := result.FirstMonoid[int](zero)
// m.Concat(result.Of(2), result.Of(3)) // Ok(2) - returns first Ok
// m.Concat(result.Error[int](errors.New("err")), result.Of(3)) // Ok(3)
// m.Concat(result.Of(2), result.Error[int](errors.New("err"))) // Ok(2)
// m.Empty() // Error(error("empty"))
//
//go:inline
func FirstMonoid[A any](zero Lazy[Result[A]]) M.Monoid[Result[A]] {
return either.FirstMonoid(zero)
}
// LastMonoid creates a Monoid for Result[A] that returns the last Ok (Right) value.
// This monoid prefers the right operand when it is Ok, otherwise returns the left operand.
// The empty value is provided as a lazy computation.
//
// Truth table:
//
// | x | y | concat(x, y) |
// | --------- | --------- | ------------ |
// | err(e1) | err(e2) | err(e1) |
// | ok(a) | err(e) | ok(a) |
// | err(e) | ok(b) | ok(b) |
// | ok(a) | ok(b) | ok(b) |
//
// Example:
//
// import "errors"
// zero := func() result.Result[int] { return result.Error[int](errors.New("empty")) }
// m := result.LastMonoid[int](zero)
// m.Concat(result.Of(2), result.Of(3)) // Ok(3) - returns last Ok
// m.Concat(result.Error[int](errors.New("err")), result.Of(3)) // Ok(3)
// m.Concat(result.Of(2), result.Error[int](errors.New("err"))) // Ok(2)
// m.Empty() // Error(error("empty"))
//
//go:inline
func LastMonoid[A any](zero Lazy[Result[A]]) M.Monoid[Result[A]] {
return either.LastMonoid(zero)
}

498
v2/result/monoid_test.go Normal file
View File

@@ -0,0 +1,498 @@
// Copyright (c) 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 result
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
// TestFirstMonoid tests the FirstMonoid implementation
func TestFirstMonoid(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[int](zero)
t.Run("both Right values - returns first", func(t *testing.T) {
result := m.Concat(Right(2), Right(3))
assert.Equal(t, Right(2), result)
})
t.Run("left Right, right Left", func(t *testing.T) {
result := m.Concat(Right(2), Left[int](errors.New("err")))
assert.Equal(t, Right(2), result)
})
t.Run("left Left, right Right", func(t *testing.T) {
result := m.Concat(Left[int](errors.New("err")), Right(3))
assert.Equal(t, Right(3), result)
})
t.Run("both Left", func(t *testing.T) {
err1 := errors.New("err1")
err2 := errors.New("err2")
result := m.Concat(Left[int](err1), Left[int](err2))
// Should return the second Left
assert.True(t, IsLeft(result))
_, leftErr := Unwrap(result)
assert.Equal(t, err2, leftErr)
})
t.Run("empty value", func(t *testing.T) {
empty := m.Empty()
assert.True(t, IsLeft(empty))
_, leftErr := Unwrap(empty)
assert.Equal(t, "empty", leftErr.Error())
})
t.Run("left identity", func(t *testing.T) {
x := Right(5)
result := m.Concat(m.Empty(), x)
assert.Equal(t, x, result)
})
t.Run("right identity", func(t *testing.T) {
x := Right(5)
result := m.Concat(x, m.Empty())
assert.Equal(t, x, result)
})
t.Run("associativity", func(t *testing.T) {
a := Right(1)
b := Right(2)
c := Right(3)
left := m.Concat(m.Concat(a, b), c)
right := m.Concat(a, m.Concat(b, c))
assert.Equal(t, left, right)
assert.Equal(t, Right(1), left)
})
t.Run("multiple concatenations", func(t *testing.T) {
// Should return the first Right value encountered
result := m.Concat(
m.Concat(Left[int](errors.New("err1")), Right(1)),
m.Concat(Right(2), Right(3)),
)
assert.Equal(t, Right(1), result)
})
t.Run("with strings", func(t *testing.T) {
zeroStr := func() Result[string] { return Left[string](errors.New("empty")) }
strMonoid := FirstMonoid[string](zeroStr)
result := strMonoid.Concat(Right("first"), Right("second"))
assert.Equal(t, Right("first"), result)
result = strMonoid.Concat(Left[string](errors.New("err")), Right("second"))
assert.Equal(t, Right("second"), result)
})
}
// TestLastMonoid tests the LastMonoid implementation
func TestLastMonoid(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := LastMonoid[int](zero)
t.Run("both Right values - returns last", func(t *testing.T) {
result := m.Concat(Right(2), Right(3))
assert.Equal(t, Right(3), result)
})
t.Run("left Right, right Left", func(t *testing.T) {
result := m.Concat(Right(2), Left[int](errors.New("err")))
assert.Equal(t, Right(2), result)
})
t.Run("left Left, right Right", func(t *testing.T) {
result := m.Concat(Left[int](errors.New("err")), Right(3))
assert.Equal(t, Right(3), result)
})
t.Run("both Left", func(t *testing.T) {
err1 := errors.New("err1")
err2 := errors.New("err2")
result := m.Concat(Left[int](err1), Left[int](err2))
// Should return the first Left
assert.True(t, IsLeft(result))
_, leftErr := Unwrap(result)
assert.Equal(t, err1, leftErr)
})
t.Run("empty value", func(t *testing.T) {
empty := m.Empty()
assert.True(t, IsLeft(empty))
_, leftErr := Unwrap(empty)
assert.Equal(t, "empty", leftErr.Error())
})
t.Run("left identity", func(t *testing.T) {
x := Right(5)
result := m.Concat(m.Empty(), x)
assert.Equal(t, x, result)
})
t.Run("right identity", func(t *testing.T) {
x := Right(5)
result := m.Concat(x, m.Empty())
assert.Equal(t, x, result)
})
t.Run("associativity", func(t *testing.T) {
a := Right(1)
b := Right(2)
c := Right(3)
left := m.Concat(m.Concat(a, b), c)
right := m.Concat(a, m.Concat(b, c))
assert.Equal(t, left, right)
assert.Equal(t, Right(3), left)
})
t.Run("multiple concatenations", func(t *testing.T) {
// Should return the last Right value encountered
result := m.Concat(
m.Concat(Right(1), Right(2)),
m.Concat(Right(3), Left[int](errors.New("err"))),
)
assert.Equal(t, Right(3), result)
})
t.Run("with strings", func(t *testing.T) {
zeroStr := func() Result[string] { return Left[string](errors.New("empty")) }
strMonoid := LastMonoid[string](zeroStr)
result := strMonoid.Concat(Right("first"), Right("second"))
assert.Equal(t, Right("second"), result)
result = strMonoid.Concat(Right("first"), Left[string](errors.New("err")))
assert.Equal(t, Right("first"), result)
})
}
// TestAltMonoid tests the AltMonoid implementation
func TestAltMonoid(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := AltMonoid[int](zero)
t.Run("both Right values - returns first", func(t *testing.T) {
result := m.Concat(Right(2), Right(3))
assert.Equal(t, Right(2), result)
})
t.Run("left Right, right Left", func(t *testing.T) {
result := m.Concat(Right(2), Left[int](errors.New("err")))
assert.Equal(t, Right(2), result)
})
t.Run("left Left, right Right", func(t *testing.T) {
result := m.Concat(Left[int](errors.New("err")), Right(3))
assert.Equal(t, Right(3), result)
})
t.Run("both Left", func(t *testing.T) {
err1 := errors.New("err1")
err2 := errors.New("err2")
result := m.Concat(Left[int](err1), Left[int](err2))
// Should return the second Left
assert.True(t, IsLeft(result))
_, leftErr := Unwrap(result)
assert.Equal(t, err2, leftErr)
})
t.Run("empty value", func(t *testing.T) {
empty := m.Empty()
assert.True(t, IsLeft(empty))
_, leftErr := Unwrap(empty)
assert.Equal(t, "empty", leftErr.Error())
})
t.Run("left identity", func(t *testing.T) {
x := Right(5)
result := m.Concat(m.Empty(), x)
assert.Equal(t, x, result)
})
t.Run("right identity", func(t *testing.T) {
x := Right(5)
result := m.Concat(x, m.Empty())
assert.Equal(t, x, result)
})
t.Run("associativity", func(t *testing.T) {
a := Right(1)
b := Left[int](errors.New("err"))
c := Right(3)
left := m.Concat(m.Concat(a, b), c)
right := m.Concat(a, m.Concat(b, c))
assert.Equal(t, left, right)
assert.Equal(t, Right(1), left)
})
}
// TestFirstMonoidVsAltMonoid verifies FirstMonoid and AltMonoid have the same behavior
func TestFirstMonoidVsAltMonoid(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
firstMonoid := FirstMonoid[int](zero)
altMonoid := AltMonoid[int](zero)
testCases := []struct {
name string
left Result[int]
right Result[int]
}{
{"both Right", Right(1), Right(2)},
{"left Right, right Left", Right(1), Left[int](errors.New("err"))},
{"left Left, right Right", Left[int](errors.New("err")), Right(2)},
{"both Left", Left[int](errors.New("err1")), Left[int](errors.New("err2"))},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
firstResult := firstMonoid.Concat(tc.left, tc.right)
altResult := altMonoid.Concat(tc.left, tc.right)
// Both should have the same Right/Left status
assert.Equal(t, IsRight(firstResult), IsRight(altResult), "FirstMonoid and AltMonoid should have same Right/Left status")
if IsRight(firstResult) {
rightVal1, _ := Unwrap(firstResult)
rightVal2, _ := Unwrap(altResult)
assert.Equal(t, rightVal1, rightVal2, "FirstMonoid and AltMonoid should have same Right value")
}
})
}
}
// TestFirstMonoidVsLastMonoid verifies the difference between FirstMonoid and LastMonoid
func TestFirstMonoidVsLastMonoid(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
firstMonoid := FirstMonoid[int](zero)
lastMonoid := LastMonoid[int](zero)
t.Run("both Right - different results", func(t *testing.T) {
firstResult := firstMonoid.Concat(Right(1), Right(2))
lastResult := lastMonoid.Concat(Right(1), Right(2))
assert.Equal(t, Right(1), firstResult)
assert.Equal(t, Right(2), lastResult)
assert.NotEqual(t, firstResult, lastResult)
})
t.Run("with Left values - different behavior", func(t *testing.T) {
err1 := errors.New("err1")
err2 := errors.New("err2")
// Both Left: FirstMonoid returns second, LastMonoid returns first
firstResult := firstMonoid.Concat(Left[int](err1), Left[int](err2))
lastResult := lastMonoid.Concat(Left[int](err1), Left[int](err2))
assert.True(t, IsLeft(firstResult))
assert.True(t, IsLeft(lastResult))
_, leftErr1 := Unwrap(firstResult)
_, leftErr2 := Unwrap(lastResult)
assert.Equal(t, err2, leftErr1)
assert.Equal(t, err1, leftErr2)
})
t.Run("mixed values - same results", func(t *testing.T) {
testCases := []struct {
name string
left Result[int]
right Result[int]
expected Result[int]
}{
{"left Right, right Left", Right(1), Left[int](errors.New("err")), Right(1)},
{"left Left, right Right", Left[int](errors.New("err")), Right(2), Right(2)},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
firstResult := firstMonoid.Concat(tc.left, tc.right)
lastResult := lastMonoid.Concat(tc.left, tc.right)
assert.Equal(t, tc.expected, firstResult)
assert.Equal(t, tc.expected, lastResult)
assert.Equal(t, firstResult, lastResult)
})
}
})
}
// TestMonoidLaws verifies monoid laws for all monoid implementations
func TestMonoidLaws(t *testing.T) {
t.Run("FirstMonoid laws", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[int](zero)
a := Right(1)
b := Right(2)
c := Right(3)
// Associativity: (a • b) • c = a • (b • c)
left := m.Concat(m.Concat(a, b), c)
right := m.Concat(a, m.Concat(b, c))
assert.Equal(t, left, right)
// Left identity: Empty() • a = a
leftId := m.Concat(m.Empty(), a)
assert.Equal(t, a, leftId)
// Right identity: a • Empty() = a
rightId := m.Concat(a, m.Empty())
assert.Equal(t, a, rightId)
})
t.Run("LastMonoid laws", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := LastMonoid[int](zero)
a := Right(1)
b := Right(2)
c := Right(3)
// Associativity: (a • b) • c = a • (b • c)
left := m.Concat(m.Concat(a, b), c)
right := m.Concat(a, m.Concat(b, c))
assert.Equal(t, left, right)
// Left identity: Empty() • a = a
leftId := m.Concat(m.Empty(), a)
assert.Equal(t, a, leftId)
// Right identity: a • Empty() = a
rightId := m.Concat(a, m.Empty())
assert.Equal(t, a, rightId)
})
t.Run("AltMonoid laws", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := AltMonoid[int](zero)
a := Right(1)
b := Right(2)
c := Right(3)
// Associativity: (a • b) • c = a • (b • c)
left := m.Concat(m.Concat(a, b), c)
right := m.Concat(a, m.Concat(b, c))
assert.Equal(t, left, right)
// Left identity: Empty() • a = a
leftId := m.Concat(m.Empty(), a)
assert.Equal(t, a, leftId)
// Right identity: a • Empty() = a
rightId := m.Concat(a, m.Empty())
assert.Equal(t, a, rightId)
})
t.Run("FirstMonoid laws with Left values", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[int](zero)
a := Left[int](errors.New("err1"))
b := Left[int](errors.New("err2"))
c := Left[int](errors.New("err3"))
// Associativity with Left values
left := m.Concat(m.Concat(a, b), c)
right := m.Concat(a, m.Concat(b, c))
assert.Equal(t, left, right)
})
t.Run("LastMonoid laws with Left values", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := LastMonoid[int](zero)
a := Left[int](errors.New("err1"))
b := Left[int](errors.New("err2"))
c := Left[int](errors.New("err3"))
// Associativity with Left values
left := m.Concat(m.Concat(a, b), c)
right := m.Concat(a, m.Concat(b, c))
assert.Equal(t, left, right)
})
}
// TestMonoidEdgeCases tests edge cases for monoid operations
func TestMonoidEdgeCases(t *testing.T) {
t.Run("FirstMonoid with empty concatenations", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[int](zero)
// Empty with empty
result := m.Concat(m.Empty(), m.Empty())
assert.True(t, IsLeft(result))
})
t.Run("LastMonoid with empty concatenations", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := LastMonoid[int](zero)
// Empty with empty
result := m.Concat(m.Empty(), m.Empty())
assert.True(t, IsLeft(result))
})
t.Run("FirstMonoid chain of operations", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := FirstMonoid[int](zero)
// Chain multiple operations
result := m.Concat(
m.Concat(
m.Concat(Left[int](errors.New("err1")), Left[int](errors.New("err2"))),
Right(1),
),
m.Concat(Right(2), Right(3)),
)
assert.Equal(t, Right(1), result)
})
t.Run("LastMonoid chain of operations", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := LastMonoid[int](zero)
// Chain multiple operations
result := m.Concat(
m.Concat(Right(1), Right(2)),
m.Concat(
Right(3),
m.Concat(Right(4), Left[int](errors.New("err"))),
),
)
assert.Equal(t, Right(4), result)
})
t.Run("AltMonoid chain of operations", func(t *testing.T) {
zero := func() Result[int] { return Left[int](errors.New("empty")) }
m := AltMonoid[int](zero)
// Chain multiple operations - should return first Right
result := m.Concat(
m.Concat(Left[int](errors.New("err1")), Right(1)),
m.Concat(Right(2), Right(3)),
)
assert.Equal(t, Right(1), result)
})
}