mirror of
https://github.com/IBM/fp-go.git
synced 2025-11-23 22:14:53 +02:00
fix: add statereaderioresult
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
This commit is contained in:
209
v2/context/statereaderioresult/bind.go
Normal file
209
v2/context/statereaderioresult/bind.go
Normal file
@@ -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)
|
||||
}
|
||||
147
v2/context/statereaderioresult/doc.go
Normal file
147
v2/context/statereaderioresult/doc.go
Normal file
@@ -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
|
||||
41
v2/context/statereaderioresult/eq.go
Normal file
41
v2/context/statereaderioresult/eq.go
Normal file
@@ -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],
|
||||
)
|
||||
}
|
||||
103
v2/context/statereaderioresult/monad.go
Normal file
103
v2/context/statereaderioresult/monad.go
Normal file
@@ -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]{}
|
||||
}
|
||||
101
v2/context/statereaderioresult/resource.go
Normal file
101
v2/context/statereaderioresult/resource.go
Normal file
@@ -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)
|
||||
}
|
||||
415
v2/context/statereaderioresult/resource_test.go
Normal file
415
v2/context/statereaderioresult/resource_test.go
Normal file
@@ -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)
|
||||
}
|
||||
309
v2/context/statereaderioresult/state.go
Normal file
309
v2/context/statereaderioresult/state.go
Normal file
@@ -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))
|
||||
}
|
||||
567
v2/context/statereaderioresult/statereaderioresult_test.go
Normal file
567
v2/context/statereaderioresult/statereaderioresult_test.go
Normal file
@@ -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)
|
||||
}
|
||||
87
v2/context/statereaderioresult/testing/laws.go
Normal file
87
v2/context/statereaderioresult/testing/laws.go
Normal file
@@ -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,
|
||||
)
|
||||
|
||||
}
|
||||
50
v2/context/statereaderioresult/testing/laws_test.go
Normal file
50
v2/context/statereaderioresult/testing/laws_test.go
Normal file
@@ -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))
|
||||
}
|
||||
84
v2/context/statereaderioresult/type.go
Normal file
84
v2/context/statereaderioresult/type.go
Normal file
@@ -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]]
|
||||
)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
80
v2/statereaderioeither/resource.go
Normal file
80
v2/statereaderioeither/resource.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
262
v2/statereaderioeither/resource_test.go
Normal file
262
v2/statereaderioeither/resource_test.go
Normal file
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user