How to memoize a function that uses core.async and non-blocking channel read?

百般思念 提交于 2019-12-22 08:57:36

问题


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? **


回答1:


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).




回答2:


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



来源:https://stackoverflow.com/questions/24698536/how-to-memoize-a-function-that-uses-core-async-and-non-blocking-channel-read

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!