1

Originally motivated by the following question: Mapped calls to clojurescript macro


Suppose you want to create many similar functions automatically (i.e. without hand-writing them all). Suppose we have some pre-existing functions and we want wrap them with handlers for a callback of some sort:

(defn do-foo [] (println "I foo'ed"))
(defn do-bar [] (println "I bar'ed"))
(defn do-baz [] (println "I baz'ed"))

(defn manual-on-foo [] (do-foo))
(defn manual-on-bar [] (do-bar))
(defn manual-on-baz [] (do-baz))

(println "Calling manual-on-* functions")
(manual-on-foo)
(manual-on-bar)
(manual-on-baz)

with results:

Calling manual-on-* functions
I foo'ed
I bar'ed
I baz'ed

We want to generate the wrapper functions automatically instead of manually.

You might think you need a macro to create this function, and that is one solution. However, a weakness of macros is that they cannot be passed as arguments to another function such as map. Thus, we could write a macro like:

(generate-fn :foo)  ;=> creates `on-foo` w/o hand-writing it

but the following would fail:

(map generate-fn [:foo :bar :baz])  

How can we automate the generation of these functions?

1 Answer 1

4

Overview

While you can't use map with a macro, you could write a second macro to perform this function. This may, in turn, require writing a third macro, etc, which is the origin of the phrase "Macros All the Way Down" as described in Clojure for the Brave and True and other places.

A similar question was answered here by using Clojure's intern function. Our problem is a little different than that question, since here we use intern in two different ways:

  • To create a global var like with def or defn
  • To access the value of a global var using var-get

Function Solution

Using intern allows us to write the following code to automatically generate the on-* functions without using macros:

(defn generate-onstar-f
  [event-kw]
  (let [
    event-str (name event-kw)
    do-fn-sym (symbol (str "do-" event-str))
    on-fn-sym (symbol (str "on-" event-str))
    new-fn    (fn on-fn-sym []
                (let [the-var (intern 'tst.clj.core do-fn-sym) ; get the var the symbol 'do-fn-sym' points to
                      the-fn  (var-get the-var) ] ; get the fn the var is pointing to
                  (the-fn))) ]
    (intern 'tst.clj.core on-fn-sym new-fn) ; create a var 'on-fn-sym' pointing to 'new-fn'
    (println "Created" on-fn-sym)))

(println \newline "*** generating functions ***")
(mapv generate-onstar-f [:foo :bar :baz]) ; creates and interns a functions:  my-foo, my-bar, my-baz

(println \newline "Calling automatically generated on-* functions")
(on-foo)
(on-bar)
(on-baz)

with results:

 *** generating functions ***
Created on-foo
Created on-bar
Created on-baz

 Calling automatically generated on-* functions
I foo'ed
I bar'ed
I baz'ed

So we see that we created the functions on-foo, on-bar & on-baz which, in turn, call the global do-foo, do-bar, & do-baz functions. And we didn't need to use macros!

In Clojure, the var is somewhat of an invisible "middle-man" between a symbol like on-foo and the value it points to (a function in this example). For more information please see the relate post:

When to use a Var instead of a function?


Macro Solution

As mentioned previously, one could use a macro to invoke another macro, side-stepping the problem that macros can't be used with higher-order-functions (HOF) like map. Here we define a new macro run-macro, to replace the map HOF we can't use with generate-onstar-f:

(defmacro generate-onstar-m
  [event-kw]
  (let [event-str (name event-kw)
        do-fn-sym (symbol (str "do-" event-str))
        on-fn-sym (symbol (str "on-" event-str "-m"))]
    (println "Creating" on-fn-sym)
    `(defn ~on-fn-sym []
       (~do-fn-sym))))

(println \newline "Using Macro")
(generate-onstar-m :foo)
(on-foo-m)

(defmacro run-macro
  "Run the specified macro once for each arg"
  [root-macro args]
  `(do
    ~@(forv [item args]
      `(~root-macro ~item))))

(println \newline "Generating on-*-m functions using `run-macro`")
(run-macro generate-onstar-m [:foo :bar :baz])
(on-foo-m)
(on-bar-m)
(on-baz-m)

with results:

 Using Macro
Creating on-foo-m
I foo'ed

 Generating on-*-m functions using `run-macro`
Creating on-foo-m
Creating on-bar-m
Creating on-baz-m
I foo'ed
I bar'ed
I baz'ed
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.