如何在没有宏的情况下自动创建 Clojure `defn` 函数?

How to create Clojure `defn` functions automatically without macros?

最初受到以下问题的启发:Mapped calls to clojurescript macro


假设您想自动创建许多类似的功能(即无需全部手写)。假设我们有一些预先存在的函数,我们想用处理程序包装它们以进行某种回调:

(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)

结果:

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

我们希望自动而不是手动生成包装函数。

您可能认为您需要一个宏来创建此函数,这是一种解决方案。然而,宏的一个弱点是它们不能作为参数传递给另一个函数,例如 map。因此,我们可以编写如下宏:

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

但以下会失败:

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

我们如何自动生成这些函数?

概述

虽然您不能将 map 与宏一起使用,但您可以编写第二个宏来执行此功能。反过来,这可能需要编写第三个宏等,这是 Clojure for the Brave and True 和其他地方描述的短语 "Macros All the Way Down" 的来源。

一个类似的问题 was answered here,使用 Clojure 的 intern 函数。我们的问题与那个问题有点不同,因为这里我们以两种不同的方式使用 intern

  • 创建全局变量,如 defdefn
  • 使用 var-get
  • 访问全局变量的值

函数解

使用intern允许我们编写以下代码自动生成on-*函数而不使用宏:

(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)

结果:

 *** 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

所以我们看到我们创建了函数 on-fooon-baron-baz,它们依次调用全局 do-foodo-bar, & do-baz 函数。而且我们不需要使用宏!

在 Clojure 中,var 在某种程度上是一个不可见的 "middle-man",介于 on-foo 等符号和它指向的值(本例中为函数)之间。有关详细信息,请参阅相关 post:


宏解决方案

如前所述,可以使用一个宏来调用另一个宏,从而避免宏不能与 map 等高阶函数 (HOF) 一起使用的问题。这里我们定义了一个新的宏run-macro,来替换我们不能用generate-onstar-f:

map HOF
(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)

结果:

 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