mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-07 23:03:15 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d739c9b277 | ||
|
|
f0054431a5 | ||
|
|
1a89ec3df7 |
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
48
v2/pair/sequence.go
Normal 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
245
v2/readerio/logging.go
Normal 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
367
v2/readerio/logging_test.go
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user