Welcome! Please see the About page for a little more info on how this works.

+1 vote
in core.memoize by

Although it is possible to use memo-clear! to force a refresh, we are caching for efficiency purposes, so it would be nice to provide an easy way to manually set cache values. This is one way to implement it:

(defn memo-miss!
  "Reaches into a core.memo-populated function and sets the cached value
  for a given seq of function args."
  [f args result]
  (when-let [cache (cache-id f)]
    (swap! cache clojure.core.cache/miss args (delay result))))

(testing "that setting cache values works as expected"
  (is (memo-clear! id))
  (is (memo-miss! id [29] :x))
  (is (= :x (id 29)))
  (is (= 42 (id 42))))

Following the existing code style, this could be written as:

(defn memo-miss!
  "Reaches into a core.memo-populated function and sets the cached value
  for a given seq of function args."
  [f args result]
  (when-let [cache (cache-id f)]
    (swap! cache
           (constantly (clojure.core.cache/miss @cache args (delay result))))))

I don't understand why swap!... constantly is preferred, or how it is any different than reset!, but I also don't have the decades of experience of the maintainers!

2 Answers

+2 votes
selected by
Best answer

This is a good argument for adding a "memo-swap!"-like function:


As that ticket notes, the clear memo-swap! is poorly named since it is really "memo-reset!". Backward compatibility means that I can't rename it but I could add memo-reset! and also introduce a new arity for memo-swap! that really does behave the way you'd want above:

(memo/memo-swap! f assoc args result)

This would keep the implementation encapsulated and provide the semantics you want.

As for the stylistic issue of swap! ,,, constantly rather than reset!, I'm not sure why it is written that way. I'll see if I can figure that out and get back to you.

Master has this change now (an additional arity on memo-swap!):

(memo/memo-swap! f cache/miss args result)

It also adds memo-reset! with the original behavior of memo-swap! (and deprecates the 2-arity version of memo-swap!).
That's really clever. I didn't think there was a way to add a general "swap! cache-protocol-fn" function without depending on the delay implementation detail, but of course only miss uses 3 arguments. That is a brilliant solution.
0 votes

I agree, using constantly there ignores the passed in value of the cache atom (which may change if the function is retried) which makes it the same as a reset!, and means memo-miss! there introduces a race condition.

However, I think what is going is multiple layers of atoms. For whatever weird reason core.cache and core.memoize are different libraries, core.cache tries to present caches as immutable things, and memoize adds the mutability by storing caches in an atom. But as development has gone on it has become apparent that the "immutable" caches need some mutability so they internally now have their own atom. So the memoize code now seems to mostly ignore the memoize atom.

I think that makes it safe, even if ugly.

clojure.core.cache is still all immutable. clojure.core.cache.wrapped was added recently to provide a cache-wrapped-in-an-atom version of the API, since that's mostly how people use it. clojure.core.memomize has always wrapped clojure.core.cache (immutable) caches in an atom, stored in the metadata of the memoized function -- but it also implements the clojure.core.cache protocol as if it were also an immutable cache... which is both somewhat confusing and also very hard to actually use properly.

As a compromise, clojure.core.memoize has also always had a couple of functions that operate on that atom, but don't expose the full API... which has also made some operations harder than they perhaps needed to be.

I think, for the most part, clojure.core.memoize users don't tend to want to modify the cached function results manually so this use case has never been reported as a need (until John's threads here recently), so that part of the library just hasn't had much love before.