mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-17 23:37:41 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d17663f016 | ||
|
|
829365fc24 | ||
|
|
64b5660b4e | ||
|
|
16e82d6a65 | ||
|
|
0d40fdcebb | ||
|
|
6a4dfa2c93 | ||
|
|
a37f379a3c | ||
|
|
ece0cd135d |
@@ -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
|
||||
|
||||
177
v2/consumer/consumer.go
Normal file
177
v2/consumer/consumer.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
383
v2/consumer/consumer_test.go
Normal file
383
v2/consumer/consumer_test.go
Normal file
@@ -0,0 +1,383 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package consumer
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
t.Run("basic type transformation", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
// Transform string to int before consuming
|
||||
stringToInt := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
consumeString := Local(stringToInt)(consumeInt)
|
||||
consumeString("42")
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("field extraction from struct", func(t *testing.T) {
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
}
|
||||
|
||||
var capturedName string
|
||||
consumeName := func(name string) {
|
||||
capturedName = name
|
||||
}
|
||||
|
||||
extractName := func(u User) string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
consumeUser := Local(extractName)(consumeName)
|
||||
consumeUser(User{Name: "Alice", Age: 30})
|
||||
|
||||
assert.Equal(t, "Alice", capturedName)
|
||||
})
|
||||
|
||||
t.Run("simplifying complex types", func(t *testing.T) {
|
||||
type DetailedConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
Timeout time.Duration
|
||||
MaxRetry int
|
||||
}
|
||||
|
||||
type SimpleConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
var captured SimpleConfig
|
||||
consumeSimple := func(c SimpleConfig) {
|
||||
captured = c
|
||||
}
|
||||
|
||||
simplify := func(d DetailedConfig) SimpleConfig {
|
||||
return SimpleConfig{Host: d.Host, Port: d.Port}
|
||||
}
|
||||
|
||||
consumeDetailed := Local(simplify)(consumeSimple)
|
||||
consumeDetailed(DetailedConfig{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
Timeout: time.Second,
|
||||
MaxRetry: 3,
|
||||
})
|
||||
|
||||
assert.Equal(t, SimpleConfig{Host: "localhost", Port: 8080}, captured)
|
||||
})
|
||||
|
||||
t.Run("multiple transformations", func(t *testing.T) {
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Body string
|
||||
}
|
||||
|
||||
var capturedStatus int
|
||||
consumeStatus := func(code int) {
|
||||
capturedStatus = code
|
||||
}
|
||||
|
||||
getStatus := func(r Response) int {
|
||||
return r.StatusCode
|
||||
}
|
||||
|
||||
consumeResponse := Local(getStatus)(consumeStatus)
|
||||
consumeResponse(Response{StatusCode: 200, Body: "OK"})
|
||||
|
||||
assert.Equal(t, 200, capturedStatus)
|
||||
})
|
||||
|
||||
t.Run("chaining Local transformations", func(t *testing.T) {
|
||||
type Level3 struct{ Value int }
|
||||
type Level2 struct{ L3 Level3 }
|
||||
type Level1 struct{ L2 Level2 }
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
// Chain multiple Local transformations
|
||||
extract3 := func(l3 Level3) int { return l3.Value }
|
||||
extract2 := func(l2 Level2) Level3 { return l2.L3 }
|
||||
extract1 := func(l1 Level1) Level2 { return l1.L2 }
|
||||
|
||||
// Compose the transformations
|
||||
consumeLevel3 := Local(extract3)(consumeInt)
|
||||
consumeLevel2 := Local(extract2)(consumeLevel3)
|
||||
consumeLevel1 := Local(extract1)(consumeLevel2)
|
||||
|
||||
consumeLevel1(Level1{L2: Level2{L3: Level3{Value: 42}}})
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
|
||||
t.Run("identity transformation", func(t *testing.T) {
|
||||
var captured string
|
||||
consumeString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
identity := function.Identity[string]
|
||||
|
||||
consumeIdentity := Local(identity)(consumeString)
|
||||
consumeIdentity("test")
|
||||
|
||||
assert.Equal(t, "test", captured)
|
||||
})
|
||||
|
||||
t.Run("transformation with calculation", func(t *testing.T) {
|
||||
type Rectangle struct {
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
var capturedArea int
|
||||
consumeArea := func(area int) {
|
||||
capturedArea = area
|
||||
}
|
||||
|
||||
calculateArea := func(r Rectangle) int {
|
||||
return r.Width * r.Height
|
||||
}
|
||||
|
||||
consumeRectangle := Local(calculateArea)(consumeArea)
|
||||
consumeRectangle(Rectangle{Width: 5, Height: 10})
|
||||
|
||||
assert.Equal(t, 50, capturedArea)
|
||||
})
|
||||
|
||||
t.Run("multiple consumers with same transformation", func(t *testing.T) {
|
||||
type Event struct {
|
||||
Type string
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
var capturedType string
|
||||
var capturedTime time.Time
|
||||
|
||||
consumeType := func(t string) {
|
||||
capturedType = t
|
||||
}
|
||||
|
||||
consumeTime := func(t time.Time) {
|
||||
capturedTime = t
|
||||
}
|
||||
|
||||
extractType := func(e Event) string { return e.Type }
|
||||
extractTime := func(e Event) time.Time { return e.Timestamp }
|
||||
|
||||
consumeEventType := Local(extractType)(consumeType)
|
||||
consumeEventTime := Local(extractTime)(consumeTime)
|
||||
|
||||
now := time.Now()
|
||||
event := Event{Type: "UserLogin", Timestamp: now}
|
||||
|
||||
consumeEventType(event)
|
||||
consumeEventTime(event)
|
||||
|
||||
assert.Equal(t, "UserLogin", capturedType)
|
||||
assert.Equal(t, now, capturedTime)
|
||||
})
|
||||
|
||||
t.Run("transformation with slice", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeLength := func(n int) {
|
||||
captured = n
|
||||
}
|
||||
|
||||
getLength := func(s []string) int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
consumeSlice := Local(getLength)(consumeLength)
|
||||
consumeSlice([]string{"a", "b", "c"})
|
||||
|
||||
assert.Equal(t, 3, captured)
|
||||
})
|
||||
|
||||
t.Run("transformation with map", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeCount := func(n int) {
|
||||
captured = n
|
||||
}
|
||||
|
||||
getCount := func(m map[string]int) int {
|
||||
return len(m)
|
||||
}
|
||||
|
||||
consumeMap := Local(getCount)(consumeCount)
|
||||
consumeMap(map[string]int{"a": 1, "b": 2, "c": 3})
|
||||
|
||||
assert.Equal(t, 3, captured)
|
||||
})
|
||||
|
||||
t.Run("transformation with pointer", func(t *testing.T) {
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
dereference := func(p *int) int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return *p
|
||||
}
|
||||
|
||||
consumePointer := Local(dereference)(consumeInt)
|
||||
|
||||
value := 42
|
||||
consumePointer(&value)
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumePointer(nil)
|
||||
assert.Equal(t, 0, captured)
|
||||
})
|
||||
|
||||
t.Run("transformation with custom type", func(t *testing.T) {
|
||||
type MyType struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
var captured string
|
||||
consumeString := func(s string) {
|
||||
captured = s
|
||||
}
|
||||
|
||||
extractValue := func(m MyType) string {
|
||||
return m.Value
|
||||
}
|
||||
|
||||
consumeMyType := Local(extractValue)(consumeString)
|
||||
consumeMyType(MyType{Value: "test"})
|
||||
|
||||
assert.Equal(t, "test", captured)
|
||||
})
|
||||
|
||||
t.Run("accumulation through multiple calls", func(t *testing.T) {
|
||||
var sum int
|
||||
accumulate := func(x int) {
|
||||
sum += x
|
||||
}
|
||||
|
||||
double := func(x int) int {
|
||||
return x * 2
|
||||
}
|
||||
|
||||
accumulateDoubled := Local(double)(accumulate)
|
||||
|
||||
accumulateDoubled(1)
|
||||
accumulateDoubled(2)
|
||||
accumulateDoubled(3)
|
||||
|
||||
assert.Equal(t, 12, sum) // (1*2) + (2*2) + (3*2) = 2 + 4 + 6 = 12
|
||||
})
|
||||
|
||||
t.Run("transformation with error handling", func(t *testing.T) {
|
||||
type Result struct {
|
||||
Value int
|
||||
Error error
|
||||
}
|
||||
|
||||
var captured int
|
||||
consumeInt := func(x int) {
|
||||
captured = x
|
||||
}
|
||||
|
||||
extractValue := func(r Result) int {
|
||||
if r.Error != nil {
|
||||
return -1
|
||||
}
|
||||
return r.Value
|
||||
}
|
||||
|
||||
consumeResult := Local(extractValue)(consumeInt)
|
||||
|
||||
consumeResult(Result{Value: 42, Error: nil})
|
||||
assert.Equal(t, 42, captured)
|
||||
|
||||
consumeResult(Result{Value: 100, Error: assert.AnError})
|
||||
assert.Equal(t, -1, captured)
|
||||
})
|
||||
|
||||
t.Run("transformation preserves consumer behavior", func(t *testing.T) {
|
||||
callCount := 0
|
||||
consumer := func(x int) {
|
||||
callCount++
|
||||
}
|
||||
|
||||
transform := func(s string) int {
|
||||
n, _ := strconv.Atoi(s)
|
||||
return n
|
||||
}
|
||||
|
||||
transformedConsumer := Local(transform)(consumer)
|
||||
|
||||
transformedConsumer("1")
|
||||
transformedConsumer("2")
|
||||
transformedConsumer("3")
|
||||
|
||||
assert.Equal(t, 3, callCount)
|
||||
})
|
||||
|
||||
t.Run("comparison with reader.Local behavior", func(t *testing.T) {
|
||||
// This test demonstrates the dual nature of Consumer and Reader
|
||||
// Consumer: transforms input before consumption (contravariant)
|
||||
// Reader: transforms environment before reading (also contravariant on input)
|
||||
|
||||
type DetailedEnv struct {
|
||||
Value int
|
||||
Extra string
|
||||
}
|
||||
|
||||
type SimpleEnv struct {
|
||||
Value int
|
||||
}
|
||||
|
||||
var captured int
|
||||
consumeSimple := func(e SimpleEnv) {
|
||||
captured = e.Value
|
||||
}
|
||||
|
||||
simplify := func(d DetailedEnv) SimpleEnv {
|
||||
return SimpleEnv{Value: d.Value}
|
||||
}
|
||||
|
||||
consumeDetailed := Local(simplify)(consumeSimple)
|
||||
consumeDetailed(DetailedEnv{Value: 42, Extra: "ignored"})
|
||||
|
||||
assert.Equal(t, 42, captured)
|
||||
})
|
||||
}
|
||||
56
v2/consumer/types.go
Normal file
56
v2/consumer/types.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package consumer provides types and utilities for functions that consume values without returning results.
|
||||
//
|
||||
// A Consumer represents a side-effecting operation that accepts a value but produces no output.
|
||||
// This is useful for operations like logging, printing, updating state, or any action where
|
||||
// the return value is not needed.
|
||||
package consumer
|
||||
|
||||
type (
|
||||
// Consumer represents a function that accepts a value of type A and performs a side effect.
|
||||
// It does not return any value, making it useful for operations where only the side effect matters,
|
||||
// such as logging, printing, or updating external state.
|
||||
//
|
||||
// This is a fundamental concept in functional programming for handling side effects in a
|
||||
// controlled manner. Consumers can be composed, chained, or used in higher-order functions
|
||||
// to build complex side-effecting behaviors.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of value consumed by the function
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // A simple consumer that prints values
|
||||
// var printInt Consumer[int] = func(x int) {
|
||||
// fmt.Println(x)
|
||||
// }
|
||||
// printInt(42) // Prints: 42
|
||||
//
|
||||
// // A consumer that logs messages
|
||||
// var logger Consumer[string] = func(msg string) {
|
||||
// log.Println(msg)
|
||||
// }
|
||||
// logger("Hello, World!") // Logs: Hello, World!
|
||||
//
|
||||
// // Consumers can be used in functional pipelines
|
||||
// var saveToDatabase Consumer[User] = func(user User) {
|
||||
// db.Save(user)
|
||||
// }
|
||||
Consumer[A any] = func(A)
|
||||
|
||||
Operator[A, B any] = func(Consumer[A]) Consumer[B]
|
||||
)
|
||||
13
v2/context/readerio/consumer.go
Normal file
13
v2/context/readerio/consumer.go
Normal 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))
|
||||
}
|
||||
25
v2/context/readerio/rec.go
Normal file
25
v2/context/readerio/rec.go
Normal 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)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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]))
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
13
v2/context/readerioresult/consumer.go
Normal file
13
v2/context/readerioresult/consumer.go
Normal 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))
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
184
v2/context/readerioresult/rec.go
Normal file
184
v2/context/readerioresult/rec.go
Normal 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))
|
||||
}
|
||||
432
v2/context/readerioresult/rec_test.go
Normal file
432
v2/context/readerioresult/rec_test.go
Normal file
@@ -0,0 +1,432 @@
|
||||
// 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"
|
||||
|
||||
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 len(state.items) == 0 {
|
||||
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 len(state.files) == 0 {
|
||||
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)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,6 +31,8 @@ import (
|
||||
// TenantID string
|
||||
// }
|
||||
// result := readereither.Do(State{})
|
||||
//
|
||||
//go:inline
|
||||
func Do[S any](
|
||||
empty S,
|
||||
) ReaderResult[S] {
|
||||
@@ -78,14 +80,18 @@ func Do[S any](
|
||||
// },
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func Bind[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f Kleisli[S1, T],
|
||||
) Kleisli[ReaderResult[S1], S2] {
|
||||
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, f)
|
||||
return G.Bind[ReaderResult[S1], ReaderResult[S2]](setter, F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
// Let attaches the result of a computation to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func Let[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
f func(S1) T,
|
||||
@@ -94,6 +100,8 @@ func Let[S1, S2, T any](
|
||||
}
|
||||
|
||||
// LetTo attaches the a value to a context [S1] to produce a context [S2]
|
||||
//
|
||||
//go:inline
|
||||
func LetTo[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
b T,
|
||||
@@ -102,6 +110,8 @@ func LetTo[S1, S2, T any](
|
||||
}
|
||||
|
||||
// BindTo initializes a new state [S1] from a value [T]
|
||||
//
|
||||
//go:inline
|
||||
func BindTo[S1, T any](
|
||||
setter func(T) S1,
|
||||
) Kleisli[ReaderResult[T], S1] {
|
||||
@@ -145,6 +155,8 @@ func BindTo[S1, T any](
|
||||
// getTenantID,
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApS[S1, S2, T any](
|
||||
setter func(T) func(S1) S2,
|
||||
fa ReaderResult[T],
|
||||
@@ -183,6 +195,8 @@ func ApS[S1, S2, T any](
|
||||
// readereither.Do(Person{Name: "Alice", Age: 25}),
|
||||
// readereither.ApSL(ageLens, getAge),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func ApSL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
fa ReaderResult[T],
|
||||
@@ -227,11 +241,13 @@ func ApSL[S, T any](
|
||||
// readereither.Of[error](Counter{Value: 42}),
|
||||
// readereither.BindL(valueLens, increment),
|
||||
// )
|
||||
//
|
||||
//go:inline
|
||||
func BindL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f Kleisli[T, T],
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, f))
|
||||
return Bind(lens.Set, F.Flow2(lens.Get, F.Flow2(f, WithContext)))
|
||||
}
|
||||
|
||||
// LetL is a variant of Let that uses a lens to focus on a specific field in the state.
|
||||
@@ -262,9 +278,11 @@ func BindL[S, T any](
|
||||
// readereither.LetL(valueLens, double),
|
||||
// )
|
||||
// // result when executed will be Right(Counter{Value: 42})
|
||||
//
|
||||
//go:inline
|
||||
func LetL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
f func(T) T,
|
||||
f Endomorphism[T],
|
||||
) Kleisli[ReaderResult[S], S] {
|
||||
return Let(lens.Set, F.Flow2(lens.Get, f))
|
||||
}
|
||||
@@ -296,6 +314,8 @@ func LetL[S, T any](
|
||||
// readereither.LetToL(debugLens, false),
|
||||
// )
|
||||
// // result when executed will be Right(Config{Debug: false, Timeout: 30})
|
||||
//
|
||||
//go:inline
|
||||
func LetToL[S, T any](
|
||||
lens L.Lens[S, T],
|
||||
b T,
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
@@ -7,11 +22,131 @@ import (
|
||||
RR "github.com/IBM/fp-go/v2/readerresult"
|
||||
)
|
||||
|
||||
// SequenceReader swaps the order of environment parameters when the inner computation is a Reader.
|
||||
//
|
||||
// This function is specialized for the context.Context-based ReaderResult monad. It takes a
|
||||
// ReaderResult that produces a Reader and returns a reader.Kleisli that produces Results.
|
||||
// The context.Context is implicitly used as the outer environment type.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The inner environment type (becomes outer after flip)
|
||||
// - A: The success value type
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: A ReaderResult that takes context.Context and may produce a Reader[R, A]
|
||||
//
|
||||
// Returns:
|
||||
// - A reader.Kleisli[context.Context, R, Result[A]], which is func(context.Context) func(R) Result[A]
|
||||
//
|
||||
// The function preserves error handling from the outer ReaderResult layer. If the outer
|
||||
// computation fails, the error is propagated to the inner Result.
|
||||
//
|
||||
// Note: This is an inline wrapper around readerresult.SequenceReader, specialized for
|
||||
// context.Context as the outer environment type.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct {
|
||||
// ConnectionString string
|
||||
// }
|
||||
//
|
||||
// // Original: takes context, may fail, produces Reader[Database, string]
|
||||
// original := func(ctx context.Context) result.Result[reader.Reader[Database, string]] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[reader.Reader[Database, string]](ctx.Err())
|
||||
// }
|
||||
// return result.Ok[error](func(db Database) string {
|
||||
// return fmt.Sprintf("Query on %s", db.ConnectionString)
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// // Sequenced: takes context first, then Database
|
||||
// sequenced := SequenceReader(original)
|
||||
//
|
||||
// ctx := context.Background()
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
//
|
||||
// // Apply context first to get a function that takes database
|
||||
// dbReader := sequenced(ctx)
|
||||
// // Then apply database to get the final result
|
||||
// result := dbReader(db)
|
||||
// // result is Result[string]
|
||||
//
|
||||
// Use Cases:
|
||||
// - Dependency injection: Flip parameter order to inject context first, then dependencies
|
||||
// - Testing: Separate context handling from business logic for easier testing
|
||||
// - Composition: Enable point-free style by fixing the context parameter first
|
||||
//
|
||||
//go:inline
|
||||
func SequenceReader[R, A any](ma ReaderResult[Reader[R, A]]) reader.Kleisli[context.Context, R, Result[A]] {
|
||||
return RR.SequenceReader(ma)
|
||||
}
|
||||
|
||||
// TraverseReader transforms a value using a Reader function and swaps environment parameter order.
|
||||
//
|
||||
// This function combines mapping and parameter flipping in a single operation. It takes a
|
||||
// Reader function (pure computation without error handling) and returns a function that:
|
||||
// 1. Maps a ReaderResult[A] to ReaderResult[B] using the provided Reader function
|
||||
// 2. Flips the parameter order so R comes before context.Context
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: The inner environment type (becomes outer after flip)
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A reader.Kleisli[R, A, B], which is func(R) func(A) B - a pure Reader function
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes ReaderResult[A] and returns Kleisli[R, B]
|
||||
// - Kleisli[R, B] is func(R) ReaderResult[B], which is func(R) func(context.Context) Result[B]
|
||||
//
|
||||
// The function preserves error handling from the input ReaderResult. If the input computation
|
||||
// fails, the error is propagated without applying the transformation function.
|
||||
//
|
||||
// Note: This is a wrapper around readerresult.TraverseReader, specialized for context.Context.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxRetries int
|
||||
// }
|
||||
//
|
||||
// // A pure Reader function that depends on Config
|
||||
// formatMessage := func(cfg Config) func(int) string {
|
||||
// return func(value int) string {
|
||||
// return fmt.Sprintf("Value: %d, MaxRetries: %d", value, cfg.MaxRetries)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Original computation that may fail
|
||||
// computation := func(ctx context.Context) result.Result[int] {
|
||||
// if ctx.Err() != nil {
|
||||
// return result.Error[int](ctx.Err())
|
||||
// }
|
||||
// return result.Ok[error](42)
|
||||
// }
|
||||
//
|
||||
// // Create a traversal that applies formatMessage and flips parameters
|
||||
// traverse := TraverseReader[Config, int, string](formatMessage)
|
||||
//
|
||||
// // Apply to the computation
|
||||
// flipped := traverse(computation)
|
||||
//
|
||||
// // Now we can provide Config first, then context
|
||||
// cfg := Config{MaxRetries: 3}
|
||||
// ctx := context.Background()
|
||||
//
|
||||
// result := flipped(cfg)(ctx)
|
||||
// // result is Result[string] containing "Value: 42, MaxRetries: 3"
|
||||
//
|
||||
// Use Cases:
|
||||
// - Dependency injection: Inject configuration/dependencies before context
|
||||
// - Testing: Separate pure business logic from context handling
|
||||
// - Composition: Build pipelines where dependencies are fixed before execution
|
||||
// - Point-free style: Enable partial application by fixing dependencies first
|
||||
//
|
||||
//go:inline
|
||||
func TraverseReader[R, A, B any](
|
||||
f reader.Kleisli[R, A, B],
|
||||
) func(ReaderResult[A]) Kleisli[R, B] {
|
||||
|
||||
215
v2/context/readerresult/logging.go
Normal file
215
v2/context/readerresult/logging.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package readerresult provides logging utilities for the ReaderResult monad,
|
||||
// which combines the Reader monad (for dependency injection via context.Context)
|
||||
// with the Result monad (for error handling).
|
||||
//
|
||||
// The logging functions in this package allow you to log Result values (both
|
||||
// successes and errors) while preserving the functional composition style.
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
)
|
||||
|
||||
// curriedLog creates a curried logging function that takes an slog.Attr and a context,
|
||||
// then logs the attribute with the specified log level and message.
|
||||
//
|
||||
// This is an internal helper function used to create the logging pipeline in a
|
||||
// point-free style. The currying allows for partial application in functional
|
||||
// composition.
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelError)
|
||||
// - cb: A callback function that retrieves a logger from the context
|
||||
// - message: The log message to display
|
||||
//
|
||||
// Returns:
|
||||
// - A curried function that takes an slog.Attr, then a context, and performs logging
|
||||
func curriedLog(
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) func(slog.Attr) Reader[context.Context, struct{}] {
|
||||
return F.Curry2(func(a slog.Attr, ctx context.Context) struct{} {
|
||||
cb(ctx).LogAttrs(ctx, logLevel, message, a)
|
||||
return struct{}{}
|
||||
})
|
||||
}
|
||||
|
||||
// SLogWithCallback creates a Kleisli arrow that logs a Result value using a custom
|
||||
// logger callback and log level. The Result value is logged and then returned unchanged,
|
||||
// making this function suitable for use in functional pipelines.
|
||||
//
|
||||
// This function logs both successful values and errors:
|
||||
// - Success values are logged with the key "value"
|
||||
// - Error values are logged with the key "error"
|
||||
//
|
||||
// The logging is performed as a side effect while preserving the Result value,
|
||||
// allowing it to be used in the middle of a computation pipeline without
|
||||
// interrupting the flow.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value in the Result
|
||||
//
|
||||
// Parameters:
|
||||
// - logLevel: The slog.Level at which to log (e.g., LevelInfo, LevelDebug, LevelError)
|
||||
// - cb: A callback function that retrieves a *slog.Logger from the context
|
||||
// - message: The log message to display
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
|
||||
// The returned ReaderResult, when executed with a context, logs the Result
|
||||
// and returns it unchanged
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// }
|
||||
//
|
||||
// // Custom logger callback
|
||||
// getLogger := func(ctx context.Context) *slog.Logger {
|
||||
// return slog.Default()
|
||||
// }
|
||||
//
|
||||
// // Create a logging function for debug level
|
||||
// logDebug := SLogWithCallback[User](slog.LevelDebug, getLogger, "User data")
|
||||
//
|
||||
// // Use in a pipeline
|
||||
// ctx := context.Background()
|
||||
// user := result.Of(User{ID: 123, Name: "Alice"})
|
||||
// logged := logDebug(user)(ctx) // Logs: level=DEBUG msg="User data" value={ID:123 Name:Alice}
|
||||
// // logged still contains the User value
|
||||
//
|
||||
// Example with error:
|
||||
//
|
||||
// err := errors.New("user not found")
|
||||
// userResult := result.Left[User](err)
|
||||
// logged := logDebug(userResult)(ctx) // Logs: level=DEBUG msg="User data" error="user not found"
|
||||
// // logged still contains the error
|
||||
func SLogWithCallback[A any](
|
||||
logLevel slog.Level,
|
||||
cb func(context.Context) *slog.Logger,
|
||||
message string) Kleisli[Result[A], A] {
|
||||
|
||||
return F.Pipe1(
|
||||
F.Flow2(
|
||||
result.ToSLogAttr[A](),
|
||||
curriedLog(logLevel, cb, message),
|
||||
),
|
||||
reader.Chain(reader.Sequence(F.Flow2( // this flow is basically the `MapTo` function with side effects
|
||||
reader.Of[struct{}, Result[A]],
|
||||
reader.Map[context.Context, struct{}, Result[A]],
|
||||
))),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// SLog creates a Kleisli arrow that logs a Result value at INFO level using the
|
||||
// logger from the context. This is a convenience function that uses SLogWithCallback
|
||||
// with default settings.
|
||||
//
|
||||
// The Result value is logged and then returned unchanged, making this function
|
||||
// suitable for use in functional pipelines for debugging or monitoring purposes.
|
||||
//
|
||||
// This function logs both successful values and errors:
|
||||
// - Success values are logged with the key "value"
|
||||
// - Error values are logged with the key "error"
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value in the Result
|
||||
//
|
||||
// Parameters:
|
||||
// - message: The log message to display
|
||||
//
|
||||
// Returns:
|
||||
// - A Kleisli arrow that takes a Result[A] and returns a ReaderResult[A]
|
||||
// The returned ReaderResult, when executed with a context, logs the Result
|
||||
// at INFO level and returns it unchanged
|
||||
//
|
||||
// Example - Logging a successful computation:
|
||||
//
|
||||
// ctx := context.Background()
|
||||
//
|
||||
// // Simple value logging
|
||||
// res := result.Of(42)
|
||||
// logged := SLog[int]("Processing number")(res)(ctx)
|
||||
// // Logs: level=INFO msg="Processing number" value=42
|
||||
// // logged == result.Of(42)
|
||||
//
|
||||
// Example - Logging in a pipeline:
|
||||
//
|
||||
// type User struct {
|
||||
// ID int
|
||||
// Name string
|
||||
// }
|
||||
//
|
||||
// fetchUser := func(id int) result.Result[User] {
|
||||
// return result.Of(User{ID: id, Name: "Alice"})
|
||||
// }
|
||||
//
|
||||
// processUser := func(user User) result.Result[string] {
|
||||
// return result.Of(fmt.Sprintf("Processed: %s", user.Name))
|
||||
// }
|
||||
//
|
||||
// ctx := context.Background()
|
||||
//
|
||||
// // Log at each step
|
||||
// userResult := fetchUser(123)
|
||||
// logged1 := SLog[User]("Fetched user")(userResult)(ctx)
|
||||
// // Logs: level=INFO msg="Fetched user" value={ID:123 Name:Alice}
|
||||
//
|
||||
// processed := result.Chain(processUser)(logged1)
|
||||
// logged2 := SLog[string]("Processed user")(processed)(ctx)
|
||||
// // Logs: level=INFO msg="Processed user" value="Processed: Alice"
|
||||
//
|
||||
// Example - Logging errors:
|
||||
//
|
||||
// err := errors.New("database connection failed")
|
||||
// errResult := result.Left[User](err)
|
||||
// logged := SLog[User]("Database operation")(errResult)(ctx)
|
||||
// // Logs: level=INFO msg="Database operation" error="database connection failed"
|
||||
// // logged still contains the error
|
||||
//
|
||||
// Example - Using with context logger:
|
||||
//
|
||||
// // Set up a custom logger in the context
|
||||
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
// ctx := logging.WithLogger(logger)(context.Background())
|
||||
//
|
||||
// res := result.Of("important data")
|
||||
// logged := SLog[string]("Critical operation")(res)(ctx)
|
||||
// // Uses the logger from context to log the message
|
||||
//
|
||||
// Note: The function uses logging.GetLoggerFromContext to retrieve the logger,
|
||||
// which falls back to the global logger if no logger is found in the context.
|
||||
//
|
||||
//go:inline
|
||||
func SLog[A any](message string) Kleisli[Result[A], A] {
|
||||
return SLogWithCallback[A](slog.LevelInfo, logging.GetLoggerFromContext, message)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func TapSLog[A any](message string) Operator[A, A] {
|
||||
return reader.Chain(SLog[A](message))
|
||||
}
|
||||
302
v2/context/readerresult/logging_test.go
Normal file
302
v2/context/readerresult/logging_test.go
Normal file
@@ -0,0 +1,302 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"testing"
|
||||
|
||||
"github.com/IBM/fp-go/v2/logging"
|
||||
N "github.com/IBM/fp-go/v2/number"
|
||||
"github.com/IBM/fp-go/v2/result"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestSLogLogsSuccessValue tests that SLog logs successful Result values
|
||||
func TestSLogLogsSuccessValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a Result and log it
|
||||
res1 := result.Of(42)
|
||||
logged := SLog[int]("Result value")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Result value")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
}
|
||||
|
||||
// TestSLogLogsErrorValue tests that SLog logs error Result values
|
||||
func TestSLogLogsErrorValue(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("test error")
|
||||
|
||||
// Create an error Result and log it
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLog[int]("Result value")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, res1, logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Result value")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "test error")
|
||||
}
|
||||
|
||||
// TestSLogInPipeline tests SLog in a functional pipeline
|
||||
func TestSLogInPipeline(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// SLog takes a Result[A] and returns ReaderResult[A]
|
||||
// So we need to start with a Result, apply SLog, then execute with context
|
||||
res1 := result.Of(10)
|
||||
logged := SLog[int]("Initial value")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(10), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Initial value")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
}
|
||||
|
||||
// TestSLogWithContextLogger tests SLog using logger from context
|
||||
func TestSLogWithContextLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
contextLogger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
ctx := logging.WithLogger(contextLogger)(context.Background())
|
||||
|
||||
res1 := result.Of("test value")
|
||||
logged := SLog[string]("Context logger test")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of("test value"), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Context logger test")
|
||||
assert.Contains(t, logOutput, `value="test value"`)
|
||||
}
|
||||
|
||||
// TestSLogDisabled tests that SLog respects logger level
|
||||
func TestSLogDisabled(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
// Create logger with level that disables info logs
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelError, // Only log errors
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
res1 := result.Of(42)
|
||||
logged := SLog[int]("This should not be logged")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
// Should have no logs since level is ERROR
|
||||
logOutput := buf.String()
|
||||
assert.Empty(t, logOutput, "Should have no logs when logging is disabled")
|
||||
}
|
||||
|
||||
// TestSLogWithStruct tests SLog with structured data
|
||||
func TestSLogWithStruct(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
user := User{ID: 123, Name: "Alice"}
|
||||
|
||||
res1 := result.Of(user)
|
||||
logged := SLog[User]("User data")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(user), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "User data")
|
||||
assert.Contains(t, logOutput, "ID:123")
|
||||
assert.Contains(t, logOutput, "Name:Alice")
|
||||
}
|
||||
|
||||
// TestSLogWithCallbackCustomLevel tests SLogWithCallback with custom log level
|
||||
func TestSLogWithCallbackCustomLevel(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
}))
|
||||
|
||||
customCallback := func(ctx context.Context) *slog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a Result and log it with custom callback
|
||||
res1 := result.Of(42)
|
||||
logged := SLogWithCallback[int](slog.LevelDebug, customCallback, "Debug result")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(42), logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Debug result")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
assert.Contains(t, logOutput, "level=DEBUG")
|
||||
}
|
||||
|
||||
// TestSLogWithCallbackLogsError tests SLogWithCallback logs errors
|
||||
func TestSLogWithCallbackLogsError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelWarn,
|
||||
}))
|
||||
|
||||
customCallback := func(ctx context.Context) *slog.Logger {
|
||||
return logger
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("warning error")
|
||||
|
||||
// Create an error Result and log it with custom callback
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLogWithCallback[int](slog.LevelWarn, customCallback, "Warning result")(res1)(ctx)
|
||||
|
||||
assert.Equal(t, res1, logged)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Warning result")
|
||||
assert.Contains(t, logOutput, "error")
|
||||
assert.Contains(t, logOutput, "warning error")
|
||||
assert.Contains(t, logOutput, "level=WARN")
|
||||
}
|
||||
|
||||
// TestSLogChainedOperations tests SLog in chained operations
|
||||
func TestSLogChainedOperations(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// First log step 1
|
||||
res1 := result.Of(5)
|
||||
logged1 := SLog[int]("Step 1")(res1)(ctx)
|
||||
|
||||
// Then log step 2 with doubled value
|
||||
res2 := result.Map(N.Mul(2))(logged1)
|
||||
logged2 := SLog[int]("Step 2")(res2)(ctx)
|
||||
|
||||
assert.Equal(t, result.Of(10), logged2)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Step 1")
|
||||
assert.Contains(t, logOutput, "value=5")
|
||||
assert.Contains(t, logOutput, "Step 2")
|
||||
assert.Contains(t, logOutput, "value=10")
|
||||
}
|
||||
|
||||
// TestSLogPreservesError tests that SLog preserves error through the pipeline
|
||||
func TestSLogPreservesError(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("original error")
|
||||
|
||||
res1 := result.Left[int](testErr)
|
||||
logged := SLog[int]("Logging error")(res1)(ctx)
|
||||
|
||||
// Apply map to verify error is preserved
|
||||
res2 := result.Map(N.Mul(2))(logged)
|
||||
|
||||
assert.Equal(t, res1, res2)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Logging error")
|
||||
assert.Contains(t, logOutput, "original error")
|
||||
}
|
||||
|
||||
// TestSLogMultipleValues tests logging multiple different values
|
||||
func TestSLogMultipleValues(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
oldLogger := logging.SetLogger(logger)
|
||||
defer logging.SetLogger(oldLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with different types
|
||||
intRes := SLog[int]("Integer")(result.Of(42))(ctx)
|
||||
assert.Equal(t, result.Of(42), intRes)
|
||||
|
||||
strRes := SLog[string]("String")(result.Of("hello"))(ctx)
|
||||
assert.Equal(t, result.Of("hello"), strRes)
|
||||
|
||||
boolRes := SLog[bool]("Boolean")(result.Of(true))(ctx)
|
||||
assert.Equal(t, result.Of(true), boolRes)
|
||||
|
||||
logOutput := buf.String()
|
||||
assert.Contains(t, logOutput, "Integer")
|
||||
assert.Contains(t, logOutput, "value=42")
|
||||
assert.Contains(t, logOutput, "String")
|
||||
assert.Contains(t, logOutput, "value=hello")
|
||||
assert.Contains(t, logOutput, "Boolean")
|
||||
assert.Contains(t, logOutput, "value=true")
|
||||
}
|
||||
@@ -18,9 +18,17 @@ package readerresult
|
||||
import (
|
||||
"context"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/chain"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/reader"
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
func FromReader[A any](r Reader[context.Context, A]) ReaderResult[A] {
|
||||
return readereither.FromReader[error](r)
|
||||
}
|
||||
|
||||
func FromEither[A any](e Either[A]) ReaderResult[A] {
|
||||
return readereither.FromEither[context.Context](e)
|
||||
}
|
||||
@@ -42,11 +50,11 @@ func Map[A, B any](f func(A) B) Operator[A, B] {
|
||||
}
|
||||
|
||||
func MonadChain[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[B] {
|
||||
return readereither.MonadChain(ma, f)
|
||||
return readereither.MonadChain(ma, F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
func Chain[A, B any](f Kleisli[A, B]) Operator[A, B] {
|
||||
return readereither.Chain(f)
|
||||
return readereither.Chain(F.Flow2(f, WithContext))
|
||||
}
|
||||
|
||||
func Of[A any](a A) ReaderResult[A] {
|
||||
@@ -66,7 +74,7 @@ func FromPredicate[A any](pred func(A) bool, onFalse func(A) error) Kleisli[A, A
|
||||
}
|
||||
|
||||
func OrElse[A any](onLeft Kleisli[error, A]) Kleisli[ReaderResult[A], A] {
|
||||
return readereither.OrElse(onLeft)
|
||||
return readereither.OrElse(F.Flow2(onLeft, WithContext))
|
||||
}
|
||||
|
||||
func Ask() ReaderResult[context.Context] {
|
||||
@@ -81,7 +89,7 @@ func ChainEitherK[A, B any](f func(A) Either[B]) func(ma ReaderResult[A]) Reader
|
||||
return readereither.ChainEitherK[context.Context](f)
|
||||
}
|
||||
|
||||
func ChainOptionK[A, B any](onNone func() error) func(func(A) Option[B]) Operator[A, B] {
|
||||
func ChainOptionK[A, B any](onNone func() error) func(option.Kleisli[A, B]) Operator[A, B] {
|
||||
return readereither.ChainOptionK[context.Context, A, B](onNone)
|
||||
}
|
||||
|
||||
@@ -97,3 +105,197 @@ func Flap[B, A any](a A) Operator[func(A) B, B] {
|
||||
func Read[A any](r context.Context) func(ReaderResult[A]) Result[A] {
|
||||
return readereither.Read[error, A](r)
|
||||
}
|
||||
|
||||
// MonadMapTo executes a ReaderResult computation, discards its success value, and returns a constant value.
|
||||
// This is the monadic version that takes both the ReaderResult and the constant value as parameters.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadMapTo WILL
|
||||
// execute the original ReaderResult to allow any side effects to occur, then discard the success result
|
||||
// and return the constant value. If the original computation fails, the error is preserved.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||
// - B: The type of the constant value to return on success
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The ReaderResult to execute (side effects will occur, success value discarded)
|
||||
// - b: The constant value to return if ma succeeds
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult that executes ma, preserves errors, but replaces success values with b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int }
|
||||
// increment := func(ctx context.Context) result.Result[int] {
|
||||
// // Side effect: log the operation
|
||||
// fmt.Println("incrementing")
|
||||
// return result.Of(5)
|
||||
// }
|
||||
// r := readerresult.MonadMapTo(increment, "done")
|
||||
// result := r(context.Background()) // Prints "incrementing", returns Right("done")
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[A, B any](ma ReaderResult[A], b B) ReaderResult[B] {
|
||||
return MonadMap(ma, reader.Of[A](b))
|
||||
}
|
||||
|
||||
// MapTo creates an operator that executes a ReaderResult computation, discards its success value,
|
||||
// and returns a constant value. This is the curried version where the constant value is provided first,
|
||||
// returning a function that can be applied to any ReaderResult.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MapTo WILL
|
||||
// execute the input ReaderResult to allow any side effects to occur, then discard the success result
|
||||
// and return the constant value. If the computation fails, the error is preserved.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the input ReaderResult (will be discarded if successful)
|
||||
// - B: The type of the constant value to return on success
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to return on success
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that executes a ReaderResult[A], preserves errors, but replaces success with b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logStep := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("step executed")
|
||||
// return result.Of(42)
|
||||
// }
|
||||
// toDone := readerresult.MapTo[int, string]("done")
|
||||
// pipeline := toDone(logStep)
|
||||
// result := pipeline(context.Background()) // Prints "step executed", returns Right("done")
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// step1 := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("processing")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// readerresult.MapTo[int, string]("complete"),
|
||||
// )
|
||||
// output := pipeline(context.Background()) // Prints "processing", returns Right("complete")
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[A, B any](b B) Operator[A, B] {
|
||||
return Map(reader.Of[A](b))
|
||||
}
|
||||
|
||||
// MonadChainTo sequences two ReaderResult computations where the second ignores the first's success value.
|
||||
// This is the monadic version that takes both ReaderResults as parameters.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, MonadChainTo WILL
|
||||
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
|
||||
// execute the second ReaderResult with the same context. If the first computation fails, the error is
|
||||
// returned immediately without executing the second computation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||
// - B: The success type of the second ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - ma: The first ReaderResult to execute (side effects will occur, success value discarded)
|
||||
// - b: The second ReaderResult to execute if ma succeeds
|
||||
//
|
||||
// Returns:
|
||||
// - A ReaderResult that executes ma, then b if ma succeeds, returning b's result
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logStart := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("starting")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// logEnd := func(ctx context.Context) result.Result[string] {
|
||||
// fmt.Println("ending")
|
||||
// return result.Of("done")
|
||||
// }
|
||||
// r := readerresult.MonadChainTo(logStart, logEnd)
|
||||
// result := r(context.Background()) // Prints "starting" then "ending", returns Right("done")
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainTo[A, B any](ma ReaderResult[A], b ReaderResult[B]) ReaderResult[B] {
|
||||
return MonadChain(ma, reader.Of[A](b))
|
||||
}
|
||||
|
||||
// ChainTo creates an operator that sequences two ReaderResult computations where the second ignores
|
||||
// the first's success value. This is the curried version where the second ReaderResult is provided first,
|
||||
// returning a function that can be applied to any first ReaderResult.
|
||||
//
|
||||
// IMPORTANT: ReaderResult represents a side-effectful computation because it depends on context.Context,
|
||||
// which is effectful (can be cancelled, has deadlines, carries values). For this reason, ChainTo WILL
|
||||
// execute the first ReaderResult to allow any side effects to occur, then discard the success result and
|
||||
// execute the second ReaderResult with the same context. If the first computation fails, the error is
|
||||
// returned immediately without executing the second computation.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The success type of the first ReaderResult (will be discarded if successful)
|
||||
// - B: The success type of the second ReaderResult
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The second ReaderResult to execute after the first succeeds
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that executes the first ReaderResult, then b if successful
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logEnd := func(ctx context.Context) result.Result[string] {
|
||||
// fmt.Println("ending")
|
||||
// return result.Of("done")
|
||||
// }
|
||||
// thenLogEnd := readerresult.ChainTo[int, string](logEnd)
|
||||
//
|
||||
// logStart := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("starting")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// pipeline := thenLogEnd(logStart)
|
||||
// result := pipeline(context.Background()) // Prints "starting" then "ending", returns Right("done")
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// step1 := func(ctx context.Context) result.Result[int] {
|
||||
// fmt.Println("step 1")
|
||||
// return result.Of(1)
|
||||
// }
|
||||
// step2 := func(ctx context.Context) result.Result[string] {
|
||||
// fmt.Println("step 2")
|
||||
// return result.Of("complete")
|
||||
// }
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// readerresult.ChainTo[int, string](step2),
|
||||
// )
|
||||
// output := pipeline(context.Background()) // Prints "step 1" then "step 2", returns Right("complete")
|
||||
//
|
||||
//go:inline
|
||||
func ChainTo[A, B any](b ReaderResult[B]) Operator[A, B] {
|
||||
return Chain(reader.Of[A](b))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func MonadChainFirst[A, B any](ma ReaderResult[A], f Kleisli[A, B]) ReaderResult[A] {
|
||||
return chain.MonadChainFirst(
|
||||
MonadChain,
|
||||
MonadMap,
|
||||
ma,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirst[A, B any](f Kleisli[A, B]) Operator[A, A] {
|
||||
return chain.ChainFirst(
|
||||
Chain,
|
||||
Map,
|
||||
F.Flow2(f, WithContext),
|
||||
)
|
||||
}
|
||||
|
||||
315
v2/context/readerresult/reader_test.go
Normal file
315
v2/context/readerresult/reader_test.go
Normal file
@@ -0,0 +1,315 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package readerresult
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
// Apply MapTo operator
|
||||
toDone := MapTo[int]("done")
|
||||
resultReader := toDone(originalReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
// Verify the original reader WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original reader should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes reader in functional pipeline", func(t *testing.T) {
|
||||
executed := false
|
||||
step1 := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](100)
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
MapTo[int]("complete"),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error]("complete"), result)
|
||||
assert.True(t, executed, "original reader should be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("executes reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
|
||||
sideEffectOccurred = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
resultReader := MapTo[int](true)(readerWithSideEffect)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error](true), result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur")
|
||||
})
|
||||
|
||||
t.Run("preserves errors from original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
testErr := assert.AnError
|
||||
failingReader := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Left[int](testErr)
|
||||
}
|
||||
|
||||
resultReader := MapTo[int]("done")(failingReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
assert.True(t, executed, "failing reader should still be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("executes original reader and returns constant value on success", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(ctx context.Context) E.Either[error, int] {
|
||||
executed = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
// Apply MonadMapTo
|
||||
resultReader := MonadMapTo(originalReader, "done")
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
// Verify the original reader WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original reader should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes complex computation with side effects", func(t *testing.T) {
|
||||
computationExecuted := false
|
||||
complexReader := func(ctx context.Context) E.Either[error, string] {
|
||||
computationExecuted = true
|
||||
return E.Of[error]("complex result")
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(complexReader, 42)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error](42), result)
|
||||
assert.True(t, computationExecuted, "complex computation should be executed")
|
||||
})
|
||||
|
||||
t.Run("preserves errors from original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
testErr := assert.AnError
|
||||
failingReader := func(ctx context.Context) E.Either[error, []string] {
|
||||
executed = true
|
||||
return E.Left[[]string](testErr)
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(failingReader, 99)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[int](testErr), result)
|
||||
assert.True(t, executed, "failing reader should still be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainTo(t *testing.T) {
|
||||
t.Run("executes first reader then second reader on success", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
firstReader := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("result")
|
||||
}
|
||||
|
||||
// Apply ChainTo operator
|
||||
thenSecond := ChainTo[int](secondReader)
|
||||
resultReader := thenSecond(firstReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
// Verify both readers were executed
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.True(t, secondExecuted, "second reader should be executed")
|
||||
})
|
||||
|
||||
t.Run("executes both readers in functional pipeline", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
step1 := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](100)
|
||||
}
|
||||
|
||||
step2 := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("complete")
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
ChainTo[int](step2),
|
||||
)
|
||||
|
||||
result := pipeline(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error]("complete"), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed in pipeline")
|
||||
assert.True(t, secondExecuted, "second reader should be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("executes first reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(ctx context.Context) E.Either[error, int] {
|
||||
sideEffectOccurred = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, bool] {
|
||||
return E.Of[error](true)
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(readerWithSideEffect)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error](true), result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur in first reader")
|
||||
})
|
||||
|
||||
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
testErr := assert.AnError
|
||||
|
||||
failingReader := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Left[int](testErr)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("result")
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(failingReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[string](testErr), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.False(t, secondExecuted, "second reader should not be executed on error")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainTo(t *testing.T) {
|
||||
t.Run("executes first reader then second reader on success", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
firstReader := func(ctx context.Context) E.Either[error, int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error](42)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("result")
|
||||
}
|
||||
|
||||
// Apply MonadChainTo
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(context.Background())
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, E.Of[error]("result"), result)
|
||||
// Verify both readers were executed
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.True(t, secondExecuted, "second reader should be executed")
|
||||
})
|
||||
|
||||
t.Run("executes complex first computation with side effects", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
|
||||
complexFirstReader := func(ctx context.Context) E.Either[error, []int] {
|
||||
firstExecuted = true
|
||||
return E.Of[error]([]int{1, 2, 3})
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, string] {
|
||||
secondExecuted = true
|
||||
return E.Of[error]("done")
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(complexFirstReader, secondReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Of[error]("done"), result)
|
||||
assert.True(t, firstExecuted, "complex first computation should be executed")
|
||||
assert.True(t, secondExecuted, "second reader should be executed")
|
||||
})
|
||||
|
||||
t.Run("preserves error from first reader without executing second", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
secondExecuted := false
|
||||
testErr := assert.AnError
|
||||
|
||||
failingReader := func(ctx context.Context) E.Either[error, map[string]int] {
|
||||
firstExecuted = true
|
||||
return E.Left[map[string]int](testErr)
|
||||
}
|
||||
|
||||
secondReader := func(ctx context.Context) E.Either[error, float64] {
|
||||
secondExecuted = true
|
||||
return E.Of[error](3.14)
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(failingReader, secondReader)
|
||||
result := resultReader(context.Background())
|
||||
|
||||
assert.Equal(t, E.Left[float64](testErr), result)
|
||||
assert.True(t, firstExecuted, "first reader should be executed")
|
||||
assert.False(t, secondExecuted, "second reader should not be executed on error")
|
||||
})
|
||||
}
|
||||
106
v2/context/readerresult/rec.go
Normal file
106
v2/context/readerresult/rec.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
497
v2/context/readerresult/rec_test.go
Normal file
497
v2/context/readerresult/rec_test.go
Normal file
@@ -0,0 +1,497 @@
|
||||
// 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"
|
||||
|
||||
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 len(state.list) == 0 {
|
||||
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)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
9917
v2/coverage.out
9917
v2/coverage.out
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
34
v2/either/rec.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,9 +117,13 @@ func Nullary2[F1 ~func() T1, F2 ~func(T1) T2, T1, T2 any](f1 F1, f2 F2) func() T
|
||||
|
||||
// Curry2 takes a function with 2 parameters and returns a cascade of functions each taking only one parameter.
|
||||
// The inverse function is [Uncurry2]
|
||||
//go:inline
|
||||
func Curry2[FCT ~func(T0, T1) T2, T0, T1, T2 any](f FCT) func(T0) func(T1) T2 {
|
||||
//go:inline
|
||||
return func(t0 T0) func(t1 T1) T2 {
|
||||
//go:inline
|
||||
return func(t1 T1) T2 {
|
||||
//go:inline
|
||||
return f(t0, t1)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
127
v2/io/consumer.go
Normal file
127
v2/io/consumer.go
Normal 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
265
v2/io/consumer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
12
v2/io/doc.go
12
v2/io/doc.go
@@ -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:
|
||||
|
||||
@@ -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
90
v2/ioeither/consumer.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2023 - 2025 IBM Corp.
|
||||
// All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package 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))
|
||||
}
|
||||
378
v2/ioeither/consumer_test.go
Normal file
378
v2/ioeither/consumer_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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]]]
|
||||
|
||||
@@ -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
114
v2/ioeither/rec.go
Normal 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)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
285
v2/ioeither/rec_test.go
Normal file
285
v2/ioeither/rec_test.go
Normal file
@@ -0,0 +1,285 @@
|
||||
// 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"
|
||||
|
||||
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 len(state.items) == 0 {
|
||||
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 len(state.items) == 0 {
|
||||
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
7
v2/ioeither/types.go
Normal 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
13
v2/iooption/consumer.go
Normal 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))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
123
v2/iooption/rec.go
Normal file
123
v2/iooption/rec.go
Normal 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
334
v2/iooption/rec_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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
15
v2/ioresult/consumer.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
25
v2/ioresult/rec.go
Normal file
25
v2/ioresult/rec.go
Normal 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)
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -19,6 +19,22 @@
|
||||
// or None, and does not contain a value. This is a type-safe alternative to using nil
|
||||
// pointers or special sentinel values to represent the absence of a value.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This implementation corresponds to the Fantasy Land Maybe type:
|
||||
// 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
|
||||
// - Foldable: https://github.com/fantasyland/fantasy-land#foldable
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// Create an Option with Some or None:
|
||||
|
||||
@@ -19,6 +19,18 @@
|
||||
// without explicitly threading it through every function call. It represents a computation that
|
||||
// depends on some external context of type R and produces a value of type A.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This implementation corresponds to the Fantasy Land Reader 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 Concept
|
||||
//
|
||||
// A Reader[R, A] is simply a function from R to A: func(R) A
|
||||
|
||||
@@ -64,6 +64,61 @@ func Sequence[R1, R2, A any](ma Reader[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return function.Flip(ma)
|
||||
}
|
||||
|
||||
// Traverse applies a Kleisli arrow to a value wrapped in a Reader, then sequences the result.
|
||||
// It transforms a Reader[R2, A] into a function that takes R1 and returns Reader[R2, B],
|
||||
// where the transformation from A to B is defined by a Kleisli arrow that depends on R1.
|
||||
//
|
||||
// This is useful when you have a Reader computation that produces a value, and you want to
|
||||
// apply another Reader computation to that value, but with a different environment type.
|
||||
// The result is a function that takes the second environment and returns a Reader that
|
||||
// takes the first environment.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R2: The first environment type (outer Reader)
|
||||
// - R1: The second environment type (inner Reader/Kleisli)
|
||||
// - A: The input value type
|
||||
// - B: The output value type
|
||||
//
|
||||
// Parameters:
|
||||
// - f: A Kleisli arrow from A to B that depends on environment R1
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Reader[R2, A] and returns a Kleisli[R2, R1, B]
|
||||
//
|
||||
// The signature can be understood as:
|
||||
// - Input: Reader[R2, A] (a computation that produces A given R2)
|
||||
// - Output: func(R1) Reader[R2, B] (a function that takes R1 and produces a computation that produces B given R2)
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Database struct { ConnectionString string }
|
||||
// type Config struct { TableName string }
|
||||
//
|
||||
// // A Reader that gets a user ID from the database
|
||||
// getUserID := func(db Database) int {
|
||||
// // Simulate database query
|
||||
// return 42
|
||||
// }
|
||||
//
|
||||
// // A Kleisli arrow that takes a user ID and returns a Reader that formats it with config
|
||||
// formatUser := func(id int) reader.Reader[Config, string] {
|
||||
// return func(c Config) string {
|
||||
// return fmt.Sprintf("User %d from table %s", id, c.TableName)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Traverse applies formatUser to the result of getUserID
|
||||
// traversed := reader.Traverse(formatUser)(getUserID)
|
||||
//
|
||||
// // Now we can apply both environments
|
||||
// config := Config{TableName: "users"}
|
||||
// db := Database{ConnectionString: "localhost:5432"}
|
||||
// result := traversed(config)(db) // "User 42 from table users"
|
||||
//
|
||||
// The Traverse operation is particularly useful when:
|
||||
// - You need to compose computations that depend on different environments
|
||||
// - You want to apply a transformation that itself requires environmental context
|
||||
// - You're building pipelines where each stage has its own configuration
|
||||
func Traverse[R2, R1, A, B any](
|
||||
f Kleisli[R1, A, B],
|
||||
) func(Reader[R2, A]) Kleisli[R2, R1, B] {
|
||||
|
||||
@@ -338,3 +338,371 @@ func TestSequenceEdgeCases(t *testing.T) {
|
||||
assert.Equal(t, "value: 42", sequenced(transform)(42))
|
||||
})
|
||||
}
|
||||
|
||||
func TestTraverse(t *testing.T) {
|
||||
t.Run("basic traverse with two environments", func(t *testing.T) {
|
||||
type Database struct {
|
||||
UserID int
|
||||
}
|
||||
type Config struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Reader that gets user ID from database
|
||||
getUserID := func(db Database) int {
|
||||
return db.UserID
|
||||
}
|
||||
|
||||
// Kleisli that formats user ID with config
|
||||
formatUser := func(id int) Reader[Config, string] {
|
||||
return func(c Config) string {
|
||||
return fmt.Sprintf("%s%d", c.Prefix, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse applies formatUser to the result of getUserID
|
||||
traversed := Traverse[Database](formatUser)(getUserID)
|
||||
|
||||
// Apply both environments
|
||||
config := Config{Prefix: "User-"}
|
||||
db := Database{UserID: 42}
|
||||
result := traversed(config)(db)
|
||||
|
||||
assert.Equal(t, "User-42", result)
|
||||
})
|
||||
|
||||
t.Run("traverse with computation", func(t *testing.T) {
|
||||
type Source struct {
|
||||
Value int
|
||||
}
|
||||
type Multiplier struct {
|
||||
Factor int
|
||||
}
|
||||
|
||||
// Reader that extracts value from source
|
||||
getValue := func(s Source) int {
|
||||
return s.Value
|
||||
}
|
||||
|
||||
// Kleisli that multiplies value with multiplier
|
||||
multiply := func(n int) Reader[Multiplier, int] {
|
||||
return func(m Multiplier) int {
|
||||
return n * m.Factor
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Source](multiply)(getValue)
|
||||
|
||||
source := Source{Value: 10}
|
||||
multiplier := Multiplier{Factor: 5}
|
||||
result := traversed(multiplier)(source)
|
||||
|
||||
assert.Equal(t, 50, result)
|
||||
})
|
||||
|
||||
t.Run("traverse with string transformation", func(t *testing.T) {
|
||||
type Input struct {
|
||||
Text string
|
||||
}
|
||||
type Format struct {
|
||||
Template string
|
||||
}
|
||||
|
||||
// Reader that gets text from input
|
||||
getText := func(i Input) string {
|
||||
return i.Text
|
||||
}
|
||||
|
||||
// Kleisli that formats text with template
|
||||
format := func(text string) Reader[Format, string] {
|
||||
return func(f Format) string {
|
||||
return fmt.Sprintf(f.Template, text)
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Input](format)(getText)
|
||||
|
||||
input := Input{Text: "world"}
|
||||
formatCfg := Format{Template: "Hello, %s!"}
|
||||
result := traversed(formatCfg)(input)
|
||||
|
||||
assert.Equal(t, "Hello, world!", result)
|
||||
})
|
||||
|
||||
t.Run("traverse with boolean logic", func(t *testing.T) {
|
||||
type Data struct {
|
||||
Value int
|
||||
}
|
||||
type Threshold struct {
|
||||
Limit int
|
||||
}
|
||||
|
||||
// Reader that gets value from data
|
||||
getValue := func(d Data) int {
|
||||
return d.Value
|
||||
}
|
||||
|
||||
// Kleisli that checks if value exceeds threshold
|
||||
checkThreshold := func(val int) Reader[Threshold, bool] {
|
||||
return func(t Threshold) bool {
|
||||
return val > t.Limit
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Data](checkThreshold)(getValue)
|
||||
|
||||
data := Data{Value: 100}
|
||||
threshold := Threshold{Limit: 50}
|
||||
result := traversed(threshold)(data)
|
||||
|
||||
assert.True(t, result)
|
||||
|
||||
threshold2 := Threshold{Limit: 150}
|
||||
result2 := traversed(threshold2)(data)
|
||||
|
||||
assert.False(t, result2)
|
||||
})
|
||||
|
||||
t.Run("traverse with slice transformation", func(t *testing.T) {
|
||||
type Source struct {
|
||||
Items []string
|
||||
}
|
||||
type Config struct {
|
||||
Separator string
|
||||
}
|
||||
|
||||
// Reader that gets items from source
|
||||
getItems := func(s Source) []string {
|
||||
return s.Items
|
||||
}
|
||||
|
||||
// Kleisli that joins items with separator
|
||||
joinItems := func(items []string) Reader[Config, string] {
|
||||
return func(c Config) string {
|
||||
result := ""
|
||||
for i, item := range items {
|
||||
if i > 0 {
|
||||
result += c.Separator
|
||||
}
|
||||
result += item
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Source](joinItems)(getItems)
|
||||
|
||||
source := Source{Items: []string{"a", "b", "c"}}
|
||||
config := Config{Separator: ", "}
|
||||
result := traversed(config)(source)
|
||||
|
||||
assert.Equal(t, "a, b, c", result)
|
||||
})
|
||||
|
||||
t.Run("traverse with struct transformation", func(t *testing.T) {
|
||||
type User struct {
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
type Database struct {
|
||||
TablePrefix string
|
||||
}
|
||||
type UserRecord struct {
|
||||
Table string
|
||||
ID int
|
||||
Name string
|
||||
}
|
||||
|
||||
// Reader that gets user
|
||||
getUser := func(db Database) User {
|
||||
return User{ID: 123, Name: "Alice"}
|
||||
}
|
||||
|
||||
// Kleisli that creates user record
|
||||
createRecord := func(user User) Reader[Database, UserRecord] {
|
||||
return func(db Database) UserRecord {
|
||||
return UserRecord{
|
||||
Table: db.TablePrefix + "users",
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Database](createRecord)(getUser)
|
||||
|
||||
db := Database{TablePrefix: "prod_"}
|
||||
result := traversed(db)(db)
|
||||
|
||||
assert.Equal(t, UserRecord{
|
||||
Table: "prod_users",
|
||||
ID: 123,
|
||||
Name: "Alice",
|
||||
}, result)
|
||||
})
|
||||
|
||||
t.Run("traverse with nil handling", func(t *testing.T) {
|
||||
type Source struct {
|
||||
Value *int
|
||||
}
|
||||
type Config struct {
|
||||
Default int
|
||||
}
|
||||
|
||||
// Reader that gets pointer value
|
||||
getValue := func(s Source) *int {
|
||||
return s.Value
|
||||
}
|
||||
|
||||
// Kleisli that handles nil with default
|
||||
handleNil := func(ptr *int) Reader[Config, int] {
|
||||
return func(c Config) int {
|
||||
if ptr == nil {
|
||||
return c.Default
|
||||
}
|
||||
return *ptr
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Source](handleNil)(getValue)
|
||||
|
||||
config := Config{Default: 999}
|
||||
|
||||
// Test with non-nil value
|
||||
val := 42
|
||||
source1 := Source{Value: &val}
|
||||
result1 := traversed(config)(source1)
|
||||
assert.Equal(t, 42, result1)
|
||||
|
||||
// Test with nil value
|
||||
source2 := Source{Value: nil}
|
||||
result2 := traversed(config)(source2)
|
||||
assert.Equal(t, 999, result2)
|
||||
})
|
||||
|
||||
t.Run("traverse composition", func(t *testing.T) {
|
||||
type Env1 struct {
|
||||
Base int
|
||||
}
|
||||
type Env2 struct {
|
||||
Multiplier int
|
||||
}
|
||||
|
||||
// Reader that gets base value
|
||||
getBase := func(e Env1) int {
|
||||
return e.Base
|
||||
}
|
||||
|
||||
// Kleisli that multiplies
|
||||
multiply := func(n int) Reader[Env2, int] {
|
||||
return func(e Env2) int {
|
||||
return n * e.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Another Kleisli that adds
|
||||
add := func(n int) Reader[Env2, int] {
|
||||
return func(e Env2) int {
|
||||
return n + e.Multiplier
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse with multiply
|
||||
traversed1 := Traverse[Env1](multiply)(getBase)
|
||||
env1 := Env1{Base: 10}
|
||||
env2 := Env2{Multiplier: 5}
|
||||
result1 := traversed1(env2)(env1)
|
||||
assert.Equal(t, 50, result1)
|
||||
|
||||
// Traverse with add
|
||||
traversed2 := Traverse[Env1](add)(getBase)
|
||||
result2 := traversed2(env2)(env1)
|
||||
assert.Equal(t, 15, result2)
|
||||
})
|
||||
|
||||
t.Run("traverse with identity", func(t *testing.T) {
|
||||
type Env1 struct {
|
||||
Value string
|
||||
}
|
||||
type Env2 struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
// Reader that gets value
|
||||
getValue := func(e Env1) string {
|
||||
return e.Value
|
||||
}
|
||||
|
||||
// Identity Kleisli (just wraps in Of)
|
||||
identity := func(s string) Reader[Env2, string] {
|
||||
return Of[Env2](s)
|
||||
}
|
||||
|
||||
traversed := Traverse[Env1](identity)(getValue)
|
||||
|
||||
env1 := Env1{Value: "test"}
|
||||
env2 := Env2{Prefix: "ignored"}
|
||||
result := traversed(env2)(env1)
|
||||
|
||||
assert.Equal(t, "test", result)
|
||||
})
|
||||
|
||||
t.Run("traverse with complex computation", func(t *testing.T) {
|
||||
type Request struct {
|
||||
UserID int
|
||||
}
|
||||
type Database struct {
|
||||
Users map[int]string
|
||||
}
|
||||
type Response struct {
|
||||
UserID int
|
||||
UserName string
|
||||
Found bool
|
||||
}
|
||||
|
||||
// Reader that gets user ID from request
|
||||
getUserID := func(r Request) int {
|
||||
return r.UserID
|
||||
}
|
||||
|
||||
// Kleisli that looks up user in database
|
||||
lookupUser := func(id int) Reader[Database, Response] {
|
||||
return func(db Database) Response {
|
||||
name, found := db.Users[id]
|
||||
return Response{
|
||||
UserID: id,
|
||||
UserName: name,
|
||||
Found: found,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traversed := Traverse[Request](lookupUser)(getUserID)
|
||||
|
||||
request := Request{UserID: 42}
|
||||
db := Database{
|
||||
Users: map[int]string{
|
||||
42: "Alice",
|
||||
99: "Bob",
|
||||
},
|
||||
}
|
||||
|
||||
result := traversed(db)(request)
|
||||
|
||||
assert.Equal(t, Response{
|
||||
UserID: 42,
|
||||
UserName: "Alice",
|
||||
Found: true,
|
||||
}, result)
|
||||
|
||||
// Test with missing user
|
||||
request2 := Request{UserID: 123}
|
||||
result2 := traversed(db)(request2)
|
||||
|
||||
assert.Equal(t, Response{
|
||||
UserID: 123,
|
||||
UserName: "",
|
||||
Found: false,
|
||||
}, result2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ import (
|
||||
// type Config struct { Host string }
|
||||
// r := reader.Ask[Config]()
|
||||
// config := r(Config{Host: "localhost"}) // Returns the config itself
|
||||
//
|
||||
//go:inline
|
||||
func Ask[R any]() Reader[R, R] {
|
||||
return function.Identity[R]
|
||||
}
|
||||
@@ -42,6 +44,8 @@ func Ask[R any]() Reader[R, R] {
|
||||
// type Config struct { Port int }
|
||||
// getPort := reader.Asks(func(c Config) int { return c.Port })
|
||||
// port := getPort(Config{Port: 8080}) // Returns 8080
|
||||
//
|
||||
//go:inline
|
||||
func Asks[R, A any](f Reader[R, A]) Reader[R, A] {
|
||||
return f
|
||||
}
|
||||
@@ -60,7 +64,10 @@ func Asks[R, A any](f Reader[R, A]) Reader[R, A] {
|
||||
// }
|
||||
// return reader.Of[Config]("fresh")
|
||||
// })
|
||||
//
|
||||
//go:inline
|
||||
func AsksReader[R, A any](f Kleisli[R, R, A]) Reader[R, A] {
|
||||
//go:inline
|
||||
return func(r R) A {
|
||||
return f(r)(r)
|
||||
}
|
||||
@@ -75,10 +82,44 @@ func AsksReader[R, A any](f Kleisli[R, R, A]) Reader[R, A] {
|
||||
// getPort := func(c Config) int { return c.Port }
|
||||
// getPortStr := reader.MonadMap(getPort, strconv.Itoa)
|
||||
// result := getPortStr(Config{Port: 8080}) // "8080"
|
||||
//
|
||||
//go:inline
|
||||
func MonadMap[E, A, B any](fa Reader[E, A], f func(A) B) Reader[E, B] {
|
||||
return function.Flow2(fa, f)
|
||||
}
|
||||
|
||||
// MonadMapTo creates a new Reader that completely ignores the first Reader and returns a constant value.
|
||||
// This is the monadic version that takes both the Reader and the constant value as parameters.
|
||||
//
|
||||
// IMPORTANT: Readers are pure functions with no side effects. This function does NOT compose or evaluate
|
||||
// the first Reader - it completely ignores it and returns a new Reader that always returns the constant value.
|
||||
// The first Reader is neither executed during composition nor when the resulting Reader runs.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The environment type
|
||||
// - A: The result type of the first Reader (completely ignored)
|
||||
// - B: The type of the constant value to return
|
||||
//
|
||||
// Parameters:
|
||||
// - _: The first Reader (completely ignored, never evaluated)
|
||||
// - b: The constant value to return
|
||||
//
|
||||
// Returns:
|
||||
// - A new Reader that ignores the environment and always returns b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int }
|
||||
// increment := func(c Config) int { return c.Counter + 1 }
|
||||
// // Create a Reader that ignores increment and returns "done"
|
||||
// r := reader.MonadMapTo(increment, "done")
|
||||
// result := r(Config{Counter: 5}) // "done" (increment was never evaluated)
|
||||
//
|
||||
//go:inline
|
||||
func MonadMapTo[E, A, B any](_ Reader[E, A], b B) Reader[E, B] {
|
||||
return Of[E](b)
|
||||
}
|
||||
|
||||
// Map transforms the result value of a Reader using the provided function.
|
||||
// This is the Functor operation that allows you to transform values inside the Reader context.
|
||||
//
|
||||
@@ -91,10 +132,55 @@ func MonadMap[E, A, B any](fa Reader[E, A], f func(A) B) Reader[E, B] {
|
||||
// getPort := reader.Asks(func(c Config) int { return c.Port })
|
||||
// getPortStr := reader.Map(strconv.Itoa)(getPort)
|
||||
// result := getPortStr(Config{Port: 8080}) // "8080"
|
||||
//
|
||||
//go:inline
|
||||
func Map[E, A, B any](f func(A) B) Operator[E, A, B] {
|
||||
return function.Bind2nd(MonadMap[E, A, B], f)
|
||||
}
|
||||
|
||||
// MapTo creates an operator that completely ignores any Reader and returns a constant value.
|
||||
// This is the curried version where the constant value is provided first,
|
||||
// returning a function that can be applied to any Reader.
|
||||
//
|
||||
// IMPORTANT: Readers are pure functions with no side effects. This operator does NOT compose or evaluate
|
||||
// the input Reader - it completely ignores it and returns a new Reader that always returns the constant value.
|
||||
// The input Reader is neither executed during composition nor when the resulting Reader runs.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - E: The environment type
|
||||
// - A: The result type of the input Reader (completely ignored)
|
||||
// - B: The type of the constant value to return
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to return
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a Reader[E, A] and returns Reader[E, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int }
|
||||
// increment := reader.Asks(func(c Config) int { return c.Counter + 1 })
|
||||
// // Create an operator that ignores any Reader and returns "done"
|
||||
// toDone := reader.MapTo[Config, int, string]("done")
|
||||
// pipeline := toDone(increment)
|
||||
// result := pipeline(Config{Counter: 5}) // "done" (increment was never evaluated)
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// type Env struct { Step int }
|
||||
// step1 := reader.Asks(func(e Env) int { return e.Step })
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// reader.MapTo[Env, int, string]("complete"),
|
||||
// )
|
||||
// output := pipeline(Env{Step: 1}) // "complete" (step1 was never evaluated)
|
||||
//
|
||||
//go:inline
|
||||
func MapTo[E, A, B any](b B) Operator[E, A, B] {
|
||||
return Of[Reader[E, A]](Of[E](b))
|
||||
}
|
||||
|
||||
// MonadAp applies a Reader containing a function to a Reader containing a value.
|
||||
// Both Readers share the same environment and are evaluated with it.
|
||||
// This is the monadic version that takes both parameters.
|
||||
@@ -176,6 +262,86 @@ func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
|
||||
return function.Bind2nd(MonadChain[R, A, B], f)
|
||||
}
|
||||
|
||||
// MonadChainTo completely ignores the first Reader and returns the second Reader.
|
||||
// This is the monadic version that takes both Readers as parameters.
|
||||
//
|
||||
// IMPORTANT: Readers are pure functions with no side effects. This function does NOT compose or evaluate
|
||||
// the first Reader - it completely ignores it and returns the second Reader directly.
|
||||
// The first Reader is neither executed during composition nor when the resulting Reader runs.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type of the first Reader (completely ignored)
|
||||
// - R: The environment type
|
||||
// - B: The result type of the second Reader
|
||||
//
|
||||
// Parameters:
|
||||
// - _: The first Reader (completely ignored, never evaluated)
|
||||
// - b: The second Reader to return
|
||||
//
|
||||
// Returns:
|
||||
// - The second Reader unchanged
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int; Message string }
|
||||
// increment := func(c Config) int { return c.Counter + 1 }
|
||||
// getMessage := func(c Config) string { return c.Message }
|
||||
// // Ignore increment and return getMessage
|
||||
// r := reader.MonadChainTo(increment, getMessage)
|
||||
// result := r(Config{Counter: 5, Message: "done"}) // "done" (increment was never evaluated)
|
||||
//
|
||||
//go:inline
|
||||
func MonadChainTo[A, R, B any](_ Reader[R, A], b Reader[R, B]) Reader[R, B] {
|
||||
return b
|
||||
}
|
||||
|
||||
// ChainTo creates an operator that completely ignores any Reader and returns a specific Reader.
|
||||
// This is the curried version where the second Reader is provided first,
|
||||
// returning a function that can be applied to any first Reader (which will be ignored).
|
||||
//
|
||||
// IMPORTANT: Readers are pure functions with no side effects. This operator does NOT compose or evaluate
|
||||
// the input Reader - it completely ignores it and returns the specified Reader directly.
|
||||
// The input Reader is neither executed during composition nor when the resulting Reader runs.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The result type of the first Reader (completely ignored)
|
||||
// - R: The environment type
|
||||
// - B: The result type of the second Reader
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The Reader to return (ignoring any input Reader)
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that takes a Reader[R, A] and returns Reader[R, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// type Config struct { Counter int; Message string }
|
||||
// getMessage := func(c Config) string { return c.Message }
|
||||
// // Create an operator that ignores any Reader and returns getMessage
|
||||
// thenGetMessage := reader.ChainTo[int, Config, string](getMessage)
|
||||
//
|
||||
// increment := func(c Config) int { return c.Counter + 1 }
|
||||
// pipeline := thenGetMessage(increment)
|
||||
// result := pipeline(Config{Counter: 5, Message: "done"}) // "done" (increment was never evaluated)
|
||||
//
|
||||
// Example - In a functional pipeline:
|
||||
//
|
||||
// type Env struct { Step int; Result string }
|
||||
// step1 := reader.Asks(func(e Env) int { return e.Step })
|
||||
// getResult := reader.Asks(func(e Env) string { return e.Result })
|
||||
//
|
||||
// pipeline := F.Pipe1(
|
||||
// step1,
|
||||
// reader.ChainTo[int, Env, string](getResult),
|
||||
// )
|
||||
// output := pipeline(Env{Step: 1, Result: "success"}) // "success" (step1 was never evaluated)
|
||||
//
|
||||
//go:inline
|
||||
func ChainTo[A, R, B any](b Reader[R, B]) Operator[R, A, B] {
|
||||
return Of[Reader[R, A]](b)
|
||||
}
|
||||
|
||||
// Flatten removes one level of Reader nesting.
|
||||
// Converts Reader[R, Reader[R, A]] to Reader[R, A].
|
||||
//
|
||||
@@ -265,6 +431,8 @@ func Promap[E, A, D, B any](f func(D) E, g func(A) B) Kleisli[D, Reader[E, A], B
|
||||
// simplify := func(d DetailedConfig) SimpleConfig { return SimpleConfig{Host: d.Host} }
|
||||
// r := reader.Local(simplify)(getHost)
|
||||
// result := r(DetailedConfig{Host: "localhost", Port: 8080}) // "localhost"
|
||||
//
|
||||
//go:inline
|
||||
func Local[A, R2, R1 any](f func(R2) R1) Kleisli[R2, Reader[R1, A], A] {
|
||||
return Compose[A](f)
|
||||
}
|
||||
|
||||
@@ -201,3 +201,287 @@ func TestFlap(t *testing.T) {
|
||||
result := r(config)
|
||||
assert.Equal(t, 15, result)
|
||||
}
|
||||
|
||||
func TestMapTo(t *testing.T) {
|
||||
t.Run("returns constant value without executing original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(c Config) int {
|
||||
executed = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
// Apply MapTo operator
|
||||
toDone := MapTo[Config, int]("done")
|
||||
resultReader := toDone(originalReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, "done", result)
|
||||
// Verify the original reader was never executed
|
||||
assert.False(t, executed, "original reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("works in functional pipeline without executing original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
step1 := func(c Config) int {
|
||||
executed = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
MapTo[Config, int]("complete"),
|
||||
)
|
||||
|
||||
result := pipeline(Config{Port: 8080})
|
||||
|
||||
assert.Equal(t, "complete", result)
|
||||
assert.False(t, executed, "original reader should not be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("ignores reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(c Config) int {
|
||||
sideEffectOccurred = true
|
||||
return c.Port * 2
|
||||
}
|
||||
|
||||
resultReader := MapTo[Config, int](true)(readerWithSideEffect)
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
assert.True(t, result)
|
||||
assert.False(t, sideEffectOccurred, "side effect should not occur")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapTo(t *testing.T) {
|
||||
t.Run("returns constant value without executing original reader", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReader := func(c Config) int {
|
||||
executed = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
// Apply MonadMapTo
|
||||
resultReader := MonadMapTo(originalReader, "done")
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, "done", result)
|
||||
// Verify the original reader was never executed
|
||||
assert.False(t, executed, "original reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("ignores complex computation", func(t *testing.T) {
|
||||
computationExecuted := false
|
||||
complexReader := func(c Config) string {
|
||||
computationExecuted = true
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(complexReader, 42)
|
||||
result := resultReader(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
assert.False(t, computationExecuted, "complex computation should not be executed")
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
executed := false
|
||||
intReader := func(c Config) int {
|
||||
executed = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
resultReader := MonadMapTo(intReader, []string{"a", "b", "c"})
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
assert.Equal(t, []string{"a", "b", "c"}, result)
|
||||
assert.False(t, executed, "original reader should not be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestChainTo(t *testing.T) {
|
||||
t.Run("returns second reader without executing first reader", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
firstReader := func(c Config) int {
|
||||
firstExecuted = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
secondReader := func(c Config) string {
|
||||
return c.Host
|
||||
}
|
||||
|
||||
// Apply ChainTo operator
|
||||
thenSecond := ChainTo[int](secondReader)
|
||||
resultReader := thenSecond(firstReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, "localhost", result)
|
||||
// Verify the first reader was never executed
|
||||
assert.False(t, firstExecuted, "first reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("works in functional pipeline without executing first reader", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
step1 := func(c Config) int {
|
||||
firstExecuted = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
step2 := func(c Config) string {
|
||||
return fmt.Sprintf("Result: %s", c.Host)
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
ChainTo[int](step2),
|
||||
)
|
||||
|
||||
result := pipeline(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.Equal(t, "Result: localhost", result)
|
||||
assert.False(t, firstExecuted, "first reader should not be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("ignores reader with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerWithSideEffect := func(c Config) int {
|
||||
sideEffectOccurred = true
|
||||
return c.Port * 2
|
||||
}
|
||||
|
||||
secondReader := func(c Config) bool {
|
||||
return c.Port > 0
|
||||
}
|
||||
|
||||
resultReader := ChainTo[int](secondReader)(readerWithSideEffect)
|
||||
result := resultReader(Config{Port: 8080})
|
||||
|
||||
assert.True(t, result)
|
||||
assert.False(t, sideEffectOccurred, "side effect should not occur")
|
||||
})
|
||||
|
||||
t.Run("chains multiple ChainTo operations", func(t *testing.T) {
|
||||
executed1 := false
|
||||
executed2 := false
|
||||
|
||||
reader1 := func(c Config) int {
|
||||
executed1 = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
reader2 := func(c Config) string {
|
||||
executed2 = true
|
||||
return c.Host
|
||||
}
|
||||
|
||||
reader3 := func(c Config) bool {
|
||||
return c.Port > 0
|
||||
}
|
||||
|
||||
pipeline := F.Pipe2(
|
||||
reader1,
|
||||
ChainTo[int](reader2),
|
||||
ChainTo[string](reader3),
|
||||
)
|
||||
|
||||
result := pipeline(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
assert.True(t, result)
|
||||
assert.False(t, executed1, "first reader should not be executed")
|
||||
assert.False(t, executed2, "second reader should not be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainTo(t *testing.T) {
|
||||
t.Run("returns second reader without executing first reader", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
firstReader := func(c Config) int {
|
||||
firstExecuted = true
|
||||
return c.Port
|
||||
}
|
||||
|
||||
secondReader := func(c Config) string {
|
||||
return c.Host
|
||||
}
|
||||
|
||||
// Apply MonadChainTo
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
|
||||
// Execute the resulting reader
|
||||
result := resultReader(Config{Host: "localhost", Port: 8080})
|
||||
|
||||
// Verify the second reader's result is returned
|
||||
assert.Equal(t, "localhost", result)
|
||||
// Verify the first reader was never executed
|
||||
assert.False(t, firstExecuted, "first reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("ignores complex first computation", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
complexFirstReader := func(c Config) []int {
|
||||
firstExecuted = true
|
||||
result := make([]int, c.Port)
|
||||
for i := range result {
|
||||
result[i] = i * c.Multiplier
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
secondReader := func(c Config) string {
|
||||
return c.Prefix + c.Host
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(complexFirstReader, secondReader)
|
||||
result := resultReader(Config{Host: "localhost", Port: 100, Prefix: "server:"})
|
||||
|
||||
assert.Equal(t, "server:localhost", result)
|
||||
assert.False(t, firstExecuted, "complex first computation should not be executed")
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
firstReader := func(c Config) map[string]int {
|
||||
firstExecuted = true
|
||||
return map[string]int{"port": c.Port}
|
||||
}
|
||||
|
||||
secondReader := func(c Config) float64 {
|
||||
return float64(c.Multiplier) * 3.14
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
result := resultReader(Config{Multiplier: 2})
|
||||
|
||||
assert.Equal(t, 6.28, result)
|
||||
assert.False(t, firstExecuted, "first reader should not be executed")
|
||||
})
|
||||
|
||||
t.Run("preserves second reader behavior", func(t *testing.T) {
|
||||
firstExecuted := false
|
||||
firstReader := func(c Config) int {
|
||||
firstExecuted = true
|
||||
return 999
|
||||
}
|
||||
|
||||
secondReader := func(c Config) string {
|
||||
// Second reader should still have access to the environment
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
resultReader := MonadChainTo(firstReader, secondReader)
|
||||
result := resultReader(Config{Host: "example.com", Port: 443})
|
||||
|
||||
assert.Equal(t, "example.com:443", result)
|
||||
assert.False(t, firstExecuted, "first reader should not be executed")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func Right[E, L, A any](r A) ReaderEither[E, L, A] {
|
||||
return eithert.Right(reader.Of[E, Either[L, A]], r)
|
||||
}
|
||||
|
||||
func FromReader[E, L, A any](r Reader[E, A]) ReaderEither[E, L, A] {
|
||||
func FromReader[L, E, A any](r Reader[E, A]) ReaderEither[E, L, A] {
|
||||
return RightReader[L](r)
|
||||
}
|
||||
|
||||
@@ -66,19 +66,19 @@ func Chain[E, L, A, B any](f func(A) ReaderEither[E, L, B]) func(ReaderEither[E,
|
||||
return readert.Chain[ReaderEither[E, L, A]](ET.Chain[L, A, B], f)
|
||||
}
|
||||
|
||||
func MonadChainReaderK[E, L, A, B any](ma ReaderEither[E, L, A], f reader.Kleisli[E, A, B]) ReaderEither[E, L, B] {
|
||||
return MonadChain(ma, function.Flow2(f, FromReader[E, L, B]))
|
||||
func MonadChainReaderK[L, E, A, B any](ma ReaderEither[E, L, A], f reader.Kleisli[E, A, B]) ReaderEither[E, L, B] {
|
||||
return MonadChain(ma, function.Flow2(f, FromReader[L, E, B]))
|
||||
}
|
||||
|
||||
func ChainReaderK[E, L, A, B any](f reader.Kleisli[E, A, B]) func(ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
return Chain(function.Flow2(f, FromReader[E, L, B]))
|
||||
func ChainReaderK[L, E, A, B any](f reader.Kleisli[E, A, B]) func(ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
return Chain(function.Flow2(f, FromReader[L, E, B]))
|
||||
}
|
||||
|
||||
func Of[E, L, A any](a A) ReaderEither[E, L, A] {
|
||||
return readert.MonadOf[ReaderEither[E, L, A]](ET.Of[L, A], a)
|
||||
}
|
||||
|
||||
func MonadAp[E, L, A, B any](fab ReaderEither[E, L, func(A) B], fa ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
func MonadAp[B, E, L, A any](fab ReaderEither[E, L, func(A) B], fa ReaderEither[E, L, A]) ReaderEither[E, L, B] {
|
||||
return readert.MonadAp[ReaderEither[E, L, A], ReaderEither[E, L, B], ReaderEither[E, L, func(A) B], E, A](ET.MonadAp[B, L, A], fab, fa)
|
||||
}
|
||||
|
||||
@@ -112,11 +112,11 @@ func OrLeft[A, L1, E, L2 any](onLeft func(L1) Reader[E, L2]) func(ReaderEither[E
|
||||
}
|
||||
|
||||
func Ask[E, L any]() ReaderEither[E, L, E] {
|
||||
return fromreader.Ask(FromReader[E, L, E])()
|
||||
return fromreader.Ask(FromReader[L, E, E])()
|
||||
}
|
||||
|
||||
func Asks[L, E, A any](r Reader[E, A]) ReaderEither[E, L, A] {
|
||||
return fromreader.Asks(FromReader[E, L, A])(r)
|
||||
return fromreader.Asks(FromReader[L, E, A])(r)
|
||||
}
|
||||
|
||||
func MonadChainEitherK[E, L, A, B any](ma ReaderEither[E, L, A], f func(A) Either[L, B]) ReaderEither[E, L, B] {
|
||||
|
||||
39
v2/readereither/rec.go
Normal file
39
v2/readereither/rec.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// 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 readereither
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
//go:inline
|
||||
func TailRec[R, E, A, B any](f Kleisli[R, E, A, Either[A, B]]) Kleisli[R, E, A, B] {
|
||||
return func(a A) ReaderEither[R, E, B] {
|
||||
initialReader := f(a)
|
||||
return func(r R) Either[E, B] {
|
||||
current := initialReader(r)
|
||||
for {
|
||||
rec, e := either.Unwrap(current)
|
||||
if either.IsLeft(current) {
|
||||
return either.Left[B](e)
|
||||
}
|
||||
b, a := either.Unwrap(rec)
|
||||
if either.IsRight(rec) {
|
||||
return either.Right[E](b)
|
||||
}
|
||||
current = f(a)(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
v2/readerio/consumer.go
Normal file
13
v2/readerio/consumer.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package readerio
|
||||
|
||||
import "github.com/IBM/fp-go/v2/io"
|
||||
|
||||
//go:inline
|
||||
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, struct{}] {
|
||||
return ChainIOK[R](io.FromConsumerK(c))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstConsumer[R, A any](c Consumer[A]) Operator[R, A, A] {
|
||||
return ChainFirstIOK[R](io.FromConsumerK(c))
|
||||
}
|
||||
@@ -172,26 +172,36 @@ func MonadMap[R, A, B any](fa ReaderIO[R, A], f func(A) B) ReaderIO[R, B] {
|
||||
return readert.MonadMap[ReaderIO[R, A], ReaderIO[R, B]](io.MonadMap[A, B], fa, f)
|
||||
}
|
||||
|
||||
// MonadMapTo replaces the value inside a ReaderIO with a constant value.
|
||||
// This is useful when you want to discard the result of a computation but keep its effects.
|
||||
// MonadMapTo executes a ReaderIO computation, discards its result, and returns a constant value.
|
||||
// This is the monadic version that takes both the ReaderIO and the constant value as parameters.
|
||||
//
|
||||
// IMPORTANT: ReaderIO represents a side-effectful computation (IO effects). For this reason,
|
||||
// MonadMapTo WILL execute the original ReaderIO to allow any side effects to occur (such as
|
||||
// logging, file I/O, network calls, etc.), then discard the result and return the constant value.
|
||||
// The side effects are preserved even though the result value is discarded.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: Reader environment type
|
||||
// - A: Input value type (discarded)
|
||||
// - B: Output value type
|
||||
// - A: Input value type (result will be discarded after execution)
|
||||
// - B: Output value type (constant to return)
|
||||
//
|
||||
// Parameters:
|
||||
// - fa: The ReaderIO whose value will be replaced
|
||||
// - b: The constant value to use
|
||||
// - fa: The ReaderIO to execute (side effects will occur, result discarded)
|
||||
// - b: The constant value to return after executing fa
|
||||
//
|
||||
// Returns:
|
||||
// - A new ReaderIO with the constant value
|
||||
// - A new ReaderIO that executes fa for its side effects, then returns b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// rio := readerio.Of[Config](42)
|
||||
// replaced := readerio.MonadMapTo(rio, "constant")
|
||||
// result := replaced(config)() // Returns "constant"
|
||||
// logAndCompute := func(r Config) io.IO[int] {
|
||||
// return io.Of(func() int {
|
||||
// fmt.Println("Computing...") // Side effect
|
||||
// return 42
|
||||
// })
|
||||
// }
|
||||
// replaced := readerio.MonadMapTo(logAndCompute, "done")
|
||||
// result := replaced(config)() // Prints "Computing...", returns "done"
|
||||
func MonadMapTo[R, A, B any](fa ReaderIO[R, A], b B) ReaderIO[R, B] {
|
||||
return MonadMap(fa, function.Constant1[A](b))
|
||||
}
|
||||
@@ -220,26 +230,37 @@ func Map[R, A, B any](f func(A) B) Operator[R, A, B] {
|
||||
return readert.Map[ReaderIO[R, A], ReaderIO[R, B]](io.Map[A, B], f)
|
||||
}
|
||||
|
||||
// MapTo creates a function that replaces a ReaderIO value with a constant.
|
||||
// This is the curried version of [MonadMapTo], suitable for use in pipelines.
|
||||
// MapTo creates an operator that executes a ReaderIO computation, discards its result,
|
||||
// and returns a constant value. This is the curried version of [MonadMapTo], suitable for use in pipelines.
|
||||
//
|
||||
// IMPORTANT: ReaderIO represents a side-effectful computation (IO effects). For this reason,
|
||||
// MapTo WILL execute the input ReaderIO to allow any side effects to occur (such as logging,
|
||||
// file I/O, network calls, etc.), then discard the result and return the constant value.
|
||||
// The side effects are preserved even though the result value is discarded.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - R: Reader environment type
|
||||
// - A: Input value type (discarded)
|
||||
// - B: Output value type
|
||||
// - A: Input value type (result will be discarded after execution)
|
||||
// - B: Output value type (constant to return)
|
||||
//
|
||||
// Parameters:
|
||||
// - b: The constant value to use
|
||||
// - b: The constant value to return after executing the ReaderIO
|
||||
//
|
||||
// Returns:
|
||||
// - An Operator that replaces ReaderIO values with the constant
|
||||
// - An Operator that executes a ReaderIO for its side effects, then returns b
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// logStep := func(r Config) io.IO[int] {
|
||||
// return io.Of(func() int {
|
||||
// fmt.Println("Step executed") // Side effect
|
||||
// return 42
|
||||
// })
|
||||
// }
|
||||
// result := F.Pipe1(
|
||||
// readerio.Of[Config](42),
|
||||
// readerio.MapTo[Config, int]("constant"),
|
||||
// )(config)() // Returns "constant"
|
||||
// logStep,
|
||||
// readerio.MapTo[Config, int]("complete"),
|
||||
// )(config)() // Prints "Step executed", returns "complete"
|
||||
func MapTo[R, A, B any](b B) Operator[R, A, B] {
|
||||
return Map[R](function.Constant1[A](b))
|
||||
}
|
||||
|
||||
@@ -323,6 +323,161 @@ func TestMapTo(t *testing.T) {
|
||||
assert.Equal(t, "constant", result(config)())
|
||||
}
|
||||
|
||||
func TestMapToExecutesSideEffects(t *testing.T) {
|
||||
t.Run("executes original ReaderIO and returns constant value", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReaderIO := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
executed = true
|
||||
return 42
|
||||
}
|
||||
}
|
||||
|
||||
// Apply MapTo operator
|
||||
toDone := MapTo[ReaderTestConfig, int]("done")
|
||||
resultReaderIO := toDone(originalReaderIO)
|
||||
|
||||
// Execute the resulting ReaderIO
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, "done", result)
|
||||
// Verify the original ReaderIO WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original ReaderIO should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes ReaderIO in functional pipeline", func(t *testing.T) {
|
||||
executed := false
|
||||
step1 := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
executed = true
|
||||
return 100
|
||||
}
|
||||
}
|
||||
|
||||
pipeline := F.Pipe1(
|
||||
step1,
|
||||
MapTo[ReaderTestConfig, int]("complete"),
|
||||
)
|
||||
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := pipeline(config)()
|
||||
|
||||
assert.Equal(t, "complete", result)
|
||||
assert.True(t, executed, "original ReaderIO should be executed in pipeline")
|
||||
})
|
||||
|
||||
t.Run("executes ReaderIO with side effects", func(t *testing.T) {
|
||||
sideEffectOccurred := false
|
||||
readerIOWithSideEffect := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
sideEffectOccurred = true
|
||||
return 42
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MapTo[ReaderTestConfig, int](true)(readerIOWithSideEffect)
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, true, result)
|
||||
assert.True(t, sideEffectOccurred, "side effect should occur")
|
||||
})
|
||||
|
||||
t.Run("executes complex computation with side effects", func(t *testing.T) {
|
||||
computationExecuted := false
|
||||
complexReaderIO := func(c ReaderTestConfig) G.IO[string] {
|
||||
return func() string {
|
||||
computationExecuted = true
|
||||
return "complex result"
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MapTo[ReaderTestConfig, string](99)(complexReaderIO)
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, 99, result)
|
||||
assert.True(t, computationExecuted, "complex computation should be executed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadMapToExecutesSideEffects(t *testing.T) {
|
||||
t.Run("executes original ReaderIO and returns constant value", func(t *testing.T) {
|
||||
executed := false
|
||||
originalReaderIO := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
executed = true
|
||||
return 42
|
||||
}
|
||||
}
|
||||
|
||||
// Apply MonadMapTo
|
||||
resultReaderIO := MonadMapTo(originalReaderIO, "done")
|
||||
|
||||
// Execute the resulting ReaderIO
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
// Verify the constant value is returned
|
||||
assert.Equal(t, "done", result)
|
||||
// Verify the original ReaderIO WAS executed (side effect occurred)
|
||||
assert.True(t, executed, "original ReaderIO should be executed to allow side effects")
|
||||
})
|
||||
|
||||
t.Run("executes complex computation with side effects", func(t *testing.T) {
|
||||
computationExecuted := false
|
||||
complexReaderIO := func(c ReaderTestConfig) G.IO[string] {
|
||||
return func() string {
|
||||
computationExecuted = true
|
||||
return "complex result"
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MonadMapTo(complexReaderIO, 42)
|
||||
config := ReaderTestConfig{Value: 10, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
assert.True(t, computationExecuted, "complex computation should be executed")
|
||||
})
|
||||
|
||||
t.Run("executes ReaderIO with logging side effect", func(t *testing.T) {
|
||||
logged := []string{}
|
||||
loggingReaderIO := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
logged = append(logged, "computation executed")
|
||||
return c.Value * 2
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MonadMapTo(loggingReaderIO, "result")
|
||||
config := ReaderTestConfig{Value: 5, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, "result", result)
|
||||
assert.Equal(t, []string{"computation executed"}, logged)
|
||||
})
|
||||
|
||||
t.Run("executes ReaderIO accessing environment", func(t *testing.T) {
|
||||
accessedEnv := false
|
||||
envReaderIO := func(c ReaderTestConfig) G.IO[int] {
|
||||
return func() int {
|
||||
accessedEnv = true
|
||||
return c.Value + 10
|
||||
}
|
||||
}
|
||||
|
||||
resultReaderIO := MonadMapTo(envReaderIO, []int{1, 2, 3})
|
||||
config := ReaderTestConfig{Value: 20, Name: "test"}
|
||||
result := resultReaderIO(config)()
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3}, result)
|
||||
assert.True(t, accessedEnv, "ReaderIO should access environment during execution")
|
||||
})
|
||||
}
|
||||
|
||||
func TestMonadChainFirst(t *testing.T) {
|
||||
sideEffect := 0
|
||||
rio := Of[ReaderTestConfig](42)
|
||||
|
||||
163
v2/readerio/rec.go
Normal file
163
v2/readerio/rec.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package readerio
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
// TailRec implements stack-safe tail recursion for the ReaderIO monad.
|
||||
//
|
||||
// This function enables recursive computations that depend on an environment (Reader aspect)
|
||||
// and perform side effects (IO aspect) without risking stack overflow. It uses an iterative
|
||||
// loop to execute the recursion, making it safe for deep or unbounded recursion.
|
||||
//
|
||||
// # 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 and return the final result B
|
||||
//
|
||||
// The function iteratively applies the Kleisli arrow, passing the environment R to each
|
||||
// iteration, until a Right(B) value is produced. This combines:
|
||||
// - Environment dependency (Reader monad): Access to configuration, context, or dependencies
|
||||
// - Side effects (IO monad): Logging, file I/O, network calls, etc.
|
||||
// - Stack safety: Iterative execution prevents stack overflow
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - R: The environment type (Reader context) - e.g., Config, Logger, Database connection
|
||||
// - A: The state type that changes during recursion
|
||||
// - B: The final result type when recursion terminates
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow (A => ReaderIO[R, Either[A, B]]) that:
|
||||
// * Takes the current state A
|
||||
// * Returns a ReaderIO that depends on environment R
|
||||
// * Produces Either[A, B] to control recursion flow
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Kleisli arrow (A => ReaderIO[R, B]) that:
|
||||
// - Takes an initial state A
|
||||
// - Returns a ReaderIO that requires environment R
|
||||
// - Produces the final result B after recursion completes
|
||||
//
|
||||
// # Comparison with Other Monads
|
||||
//
|
||||
// Unlike IOEither and IOOption tail recursion:
|
||||
// - No error channel (like IOEither's Left error case)
|
||||
// - No failure case (like IOOption's None case)
|
||||
// - Adds environment dependency that's available throughout recursion
|
||||
// - Environment R is passed to every recursive step
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. Environment-dependent recursive algorithms:
|
||||
// - Recursive computations that need configuration at each step
|
||||
// - Algorithms that log progress using an environment-provided logger
|
||||
// - Recursive operations that access shared resources from environment
|
||||
//
|
||||
// 2. Stateful computations with context:
|
||||
// - Tree traversals that need environment context
|
||||
// - Graph algorithms with configuration-dependent behavior
|
||||
// - Recursive parsers with environment-based rules
|
||||
//
|
||||
// 3. Recursive operations with side effects:
|
||||
// - File system traversals with logging
|
||||
// - Network operations with retry configuration
|
||||
// - Database operations with connection pooling
|
||||
//
|
||||
// # Example: Factorial with Logging
|
||||
//
|
||||
// type Env struct {
|
||||
// Logger func(string)
|
||||
// }
|
||||
//
|
||||
// // Factorial that logs each step
|
||||
// factorialStep := func(state struct{ n, acc int }) readerio.ReaderIO[Env, either.Either[struct{ n, acc int }, int]] {
|
||||
// return func(env Env) io.IO[either.Either[struct{ n, acc int }, int]] {
|
||||
// return func() either.Either[struct{ n, acc int }, int] {
|
||||
// if state.n <= 0 {
|
||||
// env.Logger(fmt.Sprintf("Factorial complete: %d", state.acc))
|
||||
// return either.Right[struct{ n, acc int }](state.acc)
|
||||
// }
|
||||
// env.Logger(fmt.Sprintf("Computing: %d * %d", state.n, state.acc))
|
||||
// return either.Left[int](struct{ n, acc int }{state.n - 1, state.acc * state.n})
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// factorial := readerio.TailRec(factorialStep)
|
||||
// env := Env{Logger: func(msg string) { fmt.Println(msg) }}
|
||||
// result := factorial(struct{ n, acc int }{5, 1})(env)() // Returns 120, logs each step
|
||||
//
|
||||
// # Example: Countdown with Configuration
|
||||
//
|
||||
// type Config struct {
|
||||
// MinValue int
|
||||
// Step int
|
||||
// }
|
||||
//
|
||||
// countdownStep := func(n int) readerio.ReaderIO[Config, either.Either[int, int]] {
|
||||
// return func(cfg Config) io.IO[either.Either[int, int]] {
|
||||
// return func() either.Either[int, int] {
|
||||
// if n <= cfg.MinValue {
|
||||
// return either.Right[int](n)
|
||||
// }
|
||||
// return either.Left[int](n - cfg.Step)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// countdown := readerio.TailRec(countdownStep)
|
||||
// config := Config{MinValue: 0, Step: 2}
|
||||
// result := countdown(10)(config)() // Returns 0 (10 -> 8 -> 6 -> 4 -> 2 -> 0)
|
||||
//
|
||||
// # Stack Safety
|
||||
//
|
||||
// The iterative implementation ensures that even deeply recursive computations
|
||||
// (thousands or millions of iterations) will not cause stack overflow:
|
||||
//
|
||||
// // Safe for very large inputs
|
||||
// sumToZero := readerio.TailRec(func(n int) readerio.ReaderIO[Env, either.Either[int, int]] {
|
||||
// return func(env Env) io.IO[either.Either[int, int]] {
|
||||
// return func() either.Either[int, int] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[int](0)
|
||||
// }
|
||||
// return either.Left[int](n - 1)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// result := sumToZero(1000000)(env)() // Safe, no stack overflow
|
||||
//
|
||||
// # Performance Considerations
|
||||
//
|
||||
// - Each iteration creates a new IO action by calling f(a)(r)()
|
||||
// - The environment R is passed to every iteration
|
||||
// - For performance-critical code, consider if the environment access is necessary
|
||||
// - Memoization of environment-derived values may improve performance
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - [ioeither.TailRec]: Tail recursion with error handling
|
||||
// - [iooption.TailRec]: Tail recursion with optional results
|
||||
// - [Chain]: For sequencing ReaderIO computations
|
||||
// - [Ask]: For accessing the environment
|
||||
// - [Asks]: For extracting values from the environment
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
|
||||
return func(a A) ReaderIO[R, B] {
|
||||
initialReader := f(a)
|
||||
return func(r R) IO[B] {
|
||||
initialB := initialReader(r)
|
||||
return func() B {
|
||||
current := initialB()
|
||||
for {
|
||||
b, a := either.Unwrap(current)
|
||||
if either.IsRight(current) {
|
||||
return b
|
||||
}
|
||||
current = f(a)(r)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
455
v2/readerio/rec_test.go
Normal file
455
v2/readerio/rec_test.go
Normal file
@@ -0,0 +1,455 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
G "github.com/IBM/fp-go/v2/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test environment types
|
||||
type TestEnv struct {
|
||||
Multiplier int
|
||||
Logs []string
|
||||
}
|
||||
|
||||
type LoggerEnv struct {
|
||||
Logger func(string)
|
||||
}
|
||||
|
||||
type ConfigEnv struct {
|
||||
MinValue int
|
||||
Step int
|
||||
}
|
||||
|
||||
// TestTailRecFactorial tests factorial computation with environment-based logging
|
||||
func TestTailRecFactorial(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
logs := []string{}
|
||||
env := LoggerEnv{
|
||||
Logger: func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
},
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderIO[LoggerEnv, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
if state.n <= 0 {
|
||||
env.Logger(fmt.Sprintf("Complete: %d", state.acc))
|
||||
return E.Right[State](state.acc)
|
||||
}
|
||||
env.Logger(fmt.Sprintf("Step: %d * %d", state.n, state.acc))
|
||||
return E.Left[int](State{state.n - 1, state.acc * state.n})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result := factorial(State{5, 1})(env)()
|
||||
|
||||
assert.Equal(t, 120, result)
|
||||
assert.Equal(t, 6, len(logs)) // 5 steps + 1 complete
|
||||
assert.Contains(t, logs[0], "Step: 5 * 1")
|
||||
assert.Contains(t, logs[len(logs)-1], "Complete: 120")
|
||||
}
|
||||
|
||||
// TestTailRecFibonacci tests Fibonacci computation with environment dependency
|
||||
func TestTailRecFibonacci(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
prev int
|
||||
curr int
|
||||
}
|
||||
|
||||
env := TestEnv{Multiplier: 1, Logs: []string{}}
|
||||
|
||||
fibStep := func(state State) ReaderIO[TestEnv, E.Either[State, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
if state.n <= 0 {
|
||||
return E.Right[State](state.curr * env.Multiplier)
|
||||
}
|
||||
return E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fib := TailRec(fibStep)
|
||||
result := fib(State{10, 0, 1})(env)()
|
||||
|
||||
assert.Equal(t, 89, result) // 10th Fibonacci number
|
||||
}
|
||||
|
||||
// TestTailRecCountdown tests countdown with configuration-based step
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
config := ConfigEnv{MinValue: 0, Step: 2}
|
||||
|
||||
countdownStep := func(n int) ReaderIO[ConfigEnv, E.Either[int, int]] {
|
||||
return func(cfg ConfigEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
if n <= cfg.MinValue {
|
||||
return E.Right[int](n)
|
||||
}
|
||||
return E.Left[int](n - cfg.Step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)(config)()
|
||||
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecCountdownOddStep tests countdown with odd step size
|
||||
func TestTailRecCountdownOddStep(t *testing.T) {
|
||||
config := ConfigEnv{MinValue: 0, Step: 3}
|
||||
|
||||
countdownStep := func(n int) ReaderIO[ConfigEnv, E.Either[int, int]] {
|
||||
return func(cfg ConfigEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
if n <= cfg.MinValue {
|
||||
return E.Right[int](n)
|
||||
}
|
||||
return E.Left[int](n - cfg.Step)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)(config)()
|
||||
|
||||
assert.Equal(t, -2, result) // 10 -> 7 -> 4 -> 1 -> -2 (stops when <= 0)
|
||||
}
|
||||
|
||||
// TestTailRecSumList tests summing a list with environment-based multiplier
|
||||
func TestTailRecSumList(t *testing.T) {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
env := TestEnv{Multiplier: 2, Logs: []string{}}
|
||||
|
||||
sumStep := func(state State) ReaderIO[TestEnv, E.Either[State, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
if len(state.list) == 0 {
|
||||
return E.Right[State](state.sum * env.Multiplier)
|
||||
}
|
||||
return 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})(env)()
|
||||
|
||||
assert.Equal(t, 30, result) // (1+2+3+4+5) * 2 = 30
|
||||
}
|
||||
|
||||
// TestTailRecImmediateTermination tests immediate termination (Right on first call)
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, Logs: []string{}}
|
||||
|
||||
immediateStep := func(n int) ReaderIO[TestEnv, E.Either[int, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
return E.Right[int](n * env.Multiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(42)(env)()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, Logs: []string{}}
|
||||
|
||||
countdownStep := func(n int) ReaderIO[TestEnv, E.Either[int, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
if n <= 0 {
|
||||
return E.Right[int](n)
|
||||
}
|
||||
return E.Left[int](n - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10000)(env)()
|
||||
|
||||
assert.Equal(t, 0, result)
|
||||
}
|
||||
|
||||
// TestTailRecFindInRange tests finding a value in a range with environment-based target
|
||||
func TestTailRecFindInRange(t *testing.T) {
|
||||
type FindEnv struct {
|
||||
Target int
|
||||
}
|
||||
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
}
|
||||
|
||||
env := FindEnv{Target: 42}
|
||||
|
||||
findStep := func(state State) ReaderIO[FindEnv, E.Either[State, int]] {
|
||||
return func(env FindEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
if state.current >= state.max {
|
||||
return E.Right[State](-1) // Not found
|
||||
}
|
||||
if state.current == env.Target {
|
||||
return E.Right[State](state.current) // Found
|
||||
}
|
||||
return E.Left[int](State{state.current + 1, state.max})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result := find(State{0, 100})(env)()
|
||||
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
||||
// TestTailRecFindNotInRange tests finding a value not in range
|
||||
func TestTailRecFindNotInRange(t *testing.T) {
|
||||
type FindEnv struct {
|
||||
Target int
|
||||
}
|
||||
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
}
|
||||
|
||||
env := FindEnv{Target: 200}
|
||||
|
||||
findStep := func(state State) ReaderIO[FindEnv, E.Either[State, int]] {
|
||||
return func(env FindEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
if state.current >= state.max {
|
||||
return E.Right[State](-1) // Not found
|
||||
}
|
||||
if state.current == env.Target {
|
||||
return E.Right[State](state.current) // Found
|
||||
}
|
||||
return E.Left[int](State{state.current + 1, state.max})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result := find(State{0, 100})(env)()
|
||||
|
||||
assert.Equal(t, -1, result)
|
||||
}
|
||||
|
||||
// TestTailRecWithLogging tests that logging side effects occur during recursion
|
||||
func TestTailRecWithLogging(t *testing.T) {
|
||||
logs := []string{}
|
||||
env := LoggerEnv{
|
||||
Logger: func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
},
|
||||
}
|
||||
|
||||
countdownStep := func(n int) ReaderIO[LoggerEnv, E.Either[int, int]] {
|
||||
return func(env LoggerEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
env.Logger(fmt.Sprintf("Count: %d", n))
|
||||
if n <= 0 {
|
||||
return E.Right[int](n)
|
||||
}
|
||||
return E.Left[int](n - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(5)(env)()
|
||||
|
||||
assert.Equal(t, 0, result)
|
||||
assert.Equal(t, 6, len(logs)) // 5, 4, 3, 2, 1, 0
|
||||
assert.Equal(t, "Count: 5", logs[0])
|
||||
assert.Equal(t, "Count: 0", logs[5])
|
||||
}
|
||||
|
||||
// TestTailRecCollatzConjecture tests the Collatz conjecture with environment-based logging
|
||||
func TestTailRecCollatzConjecture(t *testing.T) {
|
||||
logs := []string{}
|
||||
env := LoggerEnv{
|
||||
Logger: func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
},
|
||||
}
|
||||
|
||||
collatzStep := func(n int) ReaderIO[LoggerEnv, E.Either[int, int]] {
|
||||
return func(env LoggerEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
env.Logger(fmt.Sprintf("n=%d", n))
|
||||
if n <= 1 {
|
||||
return E.Right[int](n)
|
||||
}
|
||||
if n%2 == 0 {
|
||||
return E.Left[int](n / 2)
|
||||
}
|
||||
return E.Left[int](3*n + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collatz := TailRec(collatzStep)
|
||||
result := collatz(10)(env)()
|
||||
|
||||
assert.Equal(t, 1, result)
|
||||
assert.Greater(t, len(logs), 5) // Multiple steps to reach 1
|
||||
assert.Equal(t, "n=10", logs[0])
|
||||
assert.Contains(t, logs[len(logs)-1], "n=1")
|
||||
}
|
||||
|
||||
// TestTailRecPowerOfTwo tests computing power of 2 with environment-based exponent limit
|
||||
func TestTailRecPowerOfTwo(t *testing.T) {
|
||||
type PowerEnv struct {
|
||||
MaxExponent int
|
||||
}
|
||||
|
||||
type State struct {
|
||||
exponent int
|
||||
result int
|
||||
}
|
||||
|
||||
env := PowerEnv{MaxExponent: 10}
|
||||
|
||||
powerStep := func(state State) ReaderIO[PowerEnv, E.Either[State, int]] {
|
||||
return func(env PowerEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
if state.exponent >= env.MaxExponent {
|
||||
return E.Right[State](state.result)
|
||||
}
|
||||
return E.Left[int](State{state.exponent + 1, state.result * 2})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
power := TailRec(powerStep)
|
||||
result := power(State{0, 1})(env)()
|
||||
|
||||
assert.Equal(t, 1024, result) // 2^10
|
||||
}
|
||||
|
||||
// TestTailRecGCD tests greatest common divisor with environment-based logging
|
||||
func TestTailRecGCD(t *testing.T) {
|
||||
type State struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
|
||||
logs := []string{}
|
||||
env := LoggerEnv{
|
||||
Logger: func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
},
|
||||
}
|
||||
|
||||
gcdStep := func(state State) ReaderIO[LoggerEnv, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) G.IO[E.Either[State, int]] {
|
||||
return func() E.Either[State, int] {
|
||||
env.Logger(fmt.Sprintf("gcd(%d, %d)", state.a, state.b))
|
||||
if state.b == 0 {
|
||||
return E.Right[State](state.a)
|
||||
}
|
||||
return E.Left[int](State{state.b, state.a % state.b})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gcd := TailRec(gcdStep)
|
||||
result := gcd(State{48, 18})(env)()
|
||||
|
||||
assert.Equal(t, 6, result)
|
||||
assert.Greater(t, len(logs), 0)
|
||||
assert.Contains(t, logs[0], "gcd(48, 18)")
|
||||
}
|
||||
|
||||
// TestTailRecMultipleEnvironmentAccess tests that environment is accessible in each iteration
|
||||
func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
|
||||
type CounterEnv struct {
|
||||
Increment int
|
||||
Limit int
|
||||
}
|
||||
|
||||
env := CounterEnv{Increment: 3, Limit: 20}
|
||||
|
||||
counterStep := func(n int) ReaderIO[CounterEnv, E.Either[int, int]] {
|
||||
return func(env CounterEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
if n >= env.Limit {
|
||||
return E.Right[int](n)
|
||||
}
|
||||
return E.Left[int](n + env.Increment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
counter := TailRec(counterStep)
|
||||
result := counter(0)(env)()
|
||||
|
||||
assert.Equal(t, 21, result) // 0 -> 3 -> 6 -> 9 -> 12 -> 15 -> 18 -> 21
|
||||
}
|
||||
|
||||
// TestTailRecDifferentEnvironments tests that different environments produce different results
|
||||
func TestTailRecDifferentEnvironments(t *testing.T) {
|
||||
multiplyStep := func(n int) ReaderIO[TestEnv, E.Either[int, int]] {
|
||||
return func(env TestEnv) G.IO[E.Either[int, int]] {
|
||||
return func() E.Either[int, int] {
|
||||
if n <= 0 {
|
||||
return E.Right[int](n)
|
||||
}
|
||||
return E.Left[int](n - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
multiply := TailRec(multiplyStep)
|
||||
|
||||
env1 := TestEnv{Multiplier: 2, Logs: []string{}}
|
||||
env2 := TestEnv{Multiplier: 5, Logs: []string{}}
|
||||
|
||||
result1 := multiply(5)(env1)()
|
||||
result2 := multiply(5)(env2)()
|
||||
|
||||
// Both should reach 0 regardless of environment (environment not used in this case)
|
||||
assert.Equal(t, 0, result1)
|
||||
assert.Equal(t, 0, result2)
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
package readerio
|
||||
|
||||
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/reader"
|
||||
)
|
||||
@@ -25,6 +27,8 @@ type (
|
||||
// It's an alias for io.IO[A] and encapsulates effectful operations.
|
||||
IO[A any] = io.IO[A]
|
||||
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Reader represents a computation that depends on an environment of type R and produces a value of type A.
|
||||
// It's an alias for reader.Reader[R, A] and is used for dependency injection patterns.
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
@@ -46,4 +50,6 @@ type (
|
||||
// for building pipelines of ReaderIO operations. This is commonly used for
|
||||
// middleware-style transformations and operation composition.
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderIO[R, A], B]
|
||||
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
)
|
||||
|
||||
13
v2/readerioeither/consumer.go
Normal file
13
v2/readerioeither/consumer.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package readerioeither
|
||||
|
||||
import "github.com/IBM/fp-go/v2/io"
|
||||
|
||||
//go:inline
|
||||
func ChainConsumer[R, E, A any](c Consumer[A]) Operator[R, E, A, struct{}] {
|
||||
return ChainIOK[R, E](io.FromConsumerK(c))
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstConsumer[R, E, A any](c Consumer[A]) Operator[R, E, A, A] {
|
||||
return ChainFirstIOK[R, E](io.FromConsumerK(c))
|
||||
}
|
||||
@@ -16,6 +16,22 @@
|
||||
// Package readerioeither provides a functional programming abstraction that combines
|
||||
// three powerful concepts: Reader, IO, and Either monads.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This is a monad transformer combining:
|
||||
// - Reader monad: https://github.com/fantasyland/fantasy-land
|
||||
// - 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
|
||||
//
|
||||
// # ReaderIOEither
|
||||
//
|
||||
// ReaderIOEither[R, E, A] represents a computation that:
|
||||
|
||||
@@ -39,10 +39,10 @@ import (
|
||||
// - ma: A ReaderIOEither that takes R2 and may produce a ReaderIOEither[R1, E, A]
|
||||
//
|
||||
// Returns:
|
||||
// - A reader.Kleisli[R2, R1, IOEither[E, A]], which is func(R2) func(R1) IOEither[E, A]
|
||||
// - A Kleisli[R2, E, R1, A], which is func(R2) func(R1) IOEither[E, A]
|
||||
//
|
||||
// The function preserves error handling and IO effects at both levels.
|
||||
func Sequence[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderIOEither[R1, E, A]]) reader.Kleisli[R2, R1, IOEither[E, A]] {
|
||||
func Sequence[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderIOEither[R1, E, A]]) Kleisli[R2, E, R1, A] {
|
||||
return readert.Sequence(
|
||||
ioeither.Chain,
|
||||
ma,
|
||||
@@ -64,8 +64,8 @@ func Sequence[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderIOEither[R1, E, A
|
||||
// - ma: A ReaderIOEither that takes R2 and may produce a Reader[R1, A]
|
||||
//
|
||||
// Returns:
|
||||
// - A reader.Kleisli[R2, R1, IOEither[E, A]], which is func(R2) func(R1) IOEither[E, A]
|
||||
func SequenceReader[R1, R2, E, A any](ma ReaderIOEither[R2, E, Reader[R1, A]]) reader.Kleisli[R2, R1, IOEither[E, A]] {
|
||||
// - A Kleisli[R2, E, R1, A], which is func(R2) func(R1) IOEither[E, A]
|
||||
func SequenceReader[R1, R2, E, A any](ma ReaderIOEither[R2, E, Reader[R1, A]]) Kleisli[R2, E, R1, A] {
|
||||
return readert.SequenceReader(
|
||||
ioeither.Map,
|
||||
ma,
|
||||
@@ -87,8 +87,8 @@ func SequenceReader[R1, R2, E, A any](ma ReaderIOEither[R2, E, Reader[R1, A]]) r
|
||||
// - ma: A ReaderIOEither that takes R2 and may produce a ReaderIO[R1, A]
|
||||
//
|
||||
// Returns:
|
||||
// - A reader.Kleisli[R2, R1, IOEither[E, A]], which is func(R2) func(R1) IOEither[E, A]
|
||||
func SequenceReaderIO[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderIO[R1, A]]) reader.Kleisli[R2, R1, IOEither[E, A]] {
|
||||
// - A Kleisli[R2, E, R1, A], which is func(R2) func(R1) IOEither[E, A]
|
||||
func SequenceReaderIO[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderIO[R1, A]]) Kleisli[R2, E, R1, A] {
|
||||
return func(r1 R1) ReaderIOEither[R2, E, A] {
|
||||
return func(r2 R2) IOEither[E, A] {
|
||||
return func() Either[E, A] {
|
||||
@@ -118,8 +118,8 @@ func SequenceReaderIO[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderIO[R1, A]
|
||||
// - ma: A ReaderIOEither that takes R2 and may produce a ReaderEither[R1, E, A]
|
||||
//
|
||||
// Returns:
|
||||
// - A reader.Kleisli[R2, R1, IOEither[E, A]], which is func(R2) func(R1) IOEither[E, A]
|
||||
func SequenceReaderEither[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderEither[R1, E, A]]) reader.Kleisli[R2, R1, IOEither[E, A]] {
|
||||
// - A Kleisli[R2, E, R1, A], which is func(R2) func(R1) IOEither[E, A]
|
||||
func SequenceReaderEither[R1, R2, E, A any](ma ReaderIOEither[R2, E, ReaderEither[R1, E, A]]) Kleisli[R2, E, R1, A] {
|
||||
return func(r1 R1) ReaderIOEither[R2, E, A] {
|
||||
return func(r2 R2) IOEither[E, A] {
|
||||
return func() Either[E, A] {
|
||||
|
||||
250
v2/readerioeither/rec.go
Normal file
250
v2/readerioeither/rec.go
Normal file
@@ -0,0 +1,250 @@
|
||||
// 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 readerioeither
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
// TailRec implements stack-safe tail recursion for the ReaderIOEither monad.
|
||||
//
|
||||
// This function enables recursive computations that combine three powerful concepts:
|
||||
// - 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.
|
||||
//
|
||||
// # How It Works
|
||||
//
|
||||
// TailRec takes a Kleisli arrow that returns Either[E, Either[A, B]]:
|
||||
// - Left(E): Computation failed with error E - recursion terminates
|
||||
// - Right(Left(A)): Continue recursion with the new state A
|
||||
// - Right(Right(B)): Terminate recursion successfully and return the final result B
|
||||
//
|
||||
// The function iteratively applies the Kleisli arrow, passing the environment R to each
|
||||
// iteration, until either an error (Left) or a final result (Right(Right(B))) is produced.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - R: The environment type (Reader context) - e.g., Config, Logger, Database connection
|
||||
// - E: The error type that can occur during computation
|
||||
// - A: The state type that changes during recursion
|
||||
// - B: The final result type when recursion terminates successfully
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - f: A Kleisli arrow (A => ReaderIOEither[R, E, Either[A, B]]) that:
|
||||
// - Takes the current state A
|
||||
// - Returns a ReaderIOEither that depends on environment R
|
||||
// - Can fail with error E (Left)
|
||||
// - Produces Either[A, B] to control recursion flow (Right)
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// A Kleisli arrow (A => ReaderIOEither[R, E, B]) that:
|
||||
// - Takes an initial state A
|
||||
// - Returns a ReaderIOEither that requires environment R
|
||||
// - Can fail with error E
|
||||
// - Produces the final result B after recursion completes
|
||||
//
|
||||
// # Comparison with Other Monads
|
||||
//
|
||||
// Compared to other tail recursion implementations:
|
||||
// - Like IOEither: Has error handling (Left for errors)
|
||||
// - Like ReaderIO: Has environment dependency (R passed to each iteration)
|
||||
// - Unlike IOOption: Uses Either for both errors and recursion control
|
||||
// - Most powerful: Combines all three aspects (Reader + IO + Either)
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. Environment-dependent recursive algorithms with error handling:
|
||||
// - Recursive computations that need configuration and can fail
|
||||
// - Algorithms that log progress and handle errors gracefully
|
||||
// - Recursive operations with retry logic based on environment settings
|
||||
//
|
||||
// 2. Stateful computations with context and error handling:
|
||||
// - Tree traversals that need environment context and can fail
|
||||
// - Graph algorithms with configuration-dependent behavior and error cases
|
||||
// - Recursive parsers with environment-based rules and error reporting
|
||||
//
|
||||
// 3. Recursive operations with side effects and error handling:
|
||||
// - File system traversals with logging and error handling
|
||||
// - Network operations with retry configuration and failure handling
|
||||
// - Database operations with connection pooling and error recovery
|
||||
//
|
||||
// # Example: Factorial with Logging and Error Handling
|
||||
//
|
||||
// type Env struct {
|
||||
// Logger func(string)
|
||||
// MaxN int
|
||||
// }
|
||||
//
|
||||
// type State struct {
|
||||
// n int
|
||||
// acc int
|
||||
// }
|
||||
//
|
||||
// // Factorial that logs each step and validates input
|
||||
// factorialStep := func(state State) readerioeither.ReaderIOEither[Env, string, either.Either[State, int]] {
|
||||
// return func(env Env) ioeither.IOEither[string, either.Either[State, int]] {
|
||||
// return func() either.Either[string, either.Either[State, int]] {
|
||||
// if state.n > env.MaxN {
|
||||
// return either.Left[either.Either[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
|
||||
// }
|
||||
// if state.n <= 0 {
|
||||
// env.Logger(fmt.Sprintf("Factorial complete: %d", state.acc))
|
||||
// return either.Right[string](either.Right[State](state.acc))
|
||||
// }
|
||||
// env.Logger(fmt.Sprintf("Computing: %d * %d", state.n, state.acc))
|
||||
// return either.Right[string](either.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// factorial := readerioeither.TailRec(factorialStep)
|
||||
// env := Env{Logger: func(msg string) { fmt.Println(msg) }, MaxN: 20}
|
||||
// result := factorial(State{5, 1})(env)() // Returns Right(120), logs each step
|
||||
// // If n > MaxN, returns Left("n too large: ...")
|
||||
//
|
||||
// # Example: File Processing with Error Handling
|
||||
//
|
||||
// type Config struct {
|
||||
// MaxRetries int
|
||||
// Logger func(string)
|
||||
// }
|
||||
//
|
||||
// type ProcessState struct {
|
||||
// files []string
|
||||
// results []string
|
||||
// retries int
|
||||
// }
|
||||
//
|
||||
// processFilesStep := func(state ProcessState) readerioeither.ReaderIOEither[Config, error, either.Either[ProcessState, []string]] {
|
||||
// return func(cfg Config) ioeither.IOEither[error, either.Either[ProcessState, []string]] {
|
||||
// return func() either.Either[error, either.Either[ProcessState, []string]] {
|
||||
// if len(state.files) == 0 {
|
||||
// cfg.Logger("All files processed")
|
||||
// return either.Right[error](either.Right[ProcessState](state.results))
|
||||
// }
|
||||
// file := state.files[0]
|
||||
// cfg.Logger(fmt.Sprintf("Processing: %s", file))
|
||||
//
|
||||
// // Simulate file processing that might fail
|
||||
// if err := processFile(file); err != nil {
|
||||
// if state.retries >= cfg.MaxRetries {
|
||||
// return either.Left[either.Either[ProcessState, []string]](
|
||||
// fmt.Errorf("max retries exceeded for %s: %w", file, err))
|
||||
// }
|
||||
// cfg.Logger(fmt.Sprintf("Retry %d for %s", state.retries+1, file))
|
||||
// return either.Right[error](either.Left[[]string](ProcessState{
|
||||
// files: state.files,
|
||||
// results: state.results,
|
||||
// retries: state.retries + 1,
|
||||
// }))
|
||||
// }
|
||||
//
|
||||
// return either.Right[error](either.Left[[]string](ProcessState{
|
||||
// files: state.files[1:],
|
||||
// results: append(state.results, file),
|
||||
// retries: 0,
|
||||
// }))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// processFiles := readerioeither.TailRec(processFilesStep)
|
||||
// config := Config{MaxRetries: 3, Logger: func(msg string) { fmt.Println(msg) }}
|
||||
// result := processFiles(ProcessState{files: []string{"a.txt", "b.txt"}, results: []string{}, retries: 0})(config)()
|
||||
// // Returns Right([]string{"a.txt", "b.txt"}) on success
|
||||
// // Returns Left(error) if max retries exceeded
|
||||
//
|
||||
// # Stack Safety
|
||||
//
|
||||
// The iterative implementation ensures that even deeply recursive computations
|
||||
// (thousands or millions of iterations) will not cause stack overflow:
|
||||
//
|
||||
// // Safe for very large inputs
|
||||
// countdownStep := func(n int) readerioeither.ReaderIOEither[Env, error, either.Either[int, int]] {
|
||||
// return func(env Env) ioeither.IOEither[error, either.Either[int, int]] {
|
||||
// return func() either.Either[error, either.Either[int, int]] {
|
||||
// if n <= 0 {
|
||||
// return either.Right[error](either.Right[int](0))
|
||||
// }
|
||||
// return either.Right[error](either.Left[int](n - 1))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// countdown := readerioeither.TailRec(countdownStep)
|
||||
// result := countdown(1000000)(env)() // Safe, no stack overflow
|
||||
//
|
||||
// # Error Handling Patterns
|
||||
//
|
||||
// The Either[E, Either[A, B]] structure provides two levels of control:
|
||||
//
|
||||
// 1. Outer Either (Left(E)): Unrecoverable errors that terminate recursion
|
||||
// - Validation failures
|
||||
// - Resource exhaustion
|
||||
// - Fatal errors
|
||||
//
|
||||
// 2. Inner Either (Right(Left(A)) or Right(Right(B))): Recursion control
|
||||
// - Left(A): Continue with new state
|
||||
// - Right(B): Terminate successfully
|
||||
//
|
||||
// This separation allows for:
|
||||
// - Early termination on errors
|
||||
// - Graceful error propagation
|
||||
// - Clear distinction between "continue" and "error" states
|
||||
//
|
||||
// # Performance Considerations
|
||||
//
|
||||
// - Each iteration creates a new IOEither action by calling f(a)(r)()
|
||||
// - The environment R is passed to every iteration
|
||||
// - Error checking happens on each iteration (Left vs Right)
|
||||
// - For performance-critical code, consider if error handling is necessary at each step
|
||||
// - Memoization of environment-derived values may improve performance
|
||||
//
|
||||
// # See Also
|
||||
//
|
||||
// - [ioeither.TailRec]: Tail recursion with error handling (no environment)
|
||||
// - [readerio.TailRec]: Tail recursion with environment (no error handling)
|
||||
// - [iooption.TailRec]: Tail recursion with optional results
|
||||
// - [Chain]: For sequencing ReaderIOEither computations
|
||||
// - [Ask]: For accessing the environment
|
||||
// - [Left]/[Right]: For creating error/success values
|
||||
func TailRec[R, E, A, B any](f Kleisli[R, E, A, Either[A, B]]) Kleisli[R, E, A, B] {
|
||||
return func(a A) ReaderIOEither[R, E, B] {
|
||||
initialReader := f(a)
|
||||
return func(r R) IOEither[E, B] {
|
||||
initialB := initialReader(r)
|
||||
return func() Either[E, B] {
|
||||
current := initialB()
|
||||
for {
|
||||
rec, e := either.Unwrap(current)
|
||||
if either.IsLeft(current) {
|
||||
return either.Left[B](e)
|
||||
}
|
||||
b, a := either.Unwrap(rec)
|
||||
if either.IsRight(rec) {
|
||||
return either.Right[E](b)
|
||||
}
|
||||
current = f(a)(r)()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
612
v2/readerioeither/rec_test.go
Normal file
612
v2/readerioeither/rec_test.go
Normal file
@@ -0,0 +1,612 @@
|
||||
// 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 readerioeither
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
E "github.com/IBM/fp-go/v2/either"
|
||||
IOE "github.com/IBM/fp-go/v2/ioeither"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Test environment types
|
||||
type TestEnv struct {
|
||||
Multiplier int
|
||||
MaxValue int
|
||||
Logs []string
|
||||
}
|
||||
|
||||
type LoggerEnv struct {
|
||||
Logger func(string)
|
||||
MaxN int
|
||||
}
|
||||
|
||||
type ConfigEnv struct {
|
||||
MinValue int
|
||||
Step int
|
||||
MaxRetries int
|
||||
}
|
||||
|
||||
// TestTailRecFactorial tests factorial computation with environment-based logging and validation
|
||||
func TestTailRecFactorial(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
logs := []string{}
|
||||
env := LoggerEnv{
|
||||
Logger: func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
},
|
||||
MaxN: 20,
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.n > env.MaxN {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
env.Logger(fmt.Sprintf("Complete: %d", state.acc))
|
||||
return E.Right[string](E.Right[State](state.acc))
|
||||
}
|
||||
env.Logger(fmt.Sprintf("Step: %d * %d", state.n, state.acc))
|
||||
return E.Right[string](E.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result := factorial(State{5, 1})(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](120), result)
|
||||
assert.Equal(t, 6, len(logs)) // 5 steps + 1 complete
|
||||
assert.Contains(t, logs[0], "Step: 5 * 1")
|
||||
assert.Contains(t, logs[len(logs)-1], "Complete: 120")
|
||||
}
|
||||
|
||||
// TestTailRecFactorialError tests factorial with input exceeding max value
|
||||
func TestTailRecFactorialError(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
acc int
|
||||
}
|
||||
|
||||
env := LoggerEnv{
|
||||
Logger: func(msg string) {},
|
||||
MaxN: 10,
|
||||
}
|
||||
|
||||
factorialStep := func(state State) ReaderIOEither[LoggerEnv, string, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.n > env.MaxN {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("n too large: %d > %d", state.n, env.MaxN))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
return E.Right[string](E.Right[State](state.acc))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.n - 1, state.acc * state.n}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
factorial := TailRec(factorialStep)
|
||||
result := factorial(State{15, 1})(env)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.Unwrap(result)
|
||||
assert.Contains(t, err, "n too large: 15 > 10")
|
||||
}
|
||||
|
||||
// TestTailRecFibonacci tests Fibonacci computation with environment dependency
|
||||
func TestTailRecFibonacci(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
prev int
|
||||
curr int
|
||||
}
|
||||
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 1000, Logs: []string{}}
|
||||
|
||||
fibStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.curr > env.MaxValue {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
return E.Right[string](E.Right[State](state.curr * env.Multiplier))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fib := TailRec(fibStep)
|
||||
result := fib(State{10, 0, 1})(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](89), result) // 10th Fibonacci number
|
||||
}
|
||||
|
||||
// TestTailRecFibonacciError tests Fibonacci with value exceeding max
|
||||
func TestTailRecFibonacciError(t *testing.T) {
|
||||
type State struct {
|
||||
n int
|
||||
prev int
|
||||
curr int
|
||||
}
|
||||
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 50, Logs: []string{}}
|
||||
|
||||
fibStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.curr > env.MaxValue {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("value exceeds max: %d > %d", state.curr, env.MaxValue))
|
||||
}
|
||||
if state.n <= 0 {
|
||||
return E.Right[string](E.Right[State](state.curr))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fib := TailRec(fibStep)
|
||||
result := fib(State{20, 0, 1})(env)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.Unwrap(result)
|
||||
assert.Contains(t, err, "value exceeds max")
|
||||
}
|
||||
|
||||
// TestTailRecCountdown tests countdown with configuration-based step
|
||||
func TestTailRecCountdown(t *testing.T) {
|
||||
config := ConfigEnv{MinValue: 0, Step: 2, MaxRetries: 3}
|
||||
|
||||
countdownStep := func(n int) ReaderIOEither[ConfigEnv, string, E.Either[int, int]] {
|
||||
return func(cfg ConfigEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
if n < 0 {
|
||||
return E.Left[E.Either[int, int]]("negative value")
|
||||
}
|
||||
if n <= cfg.MinValue {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - cfg.Step))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)(config)()
|
||||
|
||||
assert.Equal(t, E.Of[string](0), result)
|
||||
}
|
||||
|
||||
// TestTailRecSumList tests summing a list with environment-based multiplier and error handling
|
||||
func TestTailRecSumList(t *testing.T) {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
env := TestEnv{Multiplier: 2, MaxValue: 100, Logs: []string{}}
|
||||
|
||||
sumStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.sum > env.MaxValue {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
|
||||
}
|
||||
if len(state.list) == 0 {
|
||||
return E.Right[string](E.Right[State](state.sum * env.Multiplier))
|
||||
}
|
||||
return E.Right[string](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})(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](30), result) // (1+2+3+4+5) * 2 = 30
|
||||
}
|
||||
|
||||
// TestTailRecSumListError tests sum exceeding max value
|
||||
func TestTailRecSumListError(t *testing.T) {
|
||||
type State struct {
|
||||
list []int
|
||||
sum int
|
||||
}
|
||||
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 10, Logs: []string{}}
|
||||
|
||||
sumStep := func(state State) ReaderIOEither[TestEnv, string, E.Either[State, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.sum > env.MaxValue {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("sum exceeds max: %d > %d", state.sum, env.MaxValue))
|
||||
}
|
||||
if len(state.list) == 0 {
|
||||
return E.Right[string](E.Right[State](state.sum))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sumList := TailRec(sumStep)
|
||||
result := sumList(State{[]int{5, 10, 15}, 0})(env)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.Unwrap(result)
|
||||
assert.Contains(t, err, "sum exceeds max")
|
||||
}
|
||||
|
||||
// TestTailRecImmediateTermination tests immediate termination (Right on first call)
|
||||
func TestTailRecImmediateTermination(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 100, Logs: []string{}}
|
||||
|
||||
immediateStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
return E.Right[string](E.Right[int](n * env.Multiplier))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
immediate := TailRec(immediateStep)
|
||||
result := immediate(42)(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](42), result)
|
||||
}
|
||||
|
||||
// TestTailRecImmediateError tests immediate error (Left on first call)
|
||||
func TestTailRecImmediateError(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 100, Logs: []string{}}
|
||||
|
||||
immediateErrorStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
return E.Left[E.Either[int, int]]("immediate error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
immediateError := TailRec(immediateErrorStep)
|
||||
result := immediateError(42)(env)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.Unwrap(result)
|
||||
assert.Equal(t, "immediate error", err)
|
||||
}
|
||||
|
||||
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
|
||||
func TestTailRecStackSafety(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 2000000, Logs: []string{}}
|
||||
|
||||
countdownStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
if n > env.MaxValue {
|
||||
return E.Left[E.Either[int, int]]("value too large")
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10000)(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](0), result)
|
||||
}
|
||||
|
||||
// TestTailRecFindInRange tests finding a value in a range with environment-based target
|
||||
func TestTailRecFindInRange(t *testing.T) {
|
||||
type FindEnv struct {
|
||||
Target int
|
||||
MaxN int
|
||||
}
|
||||
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
}
|
||||
|
||||
env := FindEnv{Target: 42, MaxN: 1000}
|
||||
|
||||
findStep := func(state State) ReaderIOEither[FindEnv, string, E.Either[State, int]] {
|
||||
return func(env FindEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.current > env.MaxN {
|
||||
return E.Left[E.Either[State, int]]("search exceeded max")
|
||||
}
|
||||
if state.current >= state.max {
|
||||
return E.Right[string](E.Right[State](-1)) // Not found
|
||||
}
|
||||
if state.current == env.Target {
|
||||
return E.Right[string](E.Right[State](state.current)) // Found
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.current + 1, state.max}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result := find(State{0, 100})(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](42), result)
|
||||
}
|
||||
|
||||
// TestTailRecFindNotInRange tests finding a value not in range
|
||||
func TestTailRecFindNotInRange(t *testing.T) {
|
||||
type FindEnv struct {
|
||||
Target int
|
||||
MaxN int
|
||||
}
|
||||
|
||||
type State struct {
|
||||
current int
|
||||
max int
|
||||
}
|
||||
|
||||
env := FindEnv{Target: 200, MaxN: 1000}
|
||||
|
||||
findStep := func(state State) ReaderIOEither[FindEnv, string, E.Either[State, int]] {
|
||||
return func(env FindEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.current > env.MaxN {
|
||||
return E.Left[E.Either[State, int]]("search exceeded max")
|
||||
}
|
||||
if state.current >= state.max {
|
||||
return E.Right[string](E.Right[State](-1)) // Not found
|
||||
}
|
||||
if state.current == env.Target {
|
||||
return E.Right[string](E.Right[State](state.current)) // Found
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.current + 1, state.max}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
find := TailRec(findStep)
|
||||
result := find(State{0, 100})(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](-1), result)
|
||||
}
|
||||
|
||||
// TestTailRecWithLogging tests that logging side effects occur during recursion
|
||||
func TestTailRecWithLogging(t *testing.T) {
|
||||
logs := []string{}
|
||||
env := LoggerEnv{
|
||||
Logger: func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
},
|
||||
MaxN: 100,
|
||||
}
|
||||
|
||||
countdownStep := func(n int) ReaderIOEither[LoggerEnv, string, E.Either[int, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
env.Logger(fmt.Sprintf("Count: %d", n))
|
||||
if n > env.MaxN {
|
||||
return E.Left[E.Either[int, int]]("value too large")
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(5)(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](0), result)
|
||||
assert.Equal(t, 6, len(logs)) // 5, 4, 3, 2, 1, 0
|
||||
assert.Equal(t, "Count: 5", logs[0])
|
||||
assert.Equal(t, "Count: 0", logs[5])
|
||||
}
|
||||
|
||||
// TestTailRecGCD tests greatest common divisor with environment-based logging
|
||||
func TestTailRecGCD(t *testing.T) {
|
||||
type State struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
|
||||
logs := []string{}
|
||||
env := LoggerEnv{
|
||||
Logger: func(msg string) {
|
||||
logs = append(logs, msg)
|
||||
},
|
||||
MaxN: 1000,
|
||||
}
|
||||
|
||||
gcdStep := func(state State) ReaderIOEither[LoggerEnv, string, E.Either[State, int]] {
|
||||
return func(env LoggerEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
env.Logger(fmt.Sprintf("gcd(%d, %d)", state.a, state.b))
|
||||
if state.a > env.MaxN || state.b > env.MaxN {
|
||||
return E.Left[E.Either[State, int]]("values too large")
|
||||
}
|
||||
if state.b == 0 {
|
||||
return E.Right[string](E.Right[State](state.a))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.b, state.a % state.b}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gcd := TailRec(gcdStep)
|
||||
result := gcd(State{48, 18})(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](6), result)
|
||||
assert.Greater(t, len(logs), 0)
|
||||
assert.Contains(t, logs[0], "gcd(48, 18)")
|
||||
}
|
||||
|
||||
// TestTailRecRetryLogic tests retry logic with environment-based max retries
|
||||
func TestTailRecRetryLogic(t *testing.T) {
|
||||
type State struct {
|
||||
attempt int
|
||||
value int
|
||||
}
|
||||
|
||||
config := ConfigEnv{MinValue: 0, Step: 1, MaxRetries: 3}
|
||||
|
||||
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, E.Either[State, int]] {
|
||||
return func(cfg ConfigEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.attempt > cfg.MaxRetries {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
|
||||
}
|
||||
// Simulate success on 3rd attempt
|
||||
if state.attempt == 3 {
|
||||
return E.Right[string](E.Right[State](state.value))
|
||||
}
|
||||
return E.Right[string](E.Left[int](State{state.attempt + 1, state.value}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retry := TailRec(retryStep)
|
||||
result := retry(State{0, 42})(config)()
|
||||
|
||||
assert.Equal(t, E.Of[string](42), result)
|
||||
}
|
||||
|
||||
// TestTailRecRetryExceeded tests retry logic exceeding max retries
|
||||
func TestTailRecRetryExceeded(t *testing.T) {
|
||||
type State struct {
|
||||
attempt int
|
||||
value int
|
||||
}
|
||||
|
||||
config := ConfigEnv{MinValue: 0, Step: 1, MaxRetries: 2}
|
||||
|
||||
retryStep := func(state State) ReaderIOEither[ConfigEnv, string, E.Either[State, int]] {
|
||||
return func(cfg ConfigEnv) IOE.IOEither[string, E.Either[State, int]] {
|
||||
return func() E.Either[string, E.Either[State, int]] {
|
||||
if state.attempt > cfg.MaxRetries {
|
||||
return E.Left[E.Either[State, int]](fmt.Sprintf("max retries exceeded: %d", cfg.MaxRetries))
|
||||
}
|
||||
// Never succeeds
|
||||
return E.Right[string](E.Left[int](State{state.attempt + 1, state.value}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
retry := TailRec(retryStep)
|
||||
result := retry(State{0, 42})(config)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.Unwrap(result)
|
||||
assert.Contains(t, err, "max retries exceeded: 2")
|
||||
}
|
||||
|
||||
// TestTailRecMultipleEnvironmentAccess tests that environment is accessible in each iteration
|
||||
func TestTailRecMultipleEnvironmentAccess(t *testing.T) {
|
||||
type CounterEnv struct {
|
||||
Increment int
|
||||
Limit int
|
||||
MaxValue int
|
||||
}
|
||||
|
||||
env := CounterEnv{Increment: 3, Limit: 20, MaxValue: 100}
|
||||
|
||||
counterStep := func(n int) ReaderIOEither[CounterEnv, string, E.Either[int, int]] {
|
||||
return func(env CounterEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
if n > env.MaxValue {
|
||||
return E.Left[E.Either[int, int]]("value exceeds max")
|
||||
}
|
||||
if n >= env.Limit {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n + env.Increment))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
counter := TailRec(counterStep)
|
||||
result := counter(0)(env)()
|
||||
|
||||
assert.Equal(t, E.Of[string](21), result) // 0 -> 3 -> 6 -> 9 -> 12 -> 15 -> 18 -> 21
|
||||
}
|
||||
|
||||
// TestTailRecErrorInMiddle tests error occurring in the middle of recursion
|
||||
func TestTailRecErrorInMiddle(t *testing.T) {
|
||||
env := TestEnv{Multiplier: 1, MaxValue: 50, Logs: []string{}}
|
||||
|
||||
countdownStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
if n == 5 {
|
||||
return E.Left[E.Either[int, int]]("error at 5")
|
||||
}
|
||||
if n <= 0 {
|
||||
return E.Right[string](E.Right[int](n))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countdown := TailRec(countdownStep)
|
||||
result := countdown(10)(env)()
|
||||
|
||||
assert.True(t, E.IsLeft(result))
|
||||
_, err := E.Unwrap(result)
|
||||
assert.Equal(t, "error at 5", err)
|
||||
}
|
||||
|
||||
// TestTailRecDifferentEnvironments tests that different environments produce different results
|
||||
func TestTailRecDifferentEnvironments(t *testing.T) {
|
||||
multiplyStep := func(n int) ReaderIOEither[TestEnv, string, E.Either[int, int]] {
|
||||
return func(env TestEnv) IOE.IOEither[string, E.Either[int, int]] {
|
||||
return func() E.Either[string, E.Either[int, int]] {
|
||||
if n <= 0 {
|
||||
return E.Right[string](E.Right[int](n * env.Multiplier))
|
||||
}
|
||||
return E.Right[string](E.Left[int](n - 1))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
multiply := TailRec(multiplyStep)
|
||||
|
||||
env1 := TestEnv{Multiplier: 2, MaxValue: 100, Logs: []string{}}
|
||||
env2 := TestEnv{Multiplier: 5, MaxValue: 100, Logs: []string{}}
|
||||
|
||||
result1 := multiply(5)(env1)()
|
||||
result2 := multiply(5)(env2)()
|
||||
|
||||
// Both reach 0, but multiplied by different values
|
||||
assert.Equal(t, E.Of[string](0), result1) // 0 * 2 = 0
|
||||
assert.Equal(t, E.Of[string](0), result2) // 0 * 5 = 0
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
package readerioeither
|
||||
|
||||
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/ioeither"
|
||||
@@ -92,4 +93,6 @@ type (
|
||||
|
||||
ReaderOption[R, A any] = readeroption.ReaderOption[R, A]
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
)
|
||||
|
||||
15
v2/readerioresult/consumer.go
Normal file
15
v2/readerioresult/consumer.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/readerioeither"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func ChainConsumer[R, A any](c Consumer[A]) Operator[R, A, struct{}] {
|
||||
return readerioeither.ChainConsumer[R, error](c)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func ChainFirstConsumer[R, A any](c Consumer[A]) Operator[R, A, A] {
|
||||
return readerioeither.ChainFirstConsumer[R, error](c)
|
||||
}
|
||||
@@ -16,6 +16,22 @@
|
||||
// package readerioresult provides a functional programming abstraction that combines
|
||||
// three powerful concepts: Reader, IO, and Either monads.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This is a monad transformer combining:
|
||||
// - Reader monad: https://github.com/fantasyland/fantasy-land
|
||||
// - 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
|
||||
//
|
||||
// # ReaderIOResult
|
||||
//
|
||||
// ReaderIOResult[R, A] represents a computation that:
|
||||
|
||||
@@ -6,27 +6,27 @@ import (
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func Sequence[R1, R2, A any](ma ReaderIOResult[R2, ReaderIOResult[R1, A]]) reader.Kleisli[R2, R1, IOResult[A]] {
|
||||
func Sequence[R1, R2, A any](ma ReaderIOResult[R2, ReaderIOResult[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return RIOE.Sequence(ma)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func SequenceReader[R1, R2, A any](ma ReaderIOResult[R2, Reader[R1, A]]) reader.Kleisli[R2, R1, IOResult[A]] {
|
||||
func SequenceReader[R1, R2, A any](ma ReaderIOResult[R2, Reader[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return RIOE.SequenceReader(ma)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func SequenceReaderIO[R1, R2, A any](ma ReaderIOResult[R2, ReaderIO[R1, A]]) reader.Kleisli[R2, R1, IOResult[A]] {
|
||||
func SequenceReaderIO[R1, R2, A any](ma ReaderIOResult[R2, ReaderIO[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return RIOE.SequenceReaderIO(ma)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func SequenceReaderEither[R1, R2, A any](ma ReaderIOResult[R2, ReaderResult[R1, A]]) reader.Kleisli[R2, R1, IOResult[A]] {
|
||||
func SequenceReaderEither[R1, R2, A any](ma ReaderIOResult[R2, ReaderResult[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return RIOE.SequenceReaderEither(ma)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func SequenceReaderResult[R1, R2, A any](ma ReaderIOResult[R2, ReaderResult[R1, A]]) reader.Kleisli[R2, R1, IOResult[A]] {
|
||||
func SequenceReaderResult[R1, R2, A any](ma ReaderIOResult[R2, ReaderResult[R1, A]]) Kleisli[R2, R1, A] {
|
||||
return RIOE.SequenceReaderEither(ma)
|
||||
}
|
||||
|
||||
|
||||
25
v2/readerioresult/rec.go
Normal file
25
v2/readerioresult/rec.go
Normal 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 readerioresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/readerioeither"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
|
||||
return readerioeither.TailRec(f)
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
package readerioresult
|
||||
|
||||
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"
|
||||
"github.com/IBM/fp-go/v2/ioeither"
|
||||
@@ -30,9 +32,7 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// Either represents a value of one of two possible types (a disjoint union).
|
||||
// An instance of Either is either Left (representing an error) or Right (representing a success).
|
||||
Either[E, A any] = Result[A]
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
Result[A any] = result.Result[A]
|
||||
|
||||
@@ -103,4 +103,6 @@ type (
|
||||
// Example:
|
||||
// var doubleOp Operator[Config, error, int, int] = Map(N.Mul(2))
|
||||
Operator[R, A, B any] = Kleisli[R, ReaderIOResult[R, A], B]
|
||||
|
||||
Consumer[A any] = consumer.Consumer[A]
|
||||
)
|
||||
|
||||
42
v2/readeroption/rec.go
Normal file
42
v2/readeroption/rec.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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 readeroption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
|
||||
return func(a A) ReaderOption[R, B] {
|
||||
initialReader := f(a)
|
||||
return func(r R) Option[B] {
|
||||
current := initialReader(r)
|
||||
for {
|
||||
rec, ok := option.Unwrap(current)
|
||||
if !ok {
|
||||
return option.None[B]()
|
||||
}
|
||||
b, a := either.Unwrap(rec)
|
||||
if either.IsRight(rec) {
|
||||
return option.Some(b)
|
||||
}
|
||||
current = f(a)(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,20 @@
|
||||
|
||||
// Package readeroption provides a monad transformer that combines the Reader and Option monads.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This is a monad transformer combining:
|
||||
// - Reader 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
|
||||
//
|
||||
// ReaderOption[R, A] represents a computation that:
|
||||
// - Depends on a shared environment of type R (Reader monad)
|
||||
// - May fail to produce a value of type A (Option monad)
|
||||
@@ -63,6 +77,7 @@
|
||||
package readeroption
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
@@ -78,6 +93,8 @@ type (
|
||||
// Option represents an optional value
|
||||
Option[A any] = option.Option[A]
|
||||
|
||||
Either[E, A any] = either.Either[E, A]
|
||||
|
||||
// Reader represents a computation that depends on an environment R and produces a value A
|
||||
Reader[R, A any] = reader.Reader[R, A]
|
||||
|
||||
|
||||
@@ -15,6 +15,21 @@
|
||||
|
||||
// Package readerresult provides a ReaderResult monad that combines the Reader and Result monads.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This is a monad transformer combining:
|
||||
// - Reader 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
|
||||
//
|
||||
// A ReaderResult[R, A] represents a computation that:
|
||||
// - Depends on an environment of type R (Reader aspect)
|
||||
// - May fail with an error (Result aspect, which is Either[error, A])
|
||||
|
||||
25
v2/readerresult/rec.go
Normal file
25
v2/readerresult/rec.go
Normal 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 readerresult
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/readereither"
|
||||
)
|
||||
|
||||
//go:inline
|
||||
func TailRec[R, A, B any](f Kleisli[R, A, Either[A, B]]) Kleisli[R, A, B] {
|
||||
return readereither.TailRec(f)
|
||||
}
|
||||
@@ -13,53 +13,66 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// package result provides the Either monad, a data structure representing a value of one of two possible types.
|
||||
// Package result provides the Result monad, a specialized Either monad with error as the left type.
|
||||
//
|
||||
// Either is commonly used for error handling, where by convention:
|
||||
// - Left represents an error or failure case (type E)
|
||||
// - Right represents a success case (type A)
|
||||
// Result is commonly used for error handling, where:
|
||||
// - Error represents a failure case (type error)
|
||||
// - Ok 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)
|
||||
// or a Right value (typically a successful result). This makes it ideal for computations that may fail.
|
||||
// The Result type is a discriminated union that can hold either an Error value
|
||||
// or an Ok value (successful result). This makes it ideal for computations that may fail.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// // Creating Either values
|
||||
// success := either.Right[error](42) // Right value
|
||||
// failure := either.Left[int](errors.New("oops")) // Left value
|
||||
// // Creating Result values
|
||||
// success := result.Ok(42) // Ok value
|
||||
// failure := result.Error[int](errors.New("oops")) // Error value
|
||||
//
|
||||
// // Pattern matching with Fold
|
||||
// result := either.Fold(
|
||||
// output := result.Fold(
|
||||
// func(err error) string { return "Error: " + err.Error() },
|
||||
// func(n int) string { return fmt.Sprintf("Success: %d", n) },
|
||||
// )(success)
|
||||
//
|
||||
// // Chaining operations (short-circuits on Left)
|
||||
// result := either.Chain(func(n int) either.Result[int] {
|
||||
// return either.Right[error](n * 2)
|
||||
// // Chaining operations (short-circuits on Error)
|
||||
// doubled := result.Chain(func(n int) result.Result[int] {
|
||||
// return result.Ok(n * 2)
|
||||
// })(success)
|
||||
//
|
||||
// # Monadic Operations
|
||||
//
|
||||
// Either implements the Monad interface, providing:
|
||||
// - Map: Transform the Right value
|
||||
// Result implements the Monad interface, providing:
|
||||
// - Map: Transform the Ok value
|
||||
// - Chain (FlatMap): Chain computations that may fail
|
||||
// - Ap: Apply a function wrapped in Either
|
||||
// - Ap: Apply a function wrapped in Result
|
||||
//
|
||||
// # Error Handling
|
||||
//
|
||||
// Either provides utilities for working with Go's error type:
|
||||
// - TryCatchError: Convert (value, error) tuples to Either
|
||||
// - UnwrapError: Convert Either back to (value, error) tuple
|
||||
// - FromError: Create Either from error-returning functions
|
||||
// Result provides utilities for working with Go's error type:
|
||||
// - TryCatchError: Convert (value, error) tuples to Result
|
||||
// - UnwrapError: Convert Result back to (value, error) tuple
|
||||
// - FromError: Create Result from error-returning functions
|
||||
//
|
||||
// # Subpackages
|
||||
//
|
||||
// - either/exec: Execute system commands returning Either
|
||||
// - either/http: HTTP request builders returning Either
|
||||
// - either/testing: Testing utilities for Either laws
|
||||
// - result/http: HTTP request builders returning Result
|
||||
package result
|
||||
|
||||
//go:generate go run .. either --count 15 --filename gen.go
|
||||
|
||||
@@ -17,26 +17,185 @@ package result
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log/slog"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
)
|
||||
|
||||
// Logger creates a logging function for Either values that logs both Left and Right cases.
|
||||
// The function logs the value and then returns the original Either unchanged.
|
||||
// Logger creates a logging function for Result values that logs both error and success cases.
|
||||
// The function logs the value and then returns the original Result unchanged.
|
||||
//
|
||||
// This is a specialized version of either.Logger where the Left (error) type is fixed to error.
|
||||
// It provides a convenient way to add logging to Result-based computations without affecting
|
||||
// the computation's outcome. The logger is particularly useful for debugging and monitoring
|
||||
// Result pipelines.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value (Right side of the Result)
|
||||
//
|
||||
// Parameters:
|
||||
// - loggers: Optional log.Logger instances. If none provided, uses default logger.
|
||||
// - loggers: Optional log.Logger instances. If none provided, uses the default logger.
|
||||
//
|
||||
// Example:
|
||||
// Returns:
|
||||
// - A function that takes a prefix string and returns an Operator[A, A] that logs and passes through the Result
|
||||
//
|
||||
// logger := either.Logger[error, int]()
|
||||
// Behavior:
|
||||
// - For Ok(value): Logs the success value with the given prefix
|
||||
// - For Err(error): Logs the error with the given prefix
|
||||
// - Always returns the original Result unchanged
|
||||
//
|
||||
// Example with success value:
|
||||
//
|
||||
// logger := result.Logger[int]()
|
||||
// result := F.Pipe2(
|
||||
// either.Right[error](42),
|
||||
// logger("Processing"),
|
||||
// either.Map(N.Mul(2)),
|
||||
// result.Of(42),
|
||||
// logger("Processing"), // Logs: "Processing: 42"
|
||||
// result.Map(N.Mul(2)),
|
||||
// )
|
||||
// // Logs: "Processing: 42"
|
||||
// // result is Right(84)
|
||||
// // result is Ok(84)
|
||||
//
|
||||
// Example with error:
|
||||
//
|
||||
// logger := result.Logger[User]()
|
||||
// result := F.Pipe2(
|
||||
// result.Error[User](errors.New("database connection failed")),
|
||||
// logger("Fetching user"), // Logs: "Fetching user: database connection failed"
|
||||
// result.Map(processUser),
|
||||
// )
|
||||
// // result is Err(error), Map is not executed
|
||||
//
|
||||
// Example with custom logger:
|
||||
//
|
||||
// customLogger := log.New(os.Stderr, "APP: ", log.LstdFlags)
|
||||
// logger := result.Logger[Data](customLogger)
|
||||
//
|
||||
// result := F.Pipe3(
|
||||
// fetchData(id),
|
||||
// logger("Fetched"), // Logs to custom logger
|
||||
// result.Map(transform),
|
||||
// logger("Transformed"), // Logs to custom logger
|
||||
// )
|
||||
//
|
||||
// Example in a pipeline with multiple logging points:
|
||||
//
|
||||
// logger := result.Logger[Response]()
|
||||
//
|
||||
// result := F.Pipe4(
|
||||
// validateInput(input),
|
||||
// logger("Validated"),
|
||||
// result.Chain(processData),
|
||||
// logger("Processed"),
|
||||
// result.Chain(saveToDatabase),
|
||||
// logger("Saved"),
|
||||
// )
|
||||
// // Logs at each step, showing the progression or where an error occurred
|
||||
//
|
||||
// Use Cases:
|
||||
// - Debugging: Track values flowing through Result pipelines
|
||||
// - Monitoring: Log successful operations and errors for observability
|
||||
// - Auditing: Record operations without affecting the computation
|
||||
// - Development: Inspect intermediate values during development
|
||||
// - Error tracking: Log errors as they occur in the pipeline
|
||||
//
|
||||
// Note: The logging is a side effect that doesn't modify the Result. The original
|
||||
// Result is always returned, making this function safe to insert anywhere in a
|
||||
// Result pipeline without changing the computation's semantics.
|
||||
//
|
||||
//go:inline
|
||||
func Logger[A any](loggers ...*log.Logger) func(string) Operator[A, A] {
|
||||
return either.Logger[error, A](loggers...)
|
||||
}
|
||||
|
||||
// ToSLogAttr converts a Result value to a structured logging attribute (slog.Attr).
|
||||
//
|
||||
// This function creates a converter that transforms Result values into slog.Attr for use
|
||||
// with Go's structured logging (log/slog). It maps:
|
||||
// - Err(error) values to an "error" attribute
|
||||
// - Ok(value) values to a "value" attribute
|
||||
//
|
||||
// This is a specialized version of either.ToSLogAttr where the error type is fixed to error,
|
||||
// making it particularly convenient for integrating Result-based error handling with
|
||||
// structured logging systems. It allows you to log both successful values and errors in a
|
||||
// consistent, structured format.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of the success value (Right side of the Result)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that converts Result[A] to slog.Attr
|
||||
//
|
||||
// Example with error:
|
||||
//
|
||||
// converter := result.ToSLogAttr[int]()
|
||||
// errResult := result.Error[int](errors.New("connection failed"))
|
||||
// attr := converter(errResult)
|
||||
// // 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 success value:
|
||||
//
|
||||
// converter := result.ToSLogAttr[User]()
|
||||
// okResult := result.Of(User{ID: 123, Name: "Alice"})
|
||||
// attr := converter(okResult)
|
||||
// // 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 := result.ToSLogAttr[Data]()
|
||||
//
|
||||
// res := F.Pipe2(
|
||||
// fetchData(id),
|
||||
// result.Map(processData),
|
||||
// result.Map(validateData),
|
||||
// )
|
||||
//
|
||||
// attr := toAttr(res)
|
||||
// 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 Result:
|
||||
//
|
||||
// toAttr := result.ToSLogAttr[Response]()
|
||||
// res := callAPI(endpoint)
|
||||
//
|
||||
// level := result.Fold(
|
||||
// func(error) slog.Level { return slog.LevelError },
|
||||
// func(Response) slog.Level { return slog.LevelInfo },
|
||||
// )(res)
|
||||
//
|
||||
// logger.LogAttrs(ctx, level, "API call completed", toAttr(res))
|
||||
//
|
||||
// Example with multiple attributes:
|
||||
//
|
||||
// toAttr := result.ToSLogAttr[Order]()
|
||||
// res := processOrder(orderID)
|
||||
//
|
||||
// logger.LogAttrs(ctx, slog.LevelInfo, "Order processed",
|
||||
// slog.String("order_id", orderID),
|
||||
// slog.String("user_id", userID),
|
||||
// toAttr(res), // Adds either "error" or "value" attribute
|
||||
// )
|
||||
//
|
||||
// Use Cases:
|
||||
// - Structured logging: Convert Result outcomes 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 Result-based error handling with logging systems
|
||||
// - Debugging: Inspect Result values in logs with proper structure
|
||||
// - Metrics: Extract Result values for metrics collection in logging pipelines
|
||||
// - Audit trails: Create structured audit logs from Result computations
|
||||
//
|
||||
// Note: The returned slog.Attr uses "error" for Err values and "value" for Ok values.
|
||||
// These keys are consistent with common structured logging conventions and make it easy
|
||||
// to query and filter logs based on success or failure.
|
||||
//
|
||||
//go:inline
|
||||
func ToSLogAttr[A any]() func(Result[A]) slog.Attr {
|
||||
return either.ToSLogAttr[error, A]()
|
||||
}
|
||||
|
||||
23
v2/result/rec.go
Normal file
23
v2/result/rec.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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 result
|
||||
|
||||
import "github.com/IBM/fp-go/v2/either"
|
||||
|
||||
//go:inline
|
||||
func TailRec[A, B any](f Kleisli[A, Either[A, B]]) Kleisli[A, B] {
|
||||
return either.TailRec(f)
|
||||
}
|
||||
@@ -16,6 +16,21 @@
|
||||
// Package statereaderioeither provides a functional programming abstraction that combines
|
||||
// four powerful concepts: State, Reader, IO, and Either monads.
|
||||
//
|
||||
// # Fantasy Land Specification
|
||||
//
|
||||
// This is a monad transformer combining:
|
||||
// - State monad: https://github.com/fantasyland/fantasy-land
|
||||
// - Reader monad: https://github.com/fantasyland/fantasy-land
|
||||
// - 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
|
||||
// - 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
|
||||
//
|
||||
// # StateReaderIOEither
|
||||
//
|
||||
// StateReaderIOEither[S, R, E, A] represents a computation that:
|
||||
|
||||
Reference in New Issue
Block a user