How to memoize a function that uses core.async and

2019-05-04 15:55发布

I'd like to use memoize for a function that uses core.async and <! e.g

(defn foo [x]
  (go
    (<! (timeout 2000))
    (* 2 x)))

(In the real-life, it could be useful in order to cache the results of server calls)

I was able to achieve that by writing a core.async version of memoize (almost the same code as memoize):

(defn memoize-async [f]
  (let [mem (atom {})]
    (fn [& args]
      (go
        (if-let [e (find @mem args)]
          (val e)
         (let [ret (<! (apply f args))]; this line differs from memoize [ret (apply f args)]
            (swap! mem assoc args ret)
            ret))))))

Example of usage:

(def foo-memo (memoize-async foo))
(go (println (<! (foo-memo 3)))); delay because of (<! (timeout 2000))

(go (println (<! (foo-memo 3)))); subsequent calls are memoized => no delay

I am wondering if there are simpler ways to achieve the same result.

**Remark: I need a solution that works with <!. For <!!, see this question: How to memoize a function that uses core.async and blocking channel read? **

2条回答
\"骚年 ilove
2楼-- · 2019-05-04 16:22

You can use the built in memoize function for this. Start by defining a method that reads from a channel and returns the value:

 (defn wait-for [ch]
      (<!! ch))

Note that we'll use <!! and not <! because we want this function block until there is data on the channel in all cases. <! only exhibits this behavior when used in a form inside of a go block.

You can then construct your memoized function by composing this function with foo, like such:

(def foo-memo (memoize (comp wait-for foo)))

foo returns a channel, so wait-for will block until that channel has a value (i.e. until the operation inside foo finished).

foo-memo can be used similar to your example above, except you do not need the call to <! because wait-for will block for you:

(go (println (foo-memo 3))

You can also call this outside of a go block, and it will behave like you expect (i.e. block the calling thread until foo returns).

查看更多
forever°为你锁心
3楼-- · 2019-05-04 16:31

This was a little trickier than I expected. Your solution isn't correct, because when you call your memoized function again with the same arguments, sooner than the first run finishes running its go block, you will trigger it again and get a miss. This is often the case when you process lists with core.async.

The one below uses core.async's pub/sub to solve this (tested in CLJS only):

(def lookup-sentinel  #?(:clj ::not-found :cljs (js-obj))
(def pending-sentinel #?(:clj ::pending   :cljs (js-obj))

(defn memoize-async
  [f]
  (let [>in (chan)
        pending (pub >in :args)
        mem (atom {})]
    (letfn
        [(memoized [& args]
           (go
             (let [v (get @mem args lookup-sentinel)]
               (condp identical? v
                 lookup-sentinel
                 (do
                   (swap! mem assoc args pending-sentinel)
                   (go
                     (let [ret (<! (apply f args))]
                       (swap! mem assoc args ret)
                       (put! >in {:args args :ret ret})))
                   (<! (apply memoized args)))
                 pending-sentinel
                 (let [<out (chan 1)]
                   (sub pending args <out)
                   (:ret (<! <out)))
                 v))))]
        memoized)))

NOTE: it probably leaks memory, subscriptions and <out channels are not closed

查看更多
登录 后发表回答