1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-15 23:33:46 +02:00

Compare commits

...

16 Commits

Author SHA1 Message Date
Dr. Carsten Leue
acb601fc01 fix: reuse some more code
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-15 16:30:40 +01:00
Dr. Carsten Leue
d17663f016 fix: better doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-15 11:16:09 +01:00
Dr. Carsten Leue
829365fc24 doc: improve docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-12 13:30:10 +01:00
Dr. Carsten Leue
64b5660b4e doc: remove some comments
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-12 12:35:53 +01:00
Dr. Carsten Leue
16e82d6a65 fix: better cancellation support
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-12 11:52:43 +01:00
Dr. Carsten Leue
0d40fdcebb fix: implement tail recursion
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-12 11:18:32 +01:00
Dr. Carsten Leue
6a4dfa2c93 fix: better doc
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-11 16:18:55 +01:00
Dr. Carsten Leue
a37f379a3c fix: semantic of MapTo and ChainTo and update tests
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-11 09:09:44 +01:00
Dr. Carsten Leue
ece0cd135d fix: add more tests and logging
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-10 18:23:19 +01:00
Dr. Carsten Leue
739b6a284c fix: better slog based logging
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 17:52:57 +01:00
Dr. Carsten Leue
ba10d8d314 doc: fix docs
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 13:00:03 +01:00
Dr. Carsten Leue
3d6c419185 fix: add better logging
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-09 12:49:44 +01:00
Dr. Carsten Leue
3f4b6292e4 fix: optimize Traverse
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-05 21:35:05 +01:00
Dr. Carsten Leue
b1704b6d26 fix: implement TraverseReader
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-05 17:51:13 +01:00
Dr. Carsten Leue
ffdfd218f8 fix: implement Flip for Reader
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-05 11:04:49 +01:00
Dr. Carsten Leue
34826d8c52 fix: Ask and add tests to retry
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-04 16:47:53 +01:00
228 changed files with 33718 additions and 4274 deletions

View File

@@ -61,6 +61,7 @@ package main
import (
"fmt"
"github.com/IBM/fp-go/v2/option"
N "github.com/IBM/fp-go/v2/number"
)
func main() {
@@ -145,6 +146,8 @@ func main() {
}
```
## ⚠️ Breaking Changes
### From V1 to V2
#### 1. Generic Type Aliases

View File

@@ -19,7 +19,7 @@ import (
E "github.com/IBM/fp-go/v2/eq"
)
func equals[T any](left []T, right []T, eq func(T, T) bool) bool {
func equals[T any](left, right []T, eq func(T, T) bool) bool {
if len(left) != len(right) {
return false
}

View File

@@ -297,7 +297,7 @@ func MatchLeft[AS ~[]A, A, B any](onEmpty func() B, onNonEmpty func(A, AS) B) fu
}
//go:inline
func Slice[AS ~[]A, A any](start int, end int) func(AS) AS {
func Slice[AS ~[]A, A any](start, end int) func(AS) AS {
return array.Slice[AS](start, end)
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/IBM/fp-go/v2/optics/prism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
)
func TestEqual(t *testing.T) {
@@ -334,7 +335,7 @@ func TestThat(t *testing.T) {
})
t.Run("should work with string predicates", func(t *testing.T) {
startsWithH := func(s string) bool { return len(s) > 0 && s[0] == 'h' }
startsWithH := func(s string) bool { return S.IsNonEmpty(s) && s[0] == 'h' }
result := That(startsWithH)("hello")(t)
if !result {
t.Error("Expected That to pass for string predicate")
@@ -484,7 +485,7 @@ func TestLocal(t *testing.T) {
t.Run("should compose with other assertions", func(t *testing.T) {
// Create multiple focused assertions
nameNotEmpty := Local(func(u User) string { return u.Name })(
That(func(name string) bool { return len(name) > 0 }),
That(S.IsNonEmpty),
)
ageInRange := Local(func(u User) int { return u.Age })(
That(func(age int) bool { return age >= 18 && age <= 100 }),

View File

@@ -233,5 +233,3 @@ func doSomethingSuccessful() error {
func doSomethingThatFails() error {
return errors.New("operation failed")
}
// Made with Bob

View File

@@ -27,6 +27,7 @@ import (
"strings"
"text/template"
S "github.com/IBM/fp-go/v2/string"
C "github.com/urfave/cli/v2"
)
@@ -439,7 +440,7 @@ func extractEmbeddedFields(embedType ast.Expr, fileImports map[string]string, fi
return results
}
if typeName == "" || typeIdent == nil {
if S.IsEmpty(typeName) || typeIdent == nil {
return results
}
@@ -746,7 +747,7 @@ func generateLensHelpers(dir, filename string, verbose bool) error {
}
}
if packageName == "" {
if S.IsEmpty(packageName) {
packageName = pkg
}

View File

@@ -25,6 +25,7 @@ import (
"strings"
"testing"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -60,7 +61,7 @@ func TestHasLensAnnotation(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var doc *ast.CommentGroup
if tt.comment != "" {
if S.IsNonEmpty(tt.comment) {
doc = &ast.CommentGroup{
List: []*ast.Comment{
{Text: tt.comment},
@@ -289,7 +290,7 @@ func TestHasOmitEmpty(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var tag *ast.BasicLit
if tt.tag != "" {
if S.IsNonEmpty(tt.tag) {
tag = &ast.BasicLit{
Value: tt.tag,
}
@@ -326,7 +327,7 @@ type Other struct {
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
@@ -380,7 +381,7 @@ type Config struct {
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
@@ -440,7 +441,7 @@ type TypeTest struct {
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
@@ -542,7 +543,7 @@ type TestStruct struct {
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
@@ -597,7 +598,7 @@ type TestStruct struct {
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
@@ -639,7 +640,7 @@ type TestStruct struct {
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code (should not create file)
@@ -776,7 +777,7 @@ type Extended struct {
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
@@ -824,7 +825,7 @@ type Person struct {
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
@@ -880,7 +881,7 @@ type Document struct {
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
@@ -922,7 +923,7 @@ type Container[T any] struct {
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
@@ -960,7 +961,7 @@ type Pair[K comparable, V any] struct {
}
`
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Parse the file
@@ -998,7 +999,7 @@ type Box[T any] struct {
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code
@@ -1049,7 +1050,7 @@ type ComparableBox[T comparable] struct {
`
testFile := filepath.Join(tmpDir, "test.go")
err := os.WriteFile(testFile, []byte(testCode), 0644)
err := os.WriteFile(testFile, []byte(testCode), 0o644)
require.NoError(t, err)
// Generate lens code

View File

@@ -19,6 +19,8 @@ import (
"fmt"
"os"
"strings"
S "github.com/IBM/fp-go/v2/string"
)
// Deprecated:
@@ -176,7 +178,7 @@ func generateTraverseTuple1(
}
fmt.Fprintf(f, "F%d ~func(A%d) %s", j+1, j+1, hkt(fmt.Sprintf("T%d", j+1)))
}
if infix != "" {
if S.IsNonEmpty(infix) {
fmt.Fprintf(f, ", %s", infix)
}
// types
@@ -209,7 +211,7 @@ func generateTraverseTuple1(
fmt.Fprintf(f, " return A.TraverseTuple%d(\n", i)
// map
fmt.Fprintf(f, " Map[")
if infix != "" {
if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s, T1,", infix)
} else {
fmt.Fprintf(f, "T1,")
@@ -231,7 +233,7 @@ func generateTraverseTuple1(
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "%s", tuple)
if infix != "" {
if S.IsNonEmpty(infix) {
fmt.Fprintf(f, ", %s", infix)
}
fmt.Fprintf(f, ", T%d],\n", j+1)
@@ -256,11 +258,11 @@ func generateSequenceTuple1(
fmt.Fprintf(f, "\n// SequenceTuple%d converts a [Tuple%d] of [%s] into an [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
fmt.Fprintf(f, "func SequenceTuple%d[", i)
if infix != "" {
if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s", infix)
}
for j := 0; j < i; j++ {
if infix != "" || j > 0 {
if S.IsNonEmpty(infix) || j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
@@ -276,7 +278,7 @@ func generateSequenceTuple1(
fmt.Fprintf(f, " return A.SequenceTuple%d(\n", i)
// map
fmt.Fprintf(f, " Map[")
if infix != "" {
if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s, T1,", infix)
} else {
fmt.Fprintf(f, "T1,")
@@ -298,7 +300,7 @@ func generateSequenceTuple1(
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "%s", tuple)
if infix != "" {
if S.IsNonEmpty(infix) {
fmt.Fprintf(f, ", %s", infix)
}
fmt.Fprintf(f, ", T%d],\n", j+1)
@@ -319,11 +321,11 @@ func generateSequenceT1(
fmt.Fprintf(f, "\n// SequenceT%d converts %d parameters of [%s] into a [%s].\n", i, i, hkt("T"), hkt(fmt.Sprintf("Tuple%d", i)))
fmt.Fprintf(f, "func SequenceT%d[", i)
if infix != "" {
if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s", infix)
}
for j := 0; j < i; j++ {
if infix != "" || j > 0 {
if S.IsNonEmpty(infix) || j > 0 {
fmt.Fprintf(f, ", ")
}
fmt.Fprintf(f, "T%d", j+1)
@@ -339,7 +341,7 @@ func generateSequenceT1(
fmt.Fprintf(f, " return A.SequenceT%d(\n", i)
// map
fmt.Fprintf(f, " Map[")
if infix != "" {
if S.IsNonEmpty(infix) {
fmt.Fprintf(f, "%s, T1,", infix)
} else {
fmt.Fprintf(f, "T1,")
@@ -361,7 +363,7 @@ func generateSequenceT1(
fmt.Fprintf(f, " ")
}
fmt.Fprintf(f, "%s", tuple)
if infix != "" {
if S.IsNonEmpty(infix) {
fmt.Fprintf(f, ", %s", infix)
}
fmt.Fprintf(f, ", T%d],\n", j+1)

177
v2/consumer/consumer.go Normal file
View File

@@ -0,0 +1,177 @@
// 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 consumer
// Local transforms a Consumer by preprocessing its input through a function.
// This is the contravariant map operation for Consumers, analogous to reader.Local
// but operating on the input side rather than the output side.
//
// Given a Consumer[R1] that consumes values of type R1, and a function f that
// converts R2 to R1, Local creates a new Consumer[R2] that:
// 1. Takes a value of type R2
// 2. Applies f to convert it to R1
// 3. Passes the result to the original Consumer[R1]
//
// This is particularly useful for adapting consumers to work with different input types,
// similar to how reader.Local adapts readers to work with different environment types.
//
// Comparison with reader.Local:
// - reader.Local: Transforms the environment BEFORE passing it to a Reader (preprocessing input)
// - consumer.Local: Transforms the value BEFORE passing it to a Consumer (preprocessing input)
// - Both are contravariant operations on the input type
// - Reader produces output, Consumer performs side effects
//
// Type Parameters:
// - R2: The input type of the new Consumer (what you have)
// - R1: The input type of the original Consumer (what it expects)
//
// Parameters:
// - f: A function that converts R2 to R1 (preprocessing function)
//
// Returns:
// - An Operator that transforms Consumer[R1] into Consumer[R2]
//
// Example - Basic type adaptation:
//
// // Consumer that logs integers
// logInt := func(x int) {
// fmt.Printf("Value: %d\n", x)
// }
//
// // Adapt it to consume strings by parsing them first
// parseToInt := func(s string) int {
// n, _ := strconv.Atoi(s)
// return n
// }
//
// logString := consumer.Local(parseToInt)(logInt)
// logString("42") // Logs: "Value: 42"
//
// Example - Extracting fields from structs:
//
// type User struct {
// Name string
// Age int
// }
//
// // Consumer that logs names
// logName := func(name string) {
// fmt.Printf("Name: %s\n", name)
// }
//
// // Adapt it to consume User structs
// extractName := func(u User) string {
// return u.Name
// }
//
// logUser := consumer.Local(extractName)(logName)
// logUser(User{Name: "Alice", Age: 30}) // Logs: "Name: Alice"
//
// Example - Simplifying complex types:
//
// type DetailedConfig struct {
// Host string
// Port int
// Timeout time.Duration
// MaxRetry int
// }
//
// type SimpleConfig struct {
// Host string
// Port int
// }
//
// // Consumer that logs simple configs
// logSimple := func(c SimpleConfig) {
// fmt.Printf("Server: %s:%d\n", c.Host, c.Port)
// }
//
// // Adapt it to consume detailed configs
// simplify := func(d DetailedConfig) SimpleConfig {
// return SimpleConfig{Host: d.Host, Port: d.Port}
// }
//
// logDetailed := consumer.Local(simplify)(logSimple)
// logDetailed(DetailedConfig{
// Host: "localhost",
// Port: 8080,
// Timeout: time.Second,
// MaxRetry: 3,
// }) // Logs: "Server: localhost:8080"
//
// Example - Composing multiple transformations:
//
// type Response struct {
// StatusCode int
// Body string
// }
//
// // Consumer that logs status codes
// logStatus := func(code int) {
// fmt.Printf("Status: %d\n", code)
// }
//
// // Extract status code from response
// getStatus := func(r Response) int {
// return r.StatusCode
// }
//
// // Adapt to consume responses
// logResponse := consumer.Local(getStatus)(logStatus)
// logResponse(Response{StatusCode: 200, Body: "OK"}) // Logs: "Status: 200"
//
// Example - Using with multiple consumers:
//
// type Event struct {
// Type string
// Timestamp time.Time
// Data map[string]any
// }
//
// // Consumers for different aspects
// logType := func(t string) { fmt.Printf("Type: %s\n", t) }
// logTime := func(t time.Time) { fmt.Printf("Time: %v\n", t) }
//
// // Adapt them to consume events
// logEventType := consumer.Local(func(e Event) string { return e.Type })(logType)
// logEventTime := consumer.Local(func(e Event) time.Time { return e.Timestamp })(logTime)
//
// event := Event{Type: "UserLogin", Timestamp: time.Now(), Data: nil}
// logEventType(event) // Logs: "Type: UserLogin"
// logEventTime(event) // Logs: "Time: ..."
//
// Use Cases:
// - Type adaptation: Convert between different input types
// - Field extraction: Extract specific fields from complex structures
// - Data transformation: Preprocess data before consumption
// - Interface adaptation: Adapt consumers to work with different interfaces
// - Logging pipelines: Transform data before logging
// - Event handling: Extract relevant data from events before processing
//
// Relationship to Reader:
// Consumer is the dual of Reader in category theory:
// - Reader[R, A] = R -> A (produces output from environment)
// - Consumer[A] = A -> () (consumes input, produces side effects)
// - reader.Local transforms the environment before reading
// - consumer.Local transforms the input before consuming
// - Both are contravariant functors on their input type
func Local[R2, R1 any](f func(R2) R1) Operator[R1, R2] {
return func(c Consumer[R1]) Consumer[R2] {
return func(r2 R2) {
c(f(r2))
}
}
}

View File

@@ -0,0 +1,383 @@
// 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 consumer
import (
"strconv"
"testing"
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestLocal(t *testing.T) {
t.Run("basic type transformation", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
// Transform string to int before consuming
stringToInt := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
consumeString := Local(stringToInt)(consumeInt)
consumeString("42")
assert.Equal(t, 42, captured)
})
t.Run("field extraction from struct", func(t *testing.T) {
type User struct {
Name string
Age int
}
var capturedName string
consumeName := func(name string) {
capturedName = name
}
extractName := func(u User) string {
return u.Name
}
consumeUser := Local(extractName)(consumeName)
consumeUser(User{Name: "Alice", Age: 30})
assert.Equal(t, "Alice", capturedName)
})
t.Run("simplifying complex types", func(t *testing.T) {
type DetailedConfig struct {
Host string
Port int
Timeout time.Duration
MaxRetry int
}
type SimpleConfig struct {
Host string
Port int
}
var captured SimpleConfig
consumeSimple := func(c SimpleConfig) {
captured = c
}
simplify := func(d DetailedConfig) SimpleConfig {
return SimpleConfig{Host: d.Host, Port: d.Port}
}
consumeDetailed := Local(simplify)(consumeSimple)
consumeDetailed(DetailedConfig{
Host: "localhost",
Port: 8080,
Timeout: time.Second,
MaxRetry: 3,
})
assert.Equal(t, SimpleConfig{Host: "localhost", Port: 8080}, captured)
})
t.Run("multiple transformations", func(t *testing.T) {
type Response struct {
StatusCode int
Body string
}
var capturedStatus int
consumeStatus := func(code int) {
capturedStatus = code
}
getStatus := func(r Response) int {
return r.StatusCode
}
consumeResponse := Local(getStatus)(consumeStatus)
consumeResponse(Response{StatusCode: 200, Body: "OK"})
assert.Equal(t, 200, capturedStatus)
})
t.Run("chaining Local transformations", func(t *testing.T) {
type Level3 struct{ Value int }
type Level2 struct{ L3 Level3 }
type Level1 struct{ L2 Level2 }
var captured int
consumeInt := func(x int) {
captured = x
}
// Chain multiple Local transformations
extract3 := func(l3 Level3) int { return l3.Value }
extract2 := func(l2 Level2) Level3 { return l2.L3 }
extract1 := func(l1 Level1) Level2 { return l1.L2 }
// Compose the transformations
consumeLevel3 := Local(extract3)(consumeInt)
consumeLevel2 := Local(extract2)(consumeLevel3)
consumeLevel1 := Local(extract1)(consumeLevel2)
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
assert.Equal(t, 42, captured)
})
t.Run("identity transformation", func(t *testing.T) {
var captured string
consumeString := func(s string) {
captured = s
}
identity := function.Identity[string]
consumeIdentity := Local(identity)(consumeString)
consumeIdentity("test")
assert.Equal(t, "test", captured)
})
t.Run("transformation with calculation", func(t *testing.T) {
type Rectangle struct {
Width int
Height int
}
var capturedArea int
consumeArea := func(area int) {
capturedArea = area
}
calculateArea := func(r Rectangle) int {
return r.Width * r.Height
}
consumeRectangle := Local(calculateArea)(consumeArea)
consumeRectangle(Rectangle{Width: 5, Height: 10})
assert.Equal(t, 50, capturedArea)
})
t.Run("multiple consumers with same transformation", func(t *testing.T) {
type Event struct {
Type string
Timestamp time.Time
}
var capturedType string
var capturedTime time.Time
consumeType := func(t string) {
capturedType = t
}
consumeTime := func(t time.Time) {
capturedTime = t
}
extractType := func(e Event) string { return e.Type }
extractTime := func(e Event) time.Time { return e.Timestamp }
consumeEventType := Local(extractType)(consumeType)
consumeEventTime := Local(extractTime)(consumeTime)
now := time.Now()
event := Event{Type: "UserLogin", Timestamp: now}
consumeEventType(event)
consumeEventTime(event)
assert.Equal(t, "UserLogin", capturedType)
assert.Equal(t, now, capturedTime)
})
t.Run("transformation with slice", func(t *testing.T) {
var captured int
consumeLength := func(n int) {
captured = n
}
getLength := func(s []string) int {
return len(s)
}
consumeSlice := Local(getLength)(consumeLength)
consumeSlice([]string{"a", "b", "c"})
assert.Equal(t, 3, captured)
})
t.Run("transformation with map", func(t *testing.T) {
var captured int
consumeCount := func(n int) {
captured = n
}
getCount := func(m map[string]int) int {
return len(m)
}
consumeMap := Local(getCount)(consumeCount)
consumeMap(map[string]int{"a": 1, "b": 2, "c": 3})
assert.Equal(t, 3, captured)
})
t.Run("transformation with pointer", func(t *testing.T) {
var captured int
consumeInt := func(x int) {
captured = x
}
dereference := func(p *int) int {
if p == nil {
return 0
}
return *p
}
consumePointer := Local(dereference)(consumeInt)
value := 42
consumePointer(&value)
assert.Equal(t, 42, captured)
consumePointer(nil)
assert.Equal(t, 0, captured)
})
t.Run("transformation with custom type", func(t *testing.T) {
type MyType struct {
Value string
}
var captured string
consumeString := func(s string) {
captured = s
}
extractValue := func(m MyType) string {
return m.Value
}
consumeMyType := Local(extractValue)(consumeString)
consumeMyType(MyType{Value: "test"})
assert.Equal(t, "test", captured)
})
t.Run("accumulation through multiple calls", func(t *testing.T) {
var sum int
accumulate := func(x int) {
sum += x
}
double := func(x int) int {
return x * 2
}
accumulateDoubled := Local(double)(accumulate)
accumulateDoubled(1)
accumulateDoubled(2)
accumulateDoubled(3)
assert.Equal(t, 12, sum) // (1*2) + (2*2) + (3*2) = 2 + 4 + 6 = 12
})
t.Run("transformation with error handling", func(t *testing.T) {
type Result struct {
Value int
Error error
}
var captured int
consumeInt := func(x int) {
captured = x
}
extractValue := func(r Result) int {
if r.Error != nil {
return -1
}
return r.Value
}
consumeResult := Local(extractValue)(consumeInt)
consumeResult(Result{Value: 42, Error: nil})
assert.Equal(t, 42, captured)
consumeResult(Result{Value: 100, Error: assert.AnError})
assert.Equal(t, -1, captured)
})
t.Run("transformation preserves consumer behavior", func(t *testing.T) {
callCount := 0
consumer := func(x int) {
callCount++
}
transform := func(s string) int {
n, _ := strconv.Atoi(s)
return n
}
transformedConsumer := Local(transform)(consumer)
transformedConsumer("1")
transformedConsumer("2")
transformedConsumer("3")
assert.Equal(t, 3, callCount)
})
t.Run("comparison with reader.Local behavior", func(t *testing.T) {
// This test demonstrates the dual nature of Consumer and Reader
// Consumer: transforms input before consumption (contravariant)
// Reader: transforms environment before reading (also contravariant on input)
type DetailedEnv struct {
Value int
Extra string
}
type SimpleEnv struct {
Value int
}
var captured int
consumeSimple := func(e SimpleEnv) {
captured = e.Value
}
simplify := func(d DetailedEnv) SimpleEnv {
return SimpleEnv{Value: d.Value}
}
consumeDetailed := Local(simplify)(consumeSimple)
consumeDetailed(DetailedEnv{Value: 42, Extra: "ignored"})
assert.Equal(t, 42, captured)
})
}

56
v2/consumer/types.go Normal file
View File

@@ -0,0 +1,56 @@
// 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 consumer provides types and utilities for functions that consume values without returning results.
//
// A Consumer represents a side-effecting operation that accepts a value but produces no output.
// This is useful for operations like logging, printing, updating state, or any action where
// the return value is not needed.
package consumer
type (
// Consumer represents a function that accepts a value of type A and performs a side effect.
// It does not return any value, making it useful for operations where only the side effect matters,
// such as logging, printing, or updating external state.
//
// This is a fundamental concept in functional programming for handling side effects in a
// controlled manner. Consumers can be composed, chained, or used in higher-order functions
// to build complex side-effecting behaviors.
//
// Type Parameters:
// - A: The type of value consumed by the function
//
// Example:
//
// // A simple consumer that prints values
// var printInt Consumer[int] = func(x int) {
// fmt.Println(x)
// }
// printInt(42) // Prints: 42
//
// // A consumer that logs messages
// var logger Consumer[string] = func(msg string) {
// log.Println(msg)
// }
// logger("Hello, World!") // Logs: Hello, World!
//
// // Consumers can be used in functional pipelines
// var saveToDatabase Consumer[User] = func(user User) {
// db.Save(user)
// }
Consumer[A any] = func(A)
Operator[A, B any] = func(Consumer[A]) Consumer[B]
)

View File

@@ -24,8 +24,8 @@ import (
// withContext wraps an existing IOEither and performs a context check for cancellation before delegating
func WithContext[A any](ctx context.Context, ma IOResult[A]) IOResult[A] {
return func() Result[A] {
if err := context.Cause(ctx); err != nil {
return result.Left[A](err)
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
return ma()
}

View File

@@ -0,0 +1,16 @@
package readerio
import (
RIO "github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func Bracket[
A, B, ANY any](
acquire ReaderIO[A],
use Kleisli[A, B],
release func(A, B) ReaderIO[ANY],
) ReaderIO[B] {
return RIO.Bracket(acquire, use, release)
}

View File

@@ -0,0 +1,13 @@
package readerio
import "github.com/IBM/fp-go/v2/io"
//go:inline
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
return ChainIOK(io.FromConsumerK(c))
}
//go:inline
func ChainFirstConsumer[A any](c Consumer[A]) Operator[A, A] {
return ChainFirstIOK(io.FromConsumerK(c))
}

View File

@@ -0,0 +1,20 @@
package readerio
import (
"context"
"github.com/IBM/fp-go/v2/reader"
RIO "github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func SequenceReader[R, A any](ma ReaderIO[Reader[R, A]]) Reader[R, ReaderIO[A]] {
return RIO.SequenceReader(ma)
}
//go:inline
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderIO[A]) Kleisli[R, B] {
return RIO.TraverseReader[context.Context](f)
}

View File

@@ -0,0 +1,29 @@
package readerio
import (
"context"
"log/slog"
"github.com/IBM/fp-go/v2/logging"
)
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) Kleisli[A, A] {
return func(a A) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
// logger
logger := cb(ctx)
return func() A {
logger.LogAttrs(ctx, logLevel, message, slog.Any("value", a))
return a
}
}
}
}
//go:inline
func SLog[A any](message string) Kleisli[A, A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}

View File

@@ -17,6 +17,7 @@ package readerio
import (
"context"
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/reader"
@@ -558,3 +559,197 @@ func TapReaderK[A, B any](f reader.Kleisli[context.Context, A, B]) Operator[A, A
func Read[A any](r context.Context) func(ReaderIO[A]) IO[A] {
return RIO.Read[A](r)
}
// Local transforms the context.Context environment before passing it to a ReaderIO computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerio.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerio.FromReader(func(ctx context.Context) string {
// if user := ctx.Value(userKey); user != nil {
// return user.(string)
// }
// return "unknown"
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// user := result(context.Background())() // Returns "Alice"
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerio.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderIO[A]) ReaderIO[A] {
return func(ctx context.Context) IO[A] {
return func() A {
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}
// WithTimeout adds a timeout to the context for a ReaderIO computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderIO is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{} after 5s timeout
//
// Successful Example:
//
// quickFetch := readerio.Of(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerio.WithTimeout[Data](5*time.Second),
// )
// data := result(context.Background())() // Returns Data{Value: "quick"}
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)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIO computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderIO
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Operation must complete by 3 PM
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
//
// fetchData := readerio.FromReader(func(ctx context.Context) Data {
// // Simulate operation
// select {
// case <-time.After(1 * time.Hour):
// return Data{Value: "done"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerio.WithDeadline[Data](deadline),
// )
// data := result(context.Background())() // Returns Data{} if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
// result := F.Pipe1(
// fetchData,
// readerio.WithDeadline[Data](laterDeadline),
// )
// data := 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)
})
}

View File

@@ -0,0 +1,25 @@
// 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 readerio
import (
"github.com/IBM/fp-go/v2/readerio"
)
//go:inline
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
return readerio.TailRec(f)
}

View File

@@ -18,6 +18,8 @@ package readerio
import (
"context"
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/reader"
@@ -66,4 +68,8 @@ type (
//
// Operator[A, B] is equivalent to func(ReaderIO[A]) func(context.Context) func() B
Operator[A, B any] = Kleisli[ReaderIO[A], B]
Consumer[A any] = consumer.Consumer[A]
Either[E, A any] = either.Either[E, A]
)

View File

@@ -0,0 +1,682 @@
# Sequence Functions and Point-Free Style Programming
This document explains how the `Sequence*` functions in the `context/readerioresult` package enable point-free style programming and improve code composition.
## Table of Contents
1. [What is Point-Free Style?](#what-is-point-free-style)
2. [The Problem: Nested Function Application](#the-problem-nested-function-application)
3. [The Solution: Sequence Functions](#the-solution-sequence-functions)
4. [How Sequence Enables Point-Free Style](#how-sequence-enables-point-free-style)
5. [TraverseReader: Introducing Dependencies](#traversereader-introducing-dependencies)
6. [Practical Benefits](#practical-benefits)
7. [Examples](#examples)
8. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
## What is Point-Free Style?
Point-free style (also called tacit programming) is a programming paradigm where function definitions don't explicitly mention their arguments. Instead, functions are composed using combinators and higher-order functions.
**Traditional style (with points):**
```go
func double(x int) int {
return x * 2
}
```
**Point-free style (without points):**
```go
var double = N.Mul(2)
```
The key benefit is that point-free style emphasizes **what** the function does (its transformation) rather than **how** it manipulates data.
## The Problem: Nested Function Application
In functional programming with monadic types like `ReaderIOResult`, we often have nested structures where we need to apply parameters in a specific order. Consider:
```go
type ReaderIOResult[A any] = func(context.Context) func() Either[error, A]
type Reader[R, A any] = func(R) A
// A computation that produces a Reader
type Computation = ReaderIOResult[Reader[Config, int]]
// Expands to: func(context.Context) func() Either[error, func(Config) int]
```
To use this, we must apply parameters in this order:
1. First, provide `context.Context`
2. Then, execute the IO effect (call the function)
3. Then, unwrap the `Either` to get the `Reader`
4. Finally, provide the `Config`
This creates several problems:
### Problem 1: Awkward Parameter Order
```go
computation := getComputation()
ctx := context.Background()
cfg := Config{Value: 42}
// Must apply in this specific order
result := computation(ctx)() // Get Either[error, Reader[Config, int]]
if reader, err := either.Unwrap(result); err == nil {
value := reader(cfg) // Finally apply Config
// use value
}
```
The `Config` parameter, which is often known early and stable, must be provided last. This prevents partial application and reuse.
### Problem 2: Cannot Partially Apply Dependencies
```go
// Want to do this: create a reusable computation with Config baked in
// But can't because Config comes last!
withConfig := computation(cfg) // ❌ Doesn't work - cfg comes last, not first
```
### Problem 3: Breaks Point-Free Composition
```go
// Want to compose like this:
var pipeline = F.Flow3(
getComputation,
applyConfig(cfg), // ❌ Can't do this - Config comes last
processResult,
)
```
## The Solution: Sequence Functions
The `Sequence*` functions solve this by "flipping" or "sequencing" the nested structure, changing the order in which parameters are applied.
### SequenceReader
```go
func SequenceReader[R, A any](
ma ReaderIOResult[Reader[R, A]]
) Kleisli[R, A]
```
**Type transformation:**
```
From: func(context.Context) func() Either[error, func(R) A]
To: func(R) func(context.Context) func() Either[error, A]
```
Now `R` (the Reader's environment) comes **first**, before `context.Context`!
### SequenceReaderIO
```go
func SequenceReaderIO[R, A any](
ma ReaderIOResult[ReaderIO[R, A]]
) Kleisli[R, A]
```
**Type transformation:**
```
From: func(context.Context) func() Either[error, func(R) func() A]
To: func(R) func(context.Context) func() Either[error, A]
```
### SequenceReaderResult
```go
func SequenceReaderResult[R, A any](
ma ReaderIOResult[ReaderResult[R, A]]
) Kleisli[R, A]
```
**Type transformation:**
```
From: func(context.Context) func() Either[error, func(R) Either[error, A]]
To: func(R) func(context.Context) func() Either[error, A]
```
## How Sequence Enables Point-Free Style
### 1. Partial Application
By moving the environment parameter first, we can partially apply it:
```go
type Config struct { Multiplier int }
computation := getComputation() // ReaderIOResult[Reader[Config, int]]
sequenced := SequenceReader[Config, int](computation)
// Partially apply Config
cfg := Config{Multiplier: 5}
withConfig := sequenced(cfg) // ✅ Now we have ReaderIOResult[int]
// Reuse with different contexts
result1 := withConfig(ctx1)()
result2 := withConfig(ctx2)()
```
### 2. Dependency Injection
Inject dependencies early in the pipeline:
```go
type Database struct { ConnectionString string }
makeQuery := func(ctx context.Context) func() Either[error, func(Database) string] {
// ... implementation
}
// Sequence to enable DI
queryWithDB := SequenceReader[Database, string](makeQuery)
// Inject database
db := Database{ConnectionString: "localhost:5432"}
query := queryWithDB(db) // ✅ Database injected
// Use query with any context
result := query(context.Background())()
```
### 3. Point-Free Composition
Build pipelines without mentioning intermediate values:
```go
var pipeline = F.Flow3(
getComputation, // ReaderIOResult[Reader[Config, int]]
SequenceReader[Config, int], // func(Config) ReaderIOResult[int]
applyConfig(cfg), // ReaderIOResult[int]
)
// Or with partial application:
var withConfig = F.Pipe1(
getComputation(),
SequenceReader[Config, int],
)
result := withConfig(cfg)(ctx)()
```
### 4. Reusable Computations
Create specialized versions of generic computations:
```go
// Generic computation
makeServiceInfo := func(ctx context.Context) func() Either[error, func(ServiceConfig) string] {
// ... implementation
}
sequenced := SequenceReader[ServiceConfig, string](makeServiceInfo)
// Create specialized versions
authService := sequenced(ServiceConfig{Name: "Auth", Version: "1.0"})
userService := sequenced(ServiceConfig{Name: "User", Version: "2.0"})
// Reuse across contexts
authInfo := authService(ctx)()
userInfo := userService(ctx)()
```
## TraverseReader: Introducing Dependencies
While `SequenceReader` flips the parameter order of an existing nested structure, `TraverseReader` allows you to **introduce** a new Reader dependency into an existing computation.
### Function Signature
```go
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderIOResult[A]) Kleisli[R, B]
```
**Type transformation:**
```
Input: ReaderIOResult[A] = func(context.Context) func() Either[error, A]
With: reader.Kleisli[R, A, B] = func(A) func(R) B
Output: Kleisli[R, B] = func(R) func(context.Context) func() Either[error, B]
```
### What It Does
`TraverseReader` takes:
1. A Reader-based transformation `f: func(A) func(R) B` that depends on environment `R`
2. Returns a function that transforms `ReaderIOResult[A]` into `Kleisli[R, B]`
This allows you to:
- Add environment dependencies to computations that don't have them yet
- Transform values within a ReaderIOResult using environment-dependent logic
- Build composable pipelines where transformations depend on configuration
### Key Difference from SequenceReader
- **SequenceReader**: Works with computations that **already contain** a Reader (`ReaderIOResult[Reader[R, A]]`)
- Flips the order so `R` comes first
- No transformation of the value itself
- **TraverseReader**: Works with computations that **don't have** a Reader yet (`ReaderIOResult[A]`)
- Introduces a new Reader dependency via a transformation function
- Transforms `A` to `B` using environment `R`
### Example: Adding Configuration to a Computation
```go
type Config struct {
Multiplier int
Prefix string
}
// Original computation that just produces an int
getValue := func(ctx context.Context) func() Either[error, int] {
return func() Either[error, int] {
return Right[error](10)
}
}
// A Reader-based transformation that depends on Config
formatWithConfig := func(n int) func(Config) string {
return func(cfg Config) string {
result := n * cfg.Multiplier
return fmt.Sprintf("%s: %d", cfg.Prefix, result)
}
}
// Use TraverseReader to introduce Config dependency
traversed := TraverseReader[Config, int, string](formatWithConfig)
withConfig := traversed(getValue)
// Now we can provide Config to get the final result
cfg := Config{Multiplier: 5, Prefix: "Result"}
ctx := context.Background()
result := withConfig(cfg)(ctx)() // Returns Right("Result: 50")
```
### Point-Free Composition with TraverseReader
```go
// Build a pipeline that introduces dependencies at each stage
var pipeline = F.Flow4(
loadValue, // ReaderIOResult[int]
TraverseReader(multiplyByConfig), // Kleisli[Config, int]
applyConfig(cfg), // ReaderIOResult[int]
Chain(TraverseReader(formatWithStyle)), // Introduce another dependency
)
```
### When to Use TraverseReader vs SequenceReader
**Use SequenceReader when:**
- Your computation already returns a Reader: `ReaderIOResult[Reader[R, A]]`
- You just want to flip the parameter order
- No transformation of the value is needed
```go
// Already have Reader[Config, int]
computation := getComputation() // ReaderIOResult[Reader[Config, int]]
sequenced := SequenceReader[Config, int](computation)
result := sequenced(cfg)(ctx)()
```
**Use TraverseReader when:**
- Your computation doesn't have a Reader yet: `ReaderIOResult[A]`
- You want to transform the value using environment-dependent logic
- You're introducing a new dependency into the pipeline
```go
// Have ReaderIOResult[int], want to add Config dependency
computation := getValue() // ReaderIOResult[int]
traversed := TraverseReader[Config, int, string](formatWithConfig)
withDep := traversed(computation)
result := withDep(cfg)(ctx)()
```
### Practical Example: Multi-Stage Processing
```go
type DatabaseConfig struct {
ConnectionString string
Timeout time.Duration
}
type FormattingConfig struct {
DateFormat string
Timezone string
}
// Stage 1: Load raw data (no dependencies yet)
loadData := func(ctx context.Context) func() Either[error, RawData] {
// ... implementation
}
// Stage 2: Process with database config
processWithDB := func(raw RawData) func(DatabaseConfig) ProcessedData {
return func(cfg DatabaseConfig) ProcessedData {
// Use cfg.ConnectionString, cfg.Timeout
return ProcessedData{/* ... */}
}
}
// Stage 3: Format with formatting config
formatData := func(processed ProcessedData) func(FormattingConfig) string {
return func(cfg FormattingConfig) string {
// Use cfg.DateFormat, cfg.Timezone
return "formatted result"
}
}
// Build pipeline introducing dependencies at each stage
var pipeline = F.Flow3(
loadData,
TraverseReader[DatabaseConfig, RawData, ProcessedData](processWithDB),
// Now we have Kleisli[DatabaseConfig, ProcessedData]
applyConfig(dbConfig),
// Now we have ReaderIOResult[ProcessedData]
TraverseReader[FormattingConfig, ProcessedData, string](formatData),
// Now we have Kleisli[FormattingConfig, string]
)
// Execute with both configs
result := pipeline(fmtConfig)(ctx)()
```
### Combining TraverseReader and SequenceReader
You can combine both functions in complex pipelines:
```go
// Start with nested Reader
computation := getComputation() // ReaderIOResult[Reader[Config, User]]
var pipeline = F.Flow4(
computation,
SequenceReader[Config, User], // Flip to get Kleisli[Config, User]
applyConfig(cfg), // Apply config, get ReaderIOResult[User]
TraverseReader(enrichWithDatabase), // Add database dependency
// Now have Kleisli[Database, EnrichedUser]
)
result := pipeline(db)(ctx)()
```
## Practical Benefits
### 1. **Improved Testability**
Inject test dependencies easily:
```go
// Production
prodDB := Database{ConnectionString: "prod:5432"}
prodQuery := queryWithDB(prodDB)
// Testing
testDB := Database{ConnectionString: "test:5432"}
testQuery := queryWithDB(testDB)
// Same computation, different dependencies
```
### 2. **Better Separation of Concerns**
Separate configuration from execution:
```go
// Configuration phase (pure, no effects)
cfg := loadConfig()
computation := sequenced(cfg)
// Execution phase (with effects)
result := computation(ctx)()
```
### 3. **Enhanced Composability**
Build complex pipelines from simple pieces:
```go
var processUser = F.Flow4(
loadUserConfig, // ReaderIOResult[Reader[Database, User]]
SequenceReader, // func(Database) ReaderIOResult[User]
applyDatabase(db), // ReaderIOResult[User]
Chain(validateUser), // ReaderIOResult[ValidatedUser]
)
```
### 4. **Reduced Boilerplate**
No need to manually thread parameters:
```go
// Without Sequence - manual threading
func processWithConfig(cfg Config) ReaderIOResult[Result] {
return func(ctx context.Context) func() Either[error, Result] {
return func() Either[error, Result] {
comp := getComputation()(ctx)()
if reader, err := either.Unwrap(comp); err == nil {
value := reader(cfg)
// ... more processing
}
// ... error handling
}
}
}
// With Sequence - point-free
var processWithConfig = F.Flow2(
getComputation,
SequenceReader[Config, Result],
)
```
## Examples
### Example 1: Database Query with Configuration
```go
type QueryConfig struct {
Timeout time.Duration
MaxRows int
}
type Database struct {
ConnectionString string
}
// Without Sequence
func executeQueryOld(cfg QueryConfig, db Database) ReaderIOResult[[]Row] {
return func(ctx context.Context) func() Either[error, []Row] {
return func() Either[error, []Row] {
// Must manually handle all parameters
// ...
}
}
}
// With Sequence
func makeQuery(ctx context.Context) func() Either[error, func(Database) []Row] {
return func() Either[error, func(Database) []Row] {
return Right[error](func(db Database) []Row {
// Implementation
return []Row{}
})
}
}
var executeQuery = F.Flow2(
makeQuery,
SequenceReader[Database, []Row],
)
// Usage
db := Database{ConnectionString: "localhost:5432"}
query := executeQuery(db)
result := query(ctx)()
```
### Example 2: Multi-Service Architecture
```go
type ServiceRegistry struct {
AuthService AuthService
UserService UserService
EmailService EmailService
}
// Create computations that depend on services
makeAuthCheck := func(ctx context.Context) func() Either[error, func(ServiceRegistry) bool] {
// ... implementation
}
makeSendEmail := func(ctx context.Context) func() Either[error, func(ServiceRegistry) error] {
// ... implementation
}
// Sequence them
authCheck := SequenceReader[ServiceRegistry, bool](makeAuthCheck)
sendEmail := SequenceReader[ServiceRegistry, error](makeSendEmail)
// Inject services once
registry := ServiceRegistry{ /* ... */ }
checkAuth := authCheck(registry)
sendMail := sendEmail(registry)
// Use with different contexts
if isAuth, _ := either.Unwrap(checkAuth(ctx1)()); isAuth {
sendMail(ctx2)()
}
```
### Example 3: Configuration-Driven Pipeline
```go
type PipelineConfig struct {
Stage1Config Stage1Config
Stage2Config Stage2Config
Stage3Config Stage3Config
}
// Define stages
stage1 := SequenceReader[Stage1Config, IntermediateResult1](makeStage1)
stage2 := SequenceReader[Stage2Config, IntermediateResult2](makeStage2)
stage3 := SequenceReader[Stage3Config, FinalResult](makeStage3)
// Build pipeline with configuration
func buildPipeline(cfg PipelineConfig) ReaderIOResult[FinalResult] {
return F.Pipe3(
stage1(cfg.Stage1Config),
Chain(func(r1 IntermediateResult1) ReaderIOResult[IntermediateResult2] {
return stage2(cfg.Stage2Config)
}),
Chain(func(r2 IntermediateResult2) ReaderIOResult[FinalResult] {
return stage3(cfg.Stage3Config)
}),
)
}
// Execute pipeline
cfg := loadPipelineConfig()
pipeline := buildPipeline(cfg)
result := pipeline(ctx)()
```
## Comparison: With and Without Sequence
### Without Sequence (Imperative Style)
```go
func processUser(userID string) ReaderIOResult[ProcessedUser] {
return func(ctx context.Context) func() Either[error, ProcessedUser] {
return func() Either[error, ProcessedUser] {
// Get database
dbComp := getDatabase()(ctx)()
if dbReader, err := either.Unwrap(dbComp); err != nil {
return Left[ProcessedUser](err)
}
db := dbReader(dbConfig)
// Get user
userComp := getUser(userID)(ctx)()
if userReader, err := either.Unwrap(userComp); err != nil {
return Left[ProcessedUser](err)
}
user := userReader(db)
// Process user
processComp := processUserData(user)(ctx)()
if processReader, err := either.Unwrap(processComp); err != nil {
return Left[ProcessedUser](err)
}
result := processReader(processingConfig)
return Right[error](result)
}
}
}
```
### With Sequence (Point-Free Style)
```go
var processUser = func(userID string) ReaderIOResult[ProcessedUser] {
return F.Pipe3(
getDatabase,
SequenceReader[DatabaseConfig, Database],
applyConfig(dbConfig),
Chain(func(db Database) ReaderIOResult[User] {
return F.Pipe2(
getUser(userID),
SequenceReader[Database, User],
applyDB(db),
)
}),
Chain(func(user User) ReaderIOResult[ProcessedUser] {
return F.Pipe2(
processUserData(user),
SequenceReader[ProcessingConfig, ProcessedUser],
applyConfig(processingConfig),
)
}),
)
}
```
## Key Takeaways
1. **Sequence functions flip parameter order** to enable partial application
2. **Dependencies come first**, making them easy to inject and test
3. **Point-free style** becomes natural and readable
4. **Composition** is enhanced through proper parameter ordering
5. **Reusability** increases as computations can be specialized early
6. **Testability** improves through easy dependency injection
7. **Separation of concerns** is clearer (configuration vs. execution)
## When to Use Sequence
Use `Sequence*` functions when:
- ✅ You want to partially apply environment/configuration parameters
- ✅ You're building reusable computations with injected dependencies
- ✅ You need to test with different dependency implementations
- ✅ You're composing complex pipelines in point-free style
- ✅ You want to separate configuration from execution
- ✅ You're working with nested Reader-like structures
Don't use `Sequence*` when:
- ❌ The original parameter order is already optimal
- ❌ You're not doing any composition or partial application
- ❌ The added abstraction doesn't provide value
- ❌ The code is simpler without it
## Conclusion
The `Sequence*` functions are powerful tools for enabling point-free style programming in Go. By flipping the parameter order of nested monadic structures, they make it easy to:
- Partially apply dependencies
- Build composable pipelines
- Improve testability
- Write more declarative code
While they add a layer of abstraction, the benefits in terms of code reusability, testability, and composability make them invaluable for functional programming in Go.

View File

@@ -18,6 +18,7 @@ package readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/context/readerio"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/apply"
"github.com/IBM/fp-go/v2/io"
@@ -25,7 +26,6 @@ import (
"github.com/IBM/fp-go/v2/ioresult"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
)
@@ -96,7 +96,7 @@ func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Operator[S1, S2] {
return RIOR.Bind(setter, f)
return RIOR.Bind(setter, F.Flow2(f, WithContext))
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
@@ -256,7 +256,7 @@ func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return RIOR.BindL(lens, f)
return RIOR.BindL(lens, F.Flow2(f, WithContext))
}
// LetL is a variant of Let that uses a lens to focus on a specific part of the context.
@@ -290,7 +290,7 @@ func BindL[S, T any](
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
f Endomorphism[T],
) Operator[S, S] {
return RIOR.LetL[context.Context](lens, f)
}
@@ -398,7 +398,7 @@ func BindReaderK[S1, S2, T any](
//go:inline
func BindReaderIOK[S1, S2, T any](
setter func(T) func(S1) S2,
f readerio.Kleisli[context.Context, S1, T],
f readerio.Kleisli[S1, T],
) Operator[S1, S2] {
return Bind(setter, F.Flow2(f, FromReaderIO[T]))
}
@@ -507,7 +507,7 @@ func BindReaderKL[S, T any](
//go:inline
func BindReaderIOKL[S, T any](
lens L.Lens[S, T],
f readerio.Kleisli[context.Context, T, T],
f readerio.Kleisli[T, T],
) Operator[S, S] {
return BindL(lens, F.Flow2(f, FromReaderIO[T]))
}

View File

@@ -203,9 +203,7 @@ func TestApS_EmptyState(t *testing.T) {
result := res(t.Context())()
assert.True(t, E.IsRight(result))
emptyOpt := E.ToOption(result)
assert.True(t, O.IsSome(emptyOpt))
empty, _ := O.Unwrap(emptyOpt)
assert.Equal(t, Empty{}, empty)
assert.Equal(t, O.Of(Empty{}), emptyOpt)
}
func TestApS_ChainedWithBind(t *testing.T) {

View File

@@ -16,11 +16,14 @@
package readerioresult
import (
F "github.com/IBM/fp-go/v2/function"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
// whether the body action returns and error or not.
//
//go:inline
func Bracket[
A, B, ANY any](
@@ -28,5 +31,5 @@ func Bracket[
use Kleisli[A, B],
release func(A, Either[B]) ReaderIOResult[ANY],
) ReaderIOResult[B] {
return RIOR.Bracket(acquire, use, release)
return RIOR.Bracket(acquire, F.Flow2(use, WithContext), release)
}

View File

@@ -34,8 +34,8 @@ import (
// Returns a ReaderIOResult that checks for cancellation before executing.
func WithContext[A any](ma ReaderIOResult[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOEither[A] {
if err := context.Cause(ctx); err != nil {
return ioeither.Left[A](err)
if ctx.Err() != nil {
return ioeither.Left[A](context.Cause(ctx))
}
return CIOE.WithContext(ctx, ma(ctx))
}

View File

@@ -0,0 +1,13 @@
package readerioresult
import "github.com/IBM/fp-go/v2/io"
//go:inline
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
return ChainIOK(io.FromConsumerK(c))
}
//go:inline
func ChainFirstConsumer[A any](c Consumer[A]) Operator[A, A] {
return ChainFirstIOK(io.FromConsumerK(c))
}

View File

@@ -0,0 +1,295 @@
// 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 readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/reader"
RIO "github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
RR "github.com/IBM/fp-go/v2/readerresult"
)
// SequenceReader transforms a ReaderIOResult containing a Reader into a function that
// takes the Reader's environment first, then returns a ReaderIOResult.
//
// This function "flips" or "sequences" the nested structure, changing the order in which
// parameters are applied. It's particularly useful for point-free style programming where
// you want to partially apply the inner Reader's environment before dealing with the
// outer context.
//
// Type transformation:
//
// From: ReaderIOResult[Reader[R, A]]
// = func(context.Context) func() Either[error, func(R) A]
//
// To: func(context.Context) func(R) IOResult[A]
// = func(context.Context) func(R) func() Either[error, A]
//
// This allows you to:
// 1. Provide the context.Context first
// 2. Then provide the Reader's environment R
// 3. Finally execute the IO effect to get Either[error, A]
//
// Point-free style benefits:
// - Enables partial application of the Reader environment
// - Facilitates composition of Reader-based computations
// - Allows building reusable computation pipelines
// - Supports dependency injection patterns where R represents dependencies
//
// Example:
//
// type Config struct {
// Timeout int
// }
//
// // A computation that produces a Reader based on context
// func getMultiplier(ctx context.Context) func() Either[error, func(Config) int] {
// return func() Either[error, func(Config) int] {
// return Right[error](func(cfg Config) int {
// return cfg.Timeout * 2
// })
// }
// }
//
// // Sequence it to apply Config first
// sequenced := SequenceReader[Config, int](getMultiplier)
//
// // Now we can partially apply the Config
// cfg := Config{Timeout: 30}
// ctx := context.Background()
// result := sequenced(ctx)(cfg)() // Returns Right(60)
//
// This is especially useful in point-free style when building computation pipelines:
//
// var pipeline = F.Flow3(
// loadConfig, // ReaderIOResult[Reader[Database, Config]]
// SequenceReader, // func(context.Context) func(Database) IOResult[Config]
// applyToDatabase(db), // IOResult[Config]
// )
//
//go:inline
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
return RIOR.SequenceReader(ma)
}
// SequenceReaderIO transforms a ReaderIOResult containing a ReaderIO into a function that
// takes the ReaderIO's environment first, then returns a ReaderIOResult.
//
// This is similar to SequenceReader but works with ReaderIO, which represents a computation
// that depends on an environment R and performs IO effects.
//
// Type transformation:
//
// From: ReaderIOResult[ReaderIO[R, A]]
// = func(context.Context) func() Either[error, func(R) func() A]
//
// To: func(context.Context) func(R) IOResult[A]
// = func(context.Context) func(R) func() Either[error, A]
//
// The key difference from SequenceReader is that the inner computation (ReaderIO) already
// performs IO effects, so the sequencing combines these effects properly.
//
// Point-free style benefits:
// - Enables composition of ReaderIO-based computations
// - Allows partial application of environment before IO execution
// - Facilitates building effect pipelines with dependency injection
// - Supports layered architecture where R represents service dependencies
//
// Example:
//
// type Database struct {
// ConnectionString string
// }
//
// // A computation that produces a ReaderIO based on context
// func getQuery(ctx context.Context) func() Either[error, func(Database) func() string] {
// return func() Either[error, func(Database) func() string] {
// return Right[error](func(db Database) func() string {
// return func() string {
// // Perform actual IO here
// return "Query result from " + db.ConnectionString
// }
// })
// }
// }
//
// // Sequence it to apply Database first
// sequenced := SequenceReaderIO[Database, string](getQuery)
//
// // Partially apply the Database
// db := Database{ConnectionString: "localhost:5432"}
// ctx := context.Background()
// result := sequenced(ctx)(db)() // Executes IO and returns Right("Query result...")
//
// In point-free style, this enables clean composition:
//
// var executeQuery = F.Flow3(
// prepareQuery, // ReaderIOResult[ReaderIO[Database, QueryResult]]
// SequenceReaderIO, // func(context.Context) func(Database) IOResult[QueryResult]
// withDatabase(db), // IOResult[QueryResult]
// )
//
//go:inline
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R, A] {
return RIOR.SequenceReaderIO(ma)
}
// SequenceReaderResult transforms a ReaderIOResult containing a ReaderResult into a function
// that takes the ReaderResult's environment first, then returns a ReaderIOResult.
//
// This is similar to SequenceReader but works with ReaderResult, which represents a computation
// that depends on an environment R and can fail with an error.
//
// Type transformation:
//
// From: ReaderIOResult[ReaderResult[R, A]]
// = func(context.Context) func() Either[error, func(R) Either[error, A]]
//
// To: func(context.Context) func(R) IOResult[A]
// = func(context.Context) func(R) func() Either[error, A]
//
// The sequencing properly combines the error handling from both the outer ReaderIOResult
// and the inner ReaderResult, ensuring that errors from either level are propagated correctly.
//
// Point-free style benefits:
// - Enables composition of error-handling computations with dependency injection
// - Allows partial application of dependencies before error handling
// - Facilitates building validation pipelines with environment dependencies
// - Supports service-oriented architectures with proper error propagation
//
// Example:
//
// type Config struct {
// MaxRetries int
// }
//
// // A computation that produces a ReaderResult based on context
// func validateRetries(ctx context.Context) func() Either[error, func(Config) Either[error, int]] {
// return func() Either[error, func(Config) Either[error, int]] {
// return Right[error](func(cfg Config) Either[error, int] {
// if cfg.MaxRetries < 0 {
// return Left[int](errors.New("negative retries"))
// }
// return Right[error](cfg.MaxRetries)
// })
// }
// }
//
// // Sequence it to apply Config first
// sequenced := SequenceReaderResult[Config, int](validateRetries)
//
// // Partially apply the Config
// cfg := Config{MaxRetries: 3}
// ctx := context.Background()
// result := sequenced(ctx)(cfg)() // Returns Right(3)
//
// // With invalid config
// badCfg := Config{MaxRetries: -1}
// badResult := sequenced(ctx)(badCfg)() // Returns Left(error("negative retries"))
//
// In point-free style, this enables validation pipelines:
//
// var validateAndProcess = F.Flow4(
// loadConfig, // ReaderIOResult[ReaderResult[Config, Settings]]
// SequenceReaderResult, // func(context.Context) func(Config) IOResult[Settings]
// applyConfig(cfg), // IOResult[Settings]
// Chain(processSettings), // IOResult[Result]
// )
//
//go:inline
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kleisli[R, A] {
return RIOR.SequenceReaderEither(ma)
}
// TraverseReader transforms a ReaderIOResult computation by applying a Reader-based function,
// effectively introducing a new environment dependency.
//
// This function takes a Reader-based transformation (Kleisli arrow) and returns a function that
// can transform a ReaderIOResult. The result allows you to provide the Reader's environment (R)
// first, which then produces a ReaderIOResult that depends on the context.
//
// Type transformation:
//
// From: ReaderIOResult[A]
// = func(context.Context) func() Either[error, A]
//
// With: reader.Kleisli[R, A, B]
// = func(A) func(R) B
//
// To: func(ReaderIOResult[A]) func(R) ReaderIOResult[B]
// = func(ReaderIOResult[A]) func(R) func(context.Context) func() Either[error, B]
//
// This enables:
// 1. Transforming values within a ReaderIOResult using environment-dependent logic
// 2. Introducing new environment dependencies into existing computations
// 3. Building composable pipelines where transformations depend on configuration or dependencies
// 4. Point-free style composition with Reader-based transformations
//
// Type Parameters:
// - R: The environment type that the Reader depends on
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A Reader-based Kleisli arrow that transforms A to B using environment R
//
// Returns:
// - A function that takes a ReaderIOResult[A] and returns a Kleisli[R, B],
// which is func(R) ReaderIOResult[B]
//
// The function preserves error handling and IO effects while adding the Reader environment dependency.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // A Reader-based transformation that depends on Config
// multiply := func(x int) func(Config) int {
// return func(cfg Config) int {
// return x * cfg.Multiplier
// }
// }
//
// // Original computation that produces an int
// computation := Right[int](10)
//
// // Apply TraverseReader to introduce Config dependency
// traversed := TraverseReader[Config, int, int](multiply)
// result := traversed(computation)
//
// // Now we can provide the Config to get the final result
// cfg := Config{Multiplier: 5}
// ctx := context.Background()
// finalResult := result(cfg)(ctx)() // Returns Right(50)
//
// In point-free style, this enables clean composition:
//
// var pipeline = F.Flow3(
// loadValue, // ReaderIOResult[int]
// TraverseReader(multiplyByConfig), // func(Config) ReaderIOResult[int]
// applyConfig(cfg), // ReaderIOResult[int]
// )
//
//go:inline
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderIOResult[A]) Kleisli[R, B] {
return RIOR.TraverseReader[context.Context](f)
}

View File

@@ -0,0 +1,333 @@
// 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 readerioresult_test
import (
"context"
"fmt"
RIOE "github.com/IBM/fp-go/v2/context/readerioresult"
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
)
// Example_sequenceReader_basicUsage demonstrates the basic usage of SequenceReader
// to flip the parameter order, enabling point-free style programming.
func Example_sequenceReader_basicUsage() {
type Config struct {
Multiplier int
}
// A computation that produces a Reader based on context
getComputation := func(ctx context.Context) func() either.Either[error, func(Config) int] {
return func() either.Either[error, func(Config) int] {
// This could check context for cancellation, deadlines, etc.
return either.Right[error](func(cfg Config) int {
return cfg.Multiplier * 10
})
}
}
// Sequence it to flip the parameter order
// Now Config comes first, then context
sequenced := RIOE.SequenceReader(getComputation)
// Partially apply the Config - this is the key benefit for point-free style
cfg := Config{Multiplier: 5}
withConfig := sequenced(cfg)
// Now we have a ReaderIOResult[int] that can be used with any context
ctx := context.Background()
result := withConfig(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println(value)
}
// Output: 50
}
// Example_sequenceReader_dependencyInjection demonstrates how SequenceReader
// enables clean dependency injection patterns in point-free style.
func Example_sequenceReader_dependencyInjection() {
// Define our dependencies
type Database struct {
ConnectionString string
}
type UserService struct {
db Database
}
// A function that creates a computation requiring a Database
makeQuery := func(ctx context.Context) func() either.Either[error, func(Database) string] {
return func() either.Either[error, func(Database) string] {
return either.Right[error](func(db Database) string {
return fmt.Sprintf("Querying %s", db.ConnectionString)
})
}
}
// Sequence to enable dependency injection
queryWithDB := RIOE.SequenceReader(makeQuery)
// Inject the database dependency
db := Database{ConnectionString: "localhost:5432"}
query := queryWithDB(db)
// Execute with context
ctx := context.Background()
result := query(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println(value)
}
// Output: Querying localhost:5432
}
// Example_sequenceReader_pointFreeComposition demonstrates how SequenceReader
// enables point-free style composition of computations.
func Example_sequenceReader_pointFreeComposition() {
type Config struct {
BaseValue int
}
// Step 1: Create a computation that produces a Reader
step1 := func(ctx context.Context) func() either.Either[error, func(Config) int] {
return func() either.Either[error, func(Config) int] {
return either.Right[error](func(cfg Config) int {
return cfg.BaseValue * 2
})
}
}
// Step 2: Sequence it to enable partial application
sequenced := RIOE.SequenceReader(step1)
// Step 3: Build a pipeline using point-free style
// Partially apply the config
cfg := Config{BaseValue: 10}
// Create a reusable computation with the config baked in
computation := F.Pipe1(
sequenced(cfg),
RIOE.Map(func(x int) int { return x + 5 }),
)
// Execute the pipeline
ctx := context.Background()
result := computation(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println(value)
}
// Output: 25
}
// Example_sequenceReader_multipleEnvironments demonstrates using SequenceReader
// to work with multiple environment types in a clean, composable way.
func Example_sequenceReader_multipleEnvironments() {
type DatabaseConfig struct {
Host string
Port int
}
type APIConfig struct {
Endpoint string
APIKey string
}
// Function that needs DatabaseConfig
getDatabaseURL := func(ctx context.Context) func() either.Either[error, func(DatabaseConfig) string] {
return func() either.Either[error, func(DatabaseConfig) string] {
return either.Right[error](func(cfg DatabaseConfig) string {
return fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
})
}
}
// Function that needs APIConfig
getAPIURL := func(ctx context.Context) func() either.Either[error, func(APIConfig) string] {
return func() either.Either[error, func(APIConfig) string] {
return either.Right[error](func(cfg APIConfig) string {
return cfg.Endpoint
})
}
}
// Sequence both to enable partial application
withDBConfig := RIOE.SequenceReader(getDatabaseURL)
withAPIConfig := RIOE.SequenceReader(getAPIURL)
// Partially apply different configs
dbCfg := DatabaseConfig{Host: "localhost", Port: 5432}
apiCfg := APIConfig{Endpoint: "https://api.example.com", APIKey: "secret"}
dbQuery := withDBConfig(dbCfg)
apiQuery := withAPIConfig(apiCfg)
// Execute both with the same context
ctx := context.Background()
dbResult := dbQuery(ctx)()
apiResult := apiQuery(ctx)()
if dbURL, err := either.Unwrap(dbResult); err == nil {
fmt.Println("Database:", dbURL)
}
if apiURL, err := either.Unwrap(apiResult); err == nil {
fmt.Println("API:", apiURL)
}
// Output:
// Database: localhost:5432
// API: https://api.example.com
}
// Example_sequenceReaderResult_errorHandling demonstrates how SequenceReaderResult
// enables point-free style with proper error handling at multiple levels.
func Example_sequenceReaderResult_errorHandling() {
type ValidationConfig struct {
MinValue int
MaxValue int
}
// A computation that can fail at both outer and inner levels
makeValidator := func(ctx context.Context) func() either.Either[error, func(context.Context) either.Either[error, int]] {
return func() either.Either[error, func(context.Context) either.Either[error, int]] {
// Outer level: check context
if ctx.Err() != nil {
return either.Left[func(context.Context) either.Either[error, int]](ctx.Err())
}
// Return inner computation
return either.Right[error](func(innerCtx context.Context) either.Either[error, int] {
// Inner level: perform validation
value := 42
if value < 0 {
return either.Left[int](fmt.Errorf("value too small: %d", value))
}
if value > 100 {
return either.Left[int](fmt.Errorf("value too large: %d", value))
}
return either.Right[error](value)
})
}
}
// Sequence to enable point-free composition
sequenced := RIOE.SequenceReaderResult(makeValidator)
// Build a pipeline with error handling
ctx := context.Background()
pipeline := F.Pipe2(
sequenced(ctx),
RIOE.Map(func(x int) int { return x * 2 }),
RIOE.Chain(func(x int) RIOE.ReaderIOResult[string] {
return RIOE.Of(fmt.Sprintf("Result: %d", x))
}),
)
result := pipeline(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println(value)
}
// Output: Result: 84
}
// Example_sequenceReader_partialApplication demonstrates the power of partial
// application enabled by SequenceReader for building reusable computations.
func Example_sequenceReader_partialApplication() {
type ServiceConfig struct {
ServiceName string
Version string
}
// Create a computation factory
makeServiceInfo := func(ctx context.Context) func() either.Either[error, func(ServiceConfig) string] {
return func() either.Either[error, func(ServiceConfig) string] {
return either.Right[error](func(cfg ServiceConfig) string {
return fmt.Sprintf("%s v%s", cfg.ServiceName, cfg.Version)
})
}
}
// Sequence it
sequenced := RIOE.SequenceReader(makeServiceInfo)
// Create multiple service configurations
authConfig := ServiceConfig{ServiceName: "AuthService", Version: "1.0.0"}
userConfig := ServiceConfig{ServiceName: "UserService", Version: "2.1.0"}
// Partially apply each config to create specialized computations
getAuthInfo := sequenced(authConfig)
getUserInfo := sequenced(userConfig)
// These can now be reused across different contexts
ctx := context.Background()
authResult := getAuthInfo(ctx)()
userResult := getUserInfo(ctx)()
if auth, err := either.Unwrap(authResult); err == nil {
fmt.Println(auth)
}
if user, err := either.Unwrap(userResult); err == nil {
fmt.Println(user)
}
// Output:
// AuthService v1.0.0
// UserService v2.1.0
}
// Example_sequenceReader_testingBenefits demonstrates how SequenceReader
// makes testing easier by allowing you to inject test dependencies.
func Example_sequenceReader_testingBenefits() {
// Simple logger that collects messages
type SimpleLogger struct {
Messages []string
}
// A computation that depends on a logger (using the struct directly)
makeLoggingOperation := func(ctx context.Context) func() either.Either[error, func(*SimpleLogger) string] {
return func() either.Either[error, func(*SimpleLogger) string] {
return either.Right[error](func(logger *SimpleLogger) string {
logger.Messages = append(logger.Messages, "Operation started")
result := "Success"
logger.Messages = append(logger.Messages, fmt.Sprintf("Operation completed: %s", result))
return result
})
}
}
// Sequence to enable dependency injection
sequenced := RIOE.SequenceReader(makeLoggingOperation)
// Inject a test logger
testLogger := &SimpleLogger{Messages: []string{}}
operation := sequenced(testLogger)
// Execute
ctx := context.Background()
result := operation(ctx)()
if value, err := either.Unwrap(result); err == nil {
fmt.Println("Result:", value)
fmt.Println("Logs:", len(testLogger.Messages))
}
// Output:
// Result: Success
// Logs: 2
}

View File

@@ -0,0 +1,866 @@
// 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 readerioresult
import (
"context"
"errors"
"fmt"
"testing"
"github.com/IBM/fp-go/v2/either"
"github.com/stretchr/testify/assert"
)
func TestSequenceReader(t *testing.T) {
t.Run("flips parameter order for simple types", func(t *testing.T) {
// Original: ReaderIOResult[Reader[string, int]]
// = func(context.Context) func() Either[error, func(string) int]
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Right[error](func(s string) int {
return 10 + len(s)
})
}
}
// Sequenced: func(string) func(context.Context) IOResult[int]
// The Reader environment (string) is now the first parameter
sequenced := SequenceReader(original)
ctx := context.Background()
// Test original
result1 := original(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1("hello")
assert.Equal(t, 15, value1)
// Test sequenced - note the flipped order: string first, then context
result2 := sequenced("hello")(ctx)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, 15, value2)
})
t.Run("flips parameter order for struct types", func(t *testing.T) {
type Database struct {
ConnectionString string
}
// Original: ReaderIOResult[Reader[Database, string]]
query := func(ctx context.Context) func() Either[Reader[Database, string]] {
return func() Either[Reader[Database, string]] {
if ctx.Err() != nil {
return either.Left[Reader[Database, string]](ctx.Err())
}
return either.Right[error](func(db Database) string {
return fmt.Sprintf("Query on %s", db.ConnectionString)
})
}
}
db := Database{ConnectionString: "localhost:5432"}
ctx := context.Background()
expected := "Query on localhost:5432"
// Sequence it
sequenced := SequenceReader(query)
// Test original with valid inputs
result1 := query(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1(db)
assert.Equal(t, expected, value1)
// Test sequenced with valid inputs - Database first, then context
result2 := sequenced(db)(ctx)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, expected, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
// Original that fails at outer level
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Left[Reader[string, int]](expectedError)
}
}
ctx := context.Background()
// Test original with error
result1 := original(ctx)()
assert.True(t, either.IsLeft(result1))
_, err1 := either.Unwrap(result1)
assert.Equal(t, expectedError, err1)
// Test sequenced - the outer error is preserved
sequenced := SequenceReader(original)
result2 := sequenced("test")(ctx)()
assert.True(t, either.IsLeft(result2))
_, err2 := either.Unwrap(result2)
assert.Equal(t, expectedError, err2)
})
t.Run("preserves computation logic", func(t *testing.T) {
// Original function
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Right[error](func(s string) int {
return 3 * len(s)
})
}
}
ctx := context.Background()
// Sequence
sequenced := SequenceReader(original)
// Test that sequence produces correct results
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1("test")
result2 := sequenced("test")(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, value1, value2)
assert.Equal(t, 12, value2) // 3 * 4
})
t.Run("works with zero values", func(t *testing.T) {
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Right[error](func(s string) int {
return len(s)
})
}
}
ctx := context.Background()
sequenced := SequenceReader(original)
// Test with zero values
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1("")
assert.Equal(t, 0, value1)
result2 := sequenced("")(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, 0, value2)
})
t.Run("respects context cancellation", func(t *testing.T) {
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
if ctx.Err() != nil {
return either.Left[Reader[string, int]](ctx.Err())
}
return either.Right[error](func(s string) int {
return len(s)
})
}
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
sequenced := SequenceReader(original)
result := sequenced("test")(ctx)()
assert.True(t, either.IsLeft(result))
_, err := either.Unwrap(result)
assert.Equal(t, context.Canceled, err)
})
t.Run("enables point-free style with partial application", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := func(ctx context.Context) func() Either[Reader[Config, int]] {
return func() Either[Reader[Config, int]] {
return either.Right[error](func(cfg Config) int {
return cfg.Multiplier * 10
})
}
}
// Sequence to enable partial application
sequenced := SequenceReader(original)
// Partially apply the Config
cfg := Config{Multiplier: 5}
withConfig := sequenced(cfg)
// Now we have a ReaderIOResult[int] that can be used in different contexts
ctx1 := context.Background()
result1 := withConfig(ctx1)()
assert.True(t, either.IsRight(result1))
value1, _ := either.Unwrap(result1)
assert.Equal(t, 50, value1)
// Can reuse with different context
ctx2 := context.Background()
result2 := withConfig(ctx2)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, 50, value2)
})
}
func TestSequenceReaderIO(t *testing.T) {
t.Run("flips parameter order for simple types", func(t *testing.T) {
// Original: ReaderIOResult[ReaderIO[int]]
// = func(context.Context) func() Either[error, func(context.Context) func() int]
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
return func() Either[ReaderIO[int]] {
return either.Right[error](func(innerCtx context.Context) func() int {
return func() int {
return 20
}
})
}
}
ctx := context.Background()
sequenced := SequenceReaderIO(original)
// Test original
result1 := original(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1(ctx)()
assert.Equal(t, 20, value1)
// Test sequenced - context first, then context again for inner ReaderIO
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, 20, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
// Original that fails at outer level
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
return func() Either[ReaderIO[int]] {
return either.Left[ReaderIO[int]](expectedError)
}
}
ctx := context.Background()
// Test original with error
result1 := original(ctx)()
assert.True(t, either.IsLeft(result1))
_, err1 := either.Unwrap(result1)
assert.Equal(t, expectedError, err1)
// Test sequenced - the outer error is preserved
sequenced := SequenceReaderIO(original)
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result2))
_, err2 := either.Unwrap(result2)
assert.Equal(t, expectedError, err2)
})
t.Run("respects context cancellation in outer context", func(t *testing.T) {
original := func(ctx context.Context) func() Either[ReaderIO[int]] {
return func() Either[ReaderIO[int]] {
if ctx.Err() != nil {
return either.Left[ReaderIO[int]](ctx.Err())
}
return either.Right[error](func(innerCtx context.Context) func() int {
return func() int {
return 20
}
})
}
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
sequenced := SequenceReaderIO(original)
result := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result))
_, err := either.Unwrap(result)
assert.Equal(t, context.Canceled, err)
})
}
func TestSequenceReaderResult(t *testing.T) {
t.Run("flips parameter order for simple types", func(t *testing.T) {
// Original: ReaderIOResult[ReaderResult[int]]
// = func(context.Context) func() Either[error, func(context.Context) Either[error, int]]
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
return either.Right[error](func(innerCtx context.Context) Either[int] {
return either.Right[error](20)
})
}
}
ctx := context.Background()
sequenced := SequenceReaderResult(original)
// Test original
result1 := original(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
innerResult1 := innerFunc1(ctx)
assert.True(t, either.IsRight(innerResult1))
value1, _ := either.Unwrap(innerResult1)
assert.Equal(t, 20, value1)
// Test sequenced
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsRight(result2))
value2, _ := either.Unwrap(result2)
assert.Equal(t, 20, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
// Original that fails at outer level
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
return either.Left[ReaderResult[int]](expectedError)
}
}
ctx := context.Background()
// Test original with error
result1 := original(ctx)()
assert.True(t, either.IsLeft(result1))
_, err1 := either.Unwrap(result1)
assert.Equal(t, expectedError, err1)
// Test sequenced - the outer error is preserved
sequenced := SequenceReaderResult(original)
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result2))
_, err2 := either.Unwrap(result2)
assert.Equal(t, expectedError, err2)
})
t.Run("preserves inner error", func(t *testing.T) {
expectedError := errors.New("inner error")
// Original that fails at inner level
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
return either.Right[error](func(innerCtx context.Context) Either[int] {
return either.Left[int](expectedError)
})
}
}
ctx := context.Background()
// Test original with inner error
result1 := original(ctx)()
assert.True(t, either.IsRight(result1))
innerFunc1, _ := either.Unwrap(result1)
innerResult1 := innerFunc1(ctx)
assert.True(t, either.IsLeft(innerResult1))
_, innerErr1 := either.Unwrap(innerResult1)
assert.Equal(t, expectedError, innerErr1)
// Test sequenced with inner error
sequenced := SequenceReaderResult(original)
result2 := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result2))
_, innerErr2 := either.Unwrap(result2)
assert.Equal(t, expectedError, innerErr2)
})
t.Run("handles errors at different levels", func(t *testing.T) {
// Original that can fail at both levels
makeOriginal := func(x int) ReaderIOResult[ReaderResult[int]] {
return func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
if x < -10 {
return either.Left[ReaderResult[int]](errors.New("outer: too negative"))
}
return either.Right[error](func(innerCtx context.Context) Either[int] {
if x < 0 {
return either.Left[int](errors.New("inner: negative value"))
}
return either.Right[error](x * 2)
})
}
}
}
ctx := context.Background()
// Test outer error
sequenced1 := SequenceReaderResult(makeOriginal(-20))
result1 := sequenced1(ctx)(ctx)()
assert.True(t, either.IsLeft(result1))
_, err1 := either.Unwrap(result1)
assert.Contains(t, err1.Error(), "outer")
// Test inner error
sequenced2 := SequenceReaderResult(makeOriginal(-5))
result2 := sequenced2(ctx)(ctx)()
assert.True(t, either.IsLeft(result2))
_, err2 := either.Unwrap(result2)
assert.Contains(t, err2.Error(), "inner")
// Test success
sequenced3 := SequenceReaderResult(makeOriginal(10))
result3 := sequenced3(ctx)(ctx)()
assert.True(t, either.IsRight(result3))
value3, _ := either.Unwrap(result3)
assert.Equal(t, 20, value3)
})
t.Run("respects context cancellation", func(t *testing.T) {
original := func(ctx context.Context) func() Either[ReaderResult[int]] {
return func() Either[ReaderResult[int]] {
if ctx.Err() != nil {
return either.Left[ReaderResult[int]](ctx.Err())
}
return either.Right[error](func(innerCtx context.Context) Either[int] {
if innerCtx.Err() != nil {
return either.Left[int](innerCtx.Err())
}
return either.Right[error](20)
})
}
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
sequenced := SequenceReaderResult(original)
result := sequenced(ctx)(ctx)()
assert.True(t, either.IsLeft(result))
_, err := either.Unwrap(result)
assert.Equal(t, context.Canceled, err)
})
}
func TestSequenceEdgeCases(t *testing.T) {
t.Run("works with empty struct", func(t *testing.T) {
type Empty struct{}
original := func(ctx context.Context) func() Either[Reader[Empty, int]] {
return func() Either[Reader[Empty, int]] {
return either.Right[error](func(e Empty) int {
return 20
})
}
}
ctx := context.Background()
empty := Empty{}
sequenced := SequenceReader(original)
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1(empty)
assert.Equal(t, 20, value1)
result2 := sequenced(empty)(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, 20, value2)
})
t.Run("works with pointer types", func(t *testing.T) {
type Data struct {
Value int
}
original := func(ctx context.Context) func() Either[Reader[*Data, int]] {
return func() Either[Reader[*Data, int]] {
return either.Right[error](func(d *Data) int {
if d == nil {
return 42
}
return 42 + d.Value
})
}
}
ctx := context.Background()
data := &Data{Value: 100}
sequenced := SequenceReader(original)
// Test with non-nil pointer
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1(data)
assert.Equal(t, 142, value1)
result2 := sequenced(data)(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, 142, value2)
// Test with nil pointer
result3 := sequenced(nil)(ctx)()
value3, _ := either.Unwrap(result3)
assert.Equal(t, 42, value3)
})
t.Run("maintains referential transparency", func(t *testing.T) {
// The same inputs should always produce the same outputs
original := func(ctx context.Context) func() Either[Reader[string, int]] {
return func() Either[Reader[string, int]] {
return either.Right[error](func(s string) int {
return 10 + len(s)
})
}
}
ctx := context.Background()
sequenced := SequenceReader(original)
// Call multiple times with same inputs
for range 5 {
result1 := original(ctx)()
innerFunc1, _ := either.Unwrap(result1)
value1 := innerFunc1("hello")
assert.Equal(t, 15, value1)
result2 := sequenced("hello")(ctx)()
value2, _ := either.Unwrap(result2)
assert.Equal(t, 15, value2)
}
})
}
func TestTraverseReader(t *testing.T) {
t.Run("basic transformation with Reader dependency", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := Right(10)
// Reader-based transformation
multiply := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader(multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, 50, value)
})
t.Run("preserves outer error", func(t *testing.T) {
type Config struct {
Multiplier int
}
expectedError := errors.New("computation failed")
// Original computation that fails
original := Left[int](expectedError)
// Reader-based transformation (won't be called)
multiply := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader(multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 5}
ctx := context.Background()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsLeft(finalResult))
_, err := either.Unwrap(finalResult)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
type Database struct {
Prefix string
}
// Original computation producing an int
original := Right(42)
// Reader-based transformation: int -> string using Database
format := func(x int) func(Database) string {
return func(db Database) string {
return fmt.Sprintf("%s:%d", db.Prefix, x)
}
}
// Apply TraverseReader
traversed := TraverseReader(format)
result := traversed(original)
// Provide Database and execute
db := Database{Prefix: "ID"}
ctx := context.Background()
finalResult := result(db)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, "ID:42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Settings struct {
Prefix string
Suffix string
}
// Original computation
original := Right("value")
// Reader-based transformation using Settings
decorate := func(s string) func(Settings) string {
return func(settings Settings) string {
return settings.Prefix + s + settings.Suffix
}
}
// Apply TraverseReader
traversed := TraverseReader(decorate)
result := traversed(original)
// Provide Settings and execute
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := context.Background()
finalResult := result(settings)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, "[value]", value)
})
t.Run("enables partial application", func(t *testing.T) {
type Config struct {
Factor int
}
// Original computation
original := Right(10)
// Reader-based transformation
scale := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Factor
}
}
// Apply TraverseReader
traversed := TraverseReader(scale)
result := traversed(original)
// Partially apply Config
cfg := Config{Factor: 3}
withConfig := result(cfg)
// Can now use with different contexts
ctx1 := context.Background()
finalResult1 := withConfig(ctx1)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)
assert.Equal(t, 30, value1)
// Reuse with different context
ctx2 := context.Background()
finalResult2 := withConfig(ctx2)()
assert.True(t, either.IsRight(finalResult2))
value2, _ := either.Unwrap(finalResult2)
assert.Equal(t, 30, value2)
})
t.Run("respects context cancellation", func(t *testing.T) {
type Config struct {
Value int
}
// Original computation that checks context
original := func(ctx context.Context) func() Either[int] {
return func() Either[int] {
if ctx.Err() != nil {
return either.Left[int](ctx.Err())
}
return either.Right[error](10)
}
}
// Reader-based transformation
multiply := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Value
}
}
// Apply TraverseReader
traversed := TraverseReader(multiply)
result := traversed(original)
// Use canceled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
cfg := Config{Value: 5}
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsLeft(finalResult))
_, err := either.Unwrap(finalResult)
assert.Equal(t, context.Canceled, err)
})
t.Run("works with zero values", func(t *testing.T) {
type Config struct {
Offset int
}
// Original computation with zero value
original := Right(0)
// Reader-based transformation
add := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x + cfg.Offset
}
}
// Apply TraverseReader
traversed := TraverseReader(add)
result := traversed(original)
// Provide Config with zero offset
cfg := Config{Offset: 0}
ctx := context.Background()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, 0, value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := Right(5)
// First Reader-based transformation
multiply := func(x int) Reader[Config, int] {
return func(cfg Config) int {
return x * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader(multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 4}
ctx := context.Background()
finalResult := result(cfg)(ctx)()
assert.True(t, either.IsRight(finalResult))
value, _ := either.Unwrap(finalResult)
assert.Equal(t, 20, value) // 5 * 4 = 20
})
t.Run("works with complex Reader logic", func(t *testing.T) {
type ValidationRules struct {
MinValue int
MaxValue int
}
// Original computation
original := Right(50)
// Reader-based transformation with validation logic
validate := func(x int) func(ValidationRules) int {
return func(rules ValidationRules) int {
if x < rules.MinValue {
return rules.MinValue
}
if x > rules.MaxValue {
return rules.MaxValue
}
return x
}
}
// Apply TraverseReader
traversed := TraverseReader(validate)
result := traversed(original)
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
ctx := context.Background()
finalResult1 := result(rules1)(ctx)()
assert.True(t, either.IsRight(finalResult1))
value1, _ := either.Unwrap(finalResult1)
assert.Equal(t, 50, value1)
// Test with value above max
rules2 := ValidationRules{MinValue: 0, MaxValue: 30}
finalResult2 := result(rules2)(ctx)()
assert.True(t, either.IsRight(finalResult2))
value2, _ := either.Unwrap(finalResult2)
assert.Equal(t, 30, value2) // Clamped to max
// Test with value below min
rules3 := ValidationRules{MinValue: 60, MaxValue: 100}
finalResult3 := result(rules3)(ctx)()
assert.True(t, either.IsRight(finalResult3))
value3, _ := either.Unwrap(finalResult3)
assert.Equal(t, 60, value3) // Clamped to min
})
}

View File

@@ -0,0 +1,732 @@
// 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 readerioresult provides logging utilities for ReaderIOResult computations.
// It includes functions for entry/exit logging with timing, correlation IDs, and context management.
package readerioresult
import (
"context"
"log/slog"
"sync/atomic"
"time"
"github.com/IBM/fp-go/v2/context/readerio"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/logging"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
// loggingContextKeyType is the type used as a key for storing logging information in context.Context
loggingContextKeyType int
// LoggingID is a unique identifier assigned to each logged operation for correlation
LoggingID uint64
// loggingContext holds the logging state for a computation, including timing,
// correlation ID, logger instance, and whether logging is enabled.
loggingContext struct {
contextID LoggingID // Unique identifier for this logged operation
startTime time.Time // When the operation started (for duration calculation)
logger *slog.Logger // The logger instance to use for this operation
isEnabled bool // Whether logging is enabled for this operation
}
)
var (
// loggingContextKey is the singleton key used to store/retrieve logging data from context
loggingContextKey loggingContextKeyType
// loggingCounter is an atomic counter that generates unique LoggingIDs
loggingCounter atomic.Uint64
loggingContextValue = F.Bind2nd(context.Context.Value, any(loggingContextKey))
withLoggingContextValue = F.Bind2of3(context.WithValue)(any(loggingContextKey))
// getLoggingContext retrieves the logging information (start time and ID) from the context.
// It returns a Pair containing the start time and the logging ID.
// This function assumes the context contains logging information; it will panic if not present.
getLoggingContext = F.Flow3(
loggingContextValue,
option.ToType[loggingContext],
option.GetOrElse(getDefaultLoggingContext),
)
)
// getDefaultLoggingContext returns a default logging context with the global logger.
// This is used when no logging context is found in the context.Context.
func getDefaultLoggingContext() loggingContext {
return loggingContext{
logger: logging.GetLogger(),
}
}
// withLoggingContext creates an endomorphism that adds a logging context to a context.Context.
// This is used internally to store logging state in the context for retrieval by nested operations.
//
// Parameters:
// - lctx: The logging context to store
//
// Returns:
// - An endomorphism that adds the logging context to a context.Context
func withLoggingContext(lctx loggingContext) Endomorphism[context.Context] {
return F.Bind2nd(withLoggingContextValue, any(lctx))
}
// LogEntryExitF creates a customizable operator that wraps a ReaderIOResult computation with entry/exit callbacks.
//
// This is a more flexible version of LogEntryExit that allows you to provide custom callbacks for
// entry and exit events. The onEntry callback receives the current context and can return a modified
// context (e.g., with additional logging information). The onExit callback receives the computation
// result and can perform custom logging, metrics collection, or cleanup.
//
// The function uses the bracket pattern to ensure that:
// - The onEntry callback is executed before the computation starts
// - The computation runs with the context returned by onEntry
// - The onExit callback is executed after the computation completes (success or failure)
// - The original result is preserved and returned unchanged
// - Cleanup happens even if the computation fails
//
// Type Parameters:
// - A: The success type of the ReaderIOResult
// - ANY: The return type of the onExit callback (typically any)
//
// Parameters:
// - onEntry: A ReaderIO that receives the current context and returns a (possibly modified) context.
// This is executed before the computation starts. Use this for logging entry, adding context values,
// starting timers, or initialization logic.
// - onExit: A Kleisli function that receives the Result[A] and returns a ReaderIO[ANY].
// This is executed after the computation completes, regardless of success or failure.
// Use this for logging exit, recording metrics, cleanup, or finalization logic.
//
// Returns:
// - An Operator that wraps the ReaderIOResult computation with the custom entry/exit callbacks
//
// Example with custom context modification:
//
// type RequestID string
//
// logOp := LogEntryExitF[User, any](
// func(ctx context.Context) IO[context.Context] {
// return func() context.Context {
// reqID := RequestID(uuid.New().String())
// log.Printf("[%s] Starting operation", reqID)
// return context.WithValue(ctx, "requestID", reqID)
// }
// },
// func(res Result[User]) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// reqID := ctx.Value("requestID").(RequestID)
// return F.Pipe1(
// res,
// result.Fold(
// func(err error) any {
// log.Printf("[%s] Operation failed: %v", reqID, err)
// return nil
// },
// func(_ User) any {
// log.Printf("[%s] Operation succeeded", reqID)
// return nil
// },
// ),
// )
// }
// }
// },
// )
//
// wrapped := logOp(fetchUser(123))
//
// Example with metrics collection:
//
// import "github.com/prometheus/client_golang/prometheus"
//
// metricsOp := LogEntryExitF[Response, any](
// func(ctx context.Context) IO[context.Context] {
// return func() context.Context {
// requestCount.WithLabelValues("api_call", "started").Inc()
// return context.WithValue(ctx, "startTime", time.Now())
// }
// },
// func(res Result[Response]) ReaderIO[any] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// startTime := ctx.Value("startTime").(time.Time)
// duration := time.Since(startTime).Seconds()
//
// return F.Pipe1(
// res,
// result.Fold(
// func(err error) any {
// requestCount.WithLabelValues("api_call", "error").Inc()
// requestDuration.WithLabelValues("api_call", "error").Observe(duration)
// return nil
// },
// func(_ Response) any {
// requestCount.WithLabelValues("api_call", "success").Inc()
// requestDuration.WithLabelValues("api_call", "success").Observe(duration)
// return nil
// },
// ),
// )
// }
// }
// },
// )
//
// Use Cases:
// - Custom context modification: Adding request IDs, trace IDs, or other context values
// - Structured logging: Integration with zap, logrus, or other structured loggers
// - Metrics collection: Recording operation durations, success/failure rates
// - Distributed tracing: OpenTelemetry, Jaeger integration
// - Custom monitoring: Application-specific monitoring and alerting
//
// Note: LogEntryExit is implemented using LogEntryExitF with standard logging and context management.
// Use LogEntryExitF when you need more control over the entry/exit behavior or context modification.
func LogEntryExitF[A, ANY any](
onEntry ReaderIO[context.Context],
onExit readerio.Kleisli[Result[A], ANY],
) Operator[A, A] {
bracket := F.Bind13of3(readerio.Bracket[context.Context, Result[A], ANY])(onEntry, func(newCtx context.Context, res Result[A]) ReaderIO[ANY] {
return readerio.FromIO(onExit(res)(newCtx)) // Get the exit callback for this result
})
return func(src ReaderIOResult[A]) ReaderIOResult[A] {
return bracket(F.Flow2(
src,
FromIOResult,
))
}
}
// onEntry creates a ReaderIO that handles the entry logging for an operation.
// It generates a unique logging ID, captures the start time, and logs the entry message.
// The logging context is stored in the context.Context for later retrieval.
//
// Parameters:
// - logLevel: The slog.Level to use for logging (e.g., slog.LevelInfo, slog.LevelDebug)
// - cb: Callback function to retrieve the logger from the context
// - nameAttr: The slog.Attr containing the operation name
//
// Returns:
// - A ReaderIO that prepares the context with logging information and logs the entry
func onEntry(
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
nameAttr slog.Attr,
) ReaderIO[context.Context] {
return func(ctx context.Context) IO[context.Context] {
// logger
logger := cb(ctx)
return func() context.Context {
// check if the logger is enabled
if logger.Enabled(ctx, logLevel) {
// Generate unique logging ID and capture start time
contextID := LoggingID(loggingCounter.Add(1))
startTime := time.Now()
newLogger := logger.With("ID", contextID)
// log using ID
newLogger.LogAttrs(ctx, logLevel, "[entering]", nameAttr)
withCtx := withLoggingContext(loggingContext{
contextID: contextID,
startTime: startTime,
logger: newLogger,
isEnabled: true,
})
withLogger := logging.WithLogger(newLogger)
return withCtx(withLogger(ctx))
}
// logging disabled
withCtx := withLoggingContext(loggingContext{
logger: logger,
isEnabled: false,
})
return withCtx(ctx)
}
}
}
// onExitAny creates a Kleisli function that handles exit logging for an operation.
// It logs either success or error based on the Result, including the operation duration.
// Only logs if logging was enabled during entry (checked via loggingContext.isEnabled).
//
// Parameters:
// - logLevel: The slog.Level to use for logging
// - nameAttr: The slog.Attr containing the operation name
//
// Returns:
// - A Kleisli function that logs the exit/error and returns nil
func onExitAny(
logLevel slog.Level,
nameAttr slog.Attr,
) readerio.Kleisli[Result[any], any] {
return func(res Result[any]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
value := getLoggingContext(ctx)
if value.isEnabled {
return func() any {
// Retrieve logging information from context
durationAttr := slog.Duration("duration", time.Since(value.startTime))
// Log error with ID and duration
onError := func(err error) any {
value.logger.LogAttrs(ctx, logLevel, "[throwing]",
nameAttr,
durationAttr,
slog.Any("error", err))
return nil
}
// Log success with ID and duration
onSuccess := func(_ any) any {
value.logger.LogAttrs(ctx, logLevel, "[exiting ]", nameAttr, durationAttr)
return nil
}
return F.Pipe1(
res,
result.Fold(onError, onSuccess),
)
}
}
// nothing to do
return io.Of[any](nil)
}
}
}
// LogEntryExitWithCallback creates an operator that logs entry and exit of a ReaderIOResult computation
// using a custom logger callback and log level. This provides more control than LogEntryExit.
//
// This function allows you to:
// - Use a custom log level (Debug, Info, Warn, Error)
// - Retrieve the logger from the context using a custom callback
// - Control whether logging is enabled based on the logger's configuration
//
// Type Parameters:
// - A: The success type of the ReaderIOResult
//
// Parameters:
// - logLevel: The slog.Level to use for all log messages (entry, exit, error)
// - cb: Callback function to retrieve the *slog.Logger from the context
// - name: A descriptive name for the operation
//
// Returns:
// - An Operator that wraps the ReaderIOResult with customizable logging
//
// Example with custom log level:
//
// // Log at debug level
// debugOp := LogEntryExitWithCallback[User](
// slog.LevelDebug,
// logging.GetLoggerFromContext,
// "fetchUser",
// )
// result := debugOp(fetchUser(123))
//
// Example with custom logger callback:
//
// type loggerKey int
// const myLoggerKey loggerKey = 0
//
// getMyLogger := func(ctx context.Context) *slog.Logger {
// if logger := ctx.Value(myLoggerKey); logger != nil {
// return logger.(*slog.Logger)
// }
// return slog.Default()
// }
//
// customOp := LogEntryExitWithCallback[Data](
// slog.LevelInfo,
// getMyLogger,
// "processData",
// )
func LogEntryExitWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
name string) Operator[A, A] {
nameAttr := slog.String("name", name)
return LogEntryExitF(
onEntry(logLevel, cb, nameAttr),
F.Flow2(
result.MapTo[A, any](nil),
onExitAny(logLevel, nameAttr),
),
)
}
// LogEntryExit creates an operator that logs the entry and exit of a ReaderIOResult computation with timing and correlation IDs.
//
// This function wraps a ReaderIOResult computation with automatic logging that tracks:
// - Entry: Logs when the computation starts with "[entering <id>] <name>"
// - Exit: Logs when the computation completes successfully with "[exiting <id>] <name> [duration]"
// - Error: Logs when the computation fails with "[throwing <id>] <name> [duration]: <error>"
//
// Each logged operation is assigned a unique LoggingID (a monotonically increasing counter) that
// appears in all log messages for that operation. This ID enables correlation of entry and exit
// logs, even when multiple operations are running concurrently or are interleaved.
//
// The logging information (start time and ID) is stored in the context and can be retrieved using
// getLoggingContext or getLoggingID. This allows nested operations to access the parent operation's
// logging information.
//
// Type Parameters:
// - A: The success type of the ReaderIOResult
//
// Parameters:
// - name: A descriptive name for the computation, used in log messages to identify the operation
//
// Returns:
// - An Operator that wraps the ReaderIOResult computation with entry/exit logging
//
// The function uses the bracket pattern to ensure that:
// - Entry is logged before the computation starts
// - A unique LoggingID is assigned and stored in the context
// - Exit/error is logged after the computation completes, regardless of success or failure
// - Timing is accurate, measuring from entry to exit
// - The original result is preserved and returned unchanged
//
// Log Format:
// - Entry: "[entering <id>] <name>"
// - Success: "[exiting <id>] <name> [<duration>s]"
// - Error: "[throwing <id>] <name> [<duration>s]: <error>"
//
// Example with successful computation:
//
// fetchUser := func(id int) ReaderIOResult[User] {
// return Of(User{ID: id, Name: "Alice"})
// }
//
// // Wrap with logging
// loggedFetch := LogEntryExit[User]("fetchUser")(fetchUser(123))
//
// // Execute
// result := loggedFetch(context.Background())()
// // Logs:
// // [entering 1] fetchUser
// // [exiting 1] fetchUser [0.1s]
//
// Example with error:
//
// failingOp := func() ReaderIOResult[string] {
// return Left[string](errors.New("connection timeout"))
// }
//
// logged := LogEntryExit[string]("failingOp")(failingOp())
// result := logged(context.Background())()
// // Logs:
// // [entering 2] failingOp
// // [throwing 2] failingOp [0.0s]: connection timeout
//
// Example with nested operations:
//
// fetchOrders := func(userID int) ReaderIOResult[[]Order] {
// return Of([]Order{{ID: 1}})
// }
//
// pipeline := F.Pipe3(
// fetchUser(123),
// LogEntryExit[User]("fetchUser"),
// Chain(func(user User) ReaderIOResult[[]Order] {
// return fetchOrders(user.ID)
// }),
// LogEntryExit[[]Order]("fetchOrders"),
// )
//
// result := pipeline(context.Background())()
// // Logs:
// // [entering 3] fetchUser
// // [exiting 3] fetchUser [0.1s]
// // [entering 4] fetchOrders
// // [exiting 4] fetchOrders [0.2s]
//
// Example with concurrent operations:
//
// // Multiple operations can run concurrently, each with unique IDs
// op1 := LogEntryExit[Data]("operation1")(fetchData(1))
// op2 := LogEntryExit[Data]("operation2")(fetchData(2))
//
// go op1(context.Background())()
// go op2(context.Background())()
// // Logs (order may vary):
// // [entering 5] operation1
// // [entering 6] operation2
// // [exiting 5] operation1 [0.1s]
// // [exiting 6] operation2 [0.2s]
// // The IDs allow correlation even when logs are interleaved
//
// Use Cases:
// - Debugging: Track execution flow through complex ReaderIOResult chains with correlation IDs
// - Performance monitoring: Identify slow operations with timing information
// - Production logging: Monitor critical operations with unique identifiers
// - Concurrent operations: Correlate logs from multiple concurrent operations
// - Nested operations: Track parent-child relationships in operation hierarchies
// - Troubleshooting: Quickly identify where errors occur and correlate with entry logs
//
//go:inline
func LogEntryExit[A any](name string) Operator[A, A] {
return LogEntryExitWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, name)
}
func curriedLog(
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) func(slog.Attr) func(context.Context) func() struct{} {
return F.Curry2(func(a slog.Attr, ctx context.Context) func() struct{} {
logger := cb(ctx)
return func() struct{} {
logger.LogAttrs(ctx, logLevel, message, a)
return struct{}{}
}
})
}
// SLogWithCallback creates a Kleisli arrow that logs a Result value (success or error) with a custom logger and log level.
//
// This function logs both successful values and errors, making it useful for debugging and monitoring
// Result values as they flow through a computation. Unlike TapSLog which only logs successful values,
// SLogWithCallback logs the Result regardless of whether it contains a value or an error.
//
// The logged output includes:
// - For success: The message with the value as a structured "value" attribute
// - For error: The message with the error as a structured "error" attribute
//
// The Result is passed through unchanged after logging.
//
// Type Parameters:
// - A: The success type of the Result
//
// Parameters:
// - logLevel: The slog.Level to use for logging (e.g., slog.LevelInfo, slog.LevelDebug)
// - cb: Callback function to retrieve the *slog.Logger from the context
// - message: A descriptive message to include in the log entry
//
// Returns:
// - A Kleisli arrow that logs the Result (value or error) and returns it unchanged
//
// Example with custom log level:
//
// debugLog := SLogWithCallback[User](
// slog.LevelDebug,
// logging.GetLoggerFromContext,
// "User result",
// )
//
// pipeline := F.Pipe2(
// fetchUser(123),
// Chain(debugLog),
// Map(func(u User) string { return u.Name }),
// )
//
// Example with custom logger:
//
// type loggerKey int
// const myLoggerKey loggerKey = 0
//
// getMyLogger := func(ctx context.Context) *slog.Logger {
// if logger := ctx.Value(myLoggerKey); logger != nil {
// return logger.(*slog.Logger)
// }
// return slog.Default()
// }
//
// customLog := SLogWithCallback[Data](
// slog.LevelWarn,
// getMyLogger,
// "Data processing result",
// )
//
// Use Cases:
// - Debugging: Log both successful and failed Results in a pipeline
// - Error tracking: Monitor error occurrences with custom log levels
// - Custom logging: Use application-specific loggers and log levels
// - Conditional logging: Enable/disable logging based on logger configuration
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) Kleisli[Result[A], A] {
return F.Pipe1(
F.Flow2(
// create the attribute to log depending on the condition
result.ToSLogAttr[A](),
// create an `IO` that logs the attribute
curriedLog(logLevel, cb, message),
),
// preserve the original context
reader.Chain(reader.Sequence(readerio.MapTo[struct{}, Result[A]])),
)
}
// SLog creates a Kleisli arrow that logs a Result value (success or error) with a message.
//
// This function logs both successful values and errors at Info level using the logger from the context.
// It's a convenience wrapper around SLogWithCallback with standard settings.
//
// The logged output includes:
// - For success: The message with the value as a structured "value" attribute
// - For error: The message with the error as a structured "error" attribute
//
// The Result is passed through unchanged after logging, making this function transparent in the
// computation pipeline.
//
// Type Parameters:
// - A: The success type of the Result
//
// Parameters:
// - message: A descriptive message to include in the log entry
//
// Returns:
// - A Kleisli arrow that logs the Result (value or error) and returns it unchanged
//
// Example with successful Result:
//
// pipeline := F.Pipe2(
// fetchUser(123),
// Chain(SLog[User]("Fetched user")),
// Map(func(u User) string { return u.Name }),
// )
//
// result := pipeline(context.Background())()
// // If successful, logs: "Fetched user" value={ID:123 Name:"Alice"}
// // If error, logs: "Fetched user" error="user not found"
//
// Example in error handling pipeline:
//
// pipeline := F.Pipe3(
// fetchData(id),
// Chain(SLog[Data]("Data fetched")),
// Chain(validateData),
// Chain(SLog[Data]("Data validated")),
// Chain(processData),
// )
//
// // Logs each step, including errors:
// // "Data fetched" value={...} or error="..."
// // "Data validated" value={...} or error="..."
//
// Use Cases:
// - Debugging: Track both successful and failed Results in a pipeline
// - Error monitoring: Log errors as they occur in the computation
// - Flow tracking: See the progression of Results through a pipeline
// - Troubleshooting: Identify where errors are introduced or propagated
//
// Note: This function logs the Result itself (which may contain an error), not just successful values.
// For logging only successful values, use TapSLog instead.
//
//go:inline
func SLog[A any](message string) Kleisli[Result[A], A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}
// TapSLog creates an operator that logs only successful values with a message and passes them through unchanged.
//
// This function is useful for debugging and monitoring values as they flow through a ReaderIOResult
// computation chain. Unlike SLog which logs both successes and errors, TapSLog only logs when the
// computation is successful. If the computation contains an error, no logging occurs and the error
// is propagated unchanged.
//
// The logged output includes:
// - The provided message
// - The value being passed through (as a structured "value" attribute)
//
// Type Parameters:
// - A: The type of the value to log and pass through
//
// Parameters:
// - message: A descriptive message to include in the log entry
//
// Returns:
// - An Operator that logs successful values and returns them unchanged
//
// Example with simple value logging:
//
// fetchUser := func(id int) ReaderIOResult[User] {
// return Of(User{ID: id, Name: "Alice"})
// }
//
// pipeline := F.Pipe2(
// fetchUser(123),
// TapSLog[User]("Fetched user"),
// Map(func(u User) string { return u.Name }),
// )
//
// result := pipeline(context.Background())()
// // Logs: "Fetched user" value={ID:123 Name:"Alice"}
// // Returns: result.Of("Alice")
//
// Example in a processing pipeline:
//
// processOrder := F.Pipe4(
// fetchOrder(orderId),
// TapSLog[Order]("Order fetched"),
// Chain(validateOrder),
// TapSLog[Order]("Order validated"),
// Chain(processPayment),
// TapSLog[Payment]("Payment processed"),
// )
//
// result := processOrder(context.Background())()
// // Logs each successful step with the intermediate values
// // If any step fails, subsequent TapSLog calls don't log
//
// Example with error handling:
//
// pipeline := F.Pipe3(
// fetchData(id),
// TapSLog[Data]("Data fetched"),
// Chain(func(d Data) ReaderIOResult[Result] {
// if d.IsValid() {
// return Of(processData(d))
// }
// return Left[Result](errors.New("invalid data"))
// }),
// TapSLog[Result]("Data processed"),
// )
//
// // If fetchData succeeds: logs "Data fetched" with the data
// // If processing succeeds: logs "Data processed" with the result
// // If processing fails: "Data processed" is NOT logged (error propagates)
//
// Use Cases:
// - Debugging: Inspect intermediate successful values in a computation pipeline
// - Monitoring: Track successful data flow through complex operations
// - Troubleshooting: Identify where successful computations stop (last logged value before error)
// - Auditing: Log important successful values for compliance or security
// - Development: Understand data transformations during development
//
// Note: This function only logs successful values. Errors are silently propagated without logging.
// For logging both successes and errors, use SLog instead.
//
//go:inline
func TapSLog[A any](message string) Operator[A, A] {
return readerio.ChainFirst(SLog[A](message))
}

View File

@@ -0,0 +1,662 @@
package readerioresult
import (
"bytes"
"context"
"errors"
"log/slog"
"strconv"
"strings"
"testing"
"time"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/logging"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// TestLoggingContext tests basic nested logging with correlation IDs
func TestLoggingContext(t *testing.T) {
data := F.Pipe2(
Of("Sample"),
LogEntryExit[string]("TestLoggingContext1"),
LogEntryExit[string]("TestLoggingContext2"),
)
assert.Equal(t, result.Of("Sample"), data(context.Background())())
}
// TestLogEntryExitSuccess tests successful operation logging
func TestLogEntryExitSuccess(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
operation := F.Pipe1(
Of("success value"),
LogEntryExit[string]("TestOperation"),
)
res := operation(context.Background())()
assert.Equal(t, result.Of("success value"), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "[entering]")
assert.Contains(t, logOutput, "[exiting ]")
assert.Contains(t, logOutput, "TestOperation")
assert.Contains(t, logOutput, "ID=")
assert.Contains(t, logOutput, "duration=")
}
// TestLogEntryExitError tests error operation logging
func TestLogEntryExitError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
testErr := errors.New("test error")
operation := F.Pipe1(
Left[string](testErr),
LogEntryExit[string]("FailingOperation"),
)
res := operation(context.Background())()
assert.True(t, result.IsLeft(res))
logOutput := buf.String()
assert.Contains(t, logOutput, "[entering]")
assert.Contains(t, logOutput, "[throwing]")
assert.Contains(t, logOutput, "FailingOperation")
assert.Contains(t, logOutput, "test error")
assert.Contains(t, logOutput, "ID=")
assert.Contains(t, logOutput, "duration=")
}
// TestLogEntryExitNested tests nested operations with different IDs
func TestLogEntryExitNested(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
innerOp := F.Pipe1(
Of("inner"),
LogEntryExit[string]("InnerOp"),
)
outerOp := F.Pipe2(
Of("outer"),
LogEntryExit[string]("OuterOp"),
Chain(func(s string) ReaderIOResult[string] {
return innerOp
}),
)
res := outerOp(context.Background())()
assert.True(t, result.IsRight(res))
logOutput := buf.String()
// Should have two different IDs
assert.Contains(t, logOutput, "OuterOp")
assert.Contains(t, logOutput, "InnerOp")
// Count entering and exiting logs
enterCount := strings.Count(logOutput, "[entering]")
exitCount := strings.Count(logOutput, "[exiting ]")
assert.Equal(t, 2, enterCount, "Should have 2 entering logs")
assert.Equal(t, 2, exitCount, "Should have 2 exiting logs")
}
// TestLogEntryExitWithCallback tests custom log level and callback
func TestLogEntryExitWithCallback(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
operation := F.Pipe1(
Of(42),
LogEntryExitWithCallback[int](slog.LevelDebug, customCallback, "DebugOperation"),
)
res := operation(context.Background())()
assert.Equal(t, result.Of(42), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "[entering]")
assert.Contains(t, logOutput, "[exiting ]")
assert.Contains(t, logOutput, "DebugOperation")
assert.Contains(t, logOutput, "level=DEBUG")
}
// TestLogEntryExitDisabled tests that logging can be disabled
func TestLogEntryExitDisabled(t *testing.T) {
var buf bytes.Buffer
// Create logger with level that disables info logs
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelError, // Only log errors
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
operation := F.Pipe1(
Of("value"),
LogEntryExit[string]("DisabledOperation"),
)
res := operation(context.Background())()
assert.True(t, result.IsRight(res))
// Should have no logs since level is ERROR
logOutput := buf.String()
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
}
// TestLogEntryExitF tests custom entry/exit callbacks
func TestLogEntryExitF(t *testing.T) {
var entryCount, exitCount int
onEntry := func(ctx context.Context) IO[context.Context] {
return func() context.Context {
entryCount++
return ctx
}
}
onExit := func(res Result[string]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
return func() any {
exitCount++
return nil
}
}
}
operation := F.Pipe1(
Of("test"),
LogEntryExitF(onEntry, onExit),
)
res := operation(context.Background())()
assert.True(t, result.IsRight(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
}
// TestLogEntryExitFWithError tests custom callbacks with error
func TestLogEntryExitFWithError(t *testing.T) {
var entryCount, exitCount int
var capturedError error
onEntry := func(ctx context.Context) IO[context.Context] {
return func() context.Context {
entryCount++
return ctx
}
}
onExit := func(res Result[string]) ReaderIO[any] {
return func(ctx context.Context) IO[any] {
return func() any {
exitCount++
if result.IsLeft(res) {
_, capturedError = result.Unwrap(res)
}
return nil
}
}
}
testErr := errors.New("custom error")
operation := F.Pipe1(
Left[string](testErr),
LogEntryExitF(onEntry, onExit),
)
res := operation(context.Background())()
assert.True(t, result.IsLeft(res))
assert.Equal(t, 1, entryCount, "Entry callback should be called once")
assert.Equal(t, 1, exitCount, "Exit callback should be called once")
assert.Equal(t, testErr, capturedError, "Should capture the error")
}
// TestLoggingIDUniqueness tests that logging IDs are unique
func TestLoggingIDUniqueness(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
// Run multiple operations
for i := range 5 {
op := F.Pipe1(
Of(i),
LogEntryExit[int]("Operation"),
)
op(context.Background())()
}
logOutput := buf.String()
// Extract all IDs and verify they're unique
lines := strings.Split(logOutput, "\n")
ids := make(map[string]bool)
for _, line := range lines {
if strings.Contains(line, "ID=") {
// Extract ID value
parts := strings.Split(line, "ID=")
if len(parts) > 1 {
idPart := strings.Fields(parts[1])[0]
ids[idPart] = true
}
}
}
// Should have 5 unique IDs (one per operation)
assert.GreaterOrEqual(t, len(ids), 5, "Should have at least 5 unique IDs")
}
// TestLogEntryExitWithContextLogger tests using logger from context
func TestLogEntryExitWithContextLogger(t *testing.T) {
var buf bytes.Buffer
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
operation := F.Pipe1(
Of("context value"),
LogEntryExit[string]("ContextOperation"),
)
res := operation(ctx)()
assert.True(t, result.IsRight(res))
logOutput := buf.String()
assert.Contains(t, logOutput, "[entering]")
assert.Contains(t, logOutput, "[exiting ]")
assert.Contains(t, logOutput, "ContextOperation")
}
// TestLogEntryExitTiming tests that duration is captured
func TestLogEntryExitTiming(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
// Operation with delay
slowOp := func(ctx context.Context) IOResult[string] {
return func() Result[string] {
time.Sleep(10 * time.Millisecond)
return result.Of("done")
}
}
operation := F.Pipe1(
slowOp,
LogEntryExit[string]("SlowOperation"),
)
res := operation(context.Background())()
assert.True(t, result.IsRight(res))
logOutput := buf.String()
assert.Contains(t, logOutput, "duration=")
// Verify duration is present in exit log
lines := strings.Split(logOutput, "\n")
var foundDuration bool
for _, line := range lines {
if strings.Contains(line, "[exiting ]") && strings.Contains(line, "duration=") {
foundDuration = true
break
}
}
assert.True(t, foundDuration, "Exit log should contain duration")
}
// TestLogEntryExitChainedOperations tests complex chained operations
func TestLogEntryExitChainedOperations(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
step1 := F.Pipe1(
Of(1),
LogEntryExit[int]("Step1"),
)
step2 := F.Flow3(
N.Mul(2),
Of,
LogEntryExit[int]("Step2"),
)
step3 := F.Flow3(
strconv.Itoa,
Of,
LogEntryExit[string]("Step3"),
)
pipeline := F.Pipe1(
step1,
Chain(F.Flow2(
step2,
Chain(step3),
)),
)
res := pipeline(context.Background())()
assert.Equal(t, result.Of("2"), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "Step1")
assert.Contains(t, logOutput, "Step2")
assert.Contains(t, logOutput, "Step3")
// Verify all steps completed
assert.Equal(t, 3, strings.Count(logOutput, "[entering]"))
assert.Equal(t, 3, strings.Count(logOutput, "[exiting ]"))
}
// TestTapSLog tests basic TapSLog functionality
func TestTapSLog(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
operation := F.Pipe2(
Of(42),
TapSLog[int]("Processing value"),
Map(N.Mul(2)),
)
res := operation(context.Background())()
assert.Equal(t, result.Of(84), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "Processing value")
assert.Contains(t, logOutput, "value=42")
}
// TestTapSLogInPipeline tests TapSLog in a multi-step pipeline
func TestTapSLogInPipeline(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
step1 := F.Pipe2(
Of("hello"),
TapSLog[string]("Step 1: Initial value"),
Map(func(s string) string { return s + " world" }),
)
step2 := F.Pipe2(
step1,
TapSLog[string]("Step 2: After concatenation"),
Map(S.Size),
)
pipeline := F.Pipe1(
step2,
TapSLog[int]("Step 3: Final length"),
)
res := pipeline(context.Background())()
assert.Equal(t, result.Of(11), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "Step 1: Initial value")
assert.Contains(t, logOutput, "value=hello")
assert.Contains(t, logOutput, "Step 2: After concatenation")
assert.Contains(t, logOutput, `value="hello world"`)
assert.Contains(t, logOutput, "Step 3: Final length")
assert.Contains(t, logOutput, "value=11")
}
// TestTapSLogWithError tests that TapSLog logs errors (via SLog)
func TestTapSLogWithError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
testErr := errors.New("computation failed")
pipeline := F.Pipe2(
Left[int](testErr),
TapSLog[int]("Error logged"),
Map(N.Mul(2)),
)
res := pipeline(context.Background())()
assert.True(t, result.IsLeft(res))
logOutput := buf.String()
// TapSLog uses SLog internally, which logs both successes and errors
assert.Contains(t, logOutput, "Error logged")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "computation failed")
}
// TestTapSLogWithStruct tests TapSLog with structured data
func TestTapSLogWithStruct(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
type User struct {
ID int
Name string
}
user := User{ID: 123, Name: "Alice"}
operation := F.Pipe2(
Of(user),
TapSLog[User]("User data"),
Map(func(u User) string { return u.Name }),
)
res := operation(context.Background())()
assert.Equal(t, result.Of("Alice"), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "User data")
assert.Contains(t, logOutput, "ID:123")
assert.Contains(t, logOutput, "Name:Alice")
}
// TestTapSLogDisabled tests that TapSLog respects logger level
func TestTapSLogDisabled(t *testing.T) {
var buf bytes.Buffer
// Create logger with level that disables info logs
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelError, // Only log errors
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
operation := F.Pipe2(
Of(42),
TapSLog[int]("This should not be logged"),
Map(N.Mul(2)),
)
res := operation(context.Background())()
assert.Equal(t, result.Of(84), res)
// Should have no logs since level is ERROR
logOutput := buf.String()
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
}
// TestTapSLogWithContextLogger tests TapSLog using logger from context
func TestTapSLogWithContextLogger(t *testing.T) {
var buf bytes.Buffer
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
operation := F.Pipe2(
Of("test value"),
TapSLog[string]("Context logger test"),
Map(S.Size),
)
res := operation(ctx)()
assert.Equal(t, result.Of(10), res)
logOutput := buf.String()
assert.Contains(t, logOutput, "Context logger test")
assert.Contains(t, logOutput, `value="test value"`)
}
// TestSLogLogsSuccessValue tests that SLog logs successful Result values
func TestSLogLogsSuccessValue(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// Create a Result and log it
res1 := result.Of(42)
logged := SLog[int]("Result value")(res1)(ctx)()
assert.Equal(t, result.Of(42), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Result value")
assert.Contains(t, logOutput, "value=42")
}
// TestSLogLogsErrorValue tests that SLog logs error Result values
func TestSLogLogsErrorValue(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
testErr := errors.New("test error")
// Create an error Result and log it
res1 := result.Left[int](testErr)
logged := SLog[int]("Result value")(res1)(ctx)()
assert.True(t, result.IsLeft(logged))
logOutput := buf.String()
assert.Contains(t, logOutput, "Result value")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "test error")
}
// TestSLogWithCallbackCustomLevel tests SLogWithCallback with custom log level
func TestSLogWithCallbackCustomLevel(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
ctx := context.Background()
// Create a Result and log it with custom callback
res1 := result.Of(42)
logged := SLogWithCallback[int](slog.LevelDebug, customCallback, "Debug result")(res1)(ctx)()
assert.Equal(t, result.Of(42), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Debug result")
assert.Contains(t, logOutput, "value=42")
assert.Contains(t, logOutput, "level=DEBUG")
}
// TestSLogWithCallbackLogsError tests SLogWithCallback logs errors
func TestSLogWithCallbackLogsError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelWarn,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
ctx := context.Background()
testErr := errors.New("warning error")
// Create an error Result and log it with custom callback
res1 := result.Left[int](testErr)
logged := SLogWithCallback[int](slog.LevelWarn, customCallback, "Warning result")(res1)(ctx)()
assert.True(t, result.IsLeft(logged))
logOutput := buf.String()
assert.Contains(t, logOutput, "Warning result")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "warning error")
assert.Contains(t, logOutput, "level=WARN")
}

View File

@@ -19,6 +19,7 @@ import (
"context"
"time"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/errors"
@@ -26,10 +27,11 @@ import (
"github.com/IBM/fp-go/v2/io"
"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/reader"
"github.com/IBM/fp-go/v2/readerio"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/readeroption"
"github.com/IBM/fp-go/v2/result"
)
const (
@@ -150,7 +152,7 @@ func MapTo[A, B any](b B) Operator[A, B] {
//
//go:inline
func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChain(ma, f)
return RIOR.MonadChain(ma, function.Flow2(f, WithContext))
}
// Chain sequences two [ReaderIOResult] computations, where the second depends on the result of the first.
@@ -163,7 +165,7 @@ func MonadChain[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return RIOR.Chain(f)
return RIOR.Chain(function.Flow2(f, WithContext))
}
// MonadChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
@@ -177,12 +179,12 @@ func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
//
//go:inline
func MonadChainFirst[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirst(ma, f)
return RIOR.MonadChainFirst(ma, function.Flow2(f, WithContext))
}
//go:inline
func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTap(ma, f)
return RIOR.MonadTap(ma, function.Flow2(f, WithContext))
}
// ChainFirst sequences two [ReaderIOResult] computations but returns the result of the first.
@@ -195,12 +197,12 @@ func MonadTap[A, B any](ma ReaderIOResult[A], f Kleisli[A, B]) ReaderIOResult[A]
//
//go:inline
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirst(f)
return RIOR.ChainFirst(function.Flow2(f, WithContext))
}
//go:inline
func Tap[A, B any](f Kleisli[A, B]) Operator[A, A] {
return RIOR.Tap(f)
return RIOR.Tap(function.Flow2(f, WithContext))
}
// Of creates a [ReaderIOResult] that always succeeds with the given value.
@@ -243,14 +245,14 @@ func MonadApPar[B, A any](fab ReaderIOResult[func(A) B], fa ReaderIOResult[A]) R
return func(ctx context.Context) IOResult[B] {
// quick check for cancellation
if err := context.Cause(ctx); err != nil {
return ioeither.Left[B](err)
if ctx.Err() != nil {
return ioeither.Left[B](context.Cause(ctx))
}
return func() Result[B] {
// quick check for cancellation
if err := context.Cause(ctx); err != nil {
return either.Left[B](err)
if ctx.Err() != nil {
return either.Left[B](context.Cause(ctx))
}
// create sub-contexts for fa and fab, so they can cancel one other
@@ -382,7 +384,7 @@ func Ask() ReaderIOResult[context.Context] {
// Returns a new ReaderIOResult with the chained computation.
//
//go:inline
func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[B] {
func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[B] {
return RIOR.MonadChainEitherK(ma, f)
}
@@ -395,7 +397,7 @@ func MonadChainEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) Read
// Returns a function that chains the Either-returning function.
//
//go:inline
func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
func ChainEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, B] {
return RIOR.ChainEitherK[context.Context](f)
}
@@ -409,12 +411,12 @@ func ChainEitherK[A, B any](f func(A) Either[B]) Operator[A, B] {
// Returns a ReaderIOResult with the original value if both computations succeed.
//
//go:inline
func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
func MonadChainFirstEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstEitherK(ma, f)
}
//go:inline
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) ReaderIOResult[A] {
func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f either.Kleisli[error, A, B]) ReaderIOResult[A] {
return RIOR.MonadTapEitherK(ma, f)
}
@@ -427,12 +429,12 @@ func MonadTapEitherK[A, B any](ma ReaderIOResult[A], f func(A) Either[B]) Reader
// Returns a function that chains the Either-returning function.
//
//go:inline
func ChainFirstEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
func ChainFirstEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
return RIOR.ChainFirstEitherK[context.Context](f)
}
//go:inline
func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
func TapEitherK[A, B any](f either.Kleisli[error, A, B]) Operator[A, A] {
return RIOR.TapEitherK[context.Context](f)
}
@@ -445,7 +447,7 @@ func TapEitherK[A, B any](f func(A) Either[B]) Operator[A, A] {
// Returns a function that chains Option-returning functions into ReaderIOResult.
//
//go:inline
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainOptionK[context.Context, A, B](onNone)
}
@@ -527,7 +529,7 @@ func Never[A any]() ReaderIOResult[A] {
// Returns a new ReaderIOResult with the chained IO computation.
//
//go:inline
func MonadChainIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[B] {
func MonadChainIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChainIOK(ma, f)
}
@@ -540,7 +542,7 @@ func MonadChainIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResu
// Returns a function that chains the IO-returning function.
//
//go:inline
func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
func ChainIOK[A, B any](f io.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainIOK[context.Context](f)
}
@@ -554,12 +556,12 @@ func ChainIOK[A, B any](f func(A) IO[B]) Operator[A, B] {
// Returns a ReaderIOResult with the original value after executing the IO.
//
//go:inline
func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
func MonadChainFirstIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstIOK(ma, f)
}
//go:inline
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult[A] {
func MonadTapIOK[A, B any](ma ReaderIOResult[A], f io.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTapIOK(ma, f)
}
@@ -572,12 +574,12 @@ func MonadTapIOK[A, B any](ma ReaderIOResult[A], f func(A) IO[B]) ReaderIOResult
// Returns a function that chains the IO-returning function.
//
//go:inline
func ChainFirstIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
func ChainFirstIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstIOK[context.Context](f)
}
//go:inline
func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
func TapIOK[A, B any](f io.Kleisli[A, B]) Operator[A, A] {
return RIOR.TapIOK[context.Context](f)
}
@@ -590,7 +592,7 @@ func TapIOK[A, B any](f func(A) IO[B]) Operator[A, A] {
// Returns a function that chains the IOResult-returning function.
//
//go:inline
func ChainIOEitherK[A, B any](f func(A) IOResult[B]) Operator[A, B] {
func ChainIOEitherK[A, B any](f ioresult.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainIOEitherK[context.Context](f)
}
@@ -753,7 +755,7 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
//
//go:inline
func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A, B] {
return RIOR.Fold(onLeft, onRight)
return RIOR.Fold(function.Flow2(onLeft, WithContext), function.Flow2(onRight, WithContext))
}
// GetOrElse extracts the value from a [ReaderIOResult], providing a default via a function if it fails.
@@ -765,7 +767,7 @@ func Fold[A, B any](onLeft Kleisli[error, B], onRight Kleisli[A, B]) Operator[A,
// Returns a function that converts a ReaderIOResult to a ReaderIO.
//
//go:inline
func GetOrElse[A any](onLeft func(error) ReaderIO[A]) func(ReaderIOResult[A]) ReaderIO[A] {
func GetOrElse[A any](onLeft readerio.Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIO[A] {
return RIOR.GetOrElse(onLeft)
}
@@ -858,32 +860,32 @@ func TapReaderResultK[A, B any](f readerresult.Kleisli[A, B]) Operator[A, A] {
}
//go:inline
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[B] {
func MonadChainReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[B] {
return RIOR.MonadChainReaderIOK(ma, f)
}
//go:inline
func ChainReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, B] {
func ChainReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, B] {
return RIOR.ChainReaderIOK(f)
}
//go:inline
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
func MonadChainFirstReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[context.Context, A, B]) ReaderIOResult[A] {
func MonadTapReaderIOK[A, B any](ma ReaderIOResult[A], f readerio.Kleisli[A, B]) ReaderIOResult[A] {
return RIOR.MonadTapReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
func ChainFirstReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
return RIOR.ChainFirstReaderIOK(f)
}
//go:inline
func TapReaderIOK[A, B any](f readerio.Kleisli[context.Context, A, B]) Operator[A, A] {
func TapReaderIOK[A, B any](f readerio.Kleisli[A, B]) Operator[A, A] {
return RIOR.TapReaderIOK(f)
}
@@ -913,15 +915,15 @@ func Read[A any](r context.Context) func(ReaderIOResult[A]) IOResult[A] {
//
//go:inline
func MonadChainLeft[A any](fa ReaderIOResult[A], f Kleisli[error, A]) ReaderIOResult[A] {
return RIOR.MonadChainLeft(fa, f)
return RIOR.MonadChainLeft(fa, function.Flow2(f, WithContext))
}
// ChainLeft is the curried version of [MonadChainLeft].
// It returns a function that chains a computation on the left (error) side of a [ReaderIOResult].
//
//go:inline
func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResult[A] {
return RIOR.ChainLeft(f)
func ChainLeft[A any](f Kleisli[error, A]) Operator[A, A] {
return RIOR.ChainLeft(function.Flow2(f, WithContext))
}
// MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
@@ -934,12 +936,12 @@ func ChainLeft[A any](f Kleisli[error, A]) func(ReaderIOResult[A]) ReaderIOResul
//
//go:inline
func MonadChainFirstLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadChainFirstLeft(ma, f)
return RIOR.MonadChainFirstLeft(ma, function.Flow2(f, WithContext))
}
//go:inline
func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOResult[A] {
return RIOR.MonadTapLeft(ma, f)
return RIOR.MonadTapLeft(ma, function.Flow2(f, WithContext))
}
// ChainFirstLeft is the curried version of [MonadChainFirstLeft].
@@ -951,10 +953,212 @@ func MonadTapLeft[A, B any](ma ReaderIOResult[A], f Kleisli[error, B]) ReaderIOR
//
//go:inline
func ChainFirstLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.ChainFirstLeft[A](f)
return RIOR.ChainFirstLeft[A](function.Flow2(f, WithContext))
}
//go:inline
func TapLeft[A, B any](f Kleisli[error, B]) Operator[A, A] {
return RIOR.TapLeft[A](f)
return RIOR.TapLeft[A](function.Flow2(f, WithContext))
}
// Local transforms the context.Context environment before passing it to a ReaderIOResult computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// The function checks for context cancellation before applying the transformation,
// returning an error immediately if the context is already cancelled.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerioresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerioresult.FromReader(func(ctx context.Context) string {
// if user := ctx.Value(userKey); user != nil {
// return user.(string)
// }
// return "unknown"
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// value, err := result(context.Background())() // Returns ("Alice", nil)
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerioresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) 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)
defer otherCancel()
return rr(otherCtx)()
}
}
}
}
// WithTimeout adds a timeout to the context for a ReaderIOResult computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderIOResult is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup. If the timeout expires,
// the computation will receive a context.DeadlineExceeded error.
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerioresult.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) after 5s
//
// Successful Example:
//
// quickFetch := readerioresult.Right(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerioresult.WithTimeout[Data](5*time.Second),
// )
// value, err := result(context.Background())() // 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)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderIOResult computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup. If the deadline passes, the computation
// will receive a context.DeadlineExceeded error.
//
// Type Parameters:
// - A: The value type of the ReaderIOResult
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Operation must complete by 3 PM
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
//
// fetchData := readerioresult.FromReader(func(ctx context.Context) Data {
// // Simulate operation
// select {
// case <-time.After(1 * time.Hour):
// return Data{Value: "done"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerioresult.WithDeadline[Data](deadline),
// )
// value, err := result(context.Background())() // Returns (Data{}, context.DeadlineExceeded) if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
// result := F.Pipe1(
// fetchData,
// readerioresult.WithDeadline[Data](laterDeadline),
// )
// 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)
})
}

View File

@@ -567,15 +567,13 @@ func TestMemoize(t *testing.T) {
res1 := computation(context.Background())()
assert.True(t, E.IsRight(res1))
val1 := E.ToOption(res1)
v1, _ := O.Unwrap(val1)
assert.Equal(t, 1, v1)
assert.Equal(t, O.Of(1), val1)
// Second execution should return cached value
res2 := computation(context.Background())()
assert.True(t, E.IsRight(res2))
val2 := E.ToOption(res2)
v2, _ := O.Unwrap(val2)
assert.Equal(t, 1, v2)
assert.Equal(t, O.Of(1), val2)
// Counter should only be incremented once
assert.Equal(t, 1, counter)
@@ -739,9 +737,7 @@ func TestTraverseArray(t *testing.T) {
res := result(context.Background())()
assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res)
assert.True(t, O.IsSome(arrOpt))
resultArr, _ := O.Unwrap(arrOpt)
assert.Equal(t, []int{2, 4, 6}, resultArr)
assert.Equal(t, O.Of([]int{2, 4, 6}), arrOpt)
})
t.Run("TraverseArray with error", func(t *testing.T) {
@@ -765,9 +761,7 @@ func TestSequenceArray(t *testing.T) {
res := result(context.Background())()
assert.True(t, E.IsRight(res))
arrOpt := E.ToOption(res)
assert.True(t, O.IsSome(arrOpt))
resultArr, _ := O.Unwrap(arrOpt)
assert.Equal(t, []int{1, 2, 3}, resultArr)
assert.Equal(t, O.Of([]int{1, 2, 3}), arrOpt)
}
func TestTraverseRecord(t *testing.T) {

View File

@@ -0,0 +1,184 @@
// 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 readerioresult
import (
"github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
)
// TailRec implements stack-safe tail recursion for the context-aware ReaderIOResult monad.
//
// This function enables recursive computations that combine four powerful concepts:
// - Context awareness: Automatic cancellation checking via [context.Context]
// - Environment dependency (Reader aspect): Access to configuration, context, or dependencies
// - Side effects (IO aspect): Logging, file I/O, network calls, etc.
// - Error handling (Either aspect): Computations that can fail with an error
//
// The function uses an iterative loop to execute the recursion, making it safe for deep
// or unbounded recursion without risking stack overflow. Additionally, it integrates
// context cancellation checking through [WithContext], ensuring that recursive computations
// can be cancelled gracefully.
//
// # How It Works
//
// TailRec takes a Kleisli arrow that returns Either[A, B]:
// - Left(A): Continue recursion with the new state A
// - Right(B): Terminate recursion successfully and return the final result B
//
// The function wraps each iteration with [WithContext] to ensure context cancellation
// is checked before each recursive step. If the context is cancelled, the recursion
// terminates early with a context cancellation error.
//
// # Type Parameters
//
// - A: The state type that changes during recursion
// - B: The final result type when recursion terminates successfully
//
// # Parameters
//
// - f: A Kleisli arrow (A => ReaderIOResult[Either[A, B]]) that:
// - Takes the current state A
// - Returns a ReaderIOResult that depends on [context.Context]
// - Can fail with error (Left in the outer Either)
// - Produces Either[A, B] to control recursion flow (Right in the outer Either)
//
// # Returns
//
// A Kleisli arrow (A => ReaderIOResult[B]) that:
// - Takes an initial state A
// - Returns a ReaderIOResult that requires [context.Context]
// - Can fail with error or context cancellation
// - Produces the final result B after recursion completes
//
// # Context Cancellation
//
// Unlike the base [readerioresult.TailRec], this version automatically integrates
// context cancellation checking:
// - Each recursive iteration checks if the context is cancelled
// - If cancelled, recursion terminates immediately with a cancellation error
// - This prevents runaway recursive computations in cancelled contexts
// - Enables responsive cancellation for long-running recursive operations
//
// # Use Cases
//
// 1. Cancellable recursive algorithms:
// - Tree traversals that can be cancelled mid-operation
// - Graph algorithms with timeout requirements
// - Recursive parsers that respect cancellation
//
// 2. Long-running recursive computations:
// - File system traversals with cancellation support
// - Network operations with timeout handling
// - Database operations with connection timeout awareness
//
// 3. Interactive recursive operations:
// - User-initiated operations that can be cancelled
// - Background tasks with cancellation support
// - Streaming operations with graceful shutdown
//
// # Example: Cancellable Countdown
//
// countdownStep := func(n int) readerioresult.ReaderIOResult[either.Either[int, string]] {
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[int, string]] {
// return func() either.Either[error, either.Either[int, string]] {
// if n <= 0 {
// return either.Right[error](either.Right[int]("Done!"))
// }
// // Simulate some work
// time.Sleep(100 * time.Millisecond)
// return either.Right[error](either.Left[string](n - 1))
// }
// }
// }
//
// countdown := readerioresult.TailRec(countdownStep)
//
// // With cancellation
// ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
// defer cancel()
// result := countdown(10)(ctx)() // Will be cancelled after ~500ms
//
// # Example: Cancellable File Processing
//
// type ProcessState struct {
// files []string
// processed []string
// }
//
// processStep := func(state ProcessState) readerioresult.ReaderIOResult[either.Either[ProcessState, []string]] {
// return func(ctx context.Context) ioeither.IOEither[error, either.Either[ProcessState, []string]] {
// return func() either.Either[error, either.Either[ProcessState, []string]] {
// if len(state.files) == 0 {
// return either.Right[error](either.Right[ProcessState](state.processed))
// }
//
// file := state.files[0]
// // Process file (this could be cancelled via context)
// if err := processFileWithContext(ctx, file); err != nil {
// return either.Left[either.Either[ProcessState, []string]](err)
// }
//
// return either.Right[error](either.Left[[]string](ProcessState{
// files: state.files[1:],
// processed: append(state.processed, file),
// }))
// }
// }
// }
//
// processFiles := readerioresult.TailRec(processStep)
// ctx, cancel := context.WithCancel(context.Background())
//
// // Can be cancelled at any point during processing
// go func() {
// time.Sleep(2 * time.Second)
// cancel() // Cancel after 2 seconds
// }()
//
// result := processFiles(ProcessState{files: manyFiles})(ctx)()
//
// # Stack Safety
//
// The iterative implementation ensures that even deeply recursive computations
// (thousands or millions of iterations) will not cause stack overflow, while
// still respecting context cancellation:
//
// // Safe for very large inputs with cancellation support
// largeCountdown := readerioresult.TailRec(countdownStep)
// ctx := context.Background()
// result := largeCountdown(1000000)(ctx)() // Safe, no stack overflow
//
// # Performance Considerations
//
// - Each iteration includes context cancellation checking overhead
// - Context checking happens before each recursive step
// - For performance-critical code, consider the cancellation checking cost
// - The [WithContext] wrapper adds minimal overhead for cancellation safety
//
// # See Also
//
// - [readerioresult.TailRec]: Base tail recursion without automatic context checking
// - [WithContext]: Context cancellation wrapper used internally
// - [Chain]: For sequencing ReaderIOResult computations
// - [Ask]: For accessing the context
// - [Left]/[Right]: For creating error/success values
//
//go:inline
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
return RIOR.TailRec(F.Flow2(f, WithContext))
}

View File

@@ -0,0 +1,433 @@
// 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 readerioresult
import (
"context"
"errors"
"fmt"
"sync/atomic"
"testing"
"time"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTailRec_BasicRecursion(t *testing.T) {
// Test basic countdown recursion
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
countdown := TailRec(countdownStep)
result := countdown(5)(context.Background())()
assert.Equal(t, E.Of[error]("Done!"), result)
}
func TestTailRec_FactorialRecursion(t *testing.T) {
// Test factorial computation using tail recursion
type FactorialState struct {
n int
acc int
}
factorialStep := func(state FactorialState) ReaderIOResult[E.Either[FactorialState, int]] {
return func(ctx context.Context) IOEither[E.Either[FactorialState, int]] {
return func() Either[E.Either[FactorialState, int]] {
if state.n <= 1 {
return E.Right[error](E.Right[FactorialState](state.acc))
}
return E.Right[error](E.Left[int](FactorialState{
n: state.n - 1,
acc: state.acc * state.n,
}))
}
}
}
factorial := TailRec(factorialStep)
result := factorial(FactorialState{n: 5, acc: 1})(context.Background())()
assert.Equal(t, E.Of[error](120), result) // 5! = 120
}
func TestTailRec_ErrorHandling(t *testing.T) {
// Test that errors are properly propagated
testErr := errors.New("computation error")
errorStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
if n == 3 {
return E.Left[E.Either[int, string]](testErr)
}
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
errorRecursion := TailRec(errorStep)
result := errorRecursion(5)(context.Background())()
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
assert.Equal(t, testErr, err)
}
func TestTailRec_ContextCancellation(t *testing.T) {
// Test that recursion gets cancelled early when context is canceled
var iterationCount int32
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
atomic.AddInt32(&iterationCount, 1)
// Simulate some work
time.Sleep(50 * time.Millisecond)
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
slowRecursion := TailRec(slowStep)
// Create a context that will be cancelled after 100ms
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
start := time.Now()
result := slowRecursion(10)(ctx)()
elapsed := time.Since(start)
// Should be cancelled and return an error
assert.True(t, E.IsLeft(result))
// Should complete quickly due to cancellation (much less than 10 * 50ms = 500ms)
assert.Less(t, elapsed, 200*time.Millisecond)
// Should have executed only a few iterations before cancellation
iterations := atomic.LoadInt32(&iterationCount)
assert.Less(t, iterations, int32(5), "Should have been cancelled before completing all iterations")
}
func TestTailRec_ImmediateCancellation(t *testing.T) {
// Test with an already cancelled context
countdownStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
countdown := TailRec(countdownStep)
// Create an already cancelled context
ctx, cancel := context.WithCancel(context.Background())
cancel()
result := countdown(5)(ctx)()
// Should immediately return a cancellation error
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
assert.Equal(t, context.Canceled, err)
}
func TestTailRec_StackSafety(t *testing.T) {
// Test that deep recursion doesn't cause stack overflow
const largeN = 10000
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
return func(ctx context.Context) IOEither[E.Either[int, int]] {
return func() Either[E.Either[int, int]] {
if n <= 0 {
return E.Right[error](E.Right[int](0))
}
return E.Right[error](E.Left[int](n - 1))
}
}
}
countdown := TailRec(countdownStep)
result := countdown(largeN)(context.Background())()
assert.Equal(t, E.Of[error](0), result)
}
func TestTailRec_StackSafetyWithCancellation(t *testing.T) {
// Test stack safety with cancellation after many iterations
const largeN = 100000
var iterationCount int32
countdownStep := func(n int) ReaderIOResult[E.Either[int, int]] {
return func(ctx context.Context) IOEither[E.Either[int, int]] {
return func() Either[E.Either[int, int]] {
atomic.AddInt32(&iterationCount, 1)
// Add a small delay every 1000 iterations to make cancellation more likely
if n%1000 == 0 {
time.Sleep(1 * time.Millisecond)
}
if n <= 0 {
return E.Right[error](E.Right[int](0))
}
return E.Right[error](E.Left[int](n - 1))
}
}
}
countdown := TailRec(countdownStep)
// Cancel after 50ms to allow some iterations but not all
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
result := countdown(largeN)(ctx)()
// Should be cancelled (or completed if very fast)
// The key is that it doesn't cause a stack overflow
iterations := atomic.LoadInt32(&iterationCount)
assert.Greater(t, iterations, int32(0))
// If it was cancelled, verify it didn't complete all iterations
if E.IsLeft(result) {
assert.Less(t, iterations, int32(largeN))
}
}
func TestTailRec_ComplexState(t *testing.T) {
// Test with more complex state management
type ProcessState struct {
items []string
processed []string
errors []error
}
processStep := func(state ProcessState) ReaderIOResult[E.Either[ProcessState, []string]] {
return func(ctx context.Context) IOEither[E.Either[ProcessState, []string]] {
return func() Either[E.Either[ProcessState, []string]] {
if A.IsEmpty(state.items) {
return E.Right[error](E.Right[ProcessState](state.processed))
}
item := state.items[0]
// Simulate processing that might fail for certain items
if item == "error-item" {
return E.Left[E.Either[ProcessState, []string]](
fmt.Errorf("failed to process item: %s", item))
}
return E.Right[error](E.Left[[]string](ProcessState{
items: state.items[1:],
processed: append(state.processed, item),
errors: state.errors,
}))
}
}
}
processItems := TailRec(processStep)
t.Run("successful processing", func(t *testing.T) {
initialState := ProcessState{
items: []string{"item1", "item2", "item3"},
processed: []string{},
errors: []error{},
}
result := processItems(initialState)(context.Background())()
assert.Equal(t, E.Of[error]([]string{"item1", "item2", "item3"}), result)
})
t.Run("processing with error", func(t *testing.T) {
initialState := ProcessState{
items: []string{"item1", "error-item", "item3"},
processed: []string{},
errors: []error{},
}
result := processItems(initialState)(context.Background())()
assert.True(t, E.IsLeft(result))
err := E.ToError(result)
assert.Contains(t, err.Error(), "failed to process item: error-item")
})
}
func TestTailRec_CancellationDuringProcessing(t *testing.T) {
// Test cancellation during a realistic processing scenario
type FileProcessState struct {
files []string
processed int
}
var processedCount int32
processFileStep := func(state FileProcessState) ReaderIOResult[E.Either[FileProcessState, int]] {
return func(ctx context.Context) IOEither[E.Either[FileProcessState, int]] {
return func() Either[E.Either[FileProcessState, int]] {
if A.IsEmpty(state.files) {
return E.Right[error](E.Right[FileProcessState](state.processed))
}
// Simulate file processing time
time.Sleep(20 * time.Millisecond)
atomic.AddInt32(&processedCount, 1)
return E.Right[error](E.Left[int](FileProcessState{
files: state.files[1:],
processed: state.processed + 1,
}))
}
}
}
processFiles := TailRec(processFileStep)
// Create many files to process
files := make([]string, 20)
for i := range files {
files[i] = fmt.Sprintf("file%d.txt", i)
}
initialState := FileProcessState{
files: files,
processed: 0,
}
// Cancel after 100ms (should allow ~5 files to be processed)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
start := time.Now()
result := processFiles(initialState)(ctx)()
elapsed := time.Since(start)
// Should be cancelled
assert.True(t, E.IsLeft(result))
// Should complete quickly due to cancellation
assert.Less(t, elapsed, 150*time.Millisecond)
// Should have processed some but not all files
processed := atomic.LoadInt32(&processedCount)
assert.Greater(t, processed, int32(0))
assert.Less(t, processed, int32(20))
}
func TestTailRec_ZeroIterations(t *testing.T) {
// Test case where recursion terminates immediately
immediateStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
return E.Right[error](E.Right[int]("immediate"))
}
}
}
immediate := TailRec(immediateStep)
result := immediate(100)(context.Background())()
assert.Equal(t, E.Of[error]("immediate"), result)
}
func TestTailRec_ContextWithDeadline(t *testing.T) {
// Test with context deadline
var iterationCount int32
slowStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
atomic.AddInt32(&iterationCount, 1)
time.Sleep(30 * time.Millisecond)
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
slowRecursion := TailRec(slowStep)
// Set deadline 80ms from now
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(80*time.Millisecond))
defer cancel()
result := slowRecursion(10)(ctx)()
// Should be cancelled due to deadline
assert.True(t, E.IsLeft(result))
// Should have executed only a few iterations
iterations := atomic.LoadInt32(&iterationCount)
assert.Greater(t, iterations, int32(0))
assert.Less(t, iterations, int32(5))
}
func TestTailRec_ContextWithValue(t *testing.T) {
// Test that context values are preserved through recursion
type contextKey string
const testKey contextKey = "test"
valueStep := func(n int) ReaderIOResult[E.Either[int, string]] {
return func(ctx context.Context) IOEither[E.Either[int, string]] {
return func() Either[E.Either[int, string]] {
value := ctx.Value(testKey)
require.NotNil(t, value)
assert.Equal(t, "test-value", value.(string))
if n <= 0 {
return E.Right[error](E.Right[int]("Done!"))
}
return E.Right[error](E.Left[string](n - 1))
}
}
}
valueRecursion := TailRec(valueStep)
ctx := context.WithValue(context.Background(), testKey, "test-value")
result := valueRecursion(3)(ctx)()
assert.Equal(t, E.Of[error]("Done!"), result)
}

View File

@@ -16,7 +16,11 @@
package readerioresult
import (
"context"
"io"
RIOR "github.com/IBM/fp-go/v2/readerioresult"
"github.com/IBM/fp-go/v2/result"
)
// WithResource constructs a function that creates a resource, then operates on it and then releases the resource.
@@ -55,3 +59,111 @@ import (
func WithResource[A, R, ANY any](onCreate ReaderIOResult[R], onRelease Kleisli[R, ANY]) Kleisli[Kleisli[R, A], A] {
return RIOR.WithResource[A](onCreate, onRelease)
}
// onClose is a helper function that creates a ReaderIOResult for closing an io.Closer resource.
// It safely calls the Close() method and handles any errors that may occur during closing.
//
// Type Parameters:
// - A: Must implement io.Closer interface
//
// Parameters:
// - a: The resource to close
//
// Returns:
// - ReaderIOResult[any]: A computation that closes the resource and returns nil on success
//
// The function ignores the context parameter since closing operations typically don't need context.
// Any error from Close() is captured and returned as a Result error.
func onClose[A io.Closer](a A) ReaderIOResult[any] {
return func(_ context.Context) IOResult[any] {
return func() Result[any] {
return result.TryCatchError[any](nil, a.Close())
}
}
}
// WithCloser creates a resource management function specifically for io.Closer resources.
// This is a specialized version of WithResource that automatically handles closing of resources
// that implement the io.Closer interface.
//
// The function ensures that:
// - The resource is created using the onCreate function
// - The resource is automatically closed when the operation completes (success or failure)
// - Any errors during closing are properly handled
// - The resource is closed even if the main operation fails or the context is canceled
//
// Type Parameters:
// - B: The type of value returned by the resource-using function
// - A: The type of resource that implements io.Closer
//
// Parameters:
// - onCreate: ReaderIOResult that creates the io.Closer resource
//
// Returns:
// - A function that takes a resource-using function and returns a ReaderIOResult[B]
//
// Example with file operations:
//
// openFile := func(filename string) ReaderIOResult[*os.File] {
// return TryCatch(func(ctx context.Context) func() (*os.File, error) {
// return func() (*os.File, error) {
// return os.Open(filename)
// }
// })
// }
//
// fileReader := WithCloser(openFile("data.txt"))
// result := fileReader(func(f *os.File) ReaderIOResult[string] {
// return TryCatch(func(ctx context.Context) func() (string, error) {
// return func() (string, error) {
// data, err := io.ReadAll(f)
// return string(data), err
// }
// })
// })
//
// Example with HTTP response:
//
// httpGet := func(url string) ReaderIOResult[*http.Response] {
// return TryCatch(func(ctx context.Context) func() (*http.Response, error) {
// return func() (*http.Response, error) {
// return http.Get(url)
// }
// })
// }
//
// responseReader := WithCloser(httpGet("https://api.example.com/data"))
// result := responseReader(func(resp *http.Response) ReaderIOResult[[]byte] {
// return TryCatch(func(ctx context.Context) func() ([]byte, error) {
// return func() ([]byte, error) {
// return io.ReadAll(resp.Body)
// }
// })
// })
//
// Example with database connection:
//
// openDB := func(dsn string) ReaderIOResult[*sql.DB] {
// return TryCatch(func(ctx context.Context) func() (*sql.DB, error) {
// return func() (*sql.DB, error) {
// return sql.Open("postgres", dsn)
// }
// })
// }
//
// dbQuery := WithCloser(openDB("postgres://..."))
// result := dbQuery(func(db *sql.DB) ReaderIOResult[[]User] {
// return TryCatch(func(ctx context.Context) func() ([]User, error) {
// return func() ([]User, error) {
// rows, err := db.QueryContext(ctx, "SELECT * FROM users")
// if err != nil {
// return nil, err
// }
// defer rows.Close()
// return scanUsers(rows)
// }
// })
// })
func WithCloser[B any, A io.Closer](onCreate ReaderIOResult[A]) Kleisli[Kleisli[A, B], B] {
return WithResource[B](onCreate, onClose[A])
}

View File

@@ -18,6 +18,7 @@ package readerioresult
import (
"github.com/IBM/fp-go/v2/array"
"github.com/IBM/fp-go/v2/function"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/record"
)
@@ -34,7 +35,7 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
Map[[]B, func(B) []B],
Ap[[]B, B],
f,
F.Flow2(f, WithContext),
)
}
@@ -78,7 +79,7 @@ func TraverseRecord[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A, ma
Map[map[K]B, func(B) map[K]B],
Ap[map[K]B, B],
f,
F.Flow2(f, WithContext),
)
}
@@ -123,7 +124,7 @@ func MonadTraverseArraySeq[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
Map[[]B, func(B) []B],
ApSeq[[]B, B],
as,
f,
F.Flow2(f, WithContext),
)
}
@@ -139,7 +140,7 @@ func TraverseArraySeq[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
Of[[]B],
Map[[]B, func(B) []B],
ApSeq[[]B, B],
f,
F.Flow2(f, WithContext),
)
}
@@ -171,7 +172,7 @@ func MonadTraverseRecordSeq[K comparable, A, B any](as map[K]A, f Kleisli[A, B])
Map[map[K]B, func(B) map[K]B],
ApSeq[map[K]B, B],
as,
f,
F.Flow2(f, WithContext),
)
}
@@ -182,7 +183,7 @@ func TraverseRecordSeq[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A,
Map[map[K]B, func(B) map[K]B],
ApSeq[map[K]B, B],
f,
F.Flow2(f, WithContext),
)
}
@@ -216,7 +217,7 @@ func MonadTraverseArrayPar[A, B any](as []A, f Kleisli[A, B]) ReaderIOResult[[]B
Map[[]B, func(B) []B],
ApPar[[]B, B],
as,
f,
F.Flow2(f, WithContext),
)
}
@@ -232,7 +233,7 @@ func TraverseArrayPar[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
Of[[]B],
Map[[]B, func(B) []B],
ApPar[[]B, B],
f,
F.Flow2(f, WithContext),
)
}
@@ -264,7 +265,7 @@ func TraverseRecordPar[K comparable, A, B any](f Kleisli[A, B]) Kleisli[map[K]A,
Map[map[K]B, func(B) map[K]B],
ApPar[map[K]B, B],
f,
F.Flow2(f, WithContext),
)
}
@@ -286,7 +287,7 @@ func MonadTraverseRecordPar[K comparable, A, B any](as map[K]A, f Kleisli[A, B])
Map[map[K]B, func(B) map[K]B],
ApPar[map[K]B, B],
as,
f,
F.Flow2(f, WithContext),
)
}

View File

@@ -18,9 +18,11 @@ package readerioresult
import (
"context"
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/context/ioresult"
"github.com/IBM/fp-go/v2/context/readerresult"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/ioeither"
"github.com/IBM/fp-go/v2/lazy"
@@ -126,4 +128,8 @@ type (
ReaderResult[A any] = readerresult.ReaderResult[A]
ReaderEither[R, E, A any] = readereither.ReaderEither[R, E, A]
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
Endomorphism[A any] = endomorphism.Endomorphism[A]
Consumer[A any] = consumer.Consumer[A]
)

View File

@@ -15,11 +15,14 @@
package readerresult
import "github.com/IBM/fp-go/v2/readereither"
import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/readereither"
)
// TraverseArray transforms an array
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return readereither.TraverseArray(f)
return readereither.TraverseArray(F.Flow2(f, WithContext))
}
// TraverseArrayWithIndex transforms an array

View File

@@ -31,6 +31,8 @@ import (
// TenantID string
// }
// result := readereither.Do(State{})
//
//go:inline
func Do[S any](
empty S,
) ReaderResult[S] {
@@ -78,14 +80,18 @@ func Do[S any](
// },
// ),
// )
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Kleisli[ReaderResult[S1], S2] {
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, f)
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, F.Flow2(f, WithContext))
}
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
//
//go:inline
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
@@ -94,6 +100,8 @@ func Let[S1, S2, T any](
}
// LetTo attaches the a value to a context [S1] to produce a context [S2]
//
//go:inline
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
@@ -102,6 +110,8 @@ func LetTo[S1, S2, T any](
}
// BindTo initializes a new state [S1] from a value [T]
//
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) Kleisli[ReaderResult[T], S1] {
@@ -145,6 +155,8 @@ func BindTo[S1, T any](
// getTenantID,
// ),
// )
//
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderResult[T],
@@ -183,6 +195,8 @@ func ApS[S1, S2, T any](
// readereither.Do(Person{Name: "Alice", Age: 25}),
// readereither.ApSL(ageLens, getAge),
// )
//
//go:inline
func ApSL[S, T any](
lens L.Lens[S, T],
fa ReaderResult[T],
@@ -227,11 +241,13 @@ func ApSL[S, T any](
// readereither.Of[error](Counter{Value: 42}),
// readereither.BindL(valueLens, increment),
// )
//
//go:inline
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Kleisli[ReaderResult[S], S] {
return Bind(lens.Set, F.Flow2(lens.Get, f))
return Bind(lens.Set, F.Flow2(lens.Get, F.Flow2(f, WithContext)))
}
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
@@ -262,9 +278,11 @@ func BindL[S, T any](
// readereither.LetL(valueLens, double),
// )
// // result when executed will be Right(Counter{Value: 42})
//
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
f func(T) T,
f Endomorphism[T],
) Kleisli[ReaderResult[S], S] {
return Let(lens.Set, F.Flow2(lens.Get, f))
}
@@ -296,6 +314,8 @@ func LetL[S, T any](
// readereither.LetToL(debugLens, false),
// )
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
//
//go:inline
func LetToL[S, T any](
lens L.Lens[S, T],
b T,

View File

@@ -24,8 +24,8 @@ import (
// withContext wraps an existing ReaderResult and performs a context check for cancellation before deletating
func WithContext[A any](ma ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) E.Either[error, A] {
if err := context.Cause(ctx); err != nil {
return E.Left[A](err)
if ctx.Err() != nil {
return E.Left[A](context.Cause(ctx))
}
return ma(ctx)
}

View File

@@ -0,0 +1,154 @@
// 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 readerresult
import (
"context"
"github.com/IBM/fp-go/v2/reader"
RR "github.com/IBM/fp-go/v2/readerresult"
)
// SequenceReader swaps the order of environment parameters when the inner computation is a Reader.
//
// This function is specialized for the context.Context-based ReaderResult monad. It takes a
// ReaderResult that produces a Reader and returns a reader.Kleisli that produces Results.
// The context.Context is implicitly used as the outer environment type.
//
// Type Parameters:
// - R: The inner environment type (becomes outer after flip)
// - A: The success value type
//
// Parameters:
// - ma: A ReaderResult that takes context.Context and may produce a Reader[R, A]
//
// Returns:
// - A reader.Kleisli[context.Context, R, Result[A]], which is func(context.Context) func(R) Result[A]
//
// The function preserves error handling from the outer ReaderResult layer. If the outer
// computation fails, the error is propagated to the inner Result.
//
// Note: This is an inline wrapper around readerresult.SequenceReader, specialized for
// context.Context as the outer environment type.
//
// Example:
//
// type Database struct {
// ConnectionString string
// }
//
// // Original: takes context, may fail, produces Reader[Database, string]
// original := func(ctx context.Context) result.Result[reader.Reader[Database, string]] {
// if ctx.Err() != nil {
// return result.Error[reader.Reader[Database, string]](ctx.Err())
// }
// return result.Ok[error](func(db Database) string {
// return fmt.Sprintf("Query on %s", db.ConnectionString)
// })
// }
//
// // Sequenced: takes context first, then Database
// sequenced := SequenceReader(original)
//
// ctx := context.Background()
// db := Database{ConnectionString: "localhost:5432"}
//
// // Apply context first to get a function that takes database
// dbReader := sequenced(ctx)
// // Then apply database to get the final result
// result := dbReader(db)
// // result is Result[string]
//
// Use Cases:
// - Dependency injection: Flip parameter order to inject context first, then dependencies
// - Testing: Separate context handling from business logic for easier testing
// - Composition: Enable point-free style by fixing the context parameter first
//
//go:inline
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[context.Context, R, Result[A]] {
return RR.SequenceReader(ma)
}
// TraverseReader transforms a value using a Reader function and swaps environment parameter order.
//
// This function combines mapping and parameter flipping in a single operation. It takes a
// Reader function (pure computation without error handling) and returns a function that:
// 1. Maps a ReaderResult[A] to ReaderResult[B] using the provided Reader function
// 2. Flips the parameter order so R comes before context.Context
//
// Type Parameters:
// - R: The inner environment type (becomes outer after flip)
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A reader.Kleisli[R, A, B], which is func(R) func(A) B - a pure Reader function
//
// Returns:
// - A function that takes ReaderResult[A] and returns Kleisli[R, B]
// - Kleisli[R, B] is func(R) ReaderResult[B], which is func(R) func(context.Context) Result[B]
//
// The function preserves error handling from the input ReaderResult. If the input computation
// fails, the error is propagated without applying the transformation function.
//
// Note: This is a wrapper around readerresult.TraverseReader, specialized for context.Context.
//
// Example:
//
// type Config struct {
// MaxRetries int
// }
//
// // A pure Reader function that depends on Config
// formatMessage := func(cfg Config) func(int) string {
// return func(value int) string {
// return fmt.Sprintf("Value: %d, MaxRetries: %d", value, cfg.MaxRetries)
// }
// }
//
// // Original computation that may fail
// computation := func(ctx context.Context) result.Result[int] {
// if ctx.Err() != nil {
// return result.Error[int](ctx.Err())
// }
// return result.Ok[error](42)
// }
//
// // Create a traversal that applies formatMessage and flips parameters
// traverse := TraverseReader[Config, int, string](formatMessage)
//
// // Apply to the computation
// flipped := traverse(computation)
//
// // Now we can provide Config first, then context
// cfg := Config{MaxRetries: 3}
// ctx := context.Background()
//
// result := flipped(cfg)(ctx)
// // result is Result[string] containing "Value: 42, MaxRetries: 3"
//
// Use Cases:
// - Dependency injection: Inject configuration/dependencies before context
// - Testing: Separate pure business logic from context handling
// - Composition: Build pipelines where dependencies are fixed before execution
// - Point-free style: Enable partial application by fixing dependencies first
//
//go:inline
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderResult[A]) Kleisli[R, B] {
return RR.TraverseReader[context.Context](f)
}

View File

@@ -0,0 +1,215 @@
// 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 readerresult provides logging utilities for the ReaderResult monad,
// which combines the Reader monad (for dependency injection via context.Context)
// with the Result monad (for error handling).
//
// The logging functions in this package allow you to log Result values (both
// successes and errors) while preserving the functional composition style.
package readerresult
import (
"context"
"log/slog"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/logging"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
// curriedLog creates a curried logging function that takes an slog.Attr and a context,
// then logs the attribute with the specified log level and message.
//
// This is an internal helper function used to create the logging pipeline in a
// point-free style. The currying allows for partial application in functional
// composition.
//
// Parameters:
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelError)
// - cb: A callback function that retrieves a logger from the context
// - message: The log message to display
//
// Returns:
// - A curried function that takes an slog.Attr, then a context, and performs logging
func curriedLog(
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) func(slog.Attr) Reader[context.Context, struct{}] {
return F.Curry2(func(a slog.Attr, ctx context.Context) struct{} {
cb(ctx).LogAttrs(ctx, logLevel, message, a)
return struct{}{}
})
}
// SLogWithCallback creates a Kleisli arrow that logs a Result value using a custom
// logger callback and log level. The Result value is logged and then returned unchanged,
// making this function suitable for use in functional pipelines.
//
// This function logs both successful values and errors:
// - Success values are logged with the key "value"
// - Error values are logged with the key "error"
//
// The logging is performed as a side effect while preserving the Result value,
// allowing it to be used in the middle of a computation pipeline without
// interrupting the flow.
//
// Type Parameters:
// - A: The type of the success value in the Result
//
// Parameters:
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelDebug, LevelError)
// - cb: A callback function that retrieves a *slog.Logger from the context
// - message: The log message to display
//
// Returns:
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
// The returned ReaderResult, when executed with a context, logs the Result
// and returns it unchanged
//
// Example:
//
// type User struct {
// ID int
// Name string
// }
//
// // Custom logger callback
// getLogger := func(ctx context.Context) *slog.Logger {
// return slog.Default()
// }
//
// // Create a logging function for debug level
// logDebug := SLogWithCallback[User](slog.LevelDebug, getLogger, "User data")
//
// // Use in a pipeline
// ctx := context.Background()
// user := result.Of(User{ID: 123, Name: "Alice"})
// logged := logDebug(user)(ctx) // Logs: level=DEBUG msg="User data" value={ID:123 Name:Alice}
// // logged still contains the User value
//
// Example with error:
//
// err := errors.New("user not found")
// userResult := result.Left[User](err)
// logged := logDebug(userResult)(ctx) // Logs: level=DEBUG msg="User data" error="user not found"
// // logged still contains the error
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) Kleisli[Result[A], A] {
return F.Pipe1(
F.Flow2(
result.ToSLogAttr[A](),
curriedLog(logLevel, cb, message),
),
reader.Chain(reader.Sequence(F.Flow2( // this flow is basically the `MapTo` function with side effects
reader.Of[struct{}, Result[A]],
reader.Map[context.Context, struct{}, Result[A]],
))),
)
}
// SLog creates a Kleisli arrow that logs a Result value at INFO level using the
// logger from the context. This is a convenience function that uses SLogWithCallback
// with default settings.
//
// The Result value is logged and then returned unchanged, making this function
// suitable for use in functional pipelines for debugging or monitoring purposes.
//
// This function logs both successful values and errors:
// - Success values are logged with the key "value"
// - Error values are logged with the key "error"
//
// Type Parameters:
// - A: The type of the success value in the Result
//
// Parameters:
// - message: The log message to display
//
// Returns:
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
// The returned ReaderResult, when executed with a context, logs the Result
// at INFO level and returns it unchanged
//
// Example - Logging a successful computation:
//
// ctx := context.Background()
//
// // Simple value logging
// res := result.Of(42)
// logged := SLog[int]("Processing number")(res)(ctx)
// // Logs: level=INFO msg="Processing number" value=42
// // logged == result.Of(42)
//
// Example - Logging in a pipeline:
//
// type User struct {
// ID int
// Name string
// }
//
// fetchUser := func(id int) result.Result[User] {
// return result.Of(User{ID: id, Name: "Alice"})
// }
//
// processUser := func(user User) result.Result[string] {
// return result.Of(fmt.Sprintf("Processed: %s", user.Name))
// }
//
// ctx := context.Background()
//
// // Log at each step
// userResult := fetchUser(123)
// logged1 := SLog[User]("Fetched user")(userResult)(ctx)
// // Logs: level=INFO msg="Fetched user" value={ID:123 Name:Alice}
//
// processed := result.Chain(processUser)(logged1)
// logged2 := SLog[string]("Processed user")(processed)(ctx)
// // Logs: level=INFO msg="Processed user" value="Processed: Alice"
//
// Example - Logging errors:
//
// err := errors.New("database connection failed")
// errResult := result.Left[User](err)
// logged := SLog[User]("Database operation")(errResult)(ctx)
// // Logs: level=INFO msg="Database operation" error="database connection failed"
// // logged still contains the error
//
// Example - Using with context logger:
//
// // Set up a custom logger in the context
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// ctx := logging.WithLogger(logger)(context.Background())
//
// res := result.Of("important data")
// logged := SLog[string]("Critical operation")(res)(ctx)
// // Uses the logger from context to log the message
//
// Note: The function uses logging.GetLoggerFromContext to retrieve the logger,
// which falls back to the global logger if no logger is found in the context.
//
//go:inline
func SLog[A any](message string) Kleisli[Result[A], A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}
//go:inline
func TapSLog[A any](message string) Operator[A, A] {
return reader.Chain(SLog[A](message))
}

View File

@@ -0,0 +1,302 @@
// 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 readerresult
import (
"bytes"
"context"
"errors"
"log/slog"
"testing"
"github.com/IBM/fp-go/v2/logging"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestSLogLogsSuccessValue tests that SLog logs successful Result values
func TestSLogLogsSuccessValue(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// Create a Result and log it
res1 := result.Of(42)
logged := SLog[int]("Result value")(res1)(ctx)
assert.Equal(t, result.Of(42), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Result value")
assert.Contains(t, logOutput, "value=42")
}
// TestSLogLogsErrorValue tests that SLog logs error Result values
func TestSLogLogsErrorValue(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
testErr := errors.New("test error")
// Create an error Result and log it
res1 := result.Left[int](testErr)
logged := SLog[int]("Result value")(res1)(ctx)
assert.Equal(t, res1, logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Result value")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "test error")
}
// TestSLogInPipeline tests SLog in a functional pipeline
func TestSLogInPipeline(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// SLog takes a Result[A] and returns ReaderResult[A]
// So we need to start with a Result, apply SLog, then execute with context
res1 := result.Of(10)
logged := SLog[int]("Initial value")(res1)(ctx)
assert.Equal(t, result.Of(10), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Initial value")
assert.Contains(t, logOutput, "value=10")
}
// TestSLogWithContextLogger tests SLog using logger from context
func TestSLogWithContextLogger(t *testing.T) {
var buf bytes.Buffer
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
ctx := logging.WithLogger(contextLogger)(context.Background())
res1 := result.Of("test value")
logged := SLog[string]("Context logger test")(res1)(ctx)
assert.Equal(t, result.Of("test value"), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Context logger test")
assert.Contains(t, logOutput, `value="test value"`)
}
// TestSLogDisabled tests that SLog respects logger level
func TestSLogDisabled(t *testing.T) {
var buf bytes.Buffer
// Create logger with level that disables info logs
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelError, // Only log errors
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
res1 := result.Of(42)
logged := SLog[int]("This should not be logged")(res1)(ctx)
assert.Equal(t, result.Of(42), logged)
// Should have no logs since level is ERROR
logOutput := buf.String()
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
}
// TestSLogWithStruct tests SLog with structured data
func TestSLogWithStruct(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
type User struct {
ID int
Name string
}
ctx := context.Background()
user := User{ID: 123, Name: "Alice"}
res1 := result.Of(user)
logged := SLog[User]("User data")(res1)(ctx)
assert.Equal(t, result.Of(user), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "User data")
assert.Contains(t, logOutput, "ID:123")
assert.Contains(t, logOutput, "Name:Alice")
}
// TestSLogWithCallbackCustomLevel tests SLogWithCallback with custom log level
func TestSLogWithCallbackCustomLevel(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
ctx := context.Background()
// Create a Result and log it with custom callback
res1 := result.Of(42)
logged := SLogWithCallback[int](slog.LevelDebug, customCallback, "Debug result")(res1)(ctx)
assert.Equal(t, result.Of(42), logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Debug result")
assert.Contains(t, logOutput, "value=42")
assert.Contains(t, logOutput, "level=DEBUG")
}
// TestSLogWithCallbackLogsError tests SLogWithCallback logs errors
func TestSLogWithCallbackLogsError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelWarn,
}))
customCallback := func(ctx context.Context) *slog.Logger {
return logger
}
ctx := context.Background()
testErr := errors.New("warning error")
// Create an error Result and log it with custom callback
res1 := result.Left[int](testErr)
logged := SLogWithCallback[int](slog.LevelWarn, customCallback, "Warning result")(res1)(ctx)
assert.Equal(t, res1, logged)
logOutput := buf.String()
assert.Contains(t, logOutput, "Warning result")
assert.Contains(t, logOutput, "error")
assert.Contains(t, logOutput, "warning error")
assert.Contains(t, logOutput, "level=WARN")
}
// TestSLogChainedOperations tests SLog in chained operations
func TestSLogChainedOperations(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// First log step 1
res1 := result.Of(5)
logged1 := SLog[int]("Step 1")(res1)(ctx)
// Then log step 2 with doubled value
res2 := result.Map(N.Mul(2))(logged1)
logged2 := SLog[int]("Step 2")(res2)(ctx)
assert.Equal(t, result.Of(10), logged2)
logOutput := buf.String()
assert.Contains(t, logOutput, "Step 1")
assert.Contains(t, logOutput, "value=5")
assert.Contains(t, logOutput, "Step 2")
assert.Contains(t, logOutput, "value=10")
}
// TestSLogPreservesError tests that SLog preserves error through the pipeline
func TestSLogPreservesError(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
testErr := errors.New("original error")
res1 := result.Left[int](testErr)
logged := SLog[int]("Logging error")(res1)(ctx)
// Apply map to verify error is preserved
res2 := result.Map(N.Mul(2))(logged)
assert.Equal(t, res1, res2)
logOutput := buf.String()
assert.Contains(t, logOutput, "Logging error")
assert.Contains(t, logOutput, "original error")
}
// TestSLogMultipleValues tests logging multiple different values
func TestSLogMultipleValues(t *testing.T) {
var buf bytes.Buffer
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
oldLogger := logging.SetLogger(logger)
defer logging.SetLogger(oldLogger)
ctx := context.Background()
// Test with different types
intRes := SLog[int]("Integer")(result.Of(42))(ctx)
assert.Equal(t, result.Of(42), intRes)
strRes := SLog[string]("String")(result.Of("hello"))(ctx)
assert.Equal(t, result.Of("hello"), strRes)
boolRes := SLog[bool]("Boolean")(result.Of(true))(ctx)
assert.Equal(t, result.Of(true), boolRes)
logOutput := buf.String()
assert.Contains(t, logOutput, "Integer")
assert.Contains(t, logOutput, "value=42")
assert.Contains(t, logOutput, "String")
assert.Contains(t, logOutput, "value=hello")
assert.Contains(t, logOutput, "Boolean")
assert.Contains(t, logOutput, "value=true")
}

View File

@@ -18,9 +18,17 @@ package readerresult
import (
"context"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
)
func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
return readereither.FromReader[error](r)
}
func FromEither[A any](e Either[A]) ReaderResult[A] {
return readereither.FromEither[context.Context](e)
}
@@ -42,11 +50,11 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
}
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
return readereither.MonadChain(ma, f)
return readereither.MonadChain(ma, F.Flow2(f, WithContext))
}
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return readereither.Chain(f)
return readereither.Chain(F.Flow2(f, WithContext))
}
func Of[A any](a A) ReaderResult[A] {
@@ -66,7 +74,7 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A
}
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] {
return readereither.OrElse(onLeft)
return readereither.OrElse(F.Flow2(onLeft, WithContext))
}
func Ask() ReaderResult[context.Context] {
@@ -81,7 +89,7 @@ func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderResult[A]) Reader
return readereither.ChainEitherK[context.Context](f)
}
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
return readereither.ChainOptionK[context.Context, A, B](onNone)
}
@@ -97,3 +105,197 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
return readereither.Read[error, A](r)
}
// MonadMapTo executes a ReaderResult computation, discards its success value, and returns a constant value.
// This is the monadic version that takes both the ReaderResult and the constant value as parameters.
//
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadMapTo WILL
// execute the original ReaderResult to allow any side effects to occur, then discard the success result
// and return the constant value. If the original computation fails, the error is preserved.
//
// Type Parameters:
// - A: The success type of the first ReaderResult (will be discarded if successful)
// - B: The type of the constant value to return on success
//
// Parameters:
// - ma: The ReaderResult to execute (side effects will occur, success value discarded)
// - b: The constant value to return if ma succeeds
//
// Returns:
// - A ReaderResult that executes ma, preserves errors, but replaces success values with b
//
// Example:
//
// type Config struct { Counter int }
// increment := func(ctx context.Context) result.Result[int] {
// // Side effect: log the operation
// fmt.Println("incrementing")
// return result.Of(5)
// }
// r := readerresult.MonadMapTo(increment, "done")
// result := r(context.Background()) // Prints "incrementing", returns Right("done")
//
//go:inline
func MonadMapTo[A, B any](ma ReaderResult[A], b B) ReaderResult[B] {
return MonadMap(ma, reader.Of[A](b))
}
// MapTo creates an operator that executes a ReaderResult computation, discards its success value,
// and returns a constant value. This is the curried version where the constant value is provided first,
// returning a function that can be applied to any ReaderResult.
//
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MapTo WILL
// execute the input ReaderResult to allow any side effects to occur, then discard the success result
// and return the constant value. If the computation fails, the error is preserved.
//
// Type Parameters:
// - A: The success type of the input ReaderResult (will be discarded if successful)
// - B: The type of the constant value to return on success
//
// Parameters:
// - b: The constant value to return on success
//
// Returns:
// - An Operator that executes a ReaderResult[A], preserves errors, but replaces success with b
//
// Example:
//
// logStep := func(ctx context.Context) result.Result[int] {
// fmt.Println("step executed")
// return result.Of(42)
// }
// toDone := readerresult.MapTo[int, string]("done")
// pipeline := toDone(logStep)
// result := pipeline(context.Background()) // Prints "step executed", returns Right("done")
//
// Example - In a functional pipeline:
//
// step1 := func(ctx context.Context) result.Result[int] {
// fmt.Println("processing")
// return result.Of(1)
// }
// pipeline := F.Pipe1(
// step1,
// readerresult.MapTo[int, string]("complete"),
// )
// output := pipeline(context.Background()) // Prints "processing", returns Right("complete")
//
//go:inline
func MapTo[A, B any](b B) Operator[A, B] {
return Map(reader.Of[A](b))
}
// MonadChainTo sequences two ReaderResult computations where the second ignores the first's success value.
// This is the monadic version that takes both ReaderResults as parameters.
//
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadChainTo WILL
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
// execute the second ReaderResult with the same context. If the first computation fails, the error is
// returned immediately without executing the second computation.
//
// Type Parameters:
// - A: The success type of the first ReaderResult (will be discarded if successful)
// - B: The success type of the second ReaderResult
//
// Parameters:
// - ma: The first ReaderResult to execute (side effects will occur, success value discarded)
// - b: The second ReaderResult to execute if ma succeeds
//
// Returns:
// - A ReaderResult that executes ma, then b if ma succeeds, returning b's result
//
// Example:
//
// logStart := func(ctx context.Context) result.Result[int] {
// fmt.Println("starting")
// return result.Of(1)
// }
// logEnd := func(ctx context.Context) result.Result[string] {
// fmt.Println("ending")
// return result.Of("done")
// }
// r := readerresult.MonadChainTo(logStart, logEnd)
// result := r(context.Background()) // Prints "starting" then "ending", returns Right("done")
//
//go:inline
func MonadChainTo[A, B any](ma ReaderResult[A], b ReaderResult[B]) ReaderResult[B] {
return MonadChain(ma, reader.Of[A](b))
}
// ChainTo creates an operator that sequences two ReaderResult computations where the second ignores
// the first's success value. This is the curried version where the second ReaderResult is provided first,
// returning a function that can be applied to any first ReaderResult.
//
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, ChainTo WILL
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
// execute the second ReaderResult with the same context. If the first computation fails, the error is
// returned immediately without executing the second computation.
//
// Type Parameters:
// - A: The success type of the first ReaderResult (will be discarded if successful)
// - B: The success type of the second ReaderResult
//
// Parameters:
// - b: The second ReaderResult to execute after the first succeeds
//
// Returns:
// - An Operator that executes the first ReaderResult, then b if successful
//
// Example:
//
// logEnd := func(ctx context.Context) result.Result[string] {
// fmt.Println("ending")
// return result.Of("done")
// }
// thenLogEnd := readerresult.ChainTo[int, string](logEnd)
//
// logStart := func(ctx context.Context) result.Result[int] {
// fmt.Println("starting")
// return result.Of(1)
// }
// pipeline := thenLogEnd(logStart)
// result := pipeline(context.Background()) // Prints "starting" then "ending", returns Right("done")
//
// Example - In a functional pipeline:
//
// step1 := func(ctx context.Context) result.Result[int] {
// fmt.Println("step 1")
// return result.Of(1)
// }
// step2 := func(ctx context.Context) result.Result[string] {
// fmt.Println("step 2")
// return result.Of("complete")
// }
// pipeline := F.Pipe1(
// step1,
// readerresult.ChainTo[int, string](step2),
// )
// output := pipeline(context.Background()) // Prints "step 1" then "step 2", returns Right("complete")
//
//go:inline
func ChainTo[A, B any](b ReaderResult[B]) Operator[A, B] {
return Chain(reader.Of[A](b))
}
//go:inline
func MonadChainFirst[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[A] {
return chain.MonadChainFirst(
MonadChain,
MonadMap,
ma,
F.Flow2(f, WithContext),
)
}
//go:inline
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return chain.ChainFirst(
Chain,
Map,
F.Flow2(f, WithContext),
)
}

View File

@@ -0,0 +1,315 @@
// 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 readerresult
import (
"context"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestMapTo(t *testing.T) {
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
executed := false
originalReader := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](42)
}
// Apply MapTo operator
toDone := MapTo[int]("done")
resultReader := toDone(originalReader)
// Execute the resulting reader
result := resultReader(context.Background())
// Verify the constant value is returned
assert.Equal(t, E.Of[error]("done"), result)
// Verify the original reader WAS executed (side effect occurred)
assert.True(t, executed, "original reader should be executed to allow side effects")
})
t.Run("executes reader in functional pipeline", func(t *testing.T) {
executed := false
step1 := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](100)
}
pipeline := F.Pipe1(
step1,
MapTo[int]("complete"),
)
result := pipeline(context.Background())
assert.Equal(t, E.Of[error]("complete"), result)
assert.True(t, executed, "original reader should be executed in pipeline")
})
t.Run("executes reader with side effects", func(t *testing.T) {
sideEffectOccurred := false
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
sideEffectOccurred = true
return E.Of[error](42)
}
resultReader := MapTo[int](true)(readerWithSideEffect)
result := resultReader(context.Background())
assert.Equal(t, E.Of[error](true), result)
assert.True(t, sideEffectOccurred, "side effect should occur")
})
t.Run("preserves errors from original reader", func(t *testing.T) {
executed := false
testErr := assert.AnError
failingReader := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Left[int](testErr)
}
resultReader := MapTo[int]("done")(failingReader)
result := resultReader(context.Background())
assert.Equal(t, E.Left[string](testErr), result)
assert.True(t, executed, "failing reader should still be executed")
})
}
func TestMonadMapTo(t *testing.T) {
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
executed := false
originalReader := func(ctx context.Context) E.Either[error, int] {
executed = true
return E.Of[error](42)
}
// Apply MonadMapTo
resultReader := MonadMapTo(originalReader, "done")
// Execute the resulting reader
result := resultReader(context.Background())
// Verify the constant value is returned
assert.Equal(t, E.Of[error]("done"), result)
// Verify the original reader WAS executed (side effect occurred)
assert.True(t, executed, "original reader should be executed to allow side effects")
})
t.Run("executes complex computation with side effects", func(t *testing.T) {
computationExecuted := false
complexReader := func(ctx context.Context) E.Either[error, string] {
computationExecuted = true
return E.Of[error]("complex result")
}
resultReader := MonadMapTo(complexReader, 42)
result := resultReader(context.Background())
assert.Equal(t, E.Of[error](42), result)
assert.True(t, computationExecuted, "complex computation should be executed")
})
t.Run("preserves errors from original reader", func(t *testing.T) {
executed := false
testErr := assert.AnError
failingReader := func(ctx context.Context) E.Either[error, []string] {
executed = true
return E.Left[[]string](testErr)
}
resultReader := MonadMapTo(failingReader, 99)
result := resultReader(context.Background())
assert.Equal(t, E.Left[int](testErr), result)
assert.True(t, executed, "failing reader should still be executed")
})
}
func TestChainTo(t *testing.T) {
t.Run("executes first reader then second reader on success", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
firstReader := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Of[error](42)
}
secondReader := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("result")
}
// Apply ChainTo operator
thenSecond := ChainTo[int](secondReader)
resultReader := thenSecond(firstReader)
// Execute the resulting reader
result := resultReader(context.Background())
// Verify the second reader's result is returned
assert.Equal(t, E.Of[error]("result"), result)
// Verify both readers were executed
assert.True(t, firstExecuted, "first reader should be executed")
assert.True(t, secondExecuted, "second reader should be executed")
})
t.Run("executes both readers in functional pipeline", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
step1 := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Of[error](100)
}
step2 := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("complete")
}
pipeline := F.Pipe1(
step1,
ChainTo[int](step2),
)
result := pipeline(context.Background())
assert.Equal(t, E.Of[error]("complete"), result)
assert.True(t, firstExecuted, "first reader should be executed in pipeline")
assert.True(t, secondExecuted, "second reader should be executed in pipeline")
})
t.Run("executes first reader with side effects", func(t *testing.T) {
sideEffectOccurred := false
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
sideEffectOccurred = true
return E.Of[error](42)
}
secondReader := func(ctx context.Context) E.Either[error, bool] {
return E.Of[error](true)
}
resultReader := ChainTo[int](secondReader)(readerWithSideEffect)
result := resultReader(context.Background())
assert.Equal(t, E.Of[error](true), result)
assert.True(t, sideEffectOccurred, "side effect should occur in first reader")
})
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
testErr := assert.AnError
failingReader := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Left[int](testErr)
}
secondReader := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("result")
}
resultReader := ChainTo[int](secondReader)(failingReader)
result := resultReader(context.Background())
assert.Equal(t, E.Left[string](testErr), result)
assert.True(t, firstExecuted, "first reader should be executed")
assert.False(t, secondExecuted, "second reader should not be executed on error")
})
}
func TestMonadChainTo(t *testing.T) {
t.Run("executes first reader then second reader on success", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
firstReader := func(ctx context.Context) E.Either[error, int] {
firstExecuted = true
return E.Of[error](42)
}
secondReader := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("result")
}
// Apply MonadChainTo
resultReader := MonadChainTo(firstReader, secondReader)
// Execute the resulting reader
result := resultReader(context.Background())
// Verify the second reader's result is returned
assert.Equal(t, E.Of[error]("result"), result)
// Verify both readers were executed
assert.True(t, firstExecuted, "first reader should be executed")
assert.True(t, secondExecuted, "second reader should be executed")
})
t.Run("executes complex first computation with side effects", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
complexFirstReader := func(ctx context.Context) E.Either[error, []int] {
firstExecuted = true
return E.Of[error]([]int{1, 2, 3})
}
secondReader := func(ctx context.Context) E.Either[error, string] {
secondExecuted = true
return E.Of[error]("done")
}
resultReader := MonadChainTo(complexFirstReader, secondReader)
result := resultReader(context.Background())
assert.Equal(t, E.Of[error]("done"), result)
assert.True(t, firstExecuted, "complex first computation should be executed")
assert.True(t, secondExecuted, "second reader should be executed")
})
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
firstExecuted := false
secondExecuted := false
testErr := assert.AnError
failingReader := func(ctx context.Context) E.Either[error, map[string]int] {
firstExecuted = true
return E.Left[map[string]int](testErr)
}
secondReader := func(ctx context.Context) E.Either[error, float64] {
secondExecuted = true
return E.Of[error](3.14)
}
resultReader := MonadChainTo(failingReader, secondReader)
result := resultReader(context.Background())
assert.Equal(t, E.Left[float64](testErr), result)
assert.True(t, firstExecuted, "first reader should be executed")
assert.False(t, secondExecuted, "second reader should not be executed on error")
})
}

View File

@@ -0,0 +1,106 @@
// 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 readerresult implements a specialization of the Reader monad assuming a golang context as the context of the monad and a standard golang error
package readerresult
import (
"context"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/result"
)
// TailRec implements tail-recursive computation for ReaderResult with context cancellation support.
//
// TailRec takes a Kleisli function that returns Either[A, B] and converts it into a stack-safe,
// tail-recursive computation. The function repeatedly applies the Kleisli until it produces a Right value.
//
// The implementation includes a short-circuit mechanism that checks for context cancellation on each
// iteration. If the context is canceled (ctx.Err() != nil), the computation immediately returns a
// Left result containing the context's cause error, preventing unnecessary computation.
//
// Type Parameters:
// - A: The input type for the recursive step
// - B: The final result type
//
// Parameters:
// - f: A Kleisli function that takes an A and returns a ReaderResult containing Either[A, B].
// When the result is Left[B](a), recursion continues with the new value 'a'.
// When the result is Right[A](b), recursion terminates with the final value 'b'.
//
// Returns:
// - A Kleisli function that performs the tail-recursive computation in a stack-safe manner.
//
// Behavior:
// - On each iteration, checks if the context has been canceled (short circuit)
// - If canceled, returns result.Left[B](context.Cause(ctx))
// - If the step returns Left[B](error), propagates the error
// - If the step returns Right[A](Left[B](a)), continues recursion with new value 'a'
// - If the step returns Right[A](Right[A](b)), terminates with success value 'b'
//
// Example - Factorial computation with context:
//
// type State struct {
// n int
// acc int
// }
//
// factorialStep := func(state State) ReaderResult[either.Either[State, int]] {
// return func(ctx context.Context) result.Result[either.Either[State, int]] {
// if state.n <= 0 {
// return result.Of(either.Right[State](state.acc))
// }
// return result.Of(either.Left[int](State{state.n - 1, state.acc * state.n}))
// }
// }
//
// factorial := TailRec(factorialStep)
// result := factorial(State{5, 1})(ctx) // Returns result.Of(120)
//
// Example - Context cancellation:
//
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
//
// computation := TailRec(someStep)
// result := computation(initialValue)(ctx)
// // Returns result.Left[B](context.Cause(ctx)) without executing any steps
//
//go:inline
func TailRec[A, B any](f Kleisli[A, either.Either[A, B]]) Kleisli[A, B] {
return func(a A) ReaderResult[B] {
initialReader := f(a)
return func(ctx context.Context) Result[B] {
rdr := initialReader
for {
// short circuit
if ctx.Err() != nil {
return result.Left[B](context.Cause(ctx))
}
current := rdr(ctx)
rec, e := either.Unwrap(current)
if either.IsLeft(current) {
return result.Left[B](e)
}
b, a := either.Unwrap(rec)
if either.IsRight(rec) {
return result.Of(b)
}
rdr = f(a)
}
}
}
}

View File

@@ -0,0 +1,498 @@
// 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 readerresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestTailRecFactorial tests factorial computation with context
func TestTailRecFactorial(t *testing.T) {
type State struct {
n int
acc int
}
factorialStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.n <= 0 {
return R.Of(E.Right[State](state.acc))
}
return R.Of(E.Left[int](State{state.n - 1, state.acc * state.n}))
}
}
factorial := TailRec(factorialStep)
result := factorial(State{5, 1})(context.Background())
assert.Equal(t, R.Of(120), result)
}
// TestTailRecFibonacci tests Fibonacci computation
func TestTailRecFibonacci(t *testing.T) {
type State struct {
n int
prev int
curr int
}
fibStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.n <= 0 {
return R.Of(E.Right[State](state.curr))
}
return R.Of(E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
}
}
fib := TailRec(fibStep)
result := fib(State{10, 0, 1})(context.Background())
assert.Equal(t, R.Of(89), result) // 10th Fibonacci number
}
// TestTailRecCountdown tests countdown computation
func TestTailRecCountdown(t *testing.T) {
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(context.Background())
assert.Equal(t, R.Of(0), result)
}
// TestTailRecImmediateTermination tests immediate termination (Right on first call)
func TestTailRecImmediateTermination(t *testing.T) {
immediateStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
return R.Of(E.Right[int](n * 2))
}
}
immediate := TailRec(immediateStep)
result := immediate(42)(context.Background())
assert.Equal(t, R.Of(84), result)
}
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
func TestTailRecStackSafety(t *testing.T) {
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10000)(context.Background())
assert.Equal(t, R.Of(0), result)
}
// TestTailRecSumList tests summing a list
func TestTailRecSumList(t *testing.T) {
type State struct {
list []int
sum int
}
sumStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if A.IsEmpty(state.list) {
return R.Of(E.Right[State](state.sum))
}
return R.Of(E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
}
}
sumList := TailRec(sumStep)
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
assert.Equal(t, R.Of(15), result)
}
// TestTailRecCollatzConjecture tests the Collatz conjecture
func TestTailRecCollatzConjecture(t *testing.T) {
collatzStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 1 {
return R.Of(E.Right[int](n))
}
if n%2 == 0 {
return R.Of(E.Left[int](n / 2))
}
return R.Of(E.Left[int](3*n + 1))
}
}
collatz := TailRec(collatzStep)
result := collatz(10)(context.Background())
assert.Equal(t, R.Of(1), result)
}
// TestTailRecGCD tests greatest common divisor
func TestTailRecGCD(t *testing.T) {
type State struct {
a int
b int
}
gcdStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.b == 0 {
return R.Of(E.Right[State](state.a))
}
return R.Of(E.Left[int](State{state.b, state.a % state.b}))
}
}
gcd := TailRec(gcdStep)
result := gcd(State{48, 18})(context.Background())
assert.Equal(t, R.Of(6), result)
}
// TestTailRecErrorPropagation tests that errors are properly propagated
func TestTailRecErrorPropagation(t *testing.T) {
expectedErr := errors.New("computation error")
errorStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n == 5 {
return R.Left[E.Either[int, int]](expectedErr)
}
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
computation := TailRec(errorStep)
result := computation(10)(context.Background())
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
assert.Equal(t, expectedErr, err)
}
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
func TestTailRecContextCancellationImmediate(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately before execution
stepExecuted := false
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
stepExecuted = true
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(ctx)
// Should short circuit without executing any steps
assert.False(t, stepExecuted, "Step should not be executed when context is already canceled")
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
assert.Equal(t, context.Canceled, err)
}
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
// Cancel after 3 iterations
if executionCount == 3 {
cancel()
}
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(100)(ctx)
// Should stop after cancellation
assert.True(t, R.IsLeft(result))
assert.LessOrEqual(t, executionCount, 4, "Should stop shortly after cancellation")
_, err := R.Unwrap(result)
assert.Equal(t, context.Canceled, err)
}
// TestTailRecContextWithTimeout tests behavior with timeout context
func TestTailRecContextWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
executionCount := 0
slowStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
// Simulate slow computation
time.Sleep(20 * time.Millisecond)
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
computation := TailRec(slowStep)
result := computation(100)(ctx)
// Should timeout and return error
assert.True(t, R.IsLeft(result))
assert.Less(t, executionCount, 100, "Should not complete all iterations due to timeout")
_, err := R.Unwrap(result)
assert.Equal(t, context.DeadlineExceeded, err)
}
// TestTailRecContextWithCause tests that context.Cause is properly returned
func TestTailRecContextWithCause(t *testing.T) {
customErr := errors.New("custom cancellation reason")
ctx, cancel := context.WithCancelCause(context.Background())
cancel(customErr)
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(ctx)
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
assert.Equal(t, customErr, err)
}
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
maxExecutions := 5
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
if executionCount == maxExecutions {
cancel()
}
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(1000)(ctx)
// Should detect cancellation on next iteration check
assert.True(t, R.IsLeft(result))
// Should stop within 1-2 iterations after cancellation
assert.LessOrEqual(t, executionCount, maxExecutions+2)
_, err := R.Unwrap(result)
assert.Equal(t, context.Canceled, err)
}
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
func TestTailRecContextNotCanceled(t *testing.T) {
ctx := context.Background()
executionCount := 0
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(ctx)
assert.Equal(t, 11, executionCount) // 10, 9, 8, ..., 1, 0
assert.Equal(t, R.Of(0), result)
}
// TestTailRecPowerOfTwo tests computing power of 2
func TestTailRecPowerOfTwo(t *testing.T) {
type State struct {
exponent int
result int
target int
}
powerStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.exponent >= state.target {
return R.Of(E.Right[State](state.result))
}
return R.Of(E.Left[int](State{state.exponent + 1, state.result * 2, state.target}))
}
}
power := TailRec(powerStep)
result := power(State{0, 1, 10})(context.Background())
assert.Equal(t, R.Of(1024), result) // 2^10
}
// TestTailRecFindInRange tests finding a value in a range
func TestTailRecFindInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.current >= state.max {
return R.Of(E.Right[State](-1)) // Not found
}
if state.current == state.target {
return R.Of(E.Right[State](state.current)) // Found
}
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
}
}
find := TailRec(findStep)
result := find(State{0, 100, 42})(context.Background())
assert.Equal(t, R.Of(42), result)
}
// TestTailRecFindNotInRange tests finding a value not in range
func TestTailRecFindNotInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.current >= state.max {
return R.Of(E.Right[State](-1)) // Not found
}
if state.current == state.target {
return R.Of(E.Right[State](state.current)) // Found
}
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
}
}
find := TailRec(findStep)
result := find(State{0, 100, 200})(context.Background())
assert.Equal(t, R.Of(-1), result)
}
// TestTailRecWithContextValue tests that context values are accessible
func TestTailRecWithContextValue(t *testing.T) {
type contextKey string
const multiplierKey contextKey = "multiplier"
ctx := context.WithValue(context.Background(), multiplierKey, 3)
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
multiplier := ctx.Value(multiplierKey).(int)
return R.Of(E.Right[int](n * multiplier))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(5)(ctx)
assert.Equal(t, R.Of(0), result) // 0 * 3 = 0
}
// TestTailRecComplexState tests with complex state structure
func TestTailRecComplexState(t *testing.T) {
type ComplexState struct {
counter int
sum int
product int
completed bool
}
complexStep := func(state ComplexState) ReaderResult[E.Either[ComplexState, string]] {
return func(ctx context.Context) Result[E.Either[ComplexState, string]] {
if state.counter <= 0 || state.completed {
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
return R.Of(E.Right[ComplexState](result))
}
newState := ComplexState{
counter: state.counter - 1,
sum: state.sum + state.counter,
product: state.product * state.counter,
completed: state.counter == 1,
}
return R.Of(E.Left[string](newState))
}
}
computation := TailRec(complexStep)
result := computation(ComplexState{5, 0, 1, false})(context.Background())
assert.Equal(t, R.Of("sum=15, product=120"), result)
}

View File

@@ -20,6 +20,7 @@ import (
"context"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readereither"
@@ -27,12 +28,14 @@ import (
)
type (
Option[A any] = option.Option[A]
Either[A any] = either.Either[error, A]
Result[A any] = result.Result[A]
Option[A any] = option.Option[A]
Either[A any] = either.Either[error, A]
Result[A any] = result.Result[A]
Reader[R, A any] = reader.Reader[R, A]
// ReaderResult is a specialization of the Reader monad for the typical golang scenario
ReaderResult[A any] = readereither.ReaderEither[context.Context, error, A]
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
Operator[A, B any] = Kleisli[ReaderResult[A], B]
Kleisli[A, B any] = reader.Reader[A, ReaderResult[B]]
Operator[A, B any] = Kleisli[ReaderResult[A], B]
Endomorphism[A any] = endomorphism.Endomorphism[A]
)

File diff suppressed because it is too large Load Diff

View File

@@ -103,11 +103,11 @@ func (t *token[T]) Unerase(val any) Result[T] {
func (t *token[T]) ProviderFactory() Option[DIE.ProviderFactory] {
return t.base.providerFactory
}
func makeTokenBase(name string, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase {
func makeTokenBase(name, id string, typ int, providerFactory Option[DIE.ProviderFactory]) *tokenBase {
return &tokenBase{name, id, typ, providerFactory}
}
func makeToken[T any](name string, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] {
func makeToken[T any](name, id string, typ int, unerase func(val any) Result[T], providerFactory Option[DIE.ProviderFactory]) Dependency[T] {
return &token[T]{makeTokenBase(name, id, typ, providerFactory), unerase}
}

View File

@@ -75,7 +75,7 @@ func TraverseArray[E, A, B any](f Kleisli[E, A, B]) Kleisli[E, []A, []B] {
// Example:
//
// validate := func(i int, s string) either.Either[error, string] {
// if len(s) > 0 {
// if S.IsNonEmpty(s) {
// return either.Right[error](fmt.Sprintf("%d:%s", i, s))
// }
// return either.Left[string](fmt.Errorf("empty at index %d", i))
@@ -105,7 +105,7 @@ func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, E, A, B any](f func(int, A) Eithe
// Example:
//
// validate := func(i int, s string) either.Either[error, string] {
// if len(s) > 0 {
// if S.IsNonEmpty(s) {
// return either.Right[error](fmt.Sprintf("%d:%s", i, s))
// }
// return either.Left[string](fmt.Errorf("empty at index %d", i))

View File

@@ -34,7 +34,7 @@ func Curry0[R any](f func() (R, error)) func() Either[error, R] {
//
// Example:
//
// parse := func(s string) (int, error) { return strconv.Atoi(s) }
// parse := strconv.Atoi
// curried := either.Curry1(parse)
// result := curried("42") // Right(42)
func Curry1[T1, R any](f func(T1) (R, error)) func(T1) Either[error, R] {

View File

@@ -19,6 +19,21 @@
// - Left represents an error or failure case (type E)
// - Right represents a success case (type A)
//
// # Fantasy Land Specification
//
// This implementation corresponds to the Fantasy Land Either type:
// https://github.com/fantasyland/fantasy-land#either
//
// Implemented Fantasy Land algebras:
// - Functor: https://github.com/fantasyland/fantasy-land#functor
// - Bifunctor: https://github.com/fantasyland/fantasy-land#bifunctor
// - Apply: https://github.com/fantasyland/fantasy-land#apply
// - Applicative: https://github.com/fantasyland/fantasy-land#applicative
// - Chain: https://github.com/fantasyland/fantasy-land#chain
// - Monad: https://github.com/fantasyland/fantasy-land#monad
// - Alt: https://github.com/fantasyland/fantasy-land#alt
// - Foldable: https://github.com/fantasyland/fantasy-land#foldable
//
// # Core Concepts
//
// The Either type is a discriminated union that can hold either a Left value (typically an error)

View File

@@ -24,6 +24,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
@@ -305,7 +306,7 @@ func TestTraverseArray(t *testing.T) {
// Test TraverseArrayWithIndex
func TestTraverseArrayWithIndex(t *testing.T) {
validate := func(i int, s string) Either[error, string] {
if len(s) > 0 {
if S.IsNonEmpty(s) {
return Right[error](fmt.Sprintf("%d:%s", i, s))
}
return Left[string](fmt.Errorf("empty at index %d", i))
@@ -334,7 +335,7 @@ func TestTraverseRecord(t *testing.T) {
// Test TraverseRecordWithIndex
func TestTraverseRecordWithIndex(t *testing.T) {
validate := func(k string, v string) Either[error, string] {
if len(v) > 0 {
if S.IsNonEmpty(v) {
return Right[error](k + ":" + v)
}
return Left[string](fmt.Errorf("empty value for key %s", k))
@@ -373,7 +374,7 @@ func TestCurry0(t *testing.T) {
}
func TestCurry1(t *testing.T) {
parse := func(s string) (int, error) { return strconv.Atoi(s) }
parse := strconv.Atoi
curried := Curry1(parse)
result := curried("42")
assert.Equal(t, Right[error](42), result)

View File

@@ -22,7 +22,6 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
IO "github.com/IBM/fp-go/v2/io"
O "github.com/IBM/fp-go/v2/option"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
@@ -120,10 +119,3 @@ func TestStringer(t *testing.T) {
var s fmt.Stringer = &e
assert.Equal(t, exp, s.String())
}
func TestFromIO(t *testing.T) {
f := IO.Of("abc")
e := FromIO[error](f)
assert.Equal(t, Right[error]("abc"), e)
}

View File

@@ -17,11 +17,19 @@ package either
import (
"log"
"log/slog"
F "github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/logging"
)
var (
// slogError creates a slog.Attr with key "error" for logging error values
slogError = F.Bind1st(slog.Any, "error")
// slogValue creates a slog.Attr with key "value" for logging success values
slogValue = F.Bind1st(slog.Any, "value")
)
func _log[E, A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[E, A, A] {
return Fold(
func(e E) Either[E, A] {
@@ -62,3 +70,91 @@ func Logger[E, A any](loggers ...*log.Logger) func(string) Operator[E, A, A] {
}
}
}
// ToSLogAttr converts an Either value to a structured logging attribute (slog.Attr).
//
// This function creates a converter that transforms Either values into slog.Attr for use
// with Go's structured logging (log/slog). It maps:
// - Left values to an "error" attribute
// - Right values to a "value" attribute
//
// This is particularly useful when integrating Either-based error handling with structured
// logging systems, allowing you to log both successful values and errors in a consistent,
// structured format.
//
// Type Parameters:
// - E: The Left (error) type of the Either
// - A: The Right (success) type of the Either
//
// Returns:
// - A function that converts Either[E, A] to slog.Attr
//
// Example with Left (error):
//
// converter := either.ToSLogAttr[error, int]()
// leftValue := either.Left[int](errors.New("connection failed"))
// attr := converter(leftValue)
// // attr is: slog.Any("error", errors.New("connection failed"))
//
// logger.LogAttrs(ctx, slog.LevelError, "Operation failed", attr)
// // Logs: {"level":"error","msg":"Operation failed","error":"connection failed"}
//
// Example with Right (success):
//
// converter := either.ToSLogAttr[error, User]()
// rightValue := either.Right[error](User{ID: 123, Name: "Alice"})
// attr := converter(rightValue)
// // attr is: slog.Any("value", User{ID: 123, Name: "Alice"})
//
// logger.LogAttrs(ctx, slog.LevelInfo, "User fetched", attr)
// // Logs: {"level":"info","msg":"User fetched","value":{"ID":123,"Name":"Alice"}}
//
// Example in a pipeline with structured logging:
//
// toAttr := either.ToSLogAttr[error, Data]()
//
// result := F.Pipe2(
// fetchData(id),
// either.Map(processData),
// either.Map(validateData),
// )
//
// attr := toAttr(result)
// logger.LogAttrs(ctx, slog.LevelInfo, "Data processing complete", attr)
// // Logs success: {"level":"info","msg":"Data processing complete","value":{...}}
// // Or error: {"level":"info","msg":"Data processing complete","error":"validation failed"}
//
// Example with custom log levels based on Either:
//
// toAttr := either.ToSLogAttr[error, Response]()
// result := callAPI(endpoint)
//
// level := either.Fold(
// func(error) slog.Level { return slog.LevelError },
// func(Response) slog.Level { return slog.LevelInfo },
// )(result)
//
// logger.LogAttrs(ctx, level, "API call completed", toAttr(result))
//
// Use Cases:
// - Structured logging: Convert Either results to structured log attributes
// - Error tracking: Log errors with consistent "error" key in structured logs
// - Success monitoring: Log successful values with consistent "value" key
// - Observability: Integrate Either-based error handling with logging systems
// - Debugging: Inspect Either values in logs with proper structure
// - Metrics: Extract Either values for metrics collection in logging pipelines
//
// Note: The returned slog.Attr uses "error" for Left values and "value" for Right values.
// These keys are consistent with common structured logging conventions.
func ToSLogAttr[E, A any]() func(Either[E, A]) slog.Attr {
return Fold(
F.Flow2(
F.ToAny[E],
slogError,
),
F.Flow2(
F.ToAny[A],
slogValue,
),
)
}

View File

@@ -16,9 +16,12 @@
package either
import (
"errors"
"log/slog"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/stretchr/testify/assert"
)
@@ -35,3 +38,139 @@ func TestLogger(t *testing.T) {
assert.Equal(t, r, res)
}
func TestToSLogAttr_Left(t *testing.T) {
// Test with Left (error) value
converter := ToSLogAttr[error, int]()
testErr := errors.New("test error")
leftValue := Left[int](testErr)
attr := converter(leftValue)
// Verify the attribute has the correct key
assert.Equal(t, "error", attr.Key)
// Verify the attribute value is the error
assert.Equal(t, testErr, attr.Value.Any())
}
func TestToSLogAttr_Right(t *testing.T) {
// Test with Right (success) value
converter := ToSLogAttr[error, string]()
rightValue := Right[error]("success value")
attr := converter(rightValue)
// Verify the attribute has the correct key
assert.Equal(t, "value", attr.Key)
// Verify the attribute value is the success value
assert.Equal(t, "success value", attr.Value.Any())
}
func TestToSLogAttr_LeftWithCustomType(t *testing.T) {
// Test with custom error type
type CustomError struct {
Code int
Message string
}
converter := ToSLogAttr[CustomError, string]()
customErr := CustomError{Code: 404, Message: "not found"}
leftValue := Left[string](customErr)
attr := converter(leftValue)
assert.Equal(t, "error", attr.Key)
assert.Equal(t, customErr, attr.Value.Any())
}
func TestToSLogAttr_RightWithCustomType(t *testing.T) {
// Test with custom success type
type User struct {
ID int
Name string
}
converter := ToSLogAttr[error, User]()
user := User{ID: 123, Name: "Alice"}
rightValue := Right[error](user)
attr := converter(rightValue)
assert.Equal(t, "value", attr.Key)
assert.Equal(t, user, attr.Value.Any())
}
func TestToSLogAttr_InPipeline(t *testing.T) {
// Test ToSLogAttr in a functional pipeline
converter := ToSLogAttr[error, int]()
// Test with successful pipeline
successResult := F.Pipe2(
Right[error](10),
Map[error](N.Mul(2)),
converter,
)
assert.Equal(t, "value", successResult.Key)
// slog.Any converts int to int64
assert.Equal(t, int64(20), successResult.Value.Any())
// Test with failed pipeline
testErr := errors.New("computation failed")
failureResult := F.Pipe2(
Left[int](testErr),
Map[error](N.Mul(2)),
converter,
)
assert.Equal(t, "error", failureResult.Key)
assert.Equal(t, testErr, failureResult.Value.Any())
}
func TestToSLogAttr_WithNilError(t *testing.T) {
// Test with nil error (edge case)
converter := ToSLogAttr[error, string]()
var nilErr error = nil
leftValue := Left[string](nilErr)
attr := converter(leftValue)
assert.Equal(t, "error", attr.Key)
assert.Nil(t, attr.Value.Any())
}
func TestToSLogAttr_WithZeroValue(t *testing.T) {
// Test with zero value of success type
converter := ToSLogAttr[error, int]()
rightValue := Right[error](0)
attr := converter(rightValue)
assert.Equal(t, "value", attr.Key)
// slog.Any converts int to int64
assert.Equal(t, int64(0), attr.Value.Any())
}
func TestToSLogAttr_WithEmptyString(t *testing.T) {
// Test with empty string as success value
converter := ToSLogAttr[error, string]()
rightValue := Right[error]("")
attr := converter(rightValue)
assert.Equal(t, "value", attr.Key)
assert.Equal(t, "", attr.Value.Any())
}
func TestToSLogAttr_AttributeKind(t *testing.T) {
// Verify that the returned attribute has the correct Kind
converter := ToSLogAttr[error, string]()
leftAttr := converter(Left[string](errors.New("error")))
// Errors are stored as KindAny (which has value 0)
assert.Equal(t, slog.KindAny, leftAttr.Value.Kind())
rightAttr := converter(Right[error]("value"))
// Strings have KindString
assert.Equal(t, slog.KindString, rightAttr.Value.Kind())
}

34
v2/either/rec.go Normal file
View File

@@ -0,0 +1,34 @@
// 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 either
//go:inline
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
return func(a A) Either[E, B] {
current := f(a)
for {
rec, e := Unwrap(current)
if IsLeft(current) {
return Left[B](e)
}
b, a := Unwrap(rec)
if IsRight(rec) {
return Right[E](b)
}
current = f(a)
}
}
}

View File

@@ -41,7 +41,7 @@ import (
// increment := N.Add(1)
// result := endomorphism.MonadAp(double, increment) // Composes: double ∘ increment
// // result(5) = double(increment(5)) = double(6) = 12
func MonadAp[A any](fab Endomorphism[A], fa Endomorphism[A]) Endomorphism[A] {
func MonadAp[A any](fab, fa Endomorphism[A]) Endomorphism[A] {
return MonadCompose(fab, fa)
}
@@ -225,7 +225,7 @@ func Map[A any](f Endomorphism[A]) Operator[A] {
// // Compare with MonadCompose which executes RIGHT-TO-LEFT:
// composed := endomorphism.MonadCompose(increment, double)
// result2 := composed(5) // (5 * 2) + 1 = 11 (same result, different parameter order)
func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
func MonadChain[A any](ma, f Endomorphism[A]) Endomorphism[A] {
return function.Flow2(ma, f)
}
@@ -247,7 +247,7 @@ func MonadChain[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
// log := func(x int) int { fmt.Println(x); return x }
// chained := endomorphism.MonadChainFirst(double, log)
// result := chained(5) // Prints 10, returns 10
func MonadChainFirst[A any](ma Endomorphism[A], f Endomorphism[A]) Endomorphism[A] {
func MonadChainFirst[A any](ma, f Endomorphism[A]) Endomorphism[A] {
return func(a A) A {
result := ma(a)
f(result) // Apply f for its effect

View File

@@ -437,5 +437,3 @@ func BenchmarkFromSemigroupComposition(b *testing.B) {
_ = composed(3)
}
}
// Made with Bob

View File

@@ -72,9 +72,7 @@ func TestFromStrictEquals(t *testing.T) {
func TestFromEquals(t *testing.T) {
t.Run("case-insensitive string equality", func(t *testing.T) {
caseInsensitiveEq := FromEquals(func(a, b string) bool {
return strings.EqualFold(a, b)
})
caseInsensitiveEq := FromEquals(strings.EqualFold)
assert.True(t, caseInsensitiveEq.Equals("hello", "HELLO"))
assert.True(t, caseInsensitiveEq.Equals("Hello", "hello"))
@@ -243,9 +241,7 @@ func TestContramap(t *testing.T) {
})
t.Run("case-insensitive name comparison", func(t *testing.T) {
caseInsensitiveEq := FromEquals(func(a, b string) bool {
return strings.EqualFold(a, b)
})
caseInsensitiveEq := FromEquals(strings.EqualFold)
personEqByNameCI := Contramap(func(p Person) string {
return p.Name

View File

@@ -53,7 +53,10 @@ func Identity[A any](a A) A {
//
// getMessage := Constant("Hello")
// msg := getMessage() // "Hello"
//
//go:inline
func Constant[A any](a A) func() A {
//go:inline
return func() A {
return a
}
@@ -81,7 +84,10 @@ func Constant[A any](a A) func() A {
//
// defaultName := Constant1[int, string]("Unknown")
// name := defaultName(42) // "Unknown"
//
//go:inline
func Constant1[B, A any](a A) func(B) A {
//go:inline
return func(_ B) A {
return a
}
@@ -107,7 +113,10 @@ func Constant1[B, A any](a A) func(B) A {
//
// alwaysTrue := Constant2[int, string, bool](true)
// result := alwaysTrue(42, "test") // true
//
//go:inline
func Constant2[B, C, A any](a A) func(B, C) A {
//go:inline
return func(_ B, _ C) A {
return a
}
@@ -128,6 +137,8 @@ func Constant2[B, C, A any](a A) func(B, C) A {
//
// value := 42
// IsNil(&value) // false
//
//go:inline
func IsNil[A any](a *A) bool {
return a == nil
}
@@ -149,6 +160,8 @@ func IsNil[A any](a *A) bool {
//
// value := 42
// IsNonNil(&value) // true
//
//go:inline
func IsNonNil[A any](a *A) bool {
return a != nil
}
@@ -207,6 +220,8 @@ func Swap[T1, T2, R any](f func(T1, T2) R) func(T2, T1) R {
//
// result := First(42, "hello") // 42
// result := First(true, 100) // true
//
//go:inline
func First[T1, T2 any](t1 T1, _ T2) T1 {
return t1
}
@@ -231,6 +246,14 @@ func First[T1, T2 any](t1 T1, _ T2) T1 {
//
// result := Second(42, "hello") // "hello"
// result := Second(true, 100) // 100
//
//go:inline
func Second[T1, T2 any](_ T1, t2 T2) T2 {
return t2
}
// Zero returns the zero value of the given type.
func Zero[A comparable]() A {
var zero A
return zero
}

View File

@@ -117,9 +117,13 @@ func Nullary2[F1 ~func() T1, F2 ~func(T1) T2, T1, T2 any](f1 F1, f2 F2) func() T
// Curry2 takes a function with 2 parameters and returns a cascade of functions each taking only one parameter.
// The inverse function is [Uncurry2]
//go:inline
func Curry2[FCT ~func(T0, T1) T2, T0, T1, T2 any](f FCT) func(T0) func(T1) T2 {
//go:inline
return func(t0 T0) func(t1 T1) T2 {
//go:inline
return func(t1 T1) T2 {
//go:inline
return f(t0, t1)
}
}

View File

@@ -51,7 +51,7 @@ package function
// )
// result := classify(5) // "positive"
// result2 := classify(-3) // "non-positive"
func Ternary[A, B any](pred func(A) bool, onTrue func(A) B, onFalse func(A) B) func(A) B {
func Ternary[A, B any](pred func(A) bool, onTrue, onFalse func(A) B) func(A) B {
return func(a A) B {
if pred(a) {
return onTrue(a)

View File

@@ -246,7 +246,7 @@ func (builder *Builder) GetTargetURL() Result[string] {
parseQuery,
result.Map(F.Flow2(
F.Curry2(FM.ValuesMonoid.Concat)(builder.GetQuery()),
(url.Values).Encode,
url.Values.Encode,
)),
),
),

View File

@@ -16,6 +16,18 @@
/*
Package identity implements the Identity monad, the simplest possible monad.
# Fantasy Land Specification
This implementation corresponds to the Fantasy Land Identity type:
https://github.com/fantasyland/fantasy-land
Implemented Fantasy Land algebras:
- Functor: https://github.com/fantasyland/fantasy-land#functor
- Apply: https://github.com/fantasyland/fantasy-land#apply
- Applicative: https://github.com/fantasyland/fantasy-land#applicative
- Chain: https://github.com/fantasyland/fantasy-land#chain
- Monad: https://github.com/fantasyland/fantasy-land#monad
# Overview
The Identity monad is a trivial monad that simply wraps a value without adding

View File

@@ -0,0 +1,75 @@
// 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 readerresult
import (
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// TraverseArray applies a ReaderResult-returning function to each element of an array,
// collecting the results. If any element fails, the entire operation fails with the first error.
//
// Example:
//
// parseUser := func(id int) readerresult.ReaderResult[DB, User] { ... }
// ids := []int{1, 2, 3}
// result := readerresult.TraverseArray[DB](parseUser)(ids)
// // result(db) returns ([]User, nil) with all users or (nil, error) on first error
//
//go:inline
func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
return RR.TraverseArray(f)
}
//go:inline
func MonadTraverseArray[A, B any](as []A, f Kleisli[A, B]) ReaderResult[[]B] {
return RR.MonadTraverseArray(as, f)
}
// TraverseArrayWithIndex is like TraverseArray but the function also receives the element's index.
// This is useful when the transformation depends on the position in the array.
//
// Example:
//
// processItem := func(idx int, item string) readerresult.ReaderResult[Config, int] {
// return readerresult.Of[Config](idx + len(item))
// }
// items := []string{"a", "bb", "ccc"}
// result := readerresult.TraverseArrayWithIndex[Config](processItem)(items)
//
//go:inline
func TraverseArrayWithIndex[A, B any](f func(int, A) ReaderResult[B]) Kleisli[[]A, []B] {
return RR.TraverseArrayWithIndex(f)
}
// SequenceArray converts an array of ReaderResult values into a single ReaderResult of an array.
// If any element fails, the entire operation fails with the first error encountered.
// All computations share the same environment.
//
// Example:
//
// readers := []readerresult.ReaderResult[Config, int]{
// readerresult.Of[Config](1),
// readerresult.Of[Config](2),
// readerresult.Of[Config](3),
// }
// result := readerresult.SequenceArray(readers)
// // result(cfg) returns ([]int{1, 2, 3}, nil)
//
//go:inline
func SequenceArray[A any](ma []ReaderResult[A]) ReaderResult[[]A] {
return RR.SequenceArray(ma)
}

View File

@@ -0,0 +1,337 @@
// 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 readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/idiomatic/result"
C "github.com/IBM/fp-go/v2/internal/chain"
L "github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
)
// Do initializes a do-notation context with an empty state.
//
// This is the starting point for do-notation style composition, which allows
// imperative-style sequencing of ReaderResult computations while maintaining
// functional purity.
//
// Type Parameters:
// - S: The state type
//
// Parameters:
// - empty: The initial empty state
//
// Returns:
// - A ReaderResult[S] containing the initial state
//
// Example:
//
// type State struct {
// User User
// Posts []Post
// }
//
// result := F.Pipe2(
// readerresult.Do(State{}),
// readerresult.Bind(
// func(u User) func(State) State {
// return func(s State) State { s.User = u; return s }
// },
// func(s State) readerresult.ReaderResult[User] {
// return getUser(42)
// },
// ),
// readerresult.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readerresult.ReaderResult[[]Post] {
// return getPosts(s.User.ID)
// },
// ),
// )
//
//go:inline
func Do[S any](
empty S,
) ReaderResult[S] {
return RR.Do[context.Context](empty)
}
// Bind sequences a ReaderResult computation and updates the state with its result.
//
// This is the core operation for do-notation, allowing you to chain computations
// where each step can depend on the accumulated state and update it with new values.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of value produced by the computation
//
// Parameters:
// - setter: A function that takes the computation result and returns a state updater
// - f: A Kleisli arrow that produces the next computation based on current state
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
// Example:
//
// readerresult.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerresult.ReaderResult[User] {
// return getUser(s.UserID)
// },
// )
//
//go:inline
func Bind[S1, S2, T any](
setter func(T) func(S1) S2,
f Kleisli[S1, T],
) Operator[S1, S2] {
return C.Bind(
Chain[S1, S2],
Map[T, S2],
setter,
f,
)
}
// Let attaches the result of a pure computation to a state.
//
// Unlike Bind, Let works with pure functions (not ReaderResult computations).
// This is useful for deriving values from the current state without performing
// any effects.
//
// Type Parameters:
// - S1: The input state type
// - S2: The output state type
// - T: The type of value computed
//
// 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:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
// Example:
//
// readerresult.Let(
// func(fullName string) func(State) State {
// return func(s State) State { s.FullName = fullName; return s }
// },
// func(s State) string {
// return s.FirstName + " " + s.LastName
// },
// )
//
//go:inline
func Let[S1, S2, T any](
setter func(T) func(S1) S2,
f func(S1) T,
) Operator[S1, S2] {
return RR.Let[context.Context](setter, f)
}
// LetTo attaches a constant value to a state.
//
// This is a simplified version of Let for when you want to add a constant
// value to the state without computing it.
//
// Type Parameters:
// - 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 attach
//
// Returns:
// - An Operator that transforms ReaderResult[S1] to ReaderResult[S2]
//
// Example:
//
// readerresult.LetTo(
// func(status string) func(State) State {
// return func(s State) State { s.Status = status; return s }
// },
// "active",
// )
//
//go:inline
func LetTo[S1, S2, T any](
setter func(T) func(S1) S2,
b T,
) Operator[S1, S2] {
return RR.LetTo[context.Context](setter, b)
}
// BindTo initializes do-notation by binding a value to a state.
//
// This is typically used as the first operation after a computation to
// start building up a state structure.
//
// Type Parameters:
// - S1: The state type to create
// - T: The type of the initial value
//
// Parameters:
// - setter: A function that creates the initial state from a value
//
// Returns:
// - An Operator that transforms ReaderResult[T] to ReaderResult[S1]
//
// Example:
//
// type State struct {
// User User
// }
//
// result := F.Pipe1(
// getUser(42),
// readerresult.BindTo(func(u User) State {
// return State{User: u}
// }),
// )
//
//go:inline
func BindTo[S1, T any](
setter func(T) S1,
) Operator[T, S1] {
return RR.BindTo[context.Context](setter)
}
//go:inline
func ApS[S1, S2, T any](
setter func(T) func(S1) S2,
fa ReaderResult[T],
) Operator[S1, S2] {
return RR.ApS[context.Context](setter, fa)
}
//go:inline
func ApSL[S, T any](
lens L.Lens[S, T],
fa ReaderResult[T],
) Operator[S, S] {
return ApSL(lens, fa)
}
//go:inline
func BindL[S, T any](
lens L.Lens[S, T],
f Kleisli[T, T],
) Operator[S, S] {
return RR.BindL(lens, f)
}
//go:inline
func LetL[S, T any](
lens L.Lens[S, T],
f Endomorphism[T],
) Operator[S, S] {
return RR.LetL[context.Context](lens, f)
}
//go:inline
func LetToL[S, T any](
lens L.Lens[S, T],
b T,
) Operator[S, S] {
return RR.LetToL[context.Context](lens, b)
}
//go:inline
func BindReaderK[S1, S2, T any](
setter func(T) func(S1) S2,
f reader.Kleisli[context.Context, S1, T],
) Operator[S1, S2] {
return RR.BindReaderK(setter, f)
}
//go:inline
func BindEitherK[S1, S2, T any](
setter func(T) func(S1) S2,
f RES.Kleisli[S1, T],
) Operator[S1, S2] {
return RR.BindEitherK[context.Context](setter, f)
}
//go:inline
func BindResultK[S1, S2, T any](
setter func(T) func(S1) S2,
f result.Kleisli[S1, T],
) Operator[S1, S2] {
return RR.BindResultK[context.Context](setter, f)
}
//go:inline
func BindToReader[
S1, T any](
setter func(T) S1,
) func(Reader[context.Context, T]) ReaderResult[S1] {
return RR.BindToReader[context.Context](setter)
}
//go:inline
func BindToEither[
S1, T any](
setter func(T) S1,
) func(Result[T]) ReaderResult[S1] {
return RR.BindToEither[context.Context](setter)
}
//go:inline
func BindToResult[
S1, T any](
setter func(T) S1,
) func(T, error) ReaderResult[S1] {
return RR.BindToResult[context.Context](setter)
}
//go:inline
func ApReaderS[
S1, S2, T any](
setter func(T) func(S1) S2,
fa Reader[context.Context, T],
) Operator[S1, S2] {
return RR.ApReaderS(setter, fa)
}
//go:inline
func ApResultS[
S1, S2, T any](
setter func(T) func(S1) S2,
) func(T, error) Operator[S1, S2] {
return RR.ApResultS[context.Context](setter)
}
//go:inline
func ApEitherS[
S1, S2, T any](
setter func(T) func(S1) S2,
fa Result[T],
) Operator[S1, S2] {
return RR.ApEitherS[context.Context](setter, fa)
}

View File

@@ -0,0 +1,403 @@
// 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 readerresult
import (
"context"
"io"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// Bracket ensures safe resource management with guaranteed cleanup in the ReaderResult monad.
//
// This function implements the bracket pattern (also known as try-with-resources or RAII)
// for ReaderResult computations. It guarantees that the release action is called regardless
// of whether the use action succeeds or fails, making it ideal for managing resources like
// file handles, database connections, network sockets, or locks.
//
// The execution flow is:
// 1. Acquire the resource (lazily evaluated)
// 2. Use the resource with the provided function
// 3. Release the resource with access to: the resource, the result (if successful), and any error
//
// The release function is always called, even if:
// - The acquire action fails (release is not called in this case)
// - The use action fails (release receives the error)
// - The use action succeeds (release receives nil error)
//
// Type Parameters:
// - A: The type of the acquired resource
// - B: The type of the result produced by using the resource
// - ANY: The type returned by the release action (typically ignored)
//
// Parameters:
// - acquire: Lazy computation that acquires the resource
// - use: Function that uses the resource to produce a result
// - release: Function that releases the resource, receiving the resource, result, and any error
//
// Returns:
// - A ReaderResult[B] that safely manages the resource lifecycle
//
// Example - File handling:
//
// import (
// "context"
// "os"
// )
//
// readFile := readerresult.Bracket(
// // Acquire: Open file
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open("data.txt")
// }
// },
// // Use: Read file contents
// func(file *os.File) readerresult.ReaderResult[string] {
// return func(ctx context.Context) (string, error) {
// data, err := io.ReadAll(file)
// return string(data), err
// }
// },
// // Release: Close file (always called)
// func(file *os.File, content string, err error) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, file.Close()
// }
// },
// )
//
// content, err := readFile(context.Background())
//
// Example - Database connection:
//
// queryDB := readerresult.Bracket(
// // Acquire: Open connection
// func() readerresult.ReaderResult[*sql.DB] {
// return func(ctx context.Context) (*sql.DB, error) {
// return sql.Open("postgres", connString)
// }
// },
// // Use: Execute query
// func(db *sql.DB) readerresult.ReaderResult[[]User] {
// return func(ctx context.Context) ([]User, error) {
// return queryUsers(ctx, db)
// }
// },
// // Release: Close connection (always called)
// func(db *sql.DB, users []User, err error) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, db.Close()
// }
// },
// )
//
// Example - Lock management:
//
// withLock := readerresult.Bracket(
// // Acquire: Lock mutex
// func() readerresult.ReaderResult[*sync.Mutex] {
// return func(ctx context.Context) (*sync.Mutex, error) {
// mu.Lock()
// return mu, nil
// }
// },
// // Use: Perform critical section work
// func(mu *sync.Mutex) readerresult.ReaderResult[int] {
// return func(ctx context.Context) (int, error) {
// return performCriticalWork(ctx)
// }
// },
// // Release: Unlock mutex (always called)
// func(mu *sync.Mutex, result int, err error) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// mu.Unlock()
// return nil, nil
// }
// },
// )
func Bracket[
A, B, ANY any](
acquire Lazy[ReaderResult[A]],
use Kleisli[A, B],
release func(A, B, error) ReaderResult[ANY],
) ReaderResult[B] {
return RR.Bracket(acquire, use, release)
}
// WithResource creates a higher-order function for resource management with automatic cleanup.
//
// This function provides a more composable alternative to Bracket by creating a function
// that takes a resource-using function and automatically handles resource acquisition and
// release. This is particularly useful when you want to reuse the same resource management
// pattern with different operations.
//
// The pattern is:
// 1. Create a resource manager with onCreate and onRelease
// 2. Apply it to different use functions as needed
// 3. Each application ensures proper resource cleanup
//
// This is useful for:
// - Creating reusable resource management patterns
// - Building resource pools or factories
// - Composing resource-dependent operations
// - Abstracting resource lifecycle management
//
// Type Parameters:
// - B: The type of the result produced by using the resource
// - A: The type of the acquired resource
// - ANY: The type returned by the release action (typically ignored)
//
// Parameters:
// - onCreate: Lazy computation that creates/acquires the resource
// - onRelease: Function that releases the resource (receives the resource)
//
// Returns:
// - A Kleisli arrow that takes a resource-using function and returns a ReaderResult[B]
// with automatic resource management
//
// Example - Reusable database connection manager:
//
// import (
// "context"
// "database/sql"
// )
//
// // Create a reusable DB connection manager
// withDB := readerresult.WithResource(
// // onCreate: Acquire connection
// func() readerresult.ReaderResult[*sql.DB] {
// return func(ctx context.Context) (*sql.DB, error) {
// return sql.Open("postgres", connString)
// }
// },
// // onRelease: Close connection
// func(db *sql.DB) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, db.Close()
// }
// },
// )
//
// // Use the manager with different operations
// getUsers := withDB(func(db *sql.DB) readerresult.ReaderResult[[]User] {
// return func(ctx context.Context) ([]User, error) {
// return queryUsers(ctx, db)
// }
// })
//
// getOrders := withDB(func(db *sql.DB) readerresult.ReaderResult[[]Order] {
// return func(ctx context.Context) ([]Order, error) {
// return queryOrders(ctx, db)
// }
// })
//
// // Both operations automatically manage the connection
// users, err := getUsers(context.Background())
// orders, err := getOrders(context.Background())
//
// Example - File operations manager:
//
// withFile := readerresult.WithResource(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open("config.json")
// }
// },
// func(file *os.File) readerresult.ReaderResult[any] {
// return func(ctx context.Context) (any, error) {
// return nil, file.Close()
// }
// },
// )
//
// // Different operations on the same file
// readConfig := withFile(func(file *os.File) readerresult.ReaderResult[Config] {
// return func(ctx context.Context) (Config, error) {
// return parseConfig(file)
// }
// })
//
// validateConfig := withFile(func(file *os.File) readerresult.ReaderResult[bool] {
// return func(ctx context.Context) (bool, error) {
// return validateConfigFile(file)
// }
// })
//
// Example - Composing with other operations:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Create a pipeline with automatic resource management
// processData := F.Pipe2(
// loadData,
// withDB(func(db *sql.DB) readerresult.ReaderResult[Result] {
// return saveToDatabase(db)
// }),
// readerresult.Map(formatResult),
// )
func WithResource[B, A, ANY any](
onCreate Lazy[ReaderResult[A]],
onRelease Kleisli[A, ANY],
) Kleisli[Kleisli[A, B], B] {
return RR.WithResource[B](onCreate, onRelease)
}
// onClose is a helper function that creates a ReaderResult that closes an io.Closer.
// This is used internally by WithCloser to provide automatic cleanup for resources
// that implement the io.Closer interface.
func onClose[A io.Closer](a A) ReaderResult[any] {
return func(_ context.Context) (any, error) {
return nil, a.Close()
}
}
// WithCloser creates a higher-order function for managing resources that implement io.Closer.
//
// This is a specialized version of WithResource that automatically handles cleanup for any
// resource implementing the io.Closer interface (such as files, network connections, HTTP
// response bodies, etc.). It eliminates the need to manually specify the release function,
// making it more convenient for common Go resources.
//
// The function automatically calls Close() on the resource when the operation completes,
// regardless of success or failure. This ensures proper resource cleanup following Go's
// standard io.Closer pattern.
//
// Type Parameters:
// - B: The type of the result produced by using the resource
// - A: The type of the resource, which must implement io.Closer
//
// Parameters:
// - onCreate: Lazy computation that creates/acquires the io.Closer resource
//
// Returns:
// - A Kleisli arrow that takes a resource-using function and returns a ReaderResult[B]
// with automatic Close() cleanup
//
// Example - File operations:
//
// import (
// "context"
// "os"
// "io"
// )
//
// // Create a reusable file manager
// withFile := readerresult.WithCloser(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open("data.txt")
// }
// },
// )
//
// // Use with different operations - Close() is automatic
// readContent := withFile(func(file *os.File) readerresult.ReaderResult[string] {
// return func(ctx context.Context) (string, error) {
// data, err := io.ReadAll(file)
// return string(data), err
// }
// })
//
// getSize := withFile(func(file *os.File) readerresult.ReaderResult[int64] {
// return func(ctx context.Context) (int64, error) {
// info, err := file.Stat()
// if err != nil {
// return 0, err
// }
// return info.Size(), nil
// }
// })
//
// content, err := readContent(context.Background())
// size, err := getSize(context.Background())
//
// Example - HTTP response body:
//
// import "net/http"
//
// withResponse := readerresult.WithCloser(
// func() readerresult.ReaderResult[*http.Response] {
// return func(ctx context.Context) (*http.Response, error) {
// return http.Get("https://api.example.com/data")
// }
// },
// )
//
// // Body is automatically closed after use
// parseJSON := withResponse(func(resp *http.Response) readerresult.ReaderResult[Data] {
// return func(ctx context.Context) (Data, error) {
// var data Data
// err := json.NewDecoder(resp.Body).Decode(&data)
// return data, err
// }
// })
//
// Example - Multiple file operations:
//
// // Read from one file, write to another
// copyFile := func(src, dst string) readerresult.ReaderResult[int64] {
// withSrc := readerresult.WithCloser(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Open(src)
// }
// },
// )
//
// withDst := readerresult.WithCloser(
// func() readerresult.ReaderResult[*os.File] {
// return func(ctx context.Context) (*os.File, error) {
// return os.Create(dst)
// }
// },
// )
//
// return withSrc(func(srcFile *os.File) readerresult.ReaderResult[int64] {
// return withDst(func(dstFile *os.File) readerresult.ReaderResult[int64] {
// return func(ctx context.Context) (int64, error) {
// return io.Copy(dstFile, srcFile)
// }
// })
// })
// }
//
// Example - Network connection:
//
// import "net"
//
// withConn := readerresult.WithCloser(
// func() readerresult.ReaderResult[net.Conn] {
// return func(ctx context.Context) (net.Conn, error) {
// return net.Dial("tcp", "localhost:8080")
// }
// },
// )
//
// sendData := withConn(func(conn net.Conn) readerresult.ReaderResult[int] {
// return func(ctx context.Context) (int, error) {
// return conn.Write([]byte("Hello, World!"))
// }
// })
//
// Note: WithCloser is a convenience wrapper around WithResource that automatically
// provides the Close() cleanup function. For resources that don't implement io.Closer
// or require custom cleanup logic, use WithResource or Bracket instead.
func WithCloser[B any, A io.Closer](onCreate Lazy[ReaderResult[A]]) Kleisli[Kleisli[A, B], B] {
return WithResource[B](onCreate, onClose[A])
}

View File

@@ -0,0 +1,210 @@
// 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 readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// Curry0 converts a function that takes context.Context and returns (A, error) into a ReaderResult[A].
//
// This is useful for lifting existing functions that follow Go's context-first convention
// into the ReaderResult monad.
//
// Type Parameters:
// - A: The return value type
//
// Parameters:
// - f: A function that takes context.Context and returns (A, error)
//
// Returns:
// - A ReaderResult[A] that wraps the function
//
// Example:
//
// func getConfig(ctx context.Context) (Config, error) {
// // ... implementation
// return config, nil
// }
// rr := readerresult.Curry0(getConfig)
// config, err := rr(ctx)
//
//go:inline
func Curry0[A any](f func(context.Context) (A, error)) ReaderResult[A] {
return RR.Curry0(f)
}
// Curry1 converts a function with one parameter into a curried ReaderResult-returning function.
//
// The context.Context parameter is handled by the ReaderResult, allowing you to partially
// apply the business parameter before providing the context.
//
// Type Parameters:
// - T1: The first parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1) and returns (A, error)
//
// Returns:
// - A curried function that takes T1 and returns ReaderResult[A]
//
// Example:
//
// func getUser(ctx context.Context, id int) (User, error) {
// // ... implementation
// return user, nil
// }
// getUserRR := readerresult.Curry1(getUser)
// rr := getUserRR(42) // Partially applied
// user, err := rr(ctx) // Execute with context
//
//go:inline
func Curry1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderResult[A] {
return RR.Curry1(f)
}
// Curry2 converts a function with two parameters into a curried ReaderResult-returning function.
//
// The context.Context parameter is handled by the ReaderResult, allowing you to partially
// apply the business parameters before providing the context.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2) and returns (A, error)
//
// Returns:
// - A curried function that takes T1, then T2, and returns ReaderResult[A]
//
// Example:
//
// func updateUser(ctx context.Context, id int, name string) (User, error) {
// // ... implementation
// return user, nil
// }
// updateUserRR := readerresult.Curry2(updateUser)
// rr := updateUserRR(42)("Alice") // Partially applied
// user, err := rr(ctx) // Execute with context
//
//go:inline
func Curry2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1) func(T2) ReaderResult[A] {
return RR.Curry2(f)
}
// Curry3 converts a function with three parameters into a curried ReaderResult-returning function.
//
// The context.Context parameter is handled by the ReaderResult, allowing you to partially
// apply the business parameters before providing the context.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - T3: The third parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2, T3) and returns (A, error)
//
// Returns:
// - A curried function that takes T1, then T2, then T3, and returns ReaderResult[A]
//
// Example:
//
// func createPost(ctx context.Context, userID int, title string, body string) (Post, error) {
// // ... implementation
// return post, nil
// }
// createPostRR := readerresult.Curry3(createPost)
// rr := createPostRR(42)("Title")("Body") // Partially applied
// post, err := rr(ctx) // Execute with context
//
//go:inline
func Curry3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1) func(T2) func(T3) ReaderResult[A] {
return RR.Curry3(f)
}
// Uncurry1 converts a curried ReaderResult function back to a standard Go function.
//
// This is the inverse of Curry1, useful when you need to call curried functions
// in a traditional Go style.
//
// Type Parameters:
// - T1: The parameter type
// - A: The return value type
//
// Parameters:
// - f: A curried function that takes T1 and returns ReaderResult[A]
//
// Returns:
// - A function that takes (context.Context, T1) and returns (A, error)
//
// Example:
//
// curriedFn := func(id int) readerresult.ReaderResult[User] { ... }
// normalFn := readerresult.Uncurry1(curriedFn)
// user, err := normalFn(ctx, 42)
//
//go:inline
func Uncurry1[T1, A any](f func(T1) ReaderResult[A]) func(context.Context, T1) (A, error) {
return RR.Uncurry1(f)
}
// Uncurry2 converts a curried ReaderResult function with two parameters back to a standard Go function.
//
// This is the inverse of Curry2.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - A: The return value type
//
// Parameters:
// - f: A curried function that takes T1, then T2, and returns ReaderResult[A]
//
// Returns:
// - A function that takes (context.Context, T1, T2) and returns (A, error)
//
//go:inline
func Uncurry2[T1, T2, A any](f func(T1) func(T2) ReaderResult[A]) func(context.Context, T1, T2) (A, error) {
return RR.Uncurry2(f)
}
// Uncurry3 converts a curried ReaderResult function with three parameters back to a standard Go function.
//
// This is the inverse of Curry3.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - T3: The third parameter type
// - A: The return value type
//
// Parameters:
// - f: A curried function that takes T1, then T2, then T3, and returns ReaderResult[A]
//
// Returns:
// - A function that takes (context.Context, T1, T2, T3) and returns (A, error)
//
//go:inline
func Uncurry3[T1, T2, T3, A any](f func(T1) func(T2) func(T3) ReaderResult[A]) func(context.Context, T1, T2, T3) (A, error) {
return RR.Uncurry3(f)
}

View File

@@ -0,0 +1,178 @@
// 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 readerresult provides a ReaderResult monad that combines the Reader and Result monads.
//
// A ReaderResult[R, A] represents a computation that:
// - Depends on an environment of type R (Reader aspect)
// - May fail with an error (Result aspect, which is Either[error, A])
//
// This is equivalent to Reader[R, Result[A]] or Reader[R, Either[error, A]].
//
// # Use Cases
//
// ReaderResult is particularly useful for:
//
// 1. Dependency injection with error handling - pass configuration/services through
// computations that may fail
// 2. Functional error handling - compose operations that depend on context and may error
// 3. Testing - easily mock dependencies by changing the environment value
//
// # Basic Example
//
// type Config struct {
// DatabaseURL string
// }
//
// // Function that needs config and may fail
// func getUser(id int) readerresult.ReaderResult[Config, User] {
// return readerresult.Asks(func(cfg Config) result.Result[User] {
// // Use cfg.DatabaseURL to fetch user
// return result.Of(user)
// })
// }
//
// // Execute by providing the config
// cfg := Config{DatabaseURL: "postgres://..."}
// user, err := getUser(42)(cfg) // Returns (User, error)
//
// # Composition
//
// ReaderResult provides several ways to compose computations:
//
// 1. Map - transform successful values
// 2. Chain (FlatMap) - sequence dependent operations
// 3. Ap - combine independent computations
// 4. Do-notation - imperative-style composition with Bind
//
// # Do-Notation Example
//
// type State struct {
// User User
// Posts []Post
// }
//
// result := F.Pipe2(
// readerresult.Do[Config](State{}),
// readerresult.Bind(
// func(user User) func(State) State {
// return func(s State) State { s.User = user; return s }
// },
// func(s State) readerresult.ReaderResult[Config, User] {
// return getUser(42)
// },
// ),
// readerresult.Bind(
// func(posts []Post) func(State) State {
// return func(s State) State { s.Posts = posts; return s }
// },
// func(s State) readerresult.ReaderResult[Config, []Post] {
// return getPosts(s.User.ID)
// },
// ),
// )
//
// # Object-Oriented Patterns with Curry Functions
//
// The Curry functions enable an interesting pattern where you can treat the Reader context (R)
// as an object instance, effectively creating method-like functions that compose functionally.
//
// When you curry a function like func(R, T1, T2) (A, error), the context R becomes the last
// argument to be applied, even though it appears first in the original function signature.
// This is intentional and follows Go's context-first convention while enabling functional
// composition patterns.
//
// Why R is the last curried argument:
//
// - In Go, context conventionally comes first: func(ctx Context, params...) (Result, error)
// - In curried form: Curry2(f)(param1)(param2) returns ReaderResult[R, A]
// - The ReaderResult is then applied to R: Curry2(f)(param1)(param2)(ctx)
// - This allows partial application of business parameters before providing the context/object
//
// Object-Oriented Example:
//
// // A service struct that acts as the Reader context
// type UserService struct {
// db *sql.DB
// cache Cache
// }
//
// // A method-like function following Go conventions (context first)
// func (s *UserService) GetUserByID(ctx context.Context, id int) (User, error) {
// // Use s.db and s.cache...
// }
//
// func (s *UserService) UpdateUser(ctx context.Context, id int, name string) (User, error) {
// // Use s.db and s.cache...
// }
//
// // Curry these into composable operations
// getUser := readerresult.Curry1((*UserService).GetUserByID)
// updateUser := readerresult.Curry2((*UserService).UpdateUser)
//
// // Now compose operations that will be bound to a UserService instance
// type Context struct {
// Svc *UserService
// }
//
// pipeline := F.Pipe2(
// getUser(42), // ReaderResult[Context, User]
// readerresult.Chain(func(user User) readerresult.ReaderResult[Context, User] {
// newName := user.Name + " (updated)"
// return updateUser(user.ID)(newName)
// }),
// )
//
// // Execute by providing the service instance as context
// svc := &UserService{db: db, cache: cache}
// ctx := Context{Svc: svc}
// updatedUser, err := pipeline(ctx)
//
// The key insight is that currying creates a chain where:
// 1. Business parameters are applied first: getUser(42)
// 2. This returns a ReaderResult that waits for the context
// 3. Multiple operations can be composed before providing the context
// 4. Finally, the context/object is provided to execute everything: pipeline(ctx)
//
// This pattern is particularly useful for:
// - Creating reusable operation pipelines independent of service instances
// - Testing with mock service instances
// - Dependency injection in a functional style
// - Composing operations that share the same service context
//
// # Error Handling
//
// ReaderResult provides several functions for error handling:
//
// - Left/Right - create failed/successful values
// - GetOrElse - provide a default value for errors
// - OrElse - recover from errors with an alternative computation
// - Fold - handle both success and failure cases
// - ChainEitherK - lift result.Result computations into ReaderResult
//
// # Relationship to Other Monads
//
// ReaderResult is related to several other monads in this library:
//
// - Reader[R, A] - ReaderResult without error handling
// - Result[A] (Either[error, A]) - error handling without environment
// - ReaderEither[R, E, A] - like ReaderResult but with custom error type E
// - IOResult[A] - like ReaderResult but with no environment (IO with errors)
//
// # Performance Note
//
// ReaderResult is a zero-cost abstraction - it compiles to a simple function type
// with no runtime overhead beyond the underlying computation.
package readerresult

View File

@@ -0,0 +1,107 @@
// 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 readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/reader"
)
// SequenceReader swaps the order of nested environment parameters when the inner type is a Reader.
//
// It transforms ReaderResult[Reader[R, A]] into a function that takes context.Context first,
// then R, and returns (A, error). This is useful when you have a ReaderResult computation
// that produces a Reader, and you want to sequence the environment dependencies.
//
// Type Parameters:
// - R: The inner Reader's environment type
// - A: The final result type
//
// Parameters:
// - ma: A ReaderResult that produces a Reader[R, A]
//
// Returns:
// - A Kleisli arrow that takes context.Context and R to produce (A, error)
//
// Example:
//
// type Config struct {
// DatabaseURL string
// }
//
// // Returns a ReaderResult that produces a Reader
// getDBReader := func(ctx context.Context) (reader.Reader[Config, string], error) {
// return func(cfg Config) string {
// return cfg.DatabaseURL
// }, nil
// }
//
// // Sequence the environments: context.Context -> Config -> string
// sequenced := readerresult.SequenceReader[Config, string](getDBReader)
// result, err := sequenced(ctx)(config)
//
//go:inline
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) RR.Kleisli[context.Context, R, A] {
return RR.SequenceReader(ma)
}
// TraverseReader combines SequenceReader with a Kleisli arrow transformation.
//
// It takes a Reader Kleisli arrow (a function from A to Reader[R, B]) and returns
// a function that transforms ReaderResult[A] into a Kleisli arrow from context.Context
// and R to B. This is useful for transforming values within a ReaderResult while
// introducing an additional Reader dependency.
//
// Type Parameters:
// - R: The Reader's environment type
// - A: The input type
// - B: The output type
//
// Parameters:
// - f: A Kleisli arrow that transforms A into Reader[R, B]
//
// Returns:
// - A function that transforms ReaderResult[A] into a Kleisli arrow from context.Context and R to B
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // A Kleisli arrow that uses Config to transform int to string
// formatWithConfig := func(n int) reader.Reader[Config, string] {
// return func(cfg Config) string {
// return fmt.Sprintf("Value: %d", n * cfg.Multiplier)
// }
// }
//
// // Create a ReaderResult[int]
// getValue := readerresult.Of[int](42)
//
// // Traverse: transform the int using the Reader Kleisli arrow
// traversed := readerresult.TraverseReader[Config](formatWithConfig)(getValue)
// result, err := traversed(ctx)(Config{Multiplier: 2})
// // result == "Value: 84"
//
//go:inline
func TraverseReader[R, A, B any](
f reader.Kleisli[R, A, B],
) func(ReaderResult[A]) RR.Kleisli[context.Context, R, B] {
return RR.TraverseReader[context.Context](f)
}

View File

@@ -0,0 +1,134 @@
// 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 readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
)
// From0 converts a context-taking function into a thunk that returns a ReaderResult.
//
// Unlike Curry0 which returns a ReaderResult directly, From0 returns a function
// that when called produces a ReaderResult. This is useful for lazy evaluation.
//
// Type Parameters:
// - A: The return value type
//
// Parameters:
// - f: A function that takes context.Context and returns (A, error)
//
// Returns:
// - A thunk (function with no parameters) that returns ReaderResult[A]
//
// Example:
//
// func getConfig(ctx context.Context) (Config, error) {
// return Config{Port: 8080}, nil
// }
// thunk := readerresult.From0(getConfig)
// rr := thunk() // Create the ReaderResult
// config, err := rr(ctx) // Execute it
//
//go:inline
func From0[A any](f func(context.Context) (A, error)) func() ReaderResult[A] {
return RR.From0(f)
}
// From1 converts a function with one parameter into an uncurried ReaderResult-returning function.
//
// Unlike Curry1 which returns a curried function, From1 returns a function that takes
// all parameters at once (except context). This is more convenient for direct calls.
//
// Type Parameters:
// - T1: The parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1) and returns (A, error)
//
// Returns:
// - A function that takes T1 and returns ReaderResult[A]
//
// Example:
//
// func getUser(ctx context.Context, id int) (User, error) {
// return User{ID: id}, nil
// }
// getUserRR := readerresult.From1(getUser)
// rr := getUserRR(42)
// user, err := rr(ctx)
//
//go:inline
func From1[T1, A any](f func(context.Context, T1) (A, error)) func(T1) ReaderResult[A] {
return RR.From1(f)
}
// From2 converts a function with two parameters into an uncurried ReaderResult-returning function.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2) and returns (A, error)
//
// Returns:
// - A function that takes (T1, T2) and returns ReaderResult[A]
//
// Example:
//
// func updateUser(ctx context.Context, id int, name string) (User, error) {
// return User{ID: id, Name: name}, nil
// }
// updateUserRR := readerresult.From2(updateUser)
// rr := updateUserRR(42, "Alice")
// user, err := rr(ctx)
//
//go:inline
func From2[T1, T2, A any](f func(context.Context, T1, T2) (A, error)) func(T1, T2) ReaderResult[A] {
return RR.From2(f)
}
// From3 converts a function with three parameters into an uncurried ReaderResult-returning function.
//
// Type Parameters:
// - T1: The first parameter type
// - T2: The second parameter type
// - T3: The third parameter type
// - A: The return value type
//
// Parameters:
// - f: A function that takes (context.Context, T1, T2, T3) and returns (A, error)
//
// Returns:
// - A function that takes (T1, T2, T3) and returns ReaderResult[A]
//
// Example:
//
// func createPost(ctx context.Context, userID int, title, body string) (Post, error) {
// return Post{UserID: userID, Title: title, Body: body}, nil
// }
// createPostRR := readerresult.From3(createPost)
// rr := createPostRR(42, "Title", "Body")
// post, err := rr(ctx)
//
//go:inline
func From3[T1, T2, T3, A any](f func(context.Context, T1, T2, T3) (A, error)) func(T1, T2, T3) ReaderResult[A] {
return RR.From3(f)
}

View File

@@ -0,0 +1,120 @@
// 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 readerresult
import (
"context"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
M "github.com/IBM/fp-go/v2/monoid"
)
// AlternativeMonoid creates a Monoid for ReaderResult using the Alternative semantics.
//
// The Alternative semantics means that the monoid operation tries the first computation,
// and if it fails, tries the second one. The empty element is a computation that always fails.
// The inner values are combined using the provided monoid when both computations succeed.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - m: A Monoid[A] for combining successful values
//
// Returns:
// - A Monoid[ReaderResult[A]] with Alternative semantics
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
//
// // Monoid for integers with addition
// intMonoid := monoid.MonoidSum[int]()
// rrMonoid := readerresult.AlternativeMonoid(intMonoid)
//
// rr1 := readerresult.Right(10)
// rr2 := readerresult.Right(20)
// combined := rrMonoid.Concat(rr1, rr2)
// value, err := combined(ctx) // Returns (30, nil)
//
//go:inline
func AlternativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
return RR.AlternativeMonoid[context.Context](m)
}
// AltMonoid creates a Monoid for ReaderResult using Alt semantics with a custom zero.
//
// The Alt semantics means that the monoid operation tries the first computation,
// and if it fails, tries the second one. The provided zero is used as the empty element.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - zero: A lazy ReaderResult[A] to use as the empty element
//
// Returns:
// - A Monoid[ReaderResult[A]] with Alt semantics
//
// Example:
//
// zero := func() readerresult.ReaderResult[int] {
// return readerresult.Left[int](errors.New("empty"))
// }
// rrMonoid := readerresult.AltMonoid(zero)
//
// rr1 := readerresult.Left[int](errors.New("failed"))
// rr2 := readerresult.Right(42)
// combined := rrMonoid.Concat(rr1, rr2)
// value, err := combined(ctx) // Returns (42, nil) - uses second on first failure
//
//go:inline
func AltMonoid[A any](zero Lazy[ReaderResult[A]]) Monoid[A] {
return RR.AltMonoid(zero)
}
// ApplicativeMonoid creates a Monoid for ReaderResult using Applicative semantics.
//
// The Applicative semantics means that both computations are executed independently,
// and their results are combined using the provided monoid. If either fails, the
// entire operation fails.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - m: A Monoid[A] for combining successful values
//
// Returns:
// - A Monoid[ReaderResult[A]] with Applicative semantics
//
// Example:
//
// import "github.com/IBM/fp-go/v2/monoid"
//
// // Monoid for integers with addition
// intMonoid := monoid.MonoidSum[int]()
// rrMonoid := readerresult.ApplicativeMonoid(intMonoid)
//
// rr1 := readerresult.Right(10)
// rr2 := readerresult.Right(20)
// combined := rrMonoid.Concat(rr1, rr2)
// value, err := combined(ctx) // Returns (30, nil)
//
//go:inline
func ApplicativeMonoid[A any](m M.Monoid[A]) Monoid[A] {
return RR.ApplicativeMonoid[context.Context](m)
}

View File

@@ -0,0 +1,822 @@
// 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 readerresult
import (
"context"
"sync"
"time"
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/option"
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
"github.com/IBM/fp-go/v2/idiomatic/result"
"github.com/IBM/fp-go/v2/reader"
RES "github.com/IBM/fp-go/v2/result"
)
// FromEither lifts a Result (Either[error, A]) into a ReaderResult.
//
// The resulting ReaderResult ignores the context.Context environment and simply
// returns the Result value. This is useful for converting existing Result values
// into the ReaderResult monad for composition with other ReaderResult operations.
//
// Type Parameters:
// - A: The success value type
//
// Parameters:
// - e: A Result[A] (Either[error, A]) to lift
//
// Returns:
// - A ReaderResult[A] that ignores the context and returns the Result
//
// Example:
//
// result := result.Of(42)
// rr := readerresult.FromEither(result)
// value, err := rr(ctx) // Returns (42, nil)
//
//go:inline
func FromEither[A any](e Result[A]) ReaderResult[A] {
return RR.FromEither[context.Context](e)
}
// FromResult creates a ReaderResult from a Go-style (value, error) tuple.
//
// This is a convenience function for converting standard Go error handling
// into the ReaderResult monad. The resulting ReaderResult ignores the context.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: The value
// - err: The error (nil for success)
//
// Returns:
// - A ReaderResult[A] that returns the given value and error
//
// Example:
//
// rr := readerresult.FromResult(42, nil)
// value, err := rr(ctx) // Returns (42, nil)
//
// rr2 := readerresult.FromResult(0, errors.New("failed"))
// value, err := rr2(ctx) // Returns (0, error)
//
//go:inline
func FromResult[A any](a A, err error) ReaderResult[A] {
return RR.FromResult[context.Context](a, err)
}
//go:inline
func RightReader[A any](rdr Reader[context.Context, A]) ReaderResult[A] {
return RR.RightReader(rdr)
}
//go:inline
func LeftReader[A, R any](l Reader[context.Context, error]) ReaderResult[A] {
return RR.LeftReader[A](l)
}
// Left creates a ReaderResult that always fails with the given error.
//
// This is the error constructor for ReaderResult, analogous to Either's Left.
// The resulting computation ignores the context and immediately returns the error.
//
// Type Parameters:
// - A: The success type (for type inference)
//
// Parameters:
// - err: The error to return
//
// Returns:
// - A ReaderResult[A] that always fails with the given error
//
// Example:
//
// rr := readerresult.Left[int](errors.New("failed"))
// value, err := rr(ctx) // Returns (0, error)
//
//go:inline
func Left[A any](err error) ReaderResult[A] {
return RR.Left[context.Context, A](err)
}
// Right creates a ReaderResult that always succeeds with the given value.
//
// This is the success constructor for ReaderResult, analogous to Either's Right.
// The resulting computation ignores the context and immediately returns the value.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: The value to return
//
// Returns:
// - A ReaderResult[A] that always succeeds with the given value
//
// Example:
//
// rr := readerresult.Right(42)
// value, err := rr(ctx) // Returns (42, nil)
//
//go:inline
func Right[A any](a A) ReaderResult[A] {
return RR.Right[context.Context, A](a)
}
// FromReader lifts a Reader into a ReaderResult that always succeeds.
//
// The Reader computation is executed and its result is wrapped in a successful Result.
// This is useful for incorporating Reader computations into ReaderResult pipelines.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - r: A Reader[context.Context, A] to lift
//
// Returns:
// - A ReaderResult[A] that executes the Reader and always succeeds
//
// Example:
//
// getConfig := func(ctx context.Context) Config {
// return Config{Port: 8080}
// }
// rr := readerresult.FromReader(getConfig)
// value, err := rr(ctx) // Returns (Config{Port: 8080}, nil)
//
//go:inline
func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
return RR.FromReader(r)
}
// MonadMap transforms the success value of a ReaderResult using the given function.
//
// If the ReaderResult fails, the error is propagated unchanged. This is the
// Functor's map operation for ReaderResult.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - fa: The ReaderResult to transform
// - f: The transformation function
//
// Returns:
// - A ReaderResult[B] with the transformed value
//
// Example:
//
// rr := readerresult.Right(42)
// mapped := readerresult.MonadMap(rr, func(n int) string {
// return fmt.Sprintf("Value: %d", n)
// })
// value, err := mapped(ctx) // Returns ("Value: 42", nil)
//
//go:inline
func MonadMap[A, B any](fa ReaderResult[A], f func(A) B) ReaderResult[B] {
return RR.MonadMap(fa, f)
}
// Map is the curried version of MonadMap, useful for function composition.
//
// It returns an Operator that can be used in pipelines with F.Pipe.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: The transformation function
//
// Returns:
// - An Operator that transforms ReaderResult[A] to ReaderResult[B]
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// rr := readerresult.Right(42)
// result := F.Pipe1(
// rr,
// readerresult.Map(func(n int) string {
// return fmt.Sprintf("Value: %d", n)
// }),
// )
//
//go:inline
func Map[A, B any](f func(A) B) Operator[A, B] {
return RR.Map[context.Context](f)
}
// MonadChain sequences two ReaderResult computations where the second depends on the first.
//
// This is the monadic bind operation (flatMap). If the first computation fails,
// the error is propagated and the second computation is not executed. Both
// computations share the same context.Context environment.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - ma: The first ReaderResult computation
// - f: A Kleisli arrow that produces the second computation based on the first's result
//
// Returns:
// - A ReaderResult[B] representing the sequenced computation
//
// Example:
//
// getUser := readerresult.Right(User{ID: 1, Name: "Alice"})
// getPosts := func(user User) readerresult.ReaderResult[[]Post] {
// return readerresult.Right([]Post{{UserID: user.ID}})
// }
// result := readerresult.MonadChain(getUser, getPosts)
// posts, err := result(ctx)
//
//go:inline
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
return RR.MonadChain(ma, f)
}
// Chain is the curried version of MonadChain, useful for function composition.
//
// It returns an Operator that can be used in pipelines with F.Pipe.
//
// Type Parameters:
// - A: The input value type
// - B: The output value type
//
// Parameters:
// - f: A Kleisli arrow for the second computation
//
// Returns:
// - An Operator that chains ReaderResult computations
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// result := F.Pipe1(
// getUser(1),
// readerresult.Chain(func(user User) readerresult.ReaderResult[[]Post] {
// return getPosts(user.ID)
// }),
// )
//
//go:inline
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
return RR.Chain(f)
}
// Of creates a ReaderResult that always succeeds with the given value.
//
// This is an alias for Right and represents the Applicative's pure/return operation.
// The resulting computation ignores the context and immediately returns the value.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: The value to wrap
//
// Returns:
// - A ReaderResult[A] that always succeeds with the given value
//
// Example:
//
// rr := readerresult.Of(42)
// value, err := rr(ctx) // Returns (42, nil)
//
//go:inline
func Of[A any](a A) ReaderResult[A] {
return RR.Of[context.Context, A](a)
}
// MonadAp applies a function wrapped in a ReaderResult to a value wrapped in a ReaderResult.
//
// This is the Applicative's ap operation. Both computations are executed concurrently
// using goroutines, and the context is shared between them. If either computation fails,
// the entire operation fails. If the context is cancelled, the operation is aborted.
//
// The concurrent execution allows for parallel independent computations, which can
// improve performance when both operations involve I/O or other blocking operations.
//
// Type Parameters:
// - B: The result type after applying the function
// - A: The input type to the function
//
// Parameters:
// - fab: A ReaderResult containing a function from A to B
// - fa: A ReaderResult containing a value of type A
//
// Returns:
// - A ReaderResult[B] that applies the function to the value
//
// Example:
//
// // Create a function wrapped in ReaderResult
// addTen := readerresult.Right(func(n int) int {
// return n + 10
// })
//
// // Create a value wrapped in ReaderResult
// value := readerresult.Right(32)
//
// // Apply the function to the value
// result := readerresult.MonadAp(addTen, value)
// output, err := result(ctx) // Returns (42, nil)
//
// Error Handling:
//
// // If the function fails
// failedFn := readerresult.Left[func(int) int](errors.New("function error"))
// result := readerresult.MonadAp(failedFn, value)
// _, err := result(ctx) // Returns function error
//
// // If the value fails
// failedValue := readerresult.Left[int](errors.New("value error"))
// result := readerresult.MonadAp(addTen, failedValue)
// _, err := result(ctx) // Returns value error
//
// Context Cancellation:
//
// ctx, cancel := context.WithCancel(context.Background())
// cancel() // Cancel immediately
// result := readerresult.MonadAp(addTen, value)
// _, err := result(ctx) // Returns context cancellation error
func MonadAp[B, A any](fab ReaderResult[func(A) B], fa ReaderResult[A]) ReaderResult[B] {
return func(ctx context.Context) (B, error) {
if ctx.Err() != nil {
return result.Left[B](context.Cause(ctx))
}
var wg sync.WaitGroup
wg.Add(1)
cancelCtx, cancelFct := context.WithCancel(ctx)
defer cancelFct()
var a A
var aerr error
go func() {
defer wg.Done()
a, aerr = fa(cancelCtx)
if aerr != nil {
cancelFct()
}
}()
ab, aberr := fab(cancelCtx)
if aberr != nil {
cancelFct()
wg.Wait()
return result.Left[B](aberr)
}
wg.Wait()
if aerr != nil {
return result.Left[B](aerr)
}
return result.Of(ab(a))
}
}
// Ap is the curried version of MonadAp, useful for function composition.
//
// It fixes the value argument and returns an Operator that can be applied
// to a ReaderResult containing a function. This is particularly useful in
// pipelines where you want to apply a fixed value to various functions.
//
// Type Parameters:
// - B: The result type after applying the function
// - A: The input type to the function
//
// Parameters:
// - fa: A ReaderResult containing a value of type A
//
// Returns:
// - An Operator that applies the value to a function wrapped in ReaderResult
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// value := readerresult.Right(32)
// addTen := readerresult.Right(func(n int) int { return n + 10 })
//
// result := F.Pipe1(
// addTen,
// readerresult.Ap[int](value),
// )
// output, err := result(ctx) // Returns (42, nil)
//
//go:inline
func Ap[B, A any](fa ReaderResult[A]) Operator[func(A) B, B] {
return function.Bind2nd(MonadAp[B, A], fa)
}
//go:inline
func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A] {
return RR.FromPredicate[context.Context](pred, onFalse)
}
//go:inline
func Fold[A, B any](onLeft reader.Kleisli[context.Context, error, B], onRight reader.Kleisli[context.Context, A, B]) func(ReaderResult[A]) Reader[context.Context, B] {
return RR.Fold(onLeft, onRight)
}
//go:inline
func GetOrElse[A any](onLeft reader.Kleisli[context.Context, error, A]) func(ReaderResult[A]) Reader[context.Context, A] {
return RR.GetOrElse(onLeft)
}
//go:inline
func OrElse[A any](onLeft Kleisli[error, A]) Operator[A, A] {
return RR.OrElse(onLeft)
}
//go:inline
func OrLeft[A any](onLeft reader.Kleisli[context.Context, error, error]) Operator[A, A] {
return RR.OrLeft[A](onLeft)
}
// Ask retrieves the current context.Context environment.
//
// This is the Reader's ask operation, which provides access to the environment.
// It always succeeds and returns the context that was passed in.
//
// Returns:
// - A ReaderResult[context.Context] that returns the environment
//
// Example:
//
// rr := readerresult.Ask()
// ctx, err := rr(context.Background()) // Returns (context.Background(), nil)
//
//go:inline
func Ask() ReaderResult[context.Context] {
return RR.Ask[context.Context]()
}
// Asks extracts a value from the context.Context environment using a Reader function.
//
// This is useful for accessing specific parts of the environment. The Reader
// function is applied to the context, and the result is wrapped in a successful ReaderResult.
//
// Type Parameters:
// - A: The extracted value type
//
// Parameters:
// - r: A Reader function that extracts a value from the context
//
// Returns:
// - A ReaderResult[A] that extracts and returns the value
//
// Example:
//
// type key int
// const userKey key = 0
//
// getUser := readerresult.Asks(func(ctx context.Context) User {
// return ctx.Value(userKey).(User)
// })
// user, err := getUser(ctx)
//
//go:inline
func Asks[A any](r Reader[context.Context, A]) ReaderResult[A] {
return RR.Asks(r)
}
//go:inline
func MonadChainEitherK[A, B any](ma ReaderResult[A], f RES.Kleisli[A, B]) ReaderResult[B] {
return RR.MonadChainEitherK[context.Context, A, B](ma, f)
}
//go:inline
func ChainEitherK[A, B any](f RES.Kleisli[A, B]) Operator[A, B] {
return RR.ChainEitherK[context.Context, A, B](f)
}
//go:inline
func MonadChainReaderK[A, B any](ma ReaderResult[A], f result.Kleisli[A, B]) ReaderResult[B] {
return RR.MonadChainReaderK(ma, f)
}
//go:inline
func ChainReaderK[A, B any](f result.Kleisli[A, B]) Operator[A, B] {
return RR.ChainReaderK[context.Context](f)
}
//go:inline
func ChainOptionK[A, B any](onNone Lazy[error]) func(option.Kleisli[A, B]) Operator[A, B] {
return RR.ChainOptionK[context.Context, A, B](onNone)
}
// Flatten removes one level of ReaderResult nesting.
//
// This is equivalent to Chain with the identity function. It's useful when you have
// a ReaderResult that produces another ReaderResult and want to collapse them into one.
//
// Type Parameters:
// - A: The inner value type
//
// Parameters:
// - mma: A nested ReaderResult[ReaderResult[A]]
//
// Returns:
// - A flattened ReaderResult[A]
//
// Example:
//
// nested := readerresult.Right(readerresult.Right(42))
// flattened := readerresult.Flatten(nested)
// value, err := flattened(ctx) // Returns (42, nil)
//
//go:inline
func Flatten[A any](mma ReaderResult[ReaderResult[A]]) ReaderResult[A] {
return RR.Flatten(mma)
}
//go:inline
func MonadBiMap[A, B any](fa ReaderResult[A], f Endomorphism[error], g func(A) B) ReaderResult[B] {
return RR.MonadBiMap(fa, f, g)
}
//go:inline
func BiMap[A, B any](f Endomorphism[error], g func(A) B) Operator[A, B] {
return RR.BiMap[context.Context](f, g)
}
// Read executes a ReaderResult by providing it with a context.Context.
//
// This is the elimination form for ReaderResult - it "runs" the computation
// by supplying the required environment, producing a (value, error) tuple.
//
// Type Parameters:
// - A: The result value type
//
// Parameters:
// - ctx: The context.Context environment to provide
//
// Returns:
// - A function that executes a ReaderResult[A] and returns (A, error)
//
// Example:
//
// rr := readerresult.Right(42)
// execute := readerresult.Read[int](ctx)
// value, err := execute(rr) // Returns (42, nil)
//
// // Or more commonly used directly:
// value, err := rr(ctx)
//
//go:inline
func Read[A any](ctx context.Context) func(ReaderResult[A]) (A, error) {
return RR.Read[A](ctx)
}
//go:inline
func MonadFlap[A, B any](fab ReaderResult[func(A) B], a A) ReaderResult[B] {
return RR.MonadFlap(fab, a)
}
//go:inline
func Flap[B, A any](a A) Operator[func(A) B, B] {
return RR.Flap[context.Context, B](a)
}
//go:inline
func MonadMapLeft[A any](fa ReaderResult[A], f Endomorphism[error]) ReaderResult[A] {
return RR.MonadMapLeft(fa, f)
}
//go:inline
func MapLeft[A any](f Endomorphism[error]) Operator[A, A] {
return RR.MapLeft[context.Context, A](f)
}
//go:inline
func MonadAlt[A any](first ReaderResult[A], second Lazy[ReaderResult[A]]) ReaderResult[A] {
return RR.MonadAlt(first, second)
}
//go:inline
func Alt[A any](second Lazy[ReaderResult[A]]) Operator[A, A] {
return RR.Alt(second)
}
// Local transforms the context.Context environment before passing it to a ReaderResult computation.
//
// This is the Reader's local operation, which allows you to modify the environment
// for a specific computation without affecting the outer context. The transformation
// function receives the current context and returns a new context along with a
// cancel function. The cancel function is automatically called when the computation
// completes (via defer), ensuring proper cleanup of resources.
//
// This is useful for:
// - Adding timeouts or deadlines to specific operations
// - Adding context values for nested computations
// - Creating isolated context scopes
// - Implementing context-based dependency injection
//
// Type Parameters:
// - A: The value type of the ReaderResult
//
// Parameters:
// - f: A function that transforms the context and returns a cancel function
//
// Returns:
// - An Operator that runs the computation with the transformed context
//
// Example:
//
// import F "github.com/IBM/fp-go/v2/function"
//
// // Add a custom value to the context
// type key int
// const userKey key = 0
//
// addUser := readerresult.Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
// newCtx := context.WithValue(ctx, userKey, "Alice")
// return newCtx, func() {} // No-op cancel
// })
//
// getUser := readerresult.Asks(func(ctx context.Context) string {
// return ctx.Value(userKey).(string)
// })
//
// result := F.Pipe1(
// getUser,
// addUser,
// )
// user, err := result(context.Background()) // Returns ("Alice", nil)
//
// Timeout Example:
//
// // Add a 5-second timeout to a specific operation
// withTimeout := readerresult.Local[Data](func(ctx context.Context) (context.Context, context.CancelFunc) {
// return context.WithTimeout(ctx, 5*time.Second)
// })
//
// result := F.Pipe1(
// fetchData,
// withTimeout,
// )
func Local[A any](f func(context.Context) (context.Context, context.CancelFunc)) Operator[A, A] {
return func(rr ReaderResult[A]) ReaderResult[A] {
return func(ctx context.Context) (A, error) {
if ctx.Err() != nil {
return result.Left[A](context.Cause(ctx))
}
otherCtx, otherCancel := f(ctx)
defer otherCancel()
return rr(otherCtx)
}
}
}
// WithTimeout adds a timeout to the context for a ReaderResult computation.
//
// This is a convenience wrapper around Local that uses context.WithTimeout.
// The computation must complete within the specified duration, or it will be
// cancelled. This is useful for ensuring operations don't run indefinitely
// and for implementing timeout-based error handling.
//
// The timeout is relative to when the ReaderResult is executed, not when
// WithTimeout is called. The cancel function is automatically called when
// the computation completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderResult
//
// Parameters:
// - timeout: The maximum duration for the computation
//
// Returns:
// - An Operator that runs the computation with a timeout
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Fetch data with a 5-second timeout
// fetchData := readerresult.FromReader(func(ctx context.Context) Data {
// // Simulate slow operation
// select {
// case <-time.After(10 * time.Second):
// return Data{Value: "slow"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerresult.WithTimeout[Data](5*time.Second),
// )
// _, err := result(context.Background()) // Returns context.DeadlineExceeded after 5s
//
// Successful Example:
//
// quickFetch := readerresult.Right(Data{Value: "quick"})
// result := F.Pipe1(
// quickFetch,
// readerresult.WithTimeout[Data](5*time.Second),
// )
// data, err := result(context.Background()) // 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)
})
}
// WithDeadline adds an absolute deadline to the context for a ReaderResult computation.
//
// This is a convenience wrapper around Local that uses context.WithDeadline.
// The computation must complete before the specified time, or it will be
// cancelled. This is useful for coordinating operations that must finish
// by a specific time, such as request deadlines or scheduled tasks.
//
// The deadline is an absolute time, unlike WithTimeout which uses a relative
// duration. The cancel function is automatically called when the computation
// completes, ensuring proper cleanup.
//
// Type Parameters:
// - A: The value type of the ReaderResult
//
// Parameters:
// - deadline: The absolute time by which the computation must complete
//
// Returns:
// - An Operator that runs the computation with a deadline
//
// Example:
//
// import (
// "time"
// F "github.com/IBM/fp-go/v2/function"
// )
//
// // Operation must complete by 3 PM
// deadline := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC)
//
// fetchData := readerresult.FromReader(func(ctx context.Context) Data {
// // Simulate operation
// select {
// case <-time.After(1 * time.Hour):
// return Data{Value: "done"}
// case <-ctx.Done():
// return Data{}
// }
// })
//
// result := F.Pipe1(
// fetchData,
// readerresult.WithDeadline[Data](deadline),
// )
// _, err := result(context.Background()) // Returns context.DeadlineExceeded if past deadline
//
// Combining with Parent Context:
//
// // If parent context already has a deadline, the earlier one takes precedence
// parentCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Hour))
// defer cancel()
//
// laterDeadline := time.Now().Add(2 * time.Hour)
// result := F.Pipe1(
// fetchData,
// readerresult.WithDeadline[Data](laterDeadline),
// )
// _, 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)
})
}

View File

@@ -0,0 +1,952 @@
// 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 readerresult
import (
"context"
"errors"
"fmt"
"strconv"
"testing"
"time"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
// Helper types for testing
type User struct {
ID int
Name string
}
type Config struct {
Port int
DatabaseURL string
}
func TestFromEither(t *testing.T) {
ctx := context.Background()
t.Run("lifts successful Result", func(t *testing.T) {
// FromEither expects a Result[A] which is Either[error, A]
// We need to create it properly using the result package
rr := Right(42)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("lifts failing Result", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
_, err := rr(ctx)
assert.Equal(t, testErr, err)
})
}
func TestFromResult(t *testing.T) {
ctx := context.Background()
t.Run("creates successful ReaderResult", func(t *testing.T) {
rr := FromResult(42, nil)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("creates failing ReaderResult", func(t *testing.T) {
testErr := errors.New("test error")
rr := FromResult(0, testErr)
_, err := rr(ctx)
assert.Equal(t, testErr, err)
})
}
func TestLeftAndRight(t *testing.T) {
ctx := context.Background()
t.Run("Right creates successful value", func(t *testing.T) {
rr := Right(42)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("Left creates error", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
_, err := rr(ctx)
assert.Equal(t, testErr, err)
})
t.Run("Of is alias for Right", func(t *testing.T) {
rr := Of(42)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
}
func TestFromReader(t *testing.T) {
ctx := context.Background()
t.Run("lifts Reader as success", func(t *testing.T) {
r := func(ctx context.Context) int {
return 42
}
rr := FromReader(r)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("uses context", func(t *testing.T) {
type key int
const testKey key = 0
ctx := context.WithValue(context.Background(), testKey, 100)
r := func(ctx context.Context) int {
return ctx.Value(testKey).(int)
}
rr := FromReader(r)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 100, value)
})
}
func TestMonadMap(t *testing.T) {
ctx := context.Background()
t.Run("transforms success value", func(t *testing.T) {
rr := Right(42)
mapped := MonadMap(rr, S.Format[int]("Value: %d"))
value, err := mapped(ctx)
assert.NoError(t, err)
assert.Equal(t, "Value: 42", value)
})
t.Run("propagates error", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
mapped := MonadMap(rr, S.Format[int]("Value: %d"))
_, err := mapped(ctx)
assert.Equal(t, testErr, err)
})
t.Run("chains multiple maps", func(t *testing.T) {
rr := Right(10)
result := MonadMap(
MonadMap(rr, N.Mul(2)),
strconv.Itoa,
)
value, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "20", value)
})
}
func TestMap(t *testing.T) {
ctx := context.Background()
t.Run("curried version works", func(t *testing.T) {
rr := Right(42)
mapper := Map(S.Format[int]("Value: %d"))
mapped := mapper(rr)
value, err := mapped(ctx)
assert.NoError(t, err)
assert.Equal(t, "Value: 42", value)
})
}
func TestMonadChain(t *testing.T) {
ctx := context.Background()
t.Run("sequences dependent computations", func(t *testing.T) {
getUser := Right(User{ID: 1, Name: "Alice"})
getPosts := func(user User) ReaderResult[string] {
return Right(fmt.Sprintf("Posts for %s", user.Name))
}
result := MonadChain(getUser, getPosts)
value, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "Posts for Alice", value)
})
t.Run("propagates first error", func(t *testing.T) {
testErr := errors.New("first error")
getUser := Left[User](testErr)
getPosts := func(user User) ReaderResult[string] {
return Right("posts")
}
result := MonadChain(getUser, getPosts)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
t.Run("propagates second error", func(t *testing.T) {
testErr := errors.New("second error")
getUser := Right(User{ID: 1, Name: "Alice"})
getPosts := func(user User) ReaderResult[string] {
return Left[string](testErr)
}
result := MonadChain(getUser, getPosts)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
}
func TestChain(t *testing.T) {
ctx := context.Background()
t.Run("curried version works", func(t *testing.T) {
getUser := Right(User{ID: 1, Name: "Alice"})
chainer := Chain(func(user User) ReaderResult[string] {
return Right(fmt.Sprintf("Posts for %s", user.Name))
})
result := chainer(getUser)
value, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "Posts for Alice", value)
})
}
func TestAsk(t *testing.T) {
t.Run("retrieves environment", func(t *testing.T) {
ctx := context.Background()
rr := Ask()
retrievedCtx, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, ctx, retrievedCtx)
})
t.Run("always succeeds", func(t *testing.T) {
ctx := context.Background()
rr := Ask()
_, err := rr(ctx)
assert.NoError(t, err)
})
}
func TestAsks(t *testing.T) {
type key int
const userKey key = 0
t.Run("extracts value from environment", func(t *testing.T) {
user := User{ID: 1, Name: "Alice"}
ctx := context.WithValue(context.Background(), userKey, user)
getUser := Asks(func(ctx context.Context) User {
return ctx.Value(userKey).(User)
})
retrievedUser, err := getUser(ctx)
assert.NoError(t, err)
assert.Equal(t, user, retrievedUser)
})
t.Run("works with different extractors", func(t *testing.T) {
ctx := context.WithValue(context.Background(), userKey, 42)
getID := Asks(func(ctx context.Context) int {
return ctx.Value(userKey).(int)
})
id, err := getID(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, id)
})
}
func TestFlatten(t *testing.T) {
ctx := context.Background()
t.Run("removes one level of nesting", func(t *testing.T) {
nested := Right(Right(42))
flattened := Flatten(nested)
value, err := flattened(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("propagates outer error", func(t *testing.T) {
testErr := errors.New("outer error")
nested := Left[ReaderResult[int]](testErr)
flattened := Flatten(nested)
_, err := flattened(ctx)
assert.Equal(t, testErr, err)
})
t.Run("propagates inner error", func(t *testing.T) {
testErr := errors.New("inner error")
nested := Right(Left[int](testErr))
flattened := Flatten(nested)
_, err := flattened(ctx)
assert.Equal(t, testErr, err)
})
}
func TestRead(t *testing.T) {
t.Run("executes ReaderResult with context", func(t *testing.T) {
ctx := context.Background()
rr := Right(42)
execute := Read[int](ctx)
value, err := execute(rr)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
}
func TestCurry0(t *testing.T) {
ctx := context.Background()
t.Run("converts function to ReaderResult", func(t *testing.T) {
f := func(ctx context.Context) (int, error) {
return 42, nil
}
rr := Curry0(f)
value, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
}
func TestCurry1(t *testing.T) {
ctx := context.Background()
t.Run("curries function with one parameter", func(t *testing.T) {
f := func(ctx context.Context, id int) (User, error) {
return User{ID: id, Name: "Alice"}, nil
}
getUserRR := Curry1(f)
rr := getUserRR(1)
user, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, User{ID: 1, Name: "Alice"}, user)
})
}
func TestCurry2(t *testing.T) {
ctx := context.Background()
t.Run("curries function with two parameters", func(t *testing.T) {
f := func(ctx context.Context, id int, name string) (User, error) {
return User{ID: id, Name: name}, nil
}
updateUserRR := Curry2(f)
rr := updateUserRR(1)("Bob")
user, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, User{ID: 1, Name: "Bob"}, user)
})
}
func TestFrom1(t *testing.T) {
ctx := context.Background()
t.Run("converts function to uncurried form", func(t *testing.T) {
f := func(ctx context.Context, id int) (User, error) {
return User{ID: id, Name: "Alice"}, nil
}
getUserRR := From1(f)
rr := getUserRR(1)
user, err := rr(ctx)
assert.NoError(t, err)
assert.Equal(t, User{ID: 1, Name: "Alice"}, user)
})
}
// Note: SequenceReader and TraverseReader tests are complex due to type system interactions
// These functions are tested indirectly through their usage in other tests
func TestSequenceArray(t *testing.T) {
ctx := context.Background()
t.Run("sequences array of ReaderResults", func(t *testing.T) {
readers := []ReaderResult[int]{
Right(1),
Right(2),
Right(3),
}
result := SequenceArray(readers)
values, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, []int{1, 2, 3}, values)
})
t.Run("fails on first error", func(t *testing.T) {
testErr := errors.New("test error")
readers := []ReaderResult[int]{
Right(1),
Left[int](testErr),
Right(3),
}
result := SequenceArray(readers)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
}
func TestTraverseArray(t *testing.T) {
ctx := context.Background()
t.Run("applies function to each element", func(t *testing.T) {
double := func(n int) ReaderResult[int] {
return Right(n * 2)
}
numbers := []int{1, 2, 3}
result := TraverseArray(double)(numbers)
values, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, []int{2, 4, 6}, values)
})
}
func TestSequenceT2(t *testing.T) {
ctx := context.Background()
t.Run("combines two ReaderResults", func(t *testing.T) {
rr1 := Right(42)
rr2 := Right("hello")
result := SequenceT2(rr1, rr2)
tuple, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, tuple.F1)
assert.Equal(t, "hello", tuple.F2)
})
t.Run("fails if first fails", func(t *testing.T) {
testErr := errors.New("test error")
rr1 := Left[int](testErr)
rr2 := Right("hello")
result := SequenceT2(rr1, rr2)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
}
func TestDo(t *testing.T) {
ctx := context.Background()
t.Run("initializes do-notation", func(t *testing.T) {
type State struct {
Value int
}
result := Do(State{})
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, State{Value: 0}, state)
})
}
func TestBindTo(t *testing.T) {
ctx := context.Background()
t.Run("binds value to state", func(t *testing.T) {
type State struct {
User User
}
getUser := Right(User{ID: 1, Name: "Alice"})
result := BindTo(func(u User) State {
return State{User: u}
})(getUser)
state, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, User{ID: 1, Name: "Alice"}, state.User)
})
}
func TestMonadAp(t *testing.T) {
ctx := context.Background()
t.Run("applies function to value", func(t *testing.T) {
addTen := Right(N.Add(10))
value := Right(32)
result := MonadAp(addTen, value)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, output)
})
t.Run("propagates function error", func(t *testing.T) {
testErr := errors.New("function error")
failedFn := Left[func(int) int](testErr)
value := Right(32)
result := MonadAp(failedFn, value)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
t.Run("propagates value error", func(t *testing.T) {
testErr := errors.New("value error")
addTen := Right(N.Add(10))
failedValue := Left[int](testErr)
result := MonadAp(addTen, failedValue)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
t.Run("handles context cancellation", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
addTen := Right(N.Add(10))
value := Right(32)
result := MonadAp(addTen, value)
_, err := result(cancelCtx)
assert.Error(t, err)
assert.Equal(t, context.Canceled, err)
})
t.Run("works with different types", func(t *testing.T) {
toString := Right(func(n int) string {
return fmt.Sprintf("Number: %d", n)
})
value := Right(42)
result := MonadAp(toString, value)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "Number: 42", output)
})
t.Run("works with complex functions", func(t *testing.T) {
multiply := Right(func(user User) int {
return user.ID * 10
})
user := Right(User{ID: 5, Name: "Bob"})
result := MonadAp(multiply, user)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 50, output)
})
t.Run("executes both computations concurrently", func(t *testing.T) {
// This test verifies that both computations run concurrently
// by checking that they both complete even if one takes time
slowFn := func(ctx context.Context) (func(int) int, error) {
// Simulate some work
return N.Mul(2), nil
}
slowValue := func(ctx context.Context) (int, error) {
// Simulate some work
return 21, nil
}
result := MonadAp(slowFn, slowValue)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, output)
})
}
func TestAp(t *testing.T) {
ctx := context.Background()
t.Run("curried version works", func(t *testing.T) {
value := Right(32)
addTen := Right(N.Add(10))
applyValue := Ap[int](value)
result := applyValue(addTen)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, output)
})
t.Run("works in pipeline", func(t *testing.T) {
value := Right(32)
addTen := Right(N.Add(10))
// Using Ap in a functional pipeline style
result := Ap[int](value)(addTen)
output, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, 42, output)
})
t.Run("propagates errors", func(t *testing.T) {
testErr := errors.New("value error")
failedValue := Left[int](testErr)
addTen := Right(N.Add(10))
result := Ap[int](failedValue)(addTen)
_, err := result(ctx)
assert.Equal(t, testErr, err)
})
}
func TestLocal(t *testing.T) {
t.Run("transforms context with custom value", func(t *testing.T) {
type key int
const userKey key = 0
// Create a computation that reads from context
getUser := Asks(func(ctx context.Context) string {
if user := ctx.Value(userKey); user != nil {
return user.(string)
}
return "unknown"
})
// Transform context to add user value
addUser := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
newCtx := context.WithValue(ctx, userKey, "Alice")
return newCtx, func() {} // No-op cancel
})
// Apply transformation
result := addUser(getUser)
user, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, "Alice", user)
})
t.Run("cancel function is called", func(t *testing.T) {
cancelCalled := false
transform := Local[int](func(ctx context.Context) (context.Context, context.CancelFunc) {
return ctx, func() {
cancelCalled = true
}
})
rr := Right(42)
result := transform(rr)
_, err := result(context.Background())
assert.NoError(t, err)
assert.True(t, cancelCalled, "cancel function should be called")
})
t.Run("propagates errors", func(t *testing.T) {
testErr := errors.New("test error")
transform := Local[int](func(ctx context.Context) (context.Context, context.CancelFunc) {
return ctx, func() {}
})
rr := Left[int](testErr)
result := transform(rr)
_, err := result(context.Background())
assert.Equal(t, testErr, err)
})
t.Run("nested transformations", func(t *testing.T) {
type key int
const key1 key = 0
const key2 key = 1
getValues := Asks(func(ctx context.Context) string {
v1 := ctx.Value(key1).(string)
v2 := ctx.Value(key2).(string)
return v1 + ":" + v2
})
addFirst := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, key1, "A"), func() {}
})
addSecond := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, key2, "B"), func() {}
})
result := addSecond(addFirst(getValues))
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, "A:B", value)
})
t.Run("preserves parent context values", func(t *testing.T) {
type key int
const parentKey key = 0
const childKey key = 1
parentCtx := context.WithValue(context.Background(), parentKey, "parent")
getValues := Asks(func(ctx context.Context) string {
parent := ctx.Value(parentKey).(string)
child := ctx.Value(childKey).(string)
return parent + ":" + child
})
addChild := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, childKey, "child"), func() {}
})
result := addChild(getValues)
value, err := result(parentCtx)
assert.NoError(t, err)
assert.Equal(t, "parent:child", value)
})
}
func TestWithTimeout(t *testing.T) {
t.Run("completes within timeout", func(t *testing.T) {
rr := Right(42)
result := WithTimeout[int](1 * time.Second)(rr)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("cancels on timeout", func(t *testing.T) {
// Create a computation that takes longer than timeout
slowComputation := func(ctx context.Context) (int, error) {
select {
case <-time.After(200 * time.Millisecond):
return 42, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
result := WithTimeout[int](50 * time.Millisecond)(slowComputation)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("propagates errors", func(t *testing.T) {
testErr := errors.New("test error")
rr := Left[int](testErr)
result := WithTimeout[int](1 * time.Second)(rr)
_, err := result(context.Background())
assert.Equal(t, testErr, err)
})
t.Run("respects parent context timeout", func(t *testing.T) {
// Parent has shorter timeout
parentCtx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
slowComputation := func(ctx context.Context) (int, error) {
select {
case <-time.After(200 * time.Millisecond):
return 42, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
// Child has longer timeout, but parent's shorter timeout should win
result := WithTimeout[int](1 * time.Second)(slowComputation)
_, err := result(parentCtx)
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("works with context-aware operations", func(t *testing.T) {
type key int
const dataKey key = 0
ctx := context.WithValue(context.Background(), dataKey, "test-data")
getData := Asks(func(ctx context.Context) string {
return ctx.Value(dataKey).(string)
})
result := WithTimeout[string](1 * time.Second)(getData)
value, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, "test-data", value)
})
t.Run("multiple timeouts compose correctly", func(t *testing.T) {
rr := Right(42)
// Apply multiple timeouts - the shortest should win
result := WithTimeout[int](100 * time.Millisecond)(
WithTimeout[int](1 * time.Second)(rr),
)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
}
func TestWithDeadline(t *testing.T) {
t.Run("completes before deadline", func(t *testing.T) {
deadline := time.Now().Add(1 * time.Second)
rr := Right(42)
result := WithDeadline[int](deadline)(rr)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("cancels after deadline", func(t *testing.T) {
deadline := time.Now().Add(50 * time.Millisecond)
slowComputation := func(ctx context.Context) (int, error) {
select {
case <-time.After(200 * time.Millisecond):
return 42, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
result := WithDeadline[int](deadline)(slowComputation)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("propagates errors", func(t *testing.T) {
testErr := errors.New("test error")
deadline := time.Now().Add(1 * time.Second)
rr := Left[int](testErr)
result := WithDeadline[int](deadline)(rr)
_, err := result(context.Background())
assert.Equal(t, testErr, err)
})
t.Run("respects parent context deadline", func(t *testing.T) {
// Parent has earlier deadline
parentDeadline := time.Now().Add(50 * time.Millisecond)
parentCtx, cancel := context.WithDeadline(context.Background(), parentDeadline)
defer cancel()
slowComputation := func(ctx context.Context) (int, error) {
select {
case <-time.After(200 * time.Millisecond):
return 42, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
// Child has later deadline, but parent's earlier deadline should win
childDeadline := time.Now().Add(1 * time.Second)
result := WithDeadline[int](childDeadline)(slowComputation)
_, err := result(parentCtx)
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("works with absolute time", func(t *testing.T) {
// Set deadline to a specific time in the future
deadline := time.Date(2130, 1, 1, 0, 0, 0, 0, time.UTC)
rr := Right(42)
result := WithDeadline[int](deadline)(rr)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("handles past deadline", func(t *testing.T) {
// Deadline already passed - context will be immediately cancelled
deadline := time.Now().Add(-1 * time.Second)
// Use a computation that checks context cancellation
checkCtx := func(ctx context.Context) (int, error) {
if err := ctx.Err(); err != nil {
return 0, err
}
return 42, nil
}
result := WithDeadline[int](deadline)(checkCtx)
_, err := result(context.Background())
assert.Error(t, err)
assert.Equal(t, context.DeadlineExceeded, err)
})
t.Run("works with context values", func(t *testing.T) {
type key int
const configKey key = 0
ctx := context.WithValue(context.Background(), configKey, Config{Port: 8080})
deadline := time.Now().Add(1 * time.Second)
getConfig := Asks(func(ctx context.Context) Config {
return ctx.Value(configKey).(Config)
})
result := WithDeadline[Config](deadline)(getConfig)
config, err := result(ctx)
assert.NoError(t, err)
assert.Equal(t, Config{Port: 8080}, config)
})
}
func TestLocalWithTimeoutAndDeadline(t *testing.T) {
t.Run("combines Local with WithTimeout", func(t *testing.T) {
type key int
const userKey key = 0
addUser := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, userKey, "Alice"), func() {}
})
getUser := Asks(func(ctx context.Context) string {
return ctx.Value(userKey).(string)
})
result := WithTimeout[string](1 * time.Second)(addUser(getUser))
user, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, "Alice", user)
})
t.Run("combines Local with WithDeadline", func(t *testing.T) {
type key int
const dataKey key = 0
addData := Local[int](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, dataKey, 42), func() {}
})
getData := Asks(func(ctx context.Context) int {
return ctx.Value(dataKey).(int)
})
deadline := time.Now().Add(1 * time.Second)
result := WithDeadline[int](deadline)(addData(getData))
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("complex composition", func(t *testing.T) {
type key int
const key1 key = 0
const key2 key = 1
// Add first value
addFirst := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, key1, "A"), func() {}
})
// Add second value
addSecond := Local[string](func(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithValue(ctx, key2, "B"), func() {}
})
// Read both values
getValues := Asks(func(ctx context.Context) string {
v1 := ctx.Value(key1).(string)
v2 := ctx.Value(key2).(string)
return v1 + ":" + v2
})
// Compose with timeout
result := WithTimeout[string](1 * time.Second)(
addSecond(addFirst(getValues)),
)
value, err := result(context.Background())
assert.NoError(t, err)
assert.Equal(t, "A:B", value)
})
}

View File

@@ -0,0 +1,133 @@
// 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 readerresult
import (
RR "github.com/IBM/fp-go/v2/idiomatic/readerresult"
T "github.com/IBM/fp-go/v2/tuple"
)
// SequenceT1 wraps a single ReaderResult in a Tuple1.
//
// This is mainly for consistency with the other SequenceT functions.
//
// Type Parameters:
// - A: The value type
//
// Parameters:
// - a: A ReaderResult[A]
//
// Returns:
// - A ReaderResult[Tuple1[A]]
//
// Example:
//
// rr := readerresult.Right(42)
// result := readerresult.SequenceT1(rr)
// tuple, err := result(ctx) // Returns (Tuple1{42}, nil)
//
//go:inline
func SequenceT1[A any](a ReaderResult[A]) ReaderResult[T.Tuple1[A]] {
return RR.SequenceT1(a)
}
// SequenceT2 combines two independent ReaderResult computations into a tuple.
//
// Both computations are executed with the same context. If either fails,
// the entire operation fails with the first error encountered.
//
// Type Parameters:
// - A: The first value type
// - B: The second value type
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
//
// Returns:
// - A ReaderResult[Tuple2[A, B]] containing both results
//
// Example:
//
// getUser := readerresult.Right(User{ID: 1})
// getConfig := readerresult.Right(Config{Port: 8080})
// result := readerresult.SequenceT2(getUser, getConfig)
// tuple, err := result(ctx) // Returns (Tuple2{User, Config}, nil)
//
//go:inline
func SequenceT2[A, B any](
a ReaderResult[A],
b ReaderResult[B],
) ReaderResult[T.Tuple2[A, B]] {
return RR.SequenceT2(a, b)
}
// SequenceT3 combines three independent ReaderResult computations into a tuple.
//
// All computations are executed with the same context. If any fails,
// the entire operation fails with the first error encountered.
//
// Type Parameters:
// - A: The first value type
// - B: The second value type
// - C: The third value type
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
// - c: The third ReaderResult
//
// Returns:
// - A ReaderResult[Tuple3[A, B, C]] containing all three results
//
//go:inline
func SequenceT3[A, B, C any](
a ReaderResult[A],
b ReaderResult[B],
c ReaderResult[C],
) ReaderResult[T.Tuple3[A, B, C]] {
return RR.SequenceT3(a, b, c)
}
// SequenceT4 combines four independent ReaderResult computations into a tuple.
//
// All computations are executed with the same context. If any fails,
// the entire operation fails with the first error encountered.
//
// Type Parameters:
// - A: The first value type
// - B: The second value type
// - C: The third value type
// - D: The fourth value type
//
// Parameters:
// - a: The first ReaderResult
// - b: The second ReaderResult
// - c: The third ReaderResult
// - d: The fourth ReaderResult
//
// Returns:
// - A ReaderResult[Tuple4[A, B, C, D]] containing all four results
//
//go:inline
func SequenceT4[A, B, C, D any](
a ReaderResult[A],
b ReaderResult[B],
c ReaderResult[C],
d ReaderResult[D],
) ReaderResult[T.Tuple4[A, B, C, D]] {
return RR.SequenceT4(a, b, c, d)
}

View File

@@ -0,0 +1,57 @@
// 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 readerresult
import (
"context"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/lazy"
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
// Endomorphism represents a function from type A to type A.
Endomorphism[A any] = endomorphism.Endomorphism[A]
// Lazy represents a deferred computation that produces a value of type A when evaluated.
Lazy[A any] = lazy.Lazy[A]
// Option represents an optional value that may or may not be present.
Option[A any] = option.Option[A]
// Either represents a value that can be one of two types: Left (E) or Right (A).
Either[E, A any] = either.Either[E, A]
// Result represents an Either with error as the left type, compatible with Go's (value, error) tuple.
Result[A any] = result.Result[A]
// Reader represents a computation that depends on a read-only environment of type R and produces a value of type A.
Reader[R, A any] = reader.Reader[R, A]
ReaderResult[A any] = func(context.Context) (A, error)
// Monoid represents a monoid structure for ReaderResult values.
Monoid[A any] = monoid.Monoid[ReaderResult[A]]
Kleisli[A, B any] = Reader[A, ReaderResult[B]]
Operator[A, B any] = Kleisli[ReaderResult[A], B]
)

View File

@@ -28,7 +28,7 @@ func TestMkdir(t *testing.T) {
tmpDir := t.TempDir()
newDir := filepath.Join(tmpDir, "testdir")
result := Mkdir(newDir, 0755)
result := Mkdir(newDir, 0o755)
path, err := result()
assert.NoError(t, err)
@@ -43,14 +43,14 @@ func TestMkdir(t *testing.T) {
t.Run("mkdir with existing directory", func(t *testing.T) {
tmpDir := t.TempDir()
result := Mkdir(tmpDir, 0755)
result := Mkdir(tmpDir, 0o755)
_, err := result()
assert.Error(t, err)
})
t.Run("mkdir with parent directory not existing", func(t *testing.T) {
result := Mkdir("/non/existent/parent/child", 0755)
result := Mkdir("/non/existent/parent/child", 0o755)
_, err := result()
assert.Error(t, err)
@@ -62,7 +62,7 @@ func TestMkdirAll(t *testing.T) {
tmpDir := t.TempDir()
nestedDir := filepath.Join(tmpDir, "level1", "level2", "level3")
result := MkdirAll(nestedDir, 0755)
result := MkdirAll(nestedDir, 0o755)
path, err := result()
assert.NoError(t, err)
@@ -88,7 +88,7 @@ func TestMkdirAll(t *testing.T) {
t.Run("mkdirall with existing directory", func(t *testing.T) {
tmpDir := t.TempDir()
result := MkdirAll(tmpDir, 0755)
result := MkdirAll(tmpDir, 0o755)
path, err := result()
// MkdirAll should succeed even if directory exists
@@ -100,7 +100,7 @@ func TestMkdirAll(t *testing.T) {
tmpDir := t.TempDir()
newDir := filepath.Join(tmpDir, "single")
result := MkdirAll(newDir, 0755)
result := MkdirAll(newDir, 0o755)
path, err := result()
assert.NoError(t, err)
@@ -116,11 +116,11 @@ func TestMkdirAll(t *testing.T) {
filePath := filepath.Join(tmpDir, "file.txt")
// Create a file
err := os.WriteFile(filePath, []byte("content"), 0644)
err := os.WriteFile(filePath, []byte("content"), 0o644)
assert.NoError(t, err)
// Try to create a directory where file exists
result := MkdirAll(filepath.Join(filePath, "subdir"), 0755)
result := MkdirAll(filepath.Join(filePath, "subdir"), 0o755)
_, err = result()
assert.Error(t, err)

View File

@@ -34,7 +34,7 @@ func TestOpen(t *testing.T) {
defer os.Remove(tmpPath)
// Write some content
err = os.WriteFile(tmpPath, []byte("test content"), 0644)
err = os.WriteFile(tmpPath, []byte("test content"), 0o644)
require.NoError(t, err)
// Test Open
@@ -127,7 +127,7 @@ func TestWriteFile(t *testing.T) {
testPath := filepath.Join(tmpDir, "write-test.txt")
testData := []byte("test data")
result := WriteFile(testPath, 0644)(testData)
result := WriteFile(testPath, 0o644)(testData)
returnedData, err := result()
assert.NoError(t, err)
@@ -141,7 +141,7 @@ func TestWriteFile(t *testing.T) {
t.Run("write to invalid path", func(t *testing.T) {
testData := []byte("test data")
result := WriteFile("/non/existent/dir/file.txt", 0644)(testData)
result := WriteFile("/non/existent/dir/file.txt", 0o644)(testData)
_, err := result()
assert.Error(t, err)
@@ -155,12 +155,12 @@ func TestWriteFile(t *testing.T) {
defer os.Remove(tmpPath)
// Write initial content
err = os.WriteFile(tmpPath, []byte("initial"), 0644)
err = os.WriteFile(tmpPath, []byte("initial"), 0o644)
require.NoError(t, err)
// Overwrite with new content
newData := []byte("overwritten")
result := WriteFile(tmpPath, 0644)(newData)
result := WriteFile(tmpPath, 0o644)(newData)
returnedData, err := result()
assert.NoError(t, err)
@@ -212,7 +212,7 @@ func TestClose(t *testing.T) {
assert.NoError(t, err)
// Verify file is closed by attempting to write
_, writeErr := tmpFile.Write([]byte("test"))
_, writeErr := tmpFile.WriteString("test")
assert.Error(t, writeErr)
})

View File

@@ -105,7 +105,7 @@ func TestReadAll(t *testing.T) {
largeContent[i] = byte('A' + (i % 26))
}
err := os.WriteFile(testPath, largeContent, 0644)
err := os.WriteFile(testPath, largeContent, 0o644)
require.NoError(t, err)
result := ReadAll(Open(testPath))

View File

@@ -70,7 +70,7 @@ func TestWriteAll(t *testing.T) {
assert.NoError(t, err)
// Verify file is closed by trying to write to it
_, writeErr := capturedFile.Write([]byte("more"))
_, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr)
})
@@ -147,7 +147,7 @@ func TestWrite(t *testing.T) {
useFile := func(f *os.File) IOResult[string] {
return func() (string, error) {
_, err := f.Write([]byte("data"))
_, err := f.WriteString("data")
return "success", err
}
}
@@ -158,7 +158,7 @@ func TestWrite(t *testing.T) {
assert.NoError(t, err)
// Verify file is closed
_, writeErr := capturedFile.Write([]byte("more"))
_, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr)
})
@@ -183,7 +183,7 @@ func TestWrite(t *testing.T) {
assert.Error(t, err)
// Verify file is still closed even on error
_, writeErr := capturedFile.Write([]byte("more"))
_, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr)
})

View File

@@ -45,7 +45,7 @@ func Requester(builder *R.Builder) IOEH.Requester {
withoutBody := F.Curry2(func(url string, method string) IOResult[*http.Request] {
return func() (*http.Request, error) {
req, err := http.NewRequest(method, url, nil)
req, err := http.NewRequest(method, url, http.NoBody)
if err == nil {
H.Monoid.Concat(req.Header, builder.GetHeaders())
}

View File

@@ -23,6 +23,7 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
"github.com/IBM/fp-go/v2/io"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
@@ -111,7 +112,7 @@ func TestChainWithIO(t *testing.T) {
Of("test"),
ChainIOK(func(s string) IO[bool] {
return func() bool {
return len(s) > 0
return S.IsNonEmpty(s)
}
}),
)

View File

@@ -19,6 +19,7 @@ import (
"testing"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
)
// Benchmark shallow chain (1 step)
@@ -100,7 +101,7 @@ func BenchmarkChain_RealWorld_Validation(b *testing.B) {
// Step 1: Validate not empty
v1, ok1 := Chain(func(s string) (string, bool) {
if len(s) > 0 {
if S.IsNonEmpty(s) {
return s, true
}
return "", false

View File

@@ -24,6 +24,7 @@ import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
I "github.com/IBM/fp-go/v2/iterator/iter"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
@@ -225,7 +226,7 @@ func TestTraverseIter_ComplexTransformation(t *testing.T) {
}
validatePerson := func(name string) (Person, bool) {
if name == "" {
if S.IsEmpty(name) {
return None[Person]()
}
return Some(Person{Name: name, Age: len(name)})

View File

@@ -21,7 +21,7 @@ import (
L "github.com/IBM/fp-go/v2/logging"
)
func _log[A any](left func(string, ...any), right func(string, ...any), prefix string) Operator[A, A] {
func _log[A any](left, right func(string, ...any), prefix string) Operator[A, A] {
return func(a A, aok bool) (A, bool) {
if aok {
right("%s: %v", prefix, a)

View File

@@ -0,0 +1,261 @@
// 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 readerioresult
import (
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/IBM/fp-go/v2/internal/readert"
"github.com/IBM/fp-go/v2/reader"
)
// Sequence swaps the order of nested environment parameters in a ReaderIOResult computation.
//
// This function transforms a computation that takes environment R2 and produces a ReaderIOResult[R1, A]
// into a Kleisli arrow that takes R1 first and returns a ReaderIOResult[R2, A].
//
// Type Parameters:
// - R1: The type of the inner environment (becomes the outer parameter after sequencing)
// - R2: The type of the outer environment (becomes the inner environment after sequencing)
// - A: The type of the value produced by the computation
//
// Parameters:
// - ma: A ReaderIOResult that depends on R2 and produces a ReaderIOResult[R1, A]
//
// Returns:
// - A Kleisli arrow (func(R1) func(R2) func() (A, error)) that reverses the environment order
//
// The transformation preserves error handling - if the outer computation fails, the error
// is propagated; if the inner computation fails, that error is also propagated.
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// type Database struct {
// ConnectionString string
// }
// type Config struct {
// Timeout int
// }
//
// // Original: takes Config, produces ReaderIOResult[Database, string]
// original := func(cfg Config) func() (func(Database) func() (string, error), error) {
// return func() (func(Database) func() (string, error), error) {
// if cfg.Timeout <= 0 {
// return nil, errors.New("invalid timeout")
// }
// return func(db Database) func() (string, error) {
// return func() (string, error) {
// if S.IsEmpty(db.ConnectionString) {
// return "", errors.New("empty connection")
// }
// return fmt.Sprintf("Query on %s with timeout %d",
// db.ConnectionString, cfg.Timeout), nil
// }
// }, nil
// }
// }
//
// // Sequenced: takes Database first, then Config
// sequenced := Sequence(original)
// db := Database{ConnectionString: "localhost:5432"}
// cfg := Config{Timeout: 30}
// result, err := sequenced(db)(cfg)()
// // result: "Query on localhost:5432 with timeout 30"
func Sequence[R1, R2, A any](ma ReaderIOResult[R2, ReaderIOResult[R1, A]]) reader.Kleisli[R2, R1, IOResult[A]] {
return readert.Sequence(
ioresult.Chain,
ma,
)
}
// SequenceReader swaps the order of environment parameters when the inner computation is a pure Reader.
//
// This function is similar to Sequence but specialized for cases where the inner computation
// is a Reader (pure function) rather than a ReaderIOResult. It transforms a ReaderIOResult that
// produces a Reader into a Kleisli arrow with swapped environment order.
//
// Type Parameters:
// - R1: The type of the Reader's environment (becomes the outer parameter after sequencing)
// - R2: The type of the ReaderIOResult's environment (becomes the inner environment after sequencing)
// - A: The type of the value produced by the computation
//
// Parameters:
// - ma: A ReaderIOResult[R2, Reader[R1, A]] - depends on R2 and produces a pure Reader
//
// Returns:
// - A Kleisli arrow (func(R1) func(R2) func() (A, error)) that reverses the environment order
//
// The inner Reader computation is automatically lifted into the IOResult context (cannot fail).
// Only the outer ReaderIOResult can fail with an error.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // Original: takes int, produces Reader[Config, int]
// original := func(x int) func() (func(Config) int, error) {
// return func() (func(Config) int, error) {
// if x < 0 {
// return nil, errors.New("negative value")
// }
// return func(cfg Config) int {
// return x * cfg.Multiplier
// }, nil
// }
// }
//
// // Sequenced: takes Config first, then int
// sequenced := SequenceReader(original)
// cfg := Config{Multiplier: 5}
// result, err := sequenced(cfg)(10)()
// // result: 50, err: nil
func SequenceReader[R1, R2, A any](ma ReaderIOResult[R2, Reader[R1, A]]) reader.Kleisli[R2, R1, IOResult[A]] {
return readert.SequenceReader(
ioresult.Map,
ma,
)
}
// Traverse transforms a ReaderIOResult computation by applying a Kleisli arrow that introduces
// a new environment dependency, effectively swapping the environment order.
//
// This is a higher-order function that takes a Kleisli arrow and returns a function that
// can transform ReaderIOResult computations. It's useful for introducing environment-dependent
// transformations into existing computations while reordering the environment parameters.
//
// Type Parameters:
// - R2: The type of the original computation's environment
// - R1: The type of the new environment introduced by the Kleisli arrow
// - A: The input type to the Kleisli arrow
// - B: The output type of the transformation
//
// Parameters:
// - f: A Kleisli arrow (func(A) ReaderIOResult[R1, B]) that transforms A to B with R1 dependency
//
// Returns:
// - A function that transforms ReaderIOResult[R2, A] into a Kleisli arrow with swapped environments
//
// The transformation preserves error handling from both the original computation and the
// Kleisli arrow. The resulting computation takes R1 first, then R2.
//
// Example:
//
// type Database struct {
// Prefix string
// }
//
// // Original computation: depends on int environment
// original := func(x int) func() (int, error) {
// return func() (int, error) {
// if x < 0 {
// return 0, errors.New("negative value")
// }
// return x * 2, nil
// }
// }
//
// // Kleisli arrow: transforms int to string with Database dependency
// format := func(value int) func(Database) func() (string, error) {
// return func(db Database) func() (string, error) {
// return func() (string, error) {
// return fmt.Sprintf("%s:%d", db.Prefix, value), nil
// }
// }
// }
//
// // Apply Traverse
// traversed := Traverse[int](format)
// result := traversed(original)
//
// // Use with Database first, then int
// db := Database{Prefix: "ID"}
// output, err := result(db)(10)()
// // output: "ID:20", err: nil
func Traverse[R2, R1, A, B any](
f Kleisli[R1, A, B],
) func(ReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
return readert.Traverse[ReaderIOResult[R2, A]](
ioresult.Map,
ioresult.Chain,
f,
)
}
// TraverseReader transforms a ReaderIOResult computation by applying a Reader-based Kleisli arrow,
// introducing a new environment dependency while swapping the environment order.
//
// This function is similar to Traverse but specialized for pure Reader transformations that
// cannot fail. It's useful when you want to introduce environment-dependent logic without
// adding error handling complexity.
//
// Type Parameters:
// - R2: The type of the original computation's environment
// - R1: The type of the new environment introduced by the Reader Kleisli arrow
// - A: The input type to the Kleisli arrow
// - B: The output type of the transformation
//
// Parameters:
// - f: A Reader Kleisli arrow (func(A) func(R1) B) that transforms A to B with R1 dependency
//
// Returns:
// - A function that transforms ReaderIOResult[R2, A] into a Kleisli arrow with swapped environments
//
// The Reader transformation is automatically lifted into the IOResult context. Only the original
// ReaderIOResult computation can fail; the Reader transformation itself is pure and cannot fail.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // Original computation: depends on int environment, may fail
// original := func(x int) func() (int, error) {
// return func() (int, error) {
// if x < 0 {
// return 0, errors.New("negative value")
// }
// return x * 2, nil
// }
// }
//
// // Pure Reader transformation: multiplies by config value
// multiply := func(value int) func(Config) int {
// return func(cfg Config) int {
// return value * cfg.Multiplier
// }
// }
//
// // Apply TraverseReader
// traversed := TraverseReader[int, Config](multiply)
// result := traversed(original)
//
// // Use with Config first, then int
// cfg := Config{Multiplier: 5}
// output, err := result(cfg)(10)()
// // output: 100 (10 * 2 * 5), err: nil
func TraverseReader[R2, R1, A, B any](
f reader.Kleisli[R1, A, B],
) func(ReaderIOResult[R2, A]) Kleisli[R2, R1, B] {
return readert.TraverseReader[ReaderIOResult[R2, A]](
ioresult.Map,
ioresult.Map,
f,
)
}

View File

@@ -0,0 +1,866 @@
// 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 readerioresult
import (
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestSequence(t *testing.T) {
t.Run("sequences parameter order for simple types", func(t *testing.T) {
// Original: takes int, returns ReaderIOResult[string, int]
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(s string) IOResult[int] {
return func() (int, error) {
return x + len(s), nil
}
}, nil
}
}
// Sequenced: takes string first, then int
sequenced := Sequence(original)
// Test original
innerFunc1, err1 := original(10)()
assert.NoError(t, err1)
result1, err2 := innerFunc1("hello")()
assert.NoError(t, err2)
assert.Equal(t, 15, result1)
// Test sequenced
result2, err3 := sequenced("hello")(10)()
assert.NoError(t, err3)
assert.Equal(t, 15, result2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
if x < 0 {
return nil, expectedError
}
return func(s string) IOResult[int] {
return func() (int, error) {
return x + len(s), nil
}
}, nil
}
}
sequenced := Sequence(original)
// Test with error
_, err := sequenced("test")(-1)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("preserves inner error", func(t *testing.T) {
expectedError := errors.New("inner error")
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
return func(s string) IOResult[int] {
return func() (int, error) {
if S.IsEmpty(s) {
return 0, expectedError
}
return x + len(s), nil
}
}, nil
}
}
sequenced := Sequence(original)
// Test with inner error
_, err := sequenced("")(10)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string
original := func(x int) IOResult[ReaderIOResult[string, string]] {
return func() (ReaderIOResult[string, string], error) {
return func(prefix string) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("%s-%d", prefix, x), nil
}
}, nil
}
}
sequenced := Sequence(original)
result, err := sequenced("ID")(42)()
assert.NoError(t, err)
assert.Equal(t, "ID-42", result)
})
t.Run("works with struct environments", func(t *testing.T) {
type Database struct {
ConnectionString string
}
type Config struct {
Timeout int
}
original := func(cfg Config) IOResult[ReaderIOResult[Database, string]] {
return func() (ReaderIOResult[Database, string], error) {
if cfg.Timeout <= 0 {
return nil, errors.New("invalid timeout")
}
return func(db Database) IOResult[string] {
return func() (string, error) {
if S.IsEmpty(db.ConnectionString) {
return "", errors.New("empty connection string")
}
return fmt.Sprintf("Query on %s with timeout %d",
db.ConnectionString, cfg.Timeout), nil
}
}, nil
}
}
sequenced := Sequence(original)
db := Database{ConnectionString: "localhost:5432"}
cfg := Config{Timeout: 30}
result, err := sequenced(db)(cfg)()
assert.NoError(t, err)
assert.Equal(t, "Query on localhost:5432 with timeout 30", result)
})
t.Run("works with zero values", func(t *testing.T) {
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
return func(s string) IOResult[int] {
return func() (int, error) {
return x + len(s), nil
}
}, nil
}
}
sequenced := Sequence(original)
result, err := sequenced("")(0)()
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
t.Run("handles IO side effects correctly", func(t *testing.T) {
counter := 0
original := func(x int) IOResult[ReaderIOResult[string, int]] {
return func() (ReaderIOResult[string, int], error) {
counter++ // Side effect in outer IO
return func(s string) IOResult[int] {
return func() (int, error) {
counter++ // Side effect in inner IO
return x + len(s), nil
}
}, nil
}
}
sequenced := Sequence(original)
result, err := sequenced("test")(5)()
assert.NoError(t, err)
assert.Equal(t, 9, result)
assert.Equal(t, 2, counter) // Both side effects executed
})
}
func TestSequenceReader(t *testing.T) {
t.Run("sequences parameter order for Reader inner type", func(t *testing.T) {
// Original: takes int, returns Reader[string, int]
original := func(x int) IOResult[reader.Reader[string, int]] {
return func() (reader.Reader[string, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(s string) int {
return x + len(s)
}, nil
}
}
// Sequenced: takes string first, then int
sequenced := SequenceReader(original)
// Test original
readerFunc, err1 := original(10)()
assert.NoError(t, err1)
value1 := readerFunc("hello")
assert.Equal(t, 15, value1)
// Test sequenced
value2, err2 := sequenced("hello")(10)()
assert.NoError(t, err2)
assert.Equal(t, 15, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) IOResult[reader.Reader[string, int]] {
return func() (reader.Reader[string, int], error) {
if x < 0 {
return nil, expectedError
}
return func(s string) int {
return x + len(s)
}, nil
}
}
sequenced := SequenceReader(original)
// Test with error
_, err := sequenced("test")(-1)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string using Reader
original := func(x int) IOResult[reader.Reader[string, string]] {
return func() (reader.Reader[string, string], error) {
return func(prefix string) string {
return fmt.Sprintf("%s-%d", prefix, x)
}, nil
}
}
sequenced := SequenceReader(original)
result, err := sequenced("ID")(42)()
assert.NoError(t, err)
assert.Equal(t, "ID-42", result)
})
t.Run("works with struct environments", func(t *testing.T) {
type Config struct {
Multiplier int
}
original := func(x int) IOResult[reader.Reader[Config, int]] {
return func() (reader.Reader[Config, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(cfg Config) int {
return x * cfg.Multiplier
}, nil
}
}
sequenced := SequenceReader(original)
cfg := Config{Multiplier: 5}
result, err := sequenced(cfg)(10)()
assert.NoError(t, err)
assert.Equal(t, 50, result)
})
t.Run("works with zero values", func(t *testing.T) {
original := func(x int) IOResult[reader.Reader[string, int]] {
return func() (reader.Reader[string, int], error) {
return func(s string) int {
return x + len(s)
}, nil
}
}
sequenced := SequenceReader(original)
result, err := sequenced("")(0)()
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
t.Run("handles IO side effects correctly", func(t *testing.T) {
counter := 0
original := func(x int) IOResult[reader.Reader[string, int]] {
return func() (reader.Reader[string, int], error) {
counter++ // Side effect in IO
return func(s string) int {
return x + len(s)
}, nil
}
}
sequenced := SequenceReader(original)
result, err := sequenced("test")(5)()
assert.NoError(t, err)
assert.Equal(t, 9, result)
assert.Equal(t, 1, counter) // Side effect executed
})
}
func TestTraverse(t *testing.T) {
t.Run("basic transformation with environment swap", func(t *testing.T) {
// Original: ReaderIOResult[int, int] - takes int environment, produces int
original := func(x int) IOResult[int] {
return func() (int, error) {
if x < 0 {
return 0, errors.New("negative value")
}
return x * 2, nil
}
}
// Kleisli function: func(int) ReaderIOResult[string, int]
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
return a + len(s), nil
}
}
}
// Traverse returns: func(ReaderIOResult[int, int]) func(string) ReaderIOResult[int, int]
traversed := Traverse[int](kleisli)
result := traversed(original)
// result is func(string) ReaderIOResult[int, int]
// Provide string first ("hello"), then int (10)
value, err := result("hello")(10)()
assert.NoError(t, err)
assert.Equal(t, 25, value) // (10 * 2) + len("hello") = 20 + 5 = 25
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) IOResult[int] {
return func() (int, error) {
if x < 0 {
return 0, expectedError
}
return x, nil
}
}
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
return a + len(s), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Test with negative value to trigger error
_, err := result("test")(-1)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("preserves inner error from Kleisli", func(t *testing.T) {
expectedError := errors.New("inner error")
original := Ask[int]()
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
if S.IsEmpty(s) {
return 0, expectedError
}
return a + len(s), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Test with empty string to trigger inner error
_, err := result("")(10)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string using environment-dependent logic
original := Ask[int]()
kleisli := func(a int) ReaderIOResult[string, string] {
return func(prefix string) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("%s-%d", prefix, a), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("ID")(42)()
assert.NoError(t, err)
assert.Equal(t, "ID-42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Config struct {
Multiplier int
}
type Database struct {
Prefix string
}
original := func(cfg Config) IOResult[int] {
return func() (int, error) {
if cfg.Multiplier <= 0 {
return 0, errors.New("invalid multiplier")
}
return 10 * cfg.Multiplier, nil
}
}
kleisli := func(value int) ReaderIOResult[Database, string] {
return func(db Database) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("%s:%d", db.Prefix, value), nil
}
}
}
traversed := Traverse[Config](kleisli)
result := traversed(original)
cfg := Config{Multiplier: 5}
db := Database{Prefix: "result"}
value, err := result(db)(cfg)()
assert.NoError(t, err)
assert.Equal(t, "result:50", value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
original := Ask[int]()
// First transformation: multiply by environment value
kleisli1 := func(a int) ReaderIOResult[int, int] {
return func(multiplier int) IOResult[int] {
return func() (int, error) {
return a * multiplier, nil
}
}
}
traversed := Traverse[int](kleisli1)
result := traversed(original)
value, err := result(3)(5)()
assert.NoError(t, err)
assert.Equal(t, 15, value) // 5 * 3 = 15
})
t.Run("works with zero values", func(t *testing.T) {
original := Ask[int]()
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
return a + len(s), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("")(0)()
assert.NoError(t, err)
assert.Equal(t, 0, value)
})
t.Run("enables partial application", func(t *testing.T) {
original := Ask[int]()
kleisli := func(a int) ReaderIOResult[int, int] {
return func(factor int) IOResult[int] {
return func() (int, error) {
return a * factor, nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Partially apply factor
withFactor := result(3)
// Can now use with different inputs
value1, err1 := withFactor(10)()
assert.NoError(t, err1)
assert.Equal(t, 30, value1)
// Reuse with different input
value2, err2 := withFactor(20)()
assert.NoError(t, err2)
assert.Equal(t, 60, value2)
})
t.Run("handles IO side effects correctly", func(t *testing.T) {
counter := 0
original := func(x int) IOResult[int] {
return func() (int, error) {
counter++ // Side effect in outer IO
return x * 2, nil
}
}
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
counter++ // Side effect in inner IO
return a + len(s), nil
}
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("test")(5)()
assert.NoError(t, err)
assert.Equal(t, 14, value) // (5 * 2) + 4 = 14
assert.Equal(t, 2, counter) // Both side effects executed
})
}
func TestTraverseReader(t *testing.T) {
t.Run("basic transformation with Reader dependency", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := F.Pipe1(
Ask[int](),
Map[int](N.Mul(2)),
)
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config first, then int
cfg := Config{Multiplier: 5}
value, err := result(cfg)(10)()
assert.NoError(t, err)
assert.Equal(t, 100, value) // (10 * 2) * 5 = 100
})
t.Run("preserves outer error", func(t *testing.T) {
type Config struct {
Multiplier int
}
expectedError := errors.New("outer error")
// Original computation that fails
original := func(x int) IOResult[int] {
return func() (int, error) {
if x < 0 {
return 0, expectedError
}
return x, nil
}
}
// Reader-based transformation (won't be called)
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config and negative value
cfg := Config{Multiplier: 5}
_, err := result(cfg)(-1)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
type Database struct {
Prefix string
}
// Original computation producing an int
original := Ask[int]()
// Reader-based transformation: int -> string using Database
format := func(a int) func(Database) string {
return func(db Database) string {
return fmt.Sprintf("%s:%d", db.Prefix, a)
}
}
// Apply TraverseReader
traversed := TraverseReader[int](format)
result := traversed(original)
// Provide Database first, then int
db := Database{Prefix: "ID"}
value, err := result(db)(42)()
assert.NoError(t, err)
assert.Equal(t, "ID:42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Settings struct {
Prefix string
Suffix string
}
type Context struct {
Value int
}
// Original computation
original := func(ctx Context) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("value:%d", ctx.Value), nil
}
}
// Reader-based transformation using Settings
decorate := func(s string) func(Settings) string {
return func(settings Settings) string {
return settings.Prefix + s + settings.Suffix
}
}
// Apply TraverseReader
traversed := TraverseReader[Context](decorate)
result := traversed(original)
// Provide Settings first, then Context
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := Context{Value: 100}
value, err := result(settings)(ctx)()
assert.NoError(t, err)
assert.Equal(t, "[value:100]", value)
})
t.Run("enables partial application", func(t *testing.T) {
type Config struct {
Factor int
}
// Original computation
original := Ask[int]()
// Reader-based transformation
scale := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Factor
}
}
// Apply TraverseReader
traversed := TraverseReader[int](scale)
result := traversed(original)
// Partially apply Config
cfg := Config{Factor: 3}
withConfig := result(cfg)
// Can now use with different inputs
value1, err1 := withConfig(10)()
assert.NoError(t, err1)
assert.Equal(t, 30, value1)
// Reuse with different input
value2, err2 := withConfig(20)()
assert.NoError(t, err2)
assert.Equal(t, 60, value2)
})
t.Run("works with zero values", func(t *testing.T) {
type Config struct {
Offset int
}
// Original computation with zero value
original := Ask[int]()
// Reader-based transformation
add := func(a int) func(Config) int {
return func(cfg Config) int {
return a + cfg.Offset
}
}
// Apply TraverseReader
traversed := TraverseReader[int](add)
result := traversed(original)
// Provide Config with zero offset and zero input
cfg := Config{Offset: 0}
value, err := result(cfg)(0)()
assert.NoError(t, err)
assert.Equal(t, 0, value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := func(x int) IOResult[int] {
return func() (int, error) {
return x * 2, nil
}
}
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 4}
value, err := result(cfg)(5)()
assert.NoError(t, err)
assert.Equal(t, 40, value) // (5 * 2) * 4 = 40
})
t.Run("works with complex Reader logic", func(t *testing.T) {
type ValidationRules struct {
MinValue int
MaxValue int
}
// Original computation
original := Ask[int]()
// Reader-based transformation with validation logic
validate := func(a int) func(ValidationRules) int {
return func(rules ValidationRules) int {
if a < rules.MinValue {
return rules.MinValue
}
if a > rules.MaxValue {
return rules.MaxValue
}
return a
}
}
// Apply TraverseReader
traversed := TraverseReader[int](validate)
result := traversed(original)
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
value1, err1 := result(rules1)(50)()
assert.NoError(t, err1)
assert.Equal(t, 50, value1)
// Test with value above max
rules2 := ValidationRules{MinValue: 0, MaxValue: 30}
value2, err2 := result(rules2)(50)()
assert.NoError(t, err2)
assert.Equal(t, 30, value2) // Clamped to max
// Test with value below min
rules3 := ValidationRules{MinValue: 60, MaxValue: 100}
value3, err3 := result(rules3)(50)()
assert.NoError(t, err3)
assert.Equal(t, 60, value3) // Clamped to min
})
t.Run("handles IO side effects correctly", func(t *testing.T) {
type Config struct {
Multiplier int
}
counter := 0
// Original computation with side effect
original := func(x int) IOResult[int] {
return func() (int, error) {
counter++ // Side effect
return x * 2, nil
}
}
// Reader-based transformation (pure, no side effects)
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
cfg := Config{Multiplier: 5}
value, err := result(cfg)(10)()
assert.NoError(t, err)
assert.Equal(t, 100, value)
assert.Equal(t, 1, counter) // Side effect executed once
})
}

View File

@@ -0,0 +1,987 @@
// 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 readerioresult
import (
"github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/IBM/fp-go/v2/internal/chain"
"github.com/IBM/fp-go/v2/internal/fromio"
"github.com/IBM/fp-go/v2/internal/fromreader"
"github.com/IBM/fp-go/v2/internal/functor"
"github.com/IBM/fp-go/v2/io"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
)
// FromIOResult lifts an IOResult into a ReaderIOResult context.
// The resulting computation ignores the environment parameter and directly executes the IOResult.
//
// Type Parameters:
// - R: The type of the environment (ignored by the computation)
// - A: The type of the success value
//
// Parameters:
// - ma: The IOResult to lift
//
// Returns:
// - A ReaderIOResult that executes the IOResult regardless of the environment
//
// Example:
//
// ioResult := func() (int, error) { return 42, nil }
// readerIOResult := FromIOResult[Config](ioResult)
// result, err := readerIOResult(cfg)() // Returns 42, nil
//
//go:inline
func FromIOResult[R, A any](ma IOResult[A]) ReaderIOResult[R, A] {
return reader.Of[R](ma)
}
// RightIO lifts an IO computation into a ReaderIOResult as a successful value.
// The IO computation always succeeds, so it's wrapped in the Right (success) side.
//
// Type Parameters:
// - R: The type of the environment (ignored by the computation)
// - A: The type of the value produced by the IO
//
// Parameters:
// - ma: The IO computation to lift
//
// Returns:
// - A ReaderIOResult that executes the IO and wraps the result as a success
//
// Example:
//
// getCurrentTime := func() time.Time { return time.Now() }
// readerIOResult := RightIO[Config](getCurrentTime)
// result, err := readerIOResult(cfg)() // Returns current time, nil
func RightIO[R, A any](ma IO[A]) ReaderIOResult[R, A] {
return function.Pipe2(ma, ioresult.RightIO[A], FromIOResult[R, A])
}
// LeftIO lifts an IO computation that produces an error into a ReaderIOResult as a failure.
// The IO computation produces an error, which is wrapped in the Left (error) side.
//
// Type Parameters:
// - R: The type of the environment (ignored by the computation)
// - A: The type of the success value (never produced)
//
// Parameters:
// - ma: The IO computation that produces an error
//
// Returns:
// - A ReaderIOResult that executes the IO and wraps the error as a failure
//
// Example:
//
// getError := func() error { return errors.New("something went wrong") }
// readerIOResult := LeftIO[Config, int](getError)
// _, err := readerIOResult(cfg)() // Returns error
func LeftIO[R, A any](ma IO[error]) ReaderIOResult[R, A] {
return function.Pipe2(ma, ioresult.LeftIO[A], FromIOResult[R, A])
}
// FromIO lifts an IO computation into a ReaderIOResult context.
// This is an alias for RightIO - the IO computation always succeeds.
//
// Type Parameters:
// - R: The type of the environment (ignored by the computation)
// - E: Unused type parameter (kept for compatibility)
// - A: The type of the value produced by the IO
//
// Parameters:
// - ma: The IO computation to lift
//
// Returns:
// - A ReaderIOResult that executes the IO and wraps the result as a success
//
//go:inline
func FromIO[R, E, A any](ma IO[A]) ReaderIOResult[R, A] {
return RightIO[R](ma)
}
// FromReaderIO lifts a ReaderIO into a ReaderIOResult context.
// The ReaderIO computation always succeeds, so it's wrapped in the Right (success) side.
// This is an alias for RightReaderIO.
//
// Type Parameters:
// - R: The type of the environment
// - A: The type of the value produced
//
// Parameters:
// - ma: The ReaderIO to lift
//
// Returns:
// - A ReaderIOResult that executes the ReaderIO and wraps the result as a success
//
// Example:
//
// getConfigValue := func(cfg Config) func() int {
// return func() int { return cfg.Timeout }
// }
// readerIOResult := FromReaderIO(getConfigValue)
// result, err := readerIOResult(cfg)() // Returns cfg.Timeout, nil
//
//go:inline
func FromReaderIO[R, A any](ma ReaderIO[R, A]) ReaderIOResult[R, A] {
return RightReaderIO(ma)
}
// RightReaderIO lifts a ReaderIO into a ReaderIOResult as a successful value.
// The ReaderIO computation always succeeds, so it's wrapped in the Right (success) side.
//
// Type Parameters:
// - R: The type of the environment
// - A: The type of the value produced
//
// Parameters:
// - ma: The ReaderIO to lift
//
// Returns:
// - A ReaderIOResult that executes the ReaderIO and wraps the result as a success
//
// Example:
//
// logMessage := func(cfg Config) func() string {
// return func() string {
// log.Printf("Processing with timeout: %d", cfg.Timeout)
// return "logged"
// }
// }
// readerIOResult := RightReaderIO(logMessage)
func RightReaderIO[R, A any](ma ReaderIO[R, A]) ReaderIOResult[R, A] {
return function.Flow2(
ma,
ioresult.FromIO,
)
}
// MonadMap transforms the success value of a ReaderIOResult using the provided function.
// If the computation fails, the error is propagated unchanged.
//
// Type Parameters:
// - R: The type of the environment
// - A: The input type
// - B: The output type
//
// Parameters:
// - fa: The ReaderIOResult to transform
// - f: The transformation function
//
// Returns:
// - A new ReaderIOResult with the transformed value
//
// Example:
//
// getValue := Right[Config](10)
// doubled := MonadMap(getValue, func(x int) int { return x * 2 })
// result, err := doubled(cfg)() // Returns 20, nil
func MonadMap[R, A, B any](fa ReaderIOResult[R, A], f func(A) B) ReaderIOResult[R, B] {
return function.Flow2(
fa,
ioresult.Map(f),
)
}
// Map transforms the success value of a ReaderIOResult using the provided function.
// This is the curried version of MonadMap, useful for composition in pipelines.
//
// Type Parameters:
// - R: The type of the environment
// - A: The input type
// - B: The output type
//
// Parameters:
// - f: The transformation function
//
// Returns:
// - A function that transforms a ReaderIOResult
//
// Example:
//
// double := Map[Config](func(x int) int { return x * 2 })
// getValue := Right[Config](10)
// result := F.Pipe1(getValue, double)
// value, err := result(cfg)() // Returns 20, nil
func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
mp := ioresult.Map(f)
return func(ri ReaderIOResult[R, A]) ReaderIOResult[R, B] {
return function.Flow2(
ri,
mp,
)
}
}
// MonadMapTo replaces the success value with a constant value.
// Useful when you want to discard the result but keep the effect.
func MonadMapTo[R, A, B any](fa ReaderIOResult[R, A], b B) ReaderIOResult[R, B] {
return MonadMap(fa, function.Constant1[A](b))
}
// MapTo returns a function that replaces the success value with a constant.
// This is the curried version of MonadMapTo.
func MapTo[R, A, B any](b B) Operator[R, A, B] {
return Map[R](function.Constant1[A](b))
}
//go:inline
func MonadChain[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIOResult[R, B] {
return func(r R) IOResult[B] {
return function.Pipe1(
fa(r),
ioresult.Chain(func(a A) IOResult[B] {
return f(a)(r)
}),
)
}
}
//go:inline
func MonadChainFirst[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIOResult[R, A] {
return chain.MonadChainFirst(
MonadChain[R, A, A],
MonadMap[R, B, A],
fa,
f)
}
//go:inline
func MonadTap[R, A, B any](fa ReaderIOResult[R, A], f Kleisli[R, A, B]) ReaderIOResult[R, A] {
return MonadChainFirst(fa, f)
}
// // MonadChainEitherK chains a computation that returns an Either into a ReaderIOResult.
// // The Either is automatically lifted into the ReaderIOResult context.
// //
// //go:inline
// func MonadChainEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[A, B]) ReaderIOResult[R, B] {
// return fromeither.MonadChainEitherK(
// MonadChain[R, A, B],
// FromEither[R, B],
// ma,
// f,
// )
// }
// // ChainEitherK returns a function that chains an Either-returning function into ReaderIOResult.
// // This is the curried version of MonadChainEitherK.
// //
// //go:inline
// func ChainEitherK[R, A, B any](f either.Kleisli[A, B]) Operator[R, A, B] {
// return fromeither.ChainEitherK(
// Chain[R, A, B],
// FromEither[R, B],
// f,
// )
// }
// // MonadChainFirstEitherK chains an Either-returning computation but keeps the original value.
// // Useful for validation or side effects that return Either.
// //
// //go:inline
// func MonadChainFirstEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[A, B]) ReaderIOResult[R, A] {
// return fromeither.MonadChainFirstEitherK(
// MonadChain[R, A, A],
// MonadMap[R, B, A],
// FromEither[R, B],
// ma,
// f,
// )
// }
// //go:inline
// func MonadTapEitherK[R, A, B any](ma ReaderIOResult[R, A], f either.Kleisli[A, B]) ReaderIOResult[R, 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, A, B any](f either.Kleisli[A, B]) Operator[R, A, A] {
// return fromeither.ChainFirstEitherK(
// Chain[R, A, A],
// Map[R, B, A],
// FromEither[R, B],
// f,
// )
// }
// //go:inline
// func TapEitherK[R, A, B any](f either.Kleisli[A, B]) Operator[R, A, A] {
// return ChainFirstEitherK[R](f)
// }
// MonadChainReaderK chains a Reader-returning computation into a ReaderIOResult.
// The Reader is automatically lifted into the ReaderIOResult context.
//
//go:inline
func MonadChainReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, A, B],
FromReader[R, B],
ma,
f,
)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
return fromreader.ChainReaderK(
Chain[R, A, B],
FromReader[R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, A, B],
FromReader[R, B],
ma,
f,
)
}
//go:inline
func MonadTapReaderK[R, A, B any](ma ReaderIOResult[R, A], f reader.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return MonadChainFirstReaderK(ma, f)
}
// ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// This is the curried version of MonadChainReaderK.
//
//go:inline
func ChainFirstReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, A, B],
FromReader[R, B],
f,
)
}
//go:inline
func TapReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderK(f)
}
//go:inline
func MonadChainReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, B] {
return fromreader.MonadChainReaderK(
MonadChain[R, A, B],
FromReaderIO[R, B],
ma,
f,
)
}
//go:inline
func ChainReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, B] {
return fromreader.ChainReaderK(
Chain[R, A, B],
FromReaderIO[R, B],
f,
)
}
//go:inline
func MonadChainFirstReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return fromreader.MonadChainFirstReaderK(
MonadChainFirst[R, A, B],
FromReaderIO[R, B],
ma,
f,
)
}
//go:inline
func MonadTapReaderIOK[R, A, B any](ma ReaderIOResult[R, A], f readerio.Kleisli[R, A, B]) ReaderIOResult[R, A] {
return MonadChainFirstReaderIOK(ma, f)
}
//go:inline
func ChainFirstReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return fromreader.ChainFirstReaderK(
ChainFirst[R, A, B],
FromReaderIO[R, B],
f,
)
}
//go:inline
func TapReaderIOK[R, A, B any](f readerio.Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirstReaderIOK(f)
}
// //go:inline
// func MonadChainReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, A, B]) ReaderIOResult[R, B] {
// return fromreader.MonadChainReaderK(
// MonadChain[R, A, B],
// FromReaderEither[R, B],
// ma,
// f,
// )
// }
// // ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// // This is the curried version of MonadChainReaderK.
// //
// //go:inline
// func ChainReaderEitherK[R, A, B any](f RE.Kleisli[R, A, B]) Operator[R, A, B] {
// return fromreader.ChainReaderK(
// Chain[R, A, B],
// FromReaderEither[R, B],
// f,
// )
// }
// //go:inline
// func MonadChainFirstReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, A, B]) ReaderIOResult[R, A] {
// return fromreader.MonadChainFirstReaderK(
// MonadChainFirst[R, A, B],
// FromReaderEither[R, B],
// ma,
// f,
// )
// }
// //go:inline
// func MonadTapReaderEitherK[R, A, B any](ma ReaderIOResult[R, A], f RE.Kleisli[R, A, B]) ReaderIOResult[R, A] {
// return MonadChainFirstReaderEitherK(ma, f)
// }
// // ChainReaderK returns a function that chains a Reader-returning function into ReaderIOResult.
// // This is the curried version of MonadChainReaderK.
// //
// //go:inline
// func ChainFirstReaderEitherK[R, A, B any](f RE.Kleisli[R, A, B]) Operator[R, A, A] {
// return fromreader.ChainFirstReaderK(
// ChainFirst[R, A, B],
// FromReaderEither[R, B],
// f,
// )
// }
// //go:inline
// func TapReaderEitherK[R, A, B any](f RE.Kleisli[R, A, B]) Operator[R, A, A] {
// return ChainFirstReaderEitherK(f)
// }
// //go:inline
// func ChainReaderOptionK[R, A, B any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
// fro := FromReaderOption[R, B](onNone)
// return func(f readeroption.Kleisli[R, A, B]) Operator[R, A, B] {
// return fromreader.ChainReaderK(
// Chain[R, A, B],
// fro,
// f,
// )
// }
// }
// //go:inline
// func ChainFirstReaderOptionK[R, A, B any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
// fro := FromReaderOption[R, B](onNone)
// return func(f readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
// return fromreader.ChainFirstReaderK(
// ChainFirst[R, A, B],
// fro,
// f,
// )
// }
// }
// //go:inline
// func TapReaderOptionK[R, A, B any](onNone func() E) func(readeroption.Kleisli[R, A, B]) Operator[R, A, A] {
// return ChainFirstReaderOptionK[R, A, B](onNone)
// }
// // MonadChainIOEitherK chains an IOEither-returning computation into a ReaderIOResult.
// // The IOEither is automatically lifted into the ReaderIOResult context.
// //
// //go:inline
// func MonadChainIOEitherK[R, A, B any](ma ReaderIOResult[R, A], f IOE.Kleisli[A, B]) ReaderIOResult[R, B] {
// return fromioeither.MonadChainIOEitherK(
// MonadChain[R, A, B],
// FromIOEither[R, B],
// ma,
// f,
// )
// }
// // ChainIOEitherK returns a function that chains an IOEither-returning function into ReaderIOResult.
// // This is the curried version of MonadChainIOEitherK.
// //
// //go:inline
// func ChainIOEitherK[R, A, B any](f IOE.Kleisli[A, B]) Operator[R, A, B] {
// return fromioeither.ChainIOEitherK(
// Chain[R, A, B],
// FromIOEither[R, B],
// f,
// )
// }
// MonadChainIOK chains an IO-returning computation into a ReaderIOResult.
// The IO is automatically lifted into the ReaderIOResult context (always succeeds).
//
//go:inline
func MonadChainIOK[R, A, B any](ma ReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderIOResult[R, B] {
return fromio.MonadChainIOK(
MonadChain[R, A, B],
FromIO[R, B],
ma,
f,
)
}
// ChainIOK returns a function that chains an IO-returning function into ReaderIOResult.
// This is the curried version of MonadChainIOK.
//
//go:inline
func ChainIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, B] {
return fromio.ChainIOK(
Chain[R, A, B],
FromIO[R, B],
f,
)
}
// MonadChainFirstIOK chains an IO computation but keeps the original value.
// Useful for performing IO side effects while preserving the original value.
//
//go:inline
func MonadChainFirstIOK[R, A, B any](ma ReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderIOResult[R, A] {
return fromio.MonadChainFirstIOK(
MonadChain[R, A, A],
MonadMap[R, B, A],
FromIO[R, B],
ma,
f,
)
}
//go:inline
func MonadTapIOK[R, A, B any](ma ReaderIOResult[R, A], f io.Kleisli[A, B]) ReaderIOResult[R, 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, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
return fromio.ChainFirstIOK(
Chain[R, A, A],
Map[R, B, A],
FromIO[R, B],
f,
)
}
//go:inline
func TapIOK[R, A, B any](f io.Kleisli[A, B]) Operator[R, A, A] {
return ChainFirstIOK[R](f)
}
// // ChainOptionK returns a function that chains an Option-returning function into ReaderIOResult.
// // If the Option is None, the provided error function is called to produce the error value.
// //
// //go:inline
// func ChainOptionK[R, A, B any](onNone func() E) func(func(A) Option[B]) Operator[R, A, B] {
// return fromeither.ChainOptionK(
// MonadChain[R, A, B],
// FromEither[R, B],
// onNone,
// )
// }
// MonadAp applies a function wrapped in a context to a value wrapped in a context.
// Both computations are executed (default behavior may be sequential or parallel depending on implementation).
//
//go:inline
func MonadAp[R, A, B any](fab ReaderIOResult[R, func(A) B], fa ReaderIOResult[R, A]) ReaderIOResult[R, B] {
return func(r R) IOResult[B] {
return ioresult.MonadAp(fab(r), fa(r))
}
}
// MonadApSeq applies a function in a context to a value in a context, executing them sequentially.
//
//go:inline
func MonadApSeq[R, A, B any](fab ReaderIOResult[R, func(A) B], fa ReaderIOResult[R, A]) ReaderIOResult[R, B] {
return func(r R) IOResult[B] {
return ioresult.MonadApSeq(fab(r), fa(r))
}
}
// MonadApPar applies a function in a context to a value in a context, executing them in parallel.
//
//go:inline
func MonadApPar[R, A, B any](fab ReaderIOResult[R, func(A) B], fa ReaderIOResult[R, A]) ReaderIOResult[R, B] {
return func(r R) IOResult[B] {
return ioresult.MonadApPar(fab(r), fa(r))
}
}
// Ap returns a function that applies a function in a context to a value in a context.
// This is the curried version of MonadAp.
func Ap[B, R, A any](fa ReaderIOResult[R, A]) func(fab ReaderIOResult[R, func(A) B]) ReaderIOResult[R, B] {
return function.Bind2nd(MonadAp[R, A, B], fa)
}
// 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, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
return function.Bind2nd(MonadChain, f)
}
// ChainFirst returns a function that sequences computations but keeps the first result.
// This is the curried version of MonadChainFirst.
//
//go:inline
func ChainFirst[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return chain.ChainFirst(
Chain[R, A, A],
Map[R, B, A],
f)
}
//go:inline
func Tap[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, A] {
return ChainFirst(f)
}
// Right creates a successful ReaderIOResult with the given value.
//
//go:inline
func Right[R, A any](a A) ReaderIOResult[R, A] {
return reader.Of[R](ioresult.Right(a))
}
// Left creates a failed ReaderIOResult with the given error.
//
//go:inline
func Left[R, A any](e error) ReaderIOResult[R, A] {
return reader.Of[R](ioresult.Left[A](e))
}
// Of creates a successful ReaderIOResult with the given value.
// This is the pointed functor operation, lifting a pure value into the ReaderIOResult context.
func Of[R, A any](a A) ReaderIOResult[R, A] {
return Right[R](a)
}
// Flatten removes one level of nesting from a nested ReaderIOResult.
// Converts ReaderIOResult[R, ReaderIOResult[R, A]] to ReaderIOResult[R, A].
func Flatten[R, A any](mma ReaderIOResult[R, ReaderIOResult[R, A]]) ReaderIOResult[R, A] {
return MonadChain(mma, function.Identity[ReaderIOResult[R, A]])
}
// // FromEither lifts an Either into a ReaderIOResult context.
// // The Either value is independent of any context or IO effects.
// //
// //go:inline
// func FromEither[R, A any](t either.Either[A]) ReaderIOResult[R, A] {
// return readerio.Of[R](t)
// }
// RightReader lifts a Reader into a ReaderIOResult, placing the result in the Right side.
func RightReader[R, A any](ma Reader[R, A]) ReaderIOResult[R, A] {
return function.Flow2(ma, ioresult.Right[A])
}
// LeftReader lifts a Reader into a ReaderIOResult, placing the result in the Left (error) side.
func LeftReader[A, R any](ma Reader[R, error]) ReaderIOResult[R, A] {
return function.Flow2(ma, ioresult.Left[A])
}
// FromReader lifts a Reader into a ReaderIOResult context.
// The Reader result is placed in the Right side (success).
func FromReader[R, A any](ma Reader[R, A]) ReaderIOResult[R, A] {
return RightReader(ma)
}
// // FromIOEither lifts an IOEither into a ReaderIOResult context.
// // The computation becomes independent of any reader context.
// //
// //go:inline
// func FromIOEither[R, A any](ma IOEither[A]) ReaderIOResult[R, A] {
// return reader.Of[R](ma)
// }
// // FromReaderEither lifts a ReaderEither into a ReaderIOResult context.
// // The Either result is lifted into an IO effect.
// func FromReaderEither[R, A any](ma RE.ReaderEither[R, A]) ReaderIOResult[R, A] {
// return function.Flow2(ma, IOE.FromEither[A])
// }
// Ask returns a ReaderIOResult that retrieves the current context.
// Useful for accessing configuration or dependencies.
//
//go:inline
func Ask[R any]() ReaderIOResult[R, R] {
return fromreader.Ask(FromReader[R, R])()
}
// Asks returns a ReaderIOResult that retrieves a value derived from the context.
// This is useful for extracting specific fields from a configuration object.
//
//go:inline
func Asks[R, A any](r Reader[R, A]) ReaderIOResult[R, A] {
return fromreader.Asks(FromReader[R, A])(r)
}
// // FromOption converts an Option to a ReaderIOResult.
// // If the Option is None, the provided function is called to produce the error.
// //
// //go:inline
// func FromOption[R, A any](onNone func() E) func(Option[A]) ReaderIOResult[R, A] {
// return fromeither.FromOption(FromEither[R, A], onNone)
// }
// // FromPredicate creates a ReaderIOResult from a predicate.
// // If the predicate returns false, the onFalse function is called to produce the error.
// //
// //go:inline
// func FromPredicate[R, A any](pred func(A) bool, onFalse func(A) E) func(A) ReaderIOResult[R, A] {
// return fromeither.FromPredicate(FromEither[R, A], pred, onFalse)
// }
// // Fold handles both success and error cases, producing a ReaderIO.
// // This is useful for converting a ReaderIOResult into a ReaderIO by handling all cases.
// //
// //go:inline
// func Fold[R, A, B any](onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) func(ReaderIOResult[R, A]) ReaderIO[R, B] {
// return eithert.MatchE(readerio.MonadChain[R, either.Either[A], B], onLeft, onRight)
// }
// //go:inline
// func MonadFold[R, A, B any](ma ReaderIOResult[R, A], onLeft func(E) ReaderIO[R, B], onRight func(A) ReaderIO[R, B]) ReaderIO[R, B] {
// return eithert.FoldE(readerio.MonadChain[R, either.Either[A], B], ma, onLeft, onRight)
// }
// // GetOrElse provides a default value in case of error.
// // The default is computed lazily via a ReaderIO.
// //
// //go:inline
// func GetOrElse[R, A any](onLeft func(E) ReaderIO[R, A]) func(ReaderIOResult[R, A]) ReaderIO[R, A] {
// return eithert.GetOrElse(readerio.MonadChain[R, either.Either[A], A], readerio.Of[R, A], onLeft)
// }
// // OrElse tries an alternative computation if the first one fails.
// // The alternative can produce a different error type.
// //
// //go:inline
// func OrElse[R1, A2 any](onLeft func(E1) ReaderIOResult[R2, A]) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, A] {
// return eithert.OrElse(readerio.MonadChain[R, either.Either[E1, A], either.Either[E2, A]], readerio.Of[R, either.Either[E2, A]], onLeft)
// }
// // OrLeft transforms the error using a ReaderIO if the computation fails.
// // The success value is preserved unchanged.
// //
// //go:inline
// func OrLeft[A1, R2 any](onLeft func(E1) ReaderIO[R2]) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, A] {
// return eithert.OrLeft(
// readerio.MonadChain[R, either.Either[E1, A], either.Either[E2, A]],
// readerio.MonadMap[R2, either.Either[E2, A]],
// readerio.Of[R, either.Either[E2, A]],
// onLeft,
// )
// }
// // MonadBiMap applies two functions: one to the error, one to the success value.
// // This allows transforming both channels simultaneously.
// //
// //go:inline
// func MonadBiMap[R12, A, B any](fa ReaderIOResult[R1, A], f func(E1) E2, g func(A) B) ReaderIOResult[R2, B] {
// return eithert.MonadBiMap(
// readerio.MonadMap[R, either.Either[E1, A], either.Either[E2, B]],
// fa, f, g,
// )
// }
// // BiMap returns a function that maps over both the error and success channels.
// // This is the curried version of MonadBiMap.
// //
// //go:inline
// func BiMap[R12, A, B any](f func(E1) E2, g func(A) B) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, B] {
// return eithert.BiMap(readerio.Map[R, either.Either[E1, A], either.Either[E2, B]], f, g)
// }
// // TryCatch wraps a function that returns (value, error) into a ReaderIOResult.
// // The onThrow function converts the error into the desired error type.
// func TryCatch[R, A any](f func(R) func() (A, error), onThrow func(error) E) ReaderIOResult[R, A] {
// return func(r R) IOEither[A] {
// return IOE.TryCatch(f(r), onThrow)
// }
// }
// // MonadAlt tries the first computation, and if it fails, tries the second.
// // This implements the Alternative pattern for error recovery.
// //
// //go:inline
// func MonadAlt[R, A any](first ReaderIOResult[R, A], second L.Lazy[ReaderIOResult[R, A]]) ReaderIOResult[R, A] {
// return eithert.MonadAlt(
// readerio.Of[Rither[A]],
// readerio.MonadChain[Rither[A]ither[A]],
// first,
// second,
// )
// }
// // 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, A any](second L.Lazy[ReaderIOResult[R, A]]) Operator[R, A, A] {
// return eithert.Alt(
// readerio.Of[Rither[A]],
// readerio.Chain[Rither[A]ither[A]],
// second,
// )
// }
// // Memoize computes the value of the ReaderIOResult lazily but exactly once.
// // The context used is from the first call. Do not use if the value depends on the context.
// //
// //go:inline
// func Memoize[
// R, A any](rdr ReaderIOResult[R, A]) ReaderIOResult[R, A] {
// return readerio.Memoize(rdr)
// }
// MonadFlap applies a value to a function wrapped in a context.
// This is the reverse of Ap - the value is fixed and the function varies.
//
//go:inline
func MonadFlap[R, B, A any](fab ReaderIOResult[R, func(A) B], a A) ReaderIOResult[R, B] {
return functor.MonadFlap(MonadMap[R, 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, B, A any](a A) func(ReaderIOResult[R, func(A) B]) ReaderIOResult[R, B] {
return functor.Flap(Map[R, func(A) B, B], a)
}
// // MonadMapLeft applies a function to the error value, leaving success unchanged.
// //
// //go:inline
// func MonadMapLeft[R12, A any](fa ReaderIOResult[R1, A], f func(E1) E2) ReaderIOResult[R2, A] {
// return eithert.MonadMapLeft(readerio.MonadMap[Rither[E1, A]ither[E2, A]], fa, f)
// }
// // MapLeft returns a function that transforms the error channel.
// // This is the curried version of MonadMapLeft.
// //
// //go:inline
// func MapLeft[R, A12 any](f func(E1) E2) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, A] {
// return eithert.MapLeft(readerio.Map[Rither[E1, A]ither[E2, A]], f)
// }
// Local runs a computation with a modified context.
// The function f transforms the context before passing it to the computation.
// This is similar to Contravariant's contramap operation.
//
//go:inline
func Local[A, R1, R2 any](f func(R2) R1) func(ReaderIOResult[R1, A]) ReaderIOResult[R2, A] {
return reader.Local[IOResult[A]](f)
}
//go:inline
func Read[A, R any](r R) func(ReaderIOResult[R, A]) IOResult[A] {
return reader.Read[IOResult[A]](r)
}
// //go:inline
// func MonadChainLeft[RAB, A any](fa ReaderIOResult[RA, A], f Kleisli[RBA, A]) ReaderIOResult[RB, A] {
// return readert.MonadChain(
// IOE.MonadChainLeft[EAB, A],
// fa,
// f,
// )
// }
// //go:inline
// func ChainLeft[RAB, A any](f Kleisli[RBA, A]) func(ReaderIOResult[RA, A]) ReaderIOResult[RB, A] {
// return readert.Chain[ReaderIOResult[RA, A]](
// IOE.ChainLeft[EAB, A],
// f,
// )
// }
// // MonadChainFirstLeft chains a computation on the left (error) side but always returns the original error.
// // If the input is a Left value, it applies the function f to the error and executes the resulting computation,
// // but always returns the original Left error regardless of what f returns (Left or Right).
// // If the input is a Right value, it passes through unchanged without calling f.
// //
// // This is useful for side effects on errors (like logging or metrics) where you want to perform an action
// // when an error occurs but always propagate the original error, ensuring the error path is preserved.
// //
// // Parameters:
// // - ma: The input ReaderIOResult that may contain an error of type EA
// // - f: A function that takes an error of type EA and returns a ReaderIOResult (typically for side effects)
// //
// // Returns:
// // - A ReaderIOResult with the original error preserved if input was Left, or the original Right value
// //
// //go:inline
// func MonadChainFirstLeft[A, RAB, B any](ma ReaderIOResult[RA, A], f Kleisli[RBA, B]) ReaderIOResult[RA, A] {
// return MonadChainLeft(ma, function.Flow2(f, Fold(function.Constant1[EB](ma), function.Constant1[B](ma))))
// }
// //go:inline
// func MonadTapLeft[A, RAB, B any](ma ReaderIOResult[RA, A], f Kleisli[RBA, B]) ReaderIOResult[RA, A] {
// return MonadChainFirstLeft(ma, f)
// }
// // ChainFirstLeft is the curried version of [MonadChainFirstLeft].
// // It returns a function that chains a computation on the left (error) side while always preserving the original error.
// //
// // This is particularly useful for adding error handling side effects (like logging, metrics, or notifications)
// // in a functional pipeline. The original error is always returned regardless of what f returns (Left or Right),
// // ensuring the error path is preserved.
// //
// // Parameters:
// // - f: A function that takes an error of type EA and returns a ReaderIOResult (typically for side effects)
// //
// // Returns:
// // - An Operator that performs the side effect but always returns the original error if input was Left
// //
// //go:inline
// func ChainFirstLeft[A, RAB, B any](f Kleisli[RBA, B]) Operator[RA, A, A] {
// return ChainLeft(func(e EA) ReaderIOResult[RA, A] {
// ma := Left[R, A](e)
// return MonadFold(f(e), function.Constant1[EB](ma), function.Constant1[B](ma))
// })
// }
// //go:inline
// func TapLeft[A, RAB, B any](f Kleisli[RBA, B]) Operator[RA, A, A] {
// return ChainFirstLeft[A](f)
// }

View File

@@ -0,0 +1,592 @@
// 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 readerioresult
import (
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/idiomatic/ioresult"
"github.com/IBM/fp-go/v2/io"
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
type TestConfig struct {
Multiplier int
Prefix string
}
func TestFromIOResult(t *testing.T) {
t.Run("lifts successful IOResult", func(t *testing.T) {
ioResult := ioresult.Of(42)
readerIOResult := FromIOResult[TestConfig](ioResult)
cfg := TestConfig{Multiplier: 5}
result, err := readerIOResult(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, result)
})
t.Run("lifts failing IOResult", func(t *testing.T) {
expectedError := errors.New("io error")
ioResult := ioresult.Left[int](expectedError)
readerIOResult := FromIOResult[TestConfig](ioResult)
cfg := TestConfig{Multiplier: 5}
_, err := readerIOResult(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("ignores environment", func(t *testing.T) {
ioResult := ioresult.Of("constant")
readerIOResult := FromIOResult[TestConfig](ioResult)
// Different configs should produce same result
result1, _ := readerIOResult(TestConfig{Multiplier: 1})()
result2, _ := readerIOResult(TestConfig{Multiplier: 100})()
assert.Equal(t, result1, result2)
assert.Equal(t, "constant", result1)
})
}
func TestRightIO(t *testing.T) {
t.Run("lifts IO as success", func(t *testing.T) {
counter := 0
io := func() int {
counter++
return counter
}
readerIOResult := RightIO[TestConfig](io)
cfg := TestConfig{}
result, err := readerIOResult(cfg)()
assert.NoError(t, err)
assert.Equal(t, 1, result)
assert.Equal(t, 1, counter)
})
t.Run("always succeeds", func(t *testing.T) {
io := io.Of("success")
readerIOResult := RightIO[TestConfig](io)
cfg := TestConfig{}
result, err := readerIOResult(cfg)()
assert.NoError(t, err)
assert.Equal(t, "success", result)
})
}
func TestLeftIO(t *testing.T) {
t.Run("lifts IO error as failure", func(t *testing.T) {
expectedError := errors.New("io error")
io := io.Of(expectedError)
readerIOResult := LeftIO[TestConfig, int](io)
cfg := TestConfig{}
_, err := readerIOResult(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("always fails", func(t *testing.T) {
io := io.Of(errors.New("always fails"))
readerIOResult := LeftIO[TestConfig, string](io)
cfg := TestConfig{}
_, err := readerIOResult(cfg)()
assert.Error(t, err)
})
}
func TestFromReaderIO(t *testing.T) {
t.Run("lifts ReaderIO as success", func(t *testing.T) {
readerIO := func(cfg TestConfig) func() int {
return func() int {
return cfg.Multiplier * 10
}
}
readerIOResult := FromReaderIO(readerIO)
cfg := TestConfig{Multiplier: 5}
result, err := readerIOResult(cfg)()
assert.NoError(t, err)
assert.Equal(t, 50, result)
})
t.Run("uses environment", func(t *testing.T) {
readerIO := func(cfg TestConfig) func() string {
return func() string {
return fmt.Sprintf("%s:%d", cfg.Prefix, cfg.Multiplier)
}
}
readerIOResult := FromReaderIO(readerIO)
result1, _ := readerIOResult(TestConfig{Prefix: "A", Multiplier: 1})()
result2, _ := readerIOResult(TestConfig{Prefix: "B", Multiplier: 2})()
assert.Equal(t, "A:1", result1)
assert.Equal(t, "B:2", result2)
})
}
func TestMonadMap(t *testing.T) {
t.Run("transforms success value", func(t *testing.T) {
getValue := Right[TestConfig](10)
double := N.Mul(2)
result := MonadMap(getValue, double)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 20, value)
})
t.Run("propagates error", func(t *testing.T) {
expectedError := errors.New("error")
getValue := Left[TestConfig, int](expectedError)
double := N.Mul(2)
result := MonadMap(getValue, double)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("chains multiple maps", func(t *testing.T) {
getValue := Right[TestConfig](5)
result := F.Pipe3(
getValue,
Map[TestConfig](N.Mul(2)),
Map[TestConfig](N.Add(3)),
Map[TestConfig](S.Format[int]("result:%d")),
)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "result:13", value)
})
}
func TestMap(t *testing.T) {
t.Run("curried version works in pipeline", func(t *testing.T) {
double := Map[TestConfig](N.Mul(2))
getValue := Right[TestConfig](10)
result := F.Pipe1(getValue, double)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 20, value)
})
}
func TestMonadMapTo(t *testing.T) {
t.Run("replaces value with constant", func(t *testing.T) {
getValue := Right[TestConfig](10)
result := MonadMapTo(getValue, "constant")
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "constant", value)
})
t.Run("propagates error", func(t *testing.T) {
expectedError := errors.New("error")
getValue := Left[TestConfig, int](expectedError)
result := MonadMapTo(getValue, "constant")
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
}
func TestMonadChain(t *testing.T) {
t.Run("sequences dependent computations", func(t *testing.T) {
getUser := Right[TestConfig](User{ID: 1, Name: "Alice"})
getUserPosts := func(user User) ReaderIOResult[TestConfig, []string] {
return func(cfg TestConfig) IOResult[[]string] {
return func() ([]string, error) {
return []string{
fmt.Sprintf("Post 1 by %s", user.Name),
fmt.Sprintf("Post 2 by %s", user.Name),
}, nil
}
}
}
result := MonadChain(getUser, getUserPosts)
cfg := TestConfig{}
posts, err := result(cfg)()
assert.NoError(t, err)
assert.Len(t, posts, 2)
assert.Contains(t, posts[0], "Alice")
})
t.Run("propagates first error", func(t *testing.T) {
expectedError := errors.New("first error")
getUser := Left[TestConfig, User](expectedError)
getUserPosts := func(user User) ReaderIOResult[TestConfig, []string] {
return Right[TestConfig]([]string{"should not be called"})
}
result := MonadChain(getUser, getUserPosts)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedError := errors.New("second error")
getUser := Right[TestConfig](User{ID: 1, Name: "Alice"})
getUserPosts := func(user User) ReaderIOResult[TestConfig, []string] {
return Left[TestConfig, []string](expectedError)
}
result := MonadChain(getUser, getUserPosts)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("shares environment", func(t *testing.T) {
getValue := Ask[TestConfig]()
transform := func(cfg TestConfig) ReaderIOResult[TestConfig, string] {
return func(cfg2 TestConfig) IOResult[string] {
return func() (string, error) {
// Both should see the same config
assert.Equal(t, cfg.Multiplier, cfg2.Multiplier)
return fmt.Sprintf("%s:%d", cfg.Prefix, cfg.Multiplier), nil
}
}
}
result := MonadChain(getValue, transform)
cfg := TestConfig{Prefix: "test", Multiplier: 42}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "test:42", value)
})
}
func TestChain(t *testing.T) {
t.Run("curried version works in pipeline", func(t *testing.T) {
double := func(x int) ReaderIOResult[TestConfig, int] {
return Right[TestConfig](x * 2)
}
result := F.Pipe1(
Right[TestConfig](10),
Chain(double),
)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 20, value)
})
}
func TestMonadChainFirst(t *testing.T) {
t.Run("executes side effect but returns first value", func(t *testing.T) {
sideEffectCalled := false
getUser := Right[TestConfig](User{ID: 1, Name: "Alice"})
logUser := func(user User) ReaderIOResult[TestConfig, string] {
return func(cfg TestConfig) IOResult[string] {
return func() (string, error) {
sideEffectCalled = true
return "logged", nil
}
}
}
result := MonadChainFirst(getUser, logUser)
cfg := TestConfig{}
user, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
assert.True(t, sideEffectCalled)
})
t.Run("propagates first error", func(t *testing.T) {
expectedError := errors.New("first error")
getUser := Left[TestConfig, User](expectedError)
logUser := func(user User) ReaderIOResult[TestConfig, string] {
return Right[TestConfig]("should not be called")
}
result := MonadChainFirst(getUser, logUser)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("propagates second error", func(t *testing.T) {
expectedError := errors.New("second error")
getUser := Right[TestConfig](User{ID: 1, Name: "Alice"})
logUser := func(user User) ReaderIOResult[TestConfig, string] {
return Left[TestConfig, string](expectedError)
}
result := MonadChainFirst(getUser, logUser)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
}
func TestMonadAp(t *testing.T) {
t.Run("applies function to value", func(t *testing.T) {
fab := Right[TestConfig](N.Mul(2))
fa := Right[TestConfig](21)
result := MonadAp(fab, fa)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("propagates function error", func(t *testing.T) {
expectedError := errors.New("function error")
fab := Left[TestConfig, func(int) int](expectedError)
fa := Right[TestConfig](21)
result := MonadAp(fab, fa)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("propagates value error", func(t *testing.T) {
expectedError := errors.New("value error")
fab := Right[TestConfig](N.Mul(2))
fa := Left[TestConfig, int](expectedError)
result := MonadAp(fab, fa)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
}
func TestRightAndLeft(t *testing.T) {
t.Run("Right creates successful value", func(t *testing.T) {
result := Right[TestConfig](42)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("Left creates error", func(t *testing.T) {
expectedError := errors.New("error")
result := Left[TestConfig, int](expectedError)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("Of is alias for Right", func(t *testing.T) {
result1 := Right[TestConfig](42)
result2 := Of[TestConfig](42)
cfg := TestConfig{}
value1, _ := result1(cfg)()
value2, _ := result2(cfg)()
assert.Equal(t, value1, value2)
})
}
func TestFlatten(t *testing.T) {
t.Run("removes one level of nesting", func(t *testing.T) {
inner := Right[TestConfig](42)
outer := Right[TestConfig](inner)
result := Flatten(outer)
cfg := TestConfig{}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("propagates outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
outer := Left[TestConfig, ReaderIOResult[TestConfig, int]](expectedError)
result := Flatten(outer)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("propagates inner error", func(t *testing.T) {
expectedError := errors.New("inner error")
inner := Left[TestConfig, int](expectedError)
outer := Right[TestConfig](inner)
result := Flatten(outer)
cfg := TestConfig{}
_, err := result(cfg)()
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
}
func TestAsk(t *testing.T) {
t.Run("retrieves environment", func(t *testing.T) {
result := Ask[TestConfig]()
cfg := TestConfig{Multiplier: 42, Prefix: "test"}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, cfg, value)
})
t.Run("always succeeds", func(t *testing.T) {
result := Ask[TestConfig]()
cfg := TestConfig{}
_, err := result(cfg)()
assert.NoError(t, err)
})
}
func TestAsks(t *testing.T) {
t.Run("extracts value from environment", func(t *testing.T) {
getMultiplier := func(cfg TestConfig) int {
return cfg.Multiplier
}
result := Asks(getMultiplier)
cfg := TestConfig{Multiplier: 42}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, 42, value)
})
t.Run("works with different extractors", func(t *testing.T) {
getPrefix := func(cfg TestConfig) string {
return cfg.Prefix
}
result := Asks(getPrefix)
cfg := TestConfig{Prefix: "test"}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "test", value)
})
}
func TestLocal(t *testing.T) {
t.Run("transforms environment", func(t *testing.T) {
// Computation that uses TestConfig
computation := func(cfg TestConfig) IOResult[string] {
return func() (string, error) {
return fmt.Sprintf("%s:%d", cfg.Prefix, cfg.Multiplier), nil
}
}
// Transform function that modifies the config
transform := func(cfg TestConfig) TestConfig {
return TestConfig{
Prefix: "modified-" + cfg.Prefix,
Multiplier: cfg.Multiplier * 2,
}
}
result := Local[string](transform)(computation)
cfg := TestConfig{Prefix: "test", Multiplier: 5}
value, err := result(cfg)()
assert.NoError(t, err)
assert.Equal(t, "modified-test:10", value)
})
}
func TestRead(t *testing.T) {
t.Run("provides environment to computation", func(t *testing.T) {
computation := func(cfg TestConfig) IOResult[int] {
return func() (int, error) {
return cfg.Multiplier * 10, nil
}
}
cfg := TestConfig{Multiplier: 5}
result := Read[int](cfg)(computation)
value, err := result()
assert.NoError(t, err)
assert.Equal(t, 50, value)
})
}
// Helper type for tests
type User struct {
ID int
Name string
}

View File

@@ -23,6 +23,7 @@ import (
"github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/option"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/readerio"
"github.com/IBM/fp-go/v2/result"
)
@@ -53,6 +54,8 @@ type (
// It is equivalent to Reader[R, IOResult[A]] or func(R) func() (A, error).
ReaderIOResult[R, A any] = Reader[R, IOResult[A]]
ReaderIO[R, A any] = readerio.ReaderIO[R, A]
// Monoid represents a monoid structure for ReaderIOResult values.
Monoid[R, A any] = monoid.Monoid[ReaderIOResult[R, A]]

View File

@@ -43,7 +43,7 @@ func TraverseArray[R, A, B any](f Kleisli[R, A, B]) Kleisli[R, []A, []B] {
//go:inline
func MonadTraverseArray[R, A, B any](as []A, f Kleisli[R, A, B]) ReaderResult[R, []B] {
return array.MonadTraverse[[]A](
return array.MonadTraverse(
Of[R, []B],
Map[R, []B, func(B) []B],
Ap[[]B, R, B],

View File

@@ -214,7 +214,7 @@ func BenchmarkTraverseArray(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
traversed := TraverseArray[BenchContext](kleisli)
traversed := TraverseArray(kleisli)
result := traversed(arr)
_, _ = result(ctx)
}

View File

@@ -595,7 +595,7 @@ func ApReaderS[
) Operator[R, S1, S2] {
return ApS(
setter,
FromReader[R](fa),
FromReader(fa),
)
}

View File

@@ -0,0 +1,53 @@
package readerresult
import "github.com/IBM/fp-go/v2/idiomatic/result"
// Bracket makes sure that a resource is cleaned up in the event of an error. The release action is called regardless of
// whether the body action returns and error or not.
func Bracket[
R, A, B, ANY any](
acquire Lazy[ReaderResult[R, A]],
use Kleisli[R, A, B],
release func(A, B, error) ReaderResult[R, ANY],
) ReaderResult[R, B] {
return func(r R) (B, error) {
// acquire the resource
a, aerr := acquire()(r)
if aerr != nil {
return result.Left[B](aerr)
}
b, berr := use(a)(r)
_, xerr := release(a, b, berr)(r)
if berr != nil {
return result.Left[B](berr)
}
if xerr != nil {
return result.Left[B](xerr)
}
return result.Of(b)
}
}
func WithResource[B, R, A, ANY any](
onCreate Lazy[ReaderResult[R, A]],
onRelease Kleisli[R, A, ANY],
) Kleisli[R, Kleisli[R, A, B], B] {
return func(k Kleisli[R, A, B]) ReaderResult[R, B] {
return func(r R) (B, error) {
a, aerr := onCreate()(r)
if aerr != nil {
return result.Left[B](aerr)
}
b, berr := k(a)(r)
_, xerr := onRelease(a)(r)
if berr != nil {
return result.Left[B](berr)
}
if xerr != nil {
return result.Left[B](xerr)
}
return result.Of(b)
}
}
}

View File

@@ -0,0 +1,270 @@
// 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 readerresult
import (
"github.com/IBM/fp-go/v2/idiomatic/result"
"github.com/IBM/fp-go/v2/reader"
)
// Sequence swaps the order of nested environment parameters in a ReaderResult computation.
//
// This function transforms a computation that takes environment R2 and produces a ReaderResult[R1, A]
// into a Kleisli arrow that takes R1 first and returns a ReaderResult[R2, A].
//
// Type Parameters:
// - R1: The type of the inner environment (becomes the outer parameter after sequencing)
// - R2: The type of the outer environment (becomes the inner environment after sequencing)
// - A: The type of the value produced by the computation
//
// Parameters:
// - ma: A ReaderResult that depends on R2 and produces a ReaderResult[R1, A]
//
// Returns:
// - A Kleisli arrow (func(R1) ReaderResult[R2, A]) that reverses the environment order
//
// The transformation preserves error handling - if the outer computation fails, the error
// is propagated; if the inner computation fails, that error is also propagated.
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// type Database struct {
// ConnectionString string
// }
// type Config struct {
// Timeout int
// }
//
// // Original: takes Config, produces ReaderResult[Database, string]
// original := func(cfg Config) (func(Database) (string, error), error) {
// if cfg.Timeout <= 0 {
// return nil, errors.New("invalid timeout")
// }
// return func(db Database) (string, error) {
// if S.IsEmpty(db.ConnectionString) {
// return "", errors.New("empty connection")
// }
// return fmt.Sprintf("Query on %s with timeout %d",
// db.ConnectionString, cfg.Timeout), nil
// }, nil
// }
//
// // Sequenced: takes Database first, then Config
// sequenced := Sequence(original)
// db := Database{ConnectionString: "localhost:5432"}
// cfg := Config{Timeout: 30}
// result, err := sequenced(db)(cfg)
// // result: "Query on localhost:5432 with timeout 30"
func Sequence[R1, R2, A any](ma ReaderResult[R2, ReaderResult[R1, A]]) Kleisli[R2, R1, A] {
return func(r1 R1) ReaderResult[R2, A] {
return func(r2 R2) (A, error) {
mr1, err := ma(r2)
if err != nil {
return result.Left[A](err)
}
return mr1(r1)
}
}
}
// SequenceReader swaps the order of environment parameters when the inner computation is a pure Reader.
//
// This function is similar to Sequence but specialized for cases where the inner computation
// is a Reader (pure function) rather than a ReaderResult. It transforms a ReaderResult that
// produces a Reader into a Kleisli arrow with swapped environment order.
//
// Type Parameters:
// - R1: The type of the Reader's environment (becomes the outer parameter after sequencing)
// - R2: The type of the ReaderResult's environment (becomes the inner environment after sequencing)
// - A: The type of the value produced by the computation
//
// Parameters:
// - ma: A ReaderResult[R2, Reader[R1, A]] - depends on R2 and produces a pure Reader
//
// Returns:
// - A Kleisli arrow (func(R1) ReaderResult[R2, A]) that reverses the environment order
//
// The inner Reader computation is automatically lifted into the Result context (cannot fail).
// Only the outer ReaderResult can fail with an error.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // Original: takes int, produces Reader[Config, int]
// original := func(x int) (func(Config) int, error) {
// if x < 0 {
// return nil, errors.New("negative value")
// }
// return func(cfg Config) int {
// return x * cfg.Multiplier
// }, nil
// }
//
// // Sequenced: takes Config first, then int
// sequenced := SequenceReader(original)
// cfg := Config{Multiplier: 5}
// result, err := sequenced(cfg)(10)
// // result: 50, err: nil
func SequenceReader[R1, R2, A any](ma ReaderResult[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
return func(r1 R1) ReaderResult[R2, A] {
return func(r2 R2) (A, error) {
mr1, err := ma(r2)
if err != nil {
return result.Left[A](err)
}
return result.Of(mr1(r1))
}
}
}
// Traverse transforms a ReaderResult computation by applying a Kleisli arrow that introduces
// a new environment dependency, effectively swapping the environment order.
//
// This is a higher-order function that takes a Kleisli arrow and returns a function that
// can transform ReaderResult computations. It's useful for introducing environment-dependent
// transformations into existing computations while reordering the environment parameters.
//
// Type Parameters:
// - R2: The type of the original computation's environment
// - R1: The type of the new environment introduced by the Kleisli arrow
// - A: The input type to the Kleisli arrow
// - B: The output type of the transformation
//
// Parameters:
// - f: A Kleisli arrow (func(A) ReaderResult[R1, B]) that transforms A to B with R1 dependency
//
// Returns:
// - A function that transforms ReaderResult[R2, A] into a Kleisli arrow with swapped environments
//
// The transformation preserves error handling from both the original computation and the
// Kleisli arrow. The resulting computation takes R1 first, then R2.
//
// Example:
//
// type Database struct {
// Prefix string
// }
//
// // Original computation: depends on int environment
// original := func(x int) (int, error) {
// if x < 0 {
// return 0, errors.New("negative value")
// }
// return x * 2, nil
// }
//
// // Kleisli arrow: transforms int to string with Database dependency
// format := func(value int) func(Database) (string, error) {
// return func(db Database) (string, error) {
// return fmt.Sprintf("%s:%d", db.Prefix, value), nil
// }
// }
//
// // Apply Traverse
// traversed := Traverse[int](format)
// result := traversed(original)
//
// // Use with Database first, then int
// db := Database{Prefix: "ID"}
// output, err := result(db)(10)
// // output: "ID:20", err: nil
func Traverse[R2, R1, A, B any](
f Kleisli[R1, A, B],
) func(ReaderResult[R2, A]) Kleisli[R2, R1, B] {
return func(rr ReaderResult[R2, A]) Kleisli[R2, R1, B] {
return func(r1 R1) ReaderResult[R2, B] {
return func(r2 R2) (B, error) {
a, err := rr(r2)
if err != nil {
return result.Left[B](err)
}
return f(a)(r1)
}
}
}
}
// TraverseReader transforms a ReaderResult computation by applying a Reader-based Kleisli arrow,
// introducing a new environment dependency while swapping the environment order.
//
// This function is similar to Traverse but specialized for pure Reader transformations that
// cannot fail. It's useful when you want to introduce environment-dependent logic without
// adding error handling complexity.
//
// Type Parameters:
// - R2: The type of the original computation's environment
// - R1: The type of the new environment introduced by the Reader Kleisli arrow
// - A: The input type to the Kleisli arrow
// - B: The output type of the transformation
//
// Parameters:
// - f: A Reader Kleisli arrow (func(A) func(R1) B) that transforms A to B with R1 dependency
//
// Returns:
// - A function that transforms ReaderResult[R2, A] into a Kleisli arrow with swapped environments
//
// The Reader transformation is automatically lifted into the Result context. Only the original
// ReaderResult computation can fail; the Reader transformation itself is pure and cannot fail.
//
// Example:
//
// type Config struct {
// Multiplier int
// }
//
// // Original computation: depends on int environment, may fail
// original := func(x int) (int, error) {
// if x < 0 {
// return 0, errors.New("negative value")
// }
// return x * 2, nil
// }
//
// // Pure Reader transformation: multiplies by config value
// multiply := func(value int) func(Config) int {
// return func(cfg Config) int {
// return value * cfg.Multiplier
// }
// }
//
// // Apply TraverseReader
// traversed := TraverseReader[int, Config, error](multiply)
// result := traversed(original)
//
// // Use with Config first, then int
// cfg := Config{Multiplier: 5}
// output, err := result(cfg)(10)
// // output: 100 (10 * 2 * 5), err: nil
func TraverseReader[R2, R1, A, B any](
f reader.Kleisli[R1, A, B],
) func(ReaderResult[R2, A]) Kleisli[R2, R1, B] {
return func(rr ReaderResult[R2, A]) Kleisli[R2, R1, B] {
return func(r1 R1) ReaderResult[R2, B] {
return func(r2 R2) (B, error) {
a, err := rr(r2)
if err != nil {
return result.Left[B](err)
}
return result.Of(f(a)(r1))
}
}
}
}

View File

@@ -0,0 +1,700 @@
// 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 readerresult
import (
"errors"
"fmt"
"testing"
F "github.com/IBM/fp-go/v2/function"
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
func TestSequence(t *testing.T) {
t.Run("sequences parameter order for simple types", func(t *testing.T) {
// Original: takes int, returns ReaderResult[string, int]
original := func(x int) (ReaderResult[string, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(s string) (int, error) {
return x + len(s), nil
}, nil
}
// Sequenced: takes string first, then int
sequenced := Sequence(original)
// Test original
innerFunc1, err1 := original(10)
assert.NoError(t, err1)
result1, err2 := innerFunc1("hello")
assert.NoError(t, err2)
assert.Equal(t, 15, result1)
// Test sequenced
result2, err3 := sequenced("hello")(10)
assert.NoError(t, err3)
assert.Equal(t, 15, result2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) (ReaderResult[string, int], error) {
if x < 0 {
return nil, expectedError
}
return func(s string) (int, error) {
return x + len(s), nil
}, nil
}
sequenced := Sequence(original)
// Test with error
_, err := sequenced("test")(-1)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("preserves inner error", func(t *testing.T) {
expectedError := errors.New("inner error")
original := func(x int) (ReaderResult[string, int], error) {
return func(s string) (int, error) {
if S.IsEmpty(s) {
return 0, expectedError
}
return x + len(s), nil
}, nil
}
sequenced := Sequence(original)
// Test with inner error
_, err := sequenced("")(10)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string
original := func(x int) (ReaderResult[string, string], error) {
return func(prefix string) (string, error) {
return fmt.Sprintf("%s-%d", prefix, x), nil
}, nil
}
sequenced := Sequence(original)
result, err := sequenced("ID")(42)
assert.NoError(t, err)
assert.Equal(t, "ID-42", result)
})
t.Run("works with struct environments", func(t *testing.T) {
type Database struct {
ConnectionString string
}
type Config struct {
Timeout int
}
original := func(cfg Config) (ReaderResult[Database, string], error) {
if cfg.Timeout <= 0 {
return nil, errors.New("invalid timeout")
}
return func(db Database) (string, error) {
if S.IsEmpty(db.ConnectionString) {
return "", errors.New("empty connection string")
}
return fmt.Sprintf("Query on %s with timeout %d",
db.ConnectionString, cfg.Timeout), nil
}, nil
}
sequenced := Sequence(original)
db := Database{ConnectionString: "localhost:5432"}
cfg := Config{Timeout: 30}
result, err := sequenced(db)(cfg)
assert.NoError(t, err)
assert.Equal(t, "Query on localhost:5432 with timeout 30", result)
})
t.Run("works with zero values", func(t *testing.T) {
original := func(x int) (ReaderResult[string, int], error) {
return func(s string) (int, error) {
return x + len(s), nil
}, nil
}
sequenced := Sequence(original)
result, err := sequenced("")(0)
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
}
func TestSequenceReader(t *testing.T) {
t.Run("sequences parameter order for Reader inner type", func(t *testing.T) {
// Original: takes int, returns Reader[string, int]
original := func(x int) (reader.Reader[string, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(s string) int {
return x + len(s)
}, nil
}
// Sequenced: takes string first, then int
sequenced := SequenceReader(original)
// Test original
readerFunc, err1 := original(10)
assert.NoError(t, err1)
value1 := readerFunc("hello")
assert.Equal(t, 15, value1)
// Test sequenced
value2, err2 := sequenced("hello")(10)
assert.NoError(t, err2)
assert.Equal(t, 15, value2)
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) (reader.Reader[string, int], error) {
if x < 0 {
return nil, expectedError
}
return func(s string) int {
return x + len(s)
}, nil
}
sequenced := SequenceReader(original)
// Test with error
_, err := sequenced("test")(-1)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string using Reader
original := func(x int) (reader.Reader[string, string], error) {
return func(prefix string) string {
return fmt.Sprintf("%s-%d", prefix, x)
}, nil
}
sequenced := SequenceReader(original)
result, err := sequenced("ID")(42)
assert.NoError(t, err)
assert.Equal(t, "ID-42", result)
})
t.Run("works with struct environments", func(t *testing.T) {
type Config struct {
Multiplier int
}
original := func(x int) (reader.Reader[Config, int], error) {
if x < 0 {
return nil, errors.New("negative value")
}
return func(cfg Config) int {
return x * cfg.Multiplier
}, nil
}
sequenced := SequenceReader(original)
cfg := Config{Multiplier: 5}
result, err := sequenced(cfg)(10)
assert.NoError(t, err)
assert.Equal(t, 50, result)
})
t.Run("works with zero values", func(t *testing.T) {
original := func(x int) (reader.Reader[string, int], error) {
return func(s string) int {
return x + len(s)
}, nil
}
sequenced := SequenceReader(original)
result, err := sequenced("")(0)
assert.NoError(t, err)
assert.Equal(t, 0, result)
})
}
func TestTraverse(t *testing.T) {
t.Run("basic transformation with environment swap", func(t *testing.T) {
// Original: ReaderResult[int, int] - takes int environment, produces int
original := func(x int) (int, error) {
if x < 0 {
return 0, errors.New("negative value")
}
return x * 2, nil
}
// Kleisli function: func(int) ReaderResult[string, int]
kleisli := func(a int) ReaderResult[string, int] {
return func(s string) (int, error) {
return a + len(s), nil
}
}
// Traverse returns: func(ReaderResult[int, int]) func(string) ReaderResult[int, int]
traversed := Traverse[int](kleisli)
result := traversed(original)
// result is func(string) ReaderResult[int, int]
// Provide string first ("hello"), then int (10)
value, err := result("hello")(10)
assert.NoError(t, err)
assert.Equal(t, 25, value) // (10 * 2) + len("hello") = 20 + 5 = 25
})
t.Run("preserves outer error", func(t *testing.T) {
expectedError := errors.New("outer error")
original := func(x int) (int, error) {
if x < 0 {
return 0, expectedError
}
return x, nil
}
kleisli := func(a int) ReaderResult[string, int] {
return func(s string) (int, error) {
return a + len(s), nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Test with negative value to trigger error
_, err := result("test")(-1)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("preserves inner error from Kleisli", func(t *testing.T) {
expectedError := errors.New("inner error")
original := Ask[int]()
kleisli := func(a int) ReaderResult[string, int] {
return func(s string) (int, error) {
if S.IsEmpty(s) {
return 0, expectedError
}
return a + len(s), nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Test with empty string to trigger inner error
_, err := result("")(10)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
// Transform int to string using environment-dependent logic
original := Ask[int]()
kleisli := func(a int) ReaderResult[string, string] {
return func(prefix string) (string, error) {
return fmt.Sprintf("%s-%d", prefix, a), nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("ID")(42)
assert.NoError(t, err)
assert.Equal(t, "ID-42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Config struct {
Multiplier int
}
type Database struct {
Prefix string
}
original := func(cfg Config) (int, error) {
if cfg.Multiplier <= 0 {
return 0, errors.New("invalid multiplier")
}
return 10 * cfg.Multiplier, nil
}
kleisli := func(value int) ReaderResult[Database, string] {
return func(db Database) (string, error) {
return fmt.Sprintf("%s:%d", db.Prefix, value), nil
}
}
traversed := Traverse[Config](kleisli)
result := traversed(original)
cfg := Config{Multiplier: 5}
db := Database{Prefix: "result"}
value, err := result(db)(cfg)
assert.NoError(t, err)
assert.Equal(t, "result:50", value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
original := Ask[int]()
// First transformation: multiply by environment value
kleisli1 := func(a int) ReaderResult[int, int] {
return func(multiplier int) (int, error) {
return a * multiplier, nil
}
}
traversed := Traverse[int](kleisli1)
result := traversed(original)
value, err := result(3)(5)
assert.NoError(t, err)
assert.Equal(t, 15, value) // 5 * 3 = 15
})
t.Run("works with zero values", func(t *testing.T) {
original := Ask[int]()
kleisli := func(a int) ReaderResult[string, int] {
return func(s string) (int, error) {
return a + len(s), nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
value, err := result("")(0)
assert.NoError(t, err)
assert.Equal(t, 0, value)
})
t.Run("enables partial application", func(t *testing.T) {
original := Ask[int]()
kleisli := func(a int) ReaderResult[int, int] {
return func(factor int) (int, error) {
return a * factor, nil
}
}
traversed := Traverse[int](kleisli)
result := traversed(original)
// Partially apply factor
withFactor := result(3)
// Can now use with different inputs
value1, err1 := withFactor(10)
assert.NoError(t, err1)
assert.Equal(t, 30, value1)
// Reuse with different input
value2, err2 := withFactor(20)
assert.NoError(t, err2)
assert.Equal(t, 60, value2)
})
}
func TestTraverseReader(t *testing.T) {
t.Run("basic transformation with Reader dependency", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := F.Pipe1(
Ask[int](),
Map[int](N.Mul(2)),
)
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config first, then int
cfg := Config{Multiplier: 5}
value, err := result(cfg)(10)
assert.NoError(t, err)
assert.Equal(t, 100, value) // (10 * 2) * 5 = 100
})
t.Run("preserves outer error", func(t *testing.T) {
type Config struct {
Multiplier int
}
expectedError := errors.New("outer error")
// Original computation that fails
original := func(x int) (int, error) {
if x < 0 {
return 0, expectedError
}
return x, nil
}
// Reader-based transformation (won't be called)
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config and negative value
cfg := Config{Multiplier: 5}
_, err := result(cfg)(-1)
assert.Error(t, err)
assert.Equal(t, expectedError, err)
})
t.Run("works with different types", func(t *testing.T) {
type Database struct {
Prefix string
}
// Original computation producing an int
original := Ask[int]()
// Reader-based transformation: int -> string using Database
format := func(a int) func(Database) string {
return func(db Database) string {
return fmt.Sprintf("%s:%d", db.Prefix, a)
}
}
// Apply TraverseReader
traversed := TraverseReader[int](format)
result := traversed(original)
// Provide Database first, then int
db := Database{Prefix: "ID"}
value, err := result(db)(42)
assert.NoError(t, err)
assert.Equal(t, "ID:42", value)
})
t.Run("works with struct environments", func(t *testing.T) {
type Settings struct {
Prefix string
Suffix string
}
type Context struct {
Value int
}
// Original computation
original := func(ctx Context) (string, error) {
return fmt.Sprintf("value:%d", ctx.Value), nil
}
// Reader-based transformation using Settings
decorate := func(s string) func(Settings) string {
return func(settings Settings) string {
return settings.Prefix + s + settings.Suffix
}
}
// Apply TraverseReader
traversed := TraverseReader[Context](decorate)
result := traversed(original)
// Provide Settings first, then Context
settings := Settings{Prefix: "[", Suffix: "]"}
ctx := Context{Value: 100}
value, err := result(settings)(ctx)
assert.NoError(t, err)
assert.Equal(t, "[value:100]", value)
})
t.Run("enables partial application", func(t *testing.T) {
type Config struct {
Factor int
}
// Original computation
original := Ask[int]()
// Reader-based transformation
scale := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Factor
}
}
// Apply TraverseReader
traversed := TraverseReader[int](scale)
result := traversed(original)
// Partially apply Config
cfg := Config{Factor: 3}
withConfig := result(cfg)
// Can now use with different inputs
value1, err1 := withConfig(10)
assert.NoError(t, err1)
assert.Equal(t, 30, value1)
// Reuse with different input
value2, err2 := withConfig(20)
assert.NoError(t, err2)
assert.Equal(t, 60, value2)
})
t.Run("works with zero values", func(t *testing.T) {
type Config struct {
Offset int
}
// Original computation with zero value
original := Ask[int]()
// Reader-based transformation
add := func(a int) func(Config) int {
return func(cfg Config) int {
return a + cfg.Offset
}
}
// Apply TraverseReader
traversed := TraverseReader[int](add)
result := traversed(original)
// Provide Config with zero offset and zero input
cfg := Config{Offset: 0}
value, err := result(cfg)(0)
assert.NoError(t, err)
assert.Equal(t, 0, value)
})
t.Run("chains multiple transformations", func(t *testing.T) {
type Config struct {
Multiplier int
}
// Original computation
original := func(x int) (int, error) {
return x * 2, nil
}
// Reader-based transformation
multiply := func(a int) func(Config) int {
return func(cfg Config) int {
return a * cfg.Multiplier
}
}
// Apply TraverseReader
traversed := TraverseReader[int](multiply)
result := traversed(original)
// Provide Config and execute
cfg := Config{Multiplier: 4}
value, err := result(cfg)(5)
assert.NoError(t, err)
assert.Equal(t, 40, value) // (5 * 2) * 4 = 40
})
t.Run("works with complex Reader logic", func(t *testing.T) {
type ValidationRules struct {
MinValue int
MaxValue int
}
// Original computation
original := Ask[int]()
// Reader-based transformation with validation logic
validate := func(a int) func(ValidationRules) int {
return func(rules ValidationRules) int {
if a < rules.MinValue {
return rules.MinValue
}
if a > rules.MaxValue {
return rules.MaxValue
}
return a
}
}
// Apply TraverseReader
traversed := TraverseReader[int](validate)
result := traversed(original)
// Test with value within range
rules1 := ValidationRules{MinValue: 0, MaxValue: 100}
value1, err1 := result(rules1)(50)
assert.NoError(t, err1)
assert.Equal(t, 50, value1)
// Test with value above max
rules2 := ValidationRules{MinValue: 0, MaxValue: 30}
value2, err2 := result(rules2)(50)
assert.NoError(t, err2)
assert.Equal(t, 30, value2) // Clamped to max
// Test with value below min
rules3 := ValidationRules{MinValue: 60, MaxValue: 100}
value3, err3 := result(rules3)(50)
assert.NoError(t, err3)
assert.Equal(t, 60, value3) // Clamped to min
})
}

View File

@@ -316,7 +316,7 @@ func OrElse[R, A any](onLeft Kleisli[R, error, A]) Operator[R, A, A] {
// }
// }
// result := F.Pipe1(getUserRR, readerresult.OrLeft[Config](enrichError))
func OrLeft[R, A any](onLeft reader.Kleisli[R, error, error]) Operator[R, A, A] {
func OrLeft[A, R any](onLeft reader.Kleisli[R, error, error]) Operator[R, A, A] {
return func(rr ReaderResult[R, A]) ReaderResult[R, A] {
return func(r R) (A, error) {
a, err := rr(r)

View File

@@ -25,7 +25,6 @@ import (
N "github.com/IBM/fp-go/v2/number"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
RES "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
@@ -248,7 +247,7 @@ func TestOrLeft(t *testing.T) {
}
}
orLeft := OrLeft[MyContext, int](enrichErr)
orLeft := OrLeft[int, MyContext](enrichErr)
v, err := F.Pipe1(Of[MyContext](42), orLeft)(defaultContext)
assert.NoError(t, err)
@@ -292,11 +291,11 @@ func TestChainReaderK(t *testing.T) {
}
func TestChainEitherK(t *testing.T) {
parseInt := func(s string) RES.Result[int] {
parseInt := func(s string) Result[int] {
if s == "42" {
return RES.Of(42)
return result.Of(42)
}
return RES.Left[int](errors.New("parse error"))
return result.Left[int](errors.New("parse error"))
}
chain := ChainEitherK[MyContext](parseInt)

Some files were not shown because too many files have changed in this diff Show More