1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-09 23:11:40 +02:00

Compare commits

...

2 Commits

Author SHA1 Message Date
Dr. Carsten Leue
dcfb023891 fix: improve assertions
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 17:28:48 +01:00
Dr. Carsten Leue
51cf241a26 fix: add ReaderK
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 12:29:55 +01:00
11 changed files with 1457 additions and 73 deletions

268
v2/assert/assert.go Normal file
View File

@@ -0,0 +1,268 @@
// 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 assert provides functional assertion helpers for testing.
//
// This package wraps testify/assert functions in a Reader monad pattern,
// allowing for composable and functional test assertions. Each assertion
// returns a Reader that takes a *testing.T and performs the assertion.
//
// The package supports:
// - Equality and inequality assertions
// - Collection assertions (arrays, maps, strings)
// - Error handling assertions
// - Result type assertions
// - Custom predicate assertions
// - Composable test suites
//
// Example:
//
// func TestExample(t *testing.T) {
// value := 42
// assert.Equal(42)(value)(t) // Curried style
//
// // Composing multiple assertions
// arr := []int{1, 2, 3}
// assertions := assert.AllOf([]assert.Reader{
// assert.ArrayNotEmpty(arr),
// assert.ArrayLength[int](3)(arr),
// assert.ArrayContains(2)(arr),
// })
// assertions(t)
// }
package assert
import (
"fmt"
"testing"
"github.com/IBM/fp-go/v2/boolean"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
var (
// Eq is the equal predicate checking if objects are equal
Eq = eq.FromEquals(assert.ObjectsAreEqual)
)
// wrap1 is an internal helper function that wraps testify assertion functions
// into the Reader monad pattern with curried parameters.
//
// It takes a testify assertion function and converts it into a curried function
// that first takes an expected value, then an actual value, and finally returns
// a Reader that performs the assertion when given a *testing.T.
//
// Parameters:
// - wrapped: The testify assertion function to wrap
// - expected: The expected value for comparison
// - msgAndArgs: Optional message and arguments for assertion failure
//
// Returns:
// - A Kleisli function that takes the actual value and returns a Reader
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, expected T, msgAndArgs ...any) Kleisli[T] {
return func(actual T) Reader {
return func(t *testing.T) bool {
return wrapped(t, expected, actual, msgAndArgs...)
}
}
}
// NotEqual tests if the expected and the actual values are not equal
func NotEqual[T any](expected T) Kleisli[T] {
return wrap1(assert.NotEqual, expected)
}
// Equal tests if the expected and the actual values are equal
func Equal[T any](expected T) Kleisli[T] {
return wrap1(assert.Equal, expected)
}
// ArrayNotEmpty checks if an array is not empty
func ArrayNotEmpty[T any](arr []T) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, arr)
}
}
// RecordNotEmpty checks if an map is not empty
func RecordNotEmpty[K comparable, T any](mp map[K]T) Reader {
return func(t *testing.T) bool {
return assert.NotEmpty(t, mp)
}
}
// ArrayLength tests if an array has the expected length
func ArrayLength[T any](expected int) Kleisli[[]T] {
return func(actual []T) Reader {
return func(t *testing.T) bool {
return assert.Len(t, actual, expected)
}
}
}
// RecordLength tests if a map has the expected length
func RecordLength[K comparable, T any](expected int) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Len(t, actual, expected)
}
}
}
// StringLength tests if a string has the expected length
func StringLength[K comparable, T any](expected int) Kleisli[string] {
return func(actual string) Reader {
return func(t *testing.T) bool {
return assert.Len(t, actual, expected)
}
}
}
// NoError validates that there is no error
func NoError(err error) Reader {
return func(t *testing.T) bool {
return assert.NoError(t, err)
}
}
// Error validates that there is an error
func Error(err error) Reader {
return func(t *testing.T) bool {
return assert.Error(t, err)
}
}
// Success checks if a [Result] represents success
func Success[T any](res Result[T]) Reader {
return NoError(result.ToError(res))
}
// Failure checks if a [Result] represents failure
func Failure[T any](res Result[T]) Reader {
return Error(result.ToError(res))
}
// ArrayContains tests if a value is contained in an array
func ArrayContains[T any](expected T) Kleisli[[]T] {
return func(actual []T) Reader {
return func(t *testing.T) bool {
return assert.Contains(t, actual, expected)
}
}
}
// ContainsKey tests if a key is contained in a map
func ContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
return assert.Contains(t, actual, expected)
}
}
}
// NotContainsKey tests if a key is not contained in a map
func NotContainsKey[T any, K comparable](expected K) Kleisli[map[K]T] {
return func(actual map[K]T) Reader {
return func(t *testing.T) bool {
return assert.NotContains(t, actual, expected)
}
}
}
// That asserts that a particular predicate matches
func That[T any](pred Predicate[T]) Kleisli[T] {
return func(a T) Reader {
return func(t *testing.T) bool {
if pred(a) {
return true
}
return assert.Fail(t, fmt.Sprintf("Preficate %v does not match value %v", pred, a))
}
}
}
// AllOf combines multiple assertion Readers into a single Reader that passes
// only if all assertions pass.
//
// This function uses boolean AND logic (MonoidAll) to combine the results of
// all assertions. If any assertion fails, the combined assertion fails.
//
// This is useful for grouping related assertions together and ensuring all
// conditions are met.
//
// Parameters:
// - readers: Array of assertion Readers to combine
//
// Returns:
// - A single Reader that performs all assertions and returns true only if all pass
//
// Example:
//
// func TestUser(t *testing.T) {
// user := User{Name: "Alice", Age: 30, Active: true}
// assertions := assert.AllOf([]assert.Reader{
// assert.Equal("Alice")(user.Name),
// assert.Equal(30)(user.Age),
// assert.Equal(true)(user.Active),
// })
// assertions(t)
// }
//
//go:inline
func AllOf(readers []Reader) Reader {
return reader.MonadReduceArrayM(readers, boolean.MonoidAll)
}
// RunAll executes a map of named test cases, running each as a subtest.
//
// This function creates a Reader that runs multiple named test cases using
// Go's t.Run for proper test isolation and reporting. Each test case is
// executed as a separate subtest with its own name.
//
// The function returns true only if all subtests pass. This allows for
// better test organization and clearer test output.
//
// Parameters:
// - testcases: Map of test names to assertion Readers
//
// Returns:
// - A Reader that executes all named test cases and returns true if all pass
//
// Example:
//
// func TestMathOperations(t *testing.T) {
// testcases := map[string]assert.Reader{
// "addition": assert.Equal(4)(2 + 2),
// "multiplication": assert.Equal(6)(2 * 3),
// "subtraction": assert.Equal(1)(3 - 2),
// }
// assert.RunAll(testcases)(t)
// }
//
//go:inline
func RunAll(testcases map[string]Reader) Reader {
return func(t *testing.T) bool {
current := true
for k, r := range testcases {
current = current && t.Run(k, func(t1 *testing.T) {
r(t1)
})
}
return current
}
}

View File

@@ -16,94 +16,470 @@
package assert
import (
"fmt"
"errors"
"testing"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
var (
errTest = fmt.Errorf("test failure")
// Eq is the equal predicate checking if objects are equal
Eq = eq.FromEquals(assert.ObjectsAreEqual)
)
func wrap1[T any](wrapped func(t assert.TestingT, expected, actual any, msgAndArgs ...any) bool, t *testing.T, expected T) result.Kleisli[T, T] {
return func(actual T) Result[T] {
ok := wrapped(t, expected, actual)
if ok {
return result.Of(actual)
func TestEqual(t *testing.T) {
t.Run("should pass when values are equal", func(t *testing.T) {
result := Equal(42)(42)(t)
if !result {
t.Error("Expected Equal to pass for equal values")
}
return result.Left[T](errTest)
}
}
})
// NotEqual tests if the expected and the actual values are not equal
func NotEqual[T any](t *testing.T, expected T) result.Kleisli[T, T] {
return wrap1(assert.NotEqual, t, expected)
}
// Equal tests if the expected and the actual values are equal
func Equal[T any](t *testing.T, expected T) result.Kleisli[T, T] {
return wrap1(assert.Equal, t, expected)
}
// Length tests if an array has the expected length
func Length[T any](t *testing.T, expected int) result.Kleisli[[]T, []T] {
return func(actual []T) Result[[]T] {
ok := assert.Len(t, actual, expected)
if ok {
return result.Of(actual)
t.Run("should fail when values are not equal", func(t *testing.T) {
mockT := &testing.T{}
result := Equal(42)(43)(mockT)
if result {
t.Error("Expected Equal to fail for different values")
}
return result.Left[[]T](errTest)
}
})
t.Run("should work with strings", func(t *testing.T) {
result := Equal("hello")("hello")(t)
if !result {
t.Error("Expected Equal to pass for equal strings")
}
})
}
// NoError validates that there is no error
func NoError[T any](t *testing.T) result.Operator[T, T] {
return func(actual Result[T]) Result[T] {
return result.MonadFold(actual, func(e error) Result[T] {
assert.NoError(t, e)
return result.Left[T](e)
}, func(value T) Result[T] {
assert.NoError(t, nil)
return result.Of(value)
func TestNotEqual(t *testing.T) {
t.Run("should pass when values are not equal", func(t *testing.T) {
result := NotEqual(42)(43)(t)
if !result {
t.Error("Expected NotEqual to pass for different values")
}
})
t.Run("should fail when values are equal", func(t *testing.T) {
mockT := &testing.T{}
result := NotEqual(42)(42)(mockT)
if result {
t.Error("Expected NotEqual to fail for equal values")
}
})
}
func TestArrayNotEmpty(t *testing.T) {
t.Run("should pass for non-empty array", func(t *testing.T) {
arr := []int{1, 2, 3}
result := ArrayNotEmpty(arr)(t)
if !result {
t.Error("Expected ArrayNotEmpty to pass for non-empty array")
}
})
t.Run("should fail for empty array", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{}
result := ArrayNotEmpty(arr)(mockT)
if result {
t.Error("Expected ArrayNotEmpty to fail for empty array")
}
})
}
func TestRecordNotEmpty(t *testing.T) {
t.Run("should pass for non-empty map", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
result := RecordNotEmpty(mp)(t)
if !result {
t.Error("Expected RecordNotEmpty to pass for non-empty map")
}
})
t.Run("should fail for empty map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{}
result := RecordNotEmpty(mp)(mockT)
if result {
t.Error("Expected RecordNotEmpty to fail for empty map")
}
})
}
func TestArrayLength(t *testing.T) {
t.Run("should pass when length matches", func(t *testing.T) {
arr := []int{1, 2, 3}
result := ArrayLength[int](3)(arr)(t)
if !result {
t.Error("Expected ArrayLength to pass when length matches")
}
})
t.Run("should fail when length doesn't match", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{1, 2, 3}
result := ArrayLength[int](5)(arr)(mockT)
if result {
t.Error("Expected ArrayLength to fail when length doesn't match")
}
})
t.Run("should work with empty array", func(t *testing.T) {
arr := []string{}
result := ArrayLength[string](0)(arr)(t)
if !result {
t.Error("Expected ArrayLength to pass for empty array with expected length 0")
}
})
}
func TestRecordLength(t *testing.T) {
t.Run("should pass when map length matches", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
result := RecordLength[string, int](2)(mp)(t)
if !result {
t.Error("Expected RecordLength to pass when length matches")
}
})
t.Run("should fail when map length doesn't match", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1}
result := RecordLength[string, int](3)(mp)(mockT)
if result {
t.Error("Expected RecordLength to fail when length doesn't match")
}
})
}
func TestStringLength(t *testing.T) {
t.Run("should pass when string length matches", func(t *testing.T) {
str := "hello"
result := StringLength[string, int](5)(str)(t)
if !result {
t.Error("Expected StringLength to pass when length matches")
}
})
t.Run("should fail when string length doesn't match", func(t *testing.T) {
mockT := &testing.T{}
str := "hello"
result := StringLength[string, int](10)(str)(mockT)
if result {
t.Error("Expected StringLength to fail when length doesn't match")
}
})
t.Run("should work with empty string", func(t *testing.T) {
str := ""
result := StringLength[string, int](0)(str)(t)
if !result {
t.Error("Expected StringLength to pass for empty string with expected length 0")
}
})
}
func TestNoError(t *testing.T) {
t.Run("should pass when error is nil", func(t *testing.T) {
result := NoError(nil)(t)
if !result {
t.Error("Expected NoError to pass when error is nil")
}
})
t.Run("should fail when error is not nil", func(t *testing.T) {
mockT := &testing.T{}
err := errors.New("test error")
result := NoError(err)(mockT)
if result {
t.Error("Expected NoError to fail when error is not nil")
}
})
}
func TestError(t *testing.T) {
t.Run("should pass when error is not nil", func(t *testing.T) {
err := errors.New("test error")
result := Error(err)(t)
if !result {
t.Error("Expected Error to pass when error is not nil")
}
})
t.Run("should fail when error is nil", func(t *testing.T) {
mockT := &testing.T{}
result := Error(nil)(mockT)
if result {
t.Error("Expected Error to fail when error is nil")
}
})
}
func TestSuccess(t *testing.T) {
t.Run("should pass for successful result", func(t *testing.T) {
res := result.Of[int](42)
result := Success(res)(t)
if !result {
t.Error("Expected Success to pass for successful result")
}
})
t.Run("should fail for error result", func(t *testing.T) {
mockT := &testing.T{}
res := result.Left[int](errors.New("test error"))
result := Success(res)(mockT)
if result {
t.Error("Expected Success to fail for error result")
}
})
}
func TestFailure(t *testing.T) {
t.Run("should pass for error result", func(t *testing.T) {
res := result.Left[int](errors.New("test error"))
result := Failure(res)(t)
if !result {
t.Error("Expected Failure to pass for error result")
}
})
t.Run("should fail for successful result", func(t *testing.T) {
mockT := &testing.T{}
res := result.Of[int](42)
result := Failure(res)(mockT)
if result {
t.Error("Expected Failure to fail for successful result")
}
})
}
func TestArrayContains(t *testing.T) {
t.Run("should pass when element is in array", func(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
result := ArrayContains(3)(arr)(t)
if !result {
t.Error("Expected ArrayContains to pass when element is in array")
}
})
t.Run("should fail when element is not in array", func(t *testing.T) {
mockT := &testing.T{}
arr := []int{1, 2, 3}
result := ArrayContains(10)(arr)(mockT)
if result {
t.Error("Expected ArrayContains to fail when element is not in array")
}
})
t.Run("should work with strings", func(t *testing.T) {
arr := []string{"apple", "banana", "cherry"}
result := ArrayContains("banana")(arr)(t)
if !result {
t.Error("Expected ArrayContains to pass for string element")
}
})
}
func TestContainsKey(t *testing.T) {
t.Run("should pass when key exists in map", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2, "c": 3}
result := ContainsKey[int]("b")(mp)(t)
if !result {
t.Error("Expected ContainsKey to pass when key exists")
}
})
t.Run("should fail when key doesn't exist in map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1, "b": 2}
result := ContainsKey[int]("z")(mp)(mockT)
if result {
t.Error("Expected ContainsKey to fail when key doesn't exist")
}
})
}
func TestNotContainsKey(t *testing.T) {
t.Run("should pass when key doesn't exist in map", func(t *testing.T) {
mp := map[string]int{"a": 1, "b": 2}
result := NotContainsKey[int]("z")(mp)(t)
if !result {
t.Error("Expected NotContainsKey to pass when key doesn't exist")
}
})
t.Run("should fail when key exists in map", func(t *testing.T) {
mockT := &testing.T{}
mp := map[string]int{"a": 1, "b": 2}
result := NotContainsKey[int]("a")(mp)(mockT)
if result {
t.Error("Expected NotContainsKey to fail when key exists")
}
})
}
func TestThat(t *testing.T) {
t.Run("should pass when predicate is true", func(t *testing.T) {
isEven := func(n int) bool { return n%2 == 0 }
result := That(isEven)(42)(t)
if !result {
t.Error("Expected That to pass when predicate is true")
}
})
t.Run("should fail when predicate is false", func(t *testing.T) {
mockT := &testing.T{}
isEven := func(n int) bool { return n%2 == 0 }
result := That(isEven)(43)(mockT)
if result {
t.Error("Expected That to fail when predicate is false")
}
})
t.Run("should work with string predicates", func(t *testing.T) {
startsWithH := func(s string) bool { return len(s) > 0 && s[0] == 'h' }
result := That(startsWithH)("hello")(t)
if !result {
t.Error("Expected That to pass for string predicate")
}
})
}
func TestAllOf(t *testing.T) {
t.Run("should pass when all assertions pass", func(t *testing.T) {
assertions := AllOf([]Reader{
Equal(42)(42),
Equal("hello")("hello"),
ArrayNotEmpty([]int{1, 2, 3}),
})
}
result := assertions(t)
if !result {
t.Error("Expected AllOf to pass when all assertions pass")
}
})
t.Run("should fail when any assertion fails", func(t *testing.T) {
mockT := &testing.T{}
assertions := AllOf([]Reader{
Equal(42)(42),
Equal("hello")("goodbye"),
ArrayNotEmpty([]int{1, 2, 3}),
})
result := assertions(mockT)
if result {
t.Error("Expected AllOf to fail when any assertion fails")
}
})
t.Run("should work with empty array", func(t *testing.T) {
assertions := AllOf([]Reader{})
result := assertions(t)
if !result {
t.Error("Expected AllOf to pass for empty array")
}
})
t.Run("should combine multiple array assertions", func(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
assertions := AllOf([]Reader{
ArrayNotEmpty(arr),
ArrayLength[int](5)(arr),
ArrayContains(3)(arr),
})
result := assertions(t)
if !result {
t.Error("Expected AllOf to pass for multiple array assertions")
}
})
}
// ArrayContains tests if a value is contained in an array
func ArrayContains[T any](t *testing.T, expected T) result.Kleisli[[]T, []T] {
return func(actual []T) Result[[]T] {
ok := assert.Contains(t, actual, expected)
if ok {
return result.Of(actual)
func TestRunAll(t *testing.T) {
t.Run("should run all named test cases", func(t *testing.T) {
testcases := map[string]Reader{
"equality": Equal(42)(42),
"string_check": Equal("test")("test"),
"array_check": ArrayNotEmpty([]int{1, 2, 3}),
}
return result.Left[[]T](errTest)
}
result := RunAll(testcases)(t)
if !result {
t.Error("Expected RunAll to pass when all test cases pass")
}
})
// Note: Testing failure behavior of RunAll is tricky because subtests
// will actually fail in the test framework. The function works correctly
// as demonstrated by the passing test above.
t.Run("should work with empty test cases", func(t *testing.T) {
testcases := map[string]Reader{}
result := RunAll(testcases)(t)
if !result {
t.Error("Expected RunAll to pass for empty test cases")
}
})
}
// ContainsKey tests if a key is contained in a map
func ContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
return func(actual map[K]T) Result[map[K]T] {
ok := assert.Contains(t, actual, expected)
if ok {
return result.Of(actual)
func TestEq(t *testing.T) {
t.Run("should return true for equal values", func(t *testing.T) {
if !Eq.Equals(42, 42) {
t.Error("Expected Eq to return true for equal integers")
}
return result.Left[map[K]T](errTest)
}
})
t.Run("should return false for different values", func(t *testing.T) {
if Eq.Equals(42, 43) {
t.Error("Expected Eq to return false for different integers")
}
})
t.Run("should work with strings", func(t *testing.T) {
if !Eq.Equals("hello", "hello") {
t.Error("Expected Eq to return true for equal strings")
}
if Eq.Equals("hello", "world") {
t.Error("Expected Eq to return false for different strings")
}
})
t.Run("should work with slices", func(t *testing.T) {
arr1 := []int{1, 2, 3}
arr2 := []int{1, 2, 3}
if !Eq.Equals(arr1, arr2) {
t.Error("Expected Eq to return true for equal slices")
}
})
}
// NotContainsKey tests if a key is not contained in a map
func NotContainsKey[T any, K comparable](t *testing.T, expected K) result.Kleisli[map[K]T, map[K]T] {
return func(actual map[K]T) Result[map[K]T] {
ok := assert.NotContains(t, actual, expected)
if ok {
return result.Of(actual)
func TestIntegration(t *testing.T) {
t.Run("complex assertion composition", func(t *testing.T) {
type User struct {
Name string
Age int
Email string
}
return result.Left[map[K]T](errTest)
}
user := User{Name: "Alice", Age: 30, Email: "alice@example.com"}
assertions := AllOf([]Reader{
Equal("Alice")(user.Name),
Equal(30)(user.Age),
That(func(s string) bool { return len(s) > 0 })(user.Email),
})
result := assertions(t)
if !result {
t.Error("Expected complex assertion composition to pass")
}
})
t.Run("test suite with RunAll", func(t *testing.T) {
data := []int{1, 2, 3, 4, 5}
suite := RunAll(map[string]Reader{
"not_empty": ArrayNotEmpty(data),
"correct_size": ArrayLength[int](5)(data),
"contains_one": ArrayContains(1)(data),
"contains_five": ArrayContains(5)(data),
})
result := suite(t)
if !result {
t.Error("Expected test suite to pass")
}
})
}

View File

@@ -1,7 +1,16 @@
package assert
import "github.com/IBM/fp-go/v2/result"
import (
"testing"
"github.com/IBM/fp-go/v2/predicate"
"github.com/IBM/fp-go/v2/reader"
"github.com/IBM/fp-go/v2/result"
)
type (
Result[T any] = result.Result[T]
Result[T any] = result.Result[T]
Reader = reader.Reader[*testing.T, bool]
Kleisli[T any] = reader.Reader[T, Reader]
Predicate[T any] = predicate.Predicate[T]
)

View File

@@ -68,6 +68,15 @@ func MonadTraverse[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HK
return traverseWithIndex(fof, fmap, fap, r, F.Ignore1of2[K](f))
}
func MonadTraverseWithIndex[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HKTRB any](
fof func(MB) HKTRB,
fmap func(func(MB) func(B) MB) func(HKTRB) HKTAB,
fap func(HKTB) func(HKTAB) HKTRB,
r MA, f func(K, A) HKTB) HKTRB {
return traverseWithIndex(fof, fmap, fap, r, f)
}
func TraverseWithIndex[MA ~map[K]A, MB ~map[K]B, K comparable, A, B, HKTB, HKTAB, HKTRB any](
fof func(MB) HKTRB,
fmap func(func(MB) func(B) MB) func(HKTRB) HKTAB,

View File

@@ -17,6 +17,8 @@ package reader
import (
"github.com/IBM/fp-go/v2/function"
RA "github.com/IBM/fp-go/v2/internal/array"
"github.com/IBM/fp-go/v2/monoid"
G "github.com/IBM/fp-go/v2/reader/generic"
)
@@ -100,3 +102,273 @@ func TraverseArrayWithIndex[R, A, B any](f func(int, A) Reader[R, B]) func([]A)
func SequenceArray[R, A any](ma []Reader[R, A]) Reader[R, []A] {
return MonadTraverseArray(ma, function.Identity[Reader[R, A]])
}
// MonadReduceArray reduces an array of Readers to a single Reader by applying a reduction function.
// This is the monadic version that takes the array of Readers as the first parameter.
//
// Each Reader is evaluated with the same environment R, and the results are accumulated using
// the provided reduce function starting from the initial value.
//
// Parameters:
// - as: Array of Readers to reduce
// - reduce: Binary function that combines accumulated value with each Reader's result
// - initial: Starting value for the reduction
//
// Example:
//
// type Config struct { Base int }
// readers := []reader.Reader[Config, int]{
// reader.Asks(func(c Config) int { return c.Base + 1 }),
// reader.Asks(func(c Config) int { return c.Base + 2 }),
// reader.Asks(func(c Config) int { return c.Base + 3 }),
// }
// sum := func(acc, val int) int { return acc + val }
// r := reader.MonadReduceArray(readers, sum, 0)
// result := r(Config{Base: 10}) // 36 (11 + 12 + 13)
//
//go:inline
func MonadReduceArray[R, A, B any](as []Reader[R, A], reduce func(B, A) B, initial B) Reader[R, B] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
function.Identity[Reader[R, A]],
reduce,
initial,
)
}
// ReduceArray returns a curried function that reduces an array of Readers to a single Reader.
// This is the curried version where the reduction function and initial value are provided first,
// returning a function that takes the array of Readers.
//
// Parameters:
// - reduce: Binary function that combines accumulated value with each Reader's result
// - initial: Starting value for the reduction
//
// Returns:
// - A function that takes an array of Readers and returns a Reader of the reduced result
//
// Example:
//
// type Config struct { Multiplier int }
// product := func(acc, val int) int { return acc * val }
// reducer := reader.ReduceArray[Config](product, 1)
// readers := []reader.Reader[Config, int]{
// reader.Asks(func(c Config) int { return c.Multiplier * 2 }),
// reader.Asks(func(c Config) int { return c.Multiplier * 3 }),
// }
// r := reducer(readers)
// result := r(Config{Multiplier: 5}) // 150 (10 * 15)
//
//go:inline
func ReduceArray[R, A, B any](reduce func(B, A) B, initial B) Kleisli[R, []Reader[R, A], B] {
return RA.TraverseReduce[[]Reader[R, A]](
Of,
Map,
Ap,
function.Identity[Reader[R, A]],
reduce,
initial,
)
}
// MonadReduceArrayM reduces an array of Readers using a Monoid to combine the results.
// This is the monadic version that takes the array of Readers as the first parameter.
//
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
// for the reduction, making it convenient when working with monoidal types.
//
// Parameters:
// - as: Array of Readers to reduce
// - m: Monoid that defines how to combine the Reader results
//
// Example:
//
// type Config struct { Factor int }
// readers := []reader.Reader[Config, int]{
// reader.Asks(func(c Config) int { return c.Factor }),
// reader.Asks(func(c Config) int { return c.Factor * 2 }),
// reader.Asks(func(c Config) int { return c.Factor * 3 }),
// }
// intAddMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
// r := reader.MonadReduceArrayM(readers, intAddMonoid)
// result := r(Config{Factor: 5}) // 30 (5 + 10 + 15)
//
//go:inline
func MonadReduceArrayM[R, A any](as []Reader[R, A], m monoid.Monoid[A]) Reader[R, A] {
return MonadReduceArray(as, m.Concat, m.Empty())
}
// ReduceArrayM returns a curried function that reduces an array of Readers using a Monoid.
// This is the curried version where the Monoid is provided first, returning a function
// that takes the array of Readers.
//
// The Monoid provides both the binary operation (Concat) and the identity element (Empty)
// for the reduction.
//
// Parameters:
// - m: Monoid that defines how to combine the Reader results
//
// Returns:
// - A function that takes an array of Readers and returns a Reader of the reduced result
//
// Example:
//
// type Config struct { Scale int }
// intMultMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
// reducer := reader.ReduceArrayM[Config](intMultMonoid)
// readers := []reader.Reader[Config, int]{
// reader.Asks(func(c Config) int { return c.Scale }),
// reader.Asks(func(c Config) int { return c.Scale * 2 }),
// }
// r := reducer(readers)
// result := r(Config{Scale: 3}) // 18 (3 * 6)
//
//go:inline
func ReduceArrayM[R, A any](m monoid.Monoid[A]) Kleisli[R, []Reader[R, A], A] {
return ReduceArray[R](m.Concat, m.Empty())
}
// MonadTraverseReduceArray transforms and reduces an array in one operation.
// This is the monadic version that takes the array as the first parameter.
//
// First, each element is transformed using the provided Kleisli function into a Reader.
// Then, the Reader results are reduced using the provided reduction function.
//
// This is more efficient than calling TraverseArray followed by a separate reduce operation,
// as it combines both operations into a single traversal.
//
// Parameters:
// - as: Array of elements to transform and reduce
// - trfrm: Function that transforms each element into a Reader
// - reduce: Binary function that combines accumulated value with each transformed result
// - initial: Starting value for the reduction
//
// Example:
//
// type Config struct { Multiplier int }
// numbers := []int{1, 2, 3, 4}
// multiply := func(n int) reader.Reader[Config, int] {
// return reader.Asks(func(c Config) int { return n * c.Multiplier })
// }
// sum := func(acc, val int) int { return acc + val }
// r := reader.MonadTraverseReduceArray(numbers, multiply, sum, 0)
// result := r(Config{Multiplier: 10}) // 100 (10 + 20 + 30 + 40)
//
//go:inline
func MonadTraverseReduceArray[R, A, B, C any](as []A, trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) Reader[R, C] {
return RA.MonadTraverseReduce(
Of,
Map,
Ap,
as,
trfrm,
reduce,
initial,
)
}
// TraverseReduceArray returns a curried function that transforms and reduces an array.
// This is the curried version where the transformation function, reduce function, and initial value
// are provided first, returning a function that takes the array.
//
// First, each element is transformed using the provided Kleisli function into a Reader.
// Then, the Reader results are reduced using the provided reduction function.
//
// Parameters:
// - trfrm: Function that transforms each element into a Reader
// - reduce: Binary function that combines accumulated value with each transformed result
// - initial: Starting value for the reduction
//
// Returns:
// - A function that takes an array and returns a Reader of the reduced result
//
// Example:
//
// type Config struct { Base int }
// addBase := func(n int) reader.Reader[Config, int] {
// return reader.Asks(func(c Config) int { return n + c.Base })
// }
// product := func(acc, val int) int { return acc * val }
// transformer := reader.TraverseReduceArray(addBase, product, 1)
// r := transformer([]int{2, 3, 4})
// result := r(Config{Base: 10}) // 2184 (12 * 13 * 14)
//
//go:inline
func TraverseReduceArray[R, A, B, C any](trfrm Kleisli[R, A, B], reduce func(C, B) C, initial C) Kleisli[R, []A, C] {
return RA.TraverseReduce[[]A](
Of,
Map,
Ap,
trfrm,
reduce,
initial,
)
}
// MonadTraverseReduceArrayM transforms and reduces an array using a Monoid.
// This is the monadic version that takes the array as the first parameter.
//
// First, each element is transformed using the provided Kleisli function into a Reader.
// Then, the Reader results are reduced using the Monoid's binary operation and identity element.
//
// This combines transformation and monoidal reduction in a single efficient operation.
//
// Parameters:
// - as: Array of elements to transform and reduce
// - trfrm: Function that transforms each element into a Reader
// - m: Monoid that defines how to combine the transformed results
//
// Example:
//
// type Config struct { Offset int }
// numbers := []int{1, 2, 3}
// addOffset := func(n int) reader.Reader[Config, int] {
// return reader.Asks(func(c Config) int { return n + c.Offset })
// }
// intSumMonoid := monoid.MakeMonoid(func(a, b int) int { return a + b }, 0)
// r := reader.MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
// result := r(Config{Offset: 100}) // 306 (101 + 102 + 103)
//
//go:inline
func MonadTraverseReduceArrayM[R, A, B any](as []A, trfrm Kleisli[R, A, B], m monoid.Monoid[B]) Reader[R, B] {
return MonadTraverseReduceArray(as, trfrm, m.Concat, m.Empty())
}
// TraverseReduceArrayM returns a curried function that transforms and reduces an array using a Monoid.
// This is the curried version where the transformation function and Monoid are provided first,
// returning a function that takes the array.
//
// First, each element is transformed using the provided Kleisli function into a Reader.
// Then, the Reader results are reduced using the Monoid's binary operation and identity element.
//
// Parameters:
// - trfrm: Function that transforms each element into a Reader
// - m: Monoid that defines how to combine the transformed results
//
// Returns:
// - A function that takes an array and returns a Reader of the reduced result
//
// Example:
//
// type Config struct { Factor int }
// scale := func(n int) reader.Reader[Config, int] {
// return reader.Asks(func(c Config) int { return n * c.Factor })
// }
// intProdMonoid := monoid.MakeMonoid(func(a, b int) int { return a * b }, 1)
// transformer := reader.TraverseReduceArrayM(scale, intProdMonoid)
// r := transformer([]int{2, 3, 4})
// result := r(Config{Factor: 5}) // 3000 (10 * 15 * 20)
//
//go:inline
func TraverseReduceArrayM[R, A, B any](trfrm Kleisli[R, A, B], m monoid.Monoid[B]) Kleisli[R, []A, B] {
return TraverseReduceArray(trfrm, m.Concat, m.Empty())
}

View File

@@ -21,6 +21,7 @@ import (
A "github.com/IBM/fp-go/v2/array"
F "github.com/IBM/fp-go/v2/function"
M "github.com/IBM/fp-go/v2/monoid"
"github.com/stretchr/testify/assert"
)
@@ -93,3 +94,142 @@ func TestMonadTraverseArray(t *testing.T) {
assert.Equal(t, 3, len(result))
assert.Contains(t, result[0], "num")
}
func TestMonadReduceArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
readers := []Reader[Config, int]{
Asks(func(c Config) int { return c.Base + 1 }),
Asks(func(c Config) int { return c.Base + 2 }),
Asks(func(c Config) int { return c.Base + 3 }),
}
sum := func(acc, val int) int { return acc + val }
r := MonadReduceArray(readers, sum, 0)
result := r(config)
assert.Equal(t, 36, result) // 11 + 12 + 13
}
func TestReduceArray(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 5}
product := func(acc, val int) int { return acc * val }
reducer := ReduceArray[Config](product, 1)
readers := []Reader[Config, int]{
Asks(func(c Config) int { return c.Multiplier * 2 }),
Asks(func(c Config) int { return c.Multiplier * 3 }),
}
r := reducer(readers)
result := r(config)
assert.Equal(t, 150, result) // 10 * 15
}
func TestMonadReduceArrayM(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
readers := []Reader[Config, int]{
Asks(func(c Config) int { return c.Factor }),
Asks(func(c Config) int { return c.Factor * 2 }),
Asks(func(c Config) int { return c.Factor * 3 }),
}
intAddMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadReduceArrayM(readers, intAddMonoid)
result := r(config)
assert.Equal(t, 30, result) // 5 + 10 + 15
}
func TestReduceArrayM(t *testing.T) {
type Config struct{ Scale int }
config := Config{Scale: 3}
intMultMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
reducer := ReduceArrayM[Config](intMultMonoid)
readers := []Reader[Config, int]{
Asks(func(c Config) int { return c.Scale }),
Asks(func(c Config) int { return c.Scale * 2 }),
}
r := reducer(readers)
result := r(config)
assert.Equal(t, 18, result) // 3 * 6
}
func TestMonadTraverseReduceArray(t *testing.T) {
type Config struct{ Multiplier int }
config := Config{Multiplier: 10}
numbers := []int{1, 2, 3, 4}
multiply := func(n int) Reader[Config, int] {
return Asks(func(c Config) int { return n * c.Multiplier })
}
sum := func(acc, val int) int { return acc + val }
r := MonadTraverseReduceArray(numbers, multiply, sum, 0)
result := r(config)
assert.Equal(t, 100, result) // 10 + 20 + 30 + 40
}
func TestTraverseReduceArray(t *testing.T) {
type Config struct{ Base int }
config := Config{Base: 10}
addBase := func(n int) Reader[Config, int] {
return Asks(func(c Config) int { return n + c.Base })
}
product := func(acc, val int) int { return acc * val }
transformer := TraverseReduceArray(addBase, product, 1)
r := transformer([]int{2, 3, 4})
result := r(config)
assert.Equal(t, 2184, result) // 12 * 13 * 14
}
func TestMonadTraverseReduceArrayM(t *testing.T) {
type Config struct{ Offset int }
config := Config{Offset: 100}
numbers := []int{1, 2, 3}
addOffset := func(n int) Reader[Config, int] {
return Asks(func(c Config) int { return n + c.Offset })
}
intSumMonoid := M.MakeMonoid(func(a, b int) int { return a + b }, 0)
r := MonadTraverseReduceArrayM(numbers, addOffset, intSumMonoid)
result := r(config)
assert.Equal(t, 306, result) // 101 + 102 + 103
}
func TestTraverseReduceArrayM(t *testing.T) {
type Config struct{ Factor int }
config := Config{Factor: 5}
scale := func(n int) Reader[Config, int] {
return Asks(func(c Config) int { return n * c.Factor })
}
intProdMonoid := M.MakeMonoid(func(a, b int) int { return a * b }, 1)
transformer := TraverseReduceArrayM(scale, intProdMonoid)
r := transformer([]int{2, 3, 4})
result := r(config)
assert.Equal(t, 3000, result) // 10 * 15 * 20
}

68
v2/reader/record.go Normal file
View File

@@ -0,0 +1,68 @@
// 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 reader
import (
"github.com/IBM/fp-go/v2/function"
RR "github.com/IBM/fp-go/v2/internal/record"
)
//go:inline
func MonadTraverseRecord[K comparable, R, A, B any](ma map[K]A, f Kleisli[R, A, B]) Reader[R, map[K]B] {
return RR.MonadTraverse[map[K]A, map[K]B](
Of,
Map,
Ap,
ma,
f,
)
}
//go:inline
func TraverseRecord[K comparable, R, A, B any](f Kleisli[R, A, B]) func(map[K]A) Reader[R, map[K]B] {
return RR.Traverse[map[K]A, map[K]B](
Of,
Map,
Ap,
f,
)
}
//go:inline
func MonadTraverseRecordWithIndex[K comparable, R, A, B any](ma map[K]A, f func(K, A) Reader[R, B]) Reader[R, map[K]B] {
return RR.MonadTraverseWithIndex[map[K]A, map[K]B](
Of,
Map,
Ap,
ma,
f,
)
}
//go:inline
func TraverseRecordWithIndex[K comparable, R, A, B any](f func(K, A) Reader[R, B]) func(map[K]A) Reader[R, map[K]B] {
return RR.TraverseWithIndex[map[K]A, map[K]B](
Of,
Map,
Ap,
f,
)
}
//go:inline
func SequenceRecord[K comparable, R, A any](ma map[K]Reader[R, A]) Reader[R, map[K]A] {
return MonadTraverseRecord(ma, function.Identity[Reader[R, A]])
}

View File

@@ -66,6 +66,14 @@ func Chain[E, L, A, B any](f func(A) ReaderEither[E, L, B]) func(ReaderEither[E,
return readert.Chain[ReaderEither[E, L, A]](ET.Chain[L, A, B], f)
}
func MonadChainReaderK[E, L, A, B any](ma ReaderEither[E, L, A], f reader.Kleisli[E, A, B]) ReaderEither[E, L, B] {
return MonadChain(ma, function.Flow2(f, FromReader[E, L, B]))
}
func ChainReaderK[E, L, A, B any](f reader.Kleisli[E, A, B]) func(ReaderEither[E, L, A]) ReaderEither[E, L, B] {
return Chain(function.Flow2(f, FromReader[E, L, B]))
}
func Of[E, L, A any](a A) ReaderEither[E, L, A] {
return readert.MonadOf[ReaderEither[E, L, A]](ET.Of[L, A], a)
}

View File

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

View File

@@ -808,6 +808,168 @@ func TestApResultIS(t *testing.T) {
})
}
// TestMonadApResult tests the MonadApResult function
func TestMonadApResult(t *testing.T) {
t.Run("success case - both succeed", func(t *testing.T) {
add := func(x int) func(int) int {
return func(y int) int { return x + y }
}
fabr := Of[MyContext](add(5))
fa := result.Of(3)
res := MonadApResult(fabr, fa)
assert.Equal(t, result.Of(8), res(defaultContext))
})
t.Run("function is error", func(t *testing.T) {
fabr := Left[MyContext, func(int) int](idiomaticTestError)
fa := result.Of(3)
res := MonadApResult(fabr, fa)
assert.Equal(t, result.Left[int](idiomaticTestError), res(defaultContext))
})
t.Run("value is error", func(t *testing.T) {
double := func(x int) int { return x * 2 }
fabr := Of[MyContext](double)
fa := result.Left[int](idiomaticTestError)
res := MonadApResult(fabr, fa)
assert.Equal(t, result.Left[int](idiomaticTestError), res(defaultContext))
})
t.Run("both are errors", func(t *testing.T) {
funcError := errors.New("function error")
valueError := errors.New("value error")
fabr := Left[MyContext, func(int) int](funcError)
fa := result.Left[int](valueError)
res := MonadApResult(fabr, fa)
// When both fail, the function error takes precedence in Applicative semantics
assert.True(t, result.IsLeft(res(defaultContext)))
})
}
// TestApResult tests the ApResult function
func TestApResult(t *testing.T) {
t.Run("success case", func(t *testing.T) {
fa := result.Of(10)
res := F.Pipe1(
Of[MyContext](utils.Double),
ApResult[int, MyContext](fa),
)
assert.Equal(t, result.Of(20), res(defaultContext))
})
t.Run("function error", func(t *testing.T) {
fa := result.Of(10)
res := F.Pipe1(
Left[MyContext, func(int) int](idiomaticTestError),
ApResult[int, MyContext](fa),
)
assert.Equal(t, result.Left[int](idiomaticTestError), res(defaultContext))
})
t.Run("value error", func(t *testing.T) {
fa := result.Left[int](idiomaticTestError)
res := F.Pipe1(
Of[MyContext](utils.Double),
ApResult[int, MyContext](fa),
)
assert.Equal(t, result.Left[int](idiomaticTestError), res(defaultContext))
})
t.Run("with triple composition", func(t *testing.T) {
triple := func(x int) int { return x * 3 }
fa := result.Of(7)
res := F.Pipe1(
Of[MyContext](triple),
ApResult[int, MyContext](fa),
)
assert.Equal(t, result.Of(21), res(defaultContext))
})
}
// TestApResultI tests the ApResultI function
func TestApResultI(t *testing.T) {
t.Run("success case", func(t *testing.T) {
value := 10
var err error = nil
res := F.Pipe1(
Of[MyContext](utils.Double),
ApResultI[int, MyContext](value, err),
)
assert.Equal(t, result.Of(20), res(defaultContext))
})
t.Run("function error", func(t *testing.T) {
value := 10
var err error = nil
res := F.Pipe1(
Left[MyContext, func(int) int](idiomaticTestError),
ApResultI[int, MyContext](value, err),
)
assert.Equal(t, result.Left[int](idiomaticTestError), res(defaultContext))
})
t.Run("value error", func(t *testing.T) {
value := 0
err := idiomaticTestError
res := F.Pipe1(
Of[MyContext](utils.Double),
ApResultI[int, MyContext](value, err),
)
assert.Equal(t, result.Left[int](idiomaticTestError), res(defaultContext))
})
t.Run("realistic example with strconv", func(t *testing.T) {
// Simulate parsing a string to int
parseValue := func(s string) (int, error) {
if s == "42" {
return 42, nil
}
return 0, errors.New("parse error")
}
addTen := func(x int) int { return x + 10 }
t.Run("parse success", func(t *testing.T) {
value, err := parseValue("42")
res := F.Pipe1(
Of[MyContext](addTen),
ApResultI[int, MyContext](value, err),
)
assert.Equal(t, result.Of(52), res(defaultContext))
})
t.Run("parse error", func(t *testing.T) {
value, err := parseValue("invalid")
res := F.Pipe1(
Of[MyContext](addTen),
ApResultI[int, MyContext](value, err),
)
assert.True(t, result.IsLeft(res(defaultContext)))
})
})
t.Run("with curried function", func(t *testing.T) {
// Test with a curried addition function
add := func(x int) func(int) int {
return func(y int) int { return x + y }
}
// First apply 5, get a function (int -> int)
partialAdd := F.Pipe1(
Of[MyContext](add),
ApResultI[func(int) int, MyContext](5, nil),
)
// Then apply 3 to the result
finalResult := F.Pipe1(
partialAdd,
ApResultI[int, MyContext](3, nil),
)
assert.Equal(t, result.Of(8), finalResult(defaultContext))
})
}
// Test a complex scenario combining multiple idiomatic functions
func TestComplexIdiomaticScenario(t *testing.T) {
type Env struct {

View File

@@ -204,6 +204,11 @@ func MonadChain[R, A, B any](ma ReaderResult[R, A], f Kleisli[R, A, B]) ReaderRe
return readert.MonadChain(ET.MonadChain[error, A, B], ma, f)
}
//go:inline
func MonadChainReaderK[R, A, B any](ma ReaderResult[R, A], f reader.Kleisli[R, A, B]) ReaderResult[R, B] {
return readert.MonadChain(ET.MonadChain[error, A, B], ma, function.Flow2(f, FromReader[R, B]))
}
// Chain is the curried version of MonadChain.
// It returns an Operator that can be used in function composition pipelines.
//
@@ -217,6 +222,11 @@ func Chain[R, A, B any](f Kleisli[R, A, B]) Operator[R, A, B] {
return readert.Chain[ReaderResult[R, A]](ET.Chain[error, A, B], f)
}
//go:inline
func ChainReaderK[R, A, B any](f reader.Kleisli[R, A, B]) Operator[R, A, B] {
return readert.Chain[ReaderResult[R, A]](ET.Chain[error, A, B], function.Flow2(f, FromReader[R, B]))
}
// MonadChainI sequences two ReaderResult computations, where the second is an idiomatic Kleisli arrow.
// This is the idiomatic version of MonadChain, allowing you to chain with functions that return (B, error).
// The idiomatic Kleisli arrow RRI.Kleisli[R, A, B] is a function A -> R -> (B, error).
@@ -285,6 +295,11 @@ func MonadAp[B, R, A any](fab ReaderResult[R, func(A) B], fa ReaderResult[R, A])
return readert.MonadAp[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.MonadAp[B, error, A], fab, fa)
}
//go:inline
func MonadApReader[B, R, A any](fab ReaderResult[R, func(A) B], fa Reader[R, A]) ReaderResult[R, B] {
return MonadAp(fab, FromReader(fa))
}
// Ap is the curried version of MonadAp.
// It returns an Operator that can be used in function composition pipelines.
//
@@ -293,6 +308,63 @@ func Ap[B, R, A any](fa ReaderResult[R, A]) Operator[R, func(A) B, B] {
return readert.Ap[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.Ap[B, error, A], fa)
}
//go:inline
func ApReader[B, R, A any](fa Reader[R, A]) Operator[R, func(A) B, B] {
return Ap[B](FromReader(fa))
}
// MonadApResult applies a function wrapped in a ReaderResult to a value wrapped in a plain Result.
// The Result value is independent of the environment, while the function may depend on it.
// This is useful when you have a pre-computed Result value that you want to apply a context-dependent function to.
//
// Example:
//
// add := func(x int) func(int) int { return func(y int) int { return x + y } }
// fabr := readerresult.Of[Config](add(5))
// fa := result.Of(3) // Pre-computed Result, independent of environment
// result := readerresult.MonadApResult(fabr, fa) // Returns Of(8)
//
//go:inline
func MonadApResult[B, R, A any](fab ReaderResult[R, func(A) B], fa result.Result[A]) ReaderResult[R, B] {
return readert.MonadAp[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.MonadAp[B, error, A], fab, FromResult[R](fa))
}
// ApResult is the curried version of MonadApResult.
// It returns an Operator that applies a pre-computed Result value to a function in a ReaderResult context.
// This is useful in function composition pipelines when you have a static Result value.
//
// Example:
//
// fa := result.Of(10)
// result := F.Pipe1(
// readerresult.Of[Config](utils.Double),
// readerresult.ApResult[int, Config](fa),
// )
// // result(cfg) returns result.Of(20)
//
//go:inline
func ApResult[B, R, A any](fa Result[A]) Operator[R, func(A) B, B] {
return readert.Ap[ReaderResult[R, A], ReaderResult[R, B], ReaderResult[R, func(A) B], R, A](ET.Ap[B, error, A], FromResult[R](fa))
}
// ApResultI is the curried idiomatic version of ApResult.
// It accepts a (value, error) pair directly and applies it to a function in a ReaderResult context.
// This bridges Go's idiomatic error handling with the functional ApResult operation.
//
// Example:
//
// value, err := strconv.Atoi("10") // Returns (10, nil)
// result := F.Pipe1(
// readerresult.Of[Config](utils.Double),
// readerresult.ApResultI[int, Config](value, err),
// )
// // result(cfg) returns result.Of(20)
//
//go:inline
func ApResultI[B, R, A any](a A, err error) Operator[R, func(A) B, B] {
return Ap[B](FromResultI[R](a, err))
}
// MonadApI applies a function wrapped in a ReaderResult to a value wrapped in an idiomatic ReaderResult.
// This is the idiomatic version of MonadAp, where the second parameter returns (A, error) instead of Result[A].
// Both computations share the same environment.