1
0
mirror of https://github.com/IBM/fp-go.git synced 2026-03-10 13:31:01 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Dr. Carsten Leue
f0ec0b2541 fix: optimize record performance
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2026-03-08 22:20:19 +01:00
3 changed files with 73 additions and 4 deletions

View File

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

View File

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

View File

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