Clojure宏:引用、反引用和求值

3

I have the following code:

(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分支中分别返回true和false,由于我在cond表达式中使用了第二个版本,所以它会在不应该失败的地方失败(如果我没有弄错的话,我更喜欢第二个版本,因为它将在编译时评估)。我怀疑这可能与符号被命名空间限定有关?
2个回答

2

经过一些调查(请参见编辑),这里提供一个有效的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)]))
  • 首先,评估bufferfun并将它们绑定到本地变量
  • 返回一个闭包函数,接受一个参数,并根据给定的模式解构它。感谢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

该函数的目的是:针对给定的“模式”,创建一个函数,该函数接受由该模式描述的数据结构,并将其转换为字节。目标是在编译时创建此函数,并通过内联函数参数使其尽可能紧凑。因此,理想情况下,宏展开的函数不应包含类似于((fn [x] x) y)的列表。 - Viktor M.
@ViktorM。我编辑了答案(最终我会清理一切,但现在我正在尝试看看实际问题是什么)。 - coredump
虽然我不能真正理解CL代码(从未尝试过),但从我所理解的来看,它肯定很有前途。那个解构东西是我没有探索过的。关于::some情况:[::some ::int]基本上意味着数据结构包含一些整数,其数量在编译时不知道(这是我为自己定义的一些自定义DSL)。 - Viktor M.
1
关于你最后的编辑:你的代码看起来很漂亮,虽然需要一些时间来消化!如果我有进一步的问题,我会再联系你(希望你不介意)。 - Viktor M.
有一个小问题:'::some' 可以跟随任何有效的模式,而不仅仅是原始类型。我担心这可能是仅靠解构无法处理的。我会调查一下能做些什么,并在无法解决它时发布后续问题。 - Viktor M.
这有点复杂,但你需要在循环体内使用解构let来获取i的组件。看起来像是一个有趣的练习 :-) - coredump

2
在这一行中:
`(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的另一分支做出类似的更改。

顺便说一下,我第一眼看起来觉得这个不需要是宏,而可以是一个高阶函数:接收一个模式或其他内容,并返回一个数据函数。我猜测你正在使用宏来提高性能,在这种情况下,我建议您先尝试慢慢容易的方法;一旦您完成了这项工作,如果必要,再将其转换为宏。或者,也许我错了,有些东西在这里基本上必须是宏。


我已经尝试过你的建议,它可以正常运行,但是展开后的宏形式充满了像((fn [x] x) y)这样的表达式,我担心这会影响性能。我的尝试是将上述列表缩减为y(我查阅了definline和:inline元数据,但不知道它们是否与我的问题相关)。至于它是宏还是函数:我最近才开始涉足宏世界,因此可能情况是一个函数就足够了。 - Viktor M.
1
好吧,你不能通过省略语法引用来解决“愚蠢”的宏扩展问题:这样做会完全改变表达式的含义,变成基本上没有意义的东西。如果你有一个能正常工作的函数版本,那就使用它:如果在计时后发现它太慢了,那就提出另一个关于如何解决愚蠢的宏扩展的问题。 - amalloy
当你说“愚蠢”时,是指这是我的失误,还是这只是Clojure需要我接受的某种情况? - Viktor M.
我的意思是,你的宏已经扩展为你个人认为很傻的代码:((fn [x] x) y)而不是y。无论这是你的错还是Clojure的错(它不是Clojure的错),都与我的观点并不相关。直到你有证据证明这对你确实是一个实际的问题,而不仅仅是傻瓜,你可能会使用您已经编写过的工作的代码。如果您认为这是不足的,出于任何原因,您应该寻求改进已正确生成的代码的帮助,而不是提供一些错误的代码并说“我怎样才能让它正确”。 - amalloy
关于你上半句话的内容:我相信我已经表明了我对这个主题不是很熟悉,并且我准备说明与我的代码和动机有关的任何事情(是的,我之前做过研究)。尽管如此,我感谢你的反馈并感谢你将我引导到正确的轨道上。祝你拥有愉快的一天! - Viktor M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接