Atom in let inside of function does not work

Hi everybody

I’m trying to figure out how atoms work when used inside a function inside let. I can see this example:

(defn memoize [f]
  (let [mem (atom {})]
    (fn [& args]
      (if-let [e (find @mem args)]
        (val e)
        (let [ret (apply f args)]
          (swap! mem assoc args ret)
          ret)))))

which doesn’t really make sense… I understand “closure” when it comes to normal variables (x=123 outside of fn will become static 123 inside in time of creating the fn) but anything in let should be erased so… I don’t get it…

To understand it I wrote my own example:

;; save a key and value into a map but do not override and remember
(defn save-something [save-this-key save-this-value]
  (let [my-storage (atom {})]
    (println "VERBOSE:" @my-storage)
    (if-let [found (find @my-storage save-this-key)]
      (val found)
      (do
        (swap! my-storage assoc save-this-key save-this-value)
        save-this-value))))

but it doesn’t work…

I know how to write the save-something function above when I define atom outside using (def my-storage (atom {})) but that’s not the point, I just would like to understand it inside let.

Can anybody please explain how does atom in let work / why the second example doesn’t work?

Thank you!!

Jeff

I think your misunderstanding is how exactly closures work in Clojure and the JVM.

I understand “closure” when it comes to normal variables (x=123 outside of fn will become static 123 inside in time of creating the fn)

This isn’t quite true; it does not become a “static” 123 when the function is created. Instead, when the the function is created it “closes” over the binding x and keeps that binding and its value in memory until it’s no longer used. This is true whether the value is an integer, an atom, or anything else.

The reason that your example doesn’t work is because you’re not creating a function that lives past the lifetime of the let expression, thus it never closes over the my-storage binding.

If you were to, for instance, move the (let [my-storage (atom {})] ,,,) outside of the (defn save-something ,,,) to wrap around it, then the save-something function would close over the my-storage binding, keeping it around and using it for the lifetime of the function.

1 Like

I think what you’re missing is that memoize takes a function and returns a function with that mem atom. That memoized function (not memoize itself) is what you eval and pass around to be evaluated elsewhere, and so it’s the same function with the same mem. The atom is closed over in the context of the (fn ...) that is returned. [Edited to add active voice: the (fn ...) that is returned closes over the fresh mem; both are reused.]

Your function is called with a k and a v, so you create a fresh my-storage every time.

It might be instructive to compare the expressions found in a REPL session using memoize to cache a value across multiple invocations versus using save-something to do the same.

my-storage is created (an atom with an empty map) every time save-something is invoked. What happens if you re-arrange things and move the let outside the defn, so my-storage is not created inside the function?

(let [my-storage (atom {})]
  (defn save-something [save-this-key save-this-value]
        (println "VERBOSE:" @my-storage)
        (if-let [found (find @my-storage save-this-key)]
          (val found)
          (do
            (swap! my-storage assoc save-this-key save-this-value)
            save-this-value))))

Thank you @lilactown

Static was a wrong term to use. I simply thing about that that x outside will inside turn into it’s value (unless x is the function argument).

Thank you @joinr and @dave.liepmann

You’re right. But could you please tell me why this doesn’t work?

;; this function will return a function
(defn create-fn-to-save-something []
  (fn [save-this-key save-this-value]
    ((let [my-storage (atom {})]
       (if-let [found (find @my-storage save-this-key)]
         (val found)
         (do
           (swap! my-storage assoc save-this-key save-this-value)
           save-this-value))))))

;; save anonymous function into save-something => make it named function
(def save-something (create-fn-to-save-something))

(save-something :red 123)
;; class java.lang.Long cannot be cast to class clojure.lang.IFn (java.lang.Long is in module java.base of loader 'bootstrap'; clojure.lang.IFn is in unnamed module of loader 'app')

That’s again the same mistake as in the first function. Every time you call create-fn-to-save-something you will get another function (no atom is created yet).
When you call that returned function, an atom is created, data is stored and at exit atom is destroyed.

Your actual error here is created from something different though. You have double parentheses around the let and Clojure tries to call a function. But there is no function but 123.

Ouhh, I didn’t see that. Thank you.

Ouuuh, yeah, I need to create it by(atom ...) outside and then place its symbol into the function => into closure.

(defn create-fn-to-save-something []
  (let [my-storage (atom {})]
    (fn [save-this-key save-this-value]
      (if-let [found (find @my-storage save-this-key)]
        (val found)
        (do
          (swap! my-storage assoc save-this-key save-this-value)
          save-this-value)))))

(def save-something (create-fn-to-save-something))

(save-something :red 123)

It’s working now ;-). Thank you @generateme and THANK YOU ALL FOR BEING PATIENT WITH ME.

4 Likes