mirror of
https://github.com/IBM/fp-go.git
synced 2026-02-26 13:06:09 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc0c14c7cf | ||
|
|
19159ad49e | ||
|
|
b9c8fb4ff1 | ||
|
|
4529e720bc | ||
|
|
ef8b2ea65d | ||
|
|
5bd7caafdd | ||
|
|
47ebcd79b1 | ||
|
|
dbad94806e |
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -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
273
v2/cli/README.md
Normal 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
199
v2/cli/effect.go
Normal 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
204
v2/cli/effect_test.go
Normal 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
359
v2/cli/flags.go
Normal 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
287
v2/cli/flags_test.go
Normal 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)))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
|
||||
90
v2/context/readerioresult/file/types.go
Normal file
90
v2/context/readerioresult/file/types.go
Normal 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]
|
||||
)
|
||||
@@ -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])
|
||||
|
||||
@@ -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)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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
32
v2/effect/common_test.go
Normal 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())
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
649
v2/effect/effect_additional_test.go
Normal file
649
v2/effect/effect_additional_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
213
v2/effect/effect_missing_test.go
Normal file
213
v2/effect/effect_missing_test.go
Normal 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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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)())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -480,5 +480,3 @@ func BenchmarkFromReaderResult_WithContext(b *testing.B) {
|
||||
_ = validator(42)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Made with Bob
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
Reference in New Issue
Block a user