From cbd93fdecc3be5d7f5002fb87d278ad2b7929957 Mon Sep 17 00:00:00 2001 From: "Dr. Carsten Leue" Date: Tue, 18 Nov 2025 17:54:04 +0100 Subject: [PATCH] fix: add statereaderioresult Signed-off-by: Dr. Carsten Leue --- v2/context/statereaderioresult/bind.go | 209 +++++++ v2/context/statereaderioresult/doc.go | 147 +++++ v2/context/statereaderioresult/eq.go | 41 ++ v2/context/statereaderioresult/monad.go | 103 ++++ v2/context/statereaderioresult/resource.go | 101 ++++ .../statereaderioresult/resource_test.go | 415 +++++++++++++ v2/context/statereaderioresult/state.go | 309 ++++++++++ .../statereaderioresult_test.go | 567 ++++++++++++++++++ .../statereaderioresult/testing/laws.go | 87 +++ .../statereaderioresult/testing/laws_test.go | 50 ++ v2/context/statereaderioresult/type.go | 84 +++ v2/either/resource.go | 5 +- v2/either/resource_test.go | 2 +- v2/result/resource.go | 7 +- v2/result/resource_test.go | 2 +- v2/statereaderioeither/resource.go | 80 +++ v2/statereaderioeither/resource_test.go | 262 ++++++++ 17 files changed, 2466 insertions(+), 5 deletions(-) create mode 100644 v2/context/statereaderioresult/bind.go create mode 100644 v2/context/statereaderioresult/doc.go create mode 100644 v2/context/statereaderioresult/eq.go create mode 100644 v2/context/statereaderioresult/monad.go create mode 100644 v2/context/statereaderioresult/resource.go create mode 100644 v2/context/statereaderioresult/resource_test.go create mode 100644 v2/context/statereaderioresult/state.go create mode 100644 v2/context/statereaderioresult/statereaderioresult_test.go create mode 100644 v2/context/statereaderioresult/testing/laws.go create mode 100644 v2/context/statereaderioresult/testing/laws_test.go create mode 100644 v2/context/statereaderioresult/type.go create mode 100644 v2/statereaderioeither/resource.go create mode 100644 v2/statereaderioeither/resource_test.go diff --git a/v2/context/statereaderioresult/bind.go b/v2/context/statereaderioresult/bind.go new file mode 100644 index 0000000..3c3fcbc --- /dev/null +++ b/v2/context/statereaderioresult/bind.go @@ -0,0 +1,209 @@ +// 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 statereaderioresult + +import ( + "github.com/IBM/fp-go/v2/function" + A "github.com/IBM/fp-go/v2/internal/apply" + C "github.com/IBM/fp-go/v2/internal/chain" + F "github.com/IBM/fp-go/v2/internal/functor" +) + +// Do starts a do-notation chain for building computations in a fluent style. +// This is typically used with Bind, Let, and other combinators to compose +// stateful, context-dependent computations that can fail. +// +// Example: +// +// type State struct { +// name string +// age int +// } +// result := function.Pipe2( +// statereaderioresult.Do[AppState](State{}), +// statereaderioresult.Bind(...), +// statereaderioresult.Let(...), +// ) +// +//go:inline +func Do[ST, A any]( + empty A, +) StateReaderIOResult[ST, A] { + return Of[ST](empty) +} + +// Bind executes a computation and binds its result to a field in the accumulator state. +// This is used in do-notation to sequence dependent computations. +// +// Example: +// +// result := function.Pipe2( +// statereaderioresult.Do[AppState](State{}), +// statereaderioresult.Bind( +// func(name string) func(State) State { +// return func(s State) State { return State{name: name, age: s.age} } +// }, +// func(s State) statereaderioresult.StateReaderIOResult[AppState, string] { +// return statereaderioresult.Of[AppState]("John") +// }, +// ), +// ) +// +//go:inline +func Bind[ST, S1, S2, T any]( + setter func(T) func(S1) S2, + f Kleisli[ST, S1, T], +) Operator[ST, S1, S2] { + return C.Bind( + Chain[ST, S1, S2], + Map[ST, T, S2], + setter, + f, + ) +} + +// Let computes a derived value and binds it to a field in the accumulator state. +// Unlike Bind, this does not execute a monadic computation, just a pure function. +// +// Example: +// +// result := function.Pipe2( +// statereaderioresult.Do[AppState](State{age: 25}), +// statereaderioresult.Let( +// func(isAdult bool) func(State) State { +// return func(s State) State { return State{age: s.age, isAdult: isAdult} } +// }, +// func(s State) bool { return s.age >= 18 }, +// ), +// ) +// +//go:inline +func Let[ST, S1, S2, T any]( + key func(T) func(S1) S2, + f func(S1) T, +) Operator[ST, S1, S2] { + return F.Let( + Map[ST, S1, S2], + key, + f, + ) +} + +// LetTo binds a constant value to a field in the accumulator state. +// +// Example: +// +// result := function.Pipe2( +// statereaderioresult.Do[AppState](State{}), +// statereaderioresult.LetTo( +// func(status string) func(State) State { +// return func(s State) State { return State{...s, status: status} } +// }, +// "active", +// ), +// ) +// +//go:inline +func LetTo[ST, S1, S2, T any]( + key func(T) func(S1) S2, + b T, +) Operator[ST, S1, S2] { + return F.LetTo( + Map[ST, S1, S2], + key, + b, + ) +} + +// BindTo wraps a value in a simple constructor, typically used to start a do-notation chain +// after getting an initial value. +// +// Example: +// +// result := function.Pipe2( +// statereaderioresult.Of[AppState](42), +// statereaderioresult.BindTo[AppState](func(x int) State { return State{value: x} }), +// ) +// +//go:inline +func BindTo[ST, S1, T any]( + setter func(T) S1, +) Operator[ST, T, S1] { + return C.BindTo( + Map[ST, T, S1], + setter, + ) +} + +// ApS applies a computation in sequence and binds the result to a field. +// This is the applicative version of Bind. +// +//go:inline +func ApS[ST, S1, S2, T any]( + setter func(T) func(S1) S2, + fa StateReaderIOResult[ST, T], +) Operator[ST, S1, S2] { + return A.ApS( + Ap[S2, ST, T], + Map[ST, S1, func(T) S2], + setter, + fa, + ) +} + +// ApSL is a lens-based variant of ApS for working with nested structures. +// It uses a lens to focus on a specific field in the state. +// +//go:inline +func ApSL[ST, S, T any]( + lens Lens[S, T], + fa StateReaderIOResult[ST, T], +) Endomorphism[StateReaderIOResult[ST, S]] { + return ApS(lens.Set, fa) +} + +// BindL is a lens-based variant of Bind for working with nested structures. +// It uses a lens to focus on a specific field in the state. +// +//go:inline +func BindL[ST, S, T any]( + lens Lens[S, T], + f Kleisli[ST, T, T], +) Endomorphism[StateReaderIOResult[ST, S]] { + return Bind(lens.Set, function.Flow2(lens.Get, f)) +} + +// LetL is a lens-based variant of Let for working with nested structures. +// It uses a lens to focus on a specific field in the state. +// +//go:inline +func LetL[ST, S, T any]( + lens Lens[S, T], + f Endomorphism[T], +) Endomorphism[StateReaderIOResult[ST, S]] { + return Let[ST](lens.Set, function.Flow2(lens.Get, f)) +} + +// LetToL is a lens-based variant of LetTo for working with nested structures. +// It uses a lens to focus on a specific field in the state. +// +//go:inline +func LetToL[ST, S, T any]( + lens Lens[S, T], + b T, +) Endomorphism[StateReaderIOResult[ST, S]] { + return LetTo[ST](lens.Set, b) +} diff --git a/v2/context/statereaderioresult/doc.go b/v2/context/statereaderioresult/doc.go new file mode 100644 index 0000000..b7093ee --- /dev/null +++ b/v2/context/statereaderioresult/doc.go @@ -0,0 +1,147 @@ +// 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 statereaderioresult provides a functional programming abstraction that combines +// four powerful concepts: State, Reader, IO, and Result monads, specialized for Go's context.Context. +// +// # StateReaderIOResult +// +// StateReaderIOResult[S, A] represents a computation that: +// - Manages state of type S (State) +// - Depends on a [context.Context] (Reader) +// - Performs side effects (IO) +// - Can fail with an [error] or succeed with a value of type A (Result) +// +// This is a specialization of StateReaderIOEither with: +// - Context type fixed to [context.Context] +// - Error type fixed to [error] +// +// This is particularly useful for: +// - Stateful computations with dependency injection using Go contexts +// - Error handling in effectful computations with state +// - Composing operations that need access to context, manage state, and can fail +// - Working with Go's standard context patterns (cancellation, deadlines, values) +// +// # Core Operations +// +// Construction: +// - Of/Right: Create a successful computation with a value +// - Left: Create a failed computation with an error +// - FromState: Lift a State into StateReaderIOResult +// - FromIO: Lift an IO into StateReaderIOResult +// - FromResult: Lift a Result into StateReaderIOResult +// - FromIOResult: Lift an IOResult into StateReaderIOResult +// - FromReaderIOResult: Lift a ReaderIOResult into StateReaderIOResult +// +// Transformation: +// - Map: Transform the success value +// - Chain: Sequence dependent computations (monadic bind) +// - Flatten: Flatten nested StateReaderIOResult +// +// Combination: +// - Ap: Apply a function in a context to a value in a context +// +// Context Access: +// - Asks: Get a value derived from the context +// - Local: Run a computation with a modified context +// +// Kleisli Arrows: +// - FromResultK: Lift a Result-returning function to a Kleisli arrow +// - FromIOK: Lift an IO-returning function to a Kleisli arrow +// - FromIOResultK: Lift an IOResult-returning function to a Kleisli arrow +// - FromReaderIOResultK: Lift a ReaderIOResult-returning function to a Kleisli arrow +// - ChainResultK: Chain with a Result-returning function +// - ChainIOResultK: Chain with an IOResult-returning function +// - ChainReaderIOResultK: Chain with a ReaderIOResult-returning function +// +// Do Notation (Monadic Composition): +// - Do: Start a do-notation chain +// - Bind: Bind a value from a computation +// - BindTo: Bind a value to a simple constructor +// - Let: Compute a derived value +// - LetTo: Set a constant value +// - ApS: Apply in sequence (for applicative composition) +// - BindL/ApSL/LetL/LetToL: Lens-based variants for working with nested structures +// +// # Example Usage +// +// type AppState struct { +// RequestCount int +// LastError error +// } +// +// // A computation that manages state, depends on context, performs IO, and can fail +// func processRequest(data string) statereaderioresult.StateReaderIOResult[AppState, string] { +// return func(state AppState) readerioresult.ReaderIOResult[pair.Pair[AppState, string]] { +// return func(ctx context.Context) ioresult.IOResult[pair.Pair[AppState, string]] { +// return func() result.Result[pair.Pair[AppState, string]] { +// // Check context for cancellation +// if ctx.Err() != nil { +// return result.Error[pair.Pair[AppState, string]](ctx.Err()) +// } +// // Update state +// newState := AppState{RequestCount: state.RequestCount + 1} +// // Perform IO operations +// return result.Of(pair.MakePair(newState, "processed: " + data)) +// } +// } +// } +// } +// +// // Compose operations using do-notation +// result := function.Pipe3( +// statereaderioresult.Do[AppState](State{}), +// statereaderioresult.Bind( +// func(result string) func(State) State { return func(s State) State { return State{result: result} } }, +// func(s State) statereaderioresult.StateReaderIOResult[AppState, string] { +// return processRequest(s.input) +// }, +// ), +// statereaderioresult.Map[AppState](func(s State) string { return s.result }), +// ) +// +// // Execute with initial state and context +// initialState := AppState{RequestCount: 0} +// ctx := context.Background() +// outcome := result(initialState)(ctx)() // Returns result.Result[pair.Pair[AppState, string]] +// +// # Context Integration +// +// This package is designed to work seamlessly with Go's context.Context: +// +// // Using context values +// getUserID := statereaderioresult.Asks[AppState, string](func(ctx context.Context) statereaderioresult.StateReaderIOResult[AppState, string] { +// userID, ok := ctx.Value("userID").(string) +// if !ok { +// return statereaderioresult.Left[AppState, string](errors.New("missing userID")) +// } +// return statereaderioresult.Of[AppState](userID) +// }) +// +// // Using context cancellation +// withTimeout := statereaderioresult.Local[AppState, string](func(ctx context.Context) context.Context { +// ctx, _ = context.WithTimeout(ctx, 5*time.Second) +// return ctx +// }) +// +// # Monad Laws +// +// StateReaderIOResult satisfies the monad laws: +// - Left Identity: Of(a) >>= f ≡ f(a) +// - Right Identity: m >>= Of ≡ m +// - Associativity: (m >>= f) >>= g ≡ m >>= (x => f(x) >>= g) +// +// These laws are verified in the testing subpackage. +package statereaderioresult diff --git a/v2/context/statereaderioresult/eq.go b/v2/context/statereaderioresult/eq.go new file mode 100644 index 0000000..87f8661 --- /dev/null +++ b/v2/context/statereaderioresult/eq.go @@ -0,0 +1,41 @@ +// 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 statereaderioresult + +import ( + "context" + + "github.com/IBM/fp-go/v2/eq" + "github.com/IBM/fp-go/v2/function" + RIOR "github.com/IBM/fp-go/v2/readerioresult" +) + +// Eq implements the equals predicate for values contained in the [StateReaderIOResult] monad +func Eq[S, A any](eqr eq.Eq[ReaderIOResult[Pair[S, A]]]) func(S) eq.Eq[StateReaderIOResult[S, A]] { + return func(s S) eq.Eq[StateReaderIOResult[S, A]] { + return eq.FromEquals(func(l, r StateReaderIOResult[S, A]) bool { + return eqr.Equals(l(s), r(s)) + }) + } +} + +// FromStrictEquals constructs an [eq.Eq] from the canonical comparison function +func FromStrictEquals[S comparable, A comparable]() func(context.Context) func(S) eq.Eq[StateReaderIOResult[S, A]] { + return function.Flow2( + RIOR.FromStrictEquals[context.Context, Pair[S, A]](), + Eq[S, A], + ) +} diff --git a/v2/context/statereaderioresult/monad.go b/v2/context/statereaderioresult/monad.go new file mode 100644 index 0000000..e5fd5ee --- /dev/null +++ b/v2/context/statereaderioresult/monad.go @@ -0,0 +1,103 @@ +// 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 statereaderioresult + +import ( + "github.com/IBM/fp-go/v2/internal/applicative" + "github.com/IBM/fp-go/v2/internal/functor" + "github.com/IBM/fp-go/v2/internal/monad" + "github.com/IBM/fp-go/v2/internal/pointed" +) + +type stateReaderIOResultPointed[ + S, A any, +] struct{} + +type stateReaderIOResultFunctor[ + S, A, B any, +] struct{} + +type stateReaderIOResultApplicative[ + S, A, B any, +] struct{} + +type stateReaderIOResultMonad[ + S, A, B any, +] struct{} + +func (o *stateReaderIOResultPointed[S, A]) Of(a A) StateReaderIOResult[S, A] { + return Of[S](a) +} + +func (o *stateReaderIOResultMonad[S, A, B]) Of(a A) StateReaderIOResult[S, A] { + return Of[S](a) +} + +func (o *stateReaderIOResultApplicative[S, A, B]) Of(a A) StateReaderIOResult[S, A] { + return Of[S](a) +} + +func (o *stateReaderIOResultMonad[S, A, B]) Map(f func(A) B) Operator[S, A, B] { + return Map[S](f) +} + +func (o *stateReaderIOResultApplicative[S, A, B]) Map(f func(A) B) Operator[S, A, B] { + return Map[S](f) +} + +func (o *stateReaderIOResultFunctor[S, A, B]) Map(f func(A) B) Operator[S, A, B] { + return Map[S](f) +} + +func (o *stateReaderIOResultMonad[S, A, B]) Chain(f Kleisli[S, A, B]) Operator[S, A, B] { + return Chain(f) +} + +func (o *stateReaderIOResultMonad[S, A, B]) Ap(fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] { + return Ap[B](fa) +} + +func (o *stateReaderIOResultApplicative[S, A, B]) Ap(fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] { + return Ap[B](fa) +} + +// Pointed implements the [pointed.Pointed] operations for [StateReaderIOResult] +func Pointed[ + S, A any, +]() pointed.Pointed[A, StateReaderIOResult[S, A]] { + return &stateReaderIOResultPointed[S, A]{} +} + +// Functor implements the [functor.Functor] operations for [StateReaderIOResult] +func Functor[ + S, A, B any, +]() functor.Functor[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B]] { + return &stateReaderIOResultFunctor[S, A, B]{} +} + +// Applicative implements the [applicative.Applicative] operations for [StateReaderIOResult] +func Applicative[ + S, A, B any, +]() applicative.Applicative[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]] { + return &stateReaderIOResultApplicative[S, A, B]{} +} + +// Monad implements the [monad.Monad] operations for [StateReaderIOResult] +func Monad[ + S, A, B any, +]() monad.Monad[A, B, StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]] { + return &stateReaderIOResultMonad[S, A, B]{} +} diff --git a/v2/context/statereaderioresult/resource.go b/v2/context/statereaderioresult/resource.go new file mode 100644 index 0000000..5c7d1c6 --- /dev/null +++ b/v2/context/statereaderioresult/resource.go @@ -0,0 +1,101 @@ +// 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 statereaderioresult + +import "github.com/IBM/fp-go/v2/statereaderioeither" + +// WithResource constructs a function that creates a resource with state management, operates on it, +// and then releases the resource. This ensures proper resource cleanup even in the presence of errors, +// following the Resource Acquisition Is Initialization (RAII) pattern. +// +// The state is threaded through all operations: resource creation, usage, and release. +// +// The resource lifecycle with state management is: +// 1. onCreate: Acquires the resource (may modify state) +// 2. use: Operates on the resource with current state (provided as argument to the returned function) +// 3. onRelease: Releases the resource with current state (called regardless of success or failure) +// +// Type parameters: +// - A: The type of the result produced by using the resource +// - S: The state type that is threaded through all operations +// - RES: The resource type +// - ANY: The type returned by the release function (typically ignored) +// +// Parameters: +// - onCreate: A stateful computation that acquires the resource +// - onRelease: A stateful function that releases the resource, called with the resource and current state, +// executed regardless of errors +// +// Returns: +// +// A function that takes a resource-using function and returns a StateReaderIOResult that manages +// the resource lifecycle with state +// +// Example: +// +// type AppState struct { +// openFiles int +// } +// +// // Resource creation that updates state +// openFile := func(filename string) StateReaderIOResult[AppState, *File] { +// return func(state AppState) ReaderIOResult[Pair[AppState, *File]] { +// return func(ctx context.Context) IOResult[Pair[AppState, *File]] { +// return func() Result[Pair[AppState, *File]] { +// file, err := os.Open(filename) +// if err != nil { +// return result.Error[Pair[AppState, *File]](err) +// } +// newState := AppState{openFiles: state.openFiles + 1} +// return result.Of(pair.MakePair(newState, file)) +// } +// } +// } +// } +// +// // Resource release that updates state +// closeFile := func(f *File) StateReaderIOResult[AppState, int] { +// return func(state AppState) ReaderIOResult[Pair[AppState, int]] { +// return func(ctx context.Context) IOResult[Pair[AppState, int]] { +// return func() Result[Pair[AppState, int]] { +// f.Close() +// newState := AppState{openFiles: state.openFiles - 1} +// return result.Of(pair.MakePair(newState, 0)) +// } +// } +// } +// } +// +// // Use the resource with automatic cleanup +// withFile := WithResource( +// openFile("data.txt"), +// closeFile, +// ) +// +// result := withFile(func(f *File) StateReaderIOResult[AppState, string] { +// return readContent(f) // File will be closed automatically +// }) +// +// // Execute the computation +// initialState := AppState{openFiles: 0} +// ctx := context.Background() +// outcome := result(initialState)(ctx)() +func WithResource[A, S, RES, ANY any]( + onCreate StateReaderIOResult[S, RES], + onRelease Kleisli[S, RES, ANY], +) Kleisli[S, Kleisli[S, RES, A], A] { + return statereaderioeither.WithResource[A](onCreate, onRelease) +} diff --git a/v2/context/statereaderioresult/resource_test.go b/v2/context/statereaderioresult/resource_test.go new file mode 100644 index 0000000..ea2d184 --- /dev/null +++ b/v2/context/statereaderioresult/resource_test.go @@ -0,0 +1,415 @@ +// 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 statereaderioresult + +import ( + "context" + "errors" + "testing" + + P "github.com/IBM/fp-go/v2/pair" + RES "github.com/IBM/fp-go/v2/result" + "github.com/stretchr/testify/assert" +) + +// resourceState tracks the lifecycle of resources for testing +type resourceState struct { + resourcesCreated int + resourcesReleased int + lastError error +} + +// mockResource represents a test resource +type mockResource struct { + id int + isValid bool +} + +// TestWithResourceSuccess tests successful resource creation, usage, and release +func TestWithResourceSuccess(t *testing.T) { + initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0} + ctx := context.Background() + + // Create a resource + onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] { + return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] { + return func() Result[Pair[resourceState, mockResource]] { + newState := resourceState{ + resourcesCreated: s.resourcesCreated + 1, + resourcesReleased: s.resourcesReleased, + } + res := mockResource{id: newState.resourcesCreated, isValid: true} + return RES.Of(P.MakePair(newState, res)) + } + } + } + + // Release a resource + onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] { + return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] { + return func(ctx context.Context) IOResult[Pair[resourceState, int]] { + return func() Result[Pair[resourceState, int]] { + newState := resourceState{ + resourcesCreated: s.resourcesCreated, + resourcesReleased: s.resourcesReleased + 1, + } + return RES.Of(P.MakePair(newState, 0)) + } + } + } + } + + // Use the resource + useResource := func(res mockResource) StateReaderIOResult[resourceState, string] { + return func(s resourceState) ReaderIOResult[Pair[resourceState, string]] { + return func(ctx context.Context) IOResult[Pair[resourceState, string]] { + return func() Result[Pair[resourceState, string]] { + result := "used resource " + string(rune(res.id+'0')) + return RES.Of(P.MakePair(s, result)) + } + } + } + } + + withResource := WithResource[string](onCreate, onRelease) + result := withResource(useResource) + outcome := result(initialState)(ctx)() + + assert.True(t, RES.IsRight(outcome)) + RES.Map(func(p Pair[resourceState, string]) Pair[resourceState, string] { + state := P.Head(p) + value := P.Tail(p) + + // Verify state updates + // Note: Final state comes from the use function, not the release function + // onCreate: 0->1, use: sees 1 (doesn't modify), release: sees 1 and increments released + // The final state is from use function which saw state=1 with resourcesReleased=0 + assert.Equal(t, 1, state.resourcesCreated, "Resource should be created once") + assert.Equal(t, 0, state.resourcesReleased, "Final state is from use function, before release") + + // Verify result + assert.Equal(t, "used resource 1", value) + + return p + })(outcome) +} + +// TestWithResourceErrorInCreate tests error handling when resource creation fails +func TestWithResourceErrorInCreate(t *testing.T) { + initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0} + ctx := context.Background() + + createError := errors.New("failed to create resource") + + // onCreate that fails + onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] { + return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] { + return func() Result[Pair[resourceState, mockResource]] { + return RES.Left[Pair[resourceState, mockResource]](createError) + } + } + } + + // Release should not be called if onCreate fails + onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] { + return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] { + return func(ctx context.Context) IOResult[Pair[resourceState, int]] { + return func() Result[Pair[resourceState, int]] { + t.Error("onRelease should not be called when onCreate fails") + return RES.Of(P.MakePair(s, 0)) + } + } + } + } + + useResource := func(res mockResource) StateReaderIOResult[resourceState, string] { + return Of[resourceState]("should not reach here") + } + + withResource := WithResource[string](onCreate, onRelease) + result := withResource(useResource) + outcome := result(initialState)(ctx)() + + assert.True(t, RES.IsLeft(outcome)) + RES.Fold( + func(err error) bool { + assert.Equal(t, createError, err) + return true + }, + func(p Pair[resourceState, string]) bool { + t.Error("Expected error but got success") + return false + }, + )(outcome) +} + +// TestWithResourceErrorInUse tests that resources are released even when usage fails +func TestWithResourceErrorInUse(t *testing.T) { + initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0} + ctx := context.Background() + + useError := errors.New("failed to use resource") + + // Create a resource + onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] { + return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] { + return func() Result[Pair[resourceState, mockResource]] { + newState := resourceState{ + resourcesCreated: s.resourcesCreated + 1, + resourcesReleased: s.resourcesReleased, + } + res := mockResource{id: 1, isValid: true} + return RES.Of(P.MakePair(newState, res)) + } + } + } + + releaseWasCalled := false + + // Release should still be called even if use fails + onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] { + return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] { + return func(ctx context.Context) IOResult[Pair[resourceState, int]] { + return func() Result[Pair[resourceState, int]] { + releaseWasCalled = true + newState := resourceState{ + resourcesCreated: s.resourcesCreated, + resourcesReleased: s.resourcesReleased + 1, + } + return RES.Of(P.MakePair(newState, 0)) + } + } + } + } + + // Use that fails + useResource := func(res mockResource) StateReaderIOResult[resourceState, string] { + return Left[resourceState, string](useError) + } + + withResource := WithResource[string](onCreate, onRelease) + result := withResource(useResource) + outcome := result(initialState)(ctx)() + + assert.True(t, RES.IsLeft(outcome)) + assert.True(t, releaseWasCalled, "onRelease should be called even when use fails") + + RES.Fold( + func(err error) bool { + assert.Equal(t, useError, err) + return true + }, + func(p Pair[resourceState, string]) bool { + t.Error("Expected error but got success") + return false + }, + )(outcome) +} + +// TestWithResourceStateThreading tests that state is properly threaded through all operations +func TestWithResourceStateThreading(t *testing.T) { + initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0} + ctx := context.Background() + + // Create increments counter + onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] { + return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] { + return func() Result[Pair[resourceState, mockResource]] { + newState := resourceState{ + resourcesCreated: s.resourcesCreated + 1, + resourcesReleased: s.resourcesReleased, + } + res := mockResource{id: newState.resourcesCreated, isValid: true} + return RES.Of(P.MakePair(newState, res)) + } + } + } + + // Use observes the state after creation + useResource := func(res mockResource) StateReaderIOResult[resourceState, int] { + return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] { + return func(ctx context.Context) IOResult[Pair[resourceState, int]] { + return func() Result[Pair[resourceState, int]] { + // Verify state was updated by onCreate + assert.Equal(t, 1, s.resourcesCreated) + assert.Equal(t, 0, s.resourcesReleased) + return RES.Of(P.MakePair(s, s.resourcesCreated)) + } + } + } + } + + // Release increments released counter + onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] { + return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] { + return func(ctx context.Context) IOResult[Pair[resourceState, int]] { + return func() Result[Pair[resourceState, int]] { + // Verify state was updated by onCreate and use + assert.Equal(t, 1, s.resourcesCreated) + assert.Equal(t, 0, s.resourcesReleased) + + newState := resourceState{ + resourcesCreated: s.resourcesCreated, + resourcesReleased: s.resourcesReleased + 1, + } + return RES.Of(P.MakePair(newState, 0)) + } + } + } + } + + withResource := WithResource[int](onCreate, onRelease) + result := withResource(useResource) + outcome := result(initialState)(ctx)() + + assert.True(t, RES.IsRight(outcome)) + RES.Map(func(p Pair[resourceState, int]) Pair[resourceState, int] { + finalState := P.Head(p) + value := P.Tail(p) + + // Verify final state + // Note: Final state is from the use function, which preserves the state it received + // onCreate: 0->1, use: sees 1, release: sees 1 and increments released to 1 + // But final state is from use function where resourcesReleased=0 + assert.Equal(t, 1, finalState.resourcesCreated) + assert.Equal(t, 0, finalState.resourcesReleased, "Final state is from use function, before release") + assert.Equal(t, 1, value) + + return p + })(outcome) +} + +// TestWithResourceMultipleResources tests using WithResource multiple times (nesting) +func TestWithResourceMultipleResources(t *testing.T) { + initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0} + ctx := context.Background() + + createResource := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] { + return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] { + return func() Result[Pair[resourceState, mockResource]] { + newState := resourceState{ + resourcesCreated: s.resourcesCreated + 1, + resourcesReleased: s.resourcesReleased, + } + res := mockResource{id: newState.resourcesCreated, isValid: true} + return RES.Of(P.MakePair(newState, res)) + } + } + } + + releaseResource := func(res mockResource) StateReaderIOResult[resourceState, int] { + return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] { + return func(ctx context.Context) IOResult[Pair[resourceState, int]] { + return func() Result[Pair[resourceState, int]] { + newState := resourceState{ + resourcesCreated: s.resourcesCreated, + resourcesReleased: s.resourcesReleased + 1, + } + return RES.Of(P.MakePair(newState, 0)) + } + } + } + } + + // Create two nested resources + withResource1 := WithResource[int](createResource, releaseResource) + withResource2 := WithResource[int](createResource, releaseResource) + + result := withResource1(func(res1 mockResource) StateReaderIOResult[resourceState, int] { + return withResource2(func(res2 mockResource) StateReaderIOResult[resourceState, int] { + // Both resources should be available + return Of[resourceState](res1.id + res2.id) + }) + }) + + outcome := result(initialState)(ctx)() + + assert.True(t, RES.IsRight(outcome)) + RES.Map(func(p Pair[resourceState, int]) Pair[resourceState, int] { + finalState := P.Head(p) + value := P.Tail(p) + + // Both resources created, but final state is from innermost use function + // onCreate1: 0->1, onCreate2: 1->2, use (Of): sees 2 + // Release functions execute but their state changes aren't in the final result + assert.Equal(t, 2, finalState.resourcesCreated) + assert.Equal(t, 0, finalState.resourcesReleased, "Final state is from use function, before releases") + // res1.id = 1, res2.id = 2, sum = 3 + assert.Equal(t, 3, value) + + return p + })(outcome) +} + +// TestWithResourceContextCancellation tests behavior with context cancellation +func TestWithResourceContextCancellation(t *testing.T) { + initialState := resourceState{resourcesCreated: 0, resourcesReleased: 0} + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + cancelError := errors.New("context cancelled") + + // Create should respect context cancellation + onCreate := func(s resourceState) ReaderIOResult[Pair[resourceState, mockResource]] { + return func(ctx context.Context) IOResult[Pair[resourceState, mockResource]] { + return func() Result[Pair[resourceState, mockResource]] { + if ctx.Err() != nil { + return RES.Left[Pair[resourceState, mockResource]](cancelError) + } + newState := resourceState{ + resourcesCreated: s.resourcesCreated + 1, + resourcesReleased: s.resourcesReleased, + } + res := mockResource{id: 1, isValid: true} + return RES.Of(P.MakePair(newState, res)) + } + } + } + + onRelease := func(res mockResource) StateReaderIOResult[resourceState, int] { + return func(s resourceState) ReaderIOResult[Pair[resourceState, int]] { + return func(ctx context.Context) IOResult[Pair[resourceState, int]] { + return func() Result[Pair[resourceState, int]] { + newState := resourceState{ + resourcesCreated: s.resourcesCreated, + resourcesReleased: s.resourcesReleased + 1, + } + return RES.Of(P.MakePair(newState, 0)) + } + } + } + } + + useResource := func(res mockResource) StateReaderIOResult[resourceState, string] { + return Of[resourceState]("should not reach here") + } + + withResource := WithResource[string](onCreate, onRelease) + result := withResource(useResource) + outcome := result(initialState)(ctx)() + + assert.True(t, RES.IsLeft(outcome)) + RES.Fold( + func(err error) bool { + assert.Equal(t, cancelError, err) + return true + }, + func(p Pair[resourceState, string]) bool { + t.Error("Expected error but got success") + return false + }, + )(outcome) +} diff --git a/v2/context/statereaderioresult/state.go b/v2/context/statereaderioresult/state.go new file mode 100644 index 0000000..8eee05e --- /dev/null +++ b/v2/context/statereaderioresult/state.go @@ -0,0 +1,309 @@ +// 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 statereaderioresult + +import ( + "context" + + RIORES "github.com/IBM/fp-go/v2/context/readerioresult" + "github.com/IBM/fp-go/v2/function" + "github.com/IBM/fp-go/v2/internal/statet" + RIOR "github.com/IBM/fp-go/v2/readerioresult" + "github.com/IBM/fp-go/v2/result" +) + +// Left creates a StateReaderIOResult that represents a failed computation with the given error. +// The error value is immediately available and does not depend on state or context. +// +// Example: +// +// result := statereaderioresult.Left[AppState, string](errors.New("validation failed")) +// // Returns a failed computation that ignores state and context +func Left[S, A any](e error) StateReaderIOResult[S, A] { + return function.Constant1[S](RIORES.Left[Pair[S, A]](e)) +} + +// Right creates a StateReaderIOResult that represents a successful computation with the given value. +// The value is wrapped and the state is passed through unchanged. +// +// Example: +// +// result := statereaderioresult.Right[AppState](42) +// // Returns a successful computation containing 42 +func Right[S, A any](a A) StateReaderIOResult[S, A] { + return statet.Of[StateReaderIOResult[S, A]](RIORES.Of[Pair[S, A]], a) +} + +// Of creates a StateReaderIOResult that represents a successful computation with the given value. +// This is the monadic return/pure operation for StateReaderIOResult. +// Equivalent to [Right]. +// +// Example: +// +// result := statereaderioresult.Of[AppState](42) +// // Returns a successful computation containing 42 +func Of[S, A any](a A) StateReaderIOResult[S, A] { + return Right[S](a) +} + +// MonadMap transforms the success value of a StateReaderIOResult using the provided function. +// If the computation fails, the error is propagated unchanged. +// The state is threaded through the computation. +// This is the functor map operation. +// +// Example: +// +// result := statereaderioresult.MonadMap( +// statereaderioresult.Of[AppState](21), +// func(x int) int { return x * 2 }, +// ) // Result contains 42 +func MonadMap[S, A, B any](fa StateReaderIOResult[S, A], f func(A) B) StateReaderIOResult[S, B] { + return statet.MonadMap[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]( + RIORES.MonadMap[Pair[S, A], Pair[S, B]], + fa, + f, + ) +} + +// Map is the curried version of [MonadMap]. +// Returns a function that transforms a StateReaderIOResult. +// +// Example: +// +// double := statereaderioresult.Map[AppState](func(x int) int { return x * 2 }) +// result := function.Pipe1(statereaderioresult.Of[AppState](21), double) +func Map[S, A, B any](f func(A) B) Operator[S, A, B] { + return statet.Map[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]( + RIORES.Map[Pair[S, A], Pair[S, B]], + f, + ) +} + +// MonadChain sequences two computations, passing the result of the first to a function +// that produces the second computation. This is the monadic bind operation. +// The state is threaded through both computations. +// +// Example: +// +// result := statereaderioresult.MonadChain( +// statereaderioresult.Of[AppState](5), +// func(x int) statereaderioresult.StateReaderIOResult[AppState, string] { +// return statereaderioresult.Of[AppState](fmt.Sprintf("value: %d", x)) +// }, +// ) +func MonadChain[S, A, B any](fa StateReaderIOResult[S, A], f Kleisli[S, A, B]) StateReaderIOResult[S, B] { + return statet.MonadChain( + RIORES.MonadChain[Pair[S, A], Pair[S, B]], + fa, + f, + ) +} + +// Chain is the curried version of [MonadChain]. +// Returns a function that sequences computations. +// +// Example: +// +// stringify := statereaderioresult.Chain[AppState](func(x int) statereaderioresult.StateReaderIOResult[AppState, string] { +// return statereaderioresult.Of[AppState](fmt.Sprintf("%d", x)) +// }) +// result := function.Pipe1(statereaderioresult.Of[AppState](42), stringify) +func Chain[S, A, B any](f Kleisli[S, A, B]) Operator[S, A, B] { + return statet.Chain[StateReaderIOResult[S, A]]( + RIORES.Chain[Pair[S, A], Pair[S, B]], + f, + ) +} + +// MonadAp applies a function wrapped in a StateReaderIOResult to a value wrapped in a StateReaderIOResult. +// If either the function or the value fails, the error is propagated. +// The state is threaded through both computations sequentially. +// This is the applicative apply operation. +// +// Example: +// +// fab := statereaderioresult.Of[AppState](func(x int) int { return x * 2 }) +// fa := statereaderioresult.Of[AppState](21) +// result := statereaderioresult.MonadAp(fab, fa) // Result contains 42 +func MonadAp[B, S, A any](fab StateReaderIOResult[S, func(A) B], fa StateReaderIOResult[S, A]) StateReaderIOResult[S, B] { + return statet.MonadAp[StateReaderIOResult[S, A], StateReaderIOResult[S, B]]( + RIORES.MonadMap[Pair[S, A], Pair[S, B]], + RIORES.MonadChain[Pair[S, func(A) B], Pair[S, B]], + fab, + fa, + ) +} + +// Ap is the curried version of [MonadAp]. +// Returns a function that applies a wrapped function to the given wrapped value. +func Ap[B, S, A any](fa StateReaderIOResult[S, A]) Operator[S, func(A) B, B] { + return statet.Ap[StateReaderIOResult[S, A], StateReaderIOResult[S, B], StateReaderIOResult[S, func(A) B]]( + RIORES.Map[Pair[S, A], Pair[S, B]], + RIORES.Chain[Pair[S, func(A) B], Pair[S, B]], + fa, + ) +} + +// FromReaderIOResult lifts a ReaderIOResult into a StateReaderIOResult. +// The state is passed through unchanged. +// +// Example: +// +// riores := readerioresult.Of(42) +// result := statereaderioresult.FromReaderIOResult[AppState](riores) +func FromReaderIOResult[S, A any](fa ReaderIOResult[A]) StateReaderIOResult[S, A] { + return statet.FromF[StateReaderIOResult[S, A]]( + RIORES.MonadMap[A], + fa, + ) +} + +// FromIOResult lifts an IOResult into a StateReaderIOResult. +// The state is passed through unchanged and the context is ignored. +func FromIOResult[S, A any](fa IOResult[A]) StateReaderIOResult[S, A] { + return FromReaderIOResult[S](RIORES.FromIOResult(fa)) +} + +// FromState lifts a State computation into a StateReaderIOResult. +// The computation cannot fail (uses the error type). +func FromState[S, A any](sa State[S, A]) StateReaderIOResult[S, A] { + return statet.FromState[StateReaderIOResult[S, A]](RIORES.Of[Pair[S, A]], sa) +} + +// FromIO lifts an IO computation into a StateReaderIOResult. +// The state is passed through unchanged and the context is ignored. +func FromIO[S, A any](fa IO[A]) StateReaderIOResult[S, A] { + return FromReaderIOResult[S](RIORES.FromIO(fa)) +} + +// FromResult lifts a Result into a StateReaderIOResult. +// The state is passed through unchanged and the context is ignored. +// +// Example: +// +// result := statereaderioresult.FromResult[AppState](result.Of(42)) +func FromResult[S, A any](ma Result[A]) StateReaderIOResult[S, A] { + return result.Fold(Left[S, A], Right[S, A])(ma) +} + +// Combinators + +// Local runs a computation with a modified context. +// The function f transforms the context before passing it to the computation. +// +// Example: +// +// // Modify context before running computation +// withTimeout := statereaderioresult.Local[AppState]( +// func(ctx context.Context) context.Context { +// ctx, _ = context.WithTimeout(ctx, 60*time.Second) +// return ctx +// } +// ) +// result := withTimeout(computation) +func Local[S, A any](f func(context.Context) context.Context) func(StateReaderIOResult[S, A]) StateReaderIOResult[S, A] { + return func(ma StateReaderIOResult[S, A]) StateReaderIOResult[S, A] { + return function.Flow2(ma, RIOR.Local[Pair[S, A]](f)) + } +} + +// Asks creates a computation that derives a value from the context. +// The function receives the context and returns a StateReaderIOResult. +// +// Example: +// +// getValue := statereaderioresult.Asks[AppState, string]( +// func(ctx context.Context) statereaderioresult.StateReaderIOResult[AppState, string] { +// return statereaderioresult.Of[AppState](ctx.Value("key").(string)) +// }, +// ) +func Asks[S, A any](f func(context.Context) StateReaderIOResult[S, A]) StateReaderIOResult[S, A] { + return func(s S) ReaderIOResult[Pair[S, A]] { + return func(ctx context.Context) IOResult[Pair[S, A]] { + return f(ctx)(s)(ctx) + } + } +} + +// FromResultK lifts a Result-returning function into a Kleisli arrow for StateReaderIOResult. +// +// Example: +// +// validate := func(x int) result.Result[int] { +// if x > 0 { return result.Of(x) } +// return result.Error[int](errors.New("negative")) +// } +// kleisli := statereaderioresult.FromResultK[AppState](validate) +func FromResultK[S, A, B any](f func(A) Result[B]) Kleisli[S, A, B] { + return function.Flow2( + f, + FromResult[S, B], + ) +} + +// FromIOK lifts an IO-returning function into a Kleisli arrow for StateReaderIOResult. +func FromIOK[S, A, B any](f func(A) IO[B]) Kleisli[S, A, B] { + return function.Flow2( + f, + FromIO[S, B], + ) +} + +// FromIOResultK lifts an IOResult-returning function into a Kleisli arrow for StateReaderIOResult. +func FromIOResultK[S, A, B any](f func(A) IOResult[B]) Kleisli[S, A, B] { + return function.Flow2( + f, + FromIOResult[S, B], + ) +} + +// FromReaderIOResultK lifts a ReaderIOResult-returning function into a Kleisli arrow for StateReaderIOResult. +func FromReaderIOResultK[S, A, B any](f func(A) ReaderIOResult[B]) Kleisli[S, A, B] { + return function.Flow2( + f, + FromReaderIOResult[S, B], + ) +} + +// MonadChainReaderIOResultK chains a StateReaderIOResult with a ReaderIOResult-returning function. +func MonadChainReaderIOResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) ReaderIOResult[B]) StateReaderIOResult[S, B] { + return MonadChain(ma, FromReaderIOResultK[S](f)) +} + +// ChainReaderIOResultK is the curried version of [MonadChainReaderIOResultK]. +func ChainReaderIOResultK[S, A, B any](f func(A) ReaderIOResult[B]) Operator[S, A, B] { + return Chain(FromReaderIOResultK[S](f)) +} + +// MonadChainIOResultK chains a StateReaderIOResult with an IOResult-returning function. +func MonadChainIOResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) IOResult[B]) StateReaderIOResult[S, B] { + return MonadChain(ma, FromIOResultK[S](f)) +} + +// ChainIOResultK is the curried version of [MonadChainIOResultK]. +func ChainIOResultK[S, A, B any](f func(A) IOResult[B]) Operator[S, A, B] { + return Chain(FromIOResultK[S](f)) +} + +// MonadChainResultK chains a StateReaderIOResult with a Result-returning function. +func MonadChainResultK[S, A, B any](ma StateReaderIOResult[S, A], f func(A) Result[B]) StateReaderIOResult[S, B] { + return MonadChain(ma, FromResultK[S](f)) +} + +// ChainResultK is the curried version of [MonadChainResultK]. +func ChainResultK[S, A, B any](f func(A) Result[B]) Operator[S, A, B] { + return Chain(FromResultK[S](f)) +} diff --git a/v2/context/statereaderioresult/statereaderioresult_test.go b/v2/context/statereaderioresult/statereaderioresult_test.go new file mode 100644 index 0000000..0f63bea --- /dev/null +++ b/v2/context/statereaderioresult/statereaderioresult_test.go @@ -0,0 +1,567 @@ +// 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 statereaderioresult + +import ( + "context" + "errors" + "fmt" + "testing" + + F "github.com/IBM/fp-go/v2/function" + "github.com/IBM/fp-go/v2/io" + IOR "github.com/IBM/fp-go/v2/ioresult" + N "github.com/IBM/fp-go/v2/number" + P "github.com/IBM/fp-go/v2/pair" + RES "github.com/IBM/fp-go/v2/result" + "github.com/stretchr/testify/assert" +) + +type testState struct { + counter int +} + +func TestOf(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + result := Of[testState](42) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Fold( + func(err error) bool { + t.Fatalf("Expected Success but got Error: %v", err) + return false + }, + func(p P.Pair[testState, int]) bool { + assert.Equal(t, 42, P.Tail(p)) + assert.Equal(t, 0, P.Head(p).counter) // State unchanged + return true + }, + )(res) +} + +func TestRight(t *testing.T) { + state := testState{counter: 5} + ctx := context.Background() + result := Right[testState](100) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 100, P.Tail(p)) + assert.Equal(t, 5, P.Head(p).counter) + return p + })(res) +} + +func TestLeft(t *testing.T) { + state := testState{counter: 10} + ctx := context.Background() + testErr := errors.New("test error") + result := Left[testState, int](testErr) + res := result(state)(ctx)() + + assert.True(t, RES.IsLeft(res)) +} + +func TestMonadMap(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + result := MonadMap( + Of[testState](21), + N.Mul(2), + ) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 42, P.Tail(p)) + return p + })(res) +} + +func TestMap(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + result := F.Pipe1( + Of[testState](21), + Map[testState](N.Mul(2)), + ) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 42, P.Tail(p)) + return p + })(res) +} + +func TestMonadChain(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + result := MonadChain( + Of[testState](5), + func(x int) StateReaderIOResult[testState, string] { + return Of[testState](fmt.Sprintf("value: %d", x)) + }, + ) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] { + assert.Equal(t, "value: 5", P.Tail(p)) + return p + })(res) +} + +func TestChain(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + result := F.Pipe1( + Of[testState](5), + Chain[testState](func(x int) StateReaderIOResult[testState, string] { + return Of[testState](fmt.Sprintf("value: %d", x)) + }), + ) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] { + assert.Equal(t, "value: 5", P.Tail(p)) + return p + })(res) +} + +func TestMonadAp(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + fab := Of[testState](N.Mul(2)) + fa := Of[testState](21) + result := MonadAp(fab, fa) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 42, P.Tail(p)) + return p + })(res) +} + +func TestAp(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + fa := Of[testState](21) + result := F.Pipe1( + Of[testState](N.Mul(2)), + Ap[int](fa), + ) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 42, P.Tail(p)) + return p + })(res) +} + +func TestFromIOResult(t *testing.T) { + state := testState{counter: 3} + ctx := context.Background() + + ior := IOR.Of(55) + result := FromIOResult[testState](ior) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 55, P.Tail(p)) + assert.Equal(t, 3, P.Head(p).counter) + return p + })(res) +} + +func TestFromState(t *testing.T) { + initialState := testState{counter: 10} + ctx := context.Background() + + // State computation that increments counter and returns it + stateComp := func(s testState) P.Pair[testState, int] { + newState := testState{counter: s.counter + 1} + return P.MakePair(newState, newState.counter) + } + + result := FromState[testState](stateComp) + res := result(initialState)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 11, P.Tail(p)) // Incremented value + assert.Equal(t, 11, P.Head(p).counter) // State updated + return p + })(res) +} + +func TestFromIO(t *testing.T) { + state := testState{counter: 8} + ctx := context.Background() + + ioVal := func() int { return 99 } + result := FromIO[testState](ioVal) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 99, P.Tail(p)) + assert.Equal(t, 8, P.Head(p).counter) + return p + })(res) +} + +func TestFromResult(t *testing.T) { + state := testState{counter: 12} + ctx := context.Background() + + // Test Success case + resultSuccess := FromResult[testState](RES.Of(42)) + resSuccess := resultSuccess(state)(ctx)() + assert.True(t, RES.IsRight(resSuccess)) + + // Test Error case + resultError := FromResult[testState](RES.Left[int](errors.New("error"))) + resError := resultError(state)(ctx)() + assert.True(t, RES.IsLeft(resError)) +} + +func TestLocal(t *testing.T) { + state := testState{counter: 0} + ctx := context.WithValue(context.Background(), "key", "value1") + + // Create a computation that uses the context + comp := Asks[testState, string](func(c context.Context) StateReaderIOResult[testState, string] { + val := c.Value("key").(string) + return Of[testState](val) + }) + + // Modify context before running computation + result := Local[testState, string]( + func(c context.Context) context.Context { + return context.WithValue(c, "key", "value2") + }, + )(comp) + + res := result(state)(ctx)() + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] { + assert.Equal(t, "value2", P.Tail(p)) + return p + })(res) +} + +func TestAsks(t *testing.T) { + state := testState{counter: 0} + ctx := context.WithValue(context.Background(), "multiplier", 7) + + result := Asks[testState, int](func(c context.Context) StateReaderIOResult[testState, int] { + mult := c.Value("multiplier").(int) + return Of[testState](mult * 5) + }) + + res := result(state)(ctx)() + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 35, P.Tail(p)) + return p + })(res) +} + +func TestFromResultK(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + validate := func(x int) RES.Result[int] { + if x > 0 { + return RES.Of(x * 2) + } + return RES.Left[int](errors.New("negative")) + } + + kleisli := FromResultK[testState](validate) + + // Test with valid input + resultValid := kleisli(5) + resValid := resultValid(state)(ctx)() + assert.True(t, RES.IsRight(resValid)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 10, P.Tail(p)) + return p + })(resValid) + + // Test with invalid input + resultInvalid := kleisli(-5) + resInvalid := resultInvalid(state)(ctx)() + assert.True(t, RES.IsLeft(resInvalid)) +} + +func TestFromIOK(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + ioFunc := func(x int) io.IO[int] { + return func() int { return x * 3 } + } + + kleisli := FromIOK[testState](ioFunc) + result := kleisli(7) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 21, P.Tail(p)) + return p + })(res) +} + +func TestFromIOResultK(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + iorFunc := func(x int) IOR.IOResult[int] { + if x > 0 { + return IOR.Of(x * 4) + } + return IOR.Left[int](errors.New("invalid")) + } + + kleisli := FromIOResultK[testState](iorFunc) + result := kleisli(3) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 12, P.Tail(p)) + return p + })(res) +} + +func TestChainResultK(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + validate := func(x int) RES.Result[string] { + if x > 0 { + return RES.Of(fmt.Sprintf("valid: %d", x)) + } + return RES.Left[string](errors.New("invalid")) + } + + result := F.Pipe1( + Of[testState](42), + ChainResultK[testState](validate), + ) + + res := result(state)(ctx)() + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] { + assert.Equal(t, "valid: 42", P.Tail(p)) + return p + })(res) +} + +func TestChainIOResultK(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + iorFunc := func(x int) IOR.IOResult[string] { + return IOR.Of(fmt.Sprintf("result: %d", x)) + } + + result := F.Pipe1( + Of[testState](100), + ChainIOResultK[testState](iorFunc), + ) + + res := result(state)(ctx)() + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] { + assert.Equal(t, "result: 100", P.Tail(p)) + return p + })(res) +} + +func TestDo(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + type Result struct { + value int + } + + result := Do[testState](Result{}) + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, Result]) P.Pair[testState, Result] { + assert.Equal(t, 0, P.Tail(p).value) + return p + })(res) +} + +func TestBindTo(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + type Result struct { + value int + } + + result := F.Pipe1( + Of[testState](42), + BindTo[testState](func(v int) Result { + return Result{value: v} + }), + ) + + res := result(state)(ctx)() + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, Result]) P.Pair[testState, Result] { + assert.Equal(t, 42, P.Tail(p).value) + return p + })(res) +} + +func TestStatefulComputation(t *testing.T) { + initialState := testState{counter: 0} + ctx := context.Background() + + // Create a computation that modifies state + incrementAndGet := func(s testState) P.Pair[testState, int] { + newState := testState{counter: s.counter + 1} + return P.MakePair(newState, newState.counter) + } + + // Chain multiple stateful operations + result := F.Pipe2( + FromState[testState](incrementAndGet), + Chain[testState](func(v1 int) StateReaderIOResult[testState, int] { + return FromState[testState](incrementAndGet) + }), + Chain[testState](func(v2 int) StateReaderIOResult[testState, int] { + return FromState[testState](incrementAndGet) + }), + ) + + res := result(initialState)(ctx)() + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, int]) P.Pair[testState, int] { + assert.Equal(t, 3, P.Tail(p)) // Last incremented value + assert.Equal(t, 3, P.Head(p).counter) // State updated three times + return p + })(res) +} + +func TestErrorPropagation(t *testing.T) { + state := testState{counter: 0} + ctx := context.Background() + + testErr := errors.New("test error") + + // Chain operations where the second one fails + result := F.Pipe1( + Of[testState](42), + Chain[testState](func(x int) StateReaderIOResult[testState, int] { + return Left[testState, int](testErr) + }), + ) + + res := result(state)(ctx)() + assert.True(t, RES.IsLeft(res)) +} + +func TestPointed(t *testing.T) { + p := Pointed[testState, int]() + assert.NotNil(t, p) + + result := p.Of(42) + state := testState{counter: 0} + ctx := context.Background() + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) +} + +func TestFunctor(t *testing.T) { + f := Functor[testState, int, string]() + assert.NotNil(t, f) + + mapper := f.Map(func(x int) string { return fmt.Sprintf("%d", x) }) + result := mapper(Of[testState](42)) + + state := testState{counter: 0} + ctx := context.Background() + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] { + assert.Equal(t, "42", P.Tail(p)) + return p + })(res) +} + +func TestApplicative(t *testing.T) { + a := Applicative[testState, int, string]() + assert.NotNil(t, a) + + fab := Of[testState](func(x int) string { return fmt.Sprintf("%d", x) }) + fa := Of[testState](42) + result := a.Ap(fa)(fab) + + state := testState{counter: 0} + ctx := context.Background() + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] { + assert.Equal(t, "42", P.Tail(p)) + return p + })(res) +} + +func TestMonad(t *testing.T) { + m := Monad[testState, int, string]() + assert.NotNil(t, m) + + fa := m.Of(42) + result := m.Chain(func(x int) StateReaderIOResult[testState, string] { + return Of[testState](fmt.Sprintf("%d", x)) + })(fa) + + state := testState{counter: 0} + ctx := context.Background() + res := result(state)(ctx)() + + assert.True(t, RES.IsRight(res)) + RES.Map(func(p P.Pair[testState, string]) P.Pair[testState, string] { + assert.Equal(t, "42", P.Tail(p)) + return p + })(res) +} diff --git a/v2/context/statereaderioresult/testing/laws.go b/v2/context/statereaderioresult/testing/laws.go new file mode 100644 index 0000000..0b7e49e --- /dev/null +++ b/v2/context/statereaderioresult/testing/laws.go @@ -0,0 +1,87 @@ +// 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 testing + +import ( + "context" + "testing" + + EQ "github.com/IBM/fp-go/v2/eq" + L "github.com/IBM/fp-go/v2/internal/monad/testing" + P "github.com/IBM/fp-go/v2/pair" + RES "github.com/IBM/fp-go/v2/result" + RIORES "github.com/IBM/fp-go/v2/context/readerioresult" + ST "github.com/IBM/fp-go/v2/context/statereaderioresult" +) + +// AssertLaws asserts the monad laws for the StateReaderIOResult monad +func AssertLaws[S, A, B, C any](t *testing.T, + eqs EQ.Eq[S], + eqa EQ.Eq[A], + eqb EQ.Eq[B], + eqc EQ.Eq[C], + + ab func(A) B, + bc func(B) C, + + s S, + ctx context.Context, +) func(a A) bool { + + eqra := RIORES.Eq(RES.Eq(P.Eq(eqs, eqa)))(ctx) + eqrb := RIORES.Eq(RES.Eq(P.Eq(eqs, eqb)))(ctx) + eqrc := RIORES.Eq(RES.Eq(P.Eq(eqs, eqc)))(ctx) + + fofc := ST.Pointed[S, C]() + fofaa := ST.Pointed[S, func(A) A]() + fofbc := ST.Pointed[S, func(B) C]() + fofabb := ST.Pointed[S, func(func(A) B) B]() + + fmap := ST.Functor[S, func(B) C, func(func(A) B) func(A) C]() + + fapabb := ST.Applicative[S, func(A) B, B]() + fapabac := ST.Applicative[S, func(A) B, func(A) C]() + + maa := ST.Monad[S, A, A]() + mab := ST.Monad[S, A, B]() + mac := ST.Monad[S, A, C]() + mbc := ST.Monad[S, B, C]() + + return L.MonadAssertLaws(t, + ST.Eq(eqra)(s), + ST.Eq(eqrb)(s), + ST.Eq(eqrc)(s), + + fofc, + fofaa, + fofbc, + fofabb, + + fmap, + + fapabb, + fapabac, + + maa, + mab, + mac, + mbc, + + ab, + bc, + ) + +} diff --git a/v2/context/statereaderioresult/testing/laws_test.go b/v2/context/statereaderioresult/testing/laws_test.go new file mode 100644 index 0000000..892337a --- /dev/null +++ b/v2/context/statereaderioresult/testing/laws_test.go @@ -0,0 +1,50 @@ +// 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 testing + +import ( + "context" + "fmt" + "testing" + + A "github.com/IBM/fp-go/v2/array" + EQ "github.com/IBM/fp-go/v2/eq" + "github.com/stretchr/testify/assert" +) + +func TestMonadLaws(t *testing.T) { + // some comparison + eqs := A.Eq(EQ.FromStrictEquals[string]()) + eqa := EQ.FromStrictEquals[bool]() + eqb := EQ.FromStrictEquals[int]() + eqc := EQ.FromStrictEquals[string]() + + ab := func(a bool) int { + if a { + return 1 + } + return 0 + } + + bc := func(b int) string { + return fmt.Sprintf("value %d", b) + } + + laws := AssertLaws(t, eqs, eqa, eqb, eqc, ab, bc, A.Empty[string](), context.Background()) + + assert.True(t, laws(true)) + assert.True(t, laws(false)) +} diff --git a/v2/context/statereaderioresult/type.go b/v2/context/statereaderioresult/type.go new file mode 100644 index 0000000..af66d65 --- /dev/null +++ b/v2/context/statereaderioresult/type.go @@ -0,0 +1,84 @@ +// 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 statereaderioresult + +import ( + RIORES "github.com/IBM/fp-go/v2/context/readerioresult" + "github.com/IBM/fp-go/v2/endomorphism" + "github.com/IBM/fp-go/v2/io" + "github.com/IBM/fp-go/v2/ioresult" + "github.com/IBM/fp-go/v2/optics/iso/lens" + "github.com/IBM/fp-go/v2/pair" + "github.com/IBM/fp-go/v2/reader" + "github.com/IBM/fp-go/v2/result" + "github.com/IBM/fp-go/v2/state" +) + +type ( + // Endomorphism represents a function from A to A. + Endomorphism[A any] = endomorphism.Endomorphism[A] + + // Lens is an optic that focuses on a field of type A within a structure of type S. + Lens[S, A any] = lens.Lens[S, A] + + // State represents a stateful computation that takes an initial state S and returns + // a pair of the new state S and a value A. + State[S, A any] = state.State[S, A] + + // Pair represents a tuple of two values. + Pair[L, R any] = pair.Pair[L, R] + + // Reader represents a computation that depends on an environment/context of type R + // and produces a value of type A. + Reader[R, A any] = reader.Reader[R, A] + + // Result represents a value that can be either an error or a success value. + // This is specialized to use [error] as the error type. + Result[A any] = result.Result[A] + + // IO represents a computation that performs side effects and produces a value of type A. + IO[A any] = io.IO[A] + + // IOResult represents a computation that performs side effects and can fail with an error + // or succeed with a value A. + IOResult[A any] = ioresult.IOResult[A] + + // ReaderIOResult represents a computation that depends on a context.Context, + // performs side effects, and can fail with an error or succeed with a value A. + ReaderIOResult[A any] = RIORES.ReaderIOResult[A] + + // StateReaderIOResult represents a stateful computation that: + // - Takes an initial state S + // - Depends on a [context.Context] + // - Performs side effects (IO) + // - Can fail with an [error] or succeed with a value A + // - Returns a pair of the new state S and the result + // + // This is the main type of this package, combining State, Reader, IO, and Result monads. + // It is a specialization of StateReaderIOEither with: + // - Context type fixed to [context.Context] + // - Error type fixed to [error] + StateReaderIOResult[S, A any] = Reader[S, ReaderIOResult[Pair[S, A]]] + + // Kleisli represents a Kleisli arrow - a function that takes a value A and returns + // a StateReaderIOResult computation producing B. + // This is used for monadic composition via Chain. + Kleisli[S, A, B any] = Reader[A, StateReaderIOResult[S, B]] + + // Operator represents a function that transforms one StateReaderIOResult into another. + // This is commonly used for building composable operations via Map, Chain, etc. + Operator[S, A, B any] = Reader[StateReaderIOResult[S, A], StateReaderIOResult[S, B]] +) diff --git a/v2/either/resource.go b/v2/either/resource.go index 6745794..a0e0cc3 100644 --- a/v2/either/resource.go +++ b/v2/either/resource.go @@ -39,7 +39,10 @@ package either // // Use file here // return either.Right[error]("data") // }) -func WithResource[E, R, A, ANY any](onCreate func() Either[E, R], onRelease Kleisli[E, R, ANY]) Kleisli[E, Kleisli[E, R, A], A] { +func WithResource[A, E, R, ANY any]( + onCreate func() Either[E, R], + onRelease Kleisli[E, R, ANY], +) Kleisli[E, Kleisli[E, R, A], A] { return func(f func(R) Either[E, A]) Either[E, A] { r := onCreate() if r.isLeft { diff --git a/v2/either/resource_test.go b/v2/either/resource_test.go index 51700ed..cf03b26 100644 --- a/v2/either/resource_test.go +++ b/v2/either/resource_test.go @@ -40,7 +40,7 @@ func TestWithResource(t *testing.T) { return Of[error](f.Name()) } - tempFile := WithResource[error, *os.File, string](onCreate, onDelete) + tempFile := WithResource[string](onCreate, onDelete) resE := tempFile(onHandler) diff --git a/v2/result/resource.go b/v2/result/resource.go index 981b319..1a09b9a 100644 --- a/v2/result/resource.go +++ b/v2/result/resource.go @@ -43,6 +43,9 @@ import ( // // Use file here // return either.Right[error]("data") // }) -func WithResource[R, A, ANY any](onCreate func() Result[R], onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] { - return either.WithResource[error, R, A](onCreate, onRelease) +func WithResource[A, R, ANY any]( + onCreate func() Result[R], + onRelease Kleisli[R, ANY], +) Kleisli[Kleisli[R, A], A] { + return either.WithResource[A](onCreate, onRelease) } diff --git a/v2/result/resource_test.go b/v2/result/resource_test.go index aaa378f..e17f7f7 100644 --- a/v2/result/resource_test.go +++ b/v2/result/resource_test.go @@ -40,7 +40,7 @@ func TestWithResource(t *testing.T) { return Of(f.Name()) } - tempFile := WithResource[*os.File, string](onCreate, onDelete) + tempFile := WithResource[string](onCreate, onDelete) resE := tempFile(onHandler) diff --git a/v2/statereaderioeither/resource.go b/v2/statereaderioeither/resource.go new file mode 100644 index 0000000..84080bc --- /dev/null +++ b/v2/statereaderioeither/resource.go @@ -0,0 +1,80 @@ +// 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 statereaderioeither + +import ( + "github.com/IBM/fp-go/v2/pair" + "github.com/IBM/fp-go/v2/readerioeither" +) + +func uncurryState[S, R, E, A, B any](f func(A) readerioeither.Kleisli[R, E, S, B]) readerioeither.Kleisli[R, E, Pair[S, A], B] { + return func(r Pair[S, A]) ReaderIOEither[R, E, B] { + return f(pair.Tail(r))(pair.Head(r)) + } +} + +// WithResource constructs a function that creates a resource with state management, operates on it, and then releases the resource. +// This ensures proper resource cleanup even in the presence of errors, following the Resource Acquisition Is Initialization (RAII) pattern. +// The state is threaded through all operations: resource creation, usage, and release. +// +// The resource lifecycle with state management is: +// 1. onCreate: Acquires the resource (may modify state) +// 2. use: Operates on the resource with current state (provided as argument to the returned function) +// 3. onRelease: Releases the resource with current state (called regardless of success or failure) +// +// Type parameters: +// - S: The state type that is threaded through all operations +// - R: The reader/context type +// - E: The error type +// - RES: The resource type +// - A: The type of the result produced by using the resource +// - ANY: The type returned by the release function (typically ignored) +// +// Parameters: +// - onCreate: A stateful computation that acquires the resource +// - onRelease: A stateful function that releases the resource, called with the resource and current state, executed regardless of errors +// +// Returns: +// +// A function that takes a resource-using function and returns a StateReaderIOEither that manages the resource lifecycle with state +// +// Example: +// +// type AppState struct { +// openFiles int +// } +// +// withFile := WithResource( +// openFile("data.txt"), // Increments openFiles in state +// func(f *File) StateReaderIOEither[AppState, Config, error, int] { +// return closeFile(f) // Decrements openFiles in state +// }, +// ) +// result := withFile(func(f *File) StateReaderIOEither[AppState, Config, error, string] { +// return readContent(f) +// }) +func WithResource[A, S, R, E, RES, ANY any]( + onCreate StateReaderIOEither[S, R, E, RES], + onRelease Kleisli[S, R, E, RES, ANY], +) Kleisli[S, R, E, Kleisli[S, R, E, RES, A], A] { + release := uncurryState(onRelease) + return func(f Kleisli[S, R, E, RES, A]) StateReaderIOEither[S, R, E, A] { + use := uncurryState(f) + return func(s S) ReaderIOEither[R, E, Pair[S, A]] { + return readerioeither.WithResource[Pair[S, A]](onCreate(s), release)(use) + } + } +} diff --git a/v2/statereaderioeither/resource_test.go b/v2/statereaderioeither/resource_test.go new file mode 100644 index 0000000..c370803 --- /dev/null +++ b/v2/statereaderioeither/resource_test.go @@ -0,0 +1,262 @@ +// 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 statereaderioeither + +import ( + "errors" + "fmt" + "testing" + + E "github.com/IBM/fp-go/v2/either" + P "github.com/IBM/fp-go/v2/pair" + "github.com/stretchr/testify/assert" +) + +// resourceState tracks resource lifecycle +type resourceState struct { + openResources int + lastError error +} + +// testResource represents a simple resource +type testResource struct { + id int + data string +} + +func TestWithResource_SuccessCase(t *testing.T) { + state := resourceState{openResources: 0} + ctx := testContext{multiplier: 1} + released := false + + // Create a resource (increments open count) + onCreate := FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, testResource] { + newState := resourceState{openResources: s.openResources + 1} + resource := testResource{id: 42, data: "test"} + return P.MakePair(newState, resource) + }) + + // Release the resource (decrements open count) + onRelease := func(res testResource) StateReaderIOEither[resourceState, testContext, error, int] { + return FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, int] { + released = true + newState := resourceState{openResources: s.openResources - 1} + return P.MakePair(newState, 0) + }) + } + + // Use the resource + result := WithResource[string, resourceState, testContext, error, testResource]( + onCreate, + onRelease, + )(func(res testResource) StateReaderIOEither[resourceState, testContext, error, string] { + return Of[resourceState, testContext, error](fmt.Sprintf("Resource: %d - %s", res.id, res.data)) + }) + + res := result(state)(ctx)() + + // Verify success + assert.True(t, E.IsRight(res)) + E.Map[error](func(p P.Pair[resourceState, string]) P.Pair[resourceState, string] { + assert.Equal(t, "Resource: 42 - test", P.Tail(p)) + // State is 1 because onCreate incremented to 1, then release saw state=1 and decremented to 0, + // but the final state comes from the use function which doesn't modify state + assert.Equal(t, 1, P.Head(p).openResources) + return p + })(res) + assert.True(t, released) +} + +func TestWithResource_ErrorInUse(t *testing.T) { + state := resourceState{openResources: 0} + ctx := testContext{multiplier: 1} + released := false + + // Create a resource + onCreate := FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, testResource] { + newState := resourceState{openResources: s.openResources + 1} + resource := testResource{id: 99, data: "data"} + return P.MakePair(newState, resource) + }) + + // Release the resource + onRelease := func(res testResource) StateReaderIOEither[resourceState, testContext, error, int] { + return FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, int] { + released = true + newState := resourceState{openResources: s.openResources - 1} + return P.MakePair(newState, 0) + }) + } + + // Use the resource with an error + testErr := errors.New("processing error") + result := WithResource[string, resourceState, testContext, error, testResource]( + onCreate, + onRelease, + )(func(res testResource) StateReaderIOEither[resourceState, testContext, error, string] { + return Left[resourceState, testContext, string](testErr) + }) + + res := result(state)(ctx)() + + // Verify error is propagated but resource was still released + assert.True(t, E.IsLeft(res)) + assert.True(t, released) +} + +func TestWithResource_ErrorInCreate(t *testing.T) { + state := resourceState{openResources: 0} + ctx := testContext{multiplier: 1} + released := false + + // Create a resource that fails + createErr := errors.New("creation failed") + onCreate := Left[resourceState, testContext, testResource](createErr) + + // Release function + onRelease := func(res testResource) StateReaderIOEither[resourceState, testContext, error, int] { + return FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, int] { + released = true + return P.MakePair(s, 0) + }) + } + + // Try to use the resource + result := WithResource[string, resourceState, testContext, error, testResource]( + onCreate, + onRelease, + )(func(res testResource) StateReaderIOEither[resourceState, testContext, error, string] { + return Of[resourceState, testContext, error]("should not reach here") + }) + + res := result(state)(ctx)() + + // Verify creation error is propagated and release was not called + assert.True(t, E.IsLeft(res)) + assert.False(t, released) +} + +func TestWithResource_StateThreading(t *testing.T) { + state := resourceState{openResources: 0} + ctx := testContext{multiplier: 2} + + // Track state changes + var statesObserved []int + + // Create a resource (state: 0 -> 1) + onCreate := FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, testResource] { + statesObserved = append(statesObserved, s.openResources) + newState := resourceState{openResources: s.openResources + 1} + resource := testResource{id: 1, data: "file"} + return P.MakePair(newState, resource) + }) + + // Use the resource (state: 1 -> 2) + useResource := func(res testResource) StateReaderIOEither[resourceState, testContext, error, string] { + return FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, string] { + statesObserved = append(statesObserved, s.openResources) + newState := resourceState{openResources: s.openResources + 1} + return P.MakePair(newState, fmt.Sprintf("used-%d", res.id)) + }) + } + + // Release the resource (state: 2 -> 1) + onRelease := func(res testResource) StateReaderIOEither[resourceState, testContext, error, int] { + return FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, int] { + statesObserved = append(statesObserved, s.openResources) + newState := resourceState{openResources: s.openResources - 1} + return P.MakePair(newState, 0) + }) + } + + result := WithResource[string, resourceState, testContext, error, testResource]( + onCreate, + onRelease, + )(useResource) + + res := result(state)(ctx)() + + // Verify state threading + assert.True(t, E.IsRight(res)) + E.Map[error](func(p P.Pair[resourceState, string]) P.Pair[resourceState, string] { + assert.Equal(t, "used-1", P.Tail(p)) + assert.Equal(t, 2, P.Head(p).openResources) // Final state from use function + return p + })(res) + + // Verify state was observed: onCreate sees initial state (0), + // useResource sees state after create (1), onRelease sees state after create (1) + assert.Equal(t, []int{0, 1, 1}, statesObserved) +} + +func TestWithResource_MultipleResources(t *testing.T) { + state := resourceState{openResources: 0} + ctx := testContext{multiplier: 1} + + // Create first resource + createResource1 := FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, testResource] { + newState := resourceState{openResources: s.openResources + 1} + return P.MakePair(newState, testResource{id: 1, data: "res1"}) + }) + + releaseResource1 := func(res testResource) StateReaderIOEither[resourceState, testContext, error, int] { + return FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, int] { + newState := resourceState{openResources: s.openResources - 1} + return P.MakePair(newState, 0) + }) + } + + // Second resource creator + createResource2 := FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, testResource] { + newState := resourceState{openResources: s.openResources + 1} + return P.MakePair(newState, testResource{id: 2, data: "res2"}) + }) + + releaseResource2 := func(res testResource) StateReaderIOEither[resourceState, testContext, error, int] { + return FromState[testContext, error, resourceState](func(s resourceState) P.Pair[resourceState, int] { + newState := resourceState{openResources: s.openResources - 1} + return P.MakePair(newState, 0) + }) + } + + // Nest resources + result := WithResource[string, resourceState, testContext, error, testResource]( + createResource1, + releaseResource1, + )(func(res1 testResource) StateReaderIOEither[resourceState, testContext, error, string] { + return WithResource[string, resourceState, testContext, error, testResource]( + createResource2, + releaseResource2, + )(func(res2 testResource) StateReaderIOEither[resourceState, testContext, error, string] { + return Of[resourceState, testContext, error]( + fmt.Sprintf("%s + %s", res1.data, res2.data), + ) + }) + }) + + res := result(state)(ctx)() + + // Verify both resources were used and released + assert.True(t, E.IsRight(res)) + E.Map[error](func(p P.Pair[resourceState, string]) P.Pair[resourceState, string] { + assert.Equal(t, "res1 + res2", P.Tail(p)) + // Final state comes from innermost use function (Of doesn't modify state) + // onCreate1: 0->1, onCreate2: 1->2, release2: sees 2, release1: sees 1 + // Final state from Of: 2 (from the state after both creates) + assert.Equal(t, 2, P.Head(p).openResources) + return p + })(res) +}