mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-07 23:03:15 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd575d95a | ||
|
|
dcfb023891 | ||
|
|
51cf241a26 |
463
v2/assert/assert.go
Normal file
463
v2/assert/assert.go
Normal 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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
68
v2/reader/record.go
Normal 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]])
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user