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

Compare commits

...

3 Commits

Author SHA1 Message Date
Dr. Carsten Leue
7cd575d95a fix: improve Prism and Optional
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-24 18:22:52 +01:00
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
14 changed files with 1835 additions and 97 deletions

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

@@ -0,0 +1,463 @@
// 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/option"
"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
}
}
// Local transforms a Reader that works on type R1 into a Reader that works on type R2,
// by providing a function that converts R2 to R1. This allows you to focus a test on a
// specific property or subset of a larger data structure.
//
// This is particularly useful when you have an assertion that operates on a specific field
// or property, and you want to apply it to a complete object. Instead of extracting the
// property and then asserting on it, you can transform the assertion to work directly
// on the whole object.
//
// Parameters:
// - f: A function that extracts or transforms R2 into R1
//
// Returns:
// - A function that transforms a Reader[R1, Reader] into a Reader[R2, Reader]
//
// Example:
//
// type User struct {
// Name string
// Age int
// }
//
// // Create an assertion that checks if age is positive
// ageIsPositive := assert.That(func(age int) bool { return age > 0 })
//
// // Focus this assertion on the Age field of User
// userAgeIsPositive := assert.Local(func(u User) int { return u.Age })(ageIsPositive)
//
// // Now we can test the whole User object
// user := User{Name: "Alice", Age: 30}
// userAgeIsPositive(user)(t)
//
//go:inline
func Local[R1, R2 any](f func(R2) R1) func(Kleisli[R1]) Kleisli[R2] {
return reader.Local[Reader](f)
}
// LocalL is similar to Local but uses a Lens to focus on a specific property.
// A Lens is a functional programming construct that provides a composable way to
// focus on a part of a data structure.
//
// This function is particularly useful when you want to focus a test on a specific
// field of a struct using a lens, making the code more declarative and composable.
// Lenses are often code-generated or predefined for common data structures.
//
// Parameters:
// - l: A Lens that focuses from type S to type T
//
// Returns:
// - A function that transforms a Reader[T, Reader] into a Reader[S, Reader]
//
// Example:
//
// type Person struct {
// Name string
// Email string
// }
//
// // Assume we have a lens that focuses on the Email field
// var emailLens = lens.Prop[Person, string]("Email")
//
// // Create an assertion for email format
// validEmail := assert.That(func(email string) bool {
// return strings.Contains(email, "@")
// })
//
// // Focus this assertion on the Email property using a lens
// validPersonEmail := assert.LocalL(emailLens)(validEmail)
//
// // Test a Person object
// person := Person{Name: "Bob", Email: "bob@example.com"}
// validPersonEmail(person)(t)
//
//go:inline
func LocalL[S, T any](l Lens[S, T]) func(Kleisli[T]) Kleisli[S] {
return reader.Local[Reader](l.Get)
}
// fromOptionalGetter is an internal helper that creates an assertion Reader from
// an optional getter function. It asserts that the optional value is present (Some).
func fromOptionalGetter[S, T any](getter func(S) option.Option[T], msgAndArgs ...any) Kleisli[S] {
return func(s S) Reader {
return func(t *testing.T) bool {
return assert.True(t, option.IsSome(getter(s)), msgAndArgs...)
}
}
}
// FromOptional creates an assertion that checks if an Optional can successfully extract a value.
// An Optional is an optic that represents an optional reference to a subpart of a data structure.
//
// This function is useful when you have an Optional optic and want to assert that the optional
// value is present (Some) rather than absent (None). The assertion passes if the Optional's
// GetOption returns Some, and fails if it returns None.
//
// This enables property-focused testing where you verify that a particular optional field or
// sub-structure exists and is accessible.
//
// Parameters:
// - opt: An Optional optic that focuses from type S to type T
//
// Returns:
// - A Reader that asserts the optional value is present when applied to a value of type S
//
// Example:
//
// type Config struct {
// Database *DatabaseConfig // Optional field
// }
//
// type DatabaseConfig struct {
// Host string
// Port int
// }
//
// // Create an Optional that focuses on the Database field
// dbOptional := optional.MakeOptional(
// func(c Config) option.Option[*DatabaseConfig] {
// if c.Database != nil {
// return option.Some(c.Database)
// }
// return option.None[*DatabaseConfig]()
// },
// func(c Config, db *DatabaseConfig) Config {
// c.Database = db
// return c
// },
// )
//
// // Assert that the database config is present
// hasDatabaseConfig := assert.FromOptional(dbOptional)
//
// config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
// hasDatabaseConfig(config)(t) // Passes
//
// emptyConfig := Config{Database: nil}
// hasDatabaseConfig(emptyConfig)(t) // Fails
//
//go:inline
func FromOptional[S, T any](opt Optional[S, T]) reader.Reader[S, Reader] {
return fromOptionalGetter(opt.GetOption, "Optional: %s", opt)
}
// FromPrism creates an assertion that checks if a Prism can successfully extract a value.
// A Prism is an optic used to select part of a sum type (tagged union or variant).
//
// This function is useful when you have a Prism optic and want to assert that a value
// matches a specific variant of a sum type. The assertion passes if the Prism's GetOption
// returns Some (meaning the value is of the expected variant), and fails if it returns None
// (meaning the value is a different variant).
//
// This enables variant-focused testing where you verify that a value is of a particular
// type or matches a specific condition within a sum type.
//
// Parameters:
// - p: A Prism optic that focuses from type S to type T
//
// Returns:
// - A Reader that asserts the prism successfully extracts when applied to a value of type S
//
// Example:
//
// type Result interface{ isResult() }
// type Success struct{ Value int }
// type Failure struct{ Error string }
//
// func (Success) isResult() {}
// func (Failure) isResult() {}
//
// // Create a Prism that focuses on Success variant
// successPrism := prism.MakePrism(
// func(r Result) option.Option[int] {
// if s, ok := r.(Success); ok {
// return option.Some(s.Value)
// }
// return option.None[int]()
// },
// func(v int) Result { return Success{Value: v} },
// )
//
// // Assert that the result is a Success
// isSuccess := assert.FromPrism(successPrism)
//
// result1 := Success{Value: 42}
// isSuccess(result1)(t) // Passes
//
// result2 := Failure{Error: "something went wrong"}
// isSuccess(result2)(t) // Fails
//
//go:inline
func FromPrism[S, T any](p Prism[S, T]) reader.Reader[S, Reader] {
return fromOptionalGetter(p.GetOption, "Prism: %s", p)
}

View File

@@ -16,94 +16,593 @@
package assert
import (
"fmt"
"errors"
"testing"
"github.com/IBM/fp-go/v2/eq"
"github.com/IBM/fp-go/v2/option"
"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(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(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)
}
return result.Left[map[K]T](errTest)
func TestLocal(t *testing.T) {
type User struct {
Name string
Age int
}
t.Run("should focus assertion on a property", func(t *testing.T) {
// Create an assertion that checks if age is positive
ageIsPositive := That(func(age int) bool { return age > 0 })
// Focus this assertion on the Age field of User
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
// Test with a user who has a positive age
user := User{Name: "Alice", Age: 30}
result := userAgeIsPositive(user)(t)
if !result {
t.Error("Expected focused assertion to pass for positive age")
}
})
t.Run("should fail when focused property doesn't match", func(t *testing.T) {
mockT := &testing.T{}
ageIsPositive := That(func(age int) bool { return age > 0 })
userAgeIsPositive := Local(func(u User) int { return u.Age })(ageIsPositive)
// Test with a user who has zero age
user := User{Name: "Bob", Age: 0}
result := userAgeIsPositive(user)(mockT)
if result {
t.Error("Expected focused assertion to fail for zero age")
}
})
t.Run("should compose with other assertions", func(t *testing.T) {
// Create multiple focused assertions
nameNotEmpty := Local(func(u User) string { return u.Name })(
That(func(name string) bool { return len(name) > 0 }),
)
ageInRange := Local(func(u User) int { return u.Age })(
That(func(age int) bool { return age >= 18 && age <= 100 }),
)
user := User{Name: "Charlie", Age: 25}
assertions := AllOf([]Reader{
nameNotEmpty(user),
ageInRange(user),
})
result := assertions(t)
if !result {
t.Error("Expected composed focused assertions to pass")
}
})
t.Run("should work with Equal assertion", func(t *testing.T) {
// Focus Equal assertion on Name field
nameIsAlice := Local(func(u User) string { return u.Name })(Equal("Alice"))
user := User{Name: "Alice", Age: 30}
result := nameIsAlice(user)(t)
if !result {
t.Error("Expected focused Equal assertion to pass")
}
})
}
func TestLocalL(t *testing.T) {
// Note: LocalL requires lens package which provides lens operations.
// This test demonstrates the concept, but actual usage would require
// proper lens definitions.
t.Run("conceptual test for LocalL", func(t *testing.T) {
// LocalL is similar to Local but uses lenses for focusing.
// It would be used like:
// validEmail := That(func(email string) bool { return strings.Contains(email, "@") })
// validPersonEmail := LocalL(emailLens)(validEmail)
//
// The actual implementation would require lens definitions from the lens package.
// This test serves as documentation for the intended usage.
})
}
func TestFromOptional(t *testing.T) {
type DatabaseConfig struct {
Host string
Port int
}
type Config struct {
Database *DatabaseConfig
}
// Create an Optional that focuses on the Database field
dbOptional := Optional[Config, *DatabaseConfig]{
GetOption: func(c Config) option.Option[*DatabaseConfig] {
if c.Database != nil {
return option.Of(c.Database)
}
return option.None[*DatabaseConfig]()
},
Set: func(db *DatabaseConfig) func(Config) Config {
return func(c Config) Config {
c.Database = db
return c
}
},
}
t.Run("should pass when optional value is present", func(t *testing.T) {
config := Config{Database: &DatabaseConfig{Host: "localhost", Port: 5432}}
hasDatabaseConfig := FromOptional(dbOptional)
result := hasDatabaseConfig(config)(t)
if !result {
t.Error("Expected FromOptional to pass when optional value is present")
}
})
t.Run("should fail when optional value is absent", func(t *testing.T) {
mockT := &testing.T{}
emptyConfig := Config{Database: nil}
hasDatabaseConfig := FromOptional(dbOptional)
result := hasDatabaseConfig(emptyConfig)(mockT)
if result {
t.Error("Expected FromOptional to fail when optional value is absent")
}
})
t.Run("should work with nested optionals", func(t *testing.T) {
type AdvancedSettings struct {
Cache bool
}
type Settings struct {
Advanced *AdvancedSettings
}
advancedOptional := Optional[Settings, *AdvancedSettings]{
GetOption: func(s Settings) option.Option[*AdvancedSettings] {
if s.Advanced != nil {
return option.Of(s.Advanced)
}
return option.None[*AdvancedSettings]()
},
Set: func(adv *AdvancedSettings) func(Settings) Settings {
return func(s Settings) Settings {
s.Advanced = adv
return s
}
},
}
settings := Settings{Advanced: &AdvancedSettings{Cache: true}}
hasAdvanced := FromOptional(advancedOptional)
result := hasAdvanced(settings)(t)
if !result {
t.Error("Expected FromOptional to pass for nested optional")
}
})
}

View File

@@ -1,7 +1,22 @@
package assert
import "github.com/IBM/fp-go/v2/result"
import (
"testing"
"github.com/IBM/fp-go/v2/optics/lens"
"github.com/IBM/fp-go/v2/optics/optional"
"github.com/IBM/fp-go/v2/optics/prism"
"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]
Lens[S, T any] = lens.Lens[S, T]
Optional[S, T any] = optional.Optional[S, T]
Prism[S, T any] = prism.Prism[S, 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 lens
import (
"fmt"
"github.com/IBM/fp-go/v2/endomorphism"
EQ "github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
@@ -597,3 +599,11 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) Operator[
return MakeLensCurried(F.Flow2(ea.Get, ab), F.Flow2(ba, ea.Set))
}
}
func (l Lens[S, T]) String() string {
return "Lens"
}
func (l Lens[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -18,6 +18,8 @@
package optional
import (
"fmt"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
@@ -27,6 +29,7 @@ import (
type Optional[S, A any] struct {
GetOption func(s S) O.Option[A]
Set func(a A) EM.Endomorphism[S]
name string
}
// setCopy wraps a setter for a pointer into a setter that first creates a copy before
@@ -41,29 +44,42 @@ func setCopy[SET ~func(*S, A) *S, S, A any](setter SET) func(s *S, a A) *S {
// MakeOptional creates an Optional based on a getter and a setter function. Make sure that the setter creates a (shallow) copy of the
// data. This happens automatically if the data is passed by value. For pointers consider to use `MakeOptionalRef`
// and for other kinds of data structures that are copied by reference make sure the setter creates the copy.
//
//go:inline
func MakeOptional[S, A any](get func(S) O.Option[A], set func(S, A) S) Optional[S, A] {
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set)}
return MakeOptionalWithName(get, set, "GenericOptional")
}
func MakeOptionalWithName[S, A any](get func(S) O.Option[A], set func(S, A) S, name string) Optional[S, A] {
return Optional[S, A]{GetOption: get, Set: F.Bind2of2(set), name: name}
}
// MakeOptionalRef creates an Optional based on a getter and a setter function. The setter passed in does not have to create a shallow
// copy, the implementation wraps the setter into one that copies the pointer before modifying it
//
//go:inline
func MakeOptionalRef[S, A any](get func(*S) O.Option[A], set func(*S, A) *S) Optional[*S, A] {
return MakeOptional(get, setCopy(set))
}
//go:inline
func MakeOptionalRefWithName[S, A any](get func(*S) O.Option[A], set func(*S, A) *S, name string) Optional[*S, A] {
return MakeOptionalWithName(get, setCopy(set), name)
}
// Id returns am optional implementing the identity operation
func id[S any](creator func(get func(S) O.Option[S], set func(S, S) S) Optional[S, S]) Optional[S, S] {
return creator(O.Some[S], F.Second[S, S])
func idWithName[S any](creator func(get func(S) O.Option[S], set func(S, S) S, name string) Optional[S, S], name string) Optional[S, S] {
return creator(O.Some[S], F.Second[S, S], name)
}
// Id returns am optional implementing the identity operation
func Id[S any]() Optional[S, S] {
return id(MakeOptional[S, S])
return idWithName(MakeOptionalWithName[S, S], "Identity")
}
// Id returns am optional implementing the identity operation
func IdRef[S any]() Optional[*S, *S] {
return id(MakeOptionalRef[S, *S])
return idWithName(MakeOptionalRefWithName[S, *S], "Identity")
}
func optionalModifyOption[S, A any](f func(A) A, optional Optional[S, A], s S) O.Option[S] {
@@ -189,3 +205,11 @@ func IChainAny[S, A any]() func(Optional[S, any]) Optional[S, A] {
return ichain(sa, fromAny, toAny)
}
}
func (l Optional[S, T]) String() string {
return l.name
}
func (l Optional[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

View File

@@ -16,6 +16,8 @@
package prism
import (
"fmt"
EM "github.com/IBM/fp-go/v2/endomorphism"
F "github.com/IBM/fp-go/v2/function"
O "github.com/IBM/fp-go/v2/option"
@@ -48,33 +50,19 @@ type (
// },
// func(v int) Result { return Success{Value: v} },
// )
Prism[S, A any] interface {
Prism[S, A any] struct {
// GetOption attempts to extract a value of type A from S.
// Returns Some(a) if the extraction succeeds, None otherwise.
GetOption(s S) Option[A]
GetOption O.Kleisli[S, A]
// ReverseGet constructs an S from an A.
// This operation always succeeds.
ReverseGet(a A) S
}
ReverseGet func(A) S
// prismImpl is the internal implementation of the Prism interface.
prismImpl[S, A any] struct {
get func(S) Option[A]
rev func(A) S
name string
}
)
// GetOption implements the Prism interface for prismImpl.
func (prism prismImpl[S, A]) GetOption(s S) Option[A] {
return prism.get(s)
}
// ReverseGet implements the Prism interface for prismImpl.
func (prism prismImpl[S, A]) ReverseGet(a A) S {
return prism.rev(a)
}
// MakePrism constructs a Prism from GetOption and ReverseGet functions.
//
// Parameters:
@@ -91,7 +79,7 @@ func (prism prismImpl[S, A]) ReverseGet(a A) S {
// func(n int) Option[int] { return Some(n) },
// )
func MakePrism[S, A any](get func(S) Option[A], rev func(A) S) Prism[S, A] {
return prismImpl[S, A]{get, rev}
return Prism[S, A]{get, rev, "GenericPrism"}
}
// Id returns an identity prism that focuses on the entire value.
@@ -278,3 +266,11 @@ func IMap[S any, AB ~func(A) B, BA ~func(B) A, A, B any](ab AB, ba BA) func(Pris
return imap(sa, ab, ba)
}
}
func (l Prism[S, T]) String() string {
return "Prism"
}
func (l Prism[S, T]) Format(f fmt.State, c rune) {
fmt.Fprint(f, l.String())
}

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.