Memoization in Clojure
In my endless quest for a Java alternative I have arrived at Clojure. So far it is going well. I worked my way through Practical Common Lisp a year ago so the syntax does not really scare me anymore. This is one fun language.
Clojure’s standard library comes with a really cool and powerful feature. By using the memoize function you can wrap an existing function so that it will automatically remember (memoize) the results of the original function.
Here is an example of it’s usage:
;; Create a simple function that takes some time to execute (defn my-function [] "Sum all the values from 1 to 1000000" (reduce + (range 1 1000001))) ;; Execute the function user> (time (my-function)) "Elapsed time: 124.347 msecs" 500000500000 ;; Create a memoized version of the function (def my-memoized-function (memoize my-function)) ;; Execute the memoized version. First time is not cached. user> (time (my-memoized-function)) "Elapsed time: 128.871 msecs" 500000500000 ;; Execute the memoized version. Second time the result is cached. user> (time (my-memoized-function)) "Elapsed time: 0.139 msecs" 500000500000
As you can see, the execution time of the cached version is a factor 100 better the second time it is called.
I wrote a memoize function that is very similar with one change: it adds the ability to specify an expiration time in milliseconds. This is very useful for caching for example results from functions that talk to a database. You don’t want to cache those indefinitely.
(defn expire-cached-results* [cached-results time-to-live]
"Expire items from the cached function results."
(into {}
(filter (fn [[k v]]
(> time-to-live
(- (System/currentTimeMillis) (:time v)))) cached-results)))
(defn my-memoize
"Returns a memoized version of a referentially transparent function. 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. Cached results are
removed from the cache when their time to live value expires."
[function time-to-live]
(let [cached-results (atom {})]
(fn [& arguments]
(swap! cached-results expire-cached-results* time-to-live)
(if-let [entry (find @cached-results arguments)]
(:result (val entry))
(let [result (apply function arguments)]
(swap! cached-results assoc arguments
{ :result result :time (System/currentTimeMillis)})
result)))))
It works very similar, except that it needs a second parameter to specify the time-to-live of the cached function results.
;; Create a simple function that prints when it is doing some work user> (defn calculation [n] (println "Doing some bistromath") (* n 42)) ;; Create a memoized version that expires its results in 5 seconds user> (def memoized-calculation (my-memoize calculation 5000)) ;; Call the memoized function 4 times with 3 second delays in between user> (dotimes [n 4] (println (memoized-calculation 5)) (Thread/sleep 3000)) Doing some bistromath 210 210 Doing some bistromath 210 210
Cool stuff. I like Clojure and the tricks it can do.
One of my favorite things about memoize is that you can name the memoized function the same as the base function, then your users don’t know which they’re calling.
Cool to see other Cocoa hackers digging Clojure. With the Clojure-in-Clojure project well underway perhaps it won’t be too long before we see an Objective-C implementation?
Via GitHub, I got your examples, and now I got the compojure and the hackernews ones to run. This is the Common Lisp I have been looking for. Thanks for nudging me on track again. You have a tendency to do that.