Fork me on GitHub
#clojure-dev
<
2021-02-27
>
seancorfield21:02:45

Can someone help me out here? https://clojure.atlassian.net/browse/CMEMOIZE-25 says "Due to differences in locking etc, there are some subtle differences in semantics between the clojure.core/memoize function and core.memoize/memo function, and these should be explained in the README." -- which is verbatim what someone said to me about those two functions, but they did not provide any details about the "subtle differences" and I'm not sure what they meant (and, yes, I should have asked for clarification at the time but now I can't even remember who said it!).

borkdude21:02:11

Is this about core memoize potentially doing calculations multiple times (based on atoms, swap!), whereas with a locking based solution it would happen only once?

seancorfield21:02:14

Could be. memoize closes over an atom but only evals the function call once, then swap!'s it into the atom as a plain value.

borkdude21:02:36

That is true, but multiple concurrent calls might still happen

seancorfield21:02:56

I'm staring at the core.memoize and core.cache code -- the core.memoize/memo function uses the "basic" cache which is just a hash map, no eviction, wrapped in an atom so I'm not seeing the difference in semantics...

seancorfield21:02:58

Both function do a check on the args collection being in the atom and then a get on the atom, else a swap! assoc to update the atom.

seancorfield21:02:21

I'm having a hard time envisioning under what circumstances the semantics might be different -- using a basic cache -- since there's no eviction. That locking is actually on the anonymous function in through* which in turn calls (apply f args) for the original memoized f.

seancorfield21:02:44

(so, yeah, whatever potential differences there might be are certainly subtle but I can't express what they are 🙂 )

borkdude21:02:38

Maybe search the archives for this JIRA issue number? ;)

seancorfield21:02:14

Haha... I've tried that before and not turned anything up. Focusing on concurrent calls, I guess if two happen with memoize, f can be called twice -- after both threads check and do not find the cached value -- but for core.memoize/memo that isn't possible? Am I reading the d-lay code right in that respect?

borkdude21:02:07

That's what I was thinking of

seancorfield21:02:14

I suppose this should be easy enough to verify with a little bit of test code...

borkdude21:02:04

user=> (defn foo* [x] (prn x) x)
#'user/foo*
user=> (def foo (memoize foo*))
#'user/foo
user=> (run! (fn [i] (future (Thread/sleep 10) (foo :bar))) (range 10))
nil
user=> :bar:bar:bar
:bar

:bar:bar

:bar
:bar
:bar
:bar

borkdude21:02:48

I cannot reproduce this with core.memoize

seancorfield21:02:21

user=> (def p1 (promise))
#'user/p1
user=> (def p2 (promise))
#'user/p2
user=> (def a1 (atom 0))
#'user/a1
user=> (def a2 (atom 0))
#'user/a2
user=> (defn f1 [& _] (swap! a1 inc) _)
#'user/f1
user=> (defn f2 [& _] (swap! a2 inc) _)
#'user/f2
user=> (let [ff1 (memoize f1)] (dotimes [n 100] (future @p1 (ff1 1 2 3 4))))
nil
user=> (deliver p1 :go)
#object[clojure.core$promise$reify__8526 0x5d1e09bc {:status :ready, :val :go}]
;; wait a while
user=> @a1
3
user=> (require '[clojure.core.memoize :refer [memo]])
nil
user=> (let [ff2 (memo f2)] (dotimes [n 100] (future @p2 (ff2 1 2 3 4))))
nil
user=> (deliver p2 :go)
#object[clojure.core$promise$reify__8526 0x6c008c24 {:status :ready, :val :go}]
;; wait a while
user=> @a2
1
user=> 
Yup, that seems to confirm it.

seancorfield21:02:38

So the "subtle" issue is that, unlike memoize, clojure.core.memoize/memo prevents multiple invocations of the function with a given collection of arguments.