From d8ab6b0ce5c7420e89d9b081d33f0572fa3eceee Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Sat, 22 Nov 2025 10:39:56 +0100 Subject: [PATCH] fix: ChainReaderK Signed-off-by: Dr. Carsten Leue --- v2/idiomatic/readerresult/reader.go | 36 ++++++++++++++++++++++-- v2/idiomatic/readerresult/reader_test.go | 21 +++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/v2/idiomatic/readerresult/reader.go b/v2/idiomatic/readerresult/reader.go index 989c731..1fb391e 100644 --- a/v2/idiomatic/readerresult/reader.go +++ b/v2/idiomatic/readerresult/reader.go @@ -365,13 +365,13 @@ func Asks[R, A any](r Reader[R, A]) ReaderResult[R, A] { // // parseUser := func(data string) result.Result[User] { ... } // result := readerresult.MonadChainEitherK(getUserDataRR, parseUser) -func MonadChainEitherK[R, A, B any](ma ReaderResult[R, A], f result.Kleisli[A, B]) ReaderResult[R, B] { +func MonadChainEitherK[R, A, B any](ma ReaderResult[R, A], f RES.Kleisli[A, B]) ReaderResult[R, B] { return func(r R) (B, error) { a, err := ma(r) if err != nil { return result.Left[B](err) } - return f(a) + return RES.Unwrap(f(a)) } } @@ -384,10 +384,40 @@ func MonadChainEitherK[R, A, B any](ma ReaderResult[R, A], f result.Kleisli[A, B // result := F.Pipe1(getUserDataRR, readerresult.ChainEitherK[Config](parseUser)) // //go:inline -func ChainEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] { +func ChainEitherK[R, A, B any](f RES.Kleisli[A, B]) Operator[R, A, B] { return function.Bind2nd(MonadChainEitherK[R, A, B], f) } +// MonadChainReaderK chains a ReaderResult with a function that returns a plain Result. +// This is useful for integrating functions that don't need environment access. +// +// Example: +// +// parseUser := func(data string) result.Result[User] { ... } +// result := readerresult.MonadChainReaderK(getUserDataRR, parseUser) +func MonadChainReaderK[R, A, B any](ma ReaderResult[R, A], f result.Kleisli[A, B]) ReaderResult[R, B] { + return func(r R) (B, error) { + a, err := ma(r) + if err != nil { + return result.Left[B](err) + } + return f(a) + } +} + +// ChainReaderK is the curried version of MonadChainEitherK. +// It lifts a Result-returning function into a ReaderResult operator. +// +// Example: +// +// parseUser := func(data string) result.Result[User] { ... } +// result := F.Pipe1(getUserDataRR, readerresult.ChainReaderK[Config](parseUser)) +// +//go:inline +func ChainReaderK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, B] { + return function.Bind2nd(MonadChainReaderK[R, A, B], f) +} + // ChainOptionK chains with a function that returns an Option, converting None to an error. // This is useful for integrating functions that return optional values. // diff --git a/v2/idiomatic/readerresult/reader_test.go b/v2/idiomatic/readerresult/reader_test.go index a2072bd..0456519 100644 --- a/v2/idiomatic/readerresult/reader_test.go +++ b/v2/idiomatic/readerresult/reader_test.go @@ -24,6 +24,7 @@ import ( "github.com/IBM/fp-go/v2/internal/utils" "github.com/IBM/fp-go/v2/reader" "github.com/IBM/fp-go/v2/result" + RES "github.com/IBM/fp-go/v2/result" "github.com/stretchr/testify/assert" ) @@ -271,7 +272,7 @@ func TestAsks(t *testing.T) { assert.Equal(t, 7, v) } -func TestChainEitherK(t *testing.T) { +func TestChainReaderK(t *testing.T) { parseInt := func(s string) (int, error) { if s == "42" { return 42, nil @@ -279,6 +280,24 @@ func TestChainEitherK(t *testing.T) { return 0, errors.New("parse error") } + chain := ChainReaderK[MyContext](parseInt) + + v, err := F.Pipe1(Of[MyContext]("42"), chain)(defaultContext) + assert.NoError(t, err) + assert.Equal(t, 42, v) + + _, err = F.Pipe1(Of[MyContext]("invalid"), chain)(defaultContext) + assert.Error(t, err) +} + +func TestChainEitherK(t *testing.T) { + parseInt := func(s string) RES.Result[int] { + if s == "42" { + return RES.Of(42) + } + return RES.Left[int](errors.New("parse error")) + } + chain := ChainEitherK[MyContext](parseInt) v, err := F.Pipe1(Of[MyContext]("42"), chain)(defaultContext)