1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-17 23:37:41 +02:00

Compare commits

...

7 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
162 changed files with 6825 additions and 553 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

@@ -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)
})
}

View File

@@ -1,5 +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

@@ -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,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

@@ -8,9 +8,10 @@ This document explains how the `Sequence*` functions in the `context/readeriores
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. [Practical Benefits](#practical-benefits)
6. [Examples](#examples)
7. [Comparison: With and Without Sequence](#comparison-with-and-without-sequence)
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?
@@ -25,10 +26,7 @@ func double(x int) int {
**Point-free style (without points):**
```go
var double = F.Flow2(
N.Mul(2),
identity,
)
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.
@@ -99,7 +97,7 @@ The `Sequence*` functions solve this by "flipping" or "sequencing" the nested st
```go
func SequenceReader[R, A any](
ma ReaderIOResult[Reader[R, A]]
) reader.Kleisli[context.Context, R, IOResult[A]]
) Kleisli[R, A]
```
**Type transformation:**
@@ -115,7 +113,7 @@ Now `R` (the Reader's environment) comes **first**, before `context.Context`!
```go
func SequenceReaderIO[R, A any](
ma ReaderIOResult[ReaderIO[R, A]]
) reader.Kleisli[context.Context, R, IOResult[A]]
) Kleisli[R, A]
```
**Type transformation:**
@@ -129,7 +127,7 @@ To: func(R) func(context.Context) func() Either[error, A]
```go
func SequenceReaderResult[R, A any](
ma ReaderIOResult[ReaderResult[R, A]]
) reader.Kleisli[context.Context, R, IOResult[A]]
) Kleisli[R, A]
```
**Type transformation:**
@@ -222,6 +220,186 @@ 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**

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

@@ -83,7 +83,7 @@ import (
// )
//
//go:inline
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) Kleisli[R, A] {
return RIOR.SequenceReader(ma)
}
@@ -145,7 +145,7 @@ func SequenceReader[R, A any](ma ReaderIOResult[Reader[R, A]]) reader.Kleisli[co
// )
//
//go:inline
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) Kleisli[R, A] {
return RIOR.SequenceReaderIO(ma)
}
@@ -212,7 +212,7 @@ func SequenceReaderIO[R, A any](ma ReaderIOResult[RIO.ReaderIO[R, A]]) reader.Kl
// )
//
//go:inline
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) reader.Kleisli[context.Context, R, IOResult[A]] {
func SequenceReaderResult[R, A any](ma ReaderIOResult[RR.ReaderResult[R, A]]) Kleisli[R, A] {
return RIOR.SequenceReaderEither(ma)
}

View File

@@ -24,10 +24,11 @@ import (
"time"
"github.com/IBM/fp-go/v2/context/readerio"
"github.com/IBM/fp-go/v2/function"
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"
)
@@ -55,14 +56,14 @@ var (
// loggingCounter is an atomic counter that generates unique LoggingIDs
loggingCounter atomic.Uint64
loggingContextValue = function.Bind2nd(context.Context.Value, any(loggingContextKey))
loggingContextValue = F.Bind2nd(context.Context.Value, any(loggingContextKey))
withLoggingContextValue = function.Bind2of3(context.WithValue)(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 = function.Flow3(
getLoggingContext = F.Flow3(
loggingContextValue,
option.ToType[loggingContext],
option.GetOrElse(getDefaultLoggingContext),
@@ -86,7 +87,7 @@ func getDefaultLoggingContext() loggingContext {
// Returns:
// - An endomorphism that adds the logging context to a context.Context
func withLoggingContext(lctx loggingContext) Endomorphism[context.Context] {
return function.Bind2nd(withLoggingContextValue, any(lctx))
return F.Bind2nd(withLoggingContextValue, any(lctx))
}
// LogEntryExitF creates a customizable operator that wraps a ReaderIOResult computation with entry/exit callbacks.
@@ -134,7 +135,7 @@ func withLoggingContext(lctx loggingContext) Endomorphism[context.Context] {
// return func(ctx context.Context) IO[any] {
// return func() any {
// reqID := ctx.Value("requestID").(RequestID)
// return function.Pipe1(
// return F.Pipe1(
// res,
// result.Fold(
// func(err error) any {
@@ -171,7 +172,7 @@ func withLoggingContext(lctx loggingContext) Endomorphism[context.Context] {
// startTime := ctx.Value("startTime").(time.Time)
// duration := time.Since(startTime).Seconds()
//
// return function.Pipe1(
// return F.Pipe1(
// res,
// result.Fold(
// func(err error) any {
@@ -204,12 +205,12 @@ func LogEntryExitF[A, ANY any](
onEntry ReaderIO[context.Context],
onExit readerio.Kleisli[Result[A], ANY],
) Operator[A, A] {
bracket := function.Bind13of3(readerio.Bracket[context.Context, Result[A], ANY])(onEntry, func(newCtx context.Context, res Result[A]) ReaderIO[ANY] {
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(function.Flow2(
return bracket(F.Flow2(
src,
FromIOResult,
))
@@ -308,7 +309,7 @@ func onExitAny(
return nil
}
return function.Pipe1(
return F.Pipe1(
res,
result.Fold(onError, onSuccess),
)
@@ -375,7 +376,7 @@ func LogEntryExitWithCallback[A any](
return LogEntryExitF(
onEntry(logLevel, cb, nameAttr),
function.Flow2(
F.Flow2(
result.MapTo[A, any](nil),
onExitAny(logLevel, nameAttr),
),
@@ -495,6 +496,19 @@ 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
@@ -558,26 +572,18 @@ func LogEntryExit[A any](name string) Operator[A, A] {
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) readerio.Kleisli[Result[A], Result[A]] {
return func(ma Result[A]) ReaderIOResult[A] {
return func(ctx context.Context) IOResult[A] {
// logger
logger := cb(ctx)
return func() Result[A] {
return result.MonadFold(
ma,
func(e error) Result[A] {
logger.LogAttrs(ctx, logLevel, message, slog.Any("error", e))
return ma
},
func(a A) Result[A] {
logger.LogAttrs(ctx, logLevel, message, slog.Any("value", a))
return ma
},
)
}
}
}
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.
@@ -637,7 +643,7 @@ func SLogWithCallback[A any](
// For logging only successful values, use TapSLog instead.
//
//go:inline
func SLog[A any](message string) readerio.Kleisli[Result[A], Result[A]] {
func SLog[A any](message string) Kleisli[Result[A], A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}

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,8 +27,8 @@ 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"
@@ -151,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.
@@ -164,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.
@@ -178,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.
@@ -196,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.
@@ -383,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)
}
@@ -396,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)
}
@@ -410,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)
}
@@ -428,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)
}
@@ -446,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)
}
@@ -528,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)
}
@@ -541,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)
}
@@ -555,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)
}
@@ -573,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)
}
@@ -591,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)
}
@@ -754,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.
@@ -766,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)
}
@@ -859,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)
}
@@ -914,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.
@@ -935,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].
@@ -952,12 +953,12 @@ 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.

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

@@ -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,6 +18,7 @@ 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"
@@ -129,4 +130,6 @@ type (
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

@@ -86,7 +86,7 @@ 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]
@@ -247,7 +247,7 @@ 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.
@@ -282,7 +282,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],
) Kleisli[ReaderResult[S], S] {
return Let(lens.Set, F.Flow2(lens.Get, f))
}

View File

@@ -31,13 +31,6 @@ import (
"github.com/IBM/fp-go/v2/result"
)
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")
)
// 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.
//
@@ -117,23 +110,14 @@ func curriedLog(
func SLogWithCallback[A any](
logLevel slog.Level,
cb func(context.Context) *slog.Logger,
message string) reader.Kleisli[context.Context, Result[A], Result[A]] {
message string) Kleisli[Result[A], A] {
return F.Pipe1(
F.Flow2(
result.Fold(
F.Flow2(
F.ToAny[error],
slogError,
),
F.Flow2(
F.ToAny[A],
slogValue,
),
),
result.ToSLogAttr[A](),
curriedLog(logLevel, cb, message),
),
reader.Chain(reader.Sequence(F.Flow2(
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]],
))),
@@ -221,7 +205,7 @@ func SLogWithCallback[A any](
// which falls back to the global logger if no logger is found in the context.
//
//go:inline
func SLog[A any](message string) reader.Kleisli[context.Context, Result[A], Result[A]] {
func SLog[A any](message string) Kleisli[Result[A], A] {
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
}

View File

@@ -18,7 +18,9 @@ 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"
)
@@ -48,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] {
@@ -72,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] {
@@ -87,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)
}
@@ -285,7 +287,7 @@ func MonadChainFirst[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult
MonadChain,
MonadMap,
ma,
f,
F.Flow2(f, WithContext),
)
}
@@ -294,6 +296,6 @@ func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
return chain.ChainFirst(
Chain,
Map,
f,
F.Flow2(f, WithContext),
)
}

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"
@@ -34,6 +35,7 @@ type (
// 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]
)

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

@@ -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

@@ -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

@@ -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

@@ -42,6 +42,8 @@ import (
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// type Database struct {
// ConnectionString string
// }
@@ -57,7 +59,7 @@ import (
// }
// return func(db Database) func() (string, error) {
// return func() (string, error) {
// if db.ConnectionString == "" {
// if S.IsEmpty(db.ConnectionString) {
// return "", errors.New("empty connection")
// }
// return fmt.Sprintf("Query on %s with timeout %d",

View File

@@ -23,6 +23,7 @@ import (
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"
)
@@ -89,7 +90,7 @@ func TestSequence(t *testing.T) {
return func() (ReaderIOResult[string, int], error) {
return func(s string) IOResult[int] {
return func() (int, error) {
if len(s) == 0 {
if S.IsEmpty(s) {
return 0, expectedError
}
return x + len(s), nil
@@ -140,7 +141,7 @@ func TestSequence(t *testing.T) {
}
return func(db Database) IOResult[string] {
return func() (string, error) {
if db.ConnectionString == "" {
if S.IsEmpty(db.ConnectionString) {
return "", errors.New("empty connection string")
}
return fmt.Sprintf("Query on %s with timeout %d",
@@ -400,7 +401,7 @@ func TestTraverse(t *testing.T) {
kleisli := func(a int) ReaderIOResult[string, int] {
return func(s string) IOResult[int] {
return func() (int, error) {
if len(s) == 0 {
if S.IsEmpty(s) {
return 0, expectedError
}
return a + len(s), nil

View File

@@ -41,6 +41,8 @@ import (
//
// Example:
//
// import S "github.com/IBM/fp-go/v2/string"
//
// type Database struct {
// ConnectionString string
// }
@@ -54,7 +56,7 @@ import (
// return nil, errors.New("invalid timeout")
// }
// return func(db Database) (string, error) {
// if db.ConnectionString == "" {
// if S.IsEmpty(db.ConnectionString) {
// return "", errors.New("empty connection")
// }
// return fmt.Sprintf("Query on %s with timeout %d",

View File

@@ -23,6 +23,7 @@ import (
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"
)
@@ -79,7 +80,7 @@ func TestSequence(t *testing.T) {
original := func(x int) (ReaderResult[string, int], error) {
return func(s string) (int, error) {
if len(s) == 0 {
if S.IsEmpty(s) {
return 0, expectedError
}
return x + len(s), nil
@@ -122,7 +123,7 @@ func TestSequence(t *testing.T) {
return nil, errors.New("invalid timeout")
}
return func(db Database) (string, error) {
if db.ConnectionString == "" {
if S.IsEmpty(db.ConnectionString) {
return "", errors.New("empty connection string")
}
return fmt.Sprintf("Query on %s with timeout %d",
@@ -314,7 +315,7 @@ func TestTraverse(t *testing.T) {
kleisli := func(a int) ReaderResult[string, int] {
return func(s string) (int, error) {
if len(s) == 0 {
if S.IsEmpty(s) {
return 0, expectedError
}
return a + len(s), nil

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"
)
@@ -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)

View File

@@ -129,7 +129,7 @@ func TraverseArray[A, B any](f Kleisli[A, B]) Kleisli[[]A, []B] {
// Example - Annotate with index:
//
// annotate := func(i int, s string) (string, error) {
// if len(s) == 0 {
// if S.IsEmpty(s) {
// return "", fmt.Errorf("empty string at index %d", i)
// }
// return fmt.Sprintf("[%d]=%s", i, s), nil
@@ -170,7 +170,7 @@ func TraverseArrayWithIndexG[GA ~[]A, GB ~[]B, A, B any](f func(int, A) (B, erro
// Example - Validate with position info:
//
// check := func(i int, s string) (string, error) {
// if len(s) == 0 {
// if S.IsEmpty(s) {
// return "", fmt.Errorf("empty value at position %d", i)
// }
// return strings.ToUpper(s), nil

View File

@@ -21,15 +21,14 @@ import (
"strconv"
"testing"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestTraverseArrayG_Success tests successful traversal of an array with all valid elements
func TestTraverseArrayG_Success(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
parse := strconv.Atoi
result, err := TraverseArrayG[[]string, []int](parse)([]string{"1", "2", "3"})
@@ -39,9 +38,7 @@ func TestTraverseArrayG_Success(t *testing.T) {
// TestTraverseArrayG_Error tests that traversal short-circuits on first error
func TestTraverseArrayG_Error(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
parse := strconv.Atoi
result, err := TraverseArrayG[[]string, []int](parse)([]string{"1", "bad", "3"})
@@ -51,9 +48,7 @@ func TestTraverseArrayG_Error(t *testing.T) {
// TestTraverseArrayG_EmptyArray tests traversal of an empty array
func TestTraverseArrayG_EmptyArray(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
parse := strconv.Atoi
result, err := TraverseArrayG[[]string, []int](parse)([]string{})
@@ -64,9 +59,7 @@ func TestTraverseArrayG_EmptyArray(t *testing.T) {
// TestTraverseArrayG_SingleElement tests traversal with a single element
func TestTraverseArrayG_SingleElement(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
parse := strconv.Atoi
result, err := TraverseArrayG[[]string, []int](parse)([]string{"42"})
@@ -96,9 +89,7 @@ func TestTraverseArrayG_CustomSliceType(t *testing.T) {
type StringSlice []string
type IntSlice []int
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
parse := strconv.Atoi
input := StringSlice{"1", "2", "3"}
result, err := TraverseArrayG[StringSlice, IntSlice](parse)(input)
@@ -178,7 +169,7 @@ func TestTraverseArray_EmptyArray(t *testing.T) {
// TestTraverseArray_DifferentTypes tests transformation between different types
func TestTraverseArray_DifferentTypes(t *testing.T) {
toLength := func(s string) (int, error) {
if len(s) == 0 {
if S.IsEmpty(s) {
return 0, errors.New("empty string")
}
return len(s), nil
@@ -193,7 +184,7 @@ func TestTraverseArray_DifferentTypes(t *testing.T) {
// TestTraverseArrayWithIndexG_Success tests successful indexed traversal
func TestTraverseArrayWithIndexG_Success(t *testing.T) {
annotate := func(i int, s string) (string, error) {
if len(s) == 0 {
if S.IsEmpty(s) {
return "", fmt.Errorf("empty string at index %d", i)
}
return fmt.Sprintf("[%d]=%s", i, s), nil
@@ -208,7 +199,7 @@ func TestTraverseArrayWithIndexG_Success(t *testing.T) {
// TestTraverseArrayWithIndexG_Error tests error handling with index
func TestTraverseArrayWithIndexG_Error(t *testing.T) {
annotate := func(i int, s string) (string, error) {
if len(s) == 0 {
if S.IsEmpty(s) {
return "", fmt.Errorf("empty string at index %d", i)
}
return fmt.Sprintf("[%d]=%s", i, s), nil
@@ -284,7 +275,7 @@ func TestTraverseArrayWithIndexG_CustomSliceType(t *testing.T) {
// TestTraverseArrayWithIndex_Success tests successful indexed traversal
func TestTraverseArrayWithIndex_Success(t *testing.T) {
check := func(i int, s string) (string, error) {
if len(s) == 0 {
if S.IsEmpty(s) {
return "", fmt.Errorf("empty value at position %d", i)
}
return fmt.Sprintf("%d_%s", i, s), nil
@@ -299,7 +290,7 @@ func TestTraverseArrayWithIndex_Success(t *testing.T) {
// TestTraverseArrayWithIndex_Error tests error with position info
func TestTraverseArrayWithIndex_Error(t *testing.T) {
check := func(i int, s string) (string, error) {
if len(s) == 0 {
if S.IsEmpty(s) {
return "", fmt.Errorf("empty value at position %d", i)
}
return s, nil

View File

@@ -30,7 +30,7 @@ func Curry0[R any](f func() (R, error)) func() (R, error) {
//
// 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) (R, error) {

View File

@@ -65,6 +65,6 @@ func bodyRequest(method string) func(string) func([]byte) (*http.Request, error)
func noBodyRequest(method string) func(string) (*http.Request, error) {
return func(url string) (*http.Request, error) {
return http.NewRequest(method, url, nil)
return http.NewRequest(method, url, http.NoBody)
}
}

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, err error) (A, error) {
if err != nil {
left("%s: %v", prefix, err)

View File

@@ -21,15 +21,14 @@ import (
"strconv"
"testing"
S "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestTraverseRecordG_Success tests successful traversal of a map
func TestTraverseRecordG_Success(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
parse := strconv.Atoi
input := map[string]string{"a": "1", "b": "2", "c": "3"}
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
@@ -42,9 +41,7 @@ func TestTraverseRecordG_Success(t *testing.T) {
// TestTraverseRecordG_Error tests that traversal short-circuits on error
func TestTraverseRecordG_Error(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
parse := strconv.Atoi
input := map[string]string{"a": "1", "b": "bad", "c": "3"}
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
@@ -55,9 +52,7 @@ func TestTraverseRecordG_Error(t *testing.T) {
// TestTraverseRecordG_EmptyMap tests traversal of an empty map
func TestTraverseRecordG_EmptyMap(t *testing.T) {
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
parse := strconv.Atoi
input := map[string]string{}
result, err := TraverseRecordG[map[string]string, map[string]int](parse)(input)
@@ -72,9 +67,7 @@ func TestTraverseRecordG_CustomMapType(t *testing.T) {
type StringMap map[string]string
type IntMap map[string]int
parse := func(s string) (int, error) {
return strconv.Atoi(s)
}
parse := strconv.Atoi
input := StringMap{"x": "10", "y": "20"}
result, err := TraverseRecordG[StringMap, IntMap](parse)(input)
@@ -128,7 +121,7 @@ func TestTraverseRecord_ValidationError(t *testing.T) {
// TestTraverseRecordWithIndexG_Success tests successful indexed traversal
func TestTraverseRecordWithIndexG_Success(t *testing.T) {
annotate := func(k string, v string) (string, error) {
if len(v) == 0 {
if S.IsEmpty(v) {
return "", fmt.Errorf("empty value for key %s", k)
}
return fmt.Sprintf("%s=%s", k, v), nil
@@ -145,7 +138,7 @@ func TestTraverseRecordWithIndexG_Success(t *testing.T) {
// TestTraverseRecordWithIndexG_Error tests error handling with key
func TestTraverseRecordWithIndexG_Error(t *testing.T) {
annotate := func(k string, v string) (string, error) {
if len(v) == 0 {
if S.IsEmpty(v) {
return "", fmt.Errorf("empty value for key %s", k)
}
return v, nil

View File

@@ -23,6 +23,7 @@ import (
N "github.com/IBM/fp-go/v2/number"
S "github.com/IBM/fp-go/v2/semigroup"
STR "github.com/IBM/fp-go/v2/string"
"github.com/stretchr/testify/assert"
)
@@ -43,7 +44,7 @@ func makeErrorListSemigroup() S.Semigroup[error] {
var msgs []string
if strings.HasPrefix(msg1, "[") && strings.HasSuffix(msg1, "]") {
trimmed := strings.Trim(msg1, "[]")
if trimmed != "" {
if STR.IsNonEmpty(trimmed) {
msgs = strings.Split(trimmed, ", ")
}
} else {
@@ -52,7 +53,7 @@ func makeErrorListSemigroup() S.Semigroup[error] {
if strings.HasPrefix(msg2, "[") && strings.HasSuffix(msg2, "]") {
trimmed := strings.Trim(msg2, "[]")
if trimmed != "" {
if STR.IsNonEmpty(trimmed) {
msgs = append(msgs, strings.Split(trimmed, ", ")...)
}
} else {
@@ -160,7 +161,7 @@ func TestApV_StringTransformation(t *testing.T) {
sg := makeErrorConcatSemigroup()
apv := ApV[string, string](sg)
toUpper := func(s string) string { return strings.ToUpper(s) }
toUpper := strings.ToUpper
value, verr := Right("hello")
fn, ferr := Right(toUpper)

View File

@@ -166,7 +166,7 @@ func MonadMapWithIndex[GA ~[]A, GB ~[]B, A, B any](as GA, f func(idx int, a A) B
}
func ConstNil[GA ~[]A, A any]() GA {
return (GA)(nil)
return GA(nil)
}
func Concat[GT ~[]T, T any](left, right GT) GT {

127
v2/io/consumer.go Normal file
View File

@@ -0,0 +1,127 @@
// 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 io
// ChainConsumer converts a Consumer into an IO operator that executes the consumer
// as a side effect and returns an empty struct.
//
// This function bridges the gap between pure consumers (functions that consume values
// without returning anything) and the IO monad. It takes a Consumer[A] and returns
// an Operator that:
// 1. Executes the source IO[A] to get a value
// 2. Passes that value to the consumer for side effects
// 3. Returns IO[struct{}] to maintain the monadic chain
//
// The returned IO[struct{}] allows the operation to be composed with other IO operations
// while discarding the consumed value. This is useful for operations like logging,
// printing, or updating external state within an IO pipeline.
//
// Type Parameters:
// - A: The type of value consumed by the consumer
//
// Parameters:
// - c: A Consumer[A] that performs side effects on values of type A
//
// Returns:
// - An Operator[A, struct{}] that executes the consumer and returns an empty struct
//
// Example:
//
// // Create a consumer that logs values
// logger := func(x int) {
// fmt.Printf("Value: %d\n", x)
// }
//
// // Convert it to an IO operator
// logOp := io.ChainConsumer(logger)
//
// // Use it in an IO pipeline
// result := F.Pipe2(
// io.Of(42),
// logOp, // Logs "Value: 42"
// io.Map(func(struct{}) string { return "done" }),
// )
// result() // Returns "done" after logging
//
// // Another example with multiple operations
// var values []int
// collector := func(x int) {
// values = append(values, x)
// }
//
// pipeline := F.Pipe2(
// io.Of(100),
// io.ChainConsumer(collector), // Collects the value
// io.Map(func(struct{}) int { return len(values) }),
// )
// count := pipeline() // Returns 1, values contains [100]
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
return Chain(FromConsumerK(c))
}
// FromConsumerK converts a Consumer into a Kleisli arrow that wraps the consumer
// in an IO context.
//
// This function lifts a Consumer[A] (a function that consumes a value and performs
// side effects) into a Kleisli[A, struct{}] (a function that takes a value and returns
// an IO computation that performs the side effect and returns an empty struct).
//
// The resulting Kleisli arrow can be used with Chain and other monadic operations
// to integrate consumers into IO pipelines. This is a lower-level function compared
// to ChainConsumer, which directly returns an Operator.
//
// Type Parameters:
// - A: The type of value consumed by the consumer
//
// Parameters:
// - c: A Consumer[A] that performs side effects on values of type A
//
// Returns:
// - A Kleisli[A, struct{}] that wraps the consumer in an IO context
//
// Example:
//
// // Create a consumer
// logger := func(x int) {
// fmt.Printf("Logging: %d\n", x)
// }
//
// // Convert to Kleisli arrow
// logKleisli := io.FromConsumerK(logger)
//
// // Use with Chain
// result := F.Pipe2(
// io.Of(42),
// io.Chain(logKleisli), // Logs "Logging: 42"
// io.Map(func(struct{}) string { return "completed" }),
// )
// result() // Returns "completed"
//
// // Can also be used to build more complex operations
// logAndCount := func(x int) io.IO[int] {
// return F.Pipe2(
// logKleisli(x),
// io.Map(func(struct{}) int { return 1 }),
// )
// }
func FromConsumerK[A any](c Consumer[A]) Kleisli[A, struct{}] {
return func(a A) IO[struct{}] {
return func() struct{} {
c(a)
return struct{}{}
}
}
}

265
v2/io/consumer_test.go Normal file
View File

@@ -0,0 +1,265 @@
// 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 io
import (
"testing"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestChainConsumer(t *testing.T) {
t.Run("executes consumer with IO value", func(t *testing.T) {
var captured int
consumer := func(x int) {
captured = x
}
result := F.Pipe1(
Of(42),
ChainConsumer(consumer),
)
// Execute the IO
result()
// Verify the consumer was called with the correct value
assert.Equal(t, 42, captured)
})
t.Run("returns empty struct", func(t *testing.T) {
consumer := func(x int) {
// no-op consumer
}
result := F.Pipe1(
Of(100),
ChainConsumer(consumer),
)
// Execute and verify return type
output := result()
assert.Equal(t, struct{}{}, output)
})
t.Run("can be chained with other IO operations", func(t *testing.T) {
var captured int
consumer := func(x int) {
captured = x
}
result := F.Pipe2(
Of(21),
ChainConsumer(consumer),
Map(func(struct{}) int { return captured * 2 }),
)
output := result()
assert.Equal(t, 42, output)
assert.Equal(t, 21, captured)
})
t.Run("works with string values", func(t *testing.T) {
var captured string
consumer := func(s string) {
captured = s
}
result := F.Pipe1(
Of("hello"),
ChainConsumer(consumer),
)
result()
assert.Equal(t, "hello", captured)
})
t.Run("works with complex types", func(t *testing.T) {
type User struct {
Name string
Age int
}
var captured User
consumer := func(u User) {
captured = u
}
user := User{Name: "Alice", Age: 30}
result := F.Pipe1(
Of(user),
ChainConsumer(consumer),
)
result()
assert.Equal(t, user, captured)
})
t.Run("multiple consumers in sequence", func(t *testing.T) {
var values []int
consumer1 := func(x int) {
values = append(values, x)
}
consumer2 := func(_ struct{}) {
values = append(values, 999)
}
result := F.Pipe2(
Of(42),
ChainConsumer(consumer1),
ChainConsumer(consumer2),
)
result()
assert.Equal(t, []int{42, 999}, values)
})
t.Run("consumer with side effects", func(t *testing.T) {
counter := 0
consumer := func(x int) {
counter += x
}
// Execute multiple times
op := ChainConsumer(consumer)
io1 := op(Of(10))
io2 := op(Of(20))
io3 := op(Of(30))
io1()
assert.Equal(t, 10, counter)
io2()
assert.Equal(t, 30, counter)
io3()
assert.Equal(t, 60, counter)
})
t.Run("consumer in a pipeline with Map", func(t *testing.T) {
var log []string
logger := func(s string) {
log = append(log, s)
}
result := F.Pipe3(
Of("start"),
ChainConsumer(logger),
Map(func(struct{}) string { return "middle" }),
Chain(func(s string) IO[string] {
logger(s)
return Of("end")
}),
)
output := result()
assert.Equal(t, "end", output)
assert.Equal(t, []string{"start", "middle"}, log)
})
t.Run("consumer does not affect IO chain on panic recovery", func(t *testing.T) {
var captured int
safeConsumer := func(x int) {
captured = x
}
result := F.Pipe2(
Of(42),
ChainConsumer(safeConsumer),
Map(func(struct{}) string { return "success" }),
)
output := result()
assert.Equal(t, "success", output)
assert.Equal(t, 42, captured)
})
t.Run("consumer with pointer types", func(t *testing.T) {
var captured *int
consumer := func(p *int) {
captured = p
}
value := 42
result := F.Pipe1(
Of(&value),
ChainConsumer(consumer),
)
result()
assert.Equal(t, &value, captured)
assert.Equal(t, 42, *captured)
})
t.Run("consumer with slice accumulation", func(t *testing.T) {
var accumulated []int
consumer := func(x int) {
accumulated = append(accumulated, x)
}
op := ChainConsumer(consumer)
// Create multiple IOs and execute them
for i := 1; i <= 5; i++ {
io := op(Of(i))
io()
}
assert.Equal(t, []int{1, 2, 3, 4, 5}, accumulated)
})
t.Run("consumer with map accumulation", func(t *testing.T) {
counts := make(map[string]int)
consumer := func(s string) {
counts[s]++
}
op := ChainConsumer(consumer)
words := []string{"hello", "world", "hello", "test", "world", "hello"}
for _, word := range words {
io := op(Of(word))
io()
}
assert.Equal(t, 3, counts["hello"])
assert.Equal(t, 2, counts["world"])
assert.Equal(t, 1, counts["test"])
})
t.Run("lazy evaluation - consumer not called until IO executed", func(t *testing.T) {
called := false
consumer := func(x int) {
called = true
}
// Create the IO but don't execute it
io := F.Pipe1(
Of(42),
ChainConsumer(consumer),
)
// Consumer should not be called yet
assert.False(t, called)
// Now execute
io()
// Consumer should be called now
assert.True(t, called)
})
}

View File

@@ -19,6 +19,18 @@
// Unlike functions that execute immediately, IO values describe computations that will be
// executed when explicitly invoked.
//
// # Fantasy Land Specification
//
// This implementation corresponds to the Fantasy Land IO 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
//
// # Core Concepts
//
// The IO type is defined as a function that takes no arguments and returns a value:

View File

@@ -3,6 +3,7 @@ package io
import (
"iter"
"github.com/IBM/fp-go/v2/consumer"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/IBM/fp-go/v2/pair"
"github.com/IBM/fp-go/v2/reader"
@@ -21,5 +22,7 @@ type (
Monoid[A any] = M.Monoid[IO[A]]
Semigroup[A any] = S.Semigroup[IO[A]]
Consumer[A any] = consumer.Consumer[A]
Seq[T any] = iter.Seq[T]
)

90
v2/ioeither/consumer.go Normal file
View File

@@ -0,0 +1,90 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ioeither
import "github.com/IBM/fp-go/v2/io"
// ChainConsumer converts a Consumer into an IOEither operator that executes the consumer
// as a side effect on successful (Right) values and returns an empty struct.
//
// This function bridges the gap between pure consumers (functions that consume values
// without returning anything) and the IOEither monad. It takes a Consumer[A] and returns
// an Operator that:
// 1. If the IOEither is Right, executes the consumer with the value as a side effect
// 2. If the IOEither is Left, propagates the error without calling the consumer
// 3. Returns IOEither[E, struct{}] to maintain the monadic chain
//
// The consumer is only executed for successful (Right) values. Errors (Left values) are
// propagated unchanged. This is useful for operations like logging successful results,
// collecting metrics, or updating external state within an IOEither pipeline.
//
// Type Parameters:
// - E: The error type of the IOEither
// - A: The type of value consumed by the consumer
//
// Parameters:
// - c: A Consumer[A] that performs side effects on values of type A
//
// Returns:
// - An Operator[E, A, struct{}] that executes the consumer on Right values and returns an empty struct
//
// Example:
//
// // Create a consumer that logs successful values
// logger := func(x int) {
// fmt.Printf("Success: %d\n", x)
// }
//
// // Convert it to an IOEither operator
// logOp := ioeither.ChainConsumer[error](logger)
//
// // Use it in an IOEither pipeline
// result := F.Pipe2(
// ioeither.Right[error](42),
// logOp, // Logs "Success: 42"
// ioeither.Map[error](func(struct{}) string { return "done" }),
// )
// result() // Returns Right("done") after logging
//
// // Errors are propagated without calling the consumer
// errorResult := F.Pipe2(
// ioeither.Left[int](errors.New("failed")),
// logOp, // Consumer NOT called
// ioeither.Map[error](func(struct{}) string { return "done" }),
// )
// errorResult() // Returns Left(error) without logging
//
// // Example with data collection
// var successfulValues []int
// collector := func(x int) {
// successfulValues = append(successfulValues, x)
// }
//
// pipeline := F.Pipe2(
// ioeither.Right[error](100),
// ioeither.ChainConsumer[error](collector), // Collects the value
// ioeither.Map[error](func(struct{}) int { return len(successfulValues) }),
// )
// count := pipeline() // Returns Right(1), successfulValues contains [100]
//go:inline
func ChainConsumer[E, A any](c Consumer[A]) Operator[E, A, struct{}] {
return ChainIOK[E](io.FromConsumerK(c))
}
//go:inline
func ChainFirstConsumer[E, A any](c Consumer[A]) Operator[E, A, A] {
return ChainFirstIOK[E](io.FromConsumerK(c))
}

View File

@@ -0,0 +1,378 @@
// 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 ioeither
import (
"errors"
"testing"
E "github.com/IBM/fp-go/v2/either"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
func TestChainConsumer(t *testing.T) {
t.Run("executes consumer with Right value", func(t *testing.T) {
var captured int
consumer := func(x int) {
captured = x
}
result := F.Pipe1(
Right[error](42),
ChainConsumer[error](consumer),
)
// Execute the IOEither
result()
// Verify the consumer was called with the correct value
assert.Equal(t, 42, captured)
})
t.Run("does not execute consumer with Left value", func(t *testing.T) {
called := false
consumer := func(x int) {
called = true
}
result := F.Pipe1(
Left[int](errors.New("error")),
ChainConsumer[error](consumer),
)
// Execute the IOEither
result()
// Verify the consumer was NOT called
assert.False(t, called)
})
t.Run("returns Right with empty struct for Right input", func(t *testing.T) {
consumer := func(x int) {
// no-op consumer
}
result := F.Pipe1(
Right[error](100),
ChainConsumer[error](consumer),
)
// Execute and verify return type
output := result()
assert.Equal(t, E.Of[error](struct{}{}), output)
})
t.Run("returns Left unchanged for Left input", func(t *testing.T) {
consumer := func(x int) {
// no-op consumer
}
err := errors.New("test error")
result := F.Pipe1(
Left[int](err),
ChainConsumer[error](consumer),
)
// Execute and verify error is preserved
output := result()
assert.True(t, E.IsLeft(output))
_, leftErr := E.Unwrap(output)
assert.Equal(t, err, leftErr)
})
t.Run("can be chained with other IOEither operations", func(t *testing.T) {
var captured int
consumer := func(x int) {
captured = x
}
result := F.Pipe2(
Right[error](21),
ChainConsumer[error](consumer),
Map[error](func(struct{}) int { return captured * 2 }),
)
output := result()
assert.Equal(t, E.Right[error](42), output)
assert.Equal(t, 21, captured)
})
t.Run("works with string values", func(t *testing.T) {
var captured string
consumer := func(s string) {
captured = s
}
result := F.Pipe1(
Right[error]("hello"),
ChainConsumer[error](consumer),
)
result()
assert.Equal(t, "hello", captured)
})
t.Run("works with complex types", func(t *testing.T) {
type User struct {
Name string
Age int
}
var captured User
consumer := func(u User) {
captured = u
}
user := User{Name: "Alice", Age: 30}
result := F.Pipe1(
Right[error](user),
ChainConsumer[error](consumer),
)
result()
assert.Equal(t, user, captured)
})
t.Run("multiple consumers in sequence", func(t *testing.T) {
var values []int
consumer1 := func(x int) {
values = append(values, x)
}
consumer2 := func(_ struct{}) {
values = append(values, 999)
}
result := F.Pipe2(
Right[error](42),
ChainConsumer[error](consumer1),
ChainConsumer[error](consumer2),
)
result()
assert.Equal(t, []int{42, 999}, values)
})
t.Run("consumer with side effects on Right values only", func(t *testing.T) {
counter := 0
consumer := func(x int) {
counter += x
}
op := ChainConsumer[error](consumer)
// Execute with Right values
io1 := op(Right[error](10))
io2 := op(Right[error](20))
io3 := op(Left[int](errors.New("error")))
io4 := op(Right[error](30))
io1()
assert.Equal(t, 10, counter)
io2()
assert.Equal(t, 30, counter)
io3() // Should not increment counter
assert.Equal(t, 30, counter)
io4()
assert.Equal(t, 60, counter)
})
t.Run("consumer in a pipeline with Map and Chain", func(t *testing.T) {
var log []string
logger := func(s string) {
log = append(log, s)
}
result := F.Pipe3(
Right[error]("start"),
ChainConsumer[error](logger),
Map[error](func(struct{}) string { return "middle" }),
Chain(func(s string) IOEither[error, string] {
logger(s)
return Right[error]("end")
}),
)
output := result()
assert.Equal(t, E.Right[error]("end"), output)
assert.Equal(t, []string{"start", "middle"}, log)
})
t.Run("error propagation through consumer chain", func(t *testing.T) {
var captured []int
consumer := func(x int) {
captured = append(captured, x)
}
err := errors.New("early error")
result := F.Pipe3(
Left[int](err),
ChainConsumer[error](consumer),
Map[error](func(struct{}) int { return 100 }),
ChainConsumer[error](consumer),
)
output := result()
assert.True(t, E.IsLeft(output))
_, leftErr := E.Unwrap(output)
assert.Equal(t, err, leftErr)
assert.Empty(t, captured) // Consumer never called
})
t.Run("consumer with pointer types", func(t *testing.T) {
var captured *int
consumer := func(p *int) {
captured = p
}
value := 42
result := F.Pipe1(
Right[error](&value),
ChainConsumer[error](consumer),
)
result()
assert.Equal(t, &value, captured)
assert.Equal(t, 42, *captured)
})
t.Run("consumer with slice accumulation", func(t *testing.T) {
var accumulated []int
consumer := func(x int) {
accumulated = append(accumulated, x)
}
op := ChainConsumer[error](consumer)
// Create multiple IOEithers and execute them
for i := 1; i <= 5; i++ {
io := op(Right[error](i))
io()
}
assert.Equal(t, []int{1, 2, 3, 4, 5}, accumulated)
})
t.Run("consumer with map accumulation", func(t *testing.T) {
counts := make(map[string]int)
consumer := func(s string) {
counts[s]++
}
op := ChainConsumer[error](consumer)
words := []string{"hello", "world", "hello", "test", "world", "hello"}
for _, word := range words {
io := op(Right[error](word))
io()
}
assert.Equal(t, 3, counts["hello"])
assert.Equal(t, 2, counts["world"])
assert.Equal(t, 1, counts["test"])
})
t.Run("lazy evaluation - consumer not called until IOEither executed", func(t *testing.T) {
called := false
consumer := func(x int) {
called = true
}
// Create the IOEither but don't execute it
io := F.Pipe1(
Right[error](42),
ChainConsumer[error](consumer),
)
// Consumer should not be called yet
assert.False(t, called)
// Now execute
io()
// Consumer should be called now
assert.True(t, called)
})
t.Run("consumer with different error types", func(t *testing.T) {
var captured int
consumer := func(x int) {
captured = x
}
// Test with string error type
result1 := F.Pipe1(
Right[string](100),
ChainConsumer[string](consumer),
)
result1()
assert.Equal(t, 100, captured)
// Test with custom error type
type CustomError struct {
Code int
Message string
}
result2 := F.Pipe1(
Right[CustomError](200),
ChainConsumer[CustomError](consumer),
)
result2()
assert.Equal(t, 200, captured)
})
t.Run("consumer in error recovery scenario", func(t *testing.T) {
var successLog []int
successConsumer := func(x int) {
successLog = append(successLog, x)
}
result := F.Pipe2(
Left[int](errors.New("initial error")),
ChainLeft(func(e error) IOEither[error, int] {
// Recover from error
return Right[error](42)
}),
ChainConsumer[error](successConsumer),
)
output := result()
assert.True(t, E.IsRight(output))
assert.Equal(t, []int{42}, successLog)
})
t.Run("consumer composition with ChainFirst", func(t *testing.T) {
var log []string
logger := func(s string) {
log = append(log, "Logged: "+s)
}
result := F.Pipe2(
Right[error]("test"),
ChainConsumer[error](logger),
ChainFirst(func(_ struct{}) IOEither[error, int] {
return Right[error](42)
}),
)
output := result()
assert.True(t, E.IsRight(output))
assert.Equal(t, []string{"Logged: test"}, log)
})
}

View File

@@ -13,6 +13,28 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package ioeither provides the IOEither monad, combining IO effects with Either for error handling.
//
// # Fantasy Land Specification
//
// This is a monad transformer combining:
// - IO monad: https://github.com/fantasyland/fantasy-land
// - Either monad: 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
//
// IOEither[E, A] represents a computation that:
// - Performs side effects (IO)
// - Can fail with an error of type E or succeed with a value of type A (Either)
//
// This is defined as: IO[Either[E, A]] or func() Either[E, A]
package ioeither
//go:generate go run .. ioeither --count 10 --filename gen.go

View File

@@ -879,7 +879,7 @@ func Eitherize7[F ~func(T1, T2, T3, T4, T5, T6, T7) (R, error), T1, T2, T3, T4,
// Uneitherize7 converts a function with 8 parameters returning a tuple into a function with 7 parameters returning a [IOEither[error, R]]
func Uneitherize7[F ~func(T1, T2, T3, T4, T5, T6, T7) IOEither[error, R], T1, T2, T3, T4, T5, T6, T7, R any](f F) func(T1, T2, T3, T4, T5, T6, T7) (R, error) {
return G.Uneitherize7[IOEither[error, R]](f)
return G.Uneitherize7(f)
}
// SequenceT7 converts 7 [IOEither[E, T]] into a [IOEither[E, tuple.Tuple7[T1, T2, T3, T4, T5, T6, T7]]]

View File

@@ -45,7 +45,7 @@ func Requester(builder *R.Builder) IOEH.Requester {
withoutBody := F.Curry2(func(url string, method string) IOEither[*http.Request] {
return ioeither.TryCatchError(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

@@ -279,6 +279,16 @@ func ChainTo[A, E, B any](fb IOEither[E, B]) Operator[E, A, B] {
return Chain(function.Constant1[A](fb))
}
//go:inline
func MonadChainToIO[E, A, B any](fa IOEither[E, A], fb IO[B]) IOEither[E, B] {
return MonadChainTo(fa, FromIO[E](fb))
}
//go:inline
func ChainToIO[E, A, B any](fb IO[B]) Operator[E, A, B] {
return ChainTo[A](FromIO[E](fb))
}
// MonadChainFirst runs the [IOEither] monad returned by the function but returns the result of the original monad
func MonadChainFirst[E, A, B any](ma IOEither[E, A], f Kleisli[E, A, B]) IOEither[E, A] {
return chain.MonadChainFirst(

114
v2/ioeither/rec.go Normal file
View File

@@ -0,0 +1,114 @@
// 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 ioeither
import (
"github.com/IBM/fp-go/v2/either"
)
// TailRec creates a tail-recursive computation in the IOEither monad.
// It enables writing recursive algorithms that don't overflow the call stack by using
// trampolining - a technique where recursive calls are converted into iterations.
//
// The function takes a step function that returns either:
// - Left(A): Continue recursion with a new value of type A
// - Right(B): Terminate recursion with a final result of type B
//
// This is particularly useful for implementing recursive algorithms like:
// - Iterative calculations (factorial, fibonacci, etc.)
// - State machines with multiple steps
// - Loops that may fail at any iteration
// - Processing collections with early termination
//
// The recursion is stack-safe because each step returns a value that indicates
// whether to continue (Left) or stop (Right), rather than making direct recursive calls.
//
// Type Parameters:
// - E: The error type that may occur during computation
// - A: The intermediate type used during recursion (loop state)
// - B: The final result type when recursion terminates
//
// Parameters:
// - f: A step function that takes the current state (A) and returns an IOEither
// containing either Left(A) to continue with a new state, or Right(B) to
// terminate with a final result
//
// Returns:
// - A Kleisli arrow (function from A to IOEither[E, B]) that executes the
// tail-recursive computation starting from the initial value
//
// Example - Computing factorial in a stack-safe way:
//
// type FactState struct {
// n int
// result int
// }
//
// factorial := TailRec(func(state FactState) IOEither[error, Either[FactState, int]] {
// if state.n <= 1 {
// // Terminate with final result
// return Of[error](either.Right[FactState](state.result))
// }
// // Continue with next iteration
// return Of[error](either.Left[int](FactState{
// n: state.n - 1,
// result: state.result * state.n,
// }))
// })
//
// result := factorial(FactState{n: 5, result: 1})() // Right(120)
//
// Example - Processing a list with potential errors:
//
// type ProcessState struct {
// items []string
// sum int
// }
//
// processItems := TailRec(func(state ProcessState) IOEither[error, Either[ProcessState, int]] {
// if len(state.items) == 0 {
// return Of[error](either.Right[ProcessState](state.sum))
// }
// val, err := strconv.Atoi(state.items[0])
// if err != nil {
// return Left[Either[ProcessState, int]](err)
// }
// return Of[error](either.Left[int](ProcessState{
// items: state.items[1:],
// sum: state.sum + val,
// }))
// })
//
// result := processItems(ProcessState{items: []string{"1", "2", "3"}, sum: 0})() // Right(6)
func TailRec[E, A, B any](f Kleisli[E, A, Either[A, B]]) Kleisli[E, A, B] {
return func(a A) IOEither[E, B] {
initial := f(a)
return func() Either[E, B] {
current := initial()
for {
r, e := either.Unwrap(current)
if either.IsLeft(current) {
return either.Left[B](e)
}
b, a := either.Unwrap(r)
if either.IsRight(r) {
return either.Right[E](b)
}
current = f(a)()
}
}
}
}

286
v2/ioeither/rec_test.go Normal file
View File

@@ -0,0 +1,286 @@
// 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 ioeither
import (
"errors"
"fmt"
"testing"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
"github.com/stretchr/testify/assert"
)
// TestTailRecFactorial tests computing factorial using tail recursion
func TestTailRecFactorial(t *testing.T) {
type FactState struct {
n int
result int
}
factorial := TailRec(func(state FactState) IOEither[error, E.Either[FactState, int]] {
if state.n <= 1 {
// Terminate with final result
return Of[error](E.Right[FactState](state.result))
}
// Continue with next iteration
return Of[error](E.Left[int](FactState{
n: state.n - 1,
result: state.result * state.n,
}))
})
t.Run("factorial of 5", func(t *testing.T) {
result := factorial(FactState{n: 5, result: 1})()
assert.Equal(t, E.Right[error](120), result)
})
t.Run("factorial of 0", func(t *testing.T) {
result := factorial(FactState{n: 0, result: 1})()
assert.Equal(t, E.Right[error](1), result)
})
t.Run("factorial of 1", func(t *testing.T) {
result := factorial(FactState{n: 1, result: 1})()
assert.Equal(t, E.Right[error](1), result)
})
t.Run("factorial of 10", func(t *testing.T) {
result := factorial(FactState{n: 10, result: 1})()
assert.Equal(t, E.Right[error](3628800), result)
})
}
// TestTailRecFibonacci tests computing Fibonacci numbers using tail recursion
func TestTailRecFibonacci(t *testing.T) {
type FibState struct {
n int
prev int
curr int
}
fibonacci := TailRec(func(state FibState) IOEither[error, E.Either[FibState, int]] {
if state.n == 0 {
return Of[error](E.Right[FibState](state.curr))
}
return Of[error](E.Left[int](FibState{
n: state.n - 1,
prev: state.curr,
curr: state.prev + state.curr,
}))
})
t.Run("fibonacci of 0", func(t *testing.T) {
result := fibonacci(FibState{n: 0, prev: 0, curr: 1})()
assert.Equal(t, E.Right[error](1), result)
})
t.Run("fibonacci of 1", func(t *testing.T) {
result := fibonacci(FibState{n: 1, prev: 0, curr: 1})()
assert.Equal(t, E.Right[error](1), result)
})
t.Run("fibonacci of 10", func(t *testing.T) {
result := fibonacci(FibState{n: 10, prev: 0, curr: 1})()
assert.Equal(t, E.Right[error](89), result)
})
}
// TestTailRecSumList tests summing a list with tail recursion
func TestTailRecSumList(t *testing.T) {
type SumState struct {
items []int
sum int
}
sumList := TailRec(func(state SumState) IOEither[error, E.Either[SumState, int]] {
if A.IsEmpty(state.items) {
return Of[error](E.Right[SumState](state.sum))
}
return Of[error](E.Left[int](SumState{
items: state.items[1:],
sum: state.sum + state.items[0],
}))
})
t.Run("sum empty list", func(t *testing.T) {
result := sumList(SumState{items: []int{}, sum: 0})()
assert.Equal(t, E.Right[error](0), result)
})
t.Run("sum single element", func(t *testing.T) {
result := sumList(SumState{items: []int{42}, sum: 0})()
assert.Equal(t, E.Right[error](42), result)
})
t.Run("sum multiple elements", func(t *testing.T) {
result := sumList(SumState{items: []int{1, 2, 3, 4, 5}, sum: 0})()
assert.Equal(t, E.Right[error](15), result)
})
}
// TestTailRecWithError tests tail recursion that can fail
func TestTailRecWithError(t *testing.T) {
type DivState struct {
n int
result int
}
// Divide n by 2 repeatedly until it reaches 1, fail if we encounter an odd number > 1
divideByTwo := TailRec(func(state DivState) IOEither[error, E.Either[DivState, int]] {
if state.n == 1 {
return Of[error](E.Right[DivState](state.result))
}
if state.n%2 != 0 {
return Left[E.Either[DivState, int]](fmt.Errorf("cannot divide odd number %d", state.n))
}
return Of[error](E.Left[int](DivState{
n: state.n / 2,
result: state.result + 1,
}))
})
t.Run("success with power of 2", func(t *testing.T) {
result := divideByTwo(DivState{n: 8, result: 0})()
assert.Equal(t, E.Right[error](3), result) // 8 -> 4 -> 2 -> 1 (3 divisions)
})
t.Run("success with 1", func(t *testing.T) {
result := divideByTwo(DivState{n: 1, result: 0})()
assert.Equal(t, E.Right[error](0), result)
})
t.Run("failure with odd number", func(t *testing.T) {
result := divideByTwo(DivState{n: 5, result: 0})()
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot divide odd number 5")
})
t.Run("failure after some iterations", func(t *testing.T) {
result := divideByTwo(DivState{n: 12, result: 0})()
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cannot divide odd number 3")
})
}
// TestTailRecCountdown tests a simple countdown
func TestTailRecCountdown(t *testing.T) {
countdown := TailRec(func(n int) IOEither[error, E.Either[int, string]] {
if n <= 0 {
return Of[error](E.Right[int]("Done!"))
}
return Of[error](E.Left[string](n - 1))
})
t.Run("countdown from 5", func(t *testing.T) {
result := countdown(5)()
assert.Equal(t, E.Right[error]("Done!"), result)
})
t.Run("countdown from 0", func(t *testing.T) {
result := countdown(0)()
assert.Equal(t, E.Right[error]("Done!"), result)
})
t.Run("countdown from negative", func(t *testing.T) {
result := countdown(-5)()
assert.Equal(t, E.Right[error]("Done!"), result)
})
}
// TestTailRecStackSafety tests that TailRec doesn't overflow the stack with large iterations
func TestTailRecStackSafety(t *testing.T) {
// Count down from a large number - this would overflow the stack with regular recursion
largeCountdown := TailRec(func(n int) IOEither[error, E.Either[int, int]] {
if n <= 0 {
return Of[error](E.Right[int](0))
}
return Of[error](E.Left[int](n - 1))
})
t.Run("large iteration count", func(t *testing.T) {
// This should complete without stack overflow
result := largeCountdown(10000)()
assert.Equal(t, E.Right[error](0), result)
})
}
// TestTailRecFindInList tests searching for an element in a list
func TestTailRecFindInList(t *testing.T) {
type FindState struct {
items []string
target string
index int
}
findInList := TailRec(func(state FindState) IOEither[error, E.Either[FindState, int]] {
if A.IsEmpty(state.items) {
return Left[E.Either[FindState, int]](errors.New("not found"))
}
if state.items[0] == state.target {
return Of[error](E.Right[FindState](state.index))
}
return Of[error](E.Left[int](FindState{
items: state.items[1:],
target: state.target,
index: state.index + 1,
}))
})
t.Run("find existing element", func(t *testing.T) {
result := findInList(FindState{
items: []string{"a", "b", "c", "d"},
target: "c",
index: 0,
})()
assert.Equal(t, E.Right[error](2), result)
})
t.Run("find first element", func(t *testing.T) {
result := findInList(FindState{
items: []string{"a", "b", "c"},
target: "a",
index: 0,
})()
assert.Equal(t, E.Right[error](0), result)
})
t.Run("element not found", func(t *testing.T) {
result := findInList(FindState{
items: []string{"a", "b", "c"},
target: "z",
index: 0,
})()
assert.True(t, E.IsLeft(result))
_, err := E.UnwrapError(result)
assert.Error(t, err)
assert.Equal(t, "not found", err.Error())
})
t.Run("empty list", func(t *testing.T) {
result := findInList(FindState{
items: []string{},
target: "a",
index: 0,
})()
assert.True(t, E.IsLeft(result))
})
}

7
v2/ioeither/types.go Normal file
View File

@@ -0,0 +1,7 @@
package ioeither
import "github.com/IBM/fp-go/v2/consumer"
type (
Consumer[A any] = consumer.Consumer[A]
)

13
v2/iooption/consumer.go Normal file
View File

@@ -0,0 +1,13 @@
package iooption
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

@@ -13,6 +13,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package iooption provides the IOOption monad, combining IO effects with Option for optional values.
//
// # Fantasy Land Specification
//
// This is a monad transformer combining:
// - IO monad: https://github.com/fantasyland/fantasy-land
// - Maybe (Option) monad: https://github.com/fantasyland/fantasy-land#maybe
//
// 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
// - Alt: https://github.com/fantasyland/fantasy-land#alt
// - Plus: https://github.com/fantasyland/fantasy-land#plus
// - Alternative: https://github.com/fantasyland/fantasy-land#alternative
//
// IOOption[A] represents a computation that:
// - Performs side effects (IO)
// - May or may not produce a value of type A (Option)
//
// This is defined as: IO[Option[A]] or func() Option[A]
package iooption
//go:generate go run .. iooption --count 10 --filename gen.go

View File

@@ -192,7 +192,7 @@ func FromEither[E, A any](e Either[E, A]) IOOption[A] {
}
// MonadAlt identifies an associative operation on a type constructor
func MonadAlt[A any](first IOOption[A], second IOOption[A]) IOOption[A] {
func MonadAlt[A any](first, second IOOption[A]) IOOption[A] {
return optiont.MonadAlt(
io.MonadOf[Option[A]],
io.MonadChain[Option[A], Option[A]],

123
v2/iooption/rec.go Normal file
View File

@@ -0,0 +1,123 @@
// 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 iooption
import (
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/option"
)
// TailRec creates a tail-recursive computation in the IOOption monad.
// It enables writing recursive algorithms that don't overflow the call stack by using
// an iterative loop - a technique where recursive calls are converted into iterations.
//
// The function takes a step function that returns an IOOption containing either:
// - None: Terminate recursion with no result
// - Some(Left(A)): Continue recursion with a new value of type A
// - Some(Right(B)): Terminate recursion with a final result of type B
//
// This is particularly useful for implementing recursive algorithms that may fail at any step:
// - Iterative calculations that may not produce a result
// - State machines with multiple steps that can fail
// - Loops that may terminate early with None
// - Processing collections with optional results
//
// Unlike the IOEither version which uses lazy recursion, this implementation uses
// an explicit iterative loop for better performance and simpler control flow.
//
// Type Parameters:
// - E: Unused type parameter (kept for consistency with IOEither)
// - A: The intermediate type used during recursion (loop state)
// - B: The final result type when recursion terminates successfully
//
// Parameters:
// - f: A step function that takes the current state (A) and returns an IOOption
// containing either None (failure), Some(Left(A)) to continue with a new state,
// or Some(Right(B)) to terminate with a final result
//
// Returns:
// - A Kleisli arrow (function from A to IOOption[B]) that executes the
// tail-recursive computation starting from the initial value
//
// Example - Computing factorial with optional result:
//
// type FactState struct {
// n int
// result int
// }
//
// factorial := TailRec[any](func(state FactState) IOOption[Either[FactState, int]] {
// if state.n < 0 {
// // Negative numbers have no factorial
// return None[Either[FactState, int]]()
// }
// if state.n <= 1 {
// // Terminate with final result
// return Of(either.Right[FactState](state.result))
// }
// // Continue with next iteration
// return Of(either.Left[int](FactState{
// n: state.n - 1,
// result: state.result * state.n,
// }))
// })
//
// result := factorial(FactState{n: 5, result: 1})() // Some(120)
// result := factorial(FactState{n: -1, result: 1})() // None
//
// Example - Safe division with early termination:
//
// type DivState struct {
// numerator int
// denominator int
// steps int
// }
//
// safeDivide := TailRec[any](func(state DivState) IOOption[Either[DivState, int]] {
// if state.denominator == 0 {
// return None[Either[DivState, int]]() // Division by zero
// }
// if state.numerator < state.denominator {
// return Of(either.Right[DivState](state.steps))
// }
// return Of(either.Left[int](DivState{
// numerator: state.numerator - state.denominator,
// denominator: state.denominator,
// steps: state.steps + 1,
// }))
// })
//
// result := safeDivide(DivState{numerator: 10, denominator: 3, steps: 0})() // Some(3)
// result := safeDivide(DivState{numerator: 10, denominator: 0, steps: 0})() // None
func TailRec[E, A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
return func(a A) IOOption[B] {
initial := f(a)
return func() Option[B] {
current := initial()
for {
r, ok := option.Unwrap(current)
if !ok {
return option.None[B]()
}
b, a := either.Unwrap(r)
if either.IsRight(r) {
return option.Some(b)
}
current = f(a)()
}
}
}
}

334
v2/iooption/rec_test.go Normal file
View File

@@ -0,0 +1,334 @@
// 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 iooption
import (
"testing"
E "github.com/IBM/fp-go/v2/either"
O "github.com/IBM/fp-go/v2/option"
"github.com/stretchr/testify/assert"
)
// TestTailRecFactorial tests computing factorial using tail recursion with optional result
func TestTailRecFactorial(t *testing.T) {
type FactState struct {
n int
result int
}
factorial := TailRec[any](func(state FactState) IOOption[E.Either[FactState, int]] {
if state.n < 0 {
// Negative numbers have no factorial
return None[E.Either[FactState, int]]()
}
if state.n <= 1 {
// Terminate with final result
return Of(E.Right[FactState](state.result))
}
// Continue with next iteration
return Of(E.Left[int](FactState{
n: state.n - 1,
result: state.result * state.n,
}))
})
t.Run("factorial of 5", func(t *testing.T) {
result := factorial(FactState{n: 5, result: 1})()
assert.Equal(t, O.Some(120), result)
})
t.Run("factorial of 0", func(t *testing.T) {
result := factorial(FactState{n: 0, result: 1})()
assert.Equal(t, O.Some(1), result)
})
t.Run("factorial of 1", func(t *testing.T) {
result := factorial(FactState{n: 1, result: 1})()
assert.Equal(t, O.Some(1), result)
})
t.Run("factorial of negative number returns None", func(t *testing.T) {
result := factorial(FactState{n: -5, result: 1})()
assert.Equal(t, O.None[int](), result)
})
t.Run("factorial of 10", func(t *testing.T) {
result := factorial(FactState{n: 10, result: 1})()
assert.Equal(t, O.Some(3628800), result)
})
}
// TestTailRecSafeDivision tests integer division with optional result
func TestTailRecSafeDivision(t *testing.T) {
type DivState struct {
numerator int
denominator int
steps int
}
safeDivide := TailRec[any](func(state DivState) IOOption[E.Either[DivState, int]] {
if state.denominator == 0 {
return None[E.Either[DivState, int]]() // Division by zero
}
if state.numerator < state.denominator {
return Of(E.Right[DivState](state.steps))
}
return Of(E.Left[int](DivState{
numerator: state.numerator - state.denominator,
denominator: state.denominator,
steps: state.steps + 1,
}))
})
t.Run("10 divided by 3", func(t *testing.T) {
result := safeDivide(DivState{numerator: 10, denominator: 3, steps: 0})()
assert.Equal(t, O.Some(3), result)
})
t.Run("division by zero returns None", func(t *testing.T) {
result := safeDivide(DivState{numerator: 10, denominator: 0, steps: 0})()
assert.Equal(t, O.None[int](), result)
})
t.Run("exact division", func(t *testing.T) {
result := safeDivide(DivState{numerator: 15, denominator: 5, steps: 0})()
assert.Equal(t, O.Some(3), result)
})
t.Run("numerator less than denominator", func(t *testing.T) {
result := safeDivide(DivState{numerator: 2, denominator: 5, steps: 0})()
assert.Equal(t, O.Some(0), result)
})
}
// TestTailRecFindInRange tests finding a value in a range with optional result
func TestTailRecFindInRange(t *testing.T) {
type FindState struct {
current int
target int
max int
}
findInRange := TailRec[any](func(state FindState) IOOption[E.Either[FindState, int]] {
if state.current > state.max {
return None[E.Either[FindState, int]]() // Not found
}
if state.current == state.target {
return Of(E.Right[FindState](state.current))
}
return Of(E.Left[int](FindState{
current: state.current + 1,
target: state.target,
max: state.max,
}))
})
t.Run("find existing value", func(t *testing.T) {
result := findInRange(FindState{current: 1, target: 5, max: 10})()
assert.Equal(t, O.Some(5), result)
})
t.Run("value not in range returns None", func(t *testing.T) {
result := findInRange(FindState{current: 1, target: 15, max: 10})()
assert.Equal(t, O.None[int](), result)
})
t.Run("find first value", func(t *testing.T) {
result := findInRange(FindState{current: 1, target: 1, max: 10})()
assert.Equal(t, O.Some(1), result)
})
t.Run("find last value", func(t *testing.T) {
result := findInRange(FindState{current: 1, target: 10, max: 10})()
assert.Equal(t, O.Some(10), result)
})
}
// TestTailRecSumUntilLimit tests summing numbers until a limit with optional result
func TestTailRecSumUntilLimit(t *testing.T) {
type SumState struct {
current int
sum int
limit int
}
sumUntilLimit := TailRec[any](func(state SumState) IOOption[E.Either[SumState, int]] {
if state.sum > state.limit {
return None[E.Either[SumState, int]]() // Exceeded limit
}
if state.current <= 0 {
return Of(E.Right[SumState](state.sum))
}
return Of(E.Left[int](SumState{
current: state.current - 1,
sum: state.sum + state.current,
limit: state.limit,
}))
})
t.Run("sum within limit", func(t *testing.T) {
result := sumUntilLimit(SumState{current: 5, sum: 0, limit: 100})()
assert.Equal(t, O.Some(15), result) // 5+4+3+2+1 = 15
})
t.Run("sum exceeds limit returns None", func(t *testing.T) {
result := sumUntilLimit(SumState{current: 10, sum: 0, limit: 20})()
assert.Equal(t, O.None[int](), result) // Would exceed 20
})
t.Run("sum of zero", func(t *testing.T) {
result := sumUntilLimit(SumState{current: 0, sum: 0, limit: 100})()
assert.Equal(t, O.Some(0), result)
})
}
// TestTailRecCountdown tests a simple countdown with optional result
func TestTailRecCountdown(t *testing.T) {
countdown := TailRec[any](func(n int) IOOption[E.Either[int, string]] {
if n < 0 {
return None[E.Either[int, string]]() // Negative not allowed
}
if n == 0 {
return Of(E.Right[int]("Done!"))
}
return Of(E.Left[string](n - 1))
})
t.Run("countdown from 5", func(t *testing.T) {
result := countdown(5)()
assert.Equal(t, O.Some("Done!"), result)
})
t.Run("countdown from 0", func(t *testing.T) {
result := countdown(0)()
assert.Equal(t, O.Some("Done!"), result)
})
t.Run("countdown from negative returns None", func(t *testing.T) {
result := countdown(-5)()
assert.Equal(t, O.None[string](), result)
})
}
// TestTailRecStackSafety tests that TailRec doesn't overflow the stack with large iterations
func TestTailRecStackSafety(t *testing.T) {
// Count down from a large number - this would overflow the stack with regular recursion
largeCountdown := TailRec[any](func(n int) IOOption[E.Either[int, int]] {
if n < 0 {
return None[E.Either[int, int]]()
}
if n == 0 {
return Of(E.Right[int](0))
}
return Of(E.Left[int](n - 1))
})
t.Run("large iteration count", func(t *testing.T) {
// This should complete without stack overflow
result := largeCountdown(10000)()
assert.Equal(t, O.Some(0), result)
})
}
// TestTailRecValidation tests validation with early termination
func TestTailRecValidation(t *testing.T) {
type ValidationState struct {
items []int
index int
}
// Validate all items are positive, return count if valid
validatePositive := TailRec[any](func(state ValidationState) IOOption[E.Either[ValidationState, int]] {
if state.index >= len(state.items) {
return Of(E.Right[ValidationState](state.index))
}
if state.items[state.index] <= 0 {
return None[E.Either[ValidationState, int]]() // Invalid item
}
return Of(E.Left[int](ValidationState{
items: state.items,
index: state.index + 1,
}))
})
t.Run("all items valid", func(t *testing.T) {
result := validatePositive(ValidationState{items: []int{1, 2, 3, 4, 5}, index: 0})()
assert.Equal(t, O.Some(5), result)
})
t.Run("invalid item returns None", func(t *testing.T) {
result := validatePositive(ValidationState{items: []int{1, 2, -3, 4, 5}, index: 0})()
assert.Equal(t, O.None[int](), result)
})
t.Run("empty list", func(t *testing.T) {
result := validatePositive(ValidationState{items: []int{}, index: 0})()
assert.Equal(t, O.Some(0), result)
})
t.Run("first item invalid", func(t *testing.T) {
result := validatePositive(ValidationState{items: []int{-1, 2, 3}, index: 0})()
assert.Equal(t, O.None[int](), result)
})
}
// TestTailRecCollatzConjecture tests the Collatz conjecture with optional result
func TestTailRecCollatzConjecture(t *testing.T) {
type CollatzState struct {
n int
steps int
}
// Count steps to reach 1 in Collatz sequence
collatz := TailRec[any](func(state CollatzState) IOOption[E.Either[CollatzState, int]] {
if state.n <= 0 {
return None[E.Either[CollatzState, int]]() // Invalid input
}
if state.n == 1 {
return Of(E.Right[CollatzState](state.steps))
}
if state.n%2 == 0 {
return Of(E.Left[int](CollatzState{n: state.n / 2, steps: state.steps + 1}))
}
return Of(E.Left[int](CollatzState{n: 3*state.n + 1, steps: state.steps + 1}))
})
t.Run("collatz for 1", func(t *testing.T) {
result := collatz(CollatzState{n: 1, steps: 0})()
assert.Equal(t, O.Some(0), result)
})
t.Run("collatz for 2", func(t *testing.T) {
result := collatz(CollatzState{n: 2, steps: 0})()
assert.Equal(t, O.Some(1), result) // 2 -> 1
})
t.Run("collatz for 3", func(t *testing.T) {
result := collatz(CollatzState{n: 3, steps: 0})()
assert.Equal(t, O.Some(7), result) // 3 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1
})
t.Run("collatz for negative returns None", func(t *testing.T) {
result := collatz(CollatzState{n: -5, steps: 0})()
assert.Equal(t, O.None[int](), result)
})
t.Run("collatz for zero returns None", func(t *testing.T) {
result := collatz(CollatzState{n: 0, steps: 0})()
assert.Equal(t, O.None[int](), result)
})
}

View File

@@ -16,6 +16,7 @@
package iooption
import (
"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"
@@ -35,4 +36,5 @@ type (
Kleisli[A, B any] = reader.Reader[A, IOOption[B]]
Operator[A, B any] = Kleisli[IOOption[A], B]
Consumer[A any] = consumer.Consumer[A]
)

15
v2/ioresult/consumer.go Normal file
View File

@@ -0,0 +1,15 @@
package ioresult
import (
"github.com/IBM/fp-go/v2/ioeither"
)
//go:inline
func ChainConsumer[A any](c Consumer[A]) Operator[A, struct{}] {
return ioeither.ChainConsumer[error](c)
}
//go:inline
func ChainFirstConsumer[A any](c Consumer[A]) Operator[A, A] {
return ioeither.ChainFirstConsumer[error](c)
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2023 - 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache LicensVersion 2.0 (the "License");
// 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
//
@@ -13,6 +13,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package ioresult provides the IOResult monad, combining IO effects with Result for error handling.
//
// # Fantasy Land Specification
//
// This is a monad transformer combining:
// - IO monad: https://github.com/fantasyland/fantasy-land
// - Either monad: 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
//
// IOResult[A] represents a computation that:
// - Performs side effects (IO)
// - Can fail with an error or succeed with a value of type A (Result/Either)
//
// This is defined as: IO[Result[A]] or func() Either[error, A]
// IOResult is an alias for IOEither with error as the left type.
package ioresult
//go:generate go run .. ioeither --count 10 --filename gen.go

View File

@@ -29,7 +29,7 @@ func TestMkdir(t *testing.T) {
tmpDir := t.TempDir()
newDir := filepath.Join(tmpDir, "testdir")
result := Mkdir(newDir, 0755)()
result := Mkdir(newDir, 0o755)()
path, err := E.UnwrapError(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 := E.UnwrapError(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 := E.UnwrapError(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 := E.UnwrapError(result)
assert.NoError(t, err)
@@ -86,7 +86,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 := E.UnwrapError(result)
assert.NoError(t, err)
@@ -97,7 +97,7 @@ func TestMkdirAll(t *testing.T) {
tmpDir := t.TempDir()
newDir := filepath.Join(tmpDir, "single")
result := MkdirAll(newDir, 0755)()
result := MkdirAll(newDir, 0o755)()
path, err := E.UnwrapError(result)
assert.NoError(t, err)
@@ -112,10 +112,10 @@ func TestMkdirAll(t *testing.T) {
tmpDir := t.TempDir()
filePath := filepath.Join(tmpDir, "file.txt")
err := os.WriteFile(filePath, []byte("content"), 0644)
err := os.WriteFile(filePath, []byte("content"), 0o644)
assert.NoError(t, err)
result := MkdirAll(filepath.Join(filePath, "subdir"), 0755)()
result := MkdirAll(filepath.Join(filePath, "subdir"), 0o755)()
_, err = E.UnwrapError(result)
assert.Error(t, err)

View File

@@ -33,7 +33,7 @@ func TestOpen(t *testing.T) {
tmpFile.Close()
defer os.Remove(tmpPath)
err = os.WriteFile(tmpPath, []byte("test content"), 0644)
err = os.WriteFile(tmpPath, []byte("test content"), 0o644)
require.NoError(t, err)
result := Open(tmpPath)()
@@ -124,7 +124,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 := E.UnwrapError(result)
assert.NoError(t, err)
@@ -137,7 +137,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 := E.UnwrapError(result)
assert.Error(t, err)
@@ -150,11 +150,11 @@ func TestWriteFile(t *testing.T) {
tmpFile.Close()
defer os.Remove(tmpPath)
err = os.WriteFile(tmpPath, []byte("initial"), 0644)
err = os.WriteFile(tmpPath, []byte("initial"), 0o644)
require.NoError(t, err)
newData := []byte("overwritten")
result := WriteFile(tmpPath, 0644)(newData)()
result := WriteFile(tmpPath, 0o644)(newData)()
returnedData, err := E.UnwrapError(result)
assert.NoError(t, err)
@@ -203,7 +203,7 @@ func TestClose(t *testing.T) {
assert.NoError(t, err)
_, writeErr := tmpFile.Write([]byte("test"))
_, writeErr := tmpFile.WriteString("test")
assert.Error(t, writeErr)
})

View File

@@ -106,7 +106,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

@@ -101,7 +101,7 @@ func TestWithTempFile(t *testing.T) {
useFile := func(f *os.File) IOResult[string] {
return ioeither.TryCatchError(func() (string, error) {
tmpPath = f.Name()
_, err := f.Write([]byte("test"))
_, err := f.WriteString("test")
return tmpPath, err
})
}
@@ -199,7 +199,7 @@ func TestWithTempFile(t *testing.T) {
useFile := func(f *os.File) IOResult[string] {
capturedFile = f
return ioeither.TryCatchError(func() (string, error) {
_, err := f.Write([]byte("test"))
_, err := f.WriteString("test")
return f.Name(), err
})
}
@@ -210,7 +210,7 @@ func TestWithTempFile(t *testing.T) {
assert.NoError(t, err)
// File should be closed
_, writeErr := capturedFile.Write([]byte("more"))
_, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr)
})

View File

@@ -72,7 +72,7 @@ func TestWriteAll(t *testing.T) {
_, err := E.UnwrapError(result)
assert.NoError(t, err)
_, writeErr := capturedFile.Write([]byte("more"))
_, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr)
})
@@ -150,7 +150,7 @@ func TestWrite(t *testing.T) {
useFile := func(f *os.File) IOResult[string] {
return ioeither.TryCatchError(func() (string, error) {
_, err := f.Write([]byte("data"))
_, err := f.WriteString("data")
return "success", err
})
}
@@ -160,7 +160,7 @@ func TestWrite(t *testing.T) {
assert.NoError(t, err)
_, writeErr := capturedFile.Write([]byte("more"))
_, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr)
})
@@ -187,7 +187,7 @@ func TestWrite(t *testing.T) {
assert.Error(t, err)
_, writeErr := capturedFile.Write([]byte("more"))
_, writeErr := capturedFile.WriteString("more")
assert.Error(t, writeErr)
})

25
v2/ioresult/rec.go Normal file
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 ioresult
import (
"github.com/IBM/fp-go/v2/ioeither"
)
//go:inline
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
return ioeither.TailRec(f)
}

View File

@@ -1,6 +1,7 @@
package ioresult
import (
"github.com/IBM/fp-go/v2/consumer"
"github.com/IBM/fp-go/v2/either"
"github.com/IBM/fp-go/v2/endomorphism"
"github.com/IBM/fp-go/v2/io"
@@ -26,4 +27,6 @@ type (
Kleisli[A, B any] = reader.Reader[A, IOResult[B]]
Operator[A, B any] = Kleisli[IOResult[A], B]
Consumer[A any] = consumer.Consumer[A]
)

View File

@@ -51,7 +51,7 @@ func single() int64 {
if n%10 == 4 {
return false
}
n = n / 10
n /= 10
}
return true
}),

View File

@@ -17,6 +17,18 @@
// without side effects. It represents deferred computations that are evaluated only when
// their result is needed.
//
// # Fantasy Land Specification
//
// This implementation corresponds to the Fantasy Land IO type (for pure computations):
// 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
//
// A Lazy[A] is simply a function that takes no arguments and returns a value of type A:

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