Refactoring global atom to dynamically bound one

I have a code project library that had a global atom to service a web component. That design had a fault that, having one global atom, it couldn’t service multiple components on the same page. In redesigning to allow each component to have it’s own state, I realized this could be my first opportunity to employ dynamic scope to give each component its own copy of the atom. But somewhere in the flow of functions I am losing my binding, possibly when using an anonymous function in an on-click that should change the once-global atom.

Has anyone dealt with this situation? Any tips on refactoring a global atom to a dynamic one?

I don’t know that I fully grok what you’re trying to do, but my first reaction would be “don’t”. If you do need global state per component (a la re-frame?), why not use just one atom and different paths under it?

2 Likes

You’re right; I could dynamically generate the paths in such a way as to have a unique path to each instance. I just thought I’d finally found a true usecase for dynamic binding, which would have been an elegant way to quickly refactor my global atom into something like a closure. But I think it’s tripping on execution timings: when I define the sub-element’s on-click lambda, that lambda is perhaps created outside (before) the binding control and so it references the unbound (invalid) value. Further testing today will see if my explanation is correct.

Well, there are arguably good use cases for using binding and dynamic vars, but I’d tend to minimize (or avoid altogether) their use (just because we try and avoid global state, generally).

I agree completely with the distaste for global state and the problems it can cause. My understanding is that dynamic context (re: dynamic binding) exists precisely as an alternative to global state: hence “context.”

Martín Varela via ClojureVerse [email protected] writes:

The value bound to a dynamic var is only available within the scope of the binding. If you have anything accessing the binding after that, you’ll get the root value instead.

So you need to be careful with lazy sequences and closures. In both case, you need to be sure you only use the lazy-seq and closure within the scope as well. Because their access to the binding is delayed till realization/evaluation, they often cause scenarios where you mistakenly allow them to escape the scope without realizing that they’ll now refer to the root value or some other binding.

When you close over the binding, you close over the variable, not its value.

One way to fix that is too make sure you only use lazy-seqs or closures within the intended scope.

If you need it used beyond the scope, you need to be sure to capture the value, instead of the var. You can do that by copying the binding value to a local binding and closing over that instead:

(def ^:dynamic foo 10)

;; When evaluating the fn outside the scope we get the root value
((binding [foo 20]
  (fn[] foo)))
10

;; Inside the scope it works
(binding [foo 20]
  ((fn[] foo)))
20

;; We first get the value from inside the scope and locally bind it to a and close over a instead.
((binding [foo 20]
  (let [a foo]
    (fn[] a))))
20

I see. Are dynamic bindings just useless for front-end work? Here’s the general shape of my code:

(def :dynamic *ATOM2B* nil)

(defn print-thing [] (prn @*ATOM2B*))

(defn render-page []
  (binding [*ATOM2B* (atom {:val "A real value"})]
    [:a {:on-click print-thing} "Click Me"]))

;; Clicking on the thing in browser:
;; Error: No protocol method IDeref.-deref defined for type null

It actually doesn’t matter if I make print-thing into an inline #() fn; either way, ATOM2B has escaped scope.

It just doesn’t sound you’re looking for dynamic scope.

Dynamic scope means you want the lifetime of the value to be from the time the call stack enters the binding to the time it leaves it.

In your case, you want the binding to live beyond the scope, to a time later when the user clicks the button.

You can still do it by closing over the value. That said, keep in mind the variable will no longer point to the atom after the binding escapes. So if you intend to have it be global so that you can mutate it from outside the scope, using the global var, that won’t work.

(def :dynamic *ATOM2B* nil)

(defn make-print-atom-fn [atom]
  #(prn @atom))

(defn render-page []
  (binding [*ATOM2B* (atom {:val "A real value"})]
    [:a {:on-click (make-print-atom-fn *ATOM2B*)} "Click Me"]))

This will work, by closing over the value, and not the variable. But the ATOM2B no longer points to the atom after the binding. So you can’t for example do:

(swap! *ATOM2B* assoc :foo :bar)

Unless you do it within the binding, otherwise it won’t work.

I don’t want to say its never useful for front end code, it could very well be, but maybe not in your case, not sure.

In your case I wonder… do you need a global variable at all? Can’t you just replace that binding with a let ?

2 Likes

Excellent thoughts; thank you. I certainly could replace it with a let, and that seems like the “right” thing to do, but it would involve passing that variable around all over the place to the 3 or 4 branches of the code that refer to it. The code is built so several disparate functions all look at the namespace-global variable and read it for list arrangement, change it for sorting reversal, and compare against it for some equality checks – all stuff that works fine for one subscriber, but fails when multiple instantiations are needed. The hope was that dynamic binding might allow the simplest refactor, all the code that thinks of it as a global variable requiring no changes and no extra arguments needing passing. However, it looks like writing something like an on-click hook to the DOM breaks out of the dynamically bound scope and is not to be. This was basically my question, with uncertainty as to how mounting things in the DOM plays with CLJS function scope. It sounds like an answer from the DOM is, “don’t try anything fancy.”

Does capturing the value using a let or fn arg like I showed not work in CLJS ?

Yeah, passing the atom down the tree will work, and probably won’t require a dynamic binding at all. I think my pain point was in figuring out the scope: for an on-click, would my dynamic binding be respected because I’m defining the on-click under bound context, or would it fail because the actual click occurs outside dynamic binding? In other words, I seem to have hit a crucial difference between the functioning of a dynamic binding and a closure.

Maybe we’re saying the same thing, but I don’t mean to just pass the atom around. I mean to use dynamic binding, but when you create a closure or a lazy seq, have them capture the atom, not the dynamic var.

Suggestion:

  1. A single global atom which is a map
  2. Each “consuming namespace” has its own key, for instance ::db

Something like

(ns app.db)

(defonce state (atom {}))
(ns app.mod1
  (:require [app.db :as db]))

;; Update visitor count
(swap! db/state
       update-in [::db :visitor-count] inc)

;; Get visitor count
(get-in @db/state [::db visitor-count])

Does that fit?


Meta: It feels like this discussion approaches datascript or re-frame.

It definitely does. If it’s in a web context, I think that re-frame (possibly with posh) would be the way to go, rather than re-inventing the wheel.

Same thought and feelings about re-frame. But how portable is re-frame? My library is principally cljc and I haven’t looked in to whether that’s possible with reframe. If it is, that is definitely the right way to do global vars.

Martín Varela via ClojureVerse [email protected] writes:

As I understand it, it’s meant for front-end work (it specifically builds on top of Reagent, which is itself a React wrapper), but the code seems to be mostly clcj. The “reactive” approach could make sense also for SSR, of course, but most likely re-frame would not be the right fit there. Maybe fulcro would be closer to what you need, but tbh I haven’t yet wrapped my head around it.

It might help to know exactly what you’re trying to do, and what your requirements are…

Maybe Rum and Citrus could be valid options here? React wrapper and state management with support for server side rendering. I haven’t used any of them, so I don’t know if they provide the same kind of state abstraction as re-frame though. Just wanted to bring them to your attention. :slightly_smiling_face:

2 Likes

Another alternative is consider incorporating some env map in all your functions, I find this pattern pretty useful and extensible, if you add this env in most of the functions in the middle, this can be a map that contains anything you want. One example is ring request, or pedestal interceptors, there is map that flows on your system, making a part of the system allows you do to any kind of changes. The dynamic binding for example, is quite trick if you need to use two different ones at the same time, the code gets messy quickly, while having it as an explicit param is liberating. So, in general, global state is evil, avoid it as much as possible.

2 Likes

I find this pattern nice when you’re in “exploration mode”, and quickly iterating / prototyping. I also tend to refactor it out once I figure out my API (when it makes sense, anyway).

1 Like