dphilipson
dphilipson

Reputation: 310

What is the ClojureScript equivalent of a JavaScript scope taking an argument

I want to write a program which adds complex behavior to a div (think some kind of interactive graphic, for example). I would like to be able to pass a div to a function in the library and have the library add behavior to the div.

If I were doing this in JavaScript, I would write the following.

In my_page.html:

<div id="program-container"></div>

<script src="my_library.js" type="text/javascript"></script>
<script type="text/javascript">
    useDivForACoolProgram(document.getElementById("program-container"));
</script>

In my_library.js:

function useDivForACoolProgram(div) {
    var x = 2;
    var y = 3;

    function setup() {
        div.innerHTML = "Getting ready to run...";
        doMainSetup();
    }

    function doMainSetup() {
        ...

    // Lots more functions, many of which refer to div
}

Note that the library exposes a single function which accepts a div. When we pass it a div, the library keeps all of its state in a closure associated with the passed div, which would potentially allow me to add this behavior to many divs on the page if I were so inclined.

I want to do the same sort of thing in ClojureScript. My first attempt was as follows:

(defn use-div-for-a-cool-program [div]
    (def x 2)
    (def y 3)

    (defn setup []
        (set! (.innerHTML div) "Getting ready to run...")
        (do-main-setup))

    (defn do-main-setup []
        ...

    ;; Lots more functions, many of which refer to div
)

But this doesn't work, because def and defn define variables at the module's scope rather than defining variables local to use-div-for-a-cool-program. If I were to call use-div-for-a-cool-program multiple times with different divs, all of the new defs and defns would override the old each time.

One solution would be to use let instead, but this is a bit unsatisfying because it forces us to give the implementation of functions before they can be referenced and is also hard to read, e.g.

(defn use-div-for-a-cool-program [div]
    (let [x 2
          y 3
          do-main-setup (fn []
                            ...)
          setup (fn []
                    (set! (.innerHTML div) "Getting ready to run...")
                    (do-main-setup))
          ;; Lots more functions, many of which refer to div
         ]
      (setup)))

Is there a better solution?

Upvotes: 0

Views: 158

Answers (1)

sbensu
sbensu

Reputation: 1521

My solution is not what you want to hear :) You pass div as an argument to each function. Instead of coupling the functions to div implicitly you specify explicitly that a div is needed:

(defn do-main-setup [div]
  ...)

(defn setup [div]
  (set! (.innerHTML div) "Getting ready to run...")

(defn use-div-for-a-cool-program [div]
    (let [x 2
          y 3]
      (do-main-setup div))
      ;; Lots more functions, many of which refer to div explicitly
      (setup div)))

Even if this is a little bit more verbose (as you pass div every time) it makes your intent clear. I use a closure when I want to return the function and call it later, remembering the scope in which it was defined. I don't see how it helps to define those functions as closures and call them precisely after.

If you want to keep some state associated to your div I would also do so explicitly. For example, if you want to keep count of how many clicks your div received I would do this:

(defn add-state-handler [div state]
  (set! (.onclick div) #(swap! state inc)))

(defn use-div-for-a-cool-program [div]
    (let [x 2
          y 3
          counter (atom 0)]
      (do-main-setup div))
      (add-state-handler div counter)
      ;; Other functions that reference div and counter
      (setup div)))

In short, I would avoid handling any state or mutable values (counter or div) implicitly. If you have to render a view that depends on some changing state I would recommend any Clojurescript React wrapper like Om or Reagent.

Upvotes: 1

Related Questions