Skip to content

Commit be6c86d

Browse files
mouln2p5
andauthored
feat(examples): add p/moul/memo(ize) (#3422)
Signed-off-by: moul <[email protected]> Co-authored-by: Nathan Toups <[email protected]>
1 parent 4989dba commit be6c86d

File tree

3 files changed

+584
-0
lines changed

3 files changed

+584
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module gno.land/p/moul/memo
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Package memo provides a simple memoization utility to cache function results.
2+
//
3+
// The package offers a Memoizer type that can cache function results based on keys,
4+
// with optional validation of cached values. This is useful for expensive computations
5+
// that need to be cached and potentially invalidated based on custom conditions.
6+
//
7+
// /!\ Important Warning for Gno Usage:
8+
// In Gno, storage updates only persist during transactions. This means:
9+
// - Cache entries created during queries will NOT persist
10+
// - Creating cache entries during queries will actually decrease performance
11+
// as it wastes resources trying to save data that won't be saved
12+
//
13+
// Best Practices:
14+
// - Use this pattern in transaction-driven contexts rather than query/render scenarios
15+
// - Consider controlled cache updates, e.g., by specific accounts (like oracles)
16+
// - Ideal for cases where cache updates happen every N blocks or on specific events
17+
// - Carefully evaluate if caching will actually improve performance in your use case
18+
//
19+
// Basic usage example:
20+
//
21+
// m := memo.New()
22+
//
23+
// // Cache expensive computation
24+
// result := m.Memoize("key", func() interface{} {
25+
// // expensive operation
26+
// return "computed-value"
27+
// })
28+
//
29+
// // Subsequent calls with same key return cached result
30+
// result = m.Memoize("key", func() interface{} {
31+
// // function won't be called, cached value is returned
32+
// return "computed-value"
33+
// })
34+
//
35+
// Example with validation:
36+
//
37+
// type TimestampedValue struct {
38+
// Value string
39+
// Timestamp time.Time
40+
// }
41+
//
42+
// m := memo.New()
43+
//
44+
// // Cache value with timestamp
45+
// result := m.MemoizeWithValidator(
46+
// "key",
47+
// func() interface{} {
48+
// return TimestampedValue{
49+
// Value: "data",
50+
// Timestamp: time.Now(),
51+
// }
52+
// },
53+
// func(cached interface{}) bool {
54+
// // Validate that the cached value is not older than 1 hour
55+
// if tv, ok := cached.(TimestampedValue); ok {
56+
// return time.Since(tv.Timestamp) < time.Hour
57+
// }
58+
// return false
59+
// },
60+
// )
61+
package memo
62+
63+
import (
64+
"gno.land/p/demo/btree"
65+
"gno.land/p/demo/ufmt"
66+
)
67+
68+
// Record implements the btree.Record interface for our cache entries
69+
type cacheEntry struct {
70+
key interface{}
71+
value interface{}
72+
}
73+
74+
// Less implements btree.Record interface
75+
func (e cacheEntry) Less(than btree.Record) bool {
76+
// Convert the other record to cacheEntry
77+
other := than.(cacheEntry)
78+
// Compare string representations of keys for consistent ordering
79+
return ufmt.Sprintf("%v", e.key) < ufmt.Sprintf("%v", other.key)
80+
}
81+
82+
// Memoizer is a structure to handle memoization of function results.
83+
type Memoizer struct {
84+
cache *btree.BTree
85+
}
86+
87+
// New creates a new Memoizer instance.
88+
func New() *Memoizer {
89+
return &Memoizer{
90+
cache: btree.New(),
91+
}
92+
}
93+
94+
// Memoize ensures the result of the given function is cached for the specified key.
95+
func (m *Memoizer) Memoize(key interface{}, fn func() interface{}) interface{} {
96+
entry := cacheEntry{key: key}
97+
if found := m.cache.Get(entry); found != nil {
98+
return found.(cacheEntry).value
99+
}
100+
101+
value := fn()
102+
m.cache.Insert(cacheEntry{key: key, value: value})
103+
return value
104+
}
105+
106+
// MemoizeWithValidator ensures the result is cached and valid according to the validator function.
107+
func (m *Memoizer) MemoizeWithValidator(key interface{}, fn func() interface{}, isValid func(interface{}) bool) interface{} {
108+
entry := cacheEntry{key: key}
109+
if found := m.cache.Get(entry); found != nil {
110+
cachedEntry := found.(cacheEntry)
111+
if isValid(cachedEntry.value) {
112+
return cachedEntry.value
113+
}
114+
}
115+
116+
value := fn()
117+
m.cache.Insert(cacheEntry{key: key, value: value})
118+
return value
119+
}
120+
121+
// Invalidate removes the cached value for the specified key.
122+
func (m *Memoizer) Invalidate(key interface{}) {
123+
m.cache.Delete(cacheEntry{key: key})
124+
}
125+
126+
// Clear clears all cached values.
127+
func (m *Memoizer) Clear() {
128+
m.cache.Clear(true)
129+
}
130+
131+
// Size returns the number of items currently in the cache.
132+
func (m *Memoizer) Size() int {
133+
return m.cache.Len()
134+
}

0 commit comments

Comments
 (0)