mirror of
https://github.com/IBM/fp-go.git
synced 2026-03-12 13:36:56 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0ec0b2541 | ||
|
|
ce3c7d9359 | ||
|
|
3ed354cc8c |
@@ -40,7 +40,7 @@
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Compose them (RIGHT-TO-LEFT execution)
|
||||
// composed := endomorphism.Compose(double, increment)
|
||||
// composed := endomorphism.MonadCompose(double, increment)
|
||||
// result := composed(5) // increment(5) then double: (5 + 1) * 2 = 12
|
||||
//
|
||||
// // Chain them (LEFT-TO-RIGHT execution)
|
||||
@@ -61,11 +61,11 @@
|
||||
// monoid := endomorphism.Monoid[int]()
|
||||
//
|
||||
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||
// combined := M.ConcatAll(monoid)(
|
||||
// combined := M.ConcatAll(monoid)([]endomorphism.Endomorphism[int]{
|
||||
// N.Mul(2), // applied third
|
||||
// N.Add(1), // applied second
|
||||
// N.Mul(3), // applied first
|
||||
// )
|
||||
// })
|
||||
// result := combined(5) // (5 * 3) = 15, (15 + 1) = 16, (16 * 2) = 32
|
||||
//
|
||||
// # Monad Operations
|
||||
@@ -87,7 +87,7 @@
|
||||
// increment := N.Add(1)
|
||||
//
|
||||
// // Compose: RIGHT-TO-LEFT (mathematical composition)
|
||||
// composed := endomorphism.Compose(double, increment)
|
||||
// composed := endomorphism.MonadCompose(double, increment)
|
||||
// result1 := composed(5) // increment(5) * 2 = (5 + 1) * 2 = 12
|
||||
//
|
||||
// // MonadChain: LEFT-TO-RIGHT (sequential application)
|
||||
|
||||
@@ -111,15 +111,19 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
// This is the functor map operation for endomorphisms.
|
||||
//
|
||||
// IMPORTANT: Execution order is RIGHT-TO-LEFT:
|
||||
// - g is applied first to the input
|
||||
// - ma is applied first to the input
|
||||
// - f is applied to the result
|
||||
//
|
||||
// Note: unlike most other packages where MonadMap takes (fa, f) with the container
|
||||
// first, here f (the morphism) comes first to match the right-to-left composition
|
||||
// convention: MonadMap(f, ma) = f ∘ ma.
|
||||
//
|
||||
// Parameters:
|
||||
// - f: The function to map (outer function)
|
||||
// - g: The endomorphism to map over (inner function)
|
||||
// - f: The function to map (outer function, applied second)
|
||||
// - ma: The endomorphism to map over (inner function, applied first)
|
||||
//
|
||||
// Returns:
|
||||
// - A new endomorphism that applies g, then f
|
||||
// - A new endomorphism that applies ma, then f
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
@@ -127,8 +131,8 @@ func MonadCompose[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
// increment := N.Add(1)
|
||||
// mapped := endomorphism.MonadMap(double, increment)
|
||||
// // mapped(5) = double(increment(5)) = double(6) = 12
|
||||
func MonadMap[A any](f, g Endomorphism[A]) Endomorphism[A] {
|
||||
return MonadCompose(f, g)
|
||||
func MonadMap[A any](f, ma Endomorphism[A]) Endomorphism[A] {
|
||||
return MonadCompose(f, ma)
|
||||
}
|
||||
|
||||
// Compose returns a function that composes an endomorphism with another, executing right to left.
|
||||
@@ -386,3 +390,91 @@ func Join[A any](f Kleisli[A]) Endomorphism[A] {
|
||||
return f(a)(a)
|
||||
}
|
||||
}
|
||||
|
||||
// Read captures a value and returns a function that applies endomorphisms to it.
|
||||
//
|
||||
// This function implements a "reader" pattern for endomorphisms. It takes a value
|
||||
// and returns a function that can apply any endomorphism to that captured value.
|
||||
// This is useful for creating reusable evaluation contexts where you want to apply
|
||||
// different transformations to the same initial value.
|
||||
//
|
||||
// The returned function has the signature func(Endomorphism[A]) A, which means
|
||||
// it takes an endomorphism and returns the result of applying that endomorphism
|
||||
// to the captured value.
|
||||
//
|
||||
// # Type Parameters
|
||||
//
|
||||
// - A: The type of the value being captured and transformed
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// - a: The value to capture for later transformation
|
||||
//
|
||||
// # Returns
|
||||
//
|
||||
// - A function that applies endomorphisms to the captured value
|
||||
//
|
||||
// # Example - Basic Usage
|
||||
//
|
||||
// // Capture a value
|
||||
// applyTo5 := Read(5)
|
||||
//
|
||||
// // Apply different endomorphisms to the same value
|
||||
// doubled := applyTo5(N.Mul(2)) // 10
|
||||
// incremented := applyTo5(N.Add(1)) // 6
|
||||
// squared := applyTo5(func(x int) int { return x * x }) // 25
|
||||
//
|
||||
// # Example - Reusable Evaluation Context
|
||||
//
|
||||
// type Config struct {
|
||||
// Timeout int
|
||||
// Retries int
|
||||
// }
|
||||
//
|
||||
// baseConfig := Config{Timeout: 30, Retries: 3}
|
||||
// applyToBase := Read(baseConfig)
|
||||
//
|
||||
// // Apply different transformations to the same base config
|
||||
// withLongTimeout := applyToBase(func(c Config) Config {
|
||||
// c.Timeout = 60
|
||||
// return c
|
||||
// })
|
||||
//
|
||||
// withMoreRetries := applyToBase(func(c Config) Config {
|
||||
// c.Retries = 5
|
||||
// return c
|
||||
// })
|
||||
//
|
||||
// # Example - Testing Different Transformations
|
||||
//
|
||||
// // Useful for testing multiple transformations on the same input
|
||||
// testValue := "hello"
|
||||
// applyToTest := Read(testValue)
|
||||
//
|
||||
// upperCase := applyToTest(strings.ToUpper) // "HELLO"
|
||||
// withSuffix := applyToTest(func(s string) string {
|
||||
// return s + " world"
|
||||
// }) // "hello world"
|
||||
//
|
||||
// # Use Cases
|
||||
//
|
||||
// 1. **Testing**: Apply multiple transformations to the same test value
|
||||
// 2. **Configuration**: Create variations of a base configuration
|
||||
// 3. **Data Processing**: Evaluate different processing pipelines on the same data
|
||||
// 4. **Benchmarking**: Compare different endomorphisms on the same input
|
||||
// 5. **Functional Composition**: Build evaluation contexts for composed operations
|
||||
//
|
||||
// # Relationship to Other Functions
|
||||
//
|
||||
// Read is complementary to other endomorphism operations:
|
||||
// - Build applies an endomorphism to the zero value
|
||||
// - Read applies endomorphisms to a specific captured value
|
||||
// - Reduce applies multiple endomorphisms sequentially
|
||||
// - ConcatAll composes multiple endomorphisms
|
||||
//
|
||||
//go:inline
|
||||
func Read[A any](a A) func(Endomorphism[A]) A {
|
||||
return func(f Endomorphism[A]) A {
|
||||
return f(a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,3 +1071,226 @@ func TestReduceWithBuild(t *testing.T) {
|
||||
|
||||
assert.NotEqual(t, reduceResult, buildResult, "Reduce and Build(ConcatAll) produce different results due to execution order")
|
||||
}
|
||||
|
||||
// TestRead tests the Read function
|
||||
func TestRead(t *testing.T) {
|
||||
t.Run("applies endomorphism to captured value", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
result := applyTo5(double)
|
||||
assert.Equal(t, 10, result, "Read should apply double to captured value 5")
|
||||
|
||||
result2 := applyTo5(increment)
|
||||
assert.Equal(t, 6, result2, "Read should apply increment to captured value 5")
|
||||
|
||||
result3 := applyTo5(square)
|
||||
assert.Equal(t, 25, result3, "Read should apply square to captured value 5")
|
||||
})
|
||||
|
||||
t.Run("captures value for reuse", func(t *testing.T) {
|
||||
applyTo10 := Read(10)
|
||||
|
||||
// Apply multiple different endomorphisms to the same captured value
|
||||
doubled := applyTo10(double)
|
||||
incremented := applyTo10(increment)
|
||||
negated := applyTo10(negate)
|
||||
|
||||
assert.Equal(t, 20, doubled, "Should double 10")
|
||||
assert.Equal(t, 11, incremented, "Should increment 10")
|
||||
assert.Equal(t, -10, negated, "Should negate 10")
|
||||
})
|
||||
|
||||
t.Run("works with identity", func(t *testing.T) {
|
||||
applyTo42 := Read(42)
|
||||
|
||||
result := applyTo42(Identity[int]())
|
||||
assert.Equal(t, 42, result, "Read with identity should return original value")
|
||||
})
|
||||
|
||||
t.Run("works with composed endomorphisms", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
// Compose: double then increment (RIGHT-TO-LEFT)
|
||||
composed := MonadCompose(increment, double)
|
||||
result := applyTo5(composed)
|
||||
assert.Equal(t, 11, result, "Read should work with composed endomorphisms: (5 * 2) + 1 = 11")
|
||||
})
|
||||
|
||||
t.Run("works with chained endomorphisms", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
// Chain: double then increment (LEFT-TO-RIGHT)
|
||||
chained := MonadChain(double, increment)
|
||||
result := applyTo5(chained)
|
||||
assert.Equal(t, 11, result, "Read should work with chained endomorphisms: (5 * 2) + 1 = 11")
|
||||
})
|
||||
|
||||
t.Run("works with ConcatAll", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
// ConcatAll composes RIGHT-TO-LEFT
|
||||
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
|
||||
result := applyTo5(combined)
|
||||
// Execution: square(5) = 25, increment(25) = 26, double(26) = 52
|
||||
assert.Equal(t, 52, result, "Read should work with ConcatAll")
|
||||
})
|
||||
|
||||
t.Run("works with different types", func(t *testing.T) {
|
||||
// Test with string
|
||||
applyToHello := Read("hello")
|
||||
|
||||
toUpper := func(s string) string { return s + " WORLD" }
|
||||
result := applyToHello(toUpper)
|
||||
assert.Equal(t, "hello WORLD", result, "Read should work with strings")
|
||||
|
||||
// Test with struct
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
applyToPoint := Read(Point{X: 3, Y: 4})
|
||||
|
||||
scaleX := func(p Point) Point {
|
||||
p.X *= 2
|
||||
return p
|
||||
}
|
||||
|
||||
result2 := applyToPoint(scaleX)
|
||||
assert.Equal(t, Point{X: 6, Y: 4}, result2, "Read should work with structs")
|
||||
})
|
||||
|
||||
t.Run("creates independent evaluation contexts", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
applyTo10 := Read(10)
|
||||
|
||||
// Same endomorphism, different contexts
|
||||
result5 := applyTo5(double)
|
||||
result10 := applyTo10(double)
|
||||
|
||||
assert.Equal(t, 10, result5, "First context should double 5")
|
||||
assert.Equal(t, 20, result10, "Second context should double 10")
|
||||
})
|
||||
|
||||
t.Run("useful for testing transformations", func(t *testing.T) {
|
||||
testValue := 100
|
||||
applyToTest := Read(testValue)
|
||||
|
||||
// Test multiple transformations on the same value
|
||||
transformations := []struct {
|
||||
name string
|
||||
endo Endomorphism[int]
|
||||
expected int
|
||||
}{
|
||||
{"double", double, 200},
|
||||
{"increment", increment, 101},
|
||||
{"negate", negate, -100},
|
||||
{"square", square, 10000},
|
||||
}
|
||||
|
||||
for _, tt := range transformations {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := applyToTest(tt.endo)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("works with monoid operations", func(t *testing.T) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
// Use monoid to combine endomorphisms
|
||||
combined := M.ConcatAll(Monoid[int]())([]Endomorphism[int]{
|
||||
double,
|
||||
increment,
|
||||
})
|
||||
|
||||
result := applyTo5(combined)
|
||||
// RIGHT-TO-LEFT: increment(5) = 6, double(6) = 12
|
||||
assert.Equal(t, 12, result, "Read should work with monoid operations")
|
||||
})
|
||||
|
||||
t.Run("configuration example", func(t *testing.T) {
|
||||
type Config struct {
|
||||
Timeout int
|
||||
Retries int
|
||||
}
|
||||
|
||||
baseConfig := Config{Timeout: 30, Retries: 3}
|
||||
applyToBase := Read(baseConfig)
|
||||
|
||||
withLongTimeout := func(c Config) Config {
|
||||
c.Timeout = 60
|
||||
return c
|
||||
}
|
||||
|
||||
withMoreRetries := func(c Config) Config {
|
||||
c.Retries = 5
|
||||
return c
|
||||
}
|
||||
|
||||
result1 := applyToBase(withLongTimeout)
|
||||
assert.Equal(t, Config{Timeout: 60, Retries: 3}, result1)
|
||||
|
||||
result2 := applyToBase(withMoreRetries)
|
||||
assert.Equal(t, Config{Timeout: 30, Retries: 5}, result2)
|
||||
|
||||
// Original is unchanged
|
||||
result3 := applyToBase(Identity[Config]())
|
||||
assert.Equal(t, baseConfig, result3)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReadWithBuild tests the relationship between Read and Build
|
||||
func TestReadWithBuild(t *testing.T) {
|
||||
t.Run("Read applies to specific value, Build to zero value", func(t *testing.T) {
|
||||
endo := double
|
||||
|
||||
// Build applies to zero value
|
||||
builtResult := Build(endo)
|
||||
assert.Equal(t, 0, builtResult, "Build should apply to zero value: 0 * 2 = 0")
|
||||
|
||||
// Read applies to specific value
|
||||
readResult := Read(5)(endo)
|
||||
assert.Equal(t, 10, readResult, "Read should apply to captured value: 5 * 2 = 10")
|
||||
})
|
||||
|
||||
t.Run("Read can evaluate Build results", func(t *testing.T) {
|
||||
// Build an endomorphism
|
||||
builder := ConcatAll([]Endomorphism[int]{double, increment})
|
||||
|
||||
// Apply it to zero value
|
||||
builtValue := Build(builder)
|
||||
// RIGHT-TO-LEFT: increment(0) = 1, double(1) = 2
|
||||
assert.Equal(t, 2, builtValue)
|
||||
|
||||
// Now use Read to apply the same builder to a different value
|
||||
readValue := Read(5)(builder)
|
||||
// RIGHT-TO-LEFT: increment(5) = 6, double(6) = 12
|
||||
assert.Equal(t, 12, readValue)
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkRead benchmarks the Read function
|
||||
func BenchmarkRead(b *testing.B) {
|
||||
applyTo5 := Read(5)
|
||||
|
||||
b.Run("simple endomorphism", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = applyTo5(double)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("composed endomorphism", func(b *testing.B) {
|
||||
composed := MonadCompose(double, increment)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = applyTo5(composed)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("ConcatAll endomorphism", func(b *testing.B) {
|
||||
combined := ConcatAll([]Endomorphism[int]{double, increment, square})
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = applyTo5(combined)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -144,8 +144,8 @@ func Semigroup[A any]() S.Semigroup[Endomorphism[A]] {
|
||||
// square := func(x int) int { return x * x }
|
||||
//
|
||||
// // Combine multiple endomorphisms (RIGHT-TO-LEFT execution)
|
||||
// combined := M.ConcatAll(monoid)(double, increment, square)
|
||||
// result := combined(5) // square(increment(double(5))) = square(increment(10)) = square(11) = 121
|
||||
// combined := M.ConcatAll(monoid)([]Endomorphism[int]{double, increment, square})
|
||||
// result := combined(5) // double(increment(square(5))) = double(increment(25)) = double(26) = 52
|
||||
func Monoid[A any]() M.Monoid[Endomorphism[A]] {
|
||||
return M.MakeMonoid(MonadCompose[A], Identity[A]())
|
||||
}
|
||||
|
||||
@@ -41,20 +41,22 @@ type (
|
||||
// It's a function from A to Endomorphism[A], used for composing endomorphic operations.
|
||||
Kleisli[A any] = func(A) Endomorphism[A]
|
||||
|
||||
// Operator represents a transformation from one endomorphism to another.
|
||||
// Operator represents a higher-order transformation on endomorphisms of the same type.
|
||||
//
|
||||
// An Operator takes an endomorphism on type A and produces an endomorphism on type B.
|
||||
// This is useful for lifting operations or transforming endomorphisms in a generic way.
|
||||
// An Operator takes an endomorphism on type A and produces another endomorphism on type A.
|
||||
// Since Operator[A] = Endomorphism[Endomorphism[A]] = func(func(A)A) func(A)A,
|
||||
// both the input and output endomorphisms operate on the same type A.
|
||||
//
|
||||
// This is the return type of curried operations such as Compose, Map, and Chain.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // An operator that converts an int endomorphism to a string endomorphism
|
||||
// intToString := func(f endomorphism.Endomorphism[int]) endomorphism.Endomorphism[string] {
|
||||
// return func(s string) string {
|
||||
// n, _ := strconv.Atoi(s)
|
||||
// result := f(n)
|
||||
// return strconv.Itoa(result)
|
||||
// }
|
||||
// // An operator that applies any endomorphism twice
|
||||
// var applyTwice endomorphism.Operator[int] = func(f endomorphism.Endomorphism[int]) endomorphism.Endomorphism[int] {
|
||||
// return func(x int) int { return f(f(x)) }
|
||||
// }
|
||||
// double := N.Mul(2)
|
||||
// result := applyTwice(double) // double ∘ double
|
||||
// // result(5) = double(double(5)) = double(10) = 20
|
||||
Operator[A any] = Endomorphism[Endomorphism[A]]
|
||||
)
|
||||
|
||||
@@ -38,21 +38,41 @@ func IsNonEmpty[M ~map[K]V, K comparable, V any](r M) bool {
|
||||
}
|
||||
|
||||
func Keys[M ~map[K]V, GK ~[]K, K comparable, V any](r M) GK {
|
||||
// fast path
|
||||
if len(r) == 0 {
|
||||
return nil
|
||||
}
|
||||
// full implementation
|
||||
return collect[M, GK](r, F.First[K, V])
|
||||
}
|
||||
|
||||
func Values[M ~map[K]V, GV ~[]V, K comparable, V any](r M) GV {
|
||||
// fast path
|
||||
if len(r) == 0 {
|
||||
return nil
|
||||
}
|
||||
// full implementation
|
||||
return collect[M, GV](r, F.Second[K, V])
|
||||
}
|
||||
|
||||
func KeysOrd[M ~map[K]V, GK ~[]K, K comparable, V any](o ord.Ord[K]) func(r M) GK {
|
||||
return func(r M) GK {
|
||||
// fast path
|
||||
if len(r) == 0 {
|
||||
return nil
|
||||
}
|
||||
// full implementation
|
||||
return collectOrd[M, GK](o, r, F.First[K, V])
|
||||
}
|
||||
}
|
||||
|
||||
func ValuesOrd[M ~map[K]V, GV ~[]V, K comparable, V any](o ord.Ord[K]) func(r M) GV {
|
||||
return func(r M) GV {
|
||||
// fast path
|
||||
if len(r) == 0 {
|
||||
return nil
|
||||
}
|
||||
// full implementation
|
||||
return collectOrd[M, GV](o, r, F.Second[K, V])
|
||||
}
|
||||
}
|
||||
@@ -97,12 +117,18 @@ func collect[M ~map[K]V, GR ~[]R, K comparable, V, R any](r M, f func(K, V) R) G
|
||||
}
|
||||
|
||||
func Collect[M ~map[K]V, GR ~[]R, K comparable, V, R any](f func(K, V) R) func(M) GR {
|
||||
// full implementation
|
||||
return F.Bind2nd(collect[M, GR, K, V, R], f)
|
||||
}
|
||||
|
||||
func CollectOrd[M ~map[K]V, GR ~[]R, K comparable, V, R any](o ord.Ord[K]) func(f func(K, V) R) func(M) GR {
|
||||
return func(f func(K, V) R) func(M) GR {
|
||||
return func(r M) GR {
|
||||
// fast path
|
||||
if len(r) == 0 {
|
||||
return nil
|
||||
}
|
||||
// full implementation
|
||||
return collectOrd[M, GR](o, r, f)
|
||||
}
|
||||
}
|
||||
@@ -416,12 +442,22 @@ func duplicate[M ~map[K]V, K comparable, V any](r M) M {
|
||||
}
|
||||
|
||||
func upsertAt[M ~map[K]V, K comparable, V any](r M, k K, v V) M {
|
||||
// fast path
|
||||
if len(r) == 0 {
|
||||
return Singleton[M](k, v)
|
||||
}
|
||||
// duplicate and update
|
||||
dup := duplicate(r)
|
||||
dup[k] = v
|
||||
return dup
|
||||
}
|
||||
|
||||
func deleteAt[M ~map[K]V, K comparable, V any](r M, k K) M {
|
||||
// fast path
|
||||
if len(r) == 0 {
|
||||
return r
|
||||
}
|
||||
// duplicate and update
|
||||
dup := duplicate(r)
|
||||
delete(dup, k)
|
||||
return dup
|
||||
|
||||
@@ -55,10 +55,16 @@ func IsNonEmpty[K comparable, V any](r Record[K, V]) bool {
|
||||
// The order of keys is non-deterministic due to Go's map iteration behavior.
|
||||
// Use KeysOrd if you need keys in a specific order.
|
||||
//
|
||||
// Note: The return value can be nil in case of an empty map, since nil is a
|
||||
// valid representation of an empty slice in Go.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// record := Record[string, int]{"a": 1, "b": 2, "c": 3}
|
||||
// keys := Keys(record) // ["a", "b", "c"] in any order
|
||||
//
|
||||
// emptyRecord := Record[string, int]{}
|
||||
// emptyKeys := Keys(emptyRecord) // nil or []string{}
|
||||
func Keys[K comparable, V any](r Record[K, V]) []K {
|
||||
return G.Keys[Record[K, V], []K](r)
|
||||
}
|
||||
@@ -68,10 +74,16 @@ func Keys[K comparable, V any](r Record[K, V]) []K {
|
||||
// The order of values is non-deterministic due to Go's map iteration behavior.
|
||||
// Use ValuesOrd if you need values ordered by their keys.
|
||||
//
|
||||
// Note: The return value can be nil in case of an empty map, since nil is a
|
||||
// valid representation of an empty slice in Go.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// record := Record[string, int]{"a": 1, "b": 2, "c": 3}
|
||||
// values := Values(record) // [1, 2, 3] in any order
|
||||
//
|
||||
// emptyRecord := Record[string, int]{}
|
||||
// emptyValues := Values(emptyRecord) // nil or []int{}
|
||||
func Values[K comparable, V any](r Record[K, V]) []V {
|
||||
return G.Values[Record[K, V], []V](r)
|
||||
}
|
||||
@@ -98,6 +110,9 @@ func Collect[K comparable, V, R any](f func(K, V) R) func(Record[K, V]) []R {
|
||||
//
|
||||
// Unlike Collect, this function guarantees the order of results based on key ordering.
|
||||
//
|
||||
// Note: The return value can be nil in case of an empty map, since nil is a
|
||||
// valid representation of an empty slice in Go.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// record := Record[string, int]{"c": 3, "a": 1, "b": 2}
|
||||
@@ -105,6 +120,9 @@ func Collect[K comparable, V, R any](f func(K, V) R) func(Record[K, V]) []R {
|
||||
// return fmt.Sprintf("%s=%d", k, v)
|
||||
// })
|
||||
// result := toStrings(record) // ["a=1", "b=2", "c=3"] (ordered by key)
|
||||
//
|
||||
// emptyRecord := Record[string, int]{}
|
||||
// emptyResult := toStrings(emptyRecord) // nil or []string{}
|
||||
func CollectOrd[V, R any, K comparable](o ord.Ord[K]) func(func(K, V) R) func(Record[K, V]) []R {
|
||||
return G.CollectOrd[Record[K, V], []R](o)
|
||||
}
|
||||
@@ -458,11 +476,18 @@ func UpsertAt[K comparable, V any](k K, v V) Operator[K, V, V] {
|
||||
// If the key doesn't exist, the record is returned unchanged.
|
||||
// The original record is not modified; a new record is returned.
|
||||
//
|
||||
// In case of an empty input map (including nil maps), the identical map is returned,
|
||||
// since deleting from an empty map is an idempotent operation.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// record := Record[string, int]{"a": 1, "b": 2, "c": 3}
|
||||
// removeB := DeleteAt[string, int]("b")
|
||||
// result := removeB(record) // {"a": 1, "c": 3}
|
||||
//
|
||||
// // Deleting from empty map returns empty map
|
||||
// emptyRecord := Record[string, int]{}
|
||||
// result2 := removeB(emptyRecord) // {}
|
||||
func DeleteAt[K comparable, V any](k K) Operator[K, V, V] {
|
||||
return G.DeleteAt[Record[K, V]](k)
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestNilMap_IsNonEmpty(t *testing.T) {
|
||||
func TestNilMap_Keys(t *testing.T) {
|
||||
var nilMap Record[string, int]
|
||||
keys := Keys(nilMap)
|
||||
assert.NotNil(t, keys, "Keys should return non-nil slice")
|
||||
// Keys can return nil for empty map, which is a valid representation of an empty slice
|
||||
assert.Equal(t, 0, len(keys), "Keys should return empty slice for nil map")
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestNilMap_Keys(t *testing.T) {
|
||||
func TestNilMap_Values(t *testing.T) {
|
||||
var nilMap Record[string, int]
|
||||
values := Values(nilMap)
|
||||
assert.NotNil(t, values, "Values should return non-nil slice")
|
||||
// Values can return nil for empty map, which is a valid representation of an empty slice
|
||||
assert.Equal(t, 0, len(values), "Values should return empty slice for nil map")
|
||||
}
|
||||
|
||||
@@ -288,8 +288,16 @@ func TestNilMap_DeleteAt(t *testing.T) {
|
||||
var nilMap Record[string, int]
|
||||
deleteFunc := DeleteAt[string, int]("key")
|
||||
result := deleteFunc(nilMap)
|
||||
assert.NotNil(t, result, "DeleteAt should return non-nil map")
|
||||
assert.Equal(t, 0, len(result), "DeleteAt should return empty map for nil input")
|
||||
// DeleteAt returns the identical map for nil input (idempotent operation)
|
||||
assert.Nil(t, result, "DeleteAt should return nil for nil input (idempotent)")
|
||||
assert.Equal(t, nilMap, result, "DeleteAt should return identical map for nil input")
|
||||
|
||||
// Verify that deleting from empty (non-nil) map returns identical map (idempotent)
|
||||
emptyMap := Record[string, int]{}
|
||||
result2 := deleteFunc(emptyMap)
|
||||
assert.NotNil(t, result2, "DeleteAt should return non-nil map for empty input")
|
||||
assert.Equal(t, 0, len(result2), "DeleteAt should return empty map for empty input")
|
||||
assert.Equal(t, emptyMap, result2, "DeleteAt on empty map should be idempotent")
|
||||
}
|
||||
|
||||
// TestNilMap_Filter verifies that Filter handles nil maps correctly
|
||||
|
||||
Reference in New Issue
Block a user