1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-04-11 15:29:06 +02:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Dr. Carsten Leue
45cc0a7fc1 fix: better traversal support
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-10 17:24:08 +02:00
Dr. Carsten Leue
21b517d388 fix: better doc and NonEmptyString
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-04-09 14:51:39 +02:00
29 changed files with 2751 additions and 59 deletions

View File

@@ -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)
}

View File

@@ -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))
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,
)
}
}

View File

@@ -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())

View 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,
)
}
}
}

View File

@@ -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])
}

View File

@@ -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)
}),
)
}
}
}

View File

@@ -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])
}

View 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)
}

View 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)
})
}

View 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]
)

View 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),
)
}
}

View 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]
)

View 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)
}

View 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)
})
}

View 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),
)
}

View 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)
}),
),
),
)
}
}
}
}

View File

@@ -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]]

View File

@@ -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)
}),
),
),
)
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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],
],
)
}

View File

@@ -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]],
)
}

View File

@@ -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)
}

View File

@@ -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)
})
}

View 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]
)

View File

@@ -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
}