blob: 7dcfbfe023c417134c8334d7691d83d6395ff80a (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
|
;; Release 0.0.3
;; https://github.com/alienscience/cache-dot-clj/tree/master/src
(ns cache-dot-clj.cache "Resettable memoize")
(declare naive-strategy)
(defn- external-memoize
"Conventional memoize for use with external caching packages"
[f f-name strategy]
(let [{:keys [init lookup miss! invalidate!]} strategy
cache (init f-name)]
{:memoized
(fn [& args]
(let [[in-cache? res] (lookup cache args)]
(if in-cache?
res
(miss! cache args (apply f args)))))
:invalidate
(fn [args]
(invalidate! cache args))
}))
;; TODO Move some of doc up a level
(defn- internal-memoize
"Returns a map containing:
{:memoized fn ;; Memoized version of given function
:invalidate fn ;; Invalidate arguments in the cache
}
The memoized version of the function keeps a cache of the mapping from
arguments to results and, when calls with the same arguments are repeated
often, has higher performance at the expense of higher memory use.
The invalidation function takes a set of function arguments and causes
the appropriate cache entry to re-evaluate the memoized function.
Invalidation can be used to support the memoization of functions
that can be effected by external events.
Takes a cache strategy. The strategy is provided as a map
containing the following keys. All keys are mandatory!
- :init – the initial value for the cache and strategy state
- :cache – access function to access the cache
- :lookup – determines whether a value is in the cache or not
- :hit – a function called with the cache state and the argument
list in case of a cache hit
- :miss – a function called with the cache state, the argument list
and the computation result in case of a cache miss
- :invalidate - a function called with the cache state, the argument
list and the computation result that is used to
invalidate the cache entry for the computation.
"
[f _ strategy]
(let [{:keys [init cache lookup hit miss invalidate]} strategy
cache-state (atom init)
hit-or-miss (fn [state args]
(if (lookup state args)
(hit state args)
(miss state args (delay (apply f args)))))
mark-dirty (fn [state args]
(if (lookup state args)
(invalidate state args (delay (apply f args)))
state))]
{:memoized
(fn [& args]
(let [cs (swap! cache-state hit-or-miss args)]
(-> cs cache (get args) deref)))
:invalidate
(fn [args]
(swap! cache-state mark-dirty args)
nil)}))
(defmacro defn-cached
"Defines a cached function, like defn-memo from clojure.contrib.def
e.g
(defn-cached fib
(lru-cache-strategy 10)
[n]
(if (<= n 1)
n
(+ (fib (dec n)) (fib (- n 2)))))"
[fn-name cache-strategy & defn-stuff]
`(let [f-name# (str *ns* "." '~fn-name)]
(defn ~fn-name ~@defn-stuff)
(alter-var-root (var ~fn-name)
cached* f-name# ~cache-strategy)
(var ~fn-name)))
(def function-utils* (atom {}))
(def memoizers* {:external-memoize external-memoize
:internal-memoize internal-memoize})
(defn cached*
"Sets up a cache for the given function with the given name"
[f f-name strategy]
(let [memoizer (-> strategy :plugs-into memoizers*)
internals (memoizer f f-name strategy)
cached-f (:memoized internals)
utils (dissoc internals :memoized)]
(if (and (= memoizer external-memoize)
(= f-name :anon))
(throw (Exception. (str (strategy :description)
" does not support anonymous functions"))))
(if-not (empty? utils)
(swap! function-utils* assoc cached-f utils))
cached-f))
(defmacro cached
"Returns a cached function that can be invalidated by calling
invalidate-cache e.g
(def fib (cached fib (lru-cache-stategy 5)))"
[f strategy]
(if-not (symbol? f)
`(cached* ~f :anon ~strategy)
`(let [f-name# (str *ns* "." '~f)]
(cached* ~f f-name# ~strategy))))
(defn invalidate-cache
"Invalidates the cache for the function call with the given arguments
causing it to be re-evaluated e.g
(invalidate-cache fib 30) ;; A call to (fib 30) will not use the cache
(invalidate-cache fib 29) ;; A call to (fib 29) will not use the cache
(fib 18) ;; A call to (fib 18) will use the cache"
[cached-f & args]
(if-let [inv-fn (:invalidate (@function-utils* cached-f))]
(inv-fn args)))
;;======== Stategies for for memoize ==========================================
(def #^{:doc "A naive strategy for testing external-memoize"}
naive-external-strategy
{:init (fn [_] (atom {}))
:lookup (fn [m args]
(let [v (get @m args ::not-found)]
(if (= v ::not-found)
[false nil]
[true v])))
:miss! (fn [m args res]
(swap! m assoc args res)
res)
:invalidate! (fn [m args]
(swap! m dissoc args)
nil)
:description "Naive external strategy"
:plugs-into :external-memoize})
(def #^{:doc "The naive save-all cache strategy for memoize."}
naive-strategy
{:init {}
:cache identity
:lookup contains?
:hit (fn [state _] state)
:miss assoc
:invalidate assoc
:plugs-into :internal-memoize})
(defn lru-cache-strategy
"Implements a LRU cache strategy, which drops the least recently used
argument lists from the cache. If the given limit of items in the cache
is reached, the longest unaccessed item is removed from the cache. In
case there is a tie, the removal order is unspecified."
[limit]
{:init {:lru (into {} (for [x (range (- limit) 0)] [x x]))
:tick 0
:cache {}}
:cache :cache
:lookup (fn [state k] (contains? (:cache state) k))
:hit (fn [state args]
(-> state
(assoc-in [:lru args] (:tick state))
(update-in [:tick] inc)))
:miss (fn [state args result]
(let [k (apply min-key (:lru state) (keys (:lru state)))]
(-> state
(update-in [:lru] dissoc k)
(update-in [:cache] dissoc k)
(assoc-in [:lru args] (:tick state))
(update-in [:tick] inc)
(assoc-in [:cache args] result))))
:invalidate (fn [state args placeholder]
(if (contains? (:lru state) args)
(assoc-in state [:cache args] placeholder)))
:plugs-into :internal-memoize})
(defn ttl-cache-strategy
"Implements a time-to-live cache strategy. Upon access to the cache
all expired items will be removed. The time to live is defined by
the given expiry time span. Items will only be removed on function
call. No background activity is done."
[ttl]
(let [dissoc-dead (fn [state now]
(let [ks (map key (filter #(> (- now (val %)) ttl)
(:ttl state)))
dissoc-ks #(apply dissoc % ks)]
(-> state
(update-in [:ttl] dissoc-ks)
(update-in [:cache] dissoc-ks))))]
{:init {:ttl {} :cache {}}
:cache :cache
:lookup (fn [state args]
(when-let [t (get (:ttl state) args)]
(< (- (System/currentTimeMillis) t) ttl)))
:hit (fn [state args]
(dissoc-dead state (System/currentTimeMillis)))
:miss (fn [state args result]
(let [now (System/currentTimeMillis)]
(-> state
(dissoc-dead now)
(assoc-in [:ttl args] now)
(assoc-in [:cache args] result))))
:invalidate (fn [state args placeholder]
(if (contains? (:ttl state) args)
(assoc-in state [:cache args] placeholder)))
:plugs-into :internal-memoize}))
(defn lu-cache-strategy
"Implements a least-used cache strategy. Upon access to the cache
it will be tracked which items are requested. If the cache size reaches
the given limit, items with the lowest usage count will be removed. In
case of ties the removal order is unspecified."
[limit]
{:init {:lu (into {} (for [x (range (- limit) 0)] [x x])) :cache {}}
:cache :cache
:lookup (fn [state k] (contains? (:cache state) k))
:hit (fn [state args] (update-in state [:lu args] inc))
:miss (fn [state args result]
(let [k (apply min-key (:lu state) (keys (:lu state)))]
(-> state
(update-in [:lu] dissoc k)
(update-in [:cache] dissoc k)
(assoc-in [:lu args] 0)
(assoc-in [:cache args] result))))
:invalidate (fn [state args placeholder]
(if (contains? (:lu state) args)
(assoc-in state [:cache args] placeholder)))
:plugs-into :internal-memoize})
|