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

Compare commits

...

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
d739c9b277 fix: add doc to readerio
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-03 18:13:59 +01:00
Dr. Carsten Leue
f0054431a5 fix: add logging to readerio 2025-12-03 18:07:06 +01:00
Carsten Leue
1a89ec3df7 fix: implement Sequence for Pair
Signed-off-by: Carsten Leue <carsten.leue@de.ibm.com>
2025-11-28 11:22:23 +01:00
9 changed files with 1862 additions and 12 deletions

View File

@@ -23,6 +23,7 @@ import (
"sync"
"text/template"
"github.com/IBM/fp-go/v2/function"
L "github.com/IBM/fp-go/v2/logging"
)
@@ -99,7 +100,7 @@ func Printf[A any](prefix string) Kleisli[A, A] {
// The template is compiled lazily using sync.Once to ensure it's only parsed once.
// The function always returns the original value unchanged, making it suitable for
// use with ChainFirst or similar operations.
func handleLogging[A any](onSuccess func(string), onError func(error), prefix string) Kleisli[A, A] {
func handleLoggingG(onSuccess func(string), onError func(error), prefix string) Kleisli[any, any] {
var tmp *template.Template
var err error
var once sync.Once
@@ -108,8 +109,8 @@ func handleLogging[A any](onSuccess func(string), onError func(error), prefix st
tmp, err = template.New("").Parse(prefix)
}
return func(a A) IO[A] {
return func() A {
return func(a any) IO[any] {
return func() any {
// make sure to compile lazily
once.Do(init)
if err == nil {
@@ -131,6 +132,28 @@ func handleLogging[A any](onSuccess func(string), onError func(error), prefix st
}
}
// handleLogging is a helper function that creates a Kleisli arrow for logging/printing
// values using Go template syntax. It lazily compiles the template on first use and
// executes it with the provided value as data.
//
// Parameters:
// - onSuccess: callback function to handle successfully formatted output
// - onError: callback function to handle template parsing or execution errors
// - prefix: Go template string to format the value
//
// The template is compiled lazily using sync.Once to ensure it's only parsed once.
// The function always returns the original value unchanged, making it suitable for
// use with ChainFirst or similar operations.
func handleLogging[A any](onSuccess func(string), onError func(error), prefix string) Kleisli[A, A] {
generic := handleLoggingG(onSuccess, onError, prefix)
return func(a A) IO[A] {
return function.Pipe1(
generic(a),
MapTo[any](a),
)
}
}
// LogGo constructs a logger function using Go template syntax for formatting.
// The prefix string is parsed as a Go template and executed with the value as data.
// Both successful output and template errors are logged using log.Println.

View File

@@ -27,14 +27,28 @@ import (
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
// modifying that copy
func setCopy[SET ~func(*S, A) *S, S, A any](setter SET) func(s *S, a A) *S {
return func(s *S, a A) *S {
var empty S
safeSet := func(s *S, a A) *S {
// make sure we have a total implementation
cpy := *s
return setter(&cpy, a)
}
return func(s *S, a A) *S {
// make sure we have a total implementation
if s != nil {
return safeSet(s, a)
}
// fallback to the empty object
return safeSet(&empty, a)
}
}
func setCopyWithEq[GET ~func(*S) A, SET ~func(*S, A) *S, S, A any](pred EQ.Eq[A], getter GET, setter SET) func(s *S, a A) *S {
return func(s *S, a A) *S {
var empty S
safeSet := func(s *S, a A) *S {
if pred.Equals(getter(s), a) {
return s
}
@@ -42,17 +56,39 @@ func setCopyWithEq[GET ~func(*S) A, SET ~func(*S, A) *S, S, A any](pred EQ.Eq[A]
cpy := *s
return setter(&cpy, a)
}
return func(s *S, a A) *S {
// make sure we have a total implementation
if s != nil {
return safeSet(s, a)
}
// fallback to the empty object
return safeSet(&empty, a)
}
}
// setCopyCurried wraps a setter for a pointer into a setter that first creates a copy before
// modifying that copy
func setCopyCurried[SET ~func(A) Endomorphism[*S], S, A any](setter SET) func(A) Endomorphism[*S] {
var empty S
return func(a A) Endomorphism[*S] {
seta := setter(a)
return func(s *S) *S {
safeSet := func(s *S) *S {
// make sure we have a total implementation
cpy := *s
return seta(&cpy)
}
return func(s *S) *S {
// make sure we have a total implementation
if s != nil {
return safeSet(s)
}
// fallback to the empty object
return safeSet(&empty)
}
}
}
@@ -442,6 +478,8 @@ func compose[GET ~func(S) B, SET ~func(B) func(S) S, S, A, B any](creator func(g
// person := Person{Name: "Alice", Address: Address{Street: "Main St"}}
// street := personStreetLens.Get(person) // "Main St"
// updated := personStreetLens.Set("Oak Ave")(person)
//
//go:inline
func Compose[S, A, B any](ab Lens[A, B]) Operator[S, A, B] {
return compose(MakeLensCurried[func(S) B, func(B) func(S) S], ab)
}

View File

@@ -544,3 +544,396 @@ func TestMakeLensStrict_BoolField(t *testing.T) {
same := enabledLens.Set(true)(config)
assert.Same(t, config, same)
}
func TestMakeLensRef_WithNilState(t *testing.T) {
// Test that MakeLensRef creates a total lens that works with nil pointers
nameLens := MakeLensRef(
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
// Test Get with nil - should handle gracefully
var nilStreet *Street = nil
name := nameLens.Get(nilStreet)
assert.Equal(t, "", name)
// Test Set with nil - should create a new object with zero values except the set field
updated := nameLens.Set("NewStreet")(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, "NewStreet", updated.name)
assert.Equal(t, 0, updated.num) // Zero value for int
// Verify original nil pointer is unchanged
assert.Nil(t, nilStreet)
}
func TestMakeLensRef_WithNilState_IntField(t *testing.T) {
// Test with an int field lens
numLens := MakeLensRef(
func(s *Street) int {
if s == nil {
return 0
}
return s.num
},
func(s *Street, num int) *Street {
s.num = num
return s
},
)
var nilStreet *Street = nil
// Get from nil should return zero value
num := numLens.Get(nilStreet)
assert.Equal(t, 0, num)
// Set on nil should create new object
updated := numLens.Set(42)(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, 42, updated.num)
assert.Equal(t, "", updated.name) // Zero value for string
}
func TestMakeLensRef_WithNilState_Composed(t *testing.T) {
// Test composed lenses with nil state
streetLens := MakeLensRef(
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
(*Street).SetName,
)
addrLens := MakeLensRef(
func(a *Address) *Street {
if a == nil {
return nil
}
return a.street
},
(*Address).SetStreet,
)
// Compose the lenses
streetName := ComposeRef[Address](streetLens)(addrLens)
var nilAddress *Address = nil
// Get from nil should handle gracefully
name := streetName.Get(nilAddress)
assert.Equal(t, "", name)
// Set on nil should create new nested structure
updated := streetName.Set("TestStreet")(nilAddress)
assert.NotNil(t, updated)
assert.NotNil(t, updated.street)
assert.Equal(t, "TestStreet", updated.street.name)
assert.Equal(t, "", updated.city) // Zero value for city
}
func TestMakeLensRefCurried_WithNilState(t *testing.T) {
// Test that MakeLensRefCurried creates a total lens that works with nil pointers
nameLens := MakeLensRefCurried(
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(name string) func(*Street) *Street {
return func(s *Street) *Street {
s.name = name
return s
}
},
)
// Test Get with nil
var nilStreet *Street = nil
name := nameLens.Get(nilStreet)
assert.Equal(t, "", name)
// Test Set with nil - should create a new object
updated := nameLens.Set("CurriedStreet")(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, "CurriedStreet", updated.name)
assert.Equal(t, 0, updated.num) // Zero value for int
// Verify original nil pointer is unchanged
assert.Nil(t, nilStreet)
}
func TestMakeLensRefCurried_WithNilState_IntField(t *testing.T) {
// Test with an int field lens using curried setter
numLens := MakeLensRefCurried(
func(s *Street) int {
if s == nil {
return 0
}
return s.num
},
func(num int) func(*Street) *Street {
return func(s *Street) *Street {
s.num = num
return s
}
},
)
var nilStreet *Street = nil
// Get from nil should return zero value
num := numLens.Get(nilStreet)
assert.Equal(t, 0, num)
// Set on nil should create new object
updated := numLens.Set(99)(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, 99, updated.num)
assert.Equal(t, "", updated.name) // Zero value for string
}
func TestMakeLensRefCurried_WithNilState_MultipleOperations(t *testing.T) {
// Test multiple operations on nil and non-nil states
nameLens := MakeLensRefCurried(
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(name string) func(*Street) *Street {
return func(s *Street) *Street {
s.name = name
return s
}
},
)
var nilStreet *Street = nil
// First operation on nil
street1 := nameLens.Set("First")(nilStreet)
assert.NotNil(t, street1)
assert.Equal(t, "First", street1.name)
// Second operation on non-nil result
street2 := nameLens.Set("Second")(street1)
assert.NotNil(t, street2)
assert.Equal(t, "Second", street2.name)
assert.Equal(t, "First", street1.name) // Original unchanged
// Third operation back to nil (edge case)
street3 := nameLens.Set("Third")(nilStreet)
assert.NotNil(t, street3)
assert.Equal(t, "Third", street3.name)
}
func TestMakeLensRef_WithNilState_NestedStructure(t *testing.T) {
// Test with nested structure where inner can be nil
innerLens := MakeLensRef(
func(o *Outer) *Inner {
if o == nil {
return nil
}
return o.inner
},
func(o *Outer, i *Inner) *Outer {
o.inner = i
return o
},
)
var nilOuter *Outer = nil
// Get from nil outer
inner := innerLens.Get(nilOuter)
assert.Nil(t, inner)
// Set on nil outer
newInner := &Inner{Value: 42, Foo: "test"}
updated := innerLens.Set(newInner)(nilOuter)
assert.NotNil(t, updated)
assert.Equal(t, newInner, updated.inner)
}
func TestMakeLensWithEq_WithNilState(t *testing.T) {
// Test that MakeLensWithEq creates a total lens that works with nil pointers
nameLens := MakeLensWithEq(
EQ.FromStrictEquals[string](),
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
// Test Get with nil - should handle gracefully
var nilStreet *Street = nil
name := nameLens.Get(nilStreet)
assert.Equal(t, "", name)
// Test Set with nil - should create a new object with zero values except the set field
updated := nameLens.Set("NewStreet")(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, "NewStreet", updated.name)
assert.Equal(t, 0, updated.num) // Zero value for int
// Verify original nil pointer is unchanged
assert.Nil(t, nilStreet)
}
func TestMakeLensWithEq_WithNilState_IntField(t *testing.T) {
// Test with an int field lens with equality optimization
numLens := MakeLensWithEq(
EQ.FromStrictEquals[int](),
func(s *Street) int {
if s == nil {
return 0
}
return s.num
},
func(s *Street, num int) *Street {
s.num = num
return s
},
)
var nilStreet *Street = nil
// Get from nil should return zero value
num := numLens.Get(nilStreet)
assert.Equal(t, 0, num)
// Set on nil should create new object
updated := numLens.Set(42)(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, 42, updated.num)
assert.Equal(t, "", updated.name) // Zero value for string
}
func TestMakeLensWithEq_WithNilState_EqualityOptimization(t *testing.T) {
// Test that equality optimization works with nil state
nameLens := MakeLensWithEq(
EQ.FromStrictEquals[string](),
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
var nilStreet *Street = nil
// Setting empty string on nil should return a new object with empty string
// (since the zero value equals the set value)
updated1 := nameLens.Set("")(nilStreet)
assert.NotNil(t, updated1)
assert.Equal(t, "", updated1.name)
// Setting the same empty string again should return the same pointer (optimization)
updated2 := nameLens.Set("")(updated1)
assert.Same(t, updated1, updated2)
// Setting a different value should create a new copy
updated3 := nameLens.Set("Different")(updated1)
assert.NotSame(t, updated1, updated3)
assert.Equal(t, "Different", updated3.name)
assert.Equal(t, "", updated1.name)
}
func TestMakeLensWithEq_WithNilState_CustomEq(t *testing.T) {
// Test with custom equality predicate on nil state
customEq := EQ.FromEquals(func(a, b string) bool {
return len(a) == len(b) && a == b
})
nameLens := MakeLensWithEq(
customEq,
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
var nilStreet *Street = nil
// Get from nil
name := nameLens.Get(nilStreet)
assert.Equal(t, "", name)
// Set on nil with non-empty string
updated := nameLens.Set("Test")(nilStreet)
assert.NotNil(t, updated)
assert.Equal(t, "Test", updated.name)
// Set same value should return same pointer
same := nameLens.Set("Test")(updated)
assert.Same(t, updated, same)
}
func TestMakeLensWithEq_WithNilState_MultipleOperations(t *testing.T) {
// Test multiple operations on nil and non-nil states with equality optimization
nameLens := MakeLensWithEq(
EQ.FromStrictEquals[string](),
func(s *Street) string {
if s == nil {
return ""
}
return s.name
},
func(s *Street, name string) *Street {
s.name = name
return s
},
)
var nilStreet *Street = nil
// First operation on nil
street1 := nameLens.Set("First")(nilStreet)
assert.NotNil(t, street1)
assert.Equal(t, "First", street1.name)
// Second operation with same value - should return same pointer
street2 := nameLens.Set("First")(street1)
assert.Same(t, street1, street2)
// Third operation with different value - should create new copy
street3 := nameLens.Set("Second")(street2)
assert.NotSame(t, street2, street3)
assert.Equal(t, "Second", street3.name)
assert.Equal(t, "First", street2.name)
// Fourth operation back to nil with zero value
street4 := nameLens.Set("")(nilStreet)
assert.NotNil(t, street4)
assert.Equal(t, "", street4.name)
}

48
v2/pair/sequence.go Normal file
View File

@@ -0,0 +1,48 @@
package pair
import "github.com/IBM/fp-go/v2/function"
func MonadSequence[L, A, HKTA, HKTPA any](
mmap func(HKTA, Kleisli[L, A, A]) HKTPA,
fas Pair[L, HKTA],
) HKTPA {
return mmap(Tail(fas), FromHead[A](Head(fas)))
}
func MonadTraverse[L, A, HKTA, HKTPA any](
mmap func(HKTA, Kleisli[L, A, A]) HKTPA,
f func(A) HKTA,
fas Pair[L, A],
) HKTPA {
return mmap(f(Tail(fas)), FromHead[A](Head(fas)))
}
func Sequence[L, A, HKTA, HKTPA any](
mmap func(Kleisli[L, A, A]) func(HKTA) HKTPA,
) func(Pair[L, HKTA]) HKTPA {
fh := function.Flow2(
Head[L, HKTA],
FromHead[A, L],
)
return func(fas Pair[L, HKTA]) HKTPA {
return mmap(fh(fas))(Tail(fas))
}
}
func Traverse[L, A, HKTA, HKTPA any](
mmap func(Kleisli[L, A, A]) func(HKTA) HKTPA,
) func(func(A) HKTA) func(Pair[L, A]) HKTPA {
fh := function.Flow2(
Head[L, A],
FromHead[A, L],
)
return func(f func(A) HKTA) func(Pair[L, A]) HKTPA {
ft := function.Flow2(
Tail[L, A],
f,
)
return func(fas Pair[L, A]) HKTPA {
return mmap(fh(fas))(ft(fas))
}
}
}

245
v2/readerio/logging.go Normal file
View File

@@ -0,0 +1,245 @@
// 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"
"log"
"os"
"strings"
"sync"
"text/template"
)
// Logf constructs a logger function that can be used with ChainFirst or similar operations.
// The prefix string contains the format string for both the reader context (R) and the value (A).
// It uses log.Printf to output the formatted message.
//
// Type Parameters:
// - R: Reader context type
// - A: Value type
//
// Parameters:
// - prefix: Format string that accepts two arguments: the reader context and the value
//
// Returns:
// - A Kleisli arrow that logs the context and value, then returns the original value
//
// Example:
//
// type Config struct {
// AppName string
// }
// result := pipe.Pipe2(
// fetchUser(),
// readerio.ChainFirst(readerio.Logf[Config, User]("[%v] User: %+v")),
// processUser,
// )(Config{AppName: "MyApp"})()
func Logf[R, A any](prefix string) Kleisli[R, A, A] {
return func(a A) ReaderIO[R, A] {
return func(r R) IO[A] {
return func() A {
log.Printf(prefix, r, a)
return a
}
}
}
}
// Printf constructs a printer function that can be used with ChainFirst or similar operations.
// The prefix string contains the format string for both the reader context (R) and the value (A).
// Unlike Logf, this prints to stdout without log prefixes.
//
// Type Parameters:
// - R: Reader context type
// - A: Value type
//
// Parameters:
// - prefix: Format string that accepts two arguments: the reader context and the value
//
// Returns:
// - A Kleisli arrow that prints the context and value, then returns the original value
//
// Example:
//
// type Config struct {
// Debug bool
// }
// result := pipe.Pipe2(
// fetchData(),
// readerio.ChainFirst(readerio.Printf[Config, Data]("[%v] Data: %+v\n")),
// processData,
// )(Config{Debug: true})()
func Printf[R, A any](prefix string) Kleisli[R, A, A] {
return func(a A) ReaderIO[R, A] {
return func(r R) IO[A] {
return func() A {
fmt.Printf(prefix, r, a)
return a
}
}
}
}
// handleLoggingG is a generic helper function that creates a Kleisli arrow for logging/printing
// values using Go template syntax. It lazily compiles the template on first use and
// executes it with a context struct containing both the reader context (R) and value (A).
//
// Parameters:
// - onSuccess: callback function to handle successfully formatted output
// - onError: callback function to handle template parsing or execution errors
// - prefix: Go template string to format the context and value
//
// The template is compiled lazily using sync.Once to ensure it's only parsed once.
// The template receives a context struct with fields R (reader context) and A (value).
// The function always returns the original value unchanged, making it suitable for
// use with ChainFirst or similar operations.
func handleLoggingG(onSuccess func(string), onError func(error), prefix string) Kleisli[any, any, any] {
var tmp *template.Template
var err error
var once sync.Once
type context struct {
R any
A any
}
init := func() {
tmp, err = template.New("").Parse(prefix)
}
return func(a any) ReaderIO[any, any] {
return func(r any) IO[any] {
return func() any {
// make sure to compile lazily
once.Do(init)
if err == nil {
var buffer strings.Builder
tmpErr := tmp.Execute(&buffer, context{r, a})
if tmpErr != nil {
onError(tmpErr)
onSuccess(fmt.Sprintf("%v: %v", r, a))
} else {
onSuccess(buffer.String())
}
} else {
onError(err)
onSuccess(fmt.Sprintf("%v: %v", r, a))
}
// in any case return the original value
return a
}
}
}
}
// handleLogging is a typed wrapper around handleLoggingG that creates a Kleisli arrow
// for logging/printing values using Go template syntax.
//
// Parameters:
// - onSuccess: callback function to handle successfully formatted output
// - onError: callback function to handle template parsing or execution errors
// - prefix: Go template string to format the context and value
//
// Returns:
// - A Kleisli arrow that formats and outputs the value, then returns it unchanged
func handleLogging[R, A any](onSuccess func(string), onError func(error), prefix string) Kleisli[R, A, A] {
generic := handleLoggingG(onSuccess, onError, prefix)
return func(a A) ReaderIO[R, A] {
ga := generic(a)
return func(r R) IO[A] {
gr := ga(r)
return func() A {
gr()
return a
}
}
}
}
// LogGo constructs a logger function using Go template syntax for formatting.
// The prefix string is parsed as a Go template and executed with a context struct
// containing both the reader context (R) and the value (A) as fields .R and .A.
// Both successful output and template errors are logged using log.Println.
//
// Type Parameters:
// - R: Reader context type
// - A: Value type
//
// Parameters:
// - prefix: Go template string with access to .R (context) and .A (value)
//
// Returns:
// - A Kleisli arrow that logs the formatted output and returns the original value
//
// Example:
//
// type Config struct {
// AppName string
// }
// type User struct {
// Name string
// Age int
// }
// result := pipe.Pipe2(
// fetchUser(),
// readerio.ChainFirst(readerio.LogGo[Config, User]("[{{.R.AppName}}] User: {{.A.Name}}, Age: {{.A.Age}}")),
// processUser,
// )(Config{AppName: "MyApp"})()
func LogGo[R, A any](prefix string) Kleisli[R, A, A] {
return handleLogging[R, A](func(value string) {
log.Println(value)
}, func(err error) {
log.Println(err)
}, prefix)
}
// PrintGo constructs a printer function using Go template syntax for formatting.
// The prefix string is parsed as a Go template and executed with a context struct
// containing both the reader context (R) and the value (A) as fields .R and .A.
// Successful output is printed to stdout using fmt.Println, while template errors
// are printed to stderr using fmt.Fprintln.
//
// Type Parameters:
// - R: Reader context type
// - A: Value type
//
// Parameters:
// - prefix: Go template string with access to .R (context) and .A (value)
//
// Returns:
// - A Kleisli arrow that prints the formatted output and returns the original value
//
// Example:
//
// type Config struct {
// Verbose bool
// }
// type Data struct {
// ID int
// Value string
// }
// result := pipe.Pipe2(
// fetchData(),
// readerio.ChainFirst(readerio.PrintGo[Config, Data]("{{if .R.Verbose}}[VERBOSE] {{end}}Data: {{.A.ID}} - {{.A.Value}}")),
// processData,
// )(Config{Verbose: true})()
func PrintGo[R, A any](prefix string) Kleisli[R, A, A] {
return handleLogging[R, A](func(value string) {
fmt.Println(value)
}, func(err error) {
fmt.Fprintln(os.Stderr, err)
}, prefix)
}

367
v2/readerio/logging_test.go Normal file
View File

@@ -0,0 +1,367 @@
// 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 (
"testing"
"github.com/stretchr/testify/assert"
)
type (
TestConfig struct {
AppName string
Debug bool
}
TestUser struct {
Name string
Age int
}
TestData struct {
ID int
Value string
}
)
func TestLogf(t *testing.T) {
l := Logf[TestConfig, int]("[%v] Value: %d")
rio := l(42)
config := TestConfig{AppName: "TestApp", Debug: true}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestLogfReturnsOriginalValue(t *testing.T) {
l := Logf[TestConfig, TestUser]("[%v] User: %+v")
rio := l(TestUser{Name: "Alice", Age: 30})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, TestUser{Name: "Alice", Age: 30}, result)
}
func TestLogfWithDifferentTypes(t *testing.T) {
// Test with string value
l1 := Logf[string, string]("[%s] Message: %s")
rio1 := l1("hello")
result1 := rio1("context")()
assert.Equal(t, "hello", result1)
// Test with struct value
l2 := Logf[int, TestData]("[%d] Data: %+v")
rio2 := l2(TestData{ID: 123, Value: "test"})
result2 := rio2(999)()
assert.Equal(t, TestData{ID: 123, Value: "test"}, result2)
}
func TestPrintf(t *testing.T) {
l := Printf[TestConfig, int]("[%v] Value: %d\n")
rio := l(42)
config := TestConfig{AppName: "TestApp", Debug: false}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestPrintfReturnsOriginalValue(t *testing.T) {
l := Printf[TestConfig, TestUser]("[%v] User: %+v\n")
rio := l(TestUser{Name: "Bob", Age: 25})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, TestUser{Name: "Bob", Age: 25}, result)
}
func TestPrintfWithDifferentTypes(t *testing.T) {
// Test with float value
l1 := Printf[string, float64]("[%s] Number: %.2f\n")
rio1 := l1(3.14159)
result1 := rio1("PI")()
assert.Equal(t, 3.14159, result1)
// Test with bool value
l2 := Printf[int, bool]("[%d] Flag: %v\n")
rio2 := l2(true)
result2 := rio2(1)()
assert.True(t, result2)
}
func TestLogGo(t *testing.T) {
l := LogGo[TestConfig, TestUser]("[{{.R.AppName}}] User: {{.A.Name}}, Age: {{.A.Age}}")
rio := l(TestUser{Name: "Charlie", Age: 35})
config := TestConfig{AppName: "MyApp", Debug: true}
result := rio(config)()
assert.Equal(t, TestUser{Name: "Charlie", Age: 35}, result)
}
func TestLogGoReturnsOriginalValue(t *testing.T) {
l := LogGo[TestConfig, TestData]("{{.R.AppName}}: Data {{.A.ID}} - {{.A.Value}}")
rio := l(TestData{ID: 456, Value: "test data"})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, TestData{ID: 456, Value: "test data"}, result)
}
func TestLogGoWithInvalidTemplate(t *testing.T) {
// Invalid template syntax - should not panic
l := LogGo[TestConfig, int]("Value: {{.A.MissingField")
rio := l(42)
config := TestConfig{AppName: "TestApp"}
assert.NotPanics(t, func() {
result := rio(config)()
assert.Equal(t, 42, result)
})
}
func TestLogGoWithComplexTemplate(t *testing.T) {
type Address struct {
Street string
City string
}
type Person struct {
Name string
Address Address
}
l := LogGo[TestConfig, Person]("[{{.R.AppName}}] Person: {{.A.Name}} from {{.A.Address.City}}")
rio := l(Person{
Name: "David",
Address: Address{Street: "Main St", City: "NYC"},
})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, "David", result.Name)
assert.Equal(t, "NYC", result.Address.City)
}
func TestLogGoWithConditionalTemplate(t *testing.T) {
l := LogGo[TestConfig, int]("{{if .R.Debug}}[DEBUG] {{end}}Value: {{.A}}")
rio := l(100)
config := TestConfig{AppName: "TestApp", Debug: true}
result := rio(config)()
assert.Equal(t, 100, result)
}
func TestPrintGo(t *testing.T) {
l := PrintGo[TestConfig, TestUser]("[{{.R.AppName}}] User: {{.A.Name}}, Age: {{.A.Age}}")
rio := l(TestUser{Name: "Eve", Age: 28})
config := TestConfig{AppName: "MyApp", Debug: false}
result := rio(config)()
assert.Equal(t, TestUser{Name: "Eve", Age: 28}, result)
}
func TestPrintGoReturnsOriginalValue(t *testing.T) {
l := PrintGo[TestConfig, TestData]("{{.R.AppName}}: {{.A.ID}} - {{.A.Value}}")
rio := l(TestData{ID: 789, Value: "sample"})
config := TestConfig{AppName: "TestApp"}
result := rio(config)()
assert.Equal(t, TestData{ID: 789, Value: "sample"}, result)
}
func TestPrintGoWithInvalidTemplate(t *testing.T) {
// Invalid template syntax - should not panic
l := PrintGo[TestConfig, string]("Value: {{.")
rio := l("test")
config := TestConfig{AppName: "TestApp"}
assert.NotPanics(t, func() {
result := rio(config)()
assert.Equal(t, "test", result)
})
}
func TestPrintGoWithComplexTemplate(t *testing.T) {
type Score struct {
Player string
Points int
}
l := PrintGo[TestConfig, Score]("{{if .R.Debug}}[DEBUG] {{end}}{{.A.Player}}: {{.A.Points}} points")
rio := l(Score{Player: "Alice", Points: 100})
config := TestConfig{AppName: "GameApp", Debug: true}
result := rio(config)()
assert.Equal(t, "Alice", result.Player)
assert.Equal(t, 100, result.Points)
}
func TestLogGoInPipeline(t *testing.T) {
config := TestConfig{AppName: "PipelineApp", Debug: true}
// Create a pipeline using Chain and logging
pipeline := MonadChain(
LogGo[TestConfig, TestData]("[{{.R.AppName}}] Processing: {{.A.ID}}")(TestData{ID: 10, Value: "initial"}),
func(d TestData) ReaderIO[TestConfig, TestData] {
return Of[TestConfig](TestData{ID: d.ID * 2, Value: d.Value + "_processed"})
},
)
result := pipeline(config)()
assert.Equal(t, 20, result.ID)
assert.Equal(t, "initial_processed", result.Value)
}
func TestPrintGoInPipeline(t *testing.T) {
config := TestConfig{AppName: "PrintApp", Debug: false}
pipeline := MonadChain(
PrintGo[TestConfig, string]("[{{.R.AppName}}] Input: {{.A}}")("hello"),
func(s string) ReaderIO[TestConfig, string] {
return Of[TestConfig](s + " world")
},
)
result := pipeline(config)()
assert.Equal(t, "hello world", result)
}
func TestLogfInPipeline(t *testing.T) {
config := TestConfig{AppName: "LogfApp"}
pipeline := MonadChain(
Logf[TestConfig, int]("[%v] Value: %d")(5),
func(n int) ReaderIO[TestConfig, int] {
return Of[TestConfig](n * 3)
},
)
result := pipeline(config)()
assert.Equal(t, 15, result)
}
func TestPrintfInPipeline(t *testing.T) {
config := TestConfig{AppName: "PrintfApp"}
pipeline := MonadChain(
Printf[TestConfig, float64]("[%v] Number: %.1f\n")(2.5),
func(n float64) ReaderIO[TestConfig, float64] {
return Of[TestConfig](n * 2)
},
)
result := pipeline(config)()
assert.Equal(t, 5.0, result)
}
func TestMultipleLoggersInPipeline(t *testing.T) {
config := TestConfig{AppName: "MultiApp", Debug: true}
pipeline := MonadChain(
Logf[TestConfig, int]("[%v] Initial: %d")(10),
func(n int) ReaderIO[TestConfig, int] {
return MonadChain(
LogGo[TestConfig, int]("[{{.R.AppName}}] After add: {{.A}}")(n+5),
func(n int) ReaderIO[TestConfig, int] {
return Of[TestConfig](n * 2)
},
)
},
)
result := pipeline(config)()
assert.Equal(t, 30, result)
}
func TestLogGoWithNestedStructs(t *testing.T) {
type Inner struct {
Value int
}
type Outer struct {
Name string
Inner Inner
}
l := LogGo[TestConfig, Outer]("[{{.R.AppName}}] {{.A.Name}}: {{.A.Inner.Value}}")
rio := l(Outer{Name: "Test", Inner: Inner{Value: 42}})
config := TestConfig{AppName: "NestedApp"}
result := rio(config)()
assert.Equal(t, "Test", result.Name)
assert.Equal(t, 42, result.Inner.Value)
}
func TestPrintGoWithNestedStructs(t *testing.T) {
type Config struct {
Host string
Port int
}
type Request struct {
Method string
Config Config
}
l := PrintGo[TestConfig, Request]("{{.A.Method}} -> {{.A.Config.Host}}:{{.A.Config.Port}}")
rio := l(Request{
Method: "GET",
Config: Config{Host: "localhost", Port: 8080},
})
config := TestConfig{AppName: "HTTPApp"}
result := rio(config)()
assert.Equal(t, "GET", result.Method)
assert.Equal(t, "localhost", result.Config.Host)
assert.Equal(t, 8080, result.Config.Port)
}
func TestLogGoWithEmptyTemplate(t *testing.T) {
l := LogGo[TestConfig, int]("")
rio := l(42)
config := TestConfig{AppName: "EmptyApp"}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestPrintGoWithEmptyTemplate(t *testing.T) {
l := PrintGo[TestConfig, string]("")
rio := l("test")
config := TestConfig{AppName: "EmptyApp"}
result := rio(config)()
assert.Equal(t, "test", result)
}
// Made with Bob

View File

@@ -13,6 +13,82 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package readerio provides the ReaderIO monad, which combines the Reader and IO monads.
//
// ReaderIO[R, A] represents a computation that:
// - Requires an environment of type R (Reader aspect)
// - Performs side effects (IO aspect)
// - Produces a value of type A
//
// This monad is particularly useful for dependency injection patterns and logging scenarios,
// where you need to:
// - Access configuration or context throughout your application
// - Perform side effects like logging, file I/O, or network calls
// - Maintain functional composition and testability
//
// # Logging Use Case
//
// ReaderIO is especially well-suited for logging because it allows you to:
// - Pass a logger through your computation chain without explicit parameter threading
// - Compose logging operations with other side effects
// - Test logging behavior by providing mock loggers in the environment
//
// Key functions for logging scenarios:
// - [Ask]: Retrieve the entire environment (e.g., a logger instance)
// - [Asks]: Extract a specific value from the environment (e.g., logger.Info method)
// - [ChainIOK]: Chain logging operations that return IO effects
// - [MonadChain]: Sequence multiple logging and computation steps
//
// Example logging usage:
//
// type Env struct {
// Logger *log.Logger
// }
//
// // Log a message using the environment's logger
// logInfo := func(msg string) readerio.ReaderIO[Env, func()] {
// return readerio.Asks(func(env Env) io.IO[func()] {
// return io.Of(func() { env.Logger.Println(msg) })
// })
// }
//
// // Compose logging with computation
// computation := F.Pipe3(
// readerio.Of[Env](42),
// readerio.Chain(func(n int) readerio.ReaderIO[Env, int] {
// return F.Pipe1(
// logInfo(fmt.Sprintf("Processing: %d", n)),
// readerio.Map[Env](func(func()) int { return n * 2 }),
// )
// }),
// readerio.ChainIOK(func(result int) io.IO[int] {
// return io.Of(result)
// }),
// )
//
// // Execute with environment
// env := Env{Logger: log.New(os.Stdout, "APP: ", log.LstdFlags)}
// result := computation(env)() // Logs "Processing: 42" and returns 84
//
// # Core Operations
//
// The package provides standard monadic operations:
// - [Of]: Lift a pure value into ReaderIO
// - [Map]: Transform the result value
// - [Chain]: Sequence dependent computations
// - [Ap]: Apply a function in ReaderIO context
//
// # Integration
//
// Convert between different contexts:
// - [FromIO]: Lift an IO action into ReaderIO
// - [FromReader]: Lift a Reader into ReaderIO
// - [ChainIOK]: Chain with IO-returning functions
//
// # Performance
//
// - [Memoize]: Cache computation results (use with caution for context-dependent values)
// - [Defer]: Ensure fresh computation on each execution
package readerio
import (
@@ -27,59 +103,316 @@ import (
"github.com/IBM/fp-go/v2/reader"
)
// FromIO converts an [IO] to a [ReaderIO]
// FromIO converts an [IO] action to a [ReaderIO] that ignores the environment.
// This lifts a pure IO computation into the ReaderIO context.
//
// Type Parameters:
// - R: Reader environment type
// - A: Result type
//
// Parameters:
// - t: The IO action to lift
//
// Returns:
// - A ReaderIO that executes the IO action regardless of the environment
//
// Example:
//
// ioAction := io.Of(42)
// readerIO := readerio.FromIO[Config](ioAction)
// result := readerIO(config)() // Returns 42
func FromIO[R, A any](t IO[A]) ReaderIO[R, A] {
return reader.Of[R](t)
}
// FromReader converts a [Reader] to a [ReaderIO] by lifting the pure computation into IO.
// This allows you to use Reader computations in a ReaderIO context.
//
// Type Parameters:
// - R: Reader environment type
// - A: Result type
//
// Parameters:
// - r: The Reader to convert
//
// Returns:
// - A ReaderIO that wraps the Reader computation in IO
//
// Example:
//
// reader := func(config Config) int { return config.Port }
// readerIO := readerio.FromReader(reader)
// result := readerIO(config)() // Returns config.Port
func FromReader[R, A any](r Reader[R, A]) ReaderIO[R, A] {
return readert.MonadFromReader[Reader[R, A], ReaderIO[R, A]](io.Of[A], r)
}
// MonadMap applies a function to the value inside a ReaderIO context.
// This is the monadic version that takes the ReaderIO as the first parameter.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Output value type
//
// Parameters:
// - fa: The ReaderIO containing the value to transform
// - f: The transformation function
//
// Returns:
// - A new ReaderIO with the transformed value
//
// Example:
//
// rio := readerio.Of[Config](5)
// doubled := readerio.MonadMap(rio, func(n int) int { return n * 2 })
// result := doubled(config)() // Returns 10
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)
}
// Map creates a function that applies a transformation to a ReaderIO value.
// This is the curried version suitable for use in pipelines.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Output value type
//
// Parameters:
// - f: The transformation function
//
// Returns:
// - An Operator that transforms ReaderIO[R, A] to ReaderIO[R, B]
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](5),
// readerio.Map[Config](func(n int) int { return n * 2 }),
// )(config)() // Returns 10
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)
}
// MonadChain sequences two ReaderIO computations, where the second depends on the result of the first.
// This is the monadic bind operation for ReaderIO.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Output value type
//
// Parameters:
// - ma: The first ReaderIO computation
// - f: Function that takes the result of ma and returns a new ReaderIO
//
// Returns:
// - A ReaderIO that sequences both computations
//
// Example:
//
// rio1 := readerio.Of[Config](5)
// result := readerio.MonadChain(rio1, func(n int) readerio.ReaderIO[Config, int] {
// return readerio.Of[Config](n * 2)
// })
func MonadChain[R, A, B any](ma ReaderIO[R, A], f func(A) ReaderIO[R, B]) ReaderIO[R, B] {
return readert.MonadChain(io.MonadChain[A, B], ma, f)
}
// Chain creates a function that sequences ReaderIO computations.
// This is the curried version suitable for use in pipelines.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Output value type
//
// Parameters:
// - f: Function that takes a value and returns a ReaderIO
//
// Returns:
// - An Operator that chains ReaderIO computations
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](5),
// readerio.Chain(func(n int) readerio.ReaderIO[Config, int] {
// return readerio.Of[Config](n * 2)
// }),
// )(config)() // Returns 10
func Chain[R, A, B any](f func(A) ReaderIO[R, B]) Operator[R, A, B] {
return readert.Chain[ReaderIO[R, A]](io.Chain[A, B], f)
}
// Of creates a ReaderIO that returns a pure value, ignoring the environment.
// This is the monadic return/pure operation for ReaderIO.
//
// Type Parameters:
// - R: Reader environment type
// - A: Value type
//
// Parameters:
// - a: The value to wrap
//
// Returns:
// - A ReaderIO that always returns the given value
//
// Example:
//
// rio := readerio.Of[Config](42)
// result := rio(config)() // Returns 42
func Of[R, A any](a A) ReaderIO[R, A] {
return readert.MonadOf[ReaderIO[R, A]](io.Of[A], a)
}
// MonadAp applies a function wrapped in a ReaderIO to a value wrapped in a ReaderIO.
// This is the applicative apply operation for ReaderIO.
//
// Type Parameters:
// - B: Result type
// - R: Reader environment type
// - A: Input value type
//
// Parameters:
// - fab: ReaderIO containing a function from A to B
// - fa: ReaderIO containing a value of type A
//
// Returns:
// - A ReaderIO containing the result of applying the function to the value
//
// Example:
//
// fabIO := readerio.Of[Config](func(n int) int { return n * 2 })
// faIO := readerio.Of[Config](5)
// result := readerio.MonadAp(fabIO, faIO)(config)() // Returns 10
func MonadAp[B, R, A any](fab ReaderIO[R, func(A) B], fa ReaderIO[R, A]) ReaderIO[R, B] {
return readert.MonadAp[ReaderIO[R, A], ReaderIO[R, B], ReaderIO[R, func(A) B], R, A](io.MonadAp[A, B], fab, fa)
}
// MonadApSeq is like MonadAp but ensures sequential execution of effects.
//
// Type Parameters:
// - B: Result type
// - R: Reader environment type
// - A: Input value type
//
// Parameters:
// - fab: ReaderIO containing a function from A to B
// - fa: ReaderIO containing a value of type A
//
// Returns:
// - A ReaderIO containing the result, with sequential execution guaranteed
func MonadApSeq[B, R, A any](fab ReaderIO[R, func(A) B], fa ReaderIO[R, A]) ReaderIO[R, B] {
return readert.MonadAp[ReaderIO[R, A], ReaderIO[R, B], ReaderIO[R, func(A) B], R, A](io.MonadApSeq[A, B], fab, fa)
}
// MonadApPar is like MonadAp but allows parallel execution of effects where possible.
//
// Type Parameters:
// - B: Result type
// - R: Reader environment type
// - A: Input value type
//
// Parameters:
// - fab: ReaderIO containing a function from A to B
// - fa: ReaderIO containing a value of type A
//
// Returns:
// - A ReaderIO containing the result, with potential parallel execution
func MonadApPar[B, R, A any](fab ReaderIO[R, func(A) B], fa ReaderIO[R, A]) ReaderIO[R, B] {
return readert.MonadAp[ReaderIO[R, A], ReaderIO[R, B], ReaderIO[R, func(A) B], R, A](io.MonadApPar[A, B], fab, fa)
}
// Ap creates a function that applies a ReaderIO value to a ReaderIO function.
// This is the curried version suitable for use in pipelines.
//
// Type Parameters:
// - B: Result type
// - R: Reader environment type
// - A: Input value type
//
// Parameters:
// - fa: ReaderIO containing a value of type A
//
// Returns:
// - An Operator that applies the value to a function
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](func(n int) int { return n * 2 }),
// readerio.Ap[int](readerio.Of[Config](5)),
// )(config)() // Returns 10
func Ap[B, R, A any](fa ReaderIO[R, A]) Operator[R, func(A) B, B] {
return function.Bind2nd(MonadAp[B, R, A], fa)
}
// Ask retrieves the current environment.
// This is the fundamental operation for accessing the Reader context.
//
// Type Parameters:
// - R: Reader environment type
//
// Returns:
// - A ReaderIO that returns the environment
//
// Example:
//
// type Config struct { Port int }
// rio := readerio.Ask[Config]()
// config := Config{Port: 8080}
// result := rio(config)() // Returns Config{Port: 8080}
func Ask[R any]() ReaderIO[R, R] {
return fromreader.Ask(FromReader[R, R])()
}
// Asks retrieves a value derived from the environment using a Reader function.
// This allows you to extract specific information from the environment.
//
// Type Parameters:
// - R: Reader environment type
// - A: Result type
//
// Parameters:
// - r: Function that extracts a value from the environment
//
// Returns:
// - A ReaderIO that applies the function to the environment
//
// Example:
//
// type Config struct { Port int }
// rio := readerio.Asks(func(c Config) io.IO[int] {
// return io.Of(c.Port)
// })
// result := rio(Config{Port: 8080})() // Returns 8080
func Asks[R, A any](r Reader[R, A]) ReaderIO[R, A] {
return fromreader.Asks(FromReader[R, A])(r)
}
// MonadChainIOK chains a ReaderIO with a function that returns an IO.
// This is useful for integrating IO operations into a ReaderIO pipeline.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Output value type
//
// Parameters:
// - ma: The ReaderIO computation
// - f: Function that takes a value and returns an IO
//
// Returns:
// - A ReaderIO that sequences the computation with the IO operation
//
// Example:
//
// rio := readerio.Of[Config](5)
// result := readerio.MonadChainIOK(rio, func(n int) io.IO[int] {
// return io.Of(n * 2)
// })
func MonadChainIOK[R, A, B any](ma ReaderIO[R, A], f func(A) IO[B]) ReaderIO[R, B] {
return fromio.MonadChainIOK(
MonadChain[R, A, B],
@@ -88,6 +421,28 @@ func MonadChainIOK[R, A, B any](ma ReaderIO[R, A], f func(A) IO[B]) ReaderIO[R,
)
}
// ChainIOK creates a function that chains a ReaderIO with an IO operation.
// This is the curried version suitable for use in pipelines.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Output value type
//
// Parameters:
// - f: Function that takes a value and returns an IO
//
// Returns:
// - An Operator that chains ReaderIO with IO
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](5),
// readerio.ChainIOK(func(n int) io.IO[int] {
// return io.Of(n * 2)
// }),
// )(config)() // Returns 10
func ChainIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, B] {
return fromio.ChainIOK(
Chain[R, A, B],
@@ -96,7 +451,29 @@ func ChainIOK[R, A, B any](f func(A) IO[B]) Operator[R, A, B] {
)
}
// Defer creates an IO by creating a brand new IO via a generator function, each time
// Defer creates a ReaderIO by calling a generator function each time it's executed.
// This allows for lazy evaluation and ensures a fresh computation on each invocation.
// Useful for operations that should not be cached or memoized.
//
// Type Parameters:
// - R: Reader environment type
// - A: Result type
//
// Parameters:
// - gen: Generator function that creates a new ReaderIO on each call
//
// Returns:
// - A ReaderIO that calls the generator function on each execution
//
// Example:
//
// counter := 0
// rio := readerio.Defer(func() readerio.ReaderIO[Config, int] {
// counter++
// return readerio.Of[Config](counter)
// })
// result1 := rio(config)() // Returns 1
// result2 := rio(config)() // Returns 2 (fresh computation)
func Defer[R, A any](gen func() ReaderIO[R, A]) ReaderIO[R, A] {
return func(r R) IO[A] {
return func() A {
@@ -105,9 +482,29 @@ func Defer[R, A any](gen func() ReaderIO[R, A]) ReaderIO[R, A] {
}
}
// Memoize computes the value of the provided [ReaderIO] monad lazily but exactly once
// The context used to compute the value is the context of the first call, so do not use this
// method if the value has a functional dependency on the content of the context
// Memoize computes the value of the provided [ReaderIO] monad lazily but exactly once.
// The first execution caches the result, and subsequent executions return the cached value.
//
// IMPORTANT: The context used to compute the value is the context of the first call.
// Do not use this method if the value has a functional dependency on the content of the context,
// as subsequent calls with different contexts will still return the memoized result from the first call.
//
// Type Parameters:
// - R: Reader environment type
// - A: Result type
//
// Parameters:
// - rdr: The ReaderIO to memoize
//
// Returns:
// - A ReaderIO that caches its result after the first execution
//
// Example:
//
// expensive := readerio.Of[Config](computeExpensiveValue())
// memoized := readerio.Memoize(expensive)
// result1 := memoized(config)() // Computes the value
// result2 := memoized(config)() // Returns cached value (no recomputation)
func Memoize[R, A any](rdr ReaderIO[R, A]) ReaderIO[R, A] {
// synchronization primitives
var once sync.Once
@@ -128,14 +525,72 @@ func Memoize[R, A any](rdr ReaderIO[R, A]) ReaderIO[R, A] {
}
}
// Flatten removes one level of nesting from a ReaderIO structure.
// Converts ReaderIO[R, ReaderIO[R, A]] to ReaderIO[R, A].
// This is also known as "join" in monad terminology.
//
// Type Parameters:
// - R: Reader environment type
// - A: Result type
//
// Parameters:
// - mma: A nested ReaderIO structure
//
// Returns:
// - A flattened ReaderIO with one less level of nesting
//
// Example:
//
// nested := readerio.Of[Config](readerio.Of[Config](42))
// flattened := readerio.Flatten(nested)
// result := flattened(config)() // Returns 42
func Flatten[R, A any](mma ReaderIO[R, ReaderIO[R, A]]) ReaderIO[R, A] {
return MonadChain(mma, function.Identity[ReaderIO[R, A]])
}
// MonadFlap applies a value to a function wrapped in a ReaderIO.
// This is the "flipped" version of MonadAp, where the value comes second.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Result type
//
// Parameters:
// - fab: ReaderIO containing a function from A to B
// - a: The value to apply to the function
//
// Returns:
// - A ReaderIO containing the result of applying the value to the function
//
// Example:
//
// fabIO := readerio.Of[Config](func(n int) int { return n * 2 })
// result := readerio.MonadFlap(fabIO, 5)(config)() // Returns 10
func MonadFlap[R, A, B any](fab ReaderIO[R, func(A) B], a A) ReaderIO[R, B] {
return functor.MonadFlap(MonadMap[R, func(A) B, B], fab, a)
}
// Flap creates a function that applies a value to a ReaderIO function.
// This is the curried version of MonadFlap, suitable for use in pipelines.
//
// Type Parameters:
// - R: Reader environment type
// - A: Input value type
// - B: Result type
//
// Parameters:
// - a: The value to apply
//
// Returns:
// - An Operator that applies the value to a ReaderIO function
//
// Example:
//
// result := F.Pipe1(
// readerio.Of[Config](func(n int) int { return n * 2 }),
// readerio.Flap[Config](5),
// )(config)() // Returns 10
func Flap[R, A, B any](a A) Operator[R, func(A) B, B] {
return functor.Flap(Map[R, func(A) B, B], a)
}

View File

@@ -21,11 +21,56 @@ import (
F "github.com/IBM/fp-go/v2/function"
"github.com/IBM/fp-go/v2/internal/utils"
G "github.com/IBM/fp-go/v2/io"
"github.com/stretchr/testify/assert"
)
func TestMap(t *testing.T) {
type ReaderTestConfig struct {
Value int
Name string
}
func TestFromIO(t *testing.T) {
ioAction := G.Of(42)
rio := FromIO[ReaderTestConfig](ioAction)
config := ReaderTestConfig{Value: 10, Name: "test"}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestFromReader(t *testing.T) {
reader := func(config ReaderTestConfig) int {
return config.Value * 2
}
rio := FromReader(reader)
config := ReaderTestConfig{Value: 5, Name: "test"}
result := rio(config)()
assert.Equal(t, 10, result)
}
func TestOf(t *testing.T) {
rio := Of[ReaderTestConfig](100)
config := ReaderTestConfig{Value: 1, Name: "test"}
result := rio(config)()
assert.Equal(t, 100, result)
}
func TestMonadMap(t *testing.T) {
rio := Of[ReaderTestConfig](5)
doubled := MonadMap(rio, func(n int) int { return n * 2 })
config := ReaderTestConfig{Value: 1, Name: "test"}
result := doubled(config)()
assert.Equal(t, 10, result)
}
func TestMap(t *testing.T) {
g := F.Pipe1(
Of[context.Context](1),
Map[context.Context](utils.Double),
@@ -34,6 +79,37 @@ func TestMap(t *testing.T) {
assert.Equal(t, 2, g(context.Background())())
}
func TestMonadChain(t *testing.T) {
rio1 := Of[ReaderTestConfig](5)
result := MonadChain(rio1, func(n int) ReaderIO[ReaderTestConfig, int] {
return Of[ReaderTestConfig](n * 3)
})
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestChain(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](5),
Chain(func(n int) ReaderIO[ReaderTestConfig, int] {
return Of[ReaderTestConfig](n * 3)
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestMonadAp(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n * 2 })
faIO := Of[ReaderTestConfig](5)
result := MonadAp(fabIO, faIO)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 10, result(config)())
}
func TestAp(t *testing.T) {
g := F.Pipe1(
Of[context.Context](utils.Double),
@@ -42,3 +118,188 @@ func TestAp(t *testing.T) {
assert.Equal(t, 2, g(context.Background())())
}
func TestMonadApSeq(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n + 10 })
faIO := Of[ReaderTestConfig](5)
result := MonadApSeq(fabIO, faIO)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestMonadApPar(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n + 10 })
faIO := Of[ReaderTestConfig](5)
result := MonadApPar(fabIO, faIO)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestAsk(t *testing.T) {
rio := Ask[ReaderTestConfig]()
config := ReaderTestConfig{Value: 42, Name: "test"}
result := rio(config)()
assert.Equal(t, config, result)
assert.Equal(t, 42, result.Value)
assert.Equal(t, "test", result.Name)
}
func TestAsks(t *testing.T) {
rio := Asks(func(c ReaderTestConfig) int {
return c.Value * 2
})
config := ReaderTestConfig{Value: 21, Name: "test"}
result := rio(config)()
assert.Equal(t, 42, result)
}
func TestMonadChainIOK(t *testing.T) {
rio := Of[ReaderTestConfig](5)
result := MonadChainIOK(rio, func(n int) G.IO[int] {
return G.Of(n * 4)
})
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 20, result(config)())
}
func TestChainIOK(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](5),
ChainIOK[ReaderTestConfig, int, int](func(n int) G.IO[int] {
return G.Of(n * 4)
}),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 20, result(config)())
}
func TestDefer(t *testing.T) {
counter := 0
rio := Defer(func() ReaderIO[ReaderTestConfig, int] {
counter++
return Of[ReaderTestConfig](counter)
})
config := ReaderTestConfig{Value: 1, Name: "test"}
result1 := rio(config)()
result2 := rio(config)()
assert.Equal(t, 1, result1)
assert.Equal(t, 2, result2)
}
func TestMemoize(t *testing.T) {
counter := 0
rio := Of[ReaderTestConfig](0)
memoized := Memoize(MonadMap(rio, func(int) int {
counter++
return counter
}))
config := ReaderTestConfig{Value: 1, Name: "test"}
result1 := memoized(config)()
result2 := memoized(config)()
assert.Equal(t, 1, result1)
assert.Equal(t, 1, result2) // Same value, memoized
}
func TestMemoizeWithDifferentContexts(t *testing.T) {
rio := Ask[ReaderTestConfig]()
memoized := Memoize(MonadMap(rio, func(c ReaderTestConfig) int {
return c.Value
}))
config1 := ReaderTestConfig{Value: 10, Name: "first"}
config2 := ReaderTestConfig{Value: 20, Name: "second"}
result1 := memoized(config1)()
result2 := memoized(config2)() // Should still return 10 (memoized from first call)
assert.Equal(t, 10, result1)
assert.Equal(t, 10, result2) // Memoized value from first context
}
func TestFlatten(t *testing.T) {
nested := Of[ReaderTestConfig](Of[ReaderTestConfig](42))
flattened := Flatten(nested)
config := ReaderTestConfig{Value: 1, Name: "test"}
result := flattened(config)()
assert.Equal(t, 42, result)
}
func TestMonadFlap(t *testing.T) {
fabIO := Of[ReaderTestConfig](func(n int) int { return n * 3 })
result := MonadFlap(fabIO, 7)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 21, result(config)())
}
func TestFlap(t *testing.T) {
result := F.Pipe1(
Of[ReaderTestConfig](func(n int) int { return n * 3 }),
Flap[ReaderTestConfig, int, int](7),
)
config := ReaderTestConfig{Value: 1, Name: "test"}
assert.Equal(t, 21, result(config)())
}
func TestComplexPipeline(t *testing.T) {
// Test a complex pipeline combining multiple operations
result := F.Pipe3(
Ask[ReaderTestConfig](),
Map[ReaderTestConfig](func(c ReaderTestConfig) int { return c.Value }),
Chain(func(n int) ReaderIO[ReaderTestConfig, int] {
return Of[ReaderTestConfig](n * 2)
}),
Map[ReaderTestConfig](func(n int) int { return n + 10 }),
)
config := ReaderTestConfig{Value: 5, Name: "test"}
assert.Equal(t, 20, result(config)()) // (5 * 2) + 10 = 20
}
func TestFromIOWithChain(t *testing.T) {
ioAction := G.Of(10)
result := F.Pipe1(
FromIO[ReaderTestConfig](ioAction),
Chain(func(n int) ReaderIO[ReaderTestConfig, int] {
return MonadMap(Ask[ReaderTestConfig](), func(c ReaderTestConfig) int {
return n + c.Value
})
}),
)
config := ReaderTestConfig{Value: 5, Name: "test"}
assert.Equal(t, 15, result(config)())
}
func TestFromReaderWithMap(t *testing.T) {
reader := func(c ReaderTestConfig) string {
return c.Name
}
result := F.Pipe1(
FromReader(reader),
Map[ReaderTestConfig](func(s string) string {
return s + " modified"
}),
)
config := ReaderTestConfig{Value: 1, Name: "original"}
assert.Equal(t, "original modified", result(config)())
}
// Made with Bob

View File

@@ -18,6 +18,7 @@ package readerioresult
import (
"context"
"fmt"
"log"
"testing"
F "github.com/IBM/fp-go/v2/function"
@@ -57,3 +58,22 @@ func TestChainReaderK(t *testing.T) {
assert.Equal(t, result.Of("1"), g(context.Background())())
}
func TestTapReaderIOK(t *testing.T) {
rdr := Of[int]("TestTapReaderIOK")
x := F.Pipe1(
rdr,
TapReaderIOK(func(a string) ReaderIO[int, any] {
return func(ctx int) IO[any] {
return func() any {
log.Printf("Context: %d, Value: %s", ctx, a)
return nil
}
}
}),
)
x(10)()
}