1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-02-26 13:06:09 +02:00

Compare commits

...

8 Commits

Author SHA1 Message Date
Dr. Carsten Leue
cc0c14c7cf fix: do not fail if coverage fails
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 11:35:16 +01:00
Dr. Carsten Leue
19159ad49e fix: WriteFile
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-25 10:42:46 +01:00
Dr. Carsten Leue
b9c8fb4ff1 fix: parameter order for Local and TapIOK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 17:55:32 +01:00
Dr. Carsten Leue
4529e720bc fix: add ChainThunkK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:59:51 +01:00
Dr. Carsten Leue
ef8b2ea65d fix: add ChainReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 15:31:51 +01:00
Dr. Carsten Leue
5bd7caafdd fix: better doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-24 13:42:29 +01:00
Dr. Carsten Leue
47ebcd79b1 fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:15:00 +01:00
Dr. Carsten Leue
dbad94806e fix: add LocalIOResultK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-02-23 14:14:21 +01:00
37 changed files with 7064 additions and 542 deletions

View File

@@ -42,6 +42,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -81,6 +82,7 @@ jobs:
go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
- name: Upload coverage to Coveralls
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -106,6 +108,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Coveralls Finished
continue-on-error: true
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

273
v2/cli/README.md Normal file
View File

@@ -0,0 +1,273 @@
# CLI Package - Functional Wrappers for urfave/cli/v3
This package provides functional programming wrappers for the `github.com/urfave/cli/v3` library, enabling Effect-based command actions and type-safe flag handling through Prisms.
## Features
### 1. Effect-Based Command Actions
Transform CLI command actions into composable Effects that follow functional programming principles.
#### Key Functions
- **`ToAction(effect CommandEffect) func(context.Context, *C.Command) error`**
- Converts a CommandEffect into a standard urfave/cli Action function
- Enables Effect-based command handlers to work with cli/v3 framework
- **`FromAction(action func(context.Context, *C.Command) error) CommandEffect`**
- Lifts existing cli/v3 action handlers into the Effect type
- Allows gradual migration to functional style
- **`MakeCommand(name, usage string, flags []C.Flag, effect CommandEffect) *C.Command`**
- Creates a new Command with an Effect-based action
- Convenience function combining command creation with Effect conversion
- **`MakeCommandWithSubcommands(...) *C.Command`**
- Creates a Command with subcommands and an Effect-based action
#### Example Usage
```go
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Define an Effect-based command action
processEffect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process input...
return R.Of(F.Void{})
}
}
}
// Create command with Effect
command := cli.MakeCommand(
"process",
"Process input files",
[]C.Flag{
&C.StringFlag{Name: "input", Usage: "Input file path"},
},
processEffect,
)
// Or convert existing action to Effect
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Existing logic...
return nil
}
effect := cli.FromAction(existingAction)
```
### 2. Flag Type Prisms
Type-safe extraction and manipulation of CLI flags using Prisms from the optics package.
#### Available Prisms
- `StringFlagPrism()` - Extract `*C.StringFlag` from `C.Flag`
- `IntFlagPrism()` - Extract `*C.IntFlag` from `C.Flag`
- `BoolFlagPrism()` - Extract `*C.BoolFlag` from `C.Flag`
- `Float64FlagPrism()` - Extract `*C.Float64Flag` from `C.Flag`
- `DurationFlagPrism()` - Extract `*C.DurationFlag` from `C.Flag`
- `TimestampFlagPrism()` - Extract `*C.TimestampFlag` from `C.Flag`
- `StringSliceFlagPrism()` - Extract `*C.StringSliceFlag` from `C.Flag`
- `IntSliceFlagPrism()` - Extract `*C.IntSliceFlag` from `C.Flag`
- `Float64SliceFlagPrism()` - Extract `*C.Float64SliceFlag` from `C.Flag`
- `UintFlagPrism()` - Extract `*C.UintFlag` from `C.Flag`
- `Uint64FlagPrism()` - Extract `*C.Uint64Flag` from `C.Flag`
- `Int64FlagPrism()` - Extract `*C.Int64Flag` from `C.Flag`
#### Example Usage
```go
import (
O "github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/cli"
C "github.com/urfave/cli/v3"
)
// Extract a StringFlag from a Flag interface
var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
prism := cli.StringFlagPrism()
// Safe extraction returns Option
result := prism.GetOption(flag)
if O.IsSome(result) {
strFlag := O.MonadFold(result,
func() *C.StringFlag { return nil },
func(f *C.StringFlag) *C.StringFlag { return f },
)
// Use strFlag...
}
// Type mismatch returns None
var intFlag C.Flag = &C.IntFlag{Name: "count"}
result = prism.GetOption(intFlag) // Returns None
// Convert back to Flag
strFlag := &C.StringFlag{Name: "output"}
flag = prism.ReverseGet(strFlag)
```
## Type Definitions
### CommandEffect
```go
type CommandEffect = E.Effect[*C.Command, F.Void]
```
A CommandEffect represents a CLI command action as an Effect. It takes a `*C.Command` as context and produces a result wrapped in the Effect monad.
The Effect structure is:
```
func(*C.Command) -> func(context.Context) -> func() -> Result[Void]
```
This allows for:
- **Composability**: Effects can be composed using standard functional combinators
- **Testability**: Pure functions are easier to test
- **Error Handling**: Errors are explicitly represented in the Result type
- **Context Management**: Context flows naturally through the Effect
## Benefits
### 1. Functional Composition
Effects can be composed using standard functional programming patterns:
```go
import (
F "github.com/IBM/fp-go/v2/function"
RRIOE "github.com/IBM/fp-go/v2/context/readerreaderioresult"
)
// Compose multiple effects
validateInput := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
processData := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
saveResults := func(cmd *C.Command) E.Thunk[F.Void] { /* ... */ }
// Chain effects together
pipeline := F.Pipe3(
validateInput,
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return processData }),
RRIOE.Chain(func(F.Void) E.Effect[*C.Command, F.Void] { return saveResults }),
)
```
### 2. Type Safety
Prisms provide compile-time type safety when working with flags:
```go
// Type-safe flag extraction
flags := []C.Flag{
&C.StringFlag{Name: "input"},
&C.IntFlag{Name: "count"},
}
for _, flag := range flags {
// Safe extraction with pattern matching
O.MonadFold(
cli.StringFlagPrism().GetOption(flag),
func() { /* Not a string flag */ },
func(sf *C.StringFlag) { /* Handle string flag */ },
)
}
```
### 3. Error Handling
Errors are explicitly represented in the Result type:
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
if err := validateInput(cmd); err != nil {
return R.Left[F.Void](err) // Explicit error
}
return R.Of(F.Void{}) // Success
}
}
}
```
### 4. Testability
Pure functions are easier to test:
```go
func TestCommandEffect(t *testing.T) {
cmd := &C.Command{Name: "test"}
effect := myCommandEffect(cmd)
// Execute effect
result := effect(context.Background())()
// Assert on result
assert.True(t, R.IsRight(result))
}
```
## Migration Guide
### From Standard Actions to Effects
**Before:**
```go
command := &C.Command{
Name: "process",
Action: func(ctx context.Context, cmd *C.Command) error {
input := cmd.String("input")
// Process...
return nil
},
}
```
**After:**
```go
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
input := cmd.String("input")
// Process...
return R.Of(F.Void{})
}
}
}
command := cli.MakeCommand("process", "Process files", flags, effect)
```
### Gradual Migration
You can mix both styles during migration:
```go
// Wrap existing action
existingAction := func(ctx context.Context, cmd *C.Command) error {
// Legacy code...
return nil
}
// Use as Effect
effect := cli.FromAction(existingAction)
command := cli.MakeCommand("legacy", "Legacy command", flags, effect)
```
## See Also
- [Effect Package](../effect/) - Core Effect type definitions
- [Optics Package](../optics/) - Prism and other optics
- [urfave/cli/v3](https://github.com/urfave/cli) - Underlying CLI framework

199
v2/cli/effect.go Normal file
View File

@@ -0,0 +1,199 @@
// 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 cli
import (
"context"
E "github.com/IBM/fp-go/v2/effect"
ET "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
R "github.com/IBM/fp-go/v2/result"
C "github.com/urfave/cli/v3"
)
// CommandEffect represents a CLI command action as an Effect.
// The Effect takes a *C.Command as context and produces a result.
type CommandEffect = E.Effect[*C.Command, F.Void]
// ToAction converts a CommandEffect into a standard urfave/cli Action function.
// This allows Effect-based command handlers to be used with the cli/v3 framework.
//
// The conversion process:
// 1. Takes the Effect which expects a *C.Command context
// 2. Executes it with the provided command
// 3. Runs the resulting IO operation
// 4. Converts the Result to either nil (success) or error (failure)
//
// # Parameters
//
// - effect: The CommandEffect to convert
//
// # Returns
//
// - A function compatible with C.Command.Action signature
//
// # Example Usage
//
// effect := func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// // Command logic here
// return R.Of(F.Void{})
// }
// }
// }
// action := ToAction(effect)
// command := &C.Command{
// Name: "example",
// Action: action,
// }
func ToAction(effect CommandEffect) func(context.Context, *C.Command) error {
return func(ctx context.Context, cmd *C.Command) error {
// Execute the effect: cmd -> ctx -> IO -> Result
return F.Pipe3(
ctx,
effect(cmd),
io.Run,
// Convert Result[Void] to error
ET.Fold(F.Identity[error], F.Constant1[F.Void, error](nil)),
)
}
}
// FromAction converts a standard urfave/cli Action function into a CommandEffect.
// This allows existing cli/v3 action handlers to be lifted into the Effect type.
//
// The conversion process:
// 1. Takes a standard action function (context.Context, *C.Command) -> error
// 2. Wraps it in the Effect structure
// 3. Converts the error result to a Result type
//
// # Parameters
//
// - action: The standard cli/v3 action function to convert
//
// # Returns
//
// - A CommandEffect that wraps the original action
//
// # Example Usage
//
// standardAction := func(ctx context.Context, cmd *C.Command) error {
// // Existing command logic
// return nil
// }
// effect := FromAction(standardAction)
// // Now can be composed with other Effects
func FromAction(action func(context.Context, *C.Command) error) CommandEffect {
return func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
err := action(ctx, cmd)
if err != nil {
return R.Left[F.Void](err)
}
return R.Of(F.Void{})
}
}
}
}
// MakeCommand creates a new Command with an Effect-based action.
// This is a convenience function that combines command creation with Effect conversion.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommand(
// "process",
// "Process data files",
// []C.Flag{
// &C.StringFlag{Name: "input", Usage: "Input file"},
// },
// func(cmd *C.Command) E.Thunk[F.Void] {
// return func(ctx context.Context) E.IOResult[F.Void] {
// return func() R.Result[F.Void] {
// input := cmd.String("input")
// // Process input...
// return R.Of(F.Void{})
// }
// }
// },
// )
func MakeCommand(
name string,
usage string,
flags []C.Flag,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Action: ToAction(effect),
}
}
// MakeCommandWithSubcommands creates a new Command with subcommands and an Effect-based action.
//
// # Parameters
//
// - name: The command name
// - usage: The command usage description
// - flags: The command flags
// - commands: The subcommands
// - effect: The CommandEffect to use as the action
//
// # Returns
//
// - A *C.Command configured with subcommands and the Effect-based action
//
// # Example Usage
//
// cmd := MakeCommandWithSubcommands(
// "app",
// "Application commands",
// []C.Flag{},
// []*C.Command{subCmd1, subCmd2},
// defaultEffect,
// )
func MakeCommandWithSubcommands(
name string,
usage string,
flags []C.Flag,
commands []*C.Command,
effect CommandEffect,
) *C.Command {
return &C.Command{
Name: name,
Usage: usage,
Flags: flags,
Commands: commands,
Action: ToAction(effect),
}
}

204
v2/cli/effect_test.go Normal file
View File

@@ -0,0 +1,204 @@
// 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 cli
import (
"context"
"errors"
"testing"
E "github.com/IBM/fp-go/v2/effect"
F "github.com/IBM/fp-go/v2/function"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestToAction_Success(t *testing.T) {
t.Run("converts successful Effect to action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
})
}
func TestToAction_Failure(t *testing.T) {
t.Run("converts failed Effect to error", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Left[F.Void](expectedErr)
}
}
}
action := ToAction(effect)
cmd := &C.Command{Name: "test"}
// Act
err := action(context.Background(), cmd)
// Assert
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
}
func TestFromAction_Success(t *testing.T) {
t.Run("converts successful action to Effect", func(t *testing.T) {
// Arrange
action := func(ctx context.Context, cmd *C.Command) error {
return nil
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsRight(result))
})
}
func TestFromAction_Failure(t *testing.T) {
t.Run("converts failed action to Effect", func(t *testing.T) {
// Arrange
expectedErr := errors.New("test error")
action := func(ctx context.Context, cmd *C.Command) error {
return expectedErr
}
effect := FromAction(action)
cmd := &C.Command{Name: "test"}
// Act
result := effect(cmd)(context.Background())()
// Assert
assert.True(t, R.IsLeft(result))
err := R.MonadFold(result, F.Identity[error], func(F.Void) error { return nil })
assert.Equal(t, expectedErr, err)
})
}
func TestMakeCommand(t *testing.T) {
t.Run("creates command with Effect-based action", func(t *testing.T) {
// Arrange
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommand(
"test",
"Test command",
[]C.Flag{},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "test", cmd.Name)
assert.Equal(t, "Test command", cmd.Usage)
assert.NotNil(t, cmd.Action)
// Test the action
err := cmd.Action(context.Background(), cmd)
assert.NoError(t, err)
})
}
func TestMakeCommandWithSubcommands(t *testing.T) {
t.Run("creates command with subcommands and Effect-based action", func(t *testing.T) {
// Arrange
subCmd := &C.Command{Name: "sub"}
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
return R.Of(F.Void{})
}
}
}
// Act
cmd := MakeCommandWithSubcommands(
"parent",
"Parent command",
[]C.Flag{},
[]*C.Command{subCmd},
effect,
)
// Assert
assert.NotNil(t, cmd)
assert.Equal(t, "parent", cmd.Name)
assert.Equal(t, "Parent command", cmd.Usage)
assert.Len(t, cmd.Commands, 1)
assert.Equal(t, "sub", cmd.Commands[0].Name)
assert.NotNil(t, cmd.Action)
})
}
func TestToAction_Integration(t *testing.T) {
t.Run("Effect can access command flags", func(t *testing.T) {
// Arrange
var capturedValue string
effect := func(cmd *C.Command) E.Thunk[F.Void] {
return func(ctx context.Context) E.IOResult[F.Void] {
return func() R.Result[F.Void] {
capturedValue = cmd.String("input")
return R.Of(F.Void{})
}
}
}
cmd := &C.Command{
Name: "test",
Flags: []C.Flag{
&C.StringFlag{
Name: "input",
Value: "default-value",
},
},
Action: ToAction(effect),
}
// Act
err := cmd.Action(context.Background(), cmd)
// Assert
assert.NoError(t, err)
assert.Equal(t, "default-value", capturedValue)
})
}

359
v2/cli/flags.go Normal file
View File

@@ -0,0 +1,359 @@
// 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 cli
import (
P "github.com/IBM/fp-go/v2/optics/prism"
O "github.com/IBM/fp-go/v2/option"
C "github.com/urfave/cli/v3"
)
// StringFlagPrism creates a Prism for extracting a StringFlag from a Flag.
// This provides a type-safe way to work with string flags, handling type
// mismatches gracefully through the Option type.
//
// The prism's GetOption attempts to cast a Flag to *C.StringFlag.
// If the cast succeeds, it returns Some(*C.StringFlag); if it fails, it returns None.
//
// The prism's ReverseGet converts a *C.StringFlag back to a Flag.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringFlag] for safe StringFlag extraction
//
// # Example Usage
//
// prism := StringFlagPrism()
//
// // Extract StringFlag from Flag
// var flag C.Flag = &C.StringFlag{Name: "input", Value: "default"}
// result := prism.GetOption(flag) // Some(*C.StringFlag{...})
//
// // Type mismatch returns None
// var intFlag C.Flag = &C.IntFlag{Name: "count"}
// result = prism.GetOption(intFlag) // None[*C.StringFlag]()
//
// // Convert back to Flag
// strFlag := &C.StringFlag{Name: "output"}
// flag = prism.ReverseGet(strFlag)
func StringFlagPrism() P.Prism[C.Flag, *C.StringFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringFlag] {
if sf, ok := flag.(*C.StringFlag); ok {
return O.Some(sf)
}
return O.None[*C.StringFlag]()
},
func(f *C.StringFlag) C.Flag { return f },
)
}
// IntFlagPrism creates a Prism for extracting an IntFlag from a Flag.
// This provides a type-safe way to work with integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntFlag] for safe IntFlag extraction
//
// # Example Usage
//
// prism := IntFlagPrism()
//
// // Extract IntFlag from Flag
// var flag C.Flag = &C.IntFlag{Name: "count", Value: 10}
// result := prism.GetOption(flag) // Some(*C.IntFlag{...})
func IntFlagPrism() P.Prism[C.Flag, *C.IntFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntFlag] {
if f, ok := flag.(*C.IntFlag); ok {
return O.Some(f)
}
return O.None[*C.IntFlag]()
},
func(f *C.IntFlag) C.Flag { return f },
)
}
// BoolFlagPrism creates a Prism for extracting a BoolFlag from a Flag.
// This provides a type-safe way to work with boolean flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.BoolFlag] for safe BoolFlag extraction
//
// # Example Usage
//
// prism := BoolFlagPrism()
//
// // Extract BoolFlag from Flag
// var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// result := prism.GetOption(flag) // Some(*C.BoolFlag{...})
func BoolFlagPrism() P.Prism[C.Flag, *C.BoolFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.BoolFlag] {
if f, ok := flag.(*C.BoolFlag); ok {
return O.Some(f)
}
return O.None[*C.BoolFlag]()
},
func(f *C.BoolFlag) C.Flag { return f },
)
}
// Float64FlagPrism creates a Prism for extracting a Float64Flag from a Flag.
// This provides a type-safe way to work with float64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64Flag] for safe Float64Flag extraction
//
// # Example Usage
//
// prism := Float64FlagPrism()
//
// // Extract Float64Flag from Flag
// var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// result := prism.GetOption(flag) // Some(*C.Float64Flag{...})
func Float64FlagPrism() P.Prism[C.Flag, *C.Float64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64Flag] {
if f, ok := flag.(*C.Float64Flag); ok {
return O.Some(f)
}
return O.None[*C.Float64Flag]()
},
func(f *C.Float64Flag) C.Flag { return f },
)
}
// DurationFlagPrism creates a Prism for extracting a DurationFlag from a Flag.
// This provides a type-safe way to work with duration flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.DurationFlag] for safe DurationFlag extraction
//
// # Example Usage
//
// prism := DurationFlagPrism()
//
// // Extract DurationFlag from Flag
// var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: 30 * time.Second}
// result := prism.GetOption(flag) // Some(*C.DurationFlag{...})
func DurationFlagPrism() P.Prism[C.Flag, *C.DurationFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.DurationFlag] {
if f, ok := flag.(*C.DurationFlag); ok {
return O.Some(f)
}
return O.None[*C.DurationFlag]()
},
func(f *C.DurationFlag) C.Flag { return f },
)
}
// TimestampFlagPrism creates a Prism for extracting a TimestampFlag from a Flag.
// This provides a type-safe way to work with timestamp flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.TimestampFlag] for safe TimestampFlag extraction
//
// # Example Usage
//
// prism := TimestampFlagPrism()
//
// // Extract TimestampFlag from Flag
// var flag C.Flag = &C.TimestampFlag{Name: "created"}
// result := prism.GetOption(flag) // Some(*C.TimestampFlag{...})
func TimestampFlagPrism() P.Prism[C.Flag, *C.TimestampFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.TimestampFlag] {
if f, ok := flag.(*C.TimestampFlag); ok {
return O.Some(f)
}
return O.None[*C.TimestampFlag]()
},
func(f *C.TimestampFlag) C.Flag { return f },
)
}
// StringSliceFlagPrism creates a Prism for extracting a StringSliceFlag from a Flag.
// This provides a type-safe way to work with string slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.StringSliceFlag] for safe StringSliceFlag extraction
//
// # Example Usage
//
// prism := StringSliceFlagPrism()
//
// // Extract StringSliceFlag from Flag
// var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// result := prism.GetOption(flag) // Some(*C.StringSliceFlag{...})
func StringSliceFlagPrism() P.Prism[C.Flag, *C.StringSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.StringSliceFlag] {
if f, ok := flag.(*C.StringSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.StringSliceFlag]()
},
func(f *C.StringSliceFlag) C.Flag { return f },
)
}
// IntSliceFlagPrism creates a Prism for extracting an IntSliceFlag from a Flag.
// This provides a type-safe way to work with int slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.IntSliceFlag] for safe IntSliceFlag extraction
//
// # Example Usage
//
// prism := IntSliceFlagPrism()
//
// // Extract IntSliceFlag from Flag
// var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// result := prism.GetOption(flag) // Some(*C.IntSliceFlag{...})
func IntSliceFlagPrism() P.Prism[C.Flag, *C.IntSliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.IntSliceFlag] {
if f, ok := flag.(*C.IntSliceFlag); ok {
return O.Some(f)
}
return O.None[*C.IntSliceFlag]()
},
func(f *C.IntSliceFlag) C.Flag { return f },
)
}
// Float64SliceFlagPrism creates a Prism for extracting a Float64SliceFlag from a Flag.
// This provides a type-safe way to work with float64 slice flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Float64SliceFlag] for safe Float64SliceFlag extraction
//
// # Example Usage
//
// prism := Float64SliceFlagPrism()
//
// // Extract Float64SliceFlag from Flag
// var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// result := prism.GetOption(flag) // Some(*C.Float64SliceFlag{...})
func Float64SliceFlagPrism() P.Prism[C.Flag, *C.Float64SliceFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Float64SliceFlag] {
if f, ok := flag.(*C.Float64SliceFlag); ok {
return O.Some(f)
}
return O.None[*C.Float64SliceFlag]()
},
func(f *C.Float64SliceFlag) C.Flag { return f },
)
}
// UintFlagPrism creates a Prism for extracting a UintFlag from a Flag.
// This provides a type-safe way to work with unsigned integer flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.UintFlag] for safe UintFlag extraction
//
// # Example Usage
//
// prism := UintFlagPrism()
//
// // Extract UintFlag from Flag
// var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// result := prism.GetOption(flag) // Some(*C.UintFlag{...})
func UintFlagPrism() P.Prism[C.Flag, *C.UintFlag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.UintFlag] {
if f, ok := flag.(*C.UintFlag); ok {
return O.Some(f)
}
return O.None[*C.UintFlag]()
},
func(f *C.UintFlag) C.Flag { return f },
)
}
// Uint64FlagPrism creates a Prism for extracting a Uint64Flag from a Flag.
// This provides a type-safe way to work with uint64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Uint64Flag] for safe Uint64Flag extraction
//
// # Example Usage
//
// prism := Uint64FlagPrism()
//
// // Extract Uint64Flag from Flag
// var flag C.Flag = &C.Uint64Flag{Name: "size"}
// result := prism.GetOption(flag) // Some(*C.Uint64Flag{...})
func Uint64FlagPrism() P.Prism[C.Flag, *C.Uint64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Uint64Flag] {
if f, ok := flag.(*C.Uint64Flag); ok {
return O.Some(f)
}
return O.None[*C.Uint64Flag]()
},
func(f *C.Uint64Flag) C.Flag { return f },
)
}
// Int64FlagPrism creates a Prism for extracting an Int64Flag from a Flag.
// This provides a type-safe way to work with int64 flags, handling type
// mismatches gracefully through the Option type.
//
// # Returns
//
// - A Prism[C.Flag, *C.Int64Flag] for safe Int64Flag extraction
//
// # Example Usage
//
// prism := Int64FlagPrism()
//
// // Extract Int64Flag from Flag
// var flag C.Flag = &C.Int64Flag{Name: "offset"}
// result := prism.GetOption(flag) // Some(*C.Int64Flag{...})
func Int64FlagPrism() P.Prism[C.Flag, *C.Int64Flag] {
return P.MakePrism(
func(flag C.Flag) O.Option[*C.Int64Flag] {
if f, ok := flag.(*C.Int64Flag); ok {
return O.Some(f)
}
return O.None[*C.Int64Flag]()
},
func(f *C.Int64Flag) C.Flag { return f },
)
}

287
v2/cli/flags_test.go Normal file
View File

@@ -0,0 +1,287 @@
// 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 cli
import (
"testing"
"time"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
C "github.com/urfave/cli/v3"
)
func TestStringFlagPrism_Success(t *testing.T) {
t.Run("extracts StringFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.StringFlag{Name: "input", Value: "test"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringFlag { return nil }, func(f *C.StringFlag) *C.StringFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "input", extracted.Name)
assert.Equal(t, "test", extracted.Value)
})
}
func TestStringFlagPrism_Failure(t *testing.T) {
t.Run("returns None for non-StringFlag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsNone(result))
})
}
func TestStringFlagPrism_ReverseGet(t *testing.T) {
t.Run("converts StringFlag back to Flag", func(t *testing.T) {
// Arrange
prism := StringFlagPrism()
strFlag := &C.StringFlag{Name: "output", Value: "result"}
// Act
flag := prism.ReverseGet(strFlag)
// Assert
assert.NotNil(t, flag)
assert.IsType(t, &C.StringFlag{}, flag)
})
}
func TestIntFlagPrism_Success(t *testing.T) {
t.Run("extracts IntFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntFlagPrism()
var flag C.Flag = &C.IntFlag{Name: "count", Value: 42}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntFlag { return nil }, func(f *C.IntFlag) *C.IntFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "count", extracted.Name)
assert.Equal(t, 42, extracted.Value)
})
}
func TestBoolFlagPrism_Success(t *testing.T) {
t.Run("extracts BoolFlag from Flag", func(t *testing.T) {
// Arrange
prism := BoolFlagPrism()
var flag C.Flag = &C.BoolFlag{Name: "verbose", Value: true}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.BoolFlag { return nil }, func(f *C.BoolFlag) *C.BoolFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "verbose", extracted.Name)
assert.Equal(t, true, extracted.Value)
})
}
func TestFloat64FlagPrism_Success(t *testing.T) {
t.Run("extracts Float64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Float64FlagPrism()
var flag C.Flag = &C.Float64Flag{Name: "ratio", Value: 0.5}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64Flag { return nil }, func(f *C.Float64Flag) *C.Float64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratio", extracted.Name)
assert.Equal(t, 0.5, extracted.Value)
})
}
func TestDurationFlagPrism_Success(t *testing.T) {
t.Run("extracts DurationFlag from Flag", func(t *testing.T) {
// Arrange
prism := DurationFlagPrism()
duration := 30 * time.Second
var flag C.Flag = &C.DurationFlag{Name: "timeout", Value: duration}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.DurationFlag { return nil }, func(f *C.DurationFlag) *C.DurationFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "timeout", extracted.Name)
assert.Equal(t, duration, extracted.Value)
})
}
func TestTimestampFlagPrism_Success(t *testing.T) {
t.Run("extracts TimestampFlag from Flag", func(t *testing.T) {
// Arrange
prism := TimestampFlagPrism()
var flag C.Flag = &C.TimestampFlag{Name: "created"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.TimestampFlag { return nil }, func(f *C.TimestampFlag) *C.TimestampFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "created", extracted.Name)
})
}
func TestStringSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts StringSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := StringSliceFlagPrism()
var flag C.Flag = &C.StringSliceFlag{Name: "tags"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.StringSliceFlag { return nil }, func(f *C.StringSliceFlag) *C.StringSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "tags", extracted.Name)
})
}
func TestIntSliceFlagPrism_Success(t *testing.T) {
t.Run("extracts IntSliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := IntSliceFlagPrism()
var flag C.Flag = &C.IntSliceFlag{Name: "ports"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.IntSliceFlag { return nil }, func(f *C.IntSliceFlag) *C.IntSliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ports", extracted.Name)
})
}
func TestFloat64SliceFlagPrism_Success(t *testing.T) {
t.Run("extracts Float64SliceFlag from Flag", func(t *testing.T) {
// Arrange
prism := Float64SliceFlagPrism()
var flag C.Flag = &C.Float64SliceFlag{Name: "ratios"}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Float64SliceFlag { return nil }, func(f *C.Float64SliceFlag) *C.Float64SliceFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "ratios", extracted.Name)
})
}
func TestUintFlagPrism_Success(t *testing.T) {
t.Run("extracts UintFlag from Flag", func(t *testing.T) {
// Arrange
prism := UintFlagPrism()
var flag C.Flag = &C.UintFlag{Name: "workers", Value: 4}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.UintFlag { return nil }, func(f *C.UintFlag) *C.UintFlag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "workers", extracted.Name)
assert.Equal(t, uint(4), extracted.Value)
})
}
func TestUint64FlagPrism_Success(t *testing.T) {
t.Run("extracts Uint64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Uint64FlagPrism()
var flag C.Flag = &C.Uint64Flag{Name: "size", Value: 1024}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Uint64Flag { return nil }, func(f *C.Uint64Flag) *C.Uint64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "size", extracted.Name)
assert.Equal(t, uint64(1024), extracted.Value)
})
}
func TestInt64FlagPrism_Success(t *testing.T) {
t.Run("extracts Int64Flag from Flag", func(t *testing.T) {
// Arrange
prism := Int64FlagPrism()
var flag C.Flag = &C.Int64Flag{Name: "offset", Value: -100}
// Act
result := prism.GetOption(flag)
// Assert
assert.True(t, O.IsSome(result))
extracted := O.MonadFold(result, func() *C.Int64Flag { return nil }, func(f *C.Int64Flag) *C.Int64Flag { return f })
assert.NotNil(t, extracted)
assert.Equal(t, "offset", extracted.Name)
assert.Equal(t, int64(-100), extracted.Value)
})
}
func TestPrisms_EdgeCases(t *testing.T) {
t.Run("all prisms return None for wrong type", func(t *testing.T) {
// Arrange
var flag C.Flag = &C.StringFlag{Name: "test"}
// Act & Assert
assert.True(t, O.IsNone(IntFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(BoolFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(DurationFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(TimestampFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(StringSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(IntSliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Float64SliceFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(UintFlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Uint64FlagPrism().GetOption(flag)))
assert.True(t, O.IsNone(Int64FlagPrism().GetOption(flag)))
})
}

View File

@@ -21,6 +21,7 @@ import (
CIOE "github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/pair"
)
// WithContext wraps an existing [ReaderIOResult] and performs a context check for cancellation before delegating.
@@ -85,3 +86,7 @@ func WithContextK[A, B any](f Kleisli[A, B]) Kleisli[A, B] {
WithContext,
)
}
func pairFromContextCancel(newCtx context.Context, cancelFct context.CancelFunc) ContextCancel {
return pair.MakePair(cancelFct, newCtx)
}

View File

@@ -13,6 +13,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package file provides context-aware file operations that integrate with the ReaderIOResult monad.
// It offers safe, composable file I/O operations that respect context cancellation and properly
// manage resources using the RAII pattern.
//
// All operations in this package:
// - Respect context.Context for cancellation and timeouts
// - Return ReaderIOResult for composable error handling
// - Automatically manage resource cleanup
// - Are safe to use in concurrent environments
//
// # Example Usage
//
// // Read a file with automatic resource management
// readOp := ReadFile("data.txt")
// result := readOp(ctx)()
//
// // Open and manually manage a file
// fileOp := Open("config.json")
// fileResult := fileOp(ctx)()
package file
import (
@@ -29,32 +48,181 @@ import (
)
var (
// Open opens a file for reading within the given context
// Open opens a file for reading within the given context.
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or ReadFile.
//
// Parameters:
// - path: The path to the file to open
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that opens the file
//
// Example:
//
// openFile := Open("data.txt")
// result := openFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// // Use file...
// },
// )
//
// See Also:
// - ReadFile: For reading entire file contents with automatic resource management
// - Close: For closing file handles
Open = F.Flow3(
IOEF.Open,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name
// Create creates or truncates a file for writing within the given context.
// If the file already exists, it is truncated. If it doesn't exist, it is created
// with mode 0666 (before umask).
//
// The operation respects context cancellation and returns a ReaderIOResult
// that produces an os.File handle on success.
//
// The returned file handle should be closed using the Close function when no longer needed,
// or managed automatically using WithResource or WriteFile.
//
// Parameters:
// - path: The path to the file to create or truncate
//
// Returns:
// - ReaderIOResult[*os.File]: A context-aware computation that creates the file
//
// Example:
//
// createFile := Create("output.txt")
// result := createFile(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Error: %v", err) },
// func(f *os.File) {
// defer f.Close()
// f.WriteString("Hello, World!")
// },
// )
//
// See Also:
// - WriteFile: For writing data to a file with automatic resource management
// - Open: For opening files for reading
// - Close: For closing file handles
Create = F.Flow3(
IOEF.Create,
RIOE.FromIOEither[*os.File],
RIOE.WithContext[*os.File],
)
// Remove removes a file by name.
// The operation returns the filename on success, allowing for easy composition
// with other file operations.
//
// Parameters:
// - name: The path to the file to remove
//
// Returns:
// - ReaderIOResult[string]: A computation that removes the file and returns its name
//
// Example:
//
// removeOp := Remove("temp.txt")
// result := removeOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Failed to remove: %v", err) },
// func(name string) { log.Printf("Removed: %s", name) },
// )
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading file contents
Remove = F.Flow2(
IOEF.Remove,
RIOE.FromIOEither[string],
)
)
// Close closes an object
func Close[C io.Closer](c C) RIOE.ReaderIOResult[struct{}] {
// Close closes an io.Closer resource and returns a ReaderIOResult.
// This function is generic and works with any type that implements io.Closer,
// including os.File, network connections, and other closeable resources.
//
// The function captures any error that occurs during closing and returns it
// as part of the ReaderIOResult. On success, it returns Void (empty struct).
//
// Type Parameters:
// - C: Any type that implements io.Closer
//
// Parameters:
// - c: The resource to close
//
// Returns:
// - ReaderIOResult[Void]: A computation that closes the resource
//
// Example:
//
// file, _ := os.Open("data.txt")
// closeOp := Close(file)
// result := closeOp(ctx)()
//
// Note: This function is typically used with WithResource for automatic resource management
// rather than being called directly.
//
// See Also:
// - Open: For opening files
// - ReadFile: For reading files with automatic closing
func Close[C io.Closer](c C) ReaderIOResult[Void] {
return F.Pipe2(
c,
IOEF.Close[C],
RIOE.FromIOEither[struct{}],
RIOE.FromIOEither[Void],
)
}
// ReadFile reads a file in the scope of a context
func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) RIOE.ReaderIOResult[[]byte] {
// ReadFile reads the entire contents of a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// The operation:
// - Opens the file for reading
// - Reads all contents into a byte slice
// - Automatically closes the file when done
// - Respects context cancellation during the read operation
//
// Parameters:
// - path: The path to the file to read
//
// Returns:
// - ReaderIOResult[[]byte]: A computation that reads the file contents
//
// Example:
//
// readOp := ReadFile("config.json")
// result := readOp(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Read error: %v", err) },
// func(data []byte) { log.Printf("Read %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// ReadFile(path) = WithResource(Open(path), Close)(readAllBytes)
//
// See Also:
// - Open: For opening files without automatic reading
// - Close: For closing file handles
// - WithResource: For custom resource management patterns
func ReadFile(path string) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](Open(path), Close[*os.File])(func(r *os.File) ReaderIOResult[[]byte] {
return func(ctx context.Context) IOE.IOEither[error, []byte] {
return func() ET.Either[error, []byte] {
return file.ReadAll(ctx, r)
@@ -62,3 +230,48 @@ func ReadFile(path string) RIOE.ReaderIOResult[[]byte] {
}
})
}
// WriteFile writes data to a file in a context-aware manner.
// This function automatically manages the file resource using the RAII pattern,
// ensuring the file is properly closed even if an error occurs or the context is canceled.
//
// If the file doesn't exist, it is created with mode 0666 (before umask).
// If the file already exists, it is truncated before writing.
//
// The operation:
// - Creates or truncates the file for writing
// - Writes all data to the file
// - Automatically closes the file when done
// - Respects context cancellation during the write operation
//
// Parameters:
// - data: The byte slice to write to the file
//
// Returns:
// - Kleisli[string, []byte]: A function that takes a file path and returns a computation
// that writes the data and returns the written bytes on success
//
// Example:
//
// writeOp := WriteFile([]byte("Hello, World!"))
// result := writeOp("output.txt")(ctx)()
// either.Fold(
// result,
// func(err error) { log.Printf("Write error: %v", err) },
// func(data []byte) { log.Printf("Wrote %d bytes", len(data)) },
// )
//
// The function uses WithResource internally to ensure proper cleanup:
//
// WriteFile(data) = Create >> WriteAll(data) >> Close
//
// See Also:
// - ReadFile: For reading file contents with automatic resource management
// - Create: For creating files without automatic writing
// - WriteAll: For writing to an already-open file handle
func WriteFile(data []byte) Kleisli[string, []byte] {
return F.Flow2(
Create,
WriteAll[*os.File](data),
)
}

View File

@@ -18,11 +18,16 @@ package file
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
R "github.com/IBM/fp-go/v2/context/readerioresult"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
J "github.com/IBM/fp-go/v2/json"
"github.com/stretchr/testify/assert"
)
type RecordType struct {
@@ -49,3 +54,267 @@ func ExampleReadFile() {
// Output:
// Right[string](Carsten)
}
func TestCreate(t *testing.T) {
ctx := context.Background()
t.Run("Success - creates new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_create.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was created
_, err := os.Stat(tempFile)
assert.NoError(t, err)
// Clean up file handle
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
})
t.Run("Success - truncates existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_truncate.txt")
// Create file with initial content
err := os.WriteFile(tempFile, []byte("initial content"), 0644)
assert.NoError(t, err)
// Create should truncate
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Close the file
E.MonadFold(result,
func(error) *os.File { return nil },
func(f *os.File) *os.File { f.Close(); return f },
)
// Verify file was truncated
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
// Try to create file in non-existent directory
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
createOp := Create(invalidPath)
result := createOp(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - file can be written to", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
createOp := Create(tempFile)
result := createOp(ctx)()
assert.True(t, E.IsRight(result))
// Write to the file
E.MonadFold(result,
func(err error) *os.File { t.Fatalf("Unexpected error: %v", err); return nil },
func(f *os.File) *os.File {
defer f.Close()
_, err := f.WriteString("test content")
assert.NoError(t, err)
return f
},
)
// Verify content was written
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, "test content", string(content))
})
t.Run("Context cancellation", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
createOp := Create(tempFile)
result := createOp(cancelCtx)()
// Note: File creation itself doesn't check context, but this tests the pattern
// In practice, context cancellation would affect subsequent operations
_ = result
})
}
func TestWriteFile(t *testing.T) {
ctx := context.Background()
t.Run("Success - writes data to new file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_write.txt")
testData := []byte("Hello, World!")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify returned data
E.MonadFold(result,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, testData, content)
})
t.Run("Success - overwrites existing file", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_overwrite.txt")
// Write initial content
err := os.WriteFile(tempFile, []byte("old content"), 0644)
assert.NoError(t, err)
// Overwrite with new content
newData := []byte("new content")
writeOp := WriteFile(newData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file was overwritten
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, newData, content)
})
t.Run("Success - writes empty data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_empty.txt")
emptyData := []byte{}
writeOp := WriteFile(emptyData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file is empty
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Empty(t, content)
})
t.Run("Success - writes large data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_large.txt")
largeData := make([]byte, 1024*1024) // 1MB
for i := range largeData {
largeData[i] = byte(i % 256)
}
writeOp := WriteFile(largeData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify file content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, largeData, content)
})
t.Run("Failure - invalid path", func(t *testing.T) {
invalidPath := filepath.Join(t.TempDir(), "nonexistent", "test.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(invalidPath)(ctx)()
assert.True(t, E.IsLeft(result))
})
t.Run("Success - writes binary data", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_binary.bin")
binaryData := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD}
writeOp := WriteFile(binaryData)
result := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(result))
// Verify binary content
content, err := os.ReadFile(tempFile)
assert.NoError(t, err)
assert.Equal(t, binaryData, content)
})
t.Run("Integration - write then read", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_roundtrip.txt")
testData := []byte("Round trip test data")
// Write data
writeOp := WriteFile(testData)
writeResult := writeOp(tempFile)(ctx)()
assert.True(t, E.IsRight(writeResult))
// Read data back
readOp := ReadFile(tempFile)
readResult := readOp(ctx)()
assert.True(t, E.IsRight(readResult))
// Verify data matches
E.MonadFold(readResult,
func(err error) []byte { t.Fatalf("Unexpected error: %v", err); return nil },
func(data []byte) []byte {
assert.Equal(t, testData, data)
return data
},
)
})
t.Run("Composition with Map", func(t *testing.T) {
tempFile := filepath.Join(t.TempDir(), "test_compose.txt")
testData := []byte("test data")
// Write and transform result
pipeline := F.Pipe1(
WriteFile(testData)(tempFile),
R.Map(func(data []byte) int { return len(data) }),
)
result := pipeline(ctx)()
assert.True(t, E.IsRight(result))
E.MonadFold(result,
func(err error) int { t.Fatalf("Unexpected error: %v", err); return 0 },
func(length int) int {
assert.Equal(t, len(testData), length)
return length
},
)
})
t.Run("Context cancellation during write", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
tempFile := filepath.Join(t.TempDir(), "test_cancel.txt")
testData := []byte("test")
writeOp := WriteFile(testData)
result := writeOp(tempFile)(cancelCtx)()
// Note: The actual write may complete before cancellation is checked
// This test verifies the pattern works with cancelled contexts
_ = result
})
}

View File

@@ -38,7 +38,7 @@ var (
)
// CreateTemp created a temp file with proper parametrization
func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
func CreateTemp(dir, pattern string) ReaderIOResult[*os.File] {
return F.Pipe2(
IOEF.CreateTemp(dir, pattern),
RIOE.FromIOEither[*os.File],
@@ -47,6 +47,6 @@ func CreateTemp(dir, pattern string) RIOE.ReaderIOResult[*os.File] {
}
// WithTempFile creates a temporary file, then invokes a callback to create a resource based on the file, then close and remove the temp file
func WithTempFile[A any](f func(*os.File) RIOE.ReaderIOResult[A]) RIOE.ReaderIOResult[A] {
func WithTempFile[A any](f Kleisli[*os.File, A]) ReaderIOResult[A] {
return RIOE.WithResource[A](onCreateTempFile, onReleaseTempFile)(f)
}

View File

@@ -34,7 +34,7 @@ func TestWithTempFile(t *testing.T) {
func TestWithTempFileOnClosedFile(t *testing.T) {
res := WithTempFile(func(f *os.File) RIOE.ReaderIOResult[[]byte] {
res := WithTempFile(func(f *os.File) ReaderIOResult[[]byte] {
return F.Pipe2(
f,
onWriteAll[*os.File]([]byte("Carsten")),

View File

@@ -0,0 +1,90 @@
// 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 file
import (
"github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/function"
)
type (
// ReaderIOResult represents a context-aware computation that performs side effects
// and can fail with an error. This is the main type used throughout the file package
// for all file operations.
//
// ReaderIOResult[A] is equivalent to:
// func(context.Context) func() Either[error, A]
//
// The computation:
// - Takes a context.Context for cancellation and timeouts
// - Performs side effects (IO operations)
// - Returns Either an error or a value of type A
//
// See Also:
// - readerioresult.ReaderIOResult: The underlying type definition
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// Void represents the absence of a meaningful value, similar to unit type in other languages.
// It is used when a function performs side effects but doesn't return a meaningful result.
//
// Void is typically used as the success type in operations like Close that perform
// an action but don't produce a useful value.
//
// Example:
// Close[*os.File](file) // Returns ReaderIOResult[Void]
//
// See Also:
// - function.Void: The underlying type definition
Void = function.Void
// Kleisli represents a Kleisli arrow for ReaderIOResult.
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
//
// Kleisli arrows are used for monadic composition, allowing you to chain operations
// that produce ReaderIOResults. They are particularly useful with Chain and Bind operations.
//
// Kleisli[A, B] is equivalent to:
// func(A) ReaderIOResult[B]
//
// Example:
// // A Kleisli arrow that reads a file given its path
// var readFileK Kleisli[string, []byte] = ReadFile
//
// See Also:
// - readerioresult.Kleisli: The underlying type definition
// - Operator: For transforming ReaderIOResults
Kleisli[A, B any] = readerioresult.Kleisli[A, B]
// Operator represents a transformation from one ReaderIOResult to another.
// This is useful for point-free style composition and building reusable transformations.
//
// Operator[A, B] is equivalent to:
// func(ReaderIOResult[A]) ReaderIOResult[B]
//
// Operators are used to transform computations without executing them, enabling
// powerful composition patterns.
//
// Example:
// // An operator that maps over file contents
// var toUpper Operator[[]byte, string] = Map(func(data []byte) string {
// return strings.ToUpper(string(data))
// })
//
// See Also:
// - readerioresult.Operator: The underlying type definition
// - Kleisli: For functions that produce ReaderIOResults
Operator[A, B any] = readerioresult.Operator[A, B]
)

View File

@@ -23,8 +23,8 @@ import (
F "github.com/IBM/fp-go/v2/function"
)
func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte] {
return func(w W) RIOE.ReaderIOResult[[]byte] {
func onWriteAll[W io.Writer](data []byte) Kleisli[W, []byte] {
return func(w W) ReaderIOResult[[]byte] {
return F.Pipe1(
RIOE.TryCatch(func(_ context.Context) func() ([]byte, error) {
return func() ([]byte, error) {
@@ -38,9 +38,9 @@ func onWriteAll[W io.Writer](data []byte) func(w W) RIOE.ReaderIOResult[[]byte]
}
// WriteAll uses a generator function to create a stream, writes data to it and closes it
func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
func WriteAll[W io.WriteCloser](data []byte) Operator[W, []byte] {
onWrite := onWriteAll[W](data)
return func(onCreate RIOE.ReaderIOResult[W]) RIOE.ReaderIOResult[[]byte] {
return func(onCreate ReaderIOResult[W]) ReaderIOResult[[]byte] {
return RIOE.WithResource[[]byte](
onCreate,
Close[W])(
@@ -50,7 +50,7 @@ func WriteAll[W io.WriteCloser](data []byte) func(acquire RIOE.ReaderIOResult[W]
}
// Write uses a generator function to create a stream, writes data to it and closes it
func Write[R any, W io.WriteCloser](acquire RIOE.ReaderIOResult[W]) func(use func(W) RIOE.ReaderIOResult[R]) RIOE.ReaderIOResult[R] {
func Write[R any, W io.WriteCloser](acquire ReaderIOResult[W]) Kleisli[Kleisli[W, R], R] {
return RIOE.WithResource[R](
acquire,
Close[W])

View File

@@ -19,6 +19,10 @@ import (
"context"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/result"
)
// Promap is the profunctor map operation that transforms both the input and output of a context-based ReaderIOResult.
@@ -45,7 +49,7 @@ import (
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[B]
//
//go:inline
func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFunc), g func(A) B) Operator[A, B] {
func Promap[A, B any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context], g func(A) B) Operator[A, B] {
return function.Flow2(
Local[A](f),
Map(g),
@@ -70,6 +74,139 @@ func Promap[A, B any](f func(context.Context) (context.Context, context.CancelFu
// - An Operator that takes a ReaderIOResult[A] and returns a ReaderIOResult[A]
//
//go:inline
func Contramap[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
func Contramap[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
return Local[A](f)
}
func ContramapIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return LocalIOK[A](f)
}
// LocalIOK transforms the context using an IO-based function before passing it to a ReaderIOResult.
// This is similar to Local but the context transformation itself is wrapped in an IO effect.
//
// The function f takes a context and returns an IO effect that produces a ContextCancel
// (a pair of CancelFunc and the new Context). This allows the context transformation to
// perform side effects.
//
// # Use Cases
//
// This function is useful for sharing information via the Context that is computed through
// side effects that cannot fail, such as:
// - Generating unique request IDs or trace IDs
// - Recording timestamps or metrics
// - Logging context information
// - Computing derived values from existing context data
//
// The side effect is executed during the context transformation, and the resulting data is
// stored in the context for downstream computations to access.
//
// # Type Parameters
//
// - A: The success type (unchanged through the transformation)
//
// # Parameters
//
// - f: An IO-based Kleisli function that transforms the context
//
// # Returns
//
// - An Operator that applies the context transformation before executing the ReaderIOResult
//
// # Example Usage
//
// // Generate a request ID via side effect and add to context
// addRequestID := func(ctx context.Context) io.IO[ContextCancel] {
// return func() ContextCancel {
// // Side effect: generate unique ID
// requestID := uuid.New().String()
// // Share the ID via context
// newCtx := context.WithValue(ctx, "requestID", requestID)
// return pair.MakePair(func() {}, newCtx)
// }
// }
// adapted := LocalIOK[int](addRequestID)(computation)
//
// # See Also
//
// - Local: For pure context transformations
// - LocalIOResultK: For context transformations that can fail
//
//go:inline
func LocalIOK[A any](f io.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return LocalIOResultK[A](function.Flow2(f, ioresult.FromIO))
}
// LocalIOResultK transforms the context using an IOResult-based function before passing it to a ReaderIOResult.
// This is similar to Local but the context transformation can fail with an error.
//
// The function f takes a context and returns an IOResult that produces either an error or a ContextCancel
// (a pair of CancelFunc and the new Context). If the transformation fails, the error is propagated
// and the original ReaderIOResult is not executed.
//
// # Use Cases
//
// This function is particularly useful for sharing information via the Context that is computed
// through side effects, such as:
// - Loading configuration from a file or database
// - Fetching authentication tokens from an external service
// - Computing derived values that require I/O operations
// - Validating and enriching context with data from external sources
//
// The side effect is executed during the context transformation, and the resulting data is
// stored in the context for downstream computations to access.
//
// # Type Parameters
//
// - A: The success type (unchanged through the transformation)
//
// # Parameters
//
// - f: An IOResult-based Kleisli function that transforms the context and may fail
//
// # Returns
//
// - An Operator that applies the context transformation before executing the ReaderIOResult
//
// # Example Usage
//
// // Load configuration via side effect and add to context
// loadConfig := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
// return func() result.Result[ContextCancel] {
// // Side effect: read from file system
// config, err := os.ReadFile("config.json")
// if err != nil {
// return result.Left[ContextCancel](err)
// }
// // Share the loaded config via context
// newCtx := context.WithValue(ctx, "config", config)
// return result.Of(pair.MakePair(func() {}, newCtx))
// }
// }
// adapted := LocalIOResultK[int](loadConfig)(computation)
//
// # See Also
//
// - Local: For pure context transformations
// - LocalIOK: For context transformations with side effects that cannot fail
//
//go:inline
func LocalIOResultK[A any](f ioresult.Kleisli[context.Context, ContextCancel]) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
p, err := result.Unwrap(f(ctx)())
if err != nil {
return result.Left[A](err)
}
// unwrap
otherCancel, otherCtx := pair.Unpack(p)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}

View File

@@ -20,6 +20,10 @@ import (
"strconv"
"testing"
"github.com/IBM/fp-go/v2/context/ioresult"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/pair"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -36,9 +40,9 @@ func TestPromapBasic(t *testing.T) {
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "key", 42)
return newCtx, func() {}
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
toString := strconv.Itoa
@@ -61,9 +65,9 @@ func TestContramapBasic(t *testing.T) {
}
}
addKey := func(ctx context.Context) (context.Context, context.CancelFunc) {
addKey := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "key", 100)
return newCtx, func() {}
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
adapted := Contramap[int](addKey)(getValue)
@@ -85,9 +89,9 @@ func TestLocalBasic(t *testing.T) {
}
}
addUser := func(ctx context.Context) (context.Context, context.CancelFunc) {
addUser := func(ctx context.Context) pair.Pair[context.CancelFunc, context.Context] {
newCtx := context.WithValue(ctx, "user", "Alice")
return newCtx, func() {}
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
adapted := Local[string](addUser)(getValue)
@@ -96,3 +100,311 @@ func TestLocalBasic(t *testing.T) {
assert.Equal(t, R.Of("Alice"), result)
})
}
// TestLocalIOK_Success tests LocalIOK with successful context transformation
func TestLocalIOK_Success(t *testing.T) {
t.Run("transforms context with IO effect", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("user"); v != nil {
return R.Of(v.(string))
}
return R.Of("unknown")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Bob")
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := LocalIOK[string](addUser)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of("Bob"), result)
})
t.Run("preserves original value type", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("count"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addCount := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "count", 42)
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
adapted := LocalIOK[int](addCount)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(42), result)
})
}
// TestLocalIOK_CancelledContext tests LocalIOK with cancelled context
func TestLocalIOK_CancelledContext(t *testing.T) {
t.Run("returns error when context is cancelled", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Charlie")
return pair.MakePair(context.CancelFunc(func() {}), newCtx)
}
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
adapted := LocalIOK[string](addUser)(getValue)
result := adapted(ctx)()
assert.True(t, R.IsLeft(result))
})
}
// TestLocalIOK_CancelFuncCalled tests that CancelFunc is properly called
func TestLocalIOK_CancelFuncCalled(t *testing.T) {
t.Run("calls cancel function after execution", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
addUser := func(ctx context.Context) io.IO[ContextCancel] {
return func() ContextCancel {
newCtx := context.WithValue(ctx, "user", "Dave")
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
return pair.MakePair(cancelFunc, newCtx)
}
}
adapted := LocalIOK[string](addUser)(getValue)
_ = adapted(t.Context())()
assert.True(t, cancelCalled, "cancel function should be called")
})
}
// TestLocalIOResultK_Success tests LocalIOResultK with successful context transformation
func TestLocalIOResultK_Success(t *testing.T) {
t.Run("transforms context with IOResult effect", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
if v := ctx.Value("role"); v != nil {
return R.Of(v.(string))
}
return R.Of("guest")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "admin")
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
adapted := LocalIOResultK[string](addRole)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of("admin"), result)
})
t.Run("preserves original value type", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("score"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addScore := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "score", 100)
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
adapted := LocalIOResultK[int](addScore)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(100), result)
})
}
// TestLocalIOResultK_Failure tests LocalIOResultK with failed context transformation
func TestLocalIOResultK_Failure(t *testing.T) {
t.Run("propagates transformation error", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
result := adapted(t.Context())()
assert.True(t, R.IsLeft(result))
_, err := R.UnwrapError(result)
assert.Equal(t, assert.AnError, err)
})
t.Run("does not execute original computation on transformation failure", func(t *testing.T) {
executed := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
executed = true
return R.Of("should not execute")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
_ = adapted(t.Context())()
assert.False(t, executed, "original computation should not execute")
})
}
// TestLocalIOResultK_CancelledContext tests LocalIOResultK with cancelled context
func TestLocalIOResultK_CancelledContext(t *testing.T) {
t.Run("returns error when context is cancelled", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("should not reach here")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "user")
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
adapted := LocalIOResultK[string](addRole)(getValue)
result := adapted(ctx)()
assert.True(t, R.IsLeft(result))
})
}
// TestLocalIOResultK_CancelFuncCalled tests that CancelFunc is properly called
func TestLocalIOResultK_CancelFuncCalled(t *testing.T) {
t.Run("calls cancel function after successful execution", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
addRole := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "role", "user")
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
return R.Of(pair.MakePair(cancelFunc, newCtx))
}
}
adapted := LocalIOResultK[string](addRole)(getValue)
_ = adapted(t.Context())()
assert.True(t, cancelCalled, "cancel function should be called")
})
t.Run("does not call cancel function on transformation failure", func(t *testing.T) {
cancelCalled := false
getValue := func(ctx context.Context) IOResult[string] {
return func() R.Result[string] {
return R.Of("test")
}
}
failTransform := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
cancelFunc := context.CancelFunc(func() {
cancelCalled = true
})
_ = cancelFunc // avoid unused warning
return R.Left[ContextCancel](assert.AnError)
}
}
adapted := LocalIOResultK[string](failTransform)(getValue)
_ = adapted(t.Context())()
assert.False(t, cancelCalled, "cancel function should not be called on failure")
})
}
// TestLocalIOResultK_Integration tests integration with other operations
func TestLocalIOResultK_Integration(t *testing.T) {
t.Run("composes with Map", func(t *testing.T) {
getValue := func(ctx context.Context) IOResult[int] {
return func() R.Result[int] {
if v := ctx.Value("value"); v != nil {
return R.Of(v.(int))
}
return R.Of(0)
}
}
addValue := func(ctx context.Context) ioresult.IOResult[ContextCancel] {
return func() R.Result[ContextCancel] {
newCtx := context.WithValue(ctx, "value", 10)
return R.Of(pair.MakePair(context.CancelFunc(func() {}), newCtx))
}
}
double := func(x int) int { return x * 2 }
adapted := F.Flow2(
LocalIOResultK[int](addValue),
Map(double),
)(getValue)
result := adapted(t.Context())()
assert.Equal(t, R.Of(20), result)
})
}

View File

@@ -28,6 +28,7 @@ import (
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
@@ -1054,14 +1055,14 @@ func TapLeftIOK[A, B any](f io.Kleisli[error, B]) Operator[A, A] {
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
func Local[A any](f pair.Kleisli[context.CancelFunc, context.Context, context.Context]) Operator[A, A] {
return func(rr ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
return func() Result[A] {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
otherCtx, otherCancel := f(ctx)
otherCancel, otherCtx := pair.Unpack(f(ctx))
defer otherCancel()
return rr(otherCtx)()
}
@@ -1123,9 +1124,10 @@ func Local[A any](f func(context.Context) (context.Context, context.CancelFunc))
// )
// value, err := result(t.Context())() // Returns (Data{Value: "quick"}, nil)
func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, timeout)
})
return Local[A](
func(ctx context.Context) ContextCancel {
return pairFromContextCancel(context.WithTimeout(ctx, timeout))
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIOResult computation.
@@ -1188,7 +1190,7 @@ func WithTimeout[A any](timeout time.Duration) Operator[A, A] {
// )
// value, err := result(parentCtx)() // Will use parent's 1-hour deadline
func WithDeadline[A any](deadline time.Time) Operator[A, A] {
return Local[A](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithDeadline(ctx, deadline)
return Local[A](func(ctx context.Context) ContextCancel {
return pairFromContextCancel(context.WithDeadline(ctx, deadline))
})
}

View File

@@ -55,6 +55,10 @@ type (
// Either[A] is equivalent to Either[error, A] from the either package.
Either[A any] = either.Either[error, A]
// Result represents a computation that can either succeed with a value of type A
// or fail with an error. This is an alias for result.Result[A].
//
// Result[A] is equivalent to Either[error, A]
Result[A any] = result.Result[A]
// Lazy represents a deferred computation that produces a value of type A when executed.
@@ -73,6 +77,10 @@ type (
// IOEither[A] is equivalent to func() Either[error, A]
IOEither[A any] = ioeither.IOEither[error, A]
// IOResult represents a side-effectful computation that can fail with an error.
// This combines IO (side effects) with Result (error handling).
//
// IOResult[A] is equivalent to func() Result[A]
IOResult[A any] = ioresult.IOResult[A]
// Reader represents a computation that depends on a context of type R.
@@ -118,6 +126,13 @@ type (
// result := fetchUser("123")(ctx)()
ReaderIOResult[A any] = RIOR.ReaderIOResult[context.Context, A]
// Kleisli represents a Kleisli arrow for ReaderIOResult.
// It is a function that takes a value of type A and returns a ReaderIOResult[B].
//
// Kleisli arrows are used for monadic composition, allowing you to chain operations
// that produce ReaderIOResults. They are particularly useful with Chain operations.
//
// Kleisli[A, B] is equivalent to func(A) ReaderIOResult[B]
Kleisli[A, B any] = reader.Reader[A, ReaderIOResult[B]]
// Operator represents a transformation from one ReaderIOResult to another.
@@ -133,26 +148,76 @@ type (
// result := toUpper(computation)
Operator[A, B any] = Kleisli[ReaderIOResult[A], B]
ReaderResult[A any] = readerresult.ReaderResult[A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// ReaderResult represents a context-dependent computation that can fail.
// This is specialized to use context.Context as the context type.
//
// ReaderResult[A] is equivalent to func(context.Context) Result[A]
ReaderResult[A any] = readerresult.ReaderResult[A]
// ReaderEither represents a context-dependent computation that can fail.
// It takes a context of type R and produces an Either[E, A].
//
// ReaderEither[R, E, A] is equivalent to func(R) Either[E, A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
// ReaderOption represents a context-dependent computation that may not produce a value.
// It takes a context of type R and produces an Option[A].
//
// ReaderOption[R, A] is equivalent to func(R) Option[A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
// Endomorphism represents a function from a type to itself.
// It is used for transformations that preserve the type.
//
// Endomorphism[A] is equivalent to func(A) A
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Consumer represents a function that consumes a value without producing a result.
// It is used for side effects like logging or updating state.
//
// Consumer[A] is equivalent to func(A)
Consumer[A any] = consumer.Consumer[A]
// Prism represents an optic for working with sum types (tagged unions).
// It provides a way to focus on a specific variant of a sum type.
Prism[S, T any] = prism.Prism[S, T]
Lens[S, T any] = lens.Lens[S, T]
// Lens represents an optic for working with product types (records/structs).
// It provides a way to focus on a specific field of a product type.
Lens[S, T any] = lens.Lens[S, T]
// Trampoline represents a computation that can be executed in a stack-safe manner.
// It is used for tail-recursive computations that would otherwise overflow the stack.
Trampoline[B, L any] = tailrec.Trampoline[B, L]
// Predicate represents a function that tests a value of type A.
// It returns true if the value satisfies the predicate, false otherwise.
//
// Predicate[A] is equivalent to func(A) bool
Predicate[A any] = predicate.Predicate[A]
// Pair represents a tuple of two values of types A and B.
// It is used to group two related values together.
Pair[A, B any] = pair.Pair[A, B]
// IORef represents a mutable reference that can be safely accessed in IO computations.
// It provides thread-safe read and write operations.
IORef[A any] = ioref.IORef[A]
// State represents a stateful computation that transforms a state of type S
// and produces a value of type A.
//
// State[S, A] is equivalent to func(S) Pair[A, S]
State[S, A any] = state.State[S, A]
// Void represents the absence of a value, similar to unit type in other languages.
// It is used when a function performs side effects but doesn't return a meaningful value.
Void = function.Void
// ContextCancel represents a pair of a cancel function and a context.
// It is used in operations that create new contexts with cancellation capabilities.
//
// The first element is the CancelFunc that should be called to release resources.
// The second element is the new Context that was created.
ContextCancel = Pair[context.CancelFunc, context.Context]
)

View File

@@ -25,6 +25,31 @@ import (
"github.com/IBM/fp-go/v2/readerio"
)
// Do creates an Effect with an initial state value.
// This is the starting point for do-notation style effect composition,
// allowing you to build up complex state transformations step by step.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S: The state type
//
// # Parameters
//
// - empty: The initial state value
//
// # Returns
//
// - Effect[C, S]: An effect that produces the initial state
//
// # Example
//
// type State struct {
// Name string
// Age int
// }
// eff := effect.Do[MyContext](State{})
//
//go:inline
func Do[C, S any](
empty S,
@@ -32,6 +57,40 @@ func Do[C, S any](
return readerreaderioresult.Of[C](empty)
}
// Bind executes an effectful computation and binds its result to the state.
// This is the core operation for do-notation, allowing you to sequence effects
// while accumulating results in a state structure.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the effect
//
// # Parameters
//
// - setter: A function that takes the effect result and returns a state updater
// - f: An effectful computation that depends on the current state
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// eff := effect.Bind(
// func(age int) func(State) State {
// return func(s State) State {
// s.Age = age
// return s
// }
// },
// func(s State) Effect[MyContext, int] {
// return effect.Of[MyContext](30)
// },
// )(effect.Do[MyContext](State{}))
//
//go:inline
func Bind[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -40,6 +99,39 @@ func Bind[C, S1, S2, T any](
return readerreaderioresult.Bind(setter, f)
}
// Let computes a pure value from the current state and binds it to the state.
// Unlike Bind, this doesn't perform any effects - it's for pure computations.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S1: The input state type
// - S2: The output state type
// - T: The type of computed value
//
// # Parameters
//
// - setter: A function that takes the computed value and returns a state updater
// - f: A pure function that computes a value from the current state
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// eff := effect.Let[MyContext](
// func(nameLen int) func(State) State {
// return func(s State) State {
// s.NameLength = nameLen
// return s
// }
// },
// func(s State) int {
// return len(s.Name)
// },
// )(stateEff)
//
//go:inline
func Let[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -48,6 +140,37 @@ func Let[C, S1, S2, T any](
return readerreaderioresult.Let[C](setter, f)
}
// LetTo binds a constant value to the state.
// This is useful for setting fixed values in your state structure.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S1: The input state type
// - S2: The output state type
// - T: The type of the constant value
//
// # Parameters
//
// - setter: A function that takes the constant and returns a state updater
// - b: The constant value to bind
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// eff := effect.LetTo[MyContext](
// func(age int) func(State) State {
// return func(s State) State {
// s.Age = age
// return s
// }
// },
// 42,
// )(stateEff)
//
//go:inline
func LetTo[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -56,6 +179,30 @@ func LetTo[C, S1, S2, T any](
return readerreaderioresult.LetTo[C](setter, b)
}
// BindTo wraps a value in an initial state structure.
// This is typically used to start a bind chain by converting a simple value
// into a state structure.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S1: The state type to create
// - T: The type of the input value
//
// # Parameters
//
// - setter: A function that creates a state from the value
//
// # Returns
//
// - Operator[C, T, S1]: A function that wraps the value in state
//
// # Example
//
// eff := effect.BindTo[MyContext](func(name string) State {
// return State{Name: name}
// })(effect.Of[MyContext]("Alice"))
//
//go:inline
func BindTo[C, S1, T any](
setter func(T) S1,
@@ -63,6 +210,39 @@ func BindTo[C, S1, T any](
return readerreaderioresult.BindTo[C](setter)
}
// ApS applies an effect and binds its result to the state using a setter function.
// This is similar to Bind but takes a pre-existing effect rather than a function
// that creates an effect from the state.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the effect
//
// # Parameters
//
// - setter: A function that takes the effect result and returns a state updater
// - fa: The effect to apply
//
// # Returns
//
// - Operator[C, S1, S2]: A function that transforms the state effect
//
// # Example
//
// ageEffect := effect.Of[MyContext](30)
// eff := effect.ApS(
// func(age int) func(State) State {
// return func(s State) State {
// s.Age = age
// return s
// }
// },
// ageEffect,
// )(stateEff)
//
//go:inline
func ApS[C, S1, S2, T any](
setter func(T) func(S1) S2,
@@ -71,6 +251,33 @@ func ApS[C, S1, S2, T any](
return readerreaderioresult.ApS(setter, fa)
}
// ApSL applies an effect and updates a field in the state using a lens.
// This provides a more ergonomic way to update nested state structures.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - fa: The effect producing the new field value
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// ageEffect := effect.Of[MyContext](30)
// eff := effect.ApSL(ageLens, ageEffect)(stateEff)
//
//go:inline
func ApSL[C, S, T any](
lens Lens[S, T],
@@ -79,6 +286,37 @@ func ApSL[C, S, T any](
return readerreaderioresult.ApSL(lens, fa)
}
// BindL executes an effectful computation on a field and updates it using a lens.
// The effect function receives the current field value and produces a new value.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - f: An effectful function that transforms the field value
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// eff := effect.BindL(
// ageLens,
// func(age int) Effect[MyContext, int] {
// return effect.Of[MyContext](age + 1)
// },
// )(stateEff)
//
//go:inline
func BindL[C, S, T any](
lens Lens[S, T],
@@ -87,6 +325,35 @@ func BindL[C, S, T any](
return readerreaderioresult.BindL(lens, f)
}
// LetL computes a new field value from the current value using a lens.
// This is a pure transformation of a field within the state.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - f: A pure function that transforms the field value
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// eff := effect.LetL[MyContext](
// ageLens,
// func(age int) int { return age * 2 },
// )(stateEff)
//
//go:inline
func LetL[C, S, T any](
lens Lens[S, T],
@@ -95,6 +362,31 @@ func LetL[C, S, T any](
return readerreaderioresult.LetL[C](lens, f)
}
// LetToL sets a field to a constant value using a lens.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - S: The state type
// - T: The type of the field being updated
//
// # Parameters
//
// - lens: A lens focusing on the field to update
// - b: The constant value to set
//
// # Returns
//
// - Operator[C, S, S]: A function that updates the state field
//
// # Example
//
// ageLens := lens.MakeLens(
// func(s State) int { return s.Age },
// func(s State, age int) State { s.Age = age; return s },
// )
// eff := effect.LetToL[MyContext](ageLens, 42)(stateEff)
//
//go:inline
func LetToL[C, S, T any](
lens Lens[S, T],

32
v2/effect/common_test.go Normal file
View File

@@ -0,0 +1,32 @@
// 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 effect
import (
"context"
)
// TestContext is a common test context type used across effect tests
type TestContext struct {
Value string
}
// runEffect is a helper function to run an effect with a context and return the result
func runEffect[C, A any](eff Effect[C, A], ctx C) (A, error) {
ioResult := Provide[A, C](ctx)(eff)
readerResult := RunSync(ioResult)
return readerResult(context.Background())
}

View File

@@ -1,3 +1,18 @@
// 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 effect
import (
@@ -8,31 +23,182 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// Local transforms the context required by an effect using a pure function.
// This allows you to adapt an effect that requires one context type to work
// with a different context type by providing a transformation function.
//
// # Type Parameters
//
// - C1: The outer context type (what you have)
// - C2: The inner context type (what the effect needs)
// - A: The value type produced by the effect
//
// # Parameters
//
// - acc: A pure function that transforms C1 to C2
//
// # Returns
//
// - Kleisli[C1, Effect[C2, A], A]: A function that adapts the effect to use C1
//
// # Example
//
// type AppConfig struct { DB DatabaseConfig }
// type DatabaseConfig struct { Host string }
// dbEffect := effect.Of[DatabaseConfig]("connected")
// appEffect := effect.Local[AppConfig, DatabaseConfig, string](
// func(app AppConfig) DatabaseConfig { return app.DB },
// )(dbEffect)
//
//go:inline
func Local[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
func Local[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
// Contramap is an alias for Local, following the contravariant functor naming convention.
// It transforms the context required by an effect using a pure function.
//
// # Type Parameters
//
// - C1: The outer context type (what you have)
// - C2: The inner context type (what the effect needs)
// - A: The value type produced by the effect
//
// # Parameters
//
// - acc: A pure function that transforms C1 to C2
//
// # Returns
//
// - Kleisli[C1, Effect[C2, A], A]: A function that adapts the effect to use C1
//
//go:inline
func Contramap[C1, C2, A any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
func Contramap[A, C1, C2 any](acc Reader[C1, C2]) Kleisli[C1, Effect[C2, A], A] {
return readerreaderioresult.Local[A](acc)
}
// LocalIOK transforms the context using an IO-based function.
// This allows the context transformation itself to perform I/O operations.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: An IO function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// loadConfig := func(path string) io.IO[Config] {
// return func() Config { /* load from file */ }
// }
// transform := effect.LocalIOK[string](loadConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalIOK[A, C1, C2 any](f io.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOK[A](f)
}
// LocalIOResultK transforms the context using an IOResult-based function.
// This allows the context transformation to perform I/O and handle errors.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: An IOResult function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// loadConfig := func(path string) ioresult.IOResult[Config] {
// return func() result.Result[Config] {
// // load from file, may fail
// }
// }
// transform := effect.LocalIOResultK[string](loadConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalIOResultK[A, C1, C2 any](f ioresult.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalIOResultK[A](f)
}
// LocalResultK transforms the context using a Result-based function.
// This allows the context transformation to fail with an error.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: A Result function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// validateConfig := func(raw RawConfig) result.Result[Config] {
// if raw.IsValid() {
// return result.Of(raw.ToConfig())
// }
// return result.Left[Config](errors.New("invalid"))
// }
// transform := effect.LocalResultK[string](validateConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalResultK[A, C1, C2 any](f result.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalResultK[A](f)
}
// LocalThunkK transforms the context using a Thunk (ReaderIOResult) function.
// This allows the context transformation to depend on context.Context, perform I/O, and handle errors.
//
// # Type Parameters
//
// - A: The value type produced by the effect
// - C1: The inner context type (what the effect needs)
// - C2: The outer context type (what you have)
//
// # Parameters
//
// - f: A Thunk function that transforms C2 to C1
//
// # Returns
//
// - func(Effect[C1, A]) Effect[C2, A]: A function that adapts the effect
//
// # Example
//
// loadConfig := func(path string) readerioresult.ReaderIOResult[Config] {
// return func(ctx context.Context) ioresult.IOResult[Config] {
// // load from file with context, may fail
// }
// }
// transform := effect.LocalThunkK[string](loadConfig)
// adapted := transform(configEffect)
//
//go:inline
func LocalThunkK[A, C1, C2 any](f thunk.Kleisli[C2, C1]) func(Effect[C1, A]) Effect[C2, A] {
return readerreaderioresult.LocalReaderIOResultK[A](f)

View File

@@ -44,11 +44,11 @@ func TestLocal(t *testing.T) {
}
// Apply Local to transform the context
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string, OuterContext, InnerContext](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string, OuterContext](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -70,11 +70,11 @@ func TestLocal(t *testing.T) {
return InnerContext{Value: outer.Value + " transformed"}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string, OuterContext, InnerContext](accessor)
outerEffect := kleisli(innerEffect)
// Run with OuterContext
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string, OuterContext](OuterContext{
Value: "original",
Number: 100,
})(outerEffect)
@@ -93,10 +93,10 @@ func TestLocal(t *testing.T) {
return InnerContext{Value: outer.Value}
}
kleisli := Local[OuterContext, InnerContext, string](accessor)
kleisli := Local[string, OuterContext, InnerContext](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string, OuterContext](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -122,12 +122,12 @@ func TestLocal(t *testing.T) {
level3Effect := Of[Level3]("deep result")
// Transform Level2 -> Level3
local23 := Local[Level2, Level3, string](func(l2 Level2) Level3 {
local23 := Local[string, Level2, Level3](func(l2 Level2) Level3 {
return Level3{C: l2.B + "-c"}
})
// Transform Level1 -> Level2
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
local12 := Local[string, Level1, Level2](func(l1 Level1) Level2 {
return Level2{B: l1.A + "-b"}
})
@@ -136,7 +136,7 @@ func TestLocal(t *testing.T) {
level1Effect := local12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
ioResult := Provide[string, Level1](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -165,11 +165,11 @@ func TestLocal(t *testing.T) {
return app.DB
}
kleisli := Local[AppConfig, DatabaseConfig, string](accessor)
kleisli := Local[string, AppConfig, DatabaseConfig](accessor)
appEffect := kleisli(dbEffect)
// Run with full AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string, AppConfig](AppConfig{
DB: DatabaseConfig{
Host: "localhost",
Port: 5432,
@@ -195,21 +195,21 @@ func TestContramap(t *testing.T) {
}
// Test Local
localKleisli := Local[OuterContext, InnerContext, int](accessor)
localKleisli := Local[int, OuterContext, InnerContext](accessor)
localEffect := localKleisli(innerEffect)
// Test Contramap
contramapKleisli := Contramap[OuterContext, InnerContext, int](accessor)
contramapKleisli := Contramap[int, OuterContext, InnerContext](accessor)
contramapEffect := contramapKleisli(innerEffect)
outerCtx := OuterContext{Value: "test", Number: 100}
// Run both
localIO := Provide[OuterContext, int](outerCtx)(localEffect)
localIO := Provide[int, OuterContext](outerCtx)(localEffect)
localReader := RunSync(localIO)
localResult, localErr := localReader(context.Background())
contramapIO := Provide[OuterContext, int](outerCtx)(contramapEffect)
contramapIO := Provide[int, OuterContext](outerCtx)(contramapEffect)
contramapReader := RunSync(contramapIO)
contramapResult, contramapErr := contramapReader(context.Background())
@@ -225,10 +225,10 @@ func TestContramap(t *testing.T) {
return InnerContext{Value: outer.Value + " modified"}
}
kleisli := Contramap[OuterContext, InnerContext, string](accessor)
kleisli := Contramap[string, OuterContext, InnerContext](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, string](OuterContext{
ioResult := Provide[string, OuterContext](OuterContext{
Value: "original",
Number: 50,
})(outerEffect)
@@ -247,10 +247,10 @@ func TestContramap(t *testing.T) {
return InnerContext{Value: outer.Value}
}
kleisli := Contramap[OuterContext, InnerContext, int](accessor)
kleisli := Contramap[int, OuterContext, InnerContext](accessor)
outerEffect := kleisli(innerEffect)
ioResult := Provide[OuterContext, int](OuterContext{
ioResult := Provide[int, OuterContext](OuterContext{
Value: "test",
Number: 42,
})(outerEffect)
@@ -278,12 +278,12 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
effect3 := Of[Config3]("result")
// Use Local for first transformation
local23 := Local[Config2, Config3, string](func(c2 Config2) Config3 {
local23 := Local[string, Config2, Config3](func(c2 Config2) Config3 {
return Config3{Info: c2.Data}
})
// Use Contramap for second transformation
contramap12 := Contramap[Config1, Config2, string](func(c1 Config1) Config2 {
contramap12 := Contramap[string, Config1, Config2](func(c1 Config1) Config2 {
return Config2{Data: c1.Value}
})
@@ -292,7 +292,7 @@ func TestLocalAndContramapInteroperability(t *testing.T) {
effect1 := contramap12(effect2)
// Run
ioResult := Provide[Config1, string](Config1{Value: "test"})(effect1)
ioResult := Provide[string, Config1](Config1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -326,7 +326,7 @@ func TestLocalEffectK(t *testing.T) {
appEffect := transform(dbEffect)
// Run with AppConfig
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string, AppConfig](AppConfig{
ConfigPath: "/etc/app.conf",
})(appEffect)
readerResult := RunSync(ioResult)
@@ -356,7 +356,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](failingTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
ioResult := Provide[string, OuterCtx](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -384,7 +384,7 @@ func TestLocalEffectK(t *testing.T) {
transformK := LocalEffectK[string](transform)
outerEffect := transformK(innerEffect)
ioResult := Provide[OuterCtx, string](OuterCtx{Path: "test"})(outerEffect)
ioResult := Provide[string, OuterCtx](OuterCtx{Path: "test"})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -417,7 +417,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](loadConfigEffect)
appEffect := transform(configEffect)
ioResult := Provide[AppContext, string](AppContext{
ioResult := Provide[string, AppContext](AppContext{
ConfigFile: "config.json",
})(appEffect)
readerResult := RunSync(ioResult)
@@ -456,7 +456,7 @@ func TestLocalEffectK(t *testing.T) {
level1Effect := transform12(level2Effect)
// Run with Level1 context
ioResult := Provide[Level1, string](Level1{A: "a"})(level1Effect)
ioResult := Provide[string, Level1](Level1{A: "a"})(level1Effect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -497,7 +497,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[string](transformWithContext)
appEffect := transform(dbEffect)
ioResult := Provide[AppConfig, string](AppConfig{
ioResult := Provide[string, AppConfig](AppConfig{
Environment: "prod",
DBHost: "localhost",
DBPort: 5432,
@@ -534,14 +534,14 @@ func TestLocalEffectK(t *testing.T) {
outerEffect := transform(innerEffect)
// Test with invalid config
ioResult := Provide[RawConfig, string](RawConfig{APIKey: ""})(outerEffect)
ioResult := Provide[string, RawConfig](RawConfig{APIKey: ""})(outerEffect)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
assert.Error(t, err)
// Test with valid config
ioResult2 := Provide[RawConfig, string](RawConfig{APIKey: "valid-key"})(outerEffect)
ioResult2 := Provide[string, RawConfig](RawConfig{APIKey: "valid-key"})(outerEffect)
readerResult2 := RunSync(ioResult2)
result, err2 := readerResult2(context.Background())
@@ -569,7 +569,7 @@ func TestLocalEffectK(t *testing.T) {
})
// Use Local for second transformation (pure)
local12 := Local[Level1, Level2, string](func(l1 Level1) Level2 {
local12 := Local[string, Level1, Level2](func(l1 Level1) Level2 {
return Level2{Data: l1.Value}
})
@@ -578,7 +578,7 @@ func TestLocalEffectK(t *testing.T) {
effect1 := local12(effect2)
// Run
ioResult := Provide[Level1, string](Level1{Value: "test"})(effect1)
ioResult := Provide[string, Level1](Level1{Value: "test"})(effect1)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -610,7 +610,7 @@ func TestLocalEffectK(t *testing.T) {
transform := LocalEffectK[int](complexTransform)
outerEffect := transform(innerEffect)
ioResult := Provide[OuterCtx, int](OuterCtx{Multiplier: 3})(outerEffect)
ioResult := Provide[int, OuterCtx](OuterCtx{Multiplier: 3})(outerEffect)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())

View File

@@ -1,51 +1,614 @@
// 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 effect
import (
thunk "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/context/readerreaderioresult"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
)
// FromThunk lifts a Thunk (context-independent IO computation with error handling) into an Effect.
// This allows you to integrate computations that don't need the effect's context type C
// into effect chains. The Thunk will be executed with the runtime context when the effect runs.
//
// # Type Parameters
//
// - C: The context type required by the effect (not used by the thunk)
// - A: The type of the success value
//
// # Parameters
//
// - f: A Thunk[A] that performs IO with error handling
//
// # Returns
//
// - Effect[C, A]: An effect that ignores its context and executes the thunk
//
// # Example
//
// thunk := func(ctx context.Context) io.IO[result.Result[int]] {
// return func() result.Result[int] {
// // Perform IO operation
// return result.Of(42)
// }
// }
//
// eff := effect.FromThunk[MyContext](thunk)
// // eff can be used in any context but executes the thunk
//
//go:inline
func FromThunk[C, A any](f Thunk[A]) Effect[C, A] {
return reader.Of[C](f)
}
//go:inline
func FromResult[C, A any](r Result[A]) Effect[C, A] {
return readerreaderioresult.FromEither[C](r)
}
// Succeed creates a successful Effect that produces the given value.
// This is the primary way to lift a pure value into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - a: The value to wrap in a successful effect
//
// # Returns
//
// - Effect[C, A]: An effect that always succeeds with the given value
//
// # Example
//
// eff := effect.Succeed[MyContext](42)
// result, err := runEffect(eff, myContext)
// // result == 42, err == nil
func Succeed[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
// Fail creates a failed Effect with the given error.
// This is used to represent computations that have failed.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value (never produced)
//
// # Parameters
//
// - err: The error that caused the failure
//
// # Returns
//
// - Effect[C, A]: An effect that always fails with the given error
//
// # Example
//
// eff := effect.Fail[MyContext, int](errors.New("failed"))
// _, err := runEffect(eff, myContext)
// // err == errors.New("failed")
func Fail[C, A any](err error) Effect[C, A] {
return readerreaderioresult.Left[C, A](err)
}
// Of creates a successful Effect that produces the given value.
// This is an alias for Succeed and follows the pointed functor convention.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - a: The value to wrap in a successful effect
//
// # Returns
//
// - Effect[C, A]: An effect that always succeeds with the given value
//
// # Example
//
// eff := effect.Of[MyContext]("hello")
// result, err := runEffect(eff, myContext)
// // result == "hello", err == nil
func Of[C, A any](a A) Effect[C, A] {
return readerreaderioresult.Of[C](a)
}
// Map transforms the success value of an Effect using the provided function.
// If the effect fails, the error is propagated unchanged.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: The transformation function to apply to the success value
//
// # Returns
//
// - Operator[C, A, B]: A function that transforms Effect[C, A] to Effect[C, B]
//
// # Example
//
// eff := effect.Of[MyContext](42)
// mapped := effect.Map[MyContext](func(x int) string {
// return strconv.Itoa(x)
// })(eff)
// // mapped produces "42"
func Map[C, A, B any](f func(A) B) Operator[C, A, B] {
return readerreaderioresult.Map[C](f)
}
// Chain sequences two effects, where the second effect depends on the result of the first.
// This is the monadic bind operation (flatMap) for effects.
// If the first effect fails, the second is not executed.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes the result of the first effect and returns a new effect
//
// # Returns
//
// - Operator[C, A, B]: A function that transforms Effect[C, A] to Effect[C, B]
//
// # Example
//
// eff := effect.Of[MyContext](42)
// chained := effect.Chain[MyContext](func(x int) Effect[MyContext, string] {
// return effect.Of[MyContext](strconv.Itoa(x * 2))
// })(eff)
// // chained produces "84"
//
//go:inline
func Chain[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.Chain(f)
}
//go:inline
func ChainFirst[C, A, B any](f Kleisli[C, A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirst(f)
}
// ChainIOK chains an effect with a function that returns an IO action.
// This is useful for integrating IO-based computations (synchronous side effects)
// into effect chains. The IO action is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns IO[B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the IO-returning function with the effect
//
// # Example
//
// performIO := func(n int) io.IO[string] {
// return func() string {
// // Perform synchronous side effect
// return fmt.Sprintf("Value: %d", n)
// }
// }
//
// eff := effect.Of[MyContext](42)
// chained := effect.ChainIOK[MyContext](performIO)(eff)
// // chained produces "Value: 42"
//
//go:inline
func ChainIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainIOK[C](f)
}
// ChainFirstIOK chains an effect with a function that returns an IO action,
// but discards the result and returns the original value.
// This is useful for performing side effects (like logging) without changing the value.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the IO action (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns IO[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
//
// # Example
//
// logValue := func(n int) io.IO[any] {
// return func() any {
// fmt.Printf("Processing: %d\n", n)
// return nil
// }
// }
//
// eff := effect.Of[MyContext](42)
// logged := effect.ChainFirstIOK[MyContext](logValue)(eff)
// // Prints "Processing: 42" but still produces 42
//
//go:inline
func ChainFirstIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirstIOK[C](f)
}
// TapIOK is an alias for ChainFirstIOK.
// It chains an effect with a function that returns an IO action for side effects,
// but preserves the original value. This is useful for logging, debugging, or
// performing actions without changing the result.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The value type (preserved)
// - B: The type produced by the IO action (discarded)
//
// # Parameters
//
// - f: A function that takes A and returns IO[B] for side effects
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the IO action but preserves the original value
//
// # Example
//
// logValue := func(n int) io.IO[any] {
// return func() any {
// fmt.Printf("Value: %d\n", n)
// return nil
// }
// }
//
// eff := effect.Of[MyContext](42)
// tapped := effect.TapIOK[MyContext](logValue)(eff)
// // Prints "Value: 42" but still produces 42
//
//go:inline
func TapIOK[C, A, B any](f io.Kleisli[A, B]) Operator[C, A, A] {
return readerreaderioresult.ChainFirstIOK[C](f)
}
// Ap applies a function wrapped in an Effect to a value wrapped in an Effect.
// This is the applicative apply operation, useful for applying effects in parallel.
//
// # Type Parameters
//
// - B: The output value type
// - C: The context type required by the effects
// - A: The input value type
//
// # Parameters
//
// - fa: The effect containing the value to apply the function to
//
// # Returns
//
// - Operator[C, func(A) B, B]: A function that applies the function effect to the value effect
//
// # Example
//
// fnEff := effect.Of[MyContext](func(x int) int { return x * 2 })
// valEff := effect.Of[MyContext](21)
// result := effect.Ap[int](valEff)(fnEff)
// // result produces 42
func Ap[B, C, A any](fa Effect[C, A]) Operator[C, func(A) B, B] {
return readerreaderioresult.Ap[B](fa)
}
// Suspend delays the evaluation of an effect until it is run.
// This is useful for recursive effects or when you need lazy evaluation.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - fa: A lazy computation that produces an effect
//
// # Returns
//
// - Effect[C, A]: An effect that evaluates the lazy computation when run
//
// # Example
//
// var recursiveEff func(int) Effect[MyContext, int]
// recursiveEff = func(n int) Effect[MyContext, int] {
// if n <= 0 {
// return effect.Of[MyContext](0)
// }
// return effect.Suspend(func() Effect[MyContext, int] {
// return effect.Map[MyContext](func(x int) int {
// return x + n
// })(recursiveEff(n - 1))
// })
// }
func Suspend[C, A any](fa Lazy[Effect[C, A]]) Effect[C, A] {
return readerreaderioresult.Defer(fa)
}
// Tap executes a side effect for its effect, but returns the original value.
// This is useful for logging, debugging, or performing actions without changing the result.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The value type
// - ANY: The type produced by the side effect (ignored)
//
// # Parameters
//
// - f: A function that performs a side effect based on the value
//
// # Returns
//
// - Operator[C, A, A]: A function that executes the side effect but preserves the original value
//
// # Example
//
// eff := effect.Of[MyContext](42)
// tapped := effect.Tap[MyContext](func(x int) Effect[MyContext, any] {
// fmt.Println("Value:", x)
// return effect.Of[MyContext, any](nil)
// })(eff)
// // Prints "Value: 42" but still produces 42
func Tap[C, A, ANY any](f Kleisli[C, A, ANY]) Operator[C, A, A] {
return readerreaderioresult.Tap(f)
}
// Ternary creates a conditional effect based on a predicate.
// If the predicate returns true, onTrue is executed; otherwise, onFalse is executed.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - pred: A predicate function to test the input value
// - onTrue: The effect to execute if the predicate is true
// - onFalse: The effect to execute if the predicate is false
//
// # Returns
//
// - Kleisli[C, A, B]: A function that conditionally executes one of two effects
//
// # Example
//
// kleisli := effect.Ternary(
// func(x int) bool { return x > 10 },
// func(x int) Effect[MyContext, string] {
// return effect.Of[MyContext]("large")
// },
// func(x int) Effect[MyContext, string] {
// return effect.Of[MyContext]("small")
// },
// )
// result := kleisli(15) // produces "large"
func Ternary[C, A, B any](pred Predicate[A], onTrue, onFalse Kleisli[C, A, B]) Kleisli[C, A, B] {
return function.Ternary(pred, onTrue, onFalse)
}
// ChainResultK chains an effect with a function that returns a Result.
// This is useful for integrating Result-based computations into effect chains.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns Result[B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the Result-returning function with the effect
//
// # Example
//
// parseIntResult := result.Eitherize1(strconv.Atoi)
// eff := effect.Of[MyContext]("42")
// chained := effect.ChainResultK[MyContext](parseIntResult)(eff)
// // chained produces 42 as an int
//
//go:inline
func ChainResultK[C, A, B any](f result.Kleisli[A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainResultK[C](f)
}
// ChainReaderK chains an effect with a function that returns a Reader.
// This is useful for integrating Reader-based computations (pure context-dependent functions)
// into effect chains. The Reader is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns Reader[C, B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the Reader-returning function with the effect
//
// # Example
//
// type Config struct { Multiplier int }
//
// getMultiplied := func(n int) reader.Reader[Config, int] {
// return func(cfg Config) int {
// return n * cfg.Multiplier
// }
// }
//
// eff := effect.Of[Config](5)
// chained := effect.ChainReaderK[Config](getMultiplied)(eff)
// // With Config{Multiplier: 3}, produces 15
//
//go:inline
func ChainReaderK[C, A, B any](f reader.Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainReaderK(f)
}
// ChainThunkK chains an effect with a function that returns a Thunk.
// This is useful for integrating Thunk-based computations (context-independent IO with error handling)
// into effect chains. The Thunk is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns Thunk[B] (readerioresult.Kleisli[A, B])
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the Thunk-returning function with the effect
//
// # Example
//
// performIO := func(n int) readerioresult.ReaderIOResult[string] {
// return func(ctx context.Context) io.IO[result.Result[string]] {
// return func() result.Result[string] {
// // Perform IO operation that doesn't need effect context
// return result.Of(fmt.Sprintf("Processed: %d", n))
// }
// }
// }
//
// eff := effect.Of[MyContext](42)
// chained := effect.ChainThunkK[MyContext](performIO)(eff)
// // chained produces "Processed: 42"
//
//go:inline
func ChainThunkK[C, A, B any](f thunk.Kleisli[A, B]) Operator[C, A, B] {
return fromreader.ChainReaderK(
Chain[C, A, B],
FromThunk[C, B],
f,
)
}
// ChainReaderIOK chains an effect with a function that returns a ReaderIO.
// This is useful for integrating ReaderIO-based computations (context-dependent IO operations)
// into effect chains. The ReaderIO is automatically lifted into the Effect context.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: A function that takes A and returns ReaderIO[C, B]
//
// # Returns
//
// - Operator[C, A, B]: A function that chains the ReaderIO-returning function with the effect
//
// # Example
//
// type Config struct { LogPrefix string }
//
// logAndDouble := func(n int) readerio.ReaderIO[Config, int] {
// return func(cfg Config) io.IO[int] {
// return func() int {
// fmt.Printf("%s: %d\n", cfg.LogPrefix, n)
// return n * 2
// }
// }
// }
//
// eff := effect.Of[Config](21)
// chained := effect.ChainReaderIOK[Config](logAndDouble)(eff)
// // Logs "prefix: 21" and produces 42
//
//go:inline
func ChainReaderIOK[C, A, B any](f readerio.Kleisli[C, A, B]) Operator[C, A, B] {
return readerreaderioresult.ChainReaderIOK(f)
}
// Read provides a context to an effect, partially applying it.
// This converts an Effect[C, A] to a Thunk[A] by supplying the required context.
//
// # Type Parameters
//
// - A: The type of the success value
// - C: The context type
//
// # Parameters
//
// - c: The context to provide to the effect
//
// # Returns
//
// - func(Effect[C, A]) Thunk[A]: A function that converts an effect to a thunk
//
// # Example
//
// ctx := MyContext{Value: "test"}
// eff := effect.Of[MyContext](42)
// thunk := effect.Read[int](ctx)(eff)
// // thunk is now a Thunk[int] that can be run without context
//
//go:inline
func Read[A, C any](c C) func(Effect[C, A]) Thunk[A] {
return readerreaderioresult.Read[A](c)
}

View File

@@ -0,0 +1,649 @@
// 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 effect
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestSucceed tests the Succeed function
func TestSucceed_Success(t *testing.T) {
t.Run("creates successful effect with int", func(t *testing.T) {
eff := Succeed[TestConfig](42)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("creates successful effect with string", func(t *testing.T) {
eff := Succeed[TestConfig]("hello")
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("hello"), outcome)
})
t.Run("creates successful effect with zero value", func(t *testing.T) {
eff := Succeed[TestConfig](0)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(0), outcome)
})
}
// TestFail tests the Fail function
func TestFail_Failure(t *testing.T) {
t.Run("creates failed effect with error", func(t *testing.T) {
testErr := errors.New("test error")
eff := Fail[TestConfig, int](testErr)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("preserves error message", func(t *testing.T) {
testErr := errors.New("specific error message")
eff := Fail[TestConfig, string](testErr)
outcome := eff(testConfig)(context.Background())()
assert.True(t, result.IsLeft(outcome))
extractedErr := result.MonadFold(outcome,
F.Identity[error],
func(string) error { return nil },
)
assert.Equal(t, testErr, extractedErr)
})
}
// TestOf tests the Of function
func TestOf_Success(t *testing.T) {
t.Run("creates successful effect with value", func(t *testing.T) {
eff := Of[TestConfig](100)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(100), outcome)
})
t.Run("is equivalent to Succeed", func(t *testing.T) {
value := "test"
eff1 := Of[TestConfig](value)
eff2 := Succeed[TestConfig](value)
outcome1 := eff1(testConfig)(context.Background())()
outcome2 := eff2(testConfig)(context.Background())()
assert.Equal(t, outcome1, outcome2)
})
}
// TestMap tests the Map function
func TestMap_Success(t *testing.T) {
t.Run("transforms success value", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(84), outcome)
})
t.Run("transforms type", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Map[TestConfig](func(x int) string { return strconv.Itoa(x) }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("chains multiple maps", func(t *testing.T) {
eff := F.Pipe2(
Of[TestConfig](10),
Map[TestConfig](func(x int) int { return x + 5 }),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
}
func TestMap_Failure(t *testing.T) {
t.Run("propagates error unchanged", func(t *testing.T) {
testErr := errors.New("test error")
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Map[TestConfig](func(x int) int { return x * 2 }),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}
// TestChain tests the Chain function
func TestChain_Success(t *testing.T) {
t.Run("sequences two effects", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](42),
Chain[TestConfig](func(x int) Effect[TestConfig, string] {
return Of[TestConfig](strconv.Itoa(x))
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
t.Run("chains multiple effects", func(t *testing.T) {
eff := F.Pipe2(
Of[TestConfig](10),
Chain[TestConfig](func(x int) Effect[TestConfig, int] {
return Of[TestConfig](x + 5)
}),
Chain[TestConfig](func(x int) Effect[TestConfig, int] {
return Of[TestConfig](x * 2)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
}
func TestChain_Failure(t *testing.T) {
t.Run("propagates error from first effect", func(t *testing.T) {
testErr := errors.New("first error")
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Chain[TestConfig](func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("should not execute")
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
})
t.Run("propagates error from second effect", func(t *testing.T) {
testErr := errors.New("second error")
eff := F.Pipe1(
Of[TestConfig](42),
Chain[TestConfig](func(x int) Effect[TestConfig, string] {
return Fail[TestConfig, string](testErr)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
})
}
// TestChainIOK tests the ChainIOK function
func TestChainIOK_Success(t *testing.T) {
t.Run("chains with IO action", func(t *testing.T) {
counter := 0
eff := F.Pipe1(
Of[TestConfig](42),
ChainIOK[TestConfig](func(x int) io.IO[string] {
return func() string {
counter++
return fmt.Sprintf("Value: %d", x)
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("Value: 42"), outcome)
assert.Equal(t, 1, counter)
})
t.Run("chains multiple IO actions", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
ChainIOK[TestConfig](func(x int) io.IO[int] {
return func() int {
log = append(log, "first")
return x + 5
}
}),
ChainIOK[TestConfig](func(x int) io.IO[int] {
return func() int {
log = append(log, "second")
return x * 2
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(30), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestChainIOK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
ChainIOK[TestConfig](func(x int) io.IO[string] {
return func() string {
executed = true
return "should not execute"
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[string](testErr), outcome)
assert.False(t, executed)
})
}
// TestChainFirstIOK tests the ChainFirstIOK function
func TestChainFirstIOK_Success(t *testing.T) {
t.Run("executes IO but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, fmt.Sprintf("logged: %d", x))
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"logged: 42"}, log)
})
t.Run("chains multiple side effects", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, "first")
return nil
}
}),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, "second")
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(10), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestChainFirstIOK_Failure(t *testing.T) {
t.Run("propagates error without executing IO", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
executed = true
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestTapIOK tests the TapIOK function
func TestTapIOK_Success(t *testing.T) {
t.Run("executes IO but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log = append(log, fmt.Sprintf("tapped: %d", x))
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"tapped: 42"}, log)
})
t.Run("is equivalent to ChainFirstIOK", func(t *testing.T) {
log1 := []string{}
log2 := []string{}
eff1 := F.Pipe1(
Of[TestConfig](10),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log1 = append(log1, "tap")
return nil
}
}),
)
eff2 := F.Pipe1(
Of[TestConfig](10),
ChainFirstIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
log2 = append(log2, "tap")
return nil
}
}),
)
outcome1 := eff1(testConfig)(context.Background())()
outcome2 := eff2(testConfig)(context.Background())()
assert.Equal(t, outcome1, outcome2)
assert.Equal(t, log1, log2)
})
}
func TestTapIOK_Failure(t *testing.T) {
t.Run("propagates error without executing IO", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
TapIOK[TestConfig](func(x int) io.IO[any] {
return func() any {
executed = true
return nil
}
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestChainResultK tests the ChainResultK function
func TestChainResultK_Success(t *testing.T) {
t.Run("chains with Result-returning function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Of[TestConfig]("42"),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("chains multiple Result operations", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe2(
Of[TestConfig]("10"),
ChainResultK[TestConfig](parseIntResult),
ChainResultK[TestConfig](func(x int) result.Result[string] {
return result.Of(fmt.Sprintf("Value: %d", x*2))
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("Value: 20"), outcome)
})
}
func TestChainResultK_Failure(t *testing.T) {
t.Run("propagates error from previous effect", func(t *testing.T) {
testErr := errors.New("test error")
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Fail[TestConfig, string](testErr),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("propagates error from Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := F.Pipe1(
Of[TestConfig]("not a number"),
ChainResultK[TestConfig](parseIntResult),
)
outcome := eff(testConfig)(context.Background())()
assert.True(t, result.IsLeft(outcome))
})
}
// TestAp tests the Ap function
func TestAp_Success(t *testing.T) {
t.Run("applies function effect to value effect", func(t *testing.T) {
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
valEff := Of[TestConfig](21)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("applies function with different types", func(t *testing.T) {
fnEff := Of[TestConfig](func(x int) string { return strconv.Itoa(x) })
valEff := Of[TestConfig](42)
eff := Ap[string](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of("42"), outcome)
})
}
func TestAp_Failure(t *testing.T) {
t.Run("propagates error from function effect", func(t *testing.T) {
testErr := errors.New("function error")
fnEff := Fail[TestConfig, func(int) int](testErr)
valEff := Of[TestConfig](42)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
t.Run("propagates error from value effect", func(t *testing.T) {
testErr := errors.New("value error")
fnEff := Of[TestConfig](func(x int) int { return x * 2 })
valEff := Fail[TestConfig, int](testErr)
eff := Ap[int](valEff)(fnEff)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}
// TestSuspend tests the Suspend function
func TestSuspend_Success(t *testing.T) {
t.Run("delays evaluation of effect", func(t *testing.T) {
counter := 0
eff := Suspend(func() Effect[TestConfig, int] {
counter++
return Of[TestConfig](42)
})
assert.Equal(t, 0, counter, "should not evaluate immediately")
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, 1, counter, "should evaluate when run")
assert.Equal(t, result.Of(42), outcome)
})
t.Run("enables recursive effects", func(t *testing.T) {
var factorial func(int) Effect[TestConfig, int]
factorial = func(n int) Effect[TestConfig, int] {
if n <= 1 {
return Of[TestConfig](1)
}
return Suspend(func() Effect[TestConfig, int] {
return F.Pipe1(
factorial(n-1),
Map[TestConfig](func(x int) int { return x * n }),
)
})
}
outcome := factorial(5)(testConfig)(context.Background())()
assert.Equal(t, result.Of(120), outcome)
})
}
// TestTap tests the Tap function
func TestTap_Success(t *testing.T) {
t.Run("executes side effect but preserves value", func(t *testing.T) {
log := []string{}
eff := F.Pipe1(
Of[TestConfig](42),
Tap[TestConfig](func(x int) Effect[TestConfig, any] {
log = append(log, fmt.Sprintf("tapped: %d", x))
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(42), outcome)
assert.Equal(t, []string{"tapped: 42"}, log)
})
t.Run("chains multiple taps", func(t *testing.T) {
log := []string{}
eff := F.Pipe2(
Of[TestConfig](10),
Tap[TestConfig](func(x int) Effect[TestConfig, any] {
log = append(log, "first")
return Of[TestConfig, any](nil)
}),
Tap[TestConfig](func(x int) Effect[TestConfig, any] {
log = append(log, "second")
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Of(10), outcome)
assert.Equal(t, []string{"first", "second"}, log)
})
}
func TestTap_Failure(t *testing.T) {
t.Run("propagates error without executing tap", func(t *testing.T) {
testErr := errors.New("test error")
executed := false
eff := F.Pipe1(
Fail[TestConfig, int](testErr),
Tap[TestConfig](func(x int) Effect[TestConfig, any] {
executed = true
return Of[TestConfig, any](nil)
}),
)
outcome := eff(testConfig)(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
assert.False(t, executed)
})
}
// TestTernary tests the Ternary function
func TestTernary_Success(t *testing.T) {
t.Run("executes onTrue when predicate is true", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("large")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("small")
},
)
outcome := kleisli(15)(testConfig)(context.Background())()
assert.Equal(t, result.Of("large"), outcome)
})
t.Run("executes onFalse when predicate is false", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x > 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("large")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("small")
},
)
outcome := kleisli(5)(testConfig)(context.Background())()
assert.Equal(t, result.Of("small"), outcome)
})
t.Run("works with boundary value", func(t *testing.T) {
kleisli := Ternary(
func(x int) bool { return x >= 10 },
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("gte")
},
func(x int) Effect[TestConfig, string] {
return Of[TestConfig]("lt")
},
)
outcome := kleisli(10)(testConfig)(context.Background())()
assert.Equal(t, result.Of("gte"), outcome)
})
}
// TestRead tests the Read function
func TestRead_Success(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
eff := Of[TestConfig](42)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Of(42), outcome)
})
t.Run("converts effect to thunk", func(t *testing.T) {
eff := F.Pipe1(
Of[TestConfig](10),
Map[TestConfig](func(x int) int { return x * testConfig.Multiplier }),
)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Of(30), outcome)
})
t.Run("works with different contexts", func(t *testing.T) {
cfg1 := TestConfig{Multiplier: 2, Prefix: "A", DatabaseURL: ""}
cfg2 := TestConfig{Multiplier: 5, Prefix: "B", DatabaseURL: ""}
// Create an effect that uses the context's Multiplier
eff := F.Pipe1(
Of[TestConfig](10),
ChainReaderK[TestConfig](func(x int) reader.Reader[TestConfig, int] {
return func(cfg TestConfig) int {
return x * cfg.Multiplier
}
}),
)
thunk1 := Read[int](cfg1)(eff)
thunk2 := Read[int](cfg2)(eff)
outcome1 := thunk1(context.Background())()
outcome2 := thunk2(context.Background())()
assert.Equal(t, result.Of(20), outcome1)
assert.Equal(t, result.Of(50), outcome2)
})
}
func TestRead_Failure(t *testing.T) {
t.Run("propagates error from effect", func(t *testing.T) {
testErr := errors.New("test error")
eff := Fail[TestConfig, int](testErr)
thunk := Read[int](testConfig)(eff)
outcome := thunk(context.Background())()
assert.Equal(t, result.Left[int](testErr), outcome)
})
}

View File

@@ -0,0 +1,213 @@
// 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 effect
import (
"context"
"errors"
"strconv"
"testing"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
func TestRead(t *testing.T) {
t.Run("provides context to effect", func(t *testing.T) {
ctx := TestContext{Value: "test-context"}
eff := Of[TestContext](42)
thunk := Read[int](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsRight(res))
value, err := result.Unwrap(res)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("provides context to failing effect", func(t *testing.T) {
expectedErr := errors.New("read error")
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
thunk := Read[string](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsLeft(res))
_, err := result.Unwrap(res)
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("provides context to chained effects", func(t *testing.T) {
ctx := TestContext{Value: "base"}
eff := Chain(func(x int) Effect[TestContext, string] {
return Of[TestContext](strconv.Itoa(x * 2))
})(Of[TestContext](21))
thunk := Read[string](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsRight(res))
value, err := result.Unwrap(res)
assert.NoError(t, err)
assert.Equal(t, "42", value)
})
t.Run("works with different context types", func(t *testing.T) {
type CustomContext struct {
ID int
Name string
}
ctx := CustomContext{ID: 100, Name: "custom"}
eff := Of[CustomContext]("result")
thunk := Read[string](ctx)(eff)
ioResult := thunk(context.Background())
res := ioResult()
assert.True(t, result.IsRight(res))
value, err := result.Unwrap(res)
assert.NoError(t, err)
assert.Equal(t, "result", value)
})
t.Run("can be composed with RunSync", func(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](100)
thunk := Read[int](ctx)(eff)
readerResult := RunSync(thunk)
value, err := readerResult(context.Background())
assert.NoError(t, err)
assert.Equal(t, 100, value)
})
}
func TestChainResultK(t *testing.T) {
t.Run("chains successful Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Of[TestContext]("42")
chained := ChainResultK[TestContext](parseIntResult)(eff)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("chains failing Result function", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Of[TestContext]("not-a-number")
chained := ChainResultK[TestContext](parseIntResult)(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid syntax")
})
t.Run("propagates error from original effect", func(t *testing.T) {
expectedErr := errors.New("original error")
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Fail[TestContext, string](expectedErr)
chained := ChainResultK[TestContext](parseIntResult)(eff)
_, err := runEffect(chained, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
})
t.Run("chains multiple Result functions", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
formatResult := func(x int) result.Result[string] {
return result.Of("value: " + strconv.Itoa(x))
}
eff := Of[TestContext]("42")
chained := ChainResultK[TestContext](formatResult)(
ChainResultK[TestContext](parseIntResult)(eff),
)
result, err := runEffect(chained, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "value: 42", result)
})
t.Run("integrates with other effect operations", func(t *testing.T) {
parseIntResult := result.Eitherize1(strconv.Atoi)
eff := Map[TestContext](func(x int) string {
return "final: " + strconv.Itoa(x)
})(ChainResultK[TestContext](parseIntResult)(Of[TestContext]("100")))
result, err := runEffect(eff, TestContext{Value: "test"})
assert.NoError(t, err)
assert.Equal(t, "final: 100", result)
})
t.Run("works with custom Result functions", func(t *testing.T) {
validatePositive := func(x int) result.Result[int] {
if x > 0 {
return result.Of(x)
}
return result.Left[int](errors.New("must be positive"))
}
parseIntResult := result.Eitherize1(strconv.Atoi)
// Test with positive number
eff1 := ChainResultK[TestContext](validatePositive)(
ChainResultK[TestContext](parseIntResult)(Of[TestContext]("42")),
)
result1, err1 := runEffect(eff1, TestContext{Value: "test"})
assert.NoError(t, err1)
assert.Equal(t, 42, result1)
// Test with negative number
eff2 := ChainResultK[TestContext](validatePositive)(
ChainResultK[TestContext](parseIntResult)(Of[TestContext]("-5")),
)
_, err2 := runEffect(eff2, TestContext{Value: "test"})
assert.Error(t, err2)
assert.Contains(t, err2.Error(), "must be positive")
})
t.Run("preserves error context", func(t *testing.T) {
customError := errors.New("custom validation error")
validateFunc := func(s string) result.Result[string] {
if len(s) > 0 {
return result.Of(s)
}
return result.Left[string](customError)
}
eff := ChainResultK[TestContext](validateFunc)(Of[TestContext](""))
_, err := runEffect(eff, TestContext{Value: "test"})
assert.Error(t, err)
assert.Equal(t, customError, err)
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,18 @@
// 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 effect
import (
@@ -5,10 +20,66 @@ import (
"github.com/IBM/fp-go/v2/monoid"
)
// ApplicativeMonoid creates a monoid for effects using applicative semantics.
// This combines effects by running both and combining their results using the provided monoid.
// If either effect fails, the combined effect fails.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The value type that has a monoid instance
//
// # Parameters
//
// - m: The monoid instance for combining values of type A
//
// # Returns
//
// - Monoid[Effect[C, A]]: A monoid for combining effects
//
// # Example
//
// stringMonoid := monoid.MakeMonoid(
// func(a, b string) string { return a + b },
// "",
// )
// effectMonoid := effect.ApplicativeMonoid[MyContext](stringMonoid)
// eff1 := effect.Of[MyContext]("Hello")
// eff2 := effect.Of[MyContext](" World")
// combined := effectMonoid.Concat(eff1, eff2)
// // combined produces "Hello World"
func ApplicativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.ApplicativeMonoid[C](m)
}
// AlternativeMonoid creates a monoid for effects using alternative semantics.
// This tries the first effect, and if it fails, tries the second effect.
// If both succeed, their results are combined using the provided monoid.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The value type that has a monoid instance
//
// # Parameters
//
// - m: The monoid instance for combining values of type A
//
// # Returns
//
// - Monoid[Effect[C, A]]: A monoid for combining effects with fallback behavior
//
// # Example
//
// stringMonoid := monoid.MakeMonoid(
// func(a, b string) string { return a + b },
// "",
// )
// effectMonoid := effect.AlternativeMonoid[MyContext](stringMonoid)
// eff1 := effect.Fail[MyContext, string](errors.New("failed"))
// eff2 := effect.Of[MyContext]("fallback")
// combined := effectMonoid.Concat(eff1, eff2)
// // combined produces "fallback" (first failed, so second is used)
func AlternativeMonoid[C, A any](m monoid.Monoid[A]) Monoid[Effect[C, A]] {
return readerreaderioresult.AlternativeMonoid[C](m)
}

View File

@@ -1,3 +1,18 @@
// 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 effect
import (
@@ -5,6 +20,39 @@ import (
"github.com/IBM/fp-go/v2/retry"
)
// Retrying executes an effect with retry logic based on a policy and check predicate.
// The effect is retried according to the policy until either:
// - The effect succeeds and the check predicate returns false
// - The retry policy is exhausted
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - policy: The retry policy defining retry limits and delays
// - action: An effectful computation that receives retry status and produces a value
// - check: A predicate that determines if the result should trigger a retry
//
// # Returns
//
// - Effect[C, A]: An effect that retries according to the policy
//
// # Example
//
// policy := retry.LimitRetries(3)
// eff := effect.Retrying[MyContext, string](
// policy,
// func(status retry.RetryStatus) Effect[MyContext, string] {
// return fetchData() // may fail
// },
// func(result Result[string]) bool {
// return result.IsLeft() // retry on error
// },
// )
// // Retries up to 3 times if fetchData fails
func Retrying[C, A any](
policy retry.RetryPolicy,
action Kleisli[C, retry.RetryStatus, A],

View File

@@ -1,3 +1,18 @@
// 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 effect
import (
@@ -8,10 +23,64 @@ import (
"github.com/IBM/fp-go/v2/result"
)
func Provide[C, A any](c C) func(Effect[C, A]) ReaderIOResult[A] {
// Provide supplies a context to an effect, converting it to a Thunk.
// This is the first step in running an effect - it eliminates the context dependency
// by providing the required context value.
//
// # Type Parameters
//
// - C: The context type required by the effect
// - A: The type of the success value
//
// # Parameters
//
// - c: The context value to provide to the effect
//
// # Returns
//
// - func(Effect[C, A]) ReaderIOResult[A]: A function that converts an effect to a thunk
//
// # Example
//
// ctx := MyContext{APIKey: "secret"}
// eff := effect.Of[MyContext](42)
// thunk := effect.Provide[MyContext, int](ctx)(eff)
// // thunk is now a ReaderIOResult[int] that can be run
func Provide[A, C any](c C) func(Effect[C, A]) ReaderIOResult[A] {
return readerreaderioresult.Read[A](c)
}
// RunSync executes a Thunk synchronously, converting it to a standard Go function.
// This is the final step in running an effect - it executes the IO operations
// and returns the result as a standard (value, error) tuple.
//
// # Type Parameters
//
// - A: The type of the success value
//
// # Parameters
//
// - fa: The thunk to execute
//
// # Returns
//
// - readerresult.ReaderResult[A]: A function that takes a context.Context and returns (A, error)
//
// # Example
//
// ctx := MyContext{APIKey: "secret"}
// eff := effect.Of[MyContext](42)
// thunk := effect.Provide[MyContext, int](ctx)(eff)
// readerResult := effect.RunSync(thunk)
// value, err := readerResult(context.Background())
// // value == 42, err == nil
//
// # Complete Example
//
// // Typical usage pattern:
// result, err := effect.RunSync(
// effect.Provide[MyContext, string](myContext)(myEffect),
// )(context.Background())
func RunSync[A any](fa ReaderIOResult[A]) readerresult.ReaderResult[A] {
return func(ctx context.Context) (A, error) {
return result.Unwrap(fa(ctx)())

View File

@@ -28,7 +28,7 @@ func TestProvide(t *testing.T) {
ctx := TestContext{Value: "test-value"}
eff := Of[TestContext]("result")
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -45,7 +45,7 @@ func TestProvide(t *testing.T) {
cfg := Config{Host: "localhost", Port: 8080}
eff := Of[Config]("connected")
ioResult := Provide[Config, string](cfg)(eff)
ioResult := Provide[string, Config](cfg)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -58,7 +58,7 @@ func TestProvide(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, string](expectedErr)
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -74,7 +74,7 @@ func TestProvide(t *testing.T) {
ctx := SimpleContext{ID: 42}
eff := Of[SimpleContext](100)
ioResult := Provide[SimpleContext, int](ctx)(eff)
ioResult := Provide[int, SimpleContext](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -89,7 +89,7 @@ func TestProvide(t *testing.T) {
return Of[TestContext]("result")
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -104,7 +104,7 @@ func TestProvide(t *testing.T) {
return "mapped"
})(Of[TestContext](42))
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -118,7 +118,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -130,7 +130,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext]("hello")
ioResult := Provide[TestContext, string](ctx)(eff)
ioResult := Provide[string, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
bgCtx := context.Background()
@@ -145,7 +145,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Fail[TestContext, int](expectedErr)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
_, err := readerResult(context.Background())
@@ -162,7 +162,7 @@ func TestRunSync(t *testing.T) {
return Of[TestContext](x + 10)
})(Of[TestContext](5)))
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -174,7 +174,7 @@ func TestRunSync(t *testing.T) {
ctx := TestContext{Value: "test"}
eff := Of[TestContext](42)
ioResult := Provide[TestContext, int](ctx)(eff)
ioResult := Provide[int, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
// Run multiple times
@@ -200,7 +200,7 @@ func TestRunSync(t *testing.T) {
user := User{Name: "Alice", Age: 30}
eff := Of[TestContext](user)
ioResult := Provide[TestContext, User](ctx)(eff)
ioResult := Provide[User, TestContext](ctx)(eff)
readerResult := RunSync(ioResult)
result, err := readerResult(context.Background())
@@ -222,7 +222,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
eff := Of[AppConfig]("API call successful")
// Provide config and run
result, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
result, err := RunSync(Provide[string, AppConfig](cfg)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "API call successful", result)
@@ -238,7 +238,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
eff := Fail[AppConfig, string](expectedErr)
_, err := RunSync(Provide[AppConfig, string](cfg)(eff))(context.Background())
_, err := RunSync(Provide[string, AppConfig](cfg)(eff))(context.Background())
assert.Error(t, err)
assert.Equal(t, expectedErr, err)
@@ -253,7 +253,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return Of[TestContext](x * 2)
})(Of[TestContext](21)))
result, err := RunSync(Provide[TestContext, string](ctx)(eff))(context.Background())
result, err := RunSync(Provide[string, TestContext](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "final", result)
@@ -281,7 +281,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return State{X: x}
})(Of[TestContext](10)))
result, err := RunSync(Provide[TestContext, State](ctx)(eff))(context.Background())
result, err := RunSync(Provide[State, TestContext](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, 10, result.X)
@@ -300,11 +300,11 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
innerEff := Of[InnerCtx]("inner result")
// Transform context
transformedEff := Local[OuterCtx, InnerCtx, string](func(outer OuterCtx) InnerCtx {
transformedEff := Local[string, OuterCtx, InnerCtx](func(outer OuterCtx) InnerCtx {
return InnerCtx{Data: outer.Value + "-transformed"}
})(innerEff)
result, err := RunSync(Provide[OuterCtx, string](outerCtx)(transformedEff))(context.Background())
result, err := RunSync(Provide[string, OuterCtx](outerCtx)(transformedEff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, "inner result", result)
@@ -318,7 +318,7 @@ func TestProvideAndRunSyncIntegration(t *testing.T) {
return Of[TestContext](x * 2)
})(input)
result, err := RunSync(Provide[TestContext, []int](ctx)(eff))(context.Background())
result, err := RunSync(Provide[[]int, TestContext](ctx)(eff))(context.Background())
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)

View File

@@ -1,7 +1,55 @@
// 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 effect
import "github.com/IBM/fp-go/v2/context/readerreaderioresult"
// TraverseArray applies an effectful function to each element of an array,
// collecting the results into a new array. If any effect fails, the entire
// traversal fails and returns the first error encountered.
//
// This is useful for performing effectful operations on collections while
// maintaining the sequential order of results.
//
// # Type Parameters
//
// - C: The context type required by the effects
// - A: The input element type
// - B: The output element type
//
// # Parameters
//
// - f: An effectful function to apply to each element
//
// # Returns
//
// - Kleisli[C, []A, []B]: A function that transforms an array of A to an effect producing an array of B
//
// # Example
//
// parseIntEff := func(s string) Effect[MyContext, int] {
// val, err := strconv.Atoi(s)
// if err != nil {
// return effect.Fail[MyContext, int](err)
// }
// return effect.Of[MyContext](val)
// }
// input := []string{"1", "2", "3"}
// eff := effect.TraverseArray[MyContext](parseIntEff)(input)
// // eff produces []int{1, 2, 3}
func TraverseArray[C, A, B any](f Kleisli[C, A, B]) Kleisli[C, []A, []B] {
return readerreaderioresult.TraverseArray(f)
}

View File

@@ -1,3 +1,18 @@
// 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 effect
import (
@@ -17,21 +32,61 @@ import (
)
type (
Either[E, A any] = either.Either[E, A]
Reader[R, A any] = reader.Reader[R, A]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
IO[A any] = io.IO[A]
IOEither[E, A any] = ioeither.IOEither[E, A]
Lazy[A any] = lazy.Lazy[A]
IOResult[A any] = ioresult.IOResult[A]
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
Monoid[A any] = monoid.Monoid[A]
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
Thunk[A any] = ReaderIOResult[A]
Predicate[A any] = predicate.Predicate[A]
Result[A any] = result.Result[A]
Lens[S, T any] = lens.Lens[S, T]
// Either represents a value that can be either a Left (error) or Right (success).
Either[E, A any] = either.Either[E, A]
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
// Reader represents a computation that depends on a context R and produces a value A.
Reader[R, A any] = reader.Reader[R, A]
// ReaderIO represents a computation that depends on a context R and produces an IO action returning A.
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// IO represents a synchronous side effect that produces a value A.
IO[A any] = io.IO[A]
// IOEither represents a synchronous side effect that can fail with error E or succeed with value A.
IOEither[E, A any] = ioeither.IOEither[E, A]
// Lazy represents a lazily evaluated computation that produces a value A.
Lazy[A any] = lazy.Lazy[A]
// IOResult represents a synchronous side effect that can fail with an error or succeed with value A.
IOResult[A any] = ioresult.IOResult[A]
// ReaderIOResult represents a computation that depends on context and performs IO with error handling.
ReaderIOResult[A any] = readerioresult.ReaderIOResult[A]
// Monoid represents an algebraic structure with an associative binary operation and an identity element.
Monoid[A any] = monoid.Monoid[A]
// Effect represents an effectful computation that:
// - Requires a context of type C
// - Can perform I/O operations
// - Can fail with an error
// - Produces a value of type A on success
//
// This is the core type of the effect package, providing a complete effect system
// for managing dependencies, errors, and side effects in a composable way.
Effect[C, A any] = readerreaderioresult.ReaderReaderIOResult[C, A]
// Thunk represents a computation that performs IO with error handling but doesn't require context.
// It's equivalent to ReaderIOResult and is used as an intermediate step when providing context to an Effect.
Thunk[A any] = ReaderIOResult[A]
// Predicate represents a function that tests a value of type A and returns a boolean.
Predicate[A any] = predicate.Predicate[A]
// Result represents a computation result that can be either an error (Left) or a success value (Right).
Result[A any] = result.Result[A]
// Lens represents an optic for focusing on a field T within a structure S.
Lens[S, T any] = lens.Lens[S, T]
// Kleisli represents a function from A to Effect[C, B], enabling monadic composition.
// It's the fundamental building block for chaining effectful computations.
Kleisli[C, A, B any] = readerreaderioresult.Kleisli[C, A, B]
// Operator represents a function that transforms Effect[C, A] to Effect[C, B].
// It's used for lifting operations over effects.
Operator[C, A, B any] = readerreaderioresult.Operator[C, A, B]
)

View File

@@ -480,5 +480,3 @@ func BenchmarkFromReaderResult_WithContext(b *testing.B) {
_ = validator(42)(ctx)
}
}
// Made with Bob

View File

@@ -37,11 +37,46 @@ import (
"github.com/IBM/fp-go/v2/readeroption"
)
// FromReaderOption converts a ReaderOption to a Kleisli arrow that handles None cases.
// When the Option is None, the provided lazy error value is used.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The value type
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Kleisli arrow that converts ReaderOption to ReaderIOEither
//
//go:inline
func FromReaderOption[R, A, E any](onNone Lazy[E]) Kleisli[R, E, ReaderOption[R, A], A] {
return function.Bind2nd(function.Flow2[ReaderOption[R, A], IOE.Kleisli[E, Option[A], A]], IOE.FromOption[A](onNone))
}
// FromReaderIO lifts a ReaderIO into a ReaderIOEither, placing the result in the Right side.
// This is an alias for RightReaderIO, converting a computation that cannot fail into one
// that can fail but never does.
//
// # Type Parameters
//
// - E: The error type (will never actually contain an error)
// - R: The context/environment type
// - A: The value type
//
// # Parameters
//
// - ma: The ReaderIO to lift
//
// # Returns
//
// - ReaderIOEither with the ReaderIO result in the Right side
//
//go:inline
func FromReaderIO[E, R, A any](ma ReaderIO[R, A]) ReaderIOEither[R, E, A] {
return RightReaderIO[E](ma)
@@ -121,6 +156,26 @@ func MonadChainFirst[R, E, A, B any](fa ReaderIOEither[R, E, A], f Kleisli[R, E,
f)
}
// MonadTap is an alias for MonadChainFirst.
// It sequences two computations but keeps the result of the first, emphasizing the
// side-effect nature of the operation (like "tapping" into a pipeline).
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - fa: The ReaderIOEither computation
// - f: The side effect Kleisli arrow
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTap[R, E, A, B any](fa ReaderIOEither[R, E, A], f Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirst(fa, f)
@@ -165,6 +220,26 @@ func MonadChainFirstEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f either
)
}
// MonadTapEitherK is an alias for MonadChainFirstEitherK.
// It chains an Either-returning computation while preserving the original value,
// emphasizing the side-effect nature of the operation.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The Either-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f either.Kleisli[E, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstEitherK(ma, f)
@@ -183,6 +258,25 @@ func ChainFirstEitherK[R, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, E,
)
}
// TapEitherK is an alias for ChainFirstEitherK.
// It returns a function that chains an Either-returning side effect while preserving
// the original value, emphasizing the "tap" pattern for observing values.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The Either-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapEitherK[R, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, E, A, A] {
return ChainFirstEitherK[R](f)
@@ -213,6 +307,26 @@ func ChainReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E, A, B
)
}
// MonadChainFirstReaderK chains a Reader-returning computation while preserving the original value.
// Useful for performing Reader-based side effects (like logging with context) while keeping
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The Reader-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadChainFirstReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -223,6 +337,25 @@ func MonadChainFirstReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader
)
}
// MonadTapReaderK is an alias for MonadChainFirstReaderK.
// It chains a Reader-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The Reader-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapReaderK[E, R, A, B any](ma ReaderIOEither[R, E, A], f reader.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstReaderK(ma, f)
@@ -240,11 +373,49 @@ func ChainFirstReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E,
)
}
// TapReaderK is an alias for ChainFirstReaderK.
// It returns a function that chains a Reader-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The Reader-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapReaderK[E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderK[E](f)
}
// MonadChainReaderIOK chains a ReaderIO-returning computation into a ReaderIOEither.
// The ReaderIO is automatically lifted into the ReaderIOEither context.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderIO-returning function
//
// # Returns
//
// - ReaderIOEither with the result of the ReaderIO computation
//
//go:inline
func MonadChainReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, B] {
return fromreader.MonadChainReaderK(
@@ -255,6 +426,24 @@ func MonadChainReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.
)
}
// ChainReaderIOK returns a function that chains a ReaderIO-returning function into ReaderIOEither.
// This is the curried version of MonadChainReaderIOK.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - f: The ReaderIO-returning function
//
// # Returns
//
// - Operator that chains the ReaderIO computation
//
//go:inline
func ChainReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, B] {
return fromreader.ChainReaderK(
@@ -264,6 +453,25 @@ func ChainReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E,
)
}
// MonadChainFirstReaderIOK chains a ReaderIO-returning computation while preserving the original value.
// Useful for performing ReaderIO-based side effects while keeping the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadChainFirstReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -274,11 +482,48 @@ func MonadChainFirstReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f read
)
}
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK.
// It chains a ReaderIO-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapReaderIOK[E, R, A, B any](ma ReaderIOEither[R, E, A], f readerio.Kleisli[R, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstReaderIOK(ma, f)
}
// ChainFirstReaderIOK returns a function that chains a ReaderIO-returning function while
// preserving the original value. This is the curried version of MonadChainFirstReaderIOK.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func ChainFirstReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, A] {
return fromreader.ChainFirstReaderK(
@@ -288,11 +533,49 @@ func ChainFirstReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R
)
}
// TapReaderIOK is an alias for ChainFirstReaderIOK.
// It returns a function that chains a ReaderIO-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The ReaderIO-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapReaderIOK[E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderIOK[E](f)
}
// MonadChainReaderEitherK chains a ReaderEither-returning computation into a ReaderIOEither.
// The ReaderEither is automatically lifted into the ReaderIOEither context.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type
// - B: The output value type
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderEither-returning function
//
// # Returns
//
// - ReaderIOEither with the result of the ReaderEither computation
//
//go:inline
func MonadChainReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, B] {
return fromreader.MonadChainReaderK(
@@ -315,6 +598,25 @@ func ChainReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, E,
)
}
// MonadChainFirstReaderEitherK chains a ReaderEither-returning computation while preserving the original value.
// Useful for performing ReaderEither-based side effects while keeping the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderEither-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadChainFirstReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -325,6 +627,25 @@ func MonadChainFirstReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f
)
}
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK.
// It chains a ReaderEither-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The ReaderEither-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapReaderEitherK[R, E, A, B any](ma ReaderIOEither[R, E, A], f RE.Kleisli[R, E, A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstReaderEitherK(ma, f)
@@ -342,11 +663,48 @@ func ChainFirstReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[
)
}
// TapReaderEitherK is an alias for ChainFirstReaderEitherK.
// It returns a function that chains a ReaderEither-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - E: The error type
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The ReaderEither-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapReaderEitherK[E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderEitherK(f)
}
// ChainReaderOptionK returns a function that chains a ReaderOption-returning function into ReaderIOEither.
// When the Option is None, the provided error value is used.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The input value type
// - B: The output value type
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Function that takes a ReaderOption Kleisli and returns an Operator
//
//go:inline
func ChainReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, B] {
fro := FromReaderOption[R, B](onNone)
@@ -359,6 +717,24 @@ func ChainReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisl
}
}
// ChainFirstReaderOptionK returns a function that chains a ReaderOption-returning function
// while preserving the original value. When the Option is None, the provided error value is used.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Function that takes a ReaderOption Kleisli and returns an Operator
//
//go:inline
func ChainFirstReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
fro := FromReaderOption[R, B](onNone)
@@ -371,6 +747,25 @@ func ChainFirstReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.K
}
}
// TapReaderOptionK is an alias for ChainFirstReaderOptionK.
// It returns a function that chains a ReaderOption-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
// - E: The error type
//
// # Parameters
//
// - onNone: Lazy function that provides the error value when Option is None
//
// # Returns
//
// - Function that takes a ReaderOption Kleisli and returns an Operator
//
//go:inline
func TapReaderOptionK[R, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, E, A, A] {
return ChainFirstReaderOptionK[R, A, B](onNone)
@@ -401,6 +796,110 @@ func ChainIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, B]
)
}
// ChainFirstIOEitherK chains an IOEither computation while preserving the original value.
// This is useful for performing side effects that may fail (like logging, validation, or
// external API calls) while keeping the original value in the success case.
//
// The function executes the IOEither computation but discards its result, returning the
// original value if both computations succeed. If either computation fails, the error
// is propagated.
//
// This is the curried version that returns an Operator for use in function composition.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: IOEither.Kleisli function that performs the side effect
//
// # Returns
//
// - Operator that chains the side effect while preserving the original value
//
// # Example Usage
//
// type Config struct{ LogEnabled bool }
//
// logValue := func(v int) IOEither[error, string] {
// return IOE.Of[error](fmt.Sprintf("Value: %d", v))
// }
//
// pipeline := F.Pipe1(
// Of[Config, error](42),
// ChainFirstIOEitherK[Config](logValue),
// )
// result := pipeline(Config{LogEnabled: true})() // Right(42)
//
// # See Also
//
// - TapIOEitherK: Alias for ChainFirstIOEitherK
// - ChainIOEitherK: Chains IOEither and uses its result
// - ChainFirstEitherK: Similar but for Either computations
//
//go:inline
func ChainFirstIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, A] {
return fromioeither.ChainFirstIOEitherK(
Chain[R, E, A, A],
Map[R, E, B, A],
FromIOEither[R, E, B],
f,
)
}
// TapIOEitherK is an alias for ChainFirstIOEitherK.
// It executes an IOEither side effect while preserving the original value.
//
// The name "Tap" emphasizes the side-effect nature of the operation, similar to
// tapping into a pipeline to observe or log values without modifying the flow.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: IOEither.Kleisli function that performs the side effect
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
// # Example Usage
//
// type Config struct{}
//
// validatePositive := func(v int) IOEither[error, bool] {
// if v > 0 {
// return IOE.Of[error](true)
// }
// return IOE.Left[bool](errors.New("must be positive"))
// }
//
// pipeline := F.Pipe1(
// Of[Config, error](42),
// TapIOEitherK[Config](validatePositive),
// )
// result := pipeline(Config{})() // Right(42) if validation passes
//
// # See Also
//
// - ChainFirstIOEitherK: The underlying implementation
// - TapEitherK: Similar but for Either computations
// - TapIOK: Similar but for IO computations
//
//go:inline
func TapIOEitherK[R, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, E, A, A] {
return ChainFirstIOEitherK[R](f)
}
// MonadChainIOK chains an IO-returning computation into a ReaderIOEither.
// The IO is automatically lifted into the ReaderIOEither context (always succeeds).
//
@@ -440,6 +939,25 @@ func MonadChainFirstIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f io.Kleisli
)
}
// MonadTapIOK is an alias for MonadChainFirstIOK.
// It chains an IO-returning side effect while preserving the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - ma: The ReaderIOEither computation
// - f: The IO-returning side effect function
//
// # Returns
//
// - ReaderIOEither with the original value preserved
//
//go:inline
func MonadTapIOK[R, E, A, B any](ma ReaderIOEither[R, E, A], f io.Kleisli[A, B]) ReaderIOEither[R, E, A] {
return MonadChainFirstIOK(ma, f)
@@ -458,6 +976,25 @@ func ChainFirstIOK[R, E, A, B any](f io.Kleisli[A, B]) Operator[R, E, A, A] {
)
}
// TapIOK is an alias for ChainFirstIOK.
// It returns a function that chains an IO-returning side effect while preserving
// the original value.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The IO-returning side effect function
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func TapIOK[R, E, A, B any](f io.Kleisli[A, B]) Operator[R, E, A, A] {
return ChainFirstIOK[R, E](f)
@@ -540,6 +1077,25 @@ func ChainFirst[R, E, A, B any](f Kleisli[R, E, A, B]) Operator[R, E, A, A] {
f)
}
// Tap is an alias for ChainFirst.
// It returns a function that sequences computations but keeps the first result,
// emphasizing the side-effect nature of the operation.
//
// # Type Parameters
//
// - R: The context/environment type
// - E: The error type
// - A: The input value type (preserved in output)
// - B: The side effect result type (discarded)
//
// # Parameters
//
// - f: The Kleisli arrow for the side effect
//
// # Returns
//
// - Operator that executes the side effect while preserving the original value
//
//go:inline
func Tap[R, E, A, B any](f Kleisli[R, E, A, B]) Operator[R, E, A, A] {
return ChainFirst(f)

View File

@@ -733,3 +733,390 @@ func TestChainLeftIdenticalToOrElse(t *testing.T) {
assert.Equal(t, cfg, *chainLeftCfg)
})
}
func TestChainFirstIOEitherK(t *testing.T) {
type Config struct {
logEnabled bool
}
t.Run("Success - preserves original value", func(t *testing.T) {
sideEffectRan := false
logValue := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
sideEffectRan = true
return E.Right[error](fmt.Sprintf("Logged: %d", v))
}
}
pipeline := F.Pipe1(
Of[Config, error](42),
ChainFirstIOEitherK[Config](logValue),
)
result := pipeline(Config{logEnabled: true})()
assert.Equal(t, E.Right[error](42), result)
assert.True(t, sideEffectRan)
})
t.Run("Success - side effect result is discarded", func(t *testing.T) {
sideEffect := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error]("side effect result")
}
pipeline := F.Pipe1(
Of[Config, error](100),
ChainFirstIOEitherK[Config](sideEffect),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](100), result)
})
t.Run("Failure - side effect fails", func(t *testing.T) {
sideEffectError := errors.New("side effect failed")
failingSideEffect := func(v int) IOE.IOEither[error, string] {
return IOE.Left[string](sideEffectError)
}
pipeline := F.Pipe1(
Of[Config, error](42),
ChainFirstIOEitherK[Config](failingSideEffect),
)
result := pipeline(Config{})()
assert.Equal(t, E.Left[int](sideEffectError), result)
})
t.Run("Failure - original computation fails", func(t *testing.T) {
originalError := errors.New("original failed")
sideEffectRan := false
sideEffect := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
sideEffectRan = true
return E.Right[error]("logged")
}
}
pipeline := F.Pipe1(
Left[Config, int](originalError),
ChainFirstIOEitherK[Config](sideEffect),
)
result := pipeline(Config{})()
assert.Equal(t, E.Left[int](originalError), result)
assert.False(t, sideEffectRan, "Side effect should not run when original computation fails")
})
t.Run("Chaining multiple side effects", func(t *testing.T) {
log1Ran := false
log2Ran := false
log1 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
log1Ran = true
return E.Right[error]("log1")
}
}
log2 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
log2Ran = true
return E.Right[error]("log2")
}
}
pipeline := F.Pipe2(
Of[Config, error](42),
ChainFirstIOEitherK[Config](log1),
ChainFirstIOEitherK[Config](log2),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](42), result)
assert.True(t, log1Ran)
assert.True(t, log2Ran)
})
t.Run("Integration with Map", func(t *testing.T) {
validate := func(v int) IOE.IOEither[error, bool] {
if v > 0 {
return IOE.Of[error](true)
}
return IOE.Left[bool](errors.New("must be positive"))
}
pipeline := F.Pipe2(
Of[Config, error](10),
ChainFirstIOEitherK[Config](validate),
Map[Config, error](func(x int) int { return x * 2 }),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](20), result)
})
t.Run("Different types for input and side effect result", func(t *testing.T) {
convertToString := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error](fmt.Sprintf("Value: %d", v))
}
pipeline := F.Pipe1(
Of[Config, error](123),
ChainFirstIOEitherK[Config](convertToString),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](123), result)
})
t.Run("With context-dependent side effect", func(t *testing.T) {
logged := ""
logIfEnabled := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
logged = fmt.Sprintf("Logged: %d", v)
return E.Right[error](logged)
}
}
pipeline := F.Pipe1(
Of[Config, error](99),
ChainFirstIOEitherK[Config](logIfEnabled),
)
result := pipeline(Config{logEnabled: true})()
assert.Equal(t, E.Right[error](99), result)
assert.Equal(t, "Logged: 99", logged)
})
}
func TestTapIOEitherK(t *testing.T) {
type Config struct {
debugMode bool
}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tapped = true
return E.Right[error](fmt.Sprintf("Tapped: %d", v))
}
}
pipeline := F.Pipe1(
Of[Config, error](42),
TapIOEitherK[Config](tapFunc),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](42), result)
assert.True(t, tapped)
})
t.Run("Failure - tap fails", func(t *testing.T) {
tapError := errors.New("tap failed")
tapFunc := func(v int) IOE.IOEither[error, string] {
return IOE.Left[string](tapError)
}
pipeline := F.Pipe1(
Of[Config, error](42),
TapIOEitherK[Config](tapFunc),
)
result := pipeline(Config{})()
assert.Equal(t, E.Left[int](tapError), result)
})
t.Run("Failure - original computation fails", func(t *testing.T) {
originalError := errors.New("original failed")
tapped := false
tapFunc := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tapped = true
return E.Right[error]("tapped")
}
}
pipeline := F.Pipe1(
Left[Config, int](originalError),
TapIOEitherK[Config](tapFunc),
)
result := pipeline(Config{})()
assert.Equal(t, E.Left[int](originalError), result)
assert.False(t, tapped, "Tap should not run when original computation fails")
})
t.Run("Validation use case", func(t *testing.T) {
validatePositive := func(v int) IOE.IOEither[error, bool] {
if v > 0 {
return IOE.Of[error](true)
}
return IOE.Left[bool](errors.New("must be positive"))
}
pipeline := F.Pipe1(
Of[Config, error](42),
TapIOEitherK[Config](validatePositive),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](42), result)
})
t.Run("Validation failure", func(t *testing.T) {
validatePositive := func(v int) IOE.IOEither[error, bool] {
if v > 0 {
return IOE.Of[error](true)
}
return IOE.Left[bool](errors.New("must be positive"))
}
pipeline := F.Pipe1(
Of[Config, error](-5),
TapIOEitherK[Config](validatePositive),
)
result := pipeline(Config{})()
assert.True(t, E.IsLeft(result))
})
t.Run("Multiple taps in sequence", func(t *testing.T) {
tap1Ran := false
tap2Ran := false
tap3Ran := false
tap1 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tap1Ran = true
return E.Right[error]("tap1")
}
}
tap2 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tap2Ran = true
return E.Right[error]("tap2")
}
}
tap3 := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tap3Ran = true
return E.Right[error]("tap3")
}
}
pipeline := F.Pipe3(
Of[Config, error](100),
TapIOEitherK[Config](tap1),
TapIOEitherK[Config](tap2),
TapIOEitherK[Config](tap3),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](100), result)
assert.True(t, tap1Ran)
assert.True(t, tap2Ran)
assert.True(t, tap3Ran)
})
t.Run("Tap with transformation pipeline", func(t *testing.T) {
tappedValue := 0
tapFunc := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
tappedValue = v
return E.Right[error]("tapped")
}
}
pipeline := F.Pipe3(
Of[Config, error](10),
Map[Config, error](func(x int) int { return x * 2 }),
TapIOEitherK[Config](tapFunc),
Map[Config, error](func(x int) int { return x + 5 }),
)
result := pipeline(Config{})()
assert.Equal(t, E.Right[error](25), result)
assert.Equal(t, 20, tappedValue)
})
t.Run("Tap is alias for ChainFirstIOEitherK", func(t *testing.T) {
// Verify that TapIOEitherK and ChainFirstIOEitherK produce identical results
sideEffect := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error](fmt.Sprintf("Value: %d", v))
}
pipelineWithTap := F.Pipe1(
Of[Config, error](42),
TapIOEitherK[Config](sideEffect),
)
pipelineWithChainFirst := F.Pipe1(
Of[Config, error](42),
ChainFirstIOEitherK[Config](sideEffect),
)
resultTap := pipelineWithTap(Config{})()
resultChainFirst := pipelineWithChainFirst(Config{})()
assert.Equal(t, resultChainFirst, resultTap)
})
t.Run("Logging use case", func(t *testing.T) {
logs := []string{}
logValue := func(v int) IOE.IOEither[error, string] {
return func() E.Either[error, string] {
logMsg := fmt.Sprintf("Processing value: %d", v)
logs = append(logs, logMsg)
return E.Right[error](logMsg)
}
}
pipeline := F.Pipe2(
Of[Config, error](5),
TapIOEitherK[Config](logValue),
Map[Config, error](func(x int) int { return x * x }),
)
result := pipeline(Config{debugMode: true})()
assert.Equal(t, E.Right[error](25), result)
assert.Len(t, logs, 1)
assert.Equal(t, "Processing value: 5", logs[0])
})
t.Run("Error propagation in tap chain", func(t *testing.T) {
tap1 := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error]("tap1")
}
tap2 := func(v int) IOE.IOEither[error, string] {
return IOE.Left[string](errors.New("tap2 failed"))
}
tap3 := func(v int) IOE.IOEither[error, string] {
return IOE.Of[error]("tap3")
}
pipeline := F.Pipe3(
Of[Config, error](42),
TapIOEitherK[Config](tap1),
TapIOEitherK[Config](tap2),
TapIOEitherK[Config](tap3),
)
result := pipeline(Config{})()
assert.True(t, E.IsLeft(result))
})
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioresult"
"github.com/IBM/fp-go/v2/reader"
RE "github.com/IBM/fp-go/v2/readereither"
"github.com/IBM/fp-go/v2/readerio"
@@ -28,6 +29,9 @@ import (
"github.com/IBM/fp-go/v2/result"
)
// FromReaderOption converts a ReaderOption to a ReaderIOResult.
// If the ReaderOption is None, the provided function is called to produce the error.
//
//go:inline
func FromReaderOption[R, A any](onNone Lazy[error]) Kleisli[R, ReaderOption[R, A], A] {
return RIOE.FromReaderOption[R, A](onNone)
@@ -105,6 +109,9 @@ func MonadChainFirst[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) R
return RIOE.MonadChainFirst(fa, f)
}
// MonadTap is an alias for MonadChainFirst, executing a side effect while preserving the original value.
// The name "Tap" emphasizes the side-effect nature of the operation.
//
//go:inline
func MonadTap[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTap(fa, f)
@@ -150,6 +157,8 @@ func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleis
return RIOE.MonadChainFirstEitherK(ma, f)
}
// MonadTapEitherK is an alias for MonadChainFirstEitherK, executing a Result side effect while preserving the original value.
//
//go:inline
func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
@@ -163,11 +172,43 @@ func ChainFirstEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
// TapEitherK is an alias for ChainFirstEitherK, executing a Result side effect while preserving the original value.
//
//go:inline
func TapEitherK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
}
// ChainFirstIOEitherK chains an IOResult computation while preserving the original value.
// Useful for performing side effects that may fail while keeping the original value.
//
//go:inline
func ChainFirstIOEitherK[R, A, B any](f ioresult.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstIOEitherK[R](f)
}
// TapIOEitherK is an alias for ChainFirstIOEitherK, executing an IOResult side effect while preserving the original value.
//
//go:inline
func TapIOEitherK[R, A, B any](f ioresult.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapIOEitherK[R](f)
}
// ChainFirstIOResultK chains an IOResult computation while preserving the original value.
// This is an alias for ChainFirstIOEitherK with more explicit naming.
//
//go:inline
func ChainFirstIOResultK[R, A, B any](f ioresult.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstIOEitherK[R](f)
}
// TapIOResultK is an alias for ChainFirstIOResultK, executing an IOResult side effect while preserving the original value.
//
//go:inline
func TapIOResultK[R, A, B any](f ioresult.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapIOEitherK[R](f)
}
// MonadChainFirstEitherK chains an Either-returning computation but keeps the original value.
// Useful for validation or side effects that return Either.
//
@@ -176,19 +217,23 @@ func MonadChainFirstResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleis
return RIOE.MonadChainFirstEitherK(ma, f)
}
// MonadTapResultK is an alias for MonadChainFirstResultK, executing a Result side effect while preserving the original value.
//
//go:inline
func MonadTapResultK[R, A, B any](ma ReaderIOResult[R, A], f result.Kleisli[A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapEitherK(ma, f)
}
// ChainFirstEitherK returns a function that chains an Either computation while preserving the original value.
// This is the curried version of MonadChainFirstEitherK.
// ChainFirstResultK returns a function that chains a Result computation while preserving the original value.
// This is an alias for ChainFirstEitherK with more explicit naming.
//
//go:inline
func ChainFirstResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.ChainFirstEitherK[R](f)
}
// TapResultK is an alias for ChainFirstResultK, executing a Result side effect while preserving the original value.
//
//go:inline
func TapResultK[R, A, B any](f result.Kleisli[A, B]) Operator[R, A, A] {
return RIOE.TapEitherK[R](f)
@@ -210,36 +255,54 @@ func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderK[error](f)
}
// MonadChainFirstReaderK chains a Reader computation but keeps the original value.
// Useful for performing Reader-based side effects while preserving the original value.
//
//go:inline
func MonadChainFirstReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderK(ma, f)
}
// MonadTapReaderK is an alias for MonadChainFirstReaderK, executing a Reader side effect while preserving the original value.
//
//go:inline
func MonadTapReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapReaderK(ma, f)
}
// ChainFirstReaderK returns a function that chains a Reader computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderK.
//
//go:inline
func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderK[error](f)
}
// TapReaderK is an alias for ChainFirstReaderK, executing a Reader side effect while preserving the original value.
//
//go:inline
func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.TapReaderK[error](f)
}
// ChainReaderOptionK returns a function that chains a ReaderOption-returning function into ReaderIOResult.
// If the ReaderOption is None, the provided error function is called.
//
//go:inline
func ChainReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderOptionK[R, A, B](onNone)
}
// ChainFirstReaderOptionK chains a ReaderOption computation while preserving the original value.
// If the ReaderOption is None, the provided error function is called.
//
//go:inline
func ChainFirstReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderOptionK[R, A, B](onNone)
}
// TapReaderOptionK is an alias for ChainFirstReaderOptionK, executing a ReaderOption side effect while preserving the original value.
//
//go:inline
func TapReaderOptionK[R, A, B any](onNone Lazy[error]) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.TapReaderOptionK[R, A, B](onNone)
@@ -261,84 +324,123 @@ func ChainReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A
return RIOE.ChainReaderEitherK(f)
}
// MonadChainFirstReaderEitherK chains a ReaderEither computation but keeps the original value.
// Useful for performing ReaderEither-based side effects while preserving the original value.
//
//go:inline
func MonadChainFirstReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderEitherK(ma, f)
}
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK, executing a ReaderEither side effect while preserving the original value.
//
//go:inline
func MonadTapReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapReaderEitherK(ma, f)
}
// ChainFirstReaderEitherK returns a function that chains a ReaderEither computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderEitherK.
//
//go:inline
func ChainFirstReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderEitherK(f)
}
// TapReaderEitherK is an alias for ChainFirstReaderEitherK, executing a ReaderEither side effect while preserving the original value.
//
//go:inline
func TapReaderEitherK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.TapReaderEitherK(f)
}
// MonadChainReaderResultK chains a ReaderResult-returning computation into a ReaderIOResult.
// This is an alias for MonadChainReaderEitherK with more explicit naming.
//
//go:inline
func MonadChainReaderResultK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainReaderEitherK(ma, f)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderK.
// ChainReaderResultK returns a function that chains a ReaderResult-returning function into ReaderIOResult.
// This is an alias for ChainReaderEitherK with more explicit naming.
//
//go:inline
func ChainReaderResultK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderEitherK(f)
}
// MonadChainFirstReaderResultK chains a ReaderResult computation but keeps the original value.
// This is an alias for MonadChainFirstReaderEitherK with more explicit naming.
//
//go:inline
func MonadChainFirstReaderResultK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderEitherK(ma, f)
}
// MonadTapReaderResultK is an alias for MonadChainFirstReaderResultK, executing a ReaderResult side effect while preserving the original value.
//
//go:inline
func MonadTapReaderResultK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, error, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapReaderEitherK(ma, f)
}
// ChainFirstReaderResultK returns a function that chains a ReaderResult computation while preserving the original value.
// This is an alias for ChainFirstReaderEitherK with more explicit naming.
//
//go:inline
func ChainFirstReaderResultK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderEitherK(f)
}
// TapReaderResultK is an alias for ChainFirstReaderResultK, executing a ReaderResult side effect while preserving the original value.
//
//go:inline
func TapReaderResultK[R, A, B any](f RE.Kleisli[R, error, A, B]) Operator[R, A, A] {
return RIOE.TapReaderEitherK(f)
}
// MonadChainReaderIOK chains a ReaderIO-returning computation into a ReaderIOResult.
// The ReaderIO is automatically lifted into the ReaderIOResult context (always succeeds).
//
//go:inline
func MonadChainReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, B] {
return RIOE.MonadChainReaderIOK(ma, f)
}
// ChainReaderIOK returns a function that chains a ReaderIO-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderIOK.
//
//go:inline
func ChainReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, B] {
return RIOE.ChainReaderIOK[error](f)
}
// MonadChainFirstReaderIOK chains a ReaderIO computation but keeps the original value.
// Useful for performing ReaderIO-based side effects while preserving the original value.
//
//go:inline
func MonadChainFirstReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstReaderIOK(ma, f)
}
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK, executing a ReaderIO side effect while preserving the original value.
//
//go:inline
func MonadTapReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapReaderIOK(ma, f)
}
// ChainFirstReaderIOK returns a function that chains a ReaderIO computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderIOK.
//
//go:inline
func ChainFirstReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirstReaderIOK[error](f)
}
// TapReaderIOK is an alias for ChainFirstReaderIOK, executing a ReaderIO side effect while preserving the original value.
//
//go:inline
func TapReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.TapReaderIOK[error](f)
@@ -400,6 +502,8 @@ func MonadChainFirstIOK[R, A, B any](ma ReaderIOResult[R, A], f func(A) IO[B]) R
return RIOE.MonadChainFirstIOK(ma, f)
}
// MonadTapIOK is an alias for MonadChainFirstIOK, executing an IO side effect while preserving the original value.
//
//go:inline
func MonadTapIOK[R, A, B any](ma ReaderIOResult[R, A], f func(A) IO[B]) ReaderIOResult[R, A] {
return RIOE.MonadTapIOK(ma, f)
@@ -413,6 +517,8 @@ func ChainFirstIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, A] {
return RIOE.ChainFirstIOK[R, error](f)
}
// TapIOK is an alias for ChainFirstIOK, executing an IO side effect while preserving the original value.
//
//go:inline
func TapIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, A] {
return RIOE.TapIOK[R, error](f)
@@ -473,6 +579,9 @@ func ChainFirst[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.ChainFirst(f)
}
// Tap is an alias for ChainFirst, executing a side effect while preserving the original value.
// The name "Tap" emphasizes the side-effect nature of the operation.
//
//go:inline
func Tap[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return RIOE.Tap(f)
@@ -767,46 +876,70 @@ func Local[A, R1, R2 any](f func(R2) R1) func(ReaderIOResult[R1, A]) ReaderIORes
return RIOE.Local[error, A](f)
}
// Read executes a ReaderIOResult by providing a concrete environment value.
// This converts a ReaderIOResult[R, A] into an IOResult[A] by supplying the R value.
//
//go:inline
func Read[A, R any](r R) func(ReaderIOResult[R, A]) IOResult[A] {
return RIOE.Read[error, A](r)
}
// MonadChainLeft chains a computation on the error channel, allowing error recovery or transformation.
// If the computation is successful (Right), it passes through unchanged.
//
//go:inline
func MonadChainLeft[R, A any](fa ReaderIOResult[R, A], f Kleisli[R, error, A]) ReaderIOResult[R, A] {
return RIOE.MonadChainLeft(fa, f)
}
// ChainLeft returns a function that chains a computation on the error channel.
// This is the curried version of MonadChainLeft.
//
//go:inline
func ChainLeft[R, A any](f Kleisli[R, error, A]) func(ReaderIOResult[R, A]) ReaderIOResult[R, A] {
return RIOE.ChainLeft(f)
}
// MonadChainFirstLeft chains an error-handling computation but preserves the original error.
// Useful for logging or side effects on errors without changing the error value.
//
//go:inline
func MonadChainFirstLeft[A, R, B any](ma ReaderIOResult[R, A], f Kleisli[R, error, B]) ReaderIOResult[R, A] {
return RIOE.MonadChainFirstLeft(ma, f)
}
// MonadTapLeft is an alias for MonadChainFirstLeft, executing a side effect on errors while preserving the original error.
//
//go:inline
func MonadTapLeft[A, R, B any](ma ReaderIOResult[R, A], f Kleisli[R, error, B]) ReaderIOResult[R, A] {
return RIOE.MonadTapLeft(ma, f)
}
// ChainFirstLeft returns a function that chains an error-handling computation while preserving the original error.
// This is the curried version of MonadChainFirstLeft.
//
//go:inline
func ChainFirstLeft[A, R, B any](f Kleisli[R, error, B]) Operator[R, A, A] {
return RIOE.ChainFirstLeft[A](f)
}
// ChainFirstLeftIOK chains an IO computation on errors while preserving the original error.
// Useful for IO-based error logging or side effects.
//
//go:inline
func ChainFirstLeftIOK[A, R, B any](f io.Kleisli[error, B]) Operator[R, A, A] {
return RIOE.ChainFirstLeftIOK[A, R](f)
}
// TapLeft is an alias for ChainFirstLeft, executing a side effect on errors while preserving the original error.
//
//go:inline
func TapLeft[A, R, B any](f Kleisli[R, error, B]) Operator[R, A, A] {
return RIOE.TapLeft[A](f)
}
// TapLeftIOK is an alias for ChainFirstLeftIOK, executing an IO side effect on errors while preserving the original error.
//
//go:inline
func TapLeftIOK[A, R, B any](f io.Kleisli[error, B]) Operator[R, A, A] {
return RIOE.TapLeftIOK[A, R](f)

View File

@@ -619,3 +619,453 @@ func TestLocalIOResultK(t *testing.T) {
assert.True(t, result.IsLeft(resErr))
})
}
func TestChainFirstIOResultK(t *testing.T) {
type Config struct {
logEnabled bool
}
t.Run("Success - preserves original value", func(t *testing.T) {
sideEffectRan := false
logValue := func(v int) IOResult[string] {
return func() Result[string] {
sideEffectRan = true
return result.Of(fmt.Sprintf("Logged: %d", v))
}
}
pipeline := F.Pipe1(
Of[Config](42),
ChainFirstIOResultK[Config](logValue),
)
res := pipeline(Config{logEnabled: true})()
assert.Equal(t, result.Of(42), res)
assert.True(t, sideEffectRan)
})
t.Run("Failure - side effect fails", func(t *testing.T) {
sideEffectError := errors.New("side effect failed")
failingSideEffect := func(v int) IOResult[string] {
return func() Result[string] {
return result.Left[string](sideEffectError)
}
}
pipeline := F.Pipe1(
Of[Config](42),
ChainFirstIOResultK[Config](failingSideEffect),
)
res := pipeline(Config{})()
assert.True(t, E.IsLeft(res))
})
t.Run("Failure - original computation fails", func(t *testing.T) {
originalError := errors.New("original failed")
sideEffectRan := false
sideEffect := func(v int) IOResult[string] {
return func() Result[string] {
sideEffectRan = true
return result.Of("logged")
}
}
pipeline := F.Pipe1(
Left[Config, int](originalError),
ChainFirstIOResultK[Config](sideEffect),
)
res := pipeline(Config{})()
assert.Equal(t, result.Left[int](originalError), res)
assert.False(t, sideEffectRan)
})
}
func TestTapIOResultK(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of(fmt.Sprintf("Tapped: %d", v))
}
}
pipeline := F.Pipe1(
Of[Config](42),
TapIOResultK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
t.Run("Validation use case", func(t *testing.T) {
validatePositive := func(v int) IOResult[bool] {
return func() Result[bool] {
if v > 0 {
return result.Of(true)
}
return result.Left[bool](errors.New("must be positive"))
}
}
pipeline := F.Pipe1(
Of[Config](42),
TapIOResultK[Config](validatePositive),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
})
t.Run("Validation failure", func(t *testing.T) {
validatePositive := func(v int) IOResult[bool] {
return func() Result[bool] {
if v > 0 {
return result.Of(true)
}
return result.Left[bool](errors.New("must be positive"))
}
}
pipeline := F.Pipe1(
Of[Config](-5),
TapIOResultK[Config](validatePositive),
)
res := pipeline(Config{})()
assert.True(t, E.IsLeft(res))
})
}
func TestTap(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of(fmt.Sprintf("Tapped: %d", v))
}
}
}
pipeline := F.Pipe1(
Of[Config](42),
Tap[Config](tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
t.Run("Tap is alias for ChainFirst", func(t *testing.T) {
sideEffect := func(v int) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
return result.Of(fmt.Sprintf("Value: %d", v))
}
}
}
pipelineWithTap := F.Pipe1(
Of[Config](42),
Tap[Config](sideEffect),
)
pipelineWithChainFirst := F.Pipe1(
Of[Config](42),
ChainFirst[Config](sideEffect),
)
resultTap := pipelineWithTap(Config{})()
resultChainFirst := pipelineWithChainFirst(Config{})()
assert.Equal(t, resultChainFirst, resultTap)
})
}
func TestMonadTap(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of("tapped")
}
}
}
res := MonadTap(Of[Config](42), tapFunc)(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
}
func TestTapEitherK(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) Result[string] {
tapped = true
return result.Of(fmt.Sprintf("Tapped: %d", v))
}
pipeline := F.Pipe1(
Of[Config](42),
TapEitherK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
t.Run("Failure - tap fails", func(t *testing.T) {
tapError := errors.New("tap failed")
tapFunc := func(v int) Result[string] {
return result.Left[string](tapError)
}
pipeline := F.Pipe1(
Of[Config](42),
TapEitherK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.True(t, E.IsLeft(res))
})
}
func TestTapResultK(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) Result[string] {
tapped = true
return result.Of("tapped")
}
pipeline := F.Pipe1(
Of[Config](42),
TapResultK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
}
func TestTapReaderK(t *testing.T) {
type Config struct {
multiplier int
}
t.Run("Success - preserves original value with context", func(t *testing.T) {
tapped := false
tapFunc := func(v int) R.Reader[Config, string] {
return func(cfg Config) string {
tapped = true
return fmt.Sprintf("Value: %d, Multiplier: %d", v, cfg.multiplier)
}
}
pipeline := F.Pipe1(
Of[Config](42),
TapReaderK[Config](tapFunc),
)
res := pipeline(Config{multiplier: 2})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
}
func TestTapIOK(t *testing.T) {
type Config struct{}
t.Run("Success - preserves original value", func(t *testing.T) {
tapped := false
tapFunc := func(v int) IO[string] {
return func() string {
tapped = true
return fmt.Sprintf("Tapped: %d", v)
}
}
pipeline := F.Pipe1(
Of[Config](42),
TapIOK[Config](tapFunc),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.True(t, tapped)
})
}
func TestRead(t *testing.T) {
type Config struct {
value int
}
t.Run("Success - provides environment", func(t *testing.T) {
computation := func(cfg Config) IOResult[int] {
return func() Result[int] {
return result.Of(cfg.value * 2)
}
}
res := Read[int](Config{value: 21})(computation)()
assert.Equal(t, result.Of(42), res)
})
t.Run("Failure - computation fails", func(t *testing.T) {
compError := errors.New("computation failed")
computation := func(cfg Config) IOResult[int] {
return func() Result[int] {
return result.Left[int](compError)
}
}
res := Read[int](Config{value: 21})(computation)()
assert.Equal(t, result.Left[int](compError), res)
})
}
func TestChainLeft(t *testing.T) {
type Config struct{}
t.Run("Right passes through unchanged", func(t *testing.T) {
pipeline := F.Pipe1(
Right[Config](42),
ChainLeft[Config](func(err error) ReaderIOResult[Config, int] {
return Left[Config, int](errors.New("should not run"))
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
})
t.Run("Left transforms error", func(t *testing.T) {
originalError := errors.New("original")
newError := errors.New("transformed")
pipeline := F.Pipe1(
Left[Config, int](originalError),
ChainLeft[Config](func(err error) ReaderIOResult[Config, int] {
return Left[Config, int](newError)
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Left[int](newError), res)
})
t.Run("Left recovers to Right", func(t *testing.T) {
pipeline := F.Pipe1(
Left[Config, int](errors.New("error")),
ChainLeft[Config](func(err error) ReaderIOResult[Config, int] {
return Right[Config](99)
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(99), res)
})
}
func TestTapLeft(t *testing.T) {
type Config struct{}
t.Run("Right does not call function", func(t *testing.T) {
tapped := false
pipeline := F.Pipe1(
Right[Config](42),
TapLeft[int, Config](func(err error) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of("logged")
}
}
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Of(42), res)
assert.False(t, tapped)
})
t.Run("Left calls function but preserves error", func(t *testing.T) {
tapped := false
originalError := errors.New("original error")
pipeline := F.Pipe1(
Left[Config, int](originalError),
TapLeft[int, Config](func(err error) ReaderIOResult[Config, string] {
return func(cfg Config) IOResult[string] {
return func() Result[string] {
tapped = true
return result.Of("side effect done")
}
}
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Left[int](originalError), res)
assert.True(t, tapped)
})
}
func TestTapLeftIOK(t *testing.T) {
type Config struct{}
t.Run("Left calls IO function but preserves error", func(t *testing.T) {
tapped := false
originalError := errors.New("original error")
pipeline := F.Pipe1(
Left[Config, int](originalError),
TapLeftIOK[int, Config](func(err error) IO[string] {
return func() string {
tapped = true
return "logged error"
}
}),
)
res := pipeline(Config{})()
assert.Equal(t, result.Left[int](originalError), res)
assert.True(t, tapped)
})
}

View File

@@ -38,51 +38,75 @@ import (
"github.com/IBM/fp-go/v2/readeroption"
)
// FromReaderOption converts a ReaderOption to a ReaderReaderIOEither.
// If the ReaderOption is None, the provided function is called to produce the error.
//
//go:inline
func FromReaderOption[R, C, A, E any](onNone Lazy[E]) Kleisli[R, C, E, ReaderOption[R, A], A] {
return reader.Map[R](RIOE.FromOption[C, A](onNone))
}
// FromReaderIOEither lifts a ReaderIOEither into a ReaderReaderIOEither context.
//
//go:inline
func FromReaderIOEither[C, E, R, A any](ma ReaderIOEither[R, E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.FromIOEither[C])
}
// FromReaderIO lifts a ReaderIO into a ReaderReaderIOEither, placing the result in the Right side.
//
//go:inline
func FromReaderIO[C, E, R, A any](ma ReaderIO[R, A]) ReaderReaderIOEither[R, C, E, A] {
return RightReaderIO[C, E](ma)
}
// RightReaderIO lifts a ReaderIO into a ReaderReaderIOEither, placing the result in the Right side.
//
//go:inline
func RightReaderIO[C, E, R, A any](ma ReaderIO[R, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.RightIO[C, E, A])
}
// LeftReaderIO lifts a ReaderIO into a ReaderReaderIOEither, placing the result in the Left (error) side.
//
//go:inline
func LeftReaderIO[C, A, R, E any](me ReaderIO[R, E]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(me, RIOE.LeftIO[C, A, E])
}
// MonadMap applies a function to the value inside a ReaderReaderIOEither context.
// If the computation is successful (Right), the function is applied to the value.
//
//go:inline
func MonadMap[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f func(A) B) ReaderReaderIOEither[R, C, E, B] {
return reader.MonadMap(fa, RIOE.Map[C, E](f))
}
// Map returns a function that applies a transformation to the success value.
// This is the curried version of MonadMap.
//
//go:inline
func Map[R, C, E, A, B any](f func(A) B) Operator[R, C, E, A, B] {
return reader.Map[R](RIOE.Map[C, E](f))
}
// MonadMapTo replaces the success value with a constant value.
//
//go:inline
func MonadMapTo[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], b B) ReaderReaderIOEither[R, C, E, B] {
return reader.MonadMap(fa, RIOE.MapTo[C, E, A](b))
}
// MapTo returns a function that replaces the success value with a constant.
// This is the curried version of MonadMapTo.
//
//go:inline
func MapTo[R, C, E, A, B any](b B) Operator[R, C, E, A, B] {
return reader.Map[R](RIOE.MapTo[C, E, A](b))
}
// MonadChain sequences two computations where the second depends on the result of the first.
//
//go:inline
func MonadChain[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisli[R, C, E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadChain(
@@ -92,6 +116,9 @@ func MonadChain[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisl
)
}
// MonadChainFirst sequences two computations but keeps the result of the first.
// Useful for performing side effects while preserving the original value.
//
//go:inline
func MonadChainFirst[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisli[R, C, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return chain.MonadChainFirst(
@@ -101,11 +128,15 @@ func MonadChainFirst[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f K
f)
}
// MonadTap is an alias for MonadChainFirst, executing a side effect while preserving the original value.
//
//go:inline
func MonadTap[R, C, E, A, B any](fa ReaderReaderIOEither[R, C, E, A], f Kleisli[R, C, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirst(fa, f)
}
// MonadChainEitherK chains a computation that returns an Either into a ReaderReaderIOEither.
//
//go:inline
func MonadChainEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f either.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromeither.MonadChainEitherK(
@@ -116,6 +147,9 @@ func MonadChainEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f
)
}
// ChainEitherK returns a function that chains an Either-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainEitherK.
//
//go:inline
func ChainEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E, A, B] {
return fromeither.ChainEitherK(
@@ -125,6 +159,8 @@ func ChainEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E
)
}
// MonadChainFirstEitherK chains an Either-returning computation but keeps the original value.
//
//go:inline
func MonadChainFirstEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f either.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromeither.MonadChainFirstEitherK(
@@ -136,11 +172,16 @@ func MonadChainFirstEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E,
)
}
// MonadTapEitherK is an alias for MonadChainFirstEitherK, executing an Either side effect while preserving the original value.
//
//go:inline
func MonadTapEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f either.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstEitherK(ma, f)
}
// ChainFirstEitherK returns a function that chains an Either computation while preserving the original value.
// This is the curried version of MonadChainFirstEitherK.
//
//go:inline
func ChainFirstEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E, A, A] {
return fromeither.ChainFirstEitherK(
@@ -151,11 +192,15 @@ func ChainFirstEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R,
)
}
// TapEitherK is an alias for ChainFirstEitherK, executing an Either side effect while preserving the original value.
//
//go:inline
func TapEitherK[R, C, E, A, B any](f either.Kleisli[E, A, B]) Operator[R, C, E, A, A] {
return ChainFirstEitherK[R, C](f)
}
// MonadChainReaderK chains a Reader-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f reader.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromreader.MonadChainReaderK(
@@ -166,6 +211,9 @@ func MonadChainReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f
)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
@@ -175,6 +223,8 @@ func ChainReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E
)
}
// MonadChainFirstReaderK chains a Reader computation but keeps the original value.
//
//go:inline
func MonadChainFirstReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f reader.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -185,11 +235,16 @@ func MonadChainFirstReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E,
)
}
// MonadTapReaderK is an alias for MonadChainFirstReaderK, executing a Reader side effect while preserving the original value.
//
//go:inline
func MonadTapReaderK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f reader.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstReaderK(ma, f)
}
// ChainFirstReaderK returns a function that chains a Reader computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderK.
//
//go:inline
func ChainFirstReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
@@ -199,11 +254,15 @@ func ChainFirstReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R,
)
}
// TapReaderK is an alias for ChainFirstReaderK, executing a Reader side effect while preserving the original value.
//
//go:inline
func TapReaderK[C, E, R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderK[C, E](f)
}
// MonadChainReaderIOK chains a ReaderIO-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromreader.MonadChainReaderK(
@@ -214,6 +273,9 @@ func MonadChainReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A],
)
}
// ChainReaderIOK returns a function that chains a ReaderIO-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainReaderIOK.
//
//go:inline
func ChainReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
@@ -223,6 +285,8 @@ func ChainReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R,
)
}
// MonadChainFirstReaderIOK chains a ReaderIO computation but keeps the original value.
//
//go:inline
func MonadChainFirstReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -233,11 +297,16 @@ func MonadChainFirstReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E
)
}
// MonadTapReaderIOK is an alias for MonadChainFirstReaderIOK, executing a ReaderIO side effect while preserving the original value.
//
//go:inline
func MonadTapReaderIOK[C, E, R, A, B any](ma ReaderReaderIOEither[R, C, E, A], f readerio.Kleisli[R, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstReaderIOK(ma, f)
}
// ChainFirstReaderIOK returns a function that chains a ReaderIO computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderIOK.
//
//go:inline
func ChainFirstReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
@@ -247,11 +316,15 @@ func ChainFirstReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operato
)
}
// TapReaderIOK is an alias for ChainFirstReaderIOK, executing a ReaderIO side effect while preserving the original value.
//
//go:inline
func TapReaderIOK[C, E, R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderIOK[C, E](f)
}
// MonadChainReaderEitherK chains a ReaderEither-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromreader.MonadChainReaderK(
@@ -262,6 +335,9 @@ func MonadChainReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E,
)
}
// ChainReaderEitherK returns a function that chains a ReaderEither-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainReaderEitherK.
//
//go:inline
func ChainReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
@@ -271,6 +347,8 @@ func ChainReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R,
)
}
// ChainReaderIOEitherK returns a function that chains a ReaderIOEither-returning function into ReaderReaderIOEither.
//
//go:inline
func ChainReaderIOEitherK[C, R, E, A, B any](f RIOE.Kleisli[R, E, A, B]) Operator[R, C, E, A, B] {
return fromreader.ChainReaderK(
@@ -280,6 +358,8 @@ func ChainReaderIOEitherK[C, R, E, A, B any](f RIOE.Kleisli[R, E, A, B]) Operato
)
}
// MonadChainFirstReaderEitherK chains a ReaderEither computation but keeps the original value.
//
//go:inline
func MonadChainFirstReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.MonadChainFirstReaderK(
@@ -290,11 +370,16 @@ func MonadChainFirstReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R,
)
}
// MonadTapReaderEitherK is an alias for MonadChainFirstReaderEitherK, executing a ReaderEither side effect while preserving the original value.
//
//go:inline
func MonadTapReaderEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f RE.Kleisli[R, E, A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstReaderEitherK(ma, f)
}
// ChainFirstReaderEitherK returns a function that chains a ReaderEither computation while preserving the original value.
// This is the curried version of MonadChainFirstReaderEitherK.
//
//go:inline
func ChainFirstReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, A] {
return fromreader.ChainFirstReaderK(
@@ -304,11 +389,15 @@ func ChainFirstReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operat
)
}
// TapReaderEitherK is an alias for ChainFirstReaderEitherK, executing a ReaderEither side effect while preserving the original value.
//
//go:inline
func TapReaderEitherK[C, E, R, A, B any](f RE.Kleisli[R, E, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderEitherK[C](f)
}
// ChainReaderOptionK returns a function that chains a ReaderOption-returning function into ReaderReaderIOEither.
// If the ReaderOption is None, the provided error function is called.
func ChainReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, B] {
fro := FromReaderOption[R, C, B](onNone)
@@ -323,6 +412,8 @@ func ChainReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kle
}
}
// ChainFirstReaderOptionK chains a ReaderOption computation while preserving the original value.
// If the ReaderOption is None, the provided error function is called.
func ChainFirstReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
fro := FromReaderOption[R, C, B](onNone)
return func(f readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
@@ -334,11 +425,15 @@ func ChainFirstReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroptio
}
}
// TapReaderOptionK is an alias for ChainFirstReaderOptionK, executing a ReaderOption side effect while preserving the original value.
//
//go:inline
func TapReaderOptionK[R, C, A, B, E any](onNone Lazy[E]) func(readeroption.Kleisli[R, A, B]) Operator[R, C, E, A, A] {
return ChainFirstReaderOptionK[R, C, A, B](onNone)
}
// MonadChainIOEitherK chains an IOEither-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainIOEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f IOE.Kleisli[E, A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromioeither.MonadChainIOEitherK(
@@ -349,6 +444,9 @@ func MonadChainIOEitherK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A],
)
}
// ChainIOEitherK returns a function that chains an IOEither-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainIOEitherK.
//
//go:inline
func ChainIOEitherK[R, C, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, C, E, A, B] {
return fromioeither.ChainIOEitherK(
@@ -358,6 +456,8 @@ func ChainIOEitherK[R, C, E, A, B any](f IOE.Kleisli[E, A, B]) Operator[R, C, E,
)
}
// MonadChainIOK chains an IO-returning computation into a ReaderReaderIOEither.
//
//go:inline
func MonadChainIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.Kleisli[A, B]) ReaderReaderIOEither[R, C, E, B] {
return fromio.MonadChainIOK(
@@ -368,6 +468,9 @@ func MonadChainIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.
)
}
// ChainIOK returns a function that chains an IO-returning function into ReaderReaderIOEither.
// This is the curried version of MonadChainIOK.
//
//go:inline
func ChainIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, B] {
return fromio.ChainIOK(
@@ -377,6 +480,8 @@ func ChainIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, B] {
)
}
// MonadChainFirstIOK chains an IO computation but keeps the original value.
//
//go:inline
func MonadChainFirstIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.Kleisli[A, B]) ReaderReaderIOEither[R, C, E, A] {
return fromio.MonadChainFirstIOK(
@@ -388,11 +493,16 @@ func MonadChainFirstIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A],
)
}
// MonadTapIOK is an alias for MonadChainFirstIOK, executing an IO side effect while preserving the original value.
//
//go:inline
func MonadTapIOK[R, C, E, A, B any](ma ReaderReaderIOEither[R, C, E, A], f io.Kleisli[A, B]) ReaderReaderIOEither[R, C, E, A] {
return MonadChainFirstIOK(ma, f)
}
// ChainFirstIOK returns a function that chains an IO computation while preserving the original value.
// This is the curried version of MonadChainFirstIOK.
//
//go:inline
func ChainFirstIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, A] {
return fromio.ChainFirstIOK(
@@ -403,11 +513,16 @@ func ChainFirstIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, A
)
}
// TapIOK is an alias for ChainFirstIOK, executing an IO side effect while preserving the original value.
//
//go:inline
func TapIOK[R, C, E, A, B any](f io.Kleisli[A, B]) Operator[R, C, E, A, A] {
return ChainFirstIOK[R, C, E](f)
}
// ChainOptionK returns a function that chains an Option-returning function into ReaderReaderIOEither.
// If the Option is None, the provided error function is called.
//
//go:inline
func ChainOptionK[R, C, A, B, E any](onNone Lazy[E]) func(option.Kleisli[A, B]) Operator[R, C, E, A, B] {
return fromeither.ChainOptionK(
@@ -417,6 +532,8 @@ func ChainOptionK[R, C, A, B, E any](onNone Lazy[E]) func(option.Kleisli[A, B])
)
}
// MonadAp applies a function wrapped in a context to a value wrapped in a context.
//
//go:inline
func MonadAp[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa ReaderReaderIOEither[R, C, E, A]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadAp[
@@ -429,6 +546,8 @@ func MonadAp[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa
)
}
// MonadApSeq applies a function in a context to a value in a context, executing them sequentially.
//
//go:inline
func MonadApSeq[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa ReaderReaderIOEither[R, C, E, A]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadAp[
@@ -441,6 +560,8 @@ func MonadApSeq[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B],
)
}
// MonadApPar applies a function in a context to a value in a context, executing them in parallel.
//
//go:inline
func MonadApPar[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B], fa ReaderReaderIOEither[R, C, E, A]) ReaderReaderIOEither[R, C, E, B] {
return readert.MonadAp[
@@ -453,6 +574,9 @@ func MonadApPar[R, C, E, A, B any](fab ReaderReaderIOEither[R, C, E, func(A) B],
)
}
// Ap returns a function that applies a function in a context to a value in a context.
// This is the curried version of MonadAp.
//
//go:inline
func Ap[B, R, C, E, A any](fa ReaderReaderIOEither[R, C, E, A]) Operator[R, C, E, func(A) B, B] {
return readert.Ap[
@@ -464,6 +588,9 @@ func Ap[B, R, C, E, A any](fa ReaderReaderIOEither[R, C, E, A]) Operator[R, C, E
)
}
// Chain returns a function that sequences computations where the second depends on the first.
// This is the curried version of MonadChain.
//
//go:inline
func Chain[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, B] {
return readert.Chain[ReaderReaderIOEither[R, C, E, A]](
@@ -472,6 +599,9 @@ func Chain[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, B]
)
}
// ChainFirst returns a function that sequences computations but keeps the first result.
// This is the curried version of MonadChainFirst.
//
//go:inline
func ChainFirst[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, A] {
return chain.ChainFirst(
@@ -480,96 +610,137 @@ func ChainFirst[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A
f)
}
// Tap is an alias for ChainFirst, executing a side effect while preserving the original value.
//
//go:inline
func Tap[R, C, E, A, B any](f Kleisli[R, C, E, A, B]) Operator[R, C, E, A, A] {
return ChainFirst(f)
}
// Right creates a successful ReaderReaderIOEither with the given value.
//
//go:inline
func Right[R, C, E, A any](a A) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.Right[C, E](a))
}
// Left creates a failed ReaderReaderIOEither with the given error.
//
//go:inline
func Left[R, C, A, E any](e E) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.Left[C, A](e))
}
// Of creates a successful ReaderReaderIOEither with the given value.
// This is the pointed functor operation.
//
//go:inline
func Of[R, C, E, A any](a A) ReaderReaderIOEither[R, C, E, A] {
return Right[R, C, E](a)
}
// Flatten removes one level of nesting from a nested ReaderReaderIOEither.
//
//go:inline
func Flatten[R, C, E, A any](mma ReaderReaderIOEither[R, C, E, ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return MonadChain(mma, function.Identity[ReaderReaderIOEither[R, C, E, A]])
}
// FromEither lifts an Either into a ReaderReaderIOEither context.
//
//go:inline
func FromEither[R, C, E, A any](t Either[E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.FromEither[C](t))
}
// RightReader lifts a Reader into a ReaderReaderIOEither, placing the result in the Right side.
//
//go:inline
func RightReader[C, E, R, A any](ma Reader[R, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.Right[C, E])
}
// LeftReader lifts a Reader into a ReaderReaderIOEither, placing the result in the Left (error) side.
//
//go:inline
func LeftReader[C, A, R, E any](ma Reader[R, E]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.Left[C, A])
}
// FromReader lifts a Reader into a ReaderReaderIOEither context.
//
//go:inline
func FromReader[C, E, R, A any](ma Reader[R, A]) ReaderReaderIOEither[R, C, E, A] {
return RightReader[C, E](ma)
}
// RightIO lifts an IO into a ReaderReaderIOEither, placing the result in the Right side.
//
//go:inline
func RightIO[R, C, E, A any](ma IO[A]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.RightIO[C, E](ma))
}
// LeftIO lifts an IO into a ReaderReaderIOEither, placing the result in the Left (error) side.
//
//go:inline
func LeftIO[R, C, A, E any](ma IO[E]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.LeftIO[C, A](ma))
}
// FromIO lifts an IO into a ReaderReaderIOEither context.
//
//go:inline
func FromIO[R, C, E, A any](ma IO[A]) ReaderReaderIOEither[R, C, E, A] {
return RightIO[R, C, E](ma)
}
// FromIOEither lifts an IOEither into a ReaderReaderIOEither context.
//
//go:inline
func FromIOEither[R, C, E, A any](ma IOEither[E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.Of[R](RIOE.FromIOEither[C](ma))
}
// FromReaderEither lifts a ReaderEither into a ReaderReaderIOEither context.
//
//go:inline
func FromReaderEither[R, C, E, A any](ma RE.ReaderEither[R, E, A]) ReaderReaderIOEither[R, C, E, A] {
return reader.MonadMap(ma, RIOE.FromEither[C])
}
// Ask returns a ReaderReaderIOEither that retrieves the outer context.
//
//go:inline
func Ask[R, C, E any]() ReaderReaderIOEither[R, C, E, R] {
return fromreader.Ask(FromReader[C, E, R, R])()
}
// Asks returns a ReaderReaderIOEither that retrieves a value derived from the outer context.
//
//go:inline
func Asks[C, E, R, A any](r Reader[R, A]) ReaderReaderIOEither[R, C, E, A] {
return fromreader.Asks(FromReader[C, E, R, A])(r)
}
// FromOption converts an Option to a ReaderReaderIOEither.
// If the Option is None, the provided function is called to produce the error.
//
//go:inline
func FromOption[R, C, A, E any](onNone Lazy[E]) func(Option[A]) ReaderReaderIOEither[R, C, E, A] {
return fromeither.FromOption(FromEither[R, C, E, A], onNone)
}
// FromPredicate creates a ReaderReaderIOEither from a predicate.
// If the predicate returns false, the onFalse function is called to produce the error.
//
//go:inline
func FromPredicate[R, C, E, A any](pred func(A) bool, onFalse func(A) E) func(A) ReaderReaderIOEither[R, C, E, A] {
return fromeither.FromPredicate(FromEither[R, C, E, A], pred, onFalse)
}
// MonadAlt tries the first computation, and if it fails, tries the second.
//
//go:inline
func MonadAlt[R, C, E, A any](first ReaderReaderIOEither[R, C, E, A], second Lazy[ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return func(r R) ReaderIOEither[C, E, A] {
@@ -579,36 +750,53 @@ func MonadAlt[R, C, E, A any](first ReaderReaderIOEither[R, C, E, A], second Laz
}
}
// Alt returns a function that tries an alternative computation if the first fails.
// This is the curried version of MonadAlt.
//
//go:inline
func Alt[R, C, E, A any](second Lazy[ReaderReaderIOEither[R, C, E, A]]) Operator[R, C, E, A, A] {
return function.Bind2nd(MonadAlt, second)
}
// MonadFlap applies a value to a function wrapped in a context.
//
//go:inline
func MonadFlap[R, C, E, B, A any](fab ReaderReaderIOEither[R, C, E, func(A) B], a A) ReaderReaderIOEither[R, C, E, B] {
return functor.MonadFlap(MonadMap[R, C, E, func(A) B, B], fab, a)
}
// Flap returns a function that applies a fixed value to a function in a context.
// This is the curried version of MonadFlap.
//
//go:inline
func Flap[R, C, E, B, A any](a A) Operator[R, C, E, func(A) B, B] {
return functor.Flap(Map[R, C, E, func(A) B, B], a)
}
// MonadMapLeft applies a function to the error value, leaving success unchanged.
//
//go:inline
func MonadMapLeft[R, C, E1, E2, A any](fa ReaderReaderIOEither[R, C, E1, A], f func(E1) E2) ReaderReaderIOEither[R, C, E2, A] {
return reader.MonadMap(fa, RIOE.MapLeft[C, A](f))
}
// MapLeft returns a function that transforms the error channel.
// This is the curried version of MonadMapLeft.
//
//go:inline
func MapLeft[R, C, A, E1, E2 any](f func(E1) E2) func(ReaderReaderIOEither[R, C, E1, A]) ReaderReaderIOEither[R, C, E2, A] {
return reader.Map[R](RIOE.MapLeft[C, A](f))
}
// Read executes a ReaderReaderIOEither by providing a concrete outer environment value.
//
//go:inline
func Read[C, E, A, R any](r R) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return reader.Read[ReaderIOEither[C, E, A]](r)
}
// ReadIOEither executes a ReaderReaderIOEither by providing an outer environment obtained from an IOEither.
//
//go:inline
func ReadIOEither[A, R, C, E any](rio IOEither[E, R]) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return func(rri ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
@@ -623,6 +811,8 @@ func ReadIOEither[A, R, C, E any](rio IOEither[E, R]) func(ReaderReaderIOEither[
}
}
// ReadIO executes a ReaderReaderIOEither by providing an outer environment obtained from an IO.
//
//go:inline
func ReadIO[C, E, A, R any](rio IO[R]) func(ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
return func(rri ReaderReaderIOEither[R, C, E, A]) ReaderIOEither[C, E, A] {
@@ -637,6 +827,8 @@ func ReadIO[C, E, A, R any](rio IO[R]) func(ReaderReaderIOEither[R, C, E, A]) Re
}
}
// MonadChainLeft chains a computation on the error channel, allowing error recovery or transformation.
//
//go:inline
func MonadChainLeft[R, C, EA, EB, A any](fa ReaderReaderIOEither[R, C, EA, A], f Kleisli[R, C, EB, EA, A]) ReaderReaderIOEither[R, C, EB, A] {
return readert.MonadChain(
@@ -646,6 +838,9 @@ func MonadChainLeft[R, C, EA, EB, A any](fa ReaderReaderIOEither[R, C, EA, A], f
)
}
// ChainLeft returns a function that chains a computation on the error channel.
// This is the curried version of MonadChainLeft.
//
//go:inline
func ChainLeft[R, C, EA, EB, A any](f Kleisli[R, C, EB, EA, A]) func(ReaderReaderIOEither[R, C, EA, A]) ReaderReaderIOEither[R, C, EB, A] {
return readert.Chain[ReaderReaderIOEither[R, C, EA, A]](
@@ -654,16 +849,22 @@ func ChainLeft[R, C, EA, EB, A any](f Kleisli[R, C, EB, EA, A]) func(ReaderReade
)
}
// Delay creates an operation that passes in the value after some delay.
//
//go:inline
func Delay[R, C, E, A any](delay time.Duration) Operator[R, C, E, A, A] {
return reader.Map[R](RIOE.Delay[C, E, A](delay))
}
// After creates an operation that passes after the given time.Time.
//
//go:inline
func After[R, C, E, A any](timestamp time.Time) Operator[R, C, E, A, A] {
return reader.Map[R](RIOE.After[C, E, A](timestamp))
}
// Defer creates a ReaderReaderIOEither lazily via a generator function.
// The generator is called each time the ReaderReaderIOEither is executed.
func Defer[R, C, E, A any](fa Lazy[ReaderReaderIOEither[R, C, E, A]]) ReaderReaderIOEither[R, C, E, A] {
return func(r R) ReaderIOEither[C, E, A] {
return func(c C) RIOE.IOEither[E, A] {