summaryrefslogtreecommitdiff
path: root/src/cache_dot_clj/cache.clj
diff options
context:
space:
mode:
Diffstat (limited to 'src/cache_dot_clj/cache.clj')
-rw-r--r--src/cache_dot_clj/cache.clj241
1 files changed, 241 insertions, 0 deletions
diff --git a/src/cache_dot_clj/cache.clj b/src/cache_dot_clj/cache.clj
new file mode 100644
index 0000000..7dcfbfe
--- /dev/null
+++ b/src/cache_dot_clj/cache.clj
@@ -0,0 +1,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}) \ No newline at end of file