From 4909ad54734359d3085f8fe18a3294d3dcce30cc Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Fri, 21 Nov 2025 10:22:50 +0100 Subject: [PATCH] fix: add missing monoid Signed-off-by: Dr. Carsten Leue --- v2/coverage.txt | 5 + v2/internal/eithert/either.go | 6 +- v2/readerioeither/reader.go | 2 +- v2/readerresult/monoid.go | 104 +++++++++++++ v2/readerresult/monoid_test.go | 276 +++++++++++++++++++++++++++++++++ v2/readerresult/reader.go | 27 ++++ v2/readerresult/types.go | 2 + 7 files changed, 417 insertions(+), 5 deletions(-) create mode 100644 v2/readerresult/monoid.go create mode 100644 v2/readerresult/monoid_test.go diff --git a/v2/coverage.txt b/v2/coverage.txt index dc6745b..a436bec 100644 --- a/v2/coverage.txt +++ b/v2/coverage.txt @@ -23,6 +23,9 @@ github.com/IBM/fp-go/v2/readerresult/from.go:33.70,35.2 1 1 github.com/IBM/fp-go/v2/readerresult/from.go:45.80,47.2 1 1 github.com/IBM/fp-go/v2/readerresult/from.go:57.92,59.2 1 1 github.com/IBM/fp-go/v2/readerresult/from.go:69.104,71.2 1 1 +github.com/IBM/fp-go/v2/readerresult/monoid.go:37.62,45.2 1 1 +github.com/IBM/fp-go/v2/readerresult/monoid.go:64.70,69.2 1 1 +github.com/IBM/fp-go/v2/readerresult/monoid.go:91.62,98.2 1 1 github.com/IBM/fp-go/v2/readerresult/reader.go:41.59,43.2 1 1 github.com/IBM/fp-go/v2/readerresult/reader.go:49.59,51.2 1 1 github.com/IBM/fp-go/v2/readerresult/reader.go:61.63,63.2 1 1 @@ -56,6 +59,8 @@ github.com/IBM/fp-go/v2/readerresult/reader.go:453.85,455.2 1 1 github.com/IBM/fp-go/v2/readerresult/reader.go:460.55,462.2 1 0 github.com/IBM/fp-go/v2/readerresult/reader.go:473.94,475.2 1 0 github.com/IBM/fp-go/v2/readerresult/reader.go:486.65,488.2 1 1 +github.com/IBM/fp-go/v2/readerresult/reader.go:494.103,502.2 1 1 +github.com/IBM/fp-go/v2/readerresult/reader.go:508.71,515.2 1 0 github.com/IBM/fp-go/v2/readerresult/sequence.go:35.78,40.2 1 1 github.com/IBM/fp-go/v2/readerresult/sequence.go:54.35,60.2 1 1 github.com/IBM/fp-go/v2/readerresult/sequence.go:75.38,82.2 1 1 diff --git a/v2/internal/eithert/either.go b/v2/internal/eithert/either.go index cd3512f..541d0a2 100644 --- a/v2/internal/eithert/either.go +++ b/v2/internal/eithert/either.go @@ -34,13 +34,11 @@ func MonadAlt[LAZY ~func() HKTFA, E, A, HKTFA any]( func Alt[LAZY ~func() HKTFA, E, A, HKTFA any]( fof func(ET.Either[E, A]) HKTFA, - fchain func(HKTFA, func(ET.Either[E, A]) HKTFA) HKTFA, + fchain func(func(ET.Either[E, A]) HKTFA) func(HKTFA) HKTFA, second LAZY) func(HKTFA) HKTFA { - return func(fa HKTFA) HKTFA { - return MonadAlt(fof, fchain, fa, second) - } + return fchain(ET.Fold(F.Ignore1of1[E](second), F.Flow2(ET.Of[E, A], fof))) } // HKTFA = HKT> diff --git a/v2/readerioeither/reader.go b/v2/readerioeither/reader.go index 4a8d918..034319c 100644 --- a/v2/readerioeither/reader.go +++ b/v2/readerioeither/reader.go @@ -767,7 +767,7 @@ func MonadAlt[R, E, A any](first ReaderIOEither[R, E, A], second L.Lazy[ReaderIO func Alt[R, E, A any](second L.Lazy[ReaderIOEither[R, E, A]]) Operator[R, E, A, A] { return eithert.Alt( readerio.Of[R, Either[E, A]], - readerio.MonadChain[R, Either[E, A], Either[E, A]], + readerio.Chain[R, Either[E, A], Either[E, A]], second, ) diff --git a/v2/readerresult/monoid.go b/v2/readerresult/monoid.go new file mode 100644 index 0000000..0e4c5dd --- /dev/null +++ b/v2/readerresult/monoid.go @@ -0,0 +1,104 @@ +// 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 readerresult + +import ( + M "github.com/IBM/fp-go/v2/monoid" +) + +// AlternativeMonoid creates a Monoid for ReaderResult that combines both Alternative and Applicative behavior. +// It uses the provided monoid for the success values and falls back to alternative computations on failure. +// +// The empty element is Of(m.Empty()), and concat tries the first computation, falling back to the second +// if it fails, then combines successful values using the underlying monoid. +// +// Example: +// +// intAdd := monoid.MakeMonoid(0, func(a, b int) int { return a + b }) +// rrMonoid := readerresult.AlternativeMonoid[Config](intAdd) +// +// rr1 := readerresult.Of[Config](5) +// rr2 := readerresult.Of[Config](3) +// combined := rrMonoid.Concat(rr1, rr2) +// // combined(cfg) returns result.Of(8) +// +//go:inline +func AlternativeMonoid[R, A any](m M.Monoid[A]) Monoid[R, A] { + return M.AlternativeMonoid( + Of[R, A], + MonadMap[R, A, func(A) A], + MonadAp[A, R, A], + MonadAlt[R, A], + m, + ) +} + +// AltMonoid creates a Monoid for ReaderResult based on the Alternative pattern. +// The empty element is the provided zero computation, and concat tries the first computation, +// falling back to the second if it fails. +// +// This is useful for combining computations where you want to try alternatives until one succeeds. +// +// Example: +// +// zero := func() readerresult.ReaderResult[Config, User] { +// return readerresult.Left[Config, User](errors.New("no user")) +// } +// userMonoid := readerresult.AltMonoid[Config](zero) +// +// primary := getPrimaryUser() +// backup := getBackupUser() +// combined := userMonoid.Concat(primary, backup) +// // Tries primary, falls back to backup if primary fails +// +//go:inline +func AltMonoid[R, A any](zero Lazy[ReaderResult[R, A]]) Monoid[R, A] { + return M.AltMonoid( + zero, + MonadAlt[R, A], + ) +} + +// ApplicativeMonoid creates a Monoid for ReaderResult based on Applicative functor composition. +// The empty element is Of(m.Empty()), and concat combines two computations using the underlying monoid. +// Both computations must succeed for the result to succeed. +// +// This is useful for accumulating results from multiple independent computations. +// +// Example: +// +// intAdd := monoid.MakeMonoid(0, func(a, b int) int { return a + b }) +// rrMonoid := readerresult.ApplicativeMonoid[Config](intAdd) +// +// rr1 := readerresult.Of[Config](5) +// rr2 := readerresult.Of[Config](3) +// combined := rrMonoid.Concat(rr1, rr2) +// // combined(cfg) returns result.Of(8) +// +// // If either fails, the whole computation fails +// rr3 := readerresult.Left[Config, int](errors.New("error")) +// failed := rrMonoid.Concat(rr1, rr3) +// // failed(cfg) returns result.Left[int](error) +// +//go:inline +func ApplicativeMonoid[R, A any](m M.Monoid[A]) Monoid[R, A] { + return M.ApplicativeMonoid( + Of[R, A], + MonadMap[R, A, func(A) A], + MonadAp[A, R, A], + m, + ) +} diff --git a/v2/readerresult/monoid_test.go b/v2/readerresult/monoid_test.go new file mode 100644 index 0000000..1676753 --- /dev/null +++ b/v2/readerresult/monoid_test.go @@ -0,0 +1,276 @@ +// 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 readerresult + +import ( + "errors" + "testing" + + N "github.com/IBM/fp-go/v2/number" + "github.com/IBM/fp-go/v2/result" + S "github.com/IBM/fp-go/v2/string" + "github.com/stretchr/testify/assert" +) + +var ( + intAddMonoid = N.MonoidSum[int]() + strMonoid = S.Monoid +) + +func TestApplicativeMonoid(t *testing.T) { + rrMonoid := ApplicativeMonoid[MyContext](intAddMonoid) + + t.Run("empty element", func(t *testing.T) { + empty := rrMonoid.Empty() + assert.Equal(t, result.Of(0), empty(defaultContext)) + }) + + t.Run("concat two success values", func(t *testing.T) { + rr1 := Of[MyContext](5) + rr2 := Of[MyContext](3) + combined := rrMonoid.Concat(rr1, rr2) + assert.Equal(t, result.Of(8), combined(defaultContext)) + }) + + t.Run("concat with empty", func(t *testing.T) { + rr := Of[MyContext](42) + combined1 := rrMonoid.Concat(rr, rrMonoid.Empty()) + combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr) + + assert.Equal(t, result.Of(42), combined1(defaultContext)) + assert.Equal(t, result.Of(42), combined2(defaultContext)) + }) + + t.Run("concat with left failure", func(t *testing.T) { + rrSuccess := Of[MyContext](5) + rrFailure := Left[MyContext, int](testError) + + combined := rrMonoid.Concat(rrFailure, rrSuccess) + assert.True(t, result.IsLeft(combined(defaultContext))) + }) + + t.Run("concat with right failure", func(t *testing.T) { + rrSuccess := Of[MyContext](5) + rrFailure := Left[MyContext, int](testError) + + combined := rrMonoid.Concat(rrSuccess, rrFailure) + assert.True(t, result.IsLeft(combined(defaultContext))) + }) + + t.Run("concat multiple values", func(t *testing.T) { + rr1 := Of[MyContext](1) + rr2 := Of[MyContext](2) + rr3 := Of[MyContext](3) + rr4 := Of[MyContext](4) + + // Chain concat calls: ((1 + 2) + 3) + 4 + combined := rrMonoid.Concat( + rrMonoid.Concat( + rrMonoid.Concat(rr1, rr2), + rr3, + ), + rr4, + ) + assert.Equal(t, result.Of(10), combined(defaultContext)) + }) + + t.Run("string concatenation", func(t *testing.T) { + strRRMonoid := ApplicativeMonoid[MyContext](strMonoid) + + rr1 := Of[MyContext]("Hello") + rr2 := Of[MyContext](" ") + rr3 := Of[MyContext]("World") + + combined := strRRMonoid.Concat( + strRRMonoid.Concat(rr1, rr2), + rr3, + ) + assert.Equal(t, result.Of("Hello World"), combined(defaultContext)) + }) +} + +func TestAltMonoid(t *testing.T) { + zero := func() ReaderResult[MyContext, int] { + return Left[MyContext, int](errors.New("empty")) + } + + rrMonoid := AltMonoid(zero) + + t.Run("empty element", func(t *testing.T) { + empty := rrMonoid.Empty() + assert.True(t, result.IsLeft(empty(defaultContext))) + }) + + t.Run("concat two success values - uses first", func(t *testing.T) { + rr1 := Of[MyContext](5) + rr2 := Of[MyContext](3) + combined := rrMonoid.Concat(rr1, rr2) + // AltMonoid takes the first successful value + assert.Equal(t, result.Of(5), combined(defaultContext)) + }) + + t.Run("concat failure then success", func(t *testing.T) { + rrFailure := Left[MyContext, int](testError) + rrSuccess := Of[MyContext](42) + + combined := rrMonoid.Concat(rrFailure, rrSuccess) + // Should fall back to second when first fails + assert.Equal(t, result.Of(42), combined(defaultContext)) + }) + + t.Run("concat success then failure", func(t *testing.T) { + rrSuccess := Of[MyContext](42) + rrFailure := Left[MyContext, int](testError) + + combined := rrMonoid.Concat(rrSuccess, rrFailure) + // Should use first successful value + assert.Equal(t, result.Of(42), combined(defaultContext)) + }) + + t.Run("concat two failures", func(t *testing.T) { + err1 := errors.New("error 1") + err2 := errors.New("error 2") + + rr1 := Left[MyContext, int](err1) + rr2 := Left[MyContext, int](err2) + + combined := rrMonoid.Concat(rr1, rr2) + // Should use second error when both fail + assert.True(t, result.IsLeft(combined(defaultContext))) + }) + + t.Run("concat with empty", func(t *testing.T) { + rr := Of[MyContext](42) + combined1 := rrMonoid.Concat(rr, rrMonoid.Empty()) + combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr) + + assert.Equal(t, result.Of(42), combined1(defaultContext)) + assert.Equal(t, result.Of(42), combined2(defaultContext)) + }) + + t.Run("fallback chain", func(t *testing.T) { + // Simulate trying multiple sources until one succeeds + primary := Left[MyContext, string](errors.New("primary failed")) + secondary := Left[MyContext, string](errors.New("secondary failed")) + tertiary := Of[MyContext]("tertiary success") + + strZero := func() ReaderResult[MyContext, string] { + return Left[MyContext, string](errors.New("all failed")) + } + strMonoid := AltMonoid(strZero) + + // Chain concat: try primary, then secondary, then tertiary + combined := strMonoid.Concat( + strMonoid.Concat(primary, secondary), + tertiary, + ) + assert.Equal(t, result.Of("tertiary success"), combined(defaultContext)) + }) +} + +func TestAlternativeMonoid(t *testing.T) { + rrMonoid := AlternativeMonoid[MyContext](intAddMonoid) + + t.Run("empty element", func(t *testing.T) { + empty := rrMonoid.Empty() + assert.Equal(t, result.Of(0), empty(defaultContext)) + }) + + t.Run("concat two success values", func(t *testing.T) { + rr1 := Of[MyContext](5) + rr2 := Of[MyContext](3) + combined := rrMonoid.Concat(rr1, rr2) + assert.Equal(t, result.Of(8), combined(defaultContext)) + }) + + t.Run("concat failure then success", func(t *testing.T) { + rrFailure := Left[MyContext, int](testError) + rrSuccess := Of[MyContext](42) + + combined := rrMonoid.Concat(rrFailure, rrSuccess) + // Alternative falls back to second when first fails + assert.Equal(t, result.Of(42), combined(defaultContext)) + }) + + t.Run("concat success then failure", func(t *testing.T) { + rrSuccess := Of[MyContext](42) + rrFailure := Left[MyContext, int](testError) + + combined := rrMonoid.Concat(rrSuccess, rrFailure) + // Should use first successful value + assert.Equal(t, result.Of(42), combined(defaultContext)) + }) + + t.Run("concat with empty", func(t *testing.T) { + rr := Of[MyContext](42) + combined1 := rrMonoid.Concat(rr, rrMonoid.Empty()) + combined2 := rrMonoid.Concat(rrMonoid.Empty(), rr) + + assert.Equal(t, result.Of(42), combined1(defaultContext)) + assert.Equal(t, result.Of(42), combined2(defaultContext)) + }) + + t.Run("multiple values with some failures", func(t *testing.T) { + rr1 := Left[MyContext, int](errors.New("fail 1")) + rr2 := Of[MyContext](5) + rr3 := Left[MyContext, int](errors.New("fail 2")) + rr4 := Of[MyContext](10) + + // Alternative should skip failures and accumulate successes + combined := rrMonoid.Concat( + rrMonoid.Concat( + rrMonoid.Concat(rr1, rr2), + rr3, + ), + rr4, + ) + // Should accumulate successful values: 5 + 10 = 15 + assert.Equal(t, result.Of(15), combined(defaultContext)) + }) +} + +// Test monoid laws +func TestMonoidLaws(t *testing.T) { + rrMonoid := ApplicativeMonoid[MyContext](intAddMonoid) + + // Left identity: empty <> x == x + t.Run("left identity", func(t *testing.T) { + x := Of[MyContext](42) + result1 := rrMonoid.Concat(rrMonoid.Empty(), x)(defaultContext) + result2 := x(defaultContext) + assert.Equal(t, result2, result1) + }) + + // Right identity: x <> empty == x + t.Run("right identity", func(t *testing.T) { + x := Of[MyContext](42) + result1 := rrMonoid.Concat(x, rrMonoid.Empty())(defaultContext) + result2 := x(defaultContext) + assert.Equal(t, result2, result1) + }) + + // Associativity: (x <> y) <> z == x <> (y <> z) + t.Run("associativity", func(t *testing.T) { + x := Of[MyContext](1) + y := Of[MyContext](2) + z := Of[MyContext](3) + + left := rrMonoid.Concat(rrMonoid.Concat(x, y), z)(defaultContext) + right := rrMonoid.Concat(x, rrMonoid.Concat(y, z))(defaultContext) + + assert.Equal(t, right, left) + }) +} diff --git a/v2/readerresult/reader.go b/v2/readerresult/reader.go index 78d3b1e..6b4e08a 100644 --- a/v2/readerresult/reader.go +++ b/v2/readerresult/reader.go @@ -486,3 +486,30 @@ func MonadMapLeft[R, A any](fa ReaderResult[R, A], f Endomorphism[error]) Reader func MapLeft[R, A any](f Endomorphism[error]) Operator[R, A, A] { return eithert.MapLeft(reader.Map[R, Result[A], Result[A]], f) } + +// MonadAlt tries the first computation, and if it fails, tries the second. +// This implements the Alternative pattern for error recovery. +// +//go:inline +func MonadAlt[R, A any](first ReaderResult[R, A], second Lazy[ReaderResult[R, A]]) ReaderResult[R, A] { + return eithert.MonadAlt( + reader.Of[R, Result[A]], + reader.MonadChain[R, Result[A], Result[A]], + + first, + second, + ) +} + +// Alt tries the first computation, and if it fails, tries the second. +// This implements the Alternative pattern for error recovery. +// +//go:inline +func Alt[R, A any](second Lazy[ReaderResult[R, A]]) Operator[R, A, A] { + return eithert.Alt( + reader.Of[R, Result[A]], + reader.Chain[R, Result[A], Result[A]], + + second, + ) +} diff --git a/v2/readerresult/types.go b/v2/readerresult/types.go index 08738c4..f6fb405 100644 --- a/v2/readerresult/types.go +++ b/v2/readerresult/types.go @@ -19,6 +19,7 @@ import ( "github.com/IBM/fp-go/v2/either" "github.com/IBM/fp-go/v2/endomorphism" "github.com/IBM/fp-go/v2/lazy" + "github.com/IBM/fp-go/v2/monoid" "github.com/IBM/fp-go/v2/option" "github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/result" @@ -33,6 +34,7 @@ type ( Reader[R, A any] = reader.Reader[R, A] ReaderResult[R, A any] = Reader[R, Result[A]] + Monoid[R, A any] = monoid.Monoid[ReaderResult[R, A]] Kleisli[R, A, B any] = Reader[A, ReaderResult[R, B]] Operator[R, A, B any] = Kleisli[R, ReaderResult[R, A], B]