Skip to content

Instantly share code, notes, and snippets.

@dustingetz
Last active January 25, 2020 07:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dustingetz/72cf2376bd2ef027bf2f47e3befa3de5 to your computer and use it in GitHub Desktop.
Save dustingetz/72cf2376bd2ef027bf2f47e3befa3de5 to your computer and use it in GitHub Desktop.
    (def s (r/atom 0))

    (defn foo []
      [:input {:type "text" 
               :value @s 
               :on-click #(reset! s (.-target.value %)}]) ; unstable closure reference
      
      
    (defn change [e] (reset! s (.-target.value e)) ; you're suggesting we name it
    (defn foo2 []
      [:input {:type "text"
               :value @s
               :on-click change}]) ; which works here - this is now a stable function reference. Good!
    
    ; But what if the named function is not static, but a closure
    (defn foo3 [something]
      (let [change' (fn [e] ; closure instance is rebuilt each render
                      (if something
                        (reset! s (.-target.value e))))]
        [:input {:type "text"
               :value @s
               :on-click change'}]))   ; unstable closure reference

The answer generally is to do what you say and hoist up any closures to static scope, like foo2 here. Even foo3 can be written into a hoisted static function with additional parameters, and use reagent.core/partial instead of clojure.core/partial to preserve stabiltiy. But it is not always reasonable and it can cause great contortions in your code when you're basically no longer allowed to write (fn []) ever again.

@didibus
Copy link

didibus commented Jan 23, 2020

I was thinking:

(defn foo []
      [:input {:type "text" 
               :value @s 
               :on-click (fn [e] foo-clicked (reset! s (.-target.value e))}])

And if reagent could reflect on the Fn object and get the name foo-clicked from it. Or alternatively:

(defn foo []
      [:input {:type "text" 
               :value @s 
               :on-click ^{:name :foo-clicked} #(reset! s (.-target.value %)}])

@dustingetz
Copy link
Author

dustingetz commented Jan 24, 2020

@didibus If Reagent reflected the function (closure!!!) name in order to stabilize it, it would introduce incorrect/buggy behavior, because the closure may indeed have re-evaluated in a way that in fact shouldn't compare equal. Consider foo3 where something parameter (closed over from lexical scope) evaluates differently

@didibus
Copy link

didibus commented Jan 25, 2020

Ya, I think that's where I meant, I don't really know what React is trying to achieve exactly here, so if two functions that close over different values shouldn't be equal this won't work. But, then I don't understand how it works for top level functions with parameters either.

Like:

(let [some-val (grab-value-from-somewhere-dynamically)]
  [:input {:type "text"
               :value @s
               :on-click (fn foo-clicked [] (js/alert some-val)}])

The on-click closure above, is no different from:

(defn foo-clicked [val]
  (js/alert val))
[...]
(let [some-val (grab-value-from-somewhere-dynamically)]
  [:input {:type "text"
               :value @s
               :on-click (foo-clicked some-val)}])

Well I don't see what is different if it is, in that, what variables it closes over is static, but ya, the value captured could be different. OH, okay it just clicked in my brain. Are we saying that when ClojureScript closes over a variable, it actually copies the value in it into the closed function? I guess I never really thought about that. Here I assumed it copied the local variable, thus it would still contextually resolve the value in it when it runs, and as such, I thought of it much more like a parameter.

Hum, ok. Now I wonder, could a macro be written that is like fn but refuses to close over things? Like detects a closure and throws at compile time. Now if you used that in combination with what I mentioned above, it would be a great reminder to never close over a value, but instead take a parameter or use the partial from reagent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment