mirror of
https://github.com/IBM/fp-go.git
synced 2026-04-11 15:29:06 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45cc0a7fc1 | ||
|
|
21b517d388 |
@@ -1510,3 +1510,8 @@ func Extend[A, B any](f func([]A) B) Operator[A, B] {
|
||||
func Extract[A any](as []A) A {
|
||||
return G.Extract(as)
|
||||
}
|
||||
|
||||
//go:inline
|
||||
func UpdateAt[T any](i int, v T) func([]T) Option[[]T] {
|
||||
return G.UpdateAt[[]T](i, v)
|
||||
}
|
||||
|
||||
@@ -489,3 +489,16 @@ func Extend[GA ~[]A, GB ~[]B, A, B any](f func(GA) B) func(GA) GB {
|
||||
return MakeBy[GB](len(as), func(i int) B { return f(as[i:]) })
|
||||
}
|
||||
}
|
||||
|
||||
func UpdateAt[GT ~[]T, T any](i int, v T) func(GT) O.Option[GT] {
|
||||
none := O.None[GT]()
|
||||
if i < 0 {
|
||||
return F.Constant1[GT](none)
|
||||
}
|
||||
return func(g GT) O.Option[GT] {
|
||||
if i >= len(g) {
|
||||
return none
|
||||
}
|
||||
return O.Of(array.UnsafeUpdateAt(g, i, v))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,28 +13,218 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Package constraints defines a set of useful type constraints for generic programming in Go.
|
||||
|
||||
# Overview
|
||||
|
||||
This package provides type constraints that can be used with Go generics to restrict
|
||||
type parameters to specific categories of types. These constraints are similar to those
|
||||
in Go's standard constraints package but are defined here for consistency within the
|
||||
fp-go project.
|
||||
|
||||
# Type Constraints
|
||||
|
||||
Ordered - Types that support comparison operators:
|
||||
|
||||
type Ordered interface {
|
||||
Integer | Float | ~string
|
||||
}
|
||||
|
||||
Used for types that can be compared using <, <=, >, >= operators.
|
||||
|
||||
Integer - All integer types (signed and unsigned):
|
||||
|
||||
type Integer interface {
|
||||
Signed | Unsigned
|
||||
}
|
||||
|
||||
Signed - Signed integer types:
|
||||
|
||||
type Signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
Unsigned - Unsigned integer types:
|
||||
|
||||
type Unsigned interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
Float - Floating-point types:
|
||||
|
||||
type Float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
Complex - Complex number types:
|
||||
|
||||
type Complex interface {
|
||||
~complex64 | ~complex128
|
||||
}
|
||||
|
||||
# Usage Examples
|
||||
|
||||
Using Ordered constraint for comparison:
|
||||
|
||||
import C "github.com/IBM/fp-go/v2/constraints"
|
||||
|
||||
func Min[T C.Ordered](a, b T) T {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
result := Min(5, 3) // 3
|
||||
result := Min(3.14, 2.71) // 2.71
|
||||
result := Min("apple", "banana") // "apple"
|
||||
|
||||
Using Integer constraint:
|
||||
|
||||
func Abs[T C.Integer](n T) T {
|
||||
if n < 0 {
|
||||
return -n
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
result := Abs(-42) // 42
|
||||
result := Abs(uint(10)) // 10
|
||||
|
||||
Using Float constraint:
|
||||
|
||||
func Average[T C.Float](a, b T) T {
|
||||
return (a + b) / 2
|
||||
}
|
||||
|
||||
result := Average(3.14, 2.86) // 3.0
|
||||
|
||||
Using Complex constraint:
|
||||
|
||||
func Magnitude[T C.Complex](c T) float64 {
|
||||
r, i := real(c), imag(c)
|
||||
return math.Sqrt(r*r + i*i)
|
||||
}
|
||||
|
||||
c := complex(3, 4)
|
||||
result := Magnitude(c) // 5.0
|
||||
|
||||
# Combining Constraints
|
||||
|
||||
Constraints can be combined to create more specific type restrictions:
|
||||
|
||||
type Number interface {
|
||||
C.Integer | C.Float | C.Complex
|
||||
}
|
||||
|
||||
func Add[T Number](a, b T) T {
|
||||
return a + b
|
||||
}
|
||||
|
||||
# Tilde Operator
|
||||
|
||||
The ~ operator in type constraints means "underlying type". For example, ~int
|
||||
matches not only int but also any type whose underlying type is int:
|
||||
|
||||
type MyInt int
|
||||
|
||||
func Double[T C.Integer](n T) T {
|
||||
return n * 2
|
||||
}
|
||||
|
||||
var x MyInt = 5
|
||||
result := Double(x) // Works because MyInt's underlying type is int
|
||||
|
||||
# Related Packages
|
||||
|
||||
- number: Provides algebraic structures and utilities for numeric types
|
||||
- ord: Provides ordering operations using these constraints
|
||||
- eq: Provides equality operations for comparable types
|
||||
*/
|
||||
package constraints
|
||||
|
||||
// Ordered is a constraint that permits any ordered type: any type that supports
|
||||
// the operators < <= >= >. Ordered types include integers, floats, and strings.
|
||||
//
|
||||
// This constraint is commonly used for comparison operations, sorting, and
|
||||
// finding minimum/maximum values.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Max[T Ordered](a, b T) T {
|
||||
// if a > b {
|
||||
// return a
|
||||
// }
|
||||
// return b
|
||||
// }
|
||||
type Ordered interface {
|
||||
Integer | Float | ~string
|
||||
}
|
||||
|
||||
// Signed is a constraint that permits any signed integer type.
|
||||
// This includes int, int8, int16, int32, and int64, as well as any
|
||||
// types whose underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Negate[T Signed](n T) T {
|
||||
// return -n
|
||||
// }
|
||||
type Signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
// Unsigned is a constraint that permits any unsigned integer type.
|
||||
// This includes uint, uint8, uint16, uint32, uint64, and uintptr, as well
|
||||
// as any types whose underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func IsEven[T Unsigned](n T) bool {
|
||||
// return n%2 == 0
|
||||
// }
|
||||
type Unsigned interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
// Integer is a constraint that permits any integer type, both signed and unsigned.
|
||||
// This is a union of the Signed and Unsigned constraints.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Abs[T Integer](n T) T {
|
||||
// if n < 0 {
|
||||
// return -n
|
||||
// }
|
||||
// return n
|
||||
// }
|
||||
type Integer interface {
|
||||
Signed | Unsigned
|
||||
}
|
||||
|
||||
// Float is a constraint that permits any floating-point type.
|
||||
// This includes float32 and float64, as well as any types whose
|
||||
// underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Round[T Float](f T) T {
|
||||
// return T(math.Round(float64(f)))
|
||||
// }
|
||||
type Float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
// Complex is a constraint that permits any complex numeric type.
|
||||
// This includes complex64 and complex128, as well as any types whose
|
||||
// underlying type is one of these.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func Conjugate[T Complex](c T) T {
|
||||
// return complex(real(c), -imag(c))
|
||||
// }
|
||||
type Complex interface {
|
||||
~complex64 | ~complex128
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
package array
|
||||
|
||||
import "slices"
|
||||
|
||||
func Of[GA ~[]A, A any](a A) GA {
|
||||
return GA{a}
|
||||
}
|
||||
@@ -197,3 +199,9 @@ func Reverse[GT ~[]T, T any](as GT) GT {
|
||||
}
|
||||
return ras
|
||||
}
|
||||
|
||||
func UnsafeUpdateAt[GT ~[]T, T any](as GT, i int, v T) GT {
|
||||
c := slices.Clone(as)
|
||||
c[i] = v
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -522,3 +522,199 @@ func MarshalJSON[T any](
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// FromNonZero creates a bidirectional codec for non-zero values of comparable types.
|
||||
// This codec validates that values are not equal to their zero value (e.g., 0 for int,
|
||||
// "" for string, false for bool, nil for pointers).
|
||||
//
|
||||
// The codec uses a refinement (prism) that:
|
||||
// - Decodes: Validates that the input is not the zero value of type T
|
||||
// - Encodes: Returns the value unchanged (identity function)
|
||||
// - Validates: Ensures the value is non-zero/non-default
|
||||
//
|
||||
// This is useful for enforcing that required fields have meaningful values rather than
|
||||
// their default zero values, which often represent "not set" or "missing" states.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - T: A comparable type (must support == and != operators)
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[T, T, T] codec that validates non-zero values
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec for non-zero integers
|
||||
// nonZeroInt := FromNonZero[int]()
|
||||
//
|
||||
// // Decode non-zero value succeeds
|
||||
// result := nonZeroInt.Decode(42)
|
||||
// // result is Right(42)
|
||||
//
|
||||
// // Decode zero value fails
|
||||
// result := nonZeroInt.Decode(0)
|
||||
// // result is Left(ValidationError{...})
|
||||
//
|
||||
// // Encode is identity
|
||||
// encoded := nonZeroInt.Encode(42)
|
||||
// // encoded is 42
|
||||
//
|
||||
// // Works with strings
|
||||
// nonEmptyStr := FromNonZero[string]()
|
||||
// result := nonEmptyStr.Decode("hello") // Right("hello")
|
||||
// result = nonEmptyStr.Decode("") // Left(ValidationError{...})
|
||||
//
|
||||
// // Works with pointers
|
||||
// nonNilPtr := FromNonZero[*int]()
|
||||
// value := 42
|
||||
// result := nonNilPtr.Decode(&value) // Right(&value)
|
||||
// result = nonNilPtr.Decode(nil) // Left(ValidationError{...})
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating required numeric fields are not zero
|
||||
// - Ensuring string fields are not empty
|
||||
// - Checking pointers are not nil
|
||||
// - Validating boolean flags are explicitly set to true
|
||||
// - Composing with other codecs for multi-stage validation
|
||||
//
|
||||
// See Also:
|
||||
// - NonEmptyString: Specialized version for strings with clearer intent
|
||||
// - FromRefinement: General function for creating codecs from prisms
|
||||
func FromNonZero[T comparable]() Type[T, T, T] {
|
||||
return FromRefinement(prism.FromNonZero[T]())
|
||||
}
|
||||
|
||||
// NonEmptyString creates a bidirectional codec for non-empty strings.
|
||||
// This codec validates that string values are not empty, providing a type-safe
|
||||
// way to work with strings that must contain at least one character.
|
||||
//
|
||||
// This is a specialized version of FromNonZero[string]() that makes the intent
|
||||
// clearer when working specifically with strings that must not be empty.
|
||||
//
|
||||
// The codec:
|
||||
// - Decodes: Validates that the input string is not empty ("")
|
||||
// - Encodes: Returns the string unchanged (identity function)
|
||||
// - Validates: Ensures the string has length > 0
|
||||
//
|
||||
// Note: This codec only checks for empty strings, not whitespace-only strings.
|
||||
// A string containing only spaces, tabs, or newlines will pass validation.
|
||||
//
|
||||
// Returns:
|
||||
// - A Type[string, string, string] codec that validates non-empty strings
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// nonEmpty := NonEmptyString()
|
||||
//
|
||||
// // Decode non-empty string succeeds
|
||||
// result := nonEmpty.Decode("hello")
|
||||
// // result is Right("hello")
|
||||
//
|
||||
// // Decode empty string fails
|
||||
// result := nonEmpty.Decode("")
|
||||
// // result is Left(ValidationError{...})
|
||||
//
|
||||
// // Whitespace-only strings pass validation
|
||||
// result := nonEmpty.Decode(" ")
|
||||
// // result is Right(" ")
|
||||
//
|
||||
// // Encode is identity
|
||||
// encoded := nonEmpty.Encode("world")
|
||||
// // encoded is "world"
|
||||
//
|
||||
// // Compose with other codecs for validation pipelines
|
||||
// intFromNonEmptyString := Pipe(IntFromString())(nonEmpty)
|
||||
// result := intFromNonEmptyString.Decode("42") // Right(42)
|
||||
// result = intFromNonEmptyString.Decode("") // Left(ValidationError{...})
|
||||
// result = intFromNonEmptyString.Decode("abc") // Left(ValidationError{...})
|
||||
//
|
||||
// Common use cases:
|
||||
// - Validating required string fields (usernames, names, IDs)
|
||||
// - Ensuring configuration values are provided
|
||||
// - Validating user input before processing
|
||||
// - Composing with parsing codecs to validate before parsing
|
||||
// - Building validation pipelines for string data
|
||||
//
|
||||
// See Also:
|
||||
// - FromNonZero: General version for any comparable type
|
||||
// - String: Basic string codec without validation
|
||||
// - IntFromString: Codec for parsing integers from strings
|
||||
func NonEmptyString() Type[string, string, string] {
|
||||
return F.Pipe1(
|
||||
FromRefinement(prism.NonEmptyString()),
|
||||
WithName[string, string, string]("NonEmptyString"),
|
||||
)
|
||||
}
|
||||
|
||||
// WithName creates an endomorphism that renames a codec without changing its behavior.
|
||||
// This function returns a higher-order function that takes a codec and returns a new codec
|
||||
// with the specified name, while preserving all validation, encoding, and type-checking logic.
|
||||
//
|
||||
// This is useful for:
|
||||
// - Providing more descriptive names for composed codecs
|
||||
// - Creating domain-specific codec names for better error messages
|
||||
// - Documenting the purpose of complex codec pipelines
|
||||
// - Improving debugging and logging output
|
||||
//
|
||||
// The renamed codec maintains the same:
|
||||
// - Type checking behavior (Is function)
|
||||
// - Validation logic (Validate function)
|
||||
// - Encoding behavior (Encode function)
|
||||
//
|
||||
// Only the name returned by the Name() method changes.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The target type (what we decode to and encode from)
|
||||
// - O: The output type (what we encode to)
|
||||
// - I: The input type (what we decode from)
|
||||
//
|
||||
// Parameters:
|
||||
// - name: The new name for the codec
|
||||
//
|
||||
// Returns:
|
||||
// - An Endomorphism[Type[A, O, I]] that renames the codec
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // Create a codec with a generic name
|
||||
// positiveInt := Pipe[int, int, string, int](
|
||||
// FromRefinement(prism.FromPredicate(func(n int) bool { return n > 0 })),
|
||||
// )(IntFromString())
|
||||
// // positiveInt.Name() returns something like "Pipe(FromRefinement(...), IntFromString)"
|
||||
//
|
||||
// // Rename it for clarity
|
||||
// namedCodec := WithName[int, string, string]("PositiveIntFromString")(positiveInt)
|
||||
// // namedCodec.Name() returns "PositiveIntFromString"
|
||||
//
|
||||
// // Use in a pipeline with F.Pipe
|
||||
// userAgeCodec := F.Pipe1(
|
||||
// IntFromString(),
|
||||
// WithName[int, string, string]("UserAge"),
|
||||
// )
|
||||
//
|
||||
// // Validation errors will show the custom name
|
||||
// result := userAgeCodec.Decode("invalid")
|
||||
// // Error context will reference "UserAge" instead of "IntFromString"
|
||||
//
|
||||
// Common use cases:
|
||||
// - Naming composed codecs for better error messages
|
||||
// - Creating domain-specific codec names (e.g., "EmailAddress", "PhoneNumber")
|
||||
// - Documenting complex validation pipelines
|
||||
// - Improving debugging output in logs
|
||||
// - Making codec composition more readable
|
||||
//
|
||||
// Note: This function creates a new codec instance with the same behavior but a different
|
||||
// name. The original codec is not modified.
|
||||
//
|
||||
// See Also:
|
||||
// - MakeType: For creating codecs with custom names from scratch
|
||||
// - Pipe: For composing codecs (which generates automatic names)
|
||||
func WithName[A, O, I any](name string) Endomorphism[Type[A, O, I]] {
|
||||
return func(codec Type[A, O, I]) Type[A, O, I] {
|
||||
return MakeType(
|
||||
name,
|
||||
codec.Is,
|
||||
codec.Validate,
|
||||
codec.Encode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/IBM/fp-go/v2/either"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/codec/validation"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/option"
|
||||
@@ -688,6 +689,596 @@ func TestBoolFromString_Integration(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FromNonZero
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestFromNonZero_Decode_Success(t *testing.T) {
|
||||
t.Run("int - decodes non-zero value", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(42)
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("int - decodes negative value", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(-5)
|
||||
assert.Equal(t, validation.Success(-5), result)
|
||||
})
|
||||
|
||||
t.Run("string - decodes non-empty string", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
result := c.Decode("hello")
|
||||
assert.Equal(t, validation.Success("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("string - decodes whitespace string", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
result := c.Decode(" ")
|
||||
assert.Equal(t, validation.Success(" "), result)
|
||||
})
|
||||
|
||||
t.Run("bool - decodes true", func(t *testing.T) {
|
||||
c := FromNonZero[bool]()
|
||||
result := c.Decode(true)
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("float64 - decodes non-zero value", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
result := c.Decode(3.14)
|
||||
assert.Equal(t, validation.Success(3.14), result)
|
||||
})
|
||||
|
||||
t.Run("float64 - decodes negative value", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
result := c.Decode(-2.5)
|
||||
assert.Equal(t, validation.Success(-2.5), result)
|
||||
})
|
||||
|
||||
t.Run("pointer - decodes non-nil pointer", func(t *testing.T) {
|
||||
c := FromNonZero[*int]()
|
||||
value := 42
|
||||
result := c.Decode(&value)
|
||||
assert.True(t, either.IsRight(result))
|
||||
ptr := either.MonadFold(result, func(validation.Errors) *int { return nil }, func(p *int) *int { return p })
|
||||
require.NotNil(t, ptr)
|
||||
assert.Equal(t, 42, *ptr)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Decode_Failure(t *testing.T) {
|
||||
t.Run("int - fails on zero", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(0)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("string - fails on empty string", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("bool - fails on false", func(t *testing.T) {
|
||||
c := FromNonZero[bool]()
|
||||
result := c.Decode(false)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("float64 - fails on zero", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
result := c.Decode(0.0)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("pointer - fails on nil", func(t *testing.T) {
|
||||
c := FromNonZero[*int]()
|
||||
result := c.Decode(nil)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Encode(t *testing.T) {
|
||||
t.Run("int - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
assert.Equal(t, 42, c.Encode(42))
|
||||
})
|
||||
|
||||
t.Run("string - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
assert.Equal(t, "hello", c.Encode("hello"))
|
||||
})
|
||||
|
||||
t.Run("bool - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[bool]()
|
||||
assert.Equal(t, true, c.Encode(true))
|
||||
})
|
||||
|
||||
t.Run("float64 - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[float64]()
|
||||
assert.Equal(t, 3.14, c.Encode(3.14))
|
||||
})
|
||||
|
||||
t.Run("pointer - encodes value unchanged", func(t *testing.T) {
|
||||
c := FromNonZero[*int]()
|
||||
value := 42
|
||||
ptr := &value
|
||||
assert.Equal(t, ptr, c.Encode(ptr))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode then encode", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
original := 42
|
||||
result := c.Decode(original)
|
||||
require.True(t, either.IsRight(result))
|
||||
decoded := either.MonadFold(result, func(validation.Errors) int { return 0 }, func(n int) int { return n })
|
||||
assert.Equal(t, original, c.Encode(decoded))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Name(t *testing.T) {
|
||||
t.Run("int codec name", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
assert.Contains(t, c.Name(), "FromRefinement")
|
||||
assert.Contains(t, c.Name(), "PrismFromNonZero")
|
||||
})
|
||||
|
||||
t.Run("string codec name", func(t *testing.T) {
|
||||
c := FromNonZero[string]()
|
||||
assert.Contains(t, c.Name(), "FromRefinement")
|
||||
assert.Contains(t, c.Name(), "PrismFromNonZero")
|
||||
})
|
||||
}
|
||||
|
||||
func TestFromNonZero_Integration(t *testing.T) {
|
||||
t.Run("validates multiple non-zero integers", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
values := []int{1, -1, 42, -100, 999}
|
||||
for _, v := range values {
|
||||
result := c.Decode(v)
|
||||
require.True(t, either.IsRight(result), "expected success for %d", v)
|
||||
decoded := either.MonadFold(result, func(validation.Errors) int { return 0 }, func(n int) int { return n })
|
||||
assert.Equal(t, v, decoded)
|
||||
assert.Equal(t, v, c.Encode(decoded))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects zero values", func(t *testing.T) {
|
||||
c := FromNonZero[int]()
|
||||
result := c.Decode(0)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("works with custom comparable types", func(t *testing.T) {
|
||||
type UserID string
|
||||
c := FromNonZero[UserID]()
|
||||
|
||||
result := c.Decode(UserID("user123"))
|
||||
assert.Equal(t, validation.Success(UserID("user123")), result)
|
||||
|
||||
result = c.Decode(UserID(""))
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NonEmptyString
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestNonEmptyString_Decode_Success(t *testing.T) {
|
||||
t.Run("decodes non-empty string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("hello")
|
||||
assert.Equal(t, validation.Success("hello"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes single character", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("a")
|
||||
assert.Equal(t, validation.Success("a"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes whitespace string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode(" ")
|
||||
assert.Equal(t, validation.Success(" "), result)
|
||||
})
|
||||
|
||||
t.Run("decodes string with newlines", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("\n\t")
|
||||
assert.Equal(t, validation.Success("\n\t"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes unicode string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("你好")
|
||||
assert.Equal(t, validation.Success("你好"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes emoji string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("🎉")
|
||||
assert.Equal(t, validation.Success("🎉"), result)
|
||||
})
|
||||
|
||||
t.Run("decodes multiline string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
multiline := "line1\nline2\nline3"
|
||||
result := c.Decode(multiline)
|
||||
assert.Equal(t, validation.Success(multiline), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Decode_Failure(t *testing.T) {
|
||||
t.Run("fails on empty string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("error contains context", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("")
|
||||
require.True(t, either.IsLeft(result))
|
||||
errors := either.MonadFold(result, func(e validation.Errors) validation.Errors { return e }, func(string) validation.Errors { return nil })
|
||||
require.NotEmpty(t, errors)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Encode(t *testing.T) {
|
||||
t.Run("encodes string unchanged", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, "hello", c.Encode("hello"))
|
||||
})
|
||||
|
||||
t.Run("encodes unicode string unchanged", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, "你好", c.Encode("你好"))
|
||||
})
|
||||
|
||||
t.Run("encodes whitespace string unchanged", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, " ", c.Encode(" "))
|
||||
})
|
||||
|
||||
t.Run("round-trip: decode then encode", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
original := "test string"
|
||||
result := c.Decode(original)
|
||||
require.True(t, either.IsRight(result))
|
||||
decoded := either.MonadFold(result, func(validation.Errors) string { return "" }, func(s string) string { return s })
|
||||
assert.Equal(t, original, c.Encode(decoded))
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Name(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
assert.Equal(t, c.Name(), "NonEmptyString")
|
||||
}
|
||||
|
||||
func TestNonEmptyString_Integration(t *testing.T) {
|
||||
t.Run("validates multiple non-empty strings", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
strings := []string{"a", "hello", "world", "test123", " spaces ", "🎉"}
|
||||
for _, s := range strings {
|
||||
result := c.Decode(s)
|
||||
require.True(t, either.IsRight(result), "expected success for %q", s)
|
||||
decoded := either.MonadFold(result, func(validation.Errors) string { return "" }, func(str string) string { return str })
|
||||
assert.Equal(t, s, decoded)
|
||||
assert.Equal(t, s, c.Encode(decoded))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects empty string", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
result := c.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("compose with IntFromString", func(t *testing.T) {
|
||||
// Create a codec that only parses non-empty strings to integers
|
||||
nonEmptyThenInt := Pipe[string, string](IntFromString())(NonEmptyString())
|
||||
|
||||
// Valid non-empty string with integer
|
||||
result := nonEmptyThenInt.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Empty string fails at NonEmptyString stage
|
||||
result = nonEmptyThenInt.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
// Non-empty but invalid integer fails at IntFromString stage
|
||||
result = nonEmptyThenInt.Decode("abc")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("use in validation pipeline", func(t *testing.T) {
|
||||
c := NonEmptyString()
|
||||
|
||||
// Simulate validating user input
|
||||
inputs := []struct {
|
||||
value string
|
||||
expected bool
|
||||
}{
|
||||
{"john_doe", true},
|
||||
{"", false},
|
||||
{"a", true},
|
||||
{"user@example.com", true},
|
||||
}
|
||||
|
||||
for _, input := range inputs {
|
||||
result := c.Decode(input.value)
|
||||
if input.expected {
|
||||
assert.True(t, either.IsRight(result), "expected success for %q", input.value)
|
||||
} else {
|
||||
assert.True(t, either.IsLeft(result), "expected failure for %q", input.value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WithName
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestWithName_BasicFunctionality(t *testing.T) {
|
||||
t.Run("renames codec without changing behavior", func(t *testing.T) {
|
||||
original := IntFromString()
|
||||
renamed := WithName[int, string, string]("CustomIntCodec")(original)
|
||||
|
||||
// Name should be changed
|
||||
assert.Equal(t, "CustomIntCodec", renamed.Name())
|
||||
assert.NotEqual(t, original.Name(), renamed.Name())
|
||||
|
||||
// Behavior should be unchanged
|
||||
result := renamed.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
encoded := renamed.Encode(42)
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
|
||||
t.Run("preserves validation logic", func(t *testing.T) {
|
||||
original := IntFromString()
|
||||
renamed := WithName[int, string, string]("MyInt")(original)
|
||||
|
||||
// Valid input should succeed
|
||||
result := renamed.Decode("123")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Invalid input should fail
|
||||
result = renamed.Decode("not a number")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("preserves encoding logic", func(t *testing.T) {
|
||||
original := BoolFromString()
|
||||
renamed := WithName[bool, string, string]("CustomBool")(original)
|
||||
|
||||
assert.Equal(t, "true", renamed.Encode(true))
|
||||
assert.Equal(t, "false", renamed.Encode(false))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_WithComposedCodecs(t *testing.T) {
|
||||
t.Run("renames composed codec", func(t *testing.T) {
|
||||
// Create a composed codec
|
||||
composed := Pipe[string, string](IntFromString())(NonEmptyString())
|
||||
|
||||
// Rename it
|
||||
renamed := WithName[int, string, string]("NonEmptyIntString")(composed)
|
||||
|
||||
assert.Equal(t, "NonEmptyIntString", renamed.Name())
|
||||
|
||||
// Behavior should be preserved
|
||||
result := renamed.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
|
||||
// Empty string should fail
|
||||
result = renamed.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
|
||||
// Non-numeric should fail
|
||||
result = renamed.Decode("abc")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("works in pipeline with F.Pipe", func(t *testing.T) {
|
||||
codec := F.Pipe1(
|
||||
IntFromString(),
|
||||
WithName[int, string, string]("UserAge"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "UserAge", codec.Name())
|
||||
|
||||
result := codec.Decode("25")
|
||||
assert.Equal(t, validation.Success(25), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_PreservesTypeChecking(t *testing.T) {
|
||||
t.Run("preserves Is function", func(t *testing.T) {
|
||||
original := String()
|
||||
renamed := WithName[string, string, any]("CustomString")(original)
|
||||
|
||||
// Should accept string
|
||||
result := renamed.Is("hello")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should reject non-string
|
||||
result = renamed.Is(42)
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
|
||||
t.Run("preserves complex type checking", func(t *testing.T) {
|
||||
original := Array(Int())
|
||||
renamed := WithName[[]int, []int, any]("IntArray")(original)
|
||||
|
||||
// Should accept []int
|
||||
result := renamed.Is([]int{1, 2, 3})
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
// Should reject []string
|
||||
result = renamed.Is([]string{"a", "b"})
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_RoundTrip(t *testing.T) {
|
||||
t.Run("maintains round-trip property", func(t *testing.T) {
|
||||
original := Int64FromString()
|
||||
renamed := WithName[int64, string, string]("CustomInt64")(original)
|
||||
|
||||
testValues := []string{"0", "42", "-100", "9223372036854775807"}
|
||||
for _, input := range testValues {
|
||||
result := renamed.Decode(input)
|
||||
require.True(t, either.IsRight(result), "expected success for %s", input)
|
||||
|
||||
decoded := either.MonadFold(result, func(validation.Errors) int64 { return 0 }, func(n int64) int64 { return n })
|
||||
encoded := renamed.Encode(decoded)
|
||||
assert.Equal(t, input, encoded)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_ErrorMessages(t *testing.T) {
|
||||
t.Run("custom name appears in validation context", func(t *testing.T) {
|
||||
codec := WithName[int, string, string]("PositiveInteger")(IntFromString())
|
||||
|
||||
result := codec.Decode("not a number")
|
||||
require.True(t, either.IsLeft(result))
|
||||
|
||||
// The error context should reference the custom name
|
||||
errors := either.MonadFold(result, func(e validation.Errors) validation.Errors { return e }, func(int) validation.Errors { return nil })
|
||||
require.NotEmpty(t, errors)
|
||||
|
||||
// Check that at least one error references our custom name
|
||||
found := false
|
||||
for _, err := range errors {
|
||||
if len(err.Context) > 0 {
|
||||
for _, ctx := range err.Context {
|
||||
if ctx.Type == "PositiveInteger" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "expected custom name 'PositiveInteger' in error context")
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_MultipleRenames(t *testing.T) {
|
||||
t.Run("can rename multiple times", func(t *testing.T) {
|
||||
codec := IntFromString()
|
||||
|
||||
renamed1 := WithName[int, string, string]("FirstName")(codec)
|
||||
assert.Equal(t, "FirstName", renamed1.Name())
|
||||
|
||||
renamed2 := WithName[int, string, string]("SecondName")(renamed1)
|
||||
assert.Equal(t, "SecondName", renamed2.Name())
|
||||
|
||||
// Behavior should still work
|
||||
result := renamed2.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_WithDifferentTypes(t *testing.T) {
|
||||
t.Run("works with string codec", func(t *testing.T) {
|
||||
codec := WithName[string, string, string]("Username")(NonEmptyString())
|
||||
assert.Equal(t, "Username", codec.Name())
|
||||
|
||||
result := codec.Decode("john_doe")
|
||||
assert.Equal(t, validation.Success("john_doe"), result)
|
||||
})
|
||||
|
||||
t.Run("works with bool codec", func(t *testing.T) {
|
||||
codec := WithName[bool, string, string]("IsActive")(BoolFromString())
|
||||
assert.Equal(t, "IsActive", codec.Name())
|
||||
|
||||
result := codec.Decode("true")
|
||||
assert.Equal(t, validation.Success(true), result)
|
||||
})
|
||||
|
||||
t.Run("works with URL codec", func(t *testing.T) {
|
||||
codec := WithName[*url.URL, string, string]("WebsiteURL")(URL())
|
||||
assert.Equal(t, "WebsiteURL", codec.Name())
|
||||
|
||||
result := codec.Decode("https://example.com")
|
||||
assert.True(t, either.IsRight(result))
|
||||
})
|
||||
|
||||
t.Run("works with array codec", func(t *testing.T) {
|
||||
codec := WithName[[]int, []int, any]("Numbers")(Array(Int()))
|
||||
assert.Equal(t, "Numbers", codec.Name())
|
||||
|
||||
result := codec.Decode([]int{1, 2, 3})
|
||||
assert.Equal(t, validation.Success([]int{1, 2, 3}), result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_AsDecoderEncoder(t *testing.T) {
|
||||
t.Run("AsDecoder returns decoder interface", func(t *testing.T) {
|
||||
codec := WithName[int, string, string]("MyInt")(IntFromString())
|
||||
decoder := codec.AsDecoder()
|
||||
|
||||
result := decoder.Decode("42")
|
||||
assert.Equal(t, validation.Success(42), result)
|
||||
})
|
||||
|
||||
t.Run("AsEncoder returns encoder interface", func(t *testing.T) {
|
||||
codec := WithName[int, string, string]("MyInt")(IntFromString())
|
||||
encoder := codec.AsEncoder()
|
||||
|
||||
encoded := encoder.Encode(42)
|
||||
assert.Equal(t, "42", encoded)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithName_Integration(t *testing.T) {
|
||||
t.Run("domain-specific codec names", func(t *testing.T) {
|
||||
// Create domain-specific codecs with meaningful names
|
||||
emailCodec := WithName[string, string, string]("EmailAddress")(NonEmptyString())
|
||||
phoneCodec := WithName[string, string, string]("PhoneNumber")(NonEmptyString())
|
||||
ageCodec := WithName[int, string, string]("Age")(IntFromString())
|
||||
|
||||
// Test email
|
||||
result := emailCodec.Decode("user@example.com")
|
||||
assert.True(t, either.IsRight(result))
|
||||
assert.Equal(t, "EmailAddress", emailCodec.Name())
|
||||
|
||||
// Test phone
|
||||
result = phoneCodec.Decode("+1234567890")
|
||||
assert.True(t, either.IsRight(result))
|
||||
assert.Equal(t, "PhoneNumber", phoneCodec.Name())
|
||||
|
||||
// Test age
|
||||
ageResult := ageCodec.Decode("25")
|
||||
assert.True(t, either.IsRight(ageResult))
|
||||
assert.Equal(t, "Age", ageCodec.Name())
|
||||
})
|
||||
|
||||
t.Run("naming complex validation pipelines", func(t *testing.T) {
|
||||
// Create a complex codec and give it a clear name
|
||||
positiveIntCodec := F.Pipe2(
|
||||
NonEmptyString(),
|
||||
Pipe[string, string](IntFromString()),
|
||||
WithName[int, string, string]("PositiveIntegerFromString"),
|
||||
)
|
||||
|
||||
assert.Equal(t, "PositiveIntegerFromString", positiveIntCodec.Name())
|
||||
|
||||
result := positiveIntCodec.Decode("42")
|
||||
assert.True(t, either.IsRight(result))
|
||||
|
||||
result = positiveIntCodec.Decode("")
|
||||
assert.True(t, either.IsLeft(result))
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MarshalJSON
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -773,7 +1364,7 @@ func TestIntFromString_PipeComposition(t *testing.T) {
|
||||
func(n int) int { return n },
|
||||
"PositiveInt",
|
||||
)
|
||||
positiveIntCodec := Pipe[string, string, int, int](
|
||||
positiveIntCodec := Pipe[string, string](
|
||||
FromRefinement(positiveIntPrism),
|
||||
)(IntFromString())
|
||||
|
||||
|
||||
23
v2/optics/iso/generic/iso.go
Normal file
23
v2/optics/iso/generic/iso.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
I "github.com/IBM/fp-go/v2/optics/iso"
|
||||
)
|
||||
|
||||
// AsTraversal converts a iso to a traversal
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(I.Iso[S, A]) R {
|
||||
return func(sa I.Iso[S, A]) R {
|
||||
saSet := fmap(sa.ReverseGet)
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return F.Flow3(
|
||||
sa.Get,
|
||||
f,
|
||||
saSet,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,5 +23,5 @@ import (
|
||||
)
|
||||
|
||||
func AsTraversal[E, S, A any]() func(L.Lens[S, A]) T.Traversal[E, S, A] {
|
||||
return LG.AsTraversal[T.Traversal[E, S, A]](ET.MonadMap[E, A, S])
|
||||
return LG.AsTraversal[T.Traversal[E, S, A]](ET.Map[E, A, S])
|
||||
}
|
||||
|
||||
@@ -16,19 +16,24 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
L "github.com/IBM/fp-go/v2/optics/lens"
|
||||
)
|
||||
|
||||
// AsTraversal converts a lens to a traversal
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fmap func(HKTA, func(A) S) HKTS,
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(L.Lens[S, A]) R {
|
||||
return func(sa L.Lens[S, A]) R {
|
||||
return func(f func(a A) HKTA) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return fmap(f(sa.Get(s)), func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
})
|
||||
return F.Pipe1(
|
||||
f(sa.Get(s)),
|
||||
fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,5 +60,5 @@ import (
|
||||
// configs := []Config{{Timeout: O.Some(30)}, {Timeout: O.None[int]()}}
|
||||
// // Apply operations across all configs using the traversal
|
||||
func AsTraversal[S, A any]() func(Lens[S, A]) T.Traversal[S, A] {
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.MonadMap[A, S])
|
||||
return LG.AsTraversal[T.Traversal[S, A]](O.Map[A, S])
|
||||
}
|
||||
|
||||
86
v2/optics/lens/traversal/generic/identity/traversal.go
Normal file
86
v2/optics/lens/traversal/generic/identity/traversal.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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 identity
|
||||
|
||||
import (
|
||||
I "github.com/IBM/fp-go/v2/identity"
|
||||
G "github.com/IBM/fp-go/v2/optics/lens/traversal/generic"
|
||||
)
|
||||
|
||||
// Compose composes a lens with a traversal to create a new traversal.
|
||||
//
|
||||
// This function allows you to focus deeper into a data structure by first using
|
||||
// a lens to access a field, then using a traversal to access multiple values within
|
||||
// that field. The result is a traversal that can operate on all the nested values.
|
||||
//
|
||||
// The composition follows the pattern: Lens[S, A] → Traversal[A, B] → Traversal[S, B]
|
||||
// where the lens focuses on field A within structure S, and the traversal focuses on
|
||||
// multiple B values within A.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The outer structure type
|
||||
// - A: The intermediate field type (target of the lens)
|
||||
// - B: The final focus type (targets of the traversal)
|
||||
//
|
||||
// Parameters:
|
||||
// - t: A traversal that focuses on B values within A
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a Lens[S, A] and returns a Traversal[S, B]
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/optics/lens"
|
||||
// LT "github.com/IBM/fp-go/v2/optics/lens/traversal"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// type Team struct {
|
||||
// Name string
|
||||
// Members []string
|
||||
// }
|
||||
//
|
||||
// // Lens to access the Members field
|
||||
// membersLens := lens.MakeLens(
|
||||
// func(t Team) []string { return t.Members },
|
||||
// func(t Team, m []string) Team { t.Members = m; return t },
|
||||
// )
|
||||
//
|
||||
// // Traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[string]()
|
||||
//
|
||||
// // Compose lens with traversal to access all member names
|
||||
// memberTraversal := F.Pipe1(
|
||||
// membersLens,
|
||||
// LT.Compose[Team, []string, string](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// team := Team{Name: "Engineering", Members: []string{"Alice", "Bob"}}
|
||||
// // Uppercase all member names
|
||||
// updated := memberTraversal(strings.ToUpper)(team)
|
||||
// // updated.Members: ["ALICE", "BOB"]
|
||||
//
|
||||
// See Also:
|
||||
// - Lens: A functional reference to a subpart of a data structure
|
||||
// - Traversal: A functional reference to multiple subparts
|
||||
// - traversal.Compose: Composes two traversals
|
||||
func Compose[S, A, B any](t Traversal[A, B, A, B]) func(Lens[S, A]) Traversal[S, B, S, B] {
|
||||
return G.Compose[S, A, B, S, A, B](
|
||||
I.Map,
|
||||
)(t)
|
||||
}
|
||||
253
v2/optics/lens/traversal/generic/identity/traversal_test.go
Normal file
253
v2/optics/lens/traversal/generic/identity/traversal_test.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// 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 identity
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
AR "github.com/IBM/fp-go/v2/array"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type Team struct {
|
||||
Name string
|
||||
Members []string
|
||||
}
|
||||
|
||||
type Company struct {
|
||||
Name string
|
||||
Teams []Team
|
||||
}
|
||||
|
||||
func TestCompose_Success(t *testing.T) {
|
||||
t.Run("composes lens with array traversal to modify nested values", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice", "bob", "charlie"},
|
||||
}
|
||||
|
||||
// Act - uppercase all member names
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert
|
||||
expected := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"ALICE", "BOB", "CHARLIE"},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
|
||||
t.Run("composes lens with array traversal on empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{},
|
||||
}
|
||||
|
||||
// Act
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, team, result)
|
||||
})
|
||||
|
||||
t.Run("composes lens with array traversal to transform numbers", func(t *testing.T) {
|
||||
// Arrange
|
||||
type Stats struct {
|
||||
Name string
|
||||
Scores []int
|
||||
}
|
||||
|
||||
scoresLens := lens.MakeLens(
|
||||
func(s Stats) []int { return s.Scores },
|
||||
func(s Stats, scores []int) Stats {
|
||||
s.Scores = scores
|
||||
return s
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
|
||||
scoreTraversal := F.Pipe1(
|
||||
scoresLens,
|
||||
Compose[Stats, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
stats := Stats{
|
||||
Name: "Player1",
|
||||
Scores: []int{10, 20, 30},
|
||||
}
|
||||
|
||||
// Act - double all scores
|
||||
result := scoreTraversal(func(n int) int { return n * 2 })(stats)
|
||||
|
||||
// Assert
|
||||
expected := Stats{
|
||||
Name: "Player1",
|
||||
Scores: []int{20, 40, 60},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose_Integration(t *testing.T) {
|
||||
t.Run("composes multiple lenses and traversals", func(t *testing.T) {
|
||||
// Arrange - nested structure with Company -> Teams -> Members
|
||||
teamsLens := lens.MakeLens(
|
||||
func(c Company) []Team { return c.Teams },
|
||||
func(c Company, teams []Team) Company {
|
||||
c.Teams = teams
|
||||
return c
|
||||
},
|
||||
)
|
||||
|
||||
// First compose: Company -> []Team -> Team
|
||||
teamArrayTraversal := AI.FromArray[Team]()
|
||||
companyToTeamTraversal := F.Pipe1(
|
||||
teamsLens,
|
||||
Compose[Company, []Team, Team](teamArrayTraversal),
|
||||
)
|
||||
|
||||
// Second compose: Team -> []string -> string
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
memberArrayTraversal := AI.FromArray[string]()
|
||||
teamToMemberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](memberArrayTraversal),
|
||||
)
|
||||
|
||||
company := Company{
|
||||
Name: "TechCorp",
|
||||
Teams: []Team{
|
||||
{Name: "Engineering", Members: []string{"alice", "bob"}},
|
||||
{Name: "Design", Members: []string{"charlie", "diana"}},
|
||||
},
|
||||
}
|
||||
|
||||
// Act - uppercase all members in all teams
|
||||
// First traverse to teams, then for each team traverse to members
|
||||
result := companyToTeamTraversal(func(team Team) Team {
|
||||
return teamToMemberTraversal(strings.ToUpper)(team)
|
||||
})(company)
|
||||
|
||||
// Assert
|
||||
expected := Company{
|
||||
Name: "TechCorp",
|
||||
Teams: []Team{
|
||||
{Name: "Engineering", Members: []string{"ALICE", "BOB"}},
|
||||
{Name: "Design", Members: []string{"CHARLIE", "DIANA"}},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompose_EdgeCases(t *testing.T) {
|
||||
t.Run("preserves structure name when modifying members", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice"},
|
||||
}
|
||||
|
||||
// Act
|
||||
result := memberTraversal(strings.ToUpper)(team)
|
||||
|
||||
// Assert - Name should be unchanged
|
||||
assert.Equal(t, "Engineering", result.Name)
|
||||
assert.Equal(t, AR.From("ALICE"), result.Members)
|
||||
})
|
||||
|
||||
t.Run("handles identity transformation", func(t *testing.T) {
|
||||
// Arrange
|
||||
membersLens := lens.MakeLens(
|
||||
func(team Team) []string { return team.Members },
|
||||
func(team Team, members []string) Team {
|
||||
team.Members = members
|
||||
return team
|
||||
},
|
||||
)
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
|
||||
memberTraversal := F.Pipe1(
|
||||
membersLens,
|
||||
Compose[Team](arrayTraversal),
|
||||
)
|
||||
|
||||
team := Team{
|
||||
Name: "Engineering",
|
||||
Members: []string{"alice", "bob"},
|
||||
}
|
||||
|
||||
// Act - apply identity function
|
||||
result := memberTraversal(F.Identity[string])(team)
|
||||
|
||||
// Assert - should be unchanged
|
||||
assert.Equal(t, team, result)
|
||||
})
|
||||
}
|
||||
14
v2/optics/lens/traversal/generic/identity/types.go
Normal file
14
v2/optics/lens/traversal/generic/identity/types.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
|
||||
)
|
||||
25
v2/optics/lens/traversal/generic/traversal.go
Normal file
25
v2/optics/lens/traversal/generic/traversal.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
G "github.com/IBM/fp-go/v2/optics/lens/generic"
|
||||
TG "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
func Compose[S, A, B, HKTS, HKTA, HKTB any](
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
|
||||
lensTrav := G.AsTraversal[Traversal[S, A, HKTS, HKTA]](fmap)
|
||||
|
||||
return func(ab Traversal[A, B, HKTA, HKTB]) func(Lens[S, A]) Traversal[S, B, HKTS, HKTB] {
|
||||
return F.Flow2(
|
||||
lensTrav,
|
||||
TG.Compose[
|
||||
Traversal[A, B, HKTA, HKTB],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, B, HKTS, HKTB],
|
||||
](ab),
|
||||
)
|
||||
}
|
||||
}
|
||||
14
v2/optics/lens/traversal/generic/types.go
Normal file
14
v2/optics/lens/traversal/generic/types.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/optics/lens"
|
||||
T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Lens is a functional reference to a subpart of a data structure.
|
||||
Lens[S, A any] = lens.Lens[S, A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = T.Traversal[S, A, HKTS, HKTA]
|
||||
)
|
||||
79
v2/optics/optional/array/array.go
Normal file
79
v2/optics/optional/array/array.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// 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 array
|
||||
|
||||
import (
|
||||
OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
G "github.com/IBM/fp-go/v2/optics/optional/array/generic"
|
||||
)
|
||||
|
||||
// At creates an Optional that focuses on the element at a specific index in an array.
|
||||
//
|
||||
// This function returns an Optional that can get and set the element at the given index.
|
||||
// If the index is out of bounds, GetOption returns None and Set operations are no-ops
|
||||
// (the array is returned unchanged). This follows the Optional laws where operations
|
||||
// on non-existent values have no effect.
|
||||
//
|
||||
// The Optional provides safe array access without panicking on invalid indices, making
|
||||
// it ideal for functional transformations where you want to modify array elements only
|
||||
// when they exist.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - idx: The zero-based index to focus on
|
||||
//
|
||||
// Returns:
|
||||
// - An Optional that focuses on the element at the specified index
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
// OA "github.com/IBM/fp-go/v2/optics/optional/array"
|
||||
// )
|
||||
//
|
||||
// numbers := []int{10, 20, 30, 40}
|
||||
//
|
||||
// // Create an optional focusing on index 1
|
||||
// second := OA.At[int](1)
|
||||
//
|
||||
// // Get the element at index 1
|
||||
// value := second.GetOption(numbers)
|
||||
// // value: option.Some(20)
|
||||
//
|
||||
// // Set the element at index 1
|
||||
// updated := second.Set(25)(numbers)
|
||||
// // updated: []int{10, 25, 30, 40}
|
||||
//
|
||||
// // Out of bounds access returns None
|
||||
// outOfBounds := OA.At[int](10)
|
||||
// value = outOfBounds.GetOption(numbers)
|
||||
// // value: option.None[int]()
|
||||
//
|
||||
// // Out of bounds set is a no-op
|
||||
// unchanged := outOfBounds.Set(99)(numbers)
|
||||
// // unchanged: []int{10, 20, 30, 40} (original array)
|
||||
//
|
||||
// See Also:
|
||||
// - AR.Lookup: Gets an element at an index, returning an Option
|
||||
// - AR.UpdateAt: Updates an element at an index, returning an Option
|
||||
// - OP.Optional: The Optional optic type
|
||||
func At[A any](idx int) OP.Optional[[]A, A] {
|
||||
return G.At[[]A](idx)
|
||||
}
|
||||
466
v2/optics/optional/array/array_test.go
Normal file
466
v2/optics/optional/array/array_test.go
Normal file
@@ -0,0 +1,466 @@
|
||||
// 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 array
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
EQ "github.com/IBM/fp-go/v2/eq"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestAt_GetOption tests the GetOption functionality
|
||||
func TestAt_GetOption(t *testing.T) {
|
||||
t.Run("returns Some for valid index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.Some(20), result)
|
||||
})
|
||||
|
||||
t.Run("returns Some for first element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.Some(10), result)
|
||||
})
|
||||
|
||||
t.Run("returns Some for last element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](2)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.Some(30), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for negative index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](-1)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for out of bounds index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for empty array", func(t *testing.T) {
|
||||
numbers := []int{}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
|
||||
t.Run("returns None for nil array", func(t *testing.T) {
|
||||
var numbers []int
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.GetOption(numbers)
|
||||
|
||||
assert.Equal(t, O.None[int](), result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_Set tests the Set functionality
|
||||
func TestAt_Set(t *testing.T) {
|
||||
t.Run("updates element at valid index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
result := optional.Set(25)(numbers)
|
||||
|
||||
assert.Equal(t, []int{10, 25, 30, 40}, result)
|
||||
assert.Equal(t, []int{10, 20, 30, 40}, numbers) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("updates first element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.Set(5)(numbers)
|
||||
|
||||
assert.Equal(t, []int{5, 20, 30}, result)
|
||||
})
|
||||
|
||||
t.Run("updates last element", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](2)
|
||||
|
||||
result := optional.Set(35)(numbers)
|
||||
|
||||
assert.Equal(t, []int{10, 20, 35}, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for negative index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](-1)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for out of bounds index", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for empty array", func(t *testing.T) {
|
||||
numbers := []int{}
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("is no-op for nil array", func(t *testing.T) {
|
||||
var numbers []int
|
||||
optional := At[int](0)
|
||||
|
||||
result := optional.Set(99)(numbers)
|
||||
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_OptionalLaw1_GetSetNoOp tests Optional Law 1: GetSet Law (No-op on None)
|
||||
// If GetOption(s) returns None, then Set(a)(s) must return s unchanged (no-op).
|
||||
func TestAt_OptionalLaw1_GetSetNoOp(t *testing.T) {
|
||||
t.Run("out of bounds index - set is no-op", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("negative index - set is no-op", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](-1)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("empty array - set is no-op", func(t *testing.T) {
|
||||
numbers := []int{}
|
||||
optional := At[int](0)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
|
||||
t.Run("nil array - set is no-op", func(t *testing.T) {
|
||||
var numbers []int
|
||||
optional := At[int](0)
|
||||
|
||||
// Verify GetOption returns None
|
||||
assert.Equal(t, O.None[int](), optional.GetOption(numbers))
|
||||
|
||||
// Set should be a no-op
|
||||
result := optional.Set(99)(numbers)
|
||||
assert.Equal(t, numbers, result)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_OptionalLaw2_SetGet tests Optional Law 2: SetGet Law (Get what you Set)
|
||||
// If GetOption(s) returns Some(_), then GetOption(Set(a)(s)) must return Some(a).
|
||||
func TestAt_OptionalLaw2_SetGet(t *testing.T) {
|
||||
t.Run("set then get returns the set value", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
// Verify GetOption returns Some (precondition)
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
// Set a new value
|
||||
newValue := 25
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
// GetOption on updated should return Some(newValue)
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
})
|
||||
|
||||
t.Run("set first element then get", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
newValue := 5
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
})
|
||||
|
||||
t.Run("set last element then get", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](2)
|
||||
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
newValue := 35
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
})
|
||||
|
||||
t.Run("multiple indices satisfy law", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40, 50}
|
||||
|
||||
for i := range 5 {
|
||||
optional := At[int](i)
|
||||
|
||||
assert.True(t, O.IsSome(optional.GetOption(numbers)))
|
||||
|
||||
newValue := i * 100
|
||||
updated := optional.Set(newValue)(numbers)
|
||||
|
||||
result := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(newValue), result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_OptionalLaw3_SetSet tests Optional Law 3: SetSet Law (Last Set Wins)
|
||||
// Setting twice is the same as setting once with the final value.
|
||||
// Formally: Set(b)(Set(a)(s)) = Set(b)(s)
|
||||
func TestAt_OptionalLaw3_SetSet(t *testing.T) {
|
||||
eqSlice := EQ.FromEquals(func(a, b []int) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range len(a) {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
t.Run("setting twice equals setting once with final value", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
optional := At[int](1)
|
||||
|
||||
// Set twice: first to 25, then to 99
|
||||
setTwice := F.Pipe2(
|
||||
numbers,
|
||||
optional.Set(25),
|
||||
optional.Set(99),
|
||||
)
|
||||
|
||||
// Set once with final value
|
||||
setOnce := optional.Set(99)(numbers)
|
||||
|
||||
assert.True(t, eqSlice.Equals(setTwice, setOnce))
|
||||
})
|
||||
|
||||
t.Run("multiple sets - last one wins", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](0)
|
||||
|
||||
// Set multiple times
|
||||
result := F.Pipe4(
|
||||
numbers,
|
||||
optional.Set(1),
|
||||
optional.Set(2),
|
||||
optional.Set(3),
|
||||
optional.Set(4),
|
||||
)
|
||||
|
||||
// Should equal setting once with final value
|
||||
expected := optional.Set(4)(numbers)
|
||||
|
||||
assert.True(t, eqSlice.Equals(result, expected))
|
||||
})
|
||||
|
||||
t.Run("set twice on out of bounds - both no-ops", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](10)
|
||||
|
||||
// Set twice on out of bounds
|
||||
setTwice := F.Pipe2(
|
||||
numbers,
|
||||
optional.Set(25),
|
||||
optional.Set(99),
|
||||
)
|
||||
|
||||
// Set once on out of bounds
|
||||
setOnce := optional.Set(99)(numbers)
|
||||
|
||||
// Both should be no-ops, returning original
|
||||
assert.True(t, eqSlice.Equals(setTwice, numbers))
|
||||
assert.True(t, eqSlice.Equals(setOnce, numbers))
|
||||
assert.True(t, eqSlice.Equals(setTwice, setOnce))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_EdgeCases tests edge cases and boundary conditions
|
||||
func TestAt_EdgeCases(t *testing.T) {
|
||||
t.Run("single element array", func(t *testing.T) {
|
||||
numbers := []int{42}
|
||||
optional := At[int](0)
|
||||
|
||||
// Get
|
||||
assert.Equal(t, O.Some(42), optional.GetOption(numbers))
|
||||
|
||||
// Set
|
||||
updated := optional.Set(99)(numbers)
|
||||
assert.Equal(t, []int{99}, updated)
|
||||
|
||||
// Out of bounds
|
||||
outOfBounds := At[int](1)
|
||||
assert.Equal(t, O.None[int](), outOfBounds.GetOption(numbers))
|
||||
assert.Equal(t, numbers, outOfBounds.Set(99)(numbers))
|
||||
})
|
||||
|
||||
t.Run("large array", func(t *testing.T) {
|
||||
numbers := make([]int, 1000)
|
||||
for i := range 1000 {
|
||||
numbers[i] = i
|
||||
}
|
||||
|
||||
optional := At[int](500)
|
||||
|
||||
// Get
|
||||
assert.Equal(t, O.Some(500), optional.GetOption(numbers))
|
||||
|
||||
// Set
|
||||
updated := optional.Set(9999)(numbers)
|
||||
assert.Equal(t, 9999, updated[500])
|
||||
assert.Equal(t, 500, numbers[500]) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// String array
|
||||
strings := []string{"a", "b", "c"}
|
||||
strOptional := At[string](1)
|
||||
assert.Equal(t, O.Some("b"), strOptional.GetOption(strings))
|
||||
assert.Equal(t, []string{"a", "x", "c"}, strOptional.Set("x")(strings))
|
||||
|
||||
// Bool array
|
||||
bools := []bool{true, false, true}
|
||||
boolOptional := At[bool](1)
|
||||
assert.Equal(t, O.Some(false), boolOptional.GetOption(bools))
|
||||
assert.Equal(t, []bool{true, true, true}, boolOptional.Set(true)(bools))
|
||||
})
|
||||
|
||||
t.Run("preserves array capacity", func(t *testing.T) {
|
||||
numbers := make([]int, 3, 10)
|
||||
numbers[0], numbers[1], numbers[2] = 10, 20, 30
|
||||
|
||||
optional := At[int](1)
|
||||
updated := optional.Set(25)(numbers)
|
||||
|
||||
assert.Equal(t, []int{10, 25, 30}, updated)
|
||||
assert.Equal(t, 3, len(updated))
|
||||
})
|
||||
}
|
||||
|
||||
// TestAt_Integration tests integration scenarios
|
||||
func TestAt_Integration(t *testing.T) {
|
||||
t.Run("multiple optionals on same array", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30, 40}
|
||||
|
||||
first := At[int](0)
|
||||
second := At[int](1)
|
||||
third := At[int](2)
|
||||
|
||||
// Update multiple indices
|
||||
result := F.Pipe3(
|
||||
numbers,
|
||||
first.Set(1),
|
||||
second.Set(2),
|
||||
third.Set(3),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{1, 2, 3, 40}, result)
|
||||
assert.Equal(t, []int{10, 20, 30, 40}, numbers) // Original unchanged
|
||||
})
|
||||
|
||||
t.Run("chaining operations", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](1)
|
||||
|
||||
// Get, verify, set, get again
|
||||
original := optional.GetOption(numbers)
|
||||
assert.Equal(t, O.Some(20), original)
|
||||
|
||||
updated := optional.Set(25)(numbers)
|
||||
newValue := optional.GetOption(updated)
|
||||
assert.Equal(t, O.Some(25), newValue)
|
||||
|
||||
// Original still unchanged
|
||||
assert.Equal(t, O.Some(20), optional.GetOption(numbers))
|
||||
})
|
||||
|
||||
t.Run("conditional update based on current value", func(t *testing.T) {
|
||||
numbers := []int{10, 20, 30}
|
||||
optional := At[int](1)
|
||||
|
||||
// Get current value and conditionally update
|
||||
result := F.Pipe1(
|
||||
optional.GetOption(numbers),
|
||||
O.Fold(
|
||||
func() []int { return numbers },
|
||||
func(current int) []int {
|
||||
if current > 15 {
|
||||
return optional.Set(current * 2)(numbers)
|
||||
}
|
||||
return numbers
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
assert.Equal(t, []int{10, 40, 30}, result)
|
||||
})
|
||||
}
|
||||
98
v2/optics/optional/array/generic/array.go
Normal file
98
v2/optics/optional/array/generic/array.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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 generic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
AR "github.com/IBM/fp-go/v2/array/generic"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
// At creates an Optional that focuses on the element at a specific index in an array.
|
||||
//
|
||||
// This function returns an Optional that can get and set the element at the given index.
|
||||
// If the index is out of bounds, GetOption returns None and Set operations are no-ops
|
||||
// (the array is returned unchanged). This follows the Optional laws where operations
|
||||
// on non-existent values have no effect.
|
||||
//
|
||||
// The Optional provides safe array access without panicking on invalid indices, making
|
||||
// it ideal for functional transformations where you want to modify array elements only
|
||||
// when they exist.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - A: The type of elements in the array
|
||||
//
|
||||
// Parameters:
|
||||
// - idx: The zero-based index to focus on
|
||||
//
|
||||
// Returns:
|
||||
// - An Optional that focuses on the element at the specified index
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// OP "github.com/IBM/fp-go/v2/optics/optional"
|
||||
// OA "github.com/IBM/fp-go/v2/optics/optional/array"
|
||||
// )
|
||||
//
|
||||
// numbers := []int{10, 20, 30, 40}
|
||||
//
|
||||
// // Create an optional focusing on index 1
|
||||
// second := OA.At[int](1)
|
||||
//
|
||||
// // Get the element at index 1
|
||||
// value := second.GetOption(numbers)
|
||||
// // value: option.Some(20)
|
||||
//
|
||||
// // Set the element at index 1
|
||||
// updated := second.Set(25)(numbers)
|
||||
// // updated: []int{10, 25, 30, 40}
|
||||
//
|
||||
// // Out of bounds access returns None
|
||||
// outOfBounds := OA.At[int](10)
|
||||
// value = outOfBounds.GetOption(numbers)
|
||||
// // value: option.None[int]()
|
||||
//
|
||||
// // Out of bounds set is a no-op
|
||||
// unchanged := outOfBounds.Set(99)(numbers)
|
||||
// // unchanged: []int{10, 20, 30, 40} (original array)
|
||||
//
|
||||
// See Also:
|
||||
// - AR.Lookup: Gets an element at an index, returning an Option
|
||||
// - AR.UpdateAt: Updates an element at an index, returning an Option
|
||||
// - OP.Optional: The Optional optic type
|
||||
func At[GA ~[]A, A any](idx int) OP.Optional[GA, A] {
|
||||
lookup := AR.Lookup[GA](idx)
|
||||
return OP.MakeOptionalCurriedWithName(
|
||||
lookup,
|
||||
func(a A) func(GA) GA {
|
||||
update := AR.UpdateAt[GA](idx, a)
|
||||
return func(as GA) GA {
|
||||
return F.Pipe2(
|
||||
as,
|
||||
update,
|
||||
O.GetOrElse(lazy.Of(as)),
|
||||
)
|
||||
}
|
||||
},
|
||||
fmt.Sprintf("At[%d]", idx),
|
||||
)
|
||||
}
|
||||
34
v2/optics/optional/traversal.go
Normal file
34
v2/optics/optional/traversal.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package optional
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fof pointed.OfType[S, HKTS],
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Optional[S, A]) R {
|
||||
return func(sa Optional[S, A]) R {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.GetOption,
|
||||
O.Fold(
|
||||
lazy.Of(fof(s)),
|
||||
F.Flow2(
|
||||
f,
|
||||
fmap(func(a A) S {
|
||||
return sa.Set(a)(s)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,8 +310,10 @@ func TestAsTraversal(t *testing.T) {
|
||||
return Identity[Option[int]]{Value: s}
|
||||
}
|
||||
|
||||
fmap := func(ia Identity[int], f func(int) Option[int]) Identity[Option[int]] {
|
||||
return Identity[Option[int]]{Value: f(ia.Value)}
|
||||
fmap := func(f func(int) Option[int]) func(Identity[int]) Identity[Option[int]] {
|
||||
return func(ia Identity[int]) Identity[Option[int]] {
|
||||
return Identity[Option[int]]{Value: f(ia.Value)}
|
||||
}
|
||||
}
|
||||
|
||||
type TraversalFunc func(func(int) Identity[int]) func(Option[int]) Identity[Option[int]]
|
||||
|
||||
@@ -17,6 +17,9 @@ package prism
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/lazy"
|
||||
O "github.com/IBM/fp-go/v2/option"
|
||||
)
|
||||
|
||||
@@ -58,24 +61,23 @@ import (
|
||||
// higher-kinded types and applicative functors. Most users will work
|
||||
// directly with prisms rather than converting them to traversals.
|
||||
func AsTraversal[R ~func(func(A) HKTA) func(S) HKTS, S, A, HKTS, HKTA any](
|
||||
fof func(S) HKTS,
|
||||
fmap func(HKTA, func(A) S) HKTS,
|
||||
fof pointed.OfType[S, HKTS],
|
||||
fmap functor.MapType[A, S, HKTA, HKTS],
|
||||
) func(Prism[S, A]) R {
|
||||
return func(sa Prism[S, A]) R {
|
||||
return func(f func(a A) HKTA) func(S) HKTS {
|
||||
return func(f func(A) HKTA) func(S) HKTS {
|
||||
return func(s S) HKTS {
|
||||
return F.Pipe2(
|
||||
s,
|
||||
sa.GetOption,
|
||||
O.Fold(
|
||||
// If prism doesn't match, return the original value lifted into HKTS
|
||||
F.Nullary2(F.Constant(s), fof),
|
||||
// If prism matches, apply f to the extracted value and map back
|
||||
func(a A) HKTS {
|
||||
return fmap(f(a), func(a A) S {
|
||||
return prismModify(F.Constant1[A](a), sa, s)
|
||||
})
|
||||
},
|
||||
lazy.Of(fof(s)),
|
||||
F.Flow2(
|
||||
f,
|
||||
fmap(func(a A) S {
|
||||
return Set[S](a)(sa)(s)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,6 @@ import (
|
||||
)
|
||||
|
||||
// FromArray returns a traversal from an array for the identity [Monoid]
|
||||
func FromArray[E, A any](m M.Monoid[E]) G.Traversal[[]A, A, C.Const[E, []A], C.Const[E, A]] {
|
||||
func FromArray[A, E any](m M.Monoid[E]) G.Traversal[[]A, A, C.Const[E, []A], C.Const[E, A]] {
|
||||
return AR.FromArray[[]A](m)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,51 @@ import (
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
// FromArray returns a traversal from an array for the identity monad
|
||||
// FromArray creates a traversal for array elements using the Identity functor.
|
||||
//
|
||||
// This is a specialized version of the generic FromArray that uses the Identity
|
||||
// functor, which provides the simplest possible computational context (no context).
|
||||
// This makes it ideal for straightforward array transformations where you want to
|
||||
// modify elements directly without additional effects.
|
||||
//
|
||||
// The Identity functor means that operations are applied directly to values without
|
||||
// wrapping them in any additional structure. This results in clean, efficient
|
||||
// traversals that simply map functions over array elements.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Array type constraint (e.g., []A)
|
||||
// - A: The element type within the array
|
||||
//
|
||||
// Returns:
|
||||
// - A Traversal that can transform all elements in an array
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
// TI "github.com/IBM/fp-go/v2/optics/traversal/array/generic/identity"
|
||||
// )
|
||||
//
|
||||
// // Create a traversal for integer arrays
|
||||
// arrayTraversal := TI.FromArray[[]int, int]()
|
||||
//
|
||||
// // Compose with identity traversal
|
||||
// traversal := F.Pipe1(
|
||||
// T.Id[[]int, []int](),
|
||||
// T.Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// // Double all numbers in the array
|
||||
// numbers := []int{1, 2, 3, 4, 5}
|
||||
// doubled := traversal(func(n int) int { return n * 2 })(numbers)
|
||||
// // doubled: []int{2, 4, 6, 8, 10}
|
||||
//
|
||||
// See Also:
|
||||
// - AR.FromArray: Generic version with configurable functor
|
||||
// - I.Of: Identity functor's pure/of operation
|
||||
// - I.Map: Identity functor's map operation
|
||||
// - I.Ap: Identity functor's applicative operation
|
||||
func FromArray[GA ~[]A, A any]() G.Traversal[GA, A, GA, A] {
|
||||
return AR.FromArray[GA](
|
||||
I.Of[GA],
|
||||
@@ -29,3 +73,75 @@ func FromArray[GA ~[]A, A any]() G.Traversal[GA, A, GA, A] {
|
||||
I.Ap[GA, A],
|
||||
)
|
||||
}
|
||||
|
||||
// At creates a function that focuses a traversal on a specific array index using the Identity functor.
|
||||
//
|
||||
// This is a specialized version of the generic At that uses the Identity functor,
|
||||
// providing the simplest computational context for array element access. It transforms
|
||||
// a traversal focusing on an array into a traversal focusing on the element at the
|
||||
// specified index.
|
||||
//
|
||||
// The Identity functor means operations are applied directly without additional wrapping,
|
||||
// making this ideal for straightforward element modifications. If the index is out of
|
||||
// bounds, the traversal focuses on zero elements (no-op).
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Array type constraint (e.g., []A)
|
||||
// - S: The source type of the outer traversal
|
||||
// - A: The element type within the array
|
||||
//
|
||||
// Parameters:
|
||||
// - idx: The zero-based index to focus on
|
||||
//
|
||||
// Returns:
|
||||
// - A function that transforms a traversal on arrays into a traversal on a specific element
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
// TI "github.com/IBM/fp-go/v2/optics/traversal/array/generic/identity"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Hobbies []string
|
||||
// }
|
||||
//
|
||||
// // Create a traversal focusing on hobbies
|
||||
// hobbiesTraversal := T.Id[Person, []string]()
|
||||
//
|
||||
// // Focus on the second hobby (index 1)
|
||||
// secondHobby := F.Pipe1(
|
||||
// hobbiesTraversal,
|
||||
// TI.At[[]string, Person, string](1),
|
||||
// )
|
||||
//
|
||||
// // Modify the second hobby
|
||||
// person := Person{Name: "Alice", Hobbies: []string{"reading", "coding", "gaming"}}
|
||||
// updated := secondHobby(func(s string) string {
|
||||
// return s + "!"
|
||||
// })(person)
|
||||
// // updated.Hobbies: []string{"reading", "coding!", "gaming"}
|
||||
//
|
||||
// // Out of bounds index is a no-op
|
||||
// outOfBounds := F.Pipe1(
|
||||
// hobbiesTraversal,
|
||||
// TI.At[[]string, Person, string](10),
|
||||
// )
|
||||
// unchanged := outOfBounds(func(s string) string {
|
||||
// return s + "!"
|
||||
// })(person)
|
||||
// // unchanged.Hobbies: []string{"reading", "coding", "gaming"} (no change)
|
||||
//
|
||||
// See Also:
|
||||
// - AR.At: Generic version with configurable functor
|
||||
// - I.Of: Identity functor's pure/of operation
|
||||
// - I.Map: Identity functor's map operation
|
||||
func At[GA ~[]A, S, A any](idx int) func(G.Traversal[S, GA, S, GA]) G.Traversal[S, A, S, A] {
|
||||
return AR.At[GA, S, A, S](
|
||||
I.Of[GA],
|
||||
I.Map[A, GA],
|
||||
)(idx)
|
||||
}
|
||||
|
||||
@@ -16,19 +16,105 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/apply"
|
||||
AR "github.com/IBM/fp-go/v2/internal/array"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/optics/optional"
|
||||
OA "github.com/IBM/fp-go/v2/optics/optional/array/generic"
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
// FromArray returns a traversal from an array
|
||||
func FromArray[GA ~[]A, GB ~[]B, A, B, HKTB, HKTAB, HKTRB any](
|
||||
fof func(GB) HKTRB,
|
||||
fmap func(func(GB) func(B) GB) func(HKTRB) HKTAB,
|
||||
fap func(HKTB) func(HKTAB) HKTRB,
|
||||
fof pointed.OfType[GB, HKTRB],
|
||||
fmap functor.MapType[GB, func(B) GB, HKTRB, HKTAB],
|
||||
fap apply.ApType[HKTB, HKTRB, HKTAB],
|
||||
) G.Traversal[GA, A, HKTRB, HKTB] {
|
||||
return func(f func(A) HKTB) func(s GA) HKTRB {
|
||||
return func(s GA) HKTRB {
|
||||
return AR.MonadTraverse(fof, fmap, fap, s, f)
|
||||
}
|
||||
return func(f func(A) HKTB) func(GA) HKTRB {
|
||||
return AR.Traverse[GA](fof, fmap, fap, f)
|
||||
}
|
||||
}
|
||||
|
||||
// At creates a function that focuses a traversal on a specific array index.
|
||||
//
|
||||
// This function takes an index and returns a function that transforms a traversal
|
||||
// focusing on an array into a traversal focusing on the element at that index.
|
||||
// It works by:
|
||||
// 1. Creating an Optional that focuses on the array element at the given index
|
||||
// 2. Converting that Optional into a Traversal
|
||||
// 3. Composing it with the original traversal
|
||||
//
|
||||
// If the index is out of bounds, the traversal will focus on zero elements (no-op),
|
||||
// following the Optional laws where operations on non-existent values have no effect.
|
||||
//
|
||||
// This is particularly useful when you have a nested structure containing arrays
|
||||
// and want to traverse to a specific element within those arrays.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - GA: Array type constraint (e.g., []A)
|
||||
// - S: The source type of the outer traversal
|
||||
// - A: The element type within the array
|
||||
// - HKTS: Higher-kinded type for S (functor/applicative context)
|
||||
// - HKTGA: Higher-kinded type for GA (functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift GA into the higher-kinded type HKTGA (pure/of operation)
|
||||
// - fmap: Function to map over HKTA and produce HKTGA (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes an index and returns a traversal transformer
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/identity"
|
||||
// T "github.com/IBM/fp-go/v2/optics/traversal"
|
||||
// TA "github.com/IBM/fp-go/v2/optics/traversal/array/generic"
|
||||
// )
|
||||
//
|
||||
// type Person struct {
|
||||
// Name string
|
||||
// Hobbies []string
|
||||
// }
|
||||
//
|
||||
// // Create a traversal focusing on the hobbies array
|
||||
// hobbiesTraversal := T.Id[Person, []string]()
|
||||
//
|
||||
// // Focus on the first hobby (index 0)
|
||||
// firstHobby := F.Pipe1(
|
||||
// hobbiesTraversal,
|
||||
// TA.At[[]string, Person, string](
|
||||
// identity.Of[[]string],
|
||||
// identity.Map[string, []string],
|
||||
// )(0),
|
||||
// )
|
||||
//
|
||||
// // Modify the first hobby
|
||||
// person := Person{Name: "Alice", Hobbies: []string{"reading", "coding"}}
|
||||
// updated := firstHobby(func(s string) string {
|
||||
// return s + "!"
|
||||
// })(person)
|
||||
// // updated.Hobbies: []string{"reading!", "coding"}
|
||||
//
|
||||
// See Also:
|
||||
// - OA.At: Creates an Optional focusing on an array element
|
||||
// - optional.AsTraversal: Converts an Optional to a Traversal
|
||||
// - G.Compose: Composes two traversals
|
||||
func At[GA ~[]A, S, A, HKTS, HKTGA, HKTA any](
|
||||
fof pointed.OfType[GA, HKTGA],
|
||||
fmap functor.MapType[A, GA, HKTA, HKTGA],
|
||||
) func(int) func(G.Traversal[S, GA, HKTS, HKTGA]) G.Traversal[S, A, HKTS, HKTA] {
|
||||
return F.Flow3(
|
||||
OA.At[GA],
|
||||
optional.AsTraversal[G.Traversal[GA, A, HKTGA, HKTA]](fof, fmap),
|
||||
G.Compose[
|
||||
G.Traversal[GA, A, HKTGA, HKTA],
|
||||
G.Traversal[S, GA, HKTS, HKTGA],
|
||||
G.Traversal[S, A, HKTS, HKTA],
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@ package generic
|
||||
import (
|
||||
AR "github.com/IBM/fp-go/v2/array/generic"
|
||||
C "github.com/IBM/fp-go/v2/constant"
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
"github.com/IBM/fp-go/v2/optics/prism"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -47,7 +52,7 @@ func FromTraversable[
|
||||
}
|
||||
|
||||
// FoldMap maps each target to a `Monoid` and combines the result
|
||||
func FoldMap[M, S, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
func FoldMap[S, M, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return F.Flow2(
|
||||
F.Pipe1(
|
||||
@@ -61,13 +66,84 @@ func FoldMap[M, S, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.
|
||||
|
||||
// Fold maps each target to a `Monoid` and combines the result
|
||||
func Fold[S, A any](sa Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
|
||||
return FoldMap[A, S](F.Identity[A])(sa)
|
||||
return FoldMap[S](F.Identity[A])(sa)
|
||||
}
|
||||
|
||||
// GetAll gets all the targets of a traversal
|
||||
func GetAll[GA ~[]A, S, A any](s S) func(sa Traversal[S, A, C.Const[GA, S], C.Const[GA, A]]) GA {
|
||||
fmap := FoldMap[GA, S](AR.Of[GA, A])
|
||||
fmap := FoldMap[S](AR.Of[GA, A])
|
||||
return func(sa Traversal[S, A, C.Const[GA, S], C.Const[GA, A]]) GA {
|
||||
return fmap(sa)(s)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter creates a function that filters the targets of a traversal based on a predicate.
|
||||
//
|
||||
// This function allows you to refine a traversal to only focus on values that satisfy
|
||||
// a given predicate. It works by converting the predicate into a prism, then converting
|
||||
// that prism into a traversal, and finally composing it with the original traversal.
|
||||
//
|
||||
// The filtering is selective: when modifying values through the filtered traversal,
|
||||
// only values that satisfy the predicate will be transformed. Values that don't
|
||||
// satisfy the predicate remain unchanged.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The focus type (the values being filtered)
|
||||
// - HKTS: Higher-kinded type for S (functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift A into the higher-kinded type HKTA (pure/of operation)
|
||||
// - fmap: Function to map over HKTA (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a predicate and returns an endomorphism on traversals
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/identity"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// // Create a traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[int]()
|
||||
// baseTraversal := F.Pipe1(
|
||||
// Id[[]int, []int](),
|
||||
// Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// // Filter to only positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// filteredTraversal := F.Pipe1(
|
||||
// baseTraversal,
|
||||
// Filter[[]int, int](identity.Of[int], identity.Map[int, int])(isPositive),
|
||||
// )
|
||||
//
|
||||
// // Double only positive numbers
|
||||
// numbers := []int{-2, -1, 0, 1, 2, 3}
|
||||
// result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
|
||||
// // result: [-2, -1, 0, 2, 4, 6]
|
||||
//
|
||||
// See Also:
|
||||
// - prism.FromPredicate: Creates a prism from a predicate
|
||||
// - prism.AsTraversal: Converts a prism to a traversal
|
||||
// - Compose: Composes two traversals
|
||||
func Filter[
|
||||
S, HKTS, A, HKTA any](
|
||||
fof pointed.OfType[A, HKTA],
|
||||
fmap functor.MapType[A, A, HKTA, HKTA],
|
||||
) func(predicate.Predicate[A]) endomorphism.Endomorphism[Traversal[S, A, HKTS, HKTA]] {
|
||||
return F.Flow3(
|
||||
prism.FromPredicate,
|
||||
prism.AsTraversal[Traversal[A, A, HKTA, HKTA]](fof, fmap),
|
||||
Compose[
|
||||
Traversal[A, A, HKTA, HKTA],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, A, HKTS, HKTA]],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,46 +18,110 @@ package traversal
|
||||
import (
|
||||
C "github.com/IBM/fp-go/v2/constant"
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
"github.com/IBM/fp-go/v2/identity"
|
||||
"github.com/IBM/fp-go/v2/internal/functor"
|
||||
"github.com/IBM/fp-go/v2/internal/pointed"
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
)
|
||||
|
||||
// Id is the identity constructor of a traversal
|
||||
func Id[S, A any]() G.Traversal[S, S, A, A] {
|
||||
func Id[S, A any]() Traversal[S, S, A, A] {
|
||||
return F.Identity[func(S) A]
|
||||
}
|
||||
|
||||
// Modify applies a transformation function to a traversal
|
||||
func Modify[S, A any](f func(A) A) func(sa G.Traversal[S, A, S, A]) func(S) S {
|
||||
return func(sa G.Traversal[S, A, S, A]) func(S) S {
|
||||
return sa(f)
|
||||
}
|
||||
func Modify[S, A any](f Endomorphism[A]) func(Traversal[S, A, S, A]) Endomorphism[S] {
|
||||
return identity.Flap[Endomorphism[S]](f)
|
||||
}
|
||||
|
||||
// Set sets a constant value for all values of the traversal
|
||||
func Set[S, A any](a A) func(sa G.Traversal[S, A, S, A]) func(S) S {
|
||||
func Set[S, A any](a A) func(Traversal[S, A, S, A]) Endomorphism[S] {
|
||||
return Modify[S](F.Constant1[A](a))
|
||||
}
|
||||
|
||||
// FoldMap maps each target to a `Monoid` and combines the result
|
||||
func FoldMap[M, S, A any](f func(A) M) func(sa G.Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return G.FoldMap[M, S](f)
|
||||
func FoldMap[S, M, A any](f func(A) M) func(sa Traversal[S, A, C.Const[M, S], C.Const[M, A]]) func(S) M {
|
||||
return G.FoldMap[S](f)
|
||||
}
|
||||
|
||||
// Fold maps each target to a `Monoid` and combines the result
|
||||
func Fold[S, A any](sa G.Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
|
||||
func Fold[S, A any](sa Traversal[S, A, C.Const[A, S], C.Const[A, A]]) func(S) A {
|
||||
return G.Fold(sa)
|
||||
}
|
||||
|
||||
// GetAll gets all the targets of a traversal
|
||||
func GetAll[S, A any](s S) func(sa G.Traversal[S, A, C.Const[[]A, S], C.Const[[]A, A]]) []A {
|
||||
func GetAll[A, S any](s S) func(sa Traversal[S, A, C.Const[[]A, S], C.Const[[]A, A]]) []A {
|
||||
return G.GetAll[[]A](s)
|
||||
}
|
||||
|
||||
// Compose composes two traversables
|
||||
func Compose[
|
||||
S, A, B, HKTS, HKTA, HKTB any](ab G.Traversal[A, B, HKTA, HKTB]) func(sa G.Traversal[S, A, HKTS, HKTA]) G.Traversal[S, B, HKTS, HKTB] {
|
||||
S, HKTS, A, B, HKTA, HKTB any](ab Traversal[A, B, HKTA, HKTB]) func(Traversal[S, A, HKTS, HKTA]) Traversal[S, B, HKTS, HKTB] {
|
||||
return G.Compose[
|
||||
G.Traversal[A, B, HKTA, HKTB],
|
||||
G.Traversal[S, A, HKTS, HKTA],
|
||||
G.Traversal[S, B, HKTS, HKTB]](ab)
|
||||
Traversal[A, B, HKTA, HKTB],
|
||||
Traversal[S, A, HKTS, HKTA],
|
||||
Traversal[S, B, HKTS, HKTB]](ab)
|
||||
}
|
||||
|
||||
// Filter creates a function that filters the targets of a traversal based on a predicate.
|
||||
//
|
||||
// This function allows you to refine a traversal to only focus on values that satisfy
|
||||
// a given predicate. It works by converting the predicate into a prism, then converting
|
||||
// that prism into a traversal, and finally composing it with the original traversal.
|
||||
//
|
||||
// The filtering is selective: when modifying values through the filtered traversal,
|
||||
// only values that satisfy the predicate will be transformed. Values that don't
|
||||
// satisfy the predicate remain unchanged.
|
||||
//
|
||||
// Type Parameters:
|
||||
// - S: The source type
|
||||
// - A: The focus type (the values being filtered)
|
||||
// - HKTS: Higher-kinded type for S (functor/applicative context)
|
||||
// - HKTA: Higher-kinded type for A (functor/applicative context)
|
||||
//
|
||||
// Parameters:
|
||||
// - fof: Function to lift A into the higher-kinded type HKTA (pure/of operation)
|
||||
// - fmap: Function to map over HKTA (functor map operation)
|
||||
//
|
||||
// Returns:
|
||||
// - A function that takes a predicate and returns an endomorphism on traversals
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// import (
|
||||
// AR "github.com/IBM/fp-go/v2/array"
|
||||
// F "github.com/IBM/fp-go/v2/function"
|
||||
// "github.com/IBM/fp-go/v2/identity"
|
||||
// N "github.com/IBM/fp-go/v2/number"
|
||||
// AI "github.com/IBM/fp-go/v2/optics/traversal/array/identity"
|
||||
// )
|
||||
//
|
||||
// // Create a traversal for array elements
|
||||
// arrayTraversal := AI.FromArray[int]()
|
||||
// baseTraversal := F.Pipe1(
|
||||
// Id[[]int, []int](),
|
||||
// Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
// )
|
||||
//
|
||||
// // Filter to only positive numbers
|
||||
// isPositive := N.MoreThan(0)
|
||||
// filteredTraversal := F.Pipe1(
|
||||
// baseTraversal,
|
||||
// Filter[[]int, int](identity.Of[int], identity.Map[int, int])(isPositive),
|
||||
// )
|
||||
//
|
||||
// // Double only positive numbers
|
||||
// numbers := []int{-2, -1, 0, 1, 2, 3}
|
||||
// result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
|
||||
// // result: [-2, -1, 0, 2, 4, 6]
|
||||
//
|
||||
// See Also:
|
||||
// - prism.FromPredicate: Creates a prism from a predicate
|
||||
// - prism.AsTraversal: Converts a prism to a traversal
|
||||
// - Compose: Composes two traversals
|
||||
func Filter[S, HKTS, A, HKTA any](
|
||||
fof pointed.OfType[A, HKTA],
|
||||
fmap functor.MapType[A, A, HKTA, HKTA],
|
||||
) func(Predicate[A]) Endomorphism[Traversal[S, A, HKTS, HKTA]] {
|
||||
return G.Filter[S, HKTS](fof, fmap)
|
||||
}
|
||||
|
||||
@@ -32,14 +32,14 @@ func TestGetAll(t *testing.T) {
|
||||
|
||||
as := AR.From(1, 2, 3)
|
||||
|
||||
tr := AT.FromArray[[]int, int](AR.Monoid[int]())
|
||||
tr := AT.FromArray[int](AR.Monoid[int]())
|
||||
|
||||
sa := F.Pipe1(
|
||||
Id[[]int, C.Const[[]int, []int]](),
|
||||
Compose[[]int, []int, int, C.Const[[]int, []int]](tr),
|
||||
Compose[[]int, C.Const[[]int, []int], []int, int](tr),
|
||||
)
|
||||
|
||||
getall := GetAll[[]int, int](as)(sa)
|
||||
getall := GetAll[int](as)(sa)
|
||||
|
||||
assert.Equal(t, AR.From(1, 2, 3), getall)
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func TestFold(t *testing.T) {
|
||||
|
||||
sa := F.Pipe1(
|
||||
Id[[]int, C.Const[int, []int]](),
|
||||
Compose[[]int, []int, int, C.Const[int, []int]](tr),
|
||||
Compose[[]int, C.Const[int, []int], []int, int](tr),
|
||||
)
|
||||
|
||||
folded := Fold(sa)(as)
|
||||
@@ -70,10 +70,245 @@ func TestTraverse(t *testing.T) {
|
||||
|
||||
sa := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, int, []int](tr),
|
||||
Compose[[]int, []int, []int, int](tr),
|
||||
)
|
||||
|
||||
res := sa(utils.Double)(as)
|
||||
|
||||
assert.Equal(t, AR.From(2, 4, 6), res)
|
||||
}
|
||||
|
||||
func TestFilter_Success(t *testing.T) {
|
||||
t.Run("filters and modifies only matching elements", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{-2, -1, 0, 1, 2, 3}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter to only positive numbers
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act - double only positive numbers
|
||||
result := filteredTraversal(func(n int) int { return n * 2 })(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{-2, -1, 0, 2, 4, 6}, result)
|
||||
})
|
||||
|
||||
t.Run("filters even numbers and triples them", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5, 6}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter to only even numbers
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isEven),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(func(n int) int { return n * 3 })(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{1, 6, 3, 12, 5, 18}, result)
|
||||
})
|
||||
|
||||
t.Run("filters strings by length", func(t *testing.T) {
|
||||
// Arrange
|
||||
words := []string{"a", "ab", "abc", "abcd", "abcde"}
|
||||
arrayTraversal := AI.FromArray[string]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]string, []string](),
|
||||
Compose[[]string, []string, []string, string](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter strings with length > 2
|
||||
longerThanTwo := func(s string) bool { return len(s) > 2 }
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]string, []string, string, string](F.Identity[string], F.Identity[func(string) string])(longerThanTwo),
|
||||
)
|
||||
|
||||
// Act - convert to uppercase
|
||||
result := filteredTraversal(func(s string) string {
|
||||
return s + "!"
|
||||
})(words)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []string{"a", "ab", "abc!", "abcd!", "abcde!"}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_EdgeCases(t *testing.T) {
|
||||
t.Run("empty array returns empty array", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{}, result)
|
||||
})
|
||||
|
||||
t.Run("no elements match predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{-5, -4, -3, -2, -1}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert - all elements unchanged
|
||||
assert.Equal(t, []int{-5, -4, -3, -2, -1}, result)
|
||||
})
|
||||
|
||||
t.Run("all elements match predicate", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert - all elements doubled
|
||||
assert.Equal(t, []int{2, 4, 6, 8, 10}, result)
|
||||
})
|
||||
|
||||
t.Run("single element matching", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{42}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{84}, result)
|
||||
})
|
||||
|
||||
t.Run("single element not matching", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{-42}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
isPositive := N.MoreThan(0)
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isPositive),
|
||||
)
|
||||
|
||||
// Act
|
||||
result := filteredTraversal(utils.Double)(numbers)
|
||||
|
||||
// Assert
|
||||
assert.Equal(t, []int{-42}, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFilter_Integration(t *testing.T) {
|
||||
t.Run("multiple filters composed", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
// Filter to only even numbers, then only those > 4
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
greaterThanFour := N.MoreThan(4)
|
||||
|
||||
filteredTraversal := F.Pipe2(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isEven),
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(greaterThanFour),
|
||||
)
|
||||
|
||||
// Act - add 100 to matching elements
|
||||
result := filteredTraversal(func(n int) int { return n + 100 })(numbers)
|
||||
|
||||
// Assert - only 6, 8, 10 should be modified
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5, 106, 7, 108, 9, 110}, result)
|
||||
})
|
||||
|
||||
t.Run("filter with identity transformation", func(t *testing.T) {
|
||||
// Arrange
|
||||
numbers := []int{1, 2, 3, 4, 5}
|
||||
arrayTraversal := AI.FromArray[int]()
|
||||
baseTraversal := F.Pipe1(
|
||||
Id[[]int, []int](),
|
||||
Compose[[]int, []int, []int, int](arrayTraversal),
|
||||
)
|
||||
|
||||
isEven := func(n int) bool { return n%2 == 0 }
|
||||
filteredTraversal := F.Pipe1(
|
||||
baseTraversal,
|
||||
Filter[[]int, []int, int, int](F.Identity[int], F.Identity[func(int) int])(isEven),
|
||||
)
|
||||
|
||||
// Act - identity transformation
|
||||
result := filteredTraversal(F.Identity[int])(numbers)
|
||||
|
||||
// Assert - array unchanged
|
||||
assert.Equal(t, []int{1, 2, 3, 4, 5}, result)
|
||||
})
|
||||
}
|
||||
|
||||
15
v2/optics/traversal/types.go
Normal file
15
v2/optics/traversal/types.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package traversal
|
||||
|
||||
import (
|
||||
"github.com/IBM/fp-go/v2/endomorphism"
|
||||
G "github.com/IBM/fp-go/v2/optics/traversal/generic"
|
||||
"github.com/IBM/fp-go/v2/predicate"
|
||||
)
|
||||
|
||||
type (
|
||||
Endomorphism[A any] = endomorphism.Endomorphism[A]
|
||||
|
||||
Traversal[S, A, HKTS, HKTA any] = G.Traversal[S, A, HKTS, HKTA]
|
||||
|
||||
Predicate[A any] = predicate.Predicate[A]
|
||||
)
|
||||
@@ -16,6 +16,7 @@
|
||||
package generic
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"sort"
|
||||
|
||||
F "github.com/IBM/fp-go/v2/function"
|
||||
@@ -301,13 +302,8 @@ func unionLast[M ~map[K]V, K comparable, V any](left, right M) M {
|
||||
|
||||
result := make(M, lenLeft+lenRight)
|
||||
|
||||
for k, v := range left {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
for k, v := range right {
|
||||
result[k] = v
|
||||
}
|
||||
maps.Copy(result, left)
|
||||
maps.Copy(result, right)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user