Add a memoizedbloom

This commit is contained in:
2026-01-25 14:59:11 +01:00
parent 9cc7a41b08
commit 0104a9490a
3 changed files with 79 additions and 0 deletions

5
go.mod
View File

@@ -3,3 +3,8 @@ module git.site.quack-lab.dev/dave/cyutils
go 1.23.6
require golang.org/x/time v0.12.0
require (
github.com/bits-and-blooms/bitset v1.24.2 // indirect
github.com/bits-and-blooms/bloom/v3 v3.7.1 // indirect
)

5
go.sum
View File

@@ -1,2 +1,7 @@
github.com/bits-and-blooms/bitset v1.24.2 h1:M7/NzVbsytmtfHbumG+K2bremQPMJuqv1JD3vOaFxp0=
github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bloom/v3 v3.7.1 h1:WXovk4TRKZttAMJfoQx6K2DM0zNIt8w+c67UqO+etV0=
github.com/bits-and-blooms/bloom/v3 v3.7.1/go.mod h1:rZzYLLje2dfzXfAkJNxQQHsKurAyK55KUnL43Euk0hU=
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=

69
main.go
View File

@@ -11,6 +11,7 @@ import (
"reflect"
"sync"
"github.com/bits-and-blooms/bloom/v3"
"golang.org/x/time/rate"
)
@@ -259,3 +260,71 @@ func Memoized(fn interface{}) interface{} {
return results
}).Interface()
}
// MemoizedBloom creates a memoized version of the provided function using a Bloom filter
// to avoid caching "one hit wonders". Results are only cached on the second invocation
// with the same arguments, reducing memory usage for functions with many unique calls.
//
// The function uses a Bloom filter to track which arguments have been seen:
// - First call: Executes function, adds to Bloom filter, returns result (not cached)
// - Second call: Executes function, caches result, returns result
// - Subsequent calls: Returns cached result
//
// Parameters:
// - fn: The function to memoize (must be a function type)
// - capacity: Expected number of unique argument combinations (default: 10000)
// - falsePositiveRate: Desired false positive rate for Bloom filter (default: 0.01)
//
// Returns a memoized version of the function with the same signature.
//
// Example:
//
// func expensive(a string, b int) (string, error) {
// fmt.Println("Computing...")
// return fmt.Sprintf("Result: %s, %d", a, b), nil
// }
//
// memoized := MemoizedBloom(expensive, 10000, 0.01).(func(string, int) (string, error))
// result, _ := memoized("hello", 42) // Computes, adds to bloom filter
// result, _ = memoized("hello", 42) // Computes again, caches result
// result, _ = memoized("hello", 42) // Returns cached result
//
// Note: The cache key is generated using fmt.Sprintf("%v", args), which may not
// uniquely identify all argument combinations. This function is thread-safe.
func MemoizedBloom(fn interface{}, capacity uint, falsePositiveRate float64) interface{} {
fnVal := reflect.ValueOf(fn)
fnType := fnVal.Type()
if capacity == 0 {
capacity = 10000
}
if falsePositiveRate == 0 {
falsePositiveRate = 0.01
}
bf := bloom.NewWithEstimates(capacity, falsePositiveRate)
cache := make(map[string][]reflect.Value)
var mu sync.Mutex
return reflect.MakeFunc(fnType, func(args []reflect.Value) []reflect.Value {
key := fmt.Sprintf("%v", args)
keyBytes := []byte(key)
mu.Lock()
defer mu.Unlock()
if cached, ok := cache[key]; ok {
return cached
}
results := fnVal.Call(args)
if bf.Test(keyBytes) {
cache[key] = results
} else {
bf.Add(keyBytes)
}
return results
}).Interface()
}