Clojure 宏:引用、取消引用和求值
Clojure macros: quoting, unquoting and evaluation
我有以下代码:
(ns macroo)
(def primitives #{::byte ::short ::int})
(defn primitive? [type]
(contains? primitives type))
(def pp clojure.pprint/pprint)
(defn foo [buffer data schema]
(println schema))
(defmacro write-fn [buffer schema schemas]
(let [data (gensym)]
`(fn [~data]
~(cond
(primitive? schema) `(foo ~buffer ~data ~schema)
(vector? schema) (if (= ::some (first schema))
`(do (foo ~buffer (count ~data) ::short)
(map #((write-fn ~buffer ~(second schema) ~schemas) %)
~data))
`(do ~@(for [[i s] (map-indexed vector schema)]
((write-fn buffer s schemas) `(get ~data ~i)))))
:else [schema `(primitive? ~schema) (primitive? schema)])))) ; for debugging
(pp (clojure.walk/macroexpand-all '(write-fn 0 [::int ::int] 0)))
问题是,在计算最后一个表达式时,我得到
=>
(fn*
([G__6506]
(do
[:macroo/int :macroo/int true false]
[:macroo/int :macroo/int true false])))
如有必要,我会解释代码,但现在我只说明问题(这可能只是我犯的新手错误):
`(primitive? ~schema)
和
(primitive? schema)
在 :else 分支 return 中分别为 true 和 false,并且由于我在 cond 表达式中使用了第二个版本,它在不应该的地方失败了(我更喜欢第二个版本如果我没记错的话,它会在编译时进行评估。
我怀疑这可能与名称空间限定的符号有关?
经过一些调查(查看编辑),这是一个有效的 Clojure 替代方案。基本上,您很少需要递归宏。如果你
需要递归地构建表单,委托给辅助函数并从宏中调用它们(另外,write-fn
不是一个好名字)。
(defmacro write-fn [buffer schemas fun]
;; we will evaluate "buffer" and "fun" only once
;; and we need gensym for intermediate variables.
(let [fsym (gensym)
bsym (gensym)]
;; define two mutually recursive function
;; to parse and build a map consisting of two keys
;;
;; - args is the argument list of the generated function
;; - body is a list of generated forms
;;
(letfn [(transformer [schema]
(cond
(primitive? schema)
(let [g (gensym)]
{:args g
:body `(~fsym ~schema ~bsym ~g)})
(sequential? schema)
(if (and(= (count schema) 2)
(= (first schema) ::some)
(primitive? (second schema)))
(let [g (gensym)]
{:args ['& g]
:body
`(doseq [i# ~g]
(~fsym ~(second schema) ~bsym i#))})
(reduce reducer {:args [] :body []} schema))
:else (throw (Exception. "Bad input"))))
(reducer [{:keys [args body]} schema]
(let [{arg :args code :body} (transformer schema)]
{:args (conj args arg)
:body (conj body code)}))]
(let [{:keys [args body]} (transformer schemas)]
`(let [~fsym ~fun
~bsym ~buffer]
(fn [~args] ~@body))))))
该宏采用 缓冲区(无论它是什么)、一个由您的语言定义的模式和一个要为生成的函数访问的每个值调用的函数。
例子
(pp (macroexpand
'(write-fn 0
[::int [::some ::short] [::int ::short ::int]]
(fn [& more] (apply println more)))))
... 生成以下内容:
(let*
[G__1178 (fn [& more] (apply println more)) G__1179 0]
(clojure.core/fn
[[G__1180 [& G__1181] [G__1182 G__1183 G__1184]]]
(G__1178 :macroo/int G__1179 G__1180)
(clojure.core/doseq
[i__1110__auto__ G__1181]
(G__1178 :macroo/short G__1179 i__1110__auto__))
[(G__1178 :macroo/int G__1179 G__1182)
(G__1178 :macroo/short G__1179 G__1183)
(G__1178 :macroo/int G__1179 G__1184)]))
- 首先,评估buffer和fun并将它们绑定到局部变量
- Return 一个接受 one 参数并根据给定模式对其进行解构的闭包,这要归功于 Clojure 的解构能力。
- 对于每个值,使用适当的参数调用 fun。
- 当架构为
[::some x]
时,接受零个或多个值作为向量并为每个值调用函数 fun。这需要通过循环来完成,因为大小只有在调用函数时才知道。
如果我们将向量 [32 [1 3 4 5 6 7] [2 55 1]]
传递给由上述宏展开生成的函数,将打印以下内容:
:macroo/int 0 32
:macroo/short 0 1
:macroo/short 0 3
:macroo/short 0 4
:macroo/short 0 5
:macroo/short 0 6
:macroo/short 0 7
:macroo/int 0 2
:macroo/short 0 55
:macroo/int 0 1
这一行:
`(do ~@(for [[i s] (map-indexed vector schema)]
((write-fn buffer s schemas) `(get ~data ~i)))))
您正在调用 write-fn
,即 宏 ,在您当前的范围内,其中 s
只是一个符号,而不是 schema
。相反,您想发出将 运行 在调用者范围内的代码:
`(do ~@(for [[i s] (map-indexed vector schema)]
`((write-fn ~buffer ~s ~schemas) (get ~data ~i)))))
并对 if
的另一个分支进行类似的更改。
顺便说一句,在我看来,乍一看这并不真的需要是一个宏,而是可以是一个高阶函数:接受一个模式或其他什么,然后 return 数据的函数。我的猜测是您将其作为性能的宏来执行,在这种情况下,我建议您先尝试缓慢而简单的方法;一旦你有了这个工作,你可以在必要时将它变成一个宏。或者,也许我错了,这里有些东西基本上必须是宏。
我有以下代码:
(ns macroo)
(def primitives #{::byte ::short ::int})
(defn primitive? [type]
(contains? primitives type))
(def pp clojure.pprint/pprint)
(defn foo [buffer data schema]
(println schema))
(defmacro write-fn [buffer schema schemas]
(let [data (gensym)]
`(fn [~data]
~(cond
(primitive? schema) `(foo ~buffer ~data ~schema)
(vector? schema) (if (= ::some (first schema))
`(do (foo ~buffer (count ~data) ::short)
(map #((write-fn ~buffer ~(second schema) ~schemas) %)
~data))
`(do ~@(for [[i s] (map-indexed vector schema)]
((write-fn buffer s schemas) `(get ~data ~i)))))
:else [schema `(primitive? ~schema) (primitive? schema)])))) ; for debugging
(pp (clojure.walk/macroexpand-all '(write-fn 0 [::int ::int] 0)))
问题是,在计算最后一个表达式时,我得到
=>
(fn*
([G__6506]
(do
[:macroo/int :macroo/int true false]
[:macroo/int :macroo/int true false])))
如有必要,我会解释代码,但现在我只说明问题(这可能只是我犯的新手错误):
`(primitive? ~schema)
和
(primitive? schema)
在 :else 分支 return 中分别为 true 和 false,并且由于我在 cond 表达式中使用了第二个版本,它在不应该的地方失败了(我更喜欢第二个版本如果我没记错的话,它会在编译时进行评估。
我怀疑这可能与名称空间限定的符号有关?
经过一些调查(查看编辑),这是一个有效的 Clojure 替代方案。基本上,您很少需要递归宏。如果你
需要递归地构建表单,委托给辅助函数并从宏中调用它们(另外,write-fn
不是一个好名字)。
(defmacro write-fn [buffer schemas fun]
;; we will evaluate "buffer" and "fun" only once
;; and we need gensym for intermediate variables.
(let [fsym (gensym)
bsym (gensym)]
;; define two mutually recursive function
;; to parse and build a map consisting of two keys
;;
;; - args is the argument list of the generated function
;; - body is a list of generated forms
;;
(letfn [(transformer [schema]
(cond
(primitive? schema)
(let [g (gensym)]
{:args g
:body `(~fsym ~schema ~bsym ~g)})
(sequential? schema)
(if (and(= (count schema) 2)
(= (first schema) ::some)
(primitive? (second schema)))
(let [g (gensym)]
{:args ['& g]
:body
`(doseq [i# ~g]
(~fsym ~(second schema) ~bsym i#))})
(reduce reducer {:args [] :body []} schema))
:else (throw (Exception. "Bad input"))))
(reducer [{:keys [args body]} schema]
(let [{arg :args code :body} (transformer schema)]
{:args (conj args arg)
:body (conj body code)}))]
(let [{:keys [args body]} (transformer schemas)]
`(let [~fsym ~fun
~bsym ~buffer]
(fn [~args] ~@body))))))
该宏采用 缓冲区(无论它是什么)、一个由您的语言定义的模式和一个要为生成的函数访问的每个值调用的函数。
例子
(pp (macroexpand
'(write-fn 0
[::int [::some ::short] [::int ::short ::int]]
(fn [& more] (apply println more)))))
... 生成以下内容:
(let*
[G__1178 (fn [& more] (apply println more)) G__1179 0]
(clojure.core/fn
[[G__1180 [& G__1181] [G__1182 G__1183 G__1184]]]
(G__1178 :macroo/int G__1179 G__1180)
(clojure.core/doseq
[i__1110__auto__ G__1181]
(G__1178 :macroo/short G__1179 i__1110__auto__))
[(G__1178 :macroo/int G__1179 G__1182)
(G__1178 :macroo/short G__1179 G__1183)
(G__1178 :macroo/int G__1179 G__1184)]))
- 首先,评估buffer和fun并将它们绑定到局部变量
- Return 一个接受 one 参数并根据给定模式对其进行解构的闭包,这要归功于 Clojure 的解构能力。
- 对于每个值,使用适当的参数调用 fun。
- 当架构为
[::some x]
时,接受零个或多个值作为向量并为每个值调用函数 fun。这需要通过循环来完成,因为大小只有在调用函数时才知道。
如果我们将向量 [32 [1 3 4 5 6 7] [2 55 1]]
传递给由上述宏展开生成的函数,将打印以下内容:
:macroo/int 0 32
:macroo/short 0 1
:macroo/short 0 3
:macroo/short 0 4
:macroo/short 0 5
:macroo/short 0 6
:macroo/short 0 7
:macroo/int 0 2
:macroo/short 0 55
:macroo/int 0 1
这一行:
`(do ~@(for [[i s] (map-indexed vector schema)]
((write-fn buffer s schemas) `(get ~data ~i)))))
您正在调用 write-fn
,即 宏 ,在您当前的范围内,其中 s
只是一个符号,而不是 schema
。相反,您想发出将 运行 在调用者范围内的代码:
`(do ~@(for [[i s] (map-indexed vector schema)]
`((write-fn ~buffer ~s ~schemas) (get ~data ~i)))))
并对 if
的另一个分支进行类似的更改。
顺便说一句,在我看来,乍一看这并不真的需要是一个宏,而是可以是一个高阶函数:接受一个模式或其他什么,然后 return 数据的函数。我的猜测是您将其作为性能的宏来执行,在这种情况下,我建议您先尝试缓慢而简单的方法;一旦你有了这个工作,你可以在必要时将它变成一个宏。或者,也许我错了,这里有些东西基本上必须是宏。