Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure/Clojurescript: Argument to resolve must be a quoted symbol

I'm attempting to use a string saved in a variable to call a function like so:

(defn update-product-list [] "Test")

(defn handle-state-change [action]
  ((resolve (symbol action))))

(handle-state-change "update-product-list")

However, this gives me the following error: Assert failed: Argument to resolve must be a quoted symbol

I've also tried changing the above line to:

((resolve (quote (symbol action))))

But this still gives an error. I also tried changing it just to:

((resolve 'action))

But this gives a different error I don't quite understand: js/action is shadowed by a local. I don't want to override the function just call it. Not sure where I'm going wrong. I've looked at a few examples, but can't see to pin it down.

like image 410
newBieDev Avatar asked Mar 04 '23 17:03

newBieDev


1 Answers

ClojureScript supports :advanced optimization, in which Google Closure Compiler will rename, inline, or eliminate (unused) functions in order to implement minification. In short, the name of the function you want to look up will, in general, simply no longer exist under :advanced.

Because of this, ClojureScript's resolve is a compile-time facility (a macro requiring a literal quoted symbol).

If you are using :simple or self-hosted ClojureScript, more options are available to you because the support needed persists into runtime. For example Planck has a planck.core/resolve that behave's like Clojure's resolve. A similar approach is possible in Lumo, and similar facilities can be fashioned if using :simple.

In general though, given :advanced, if you need to map strings to a set of functions, you need to somehow arrange to have a static mapping constructed at compile time to support this (the set of functions must be known a priori, at compile time).

If you have a namespace (the name of which is statically known at compile time) which defines functions that need to be dynamically called via strings, you could consider making use of ns-publics:

cljs.user=> (ns foo.core)

foo.core=> (defn square [x] (* x x))
#'foo.core/square
foo.core=> (in-ns 'cljs.user)
nil
cljs.user=> (when-some [fn-var ((ns-publics 'foo.core) (symbol "square"))]
               (fn-var 3))
9

This will work under :advanced. The mapping constructed by ns-publics is static; built at compile-time. If you have multiple namespaces that need such treatment, you could merge several calls to ns-publics to build a larger map.

The advantage of this approach is that the code involved is pretty short and requires little maintenance. The disadvantage is that it dumps all of the public vars of the namespace (foo.core in this example) into your generated code (and the generated code for vars is somewhat verbose). Another disadvantage is that you need to statically know the namespace(s) involved at compile time.

If you need to further minimize generated code size, you could just build / maintain a simple static map from string to function value as in

(def fns {"square" foo.core/square})

and use it appropriately, keeping it up to date as your codebase evolves.

Another option would be to mark the functions that you need to access using ^:export meta, and then to call those functions using JavaScript interop. For example if you define the function this way

 (defn ^:export square [x] (* x x))

then you can use strings / interop to lookup the function and call it at runtime. Here's an example:

 ((goog.object/getValueByKeys js/window #js ["foo" "core" "square"]) 3)

The use of ^:export and :advanced is covered here. If you know that you are using :simple or less, then you can simply use JavaScript interop to call the functions of interest, without needn't to use ^:export.

Note that there is no general solution that would let you look up a function by name at runtime under :advanced without somehow putting some aspect of that function into your code at compile time. (In fact, if a function is not referenced in a way that Google Closure Compiler can statically, see, the function implementation will be completely eliminated as dead code.) In the above, ns-publics grabs all the vars for a namespace at compile time, rolling your own lookup map sets up static code to refer to the function value, and using ^:export statically arranges to make the name of the function persist into runtime.

like image 115
Mike Fikes Avatar answered Apr 29 '23 09:04

Mike Fikes