调用宏定义中的宏定义

3
我正在尝试编写一个Clojure宏,从简单的中缀符号列表创建前缀符号列表以便进行计算,比如将(2 * 3 + 4 * 2)转换为(+ (* 2 3) (* 4 2))(结果返回14)。以下是我的代码:
(defmacro infix [op inlist]
  (let [[i1 i2 & i3] inlist
        last-group? (nil? (second i3))]  

    (if last-group?
      `(if (= ~op ~i2)
        (~i2 ~i1 ~(first i3)) ; return unevaluated prefix list
        (~i1 ~i2 ~(first i3))) ; return unevaluated infix list
      `(if (= ~op ~i2)
         ; recur with prefix list in i1 position
         (infix ~op ~(conj (rest i3) (list i2 i1 (first i3)) ))
         ; return as list: i1 and i2, recur i3 (probably wrong)
         (~i1 ~i2 (infix ~op ~i3))
         ))))

为了通过使用不同的op(操作函数)参数递归调用宏来执行运算符优先级:

(infix + (infix * (2 * 3 + 4 * 2)))

上面,我只是用两个*+来使用它,但最终我希望调用宏以适用于所有(或至少为了this exercise、/ * + -)运算符。
当我执行上述嵌套的宏调用时,我会得到以下错误:
CompilerException java.lang.RuntimeException: Can't take value of a macro: #'cbat.ch7.ex2/infix, compiling:(/tmp/form-init4661580047453041691.clj:1:1)

调用宏来处理单个运算符及其列表(例如(infix * (2 * 3 * 4)))可以按预期工作。如果我使用单个(i1 i2 i3)列表调用宏,如果opi2不同,则会尝试(可以理解地)返回未计算的带有错误的中缀列表:

ClassCastException java.lang.Long cannot be cast to clojure.lang.IFn  cbat.ch7.ex2/eval3003 (form-init4661580047453041691.clj:1)

我原本希望递归调用宏可以在整行被评估前处理未评估的中缀列表,但这似乎不起作用。
我相信后面的内部if(即(~i1 ~i2 (infix ~op ~i3)))的else分支是不正确的,我可能只需要内部的中缀调用,但我更关心在评估之前使不同运算符的嵌套宏调用正常工作。 我知道这不是将中缀转换为前缀表示法的常规方法,后来发现了Dijkstra's shunting-yard algorithm,但请问有人能告诉我: 1.是否可能存在这样的嵌套宏调用? 2.我的逻辑是否合理,离解决方案不太远?如果是这样... 3. ...我需要做哪些更改才能让事情正常运行? 我真的专注于学习Clojure,因此任何详细的解释(在可能的情况下)都将受到欢迎。
2个回答

2
您可以像以下代码示例一样嵌套宏调用:

(defmacro mac [tag & forms]
  `(do
     (println "mac - enter" ~tag)
     ~@forms
     (println "mac - exit " ~tag)))

(mac :a
  (doseq [i (range 3)]
    (mac :b (println i))))

mac - enter :a
mac - enter :b
0
mac - exit  :b
mac - enter :b
1
mac - exit  :b
mac - enter :b
2
mac - exit  :b
mac - exit  :a

您可以像以下示例一样进行递归宏调用:
(defmacro macr [n]
  (if (zero? n)
    1
    `(* ~n (macr ~(dec n)))))

(macr 5)   => 120

不深入研究您的特定实现,我建议有以下两点:

  1. 至少起初,尽可能简化表单。这意味着只使用类似于 (2 + 3) 的表单。尤其是在早期版本中不要强制宏计算运算符优先级(甚至永远不要进行此操作!)。

  2. 几乎从不需要使用宏,而且在学习Clojure和其他Lisp语言时,我认为它们被夸大其词了。我建议您在前一到两年甚至不要考虑它们,因为它们比函数更加脆弱,而且在重要方面不够强大(例如您不能将宏传递给函数)。

更新

无论何时想要编写复杂的东西(宏绝对合格!),都要从简单开始,逐步构建。使用 lein-test-refresh 插件 Tupelo 库 能够帮助您实现这一目标。

首先,创建最简单的宏并观察其行为:

(ns tst.clj.core
  (:use clj.core clojure.test tupelo.test)
  (:require [tupelo.core :as t] ))
(t/refer-tupelo)

(defn infix-fn [[a op b]]
  (spyx a)
  (spyx op)
  (spyx b)
  )

(defmacro infix [form]
  (infix-fn form))

(infix (2 + 3))

a => 2
op => +
b => 3

对于许多宏而言,将宏参数发送到像infix-fn这样的辅助函数中会很有帮助。 spyx通过打印符号及其值来帮助我们。此时,我们只需将参数重新排序为前缀表示法即可开始:

(defn infix-fn [[a op b]] (list op a b))

(defmacro infix [form] (infix-fn form))

(deftest master
  (is= 5 (infix (2 + 3)))
  (is= 6 (infix (2 * 3))))

如果我们有一个递归的树形结构,那该怎么办?在infix-fn中检查是否需要递归:

(declare infix-fn)

(defn simplify [arg]
  (if (list? arg)
    (infix-fn arg)
    arg))

(defn infix-fn [[a op b]]
  (list op (simplify a) (simplify b)))

(is= 7 (infix ((2 * 2) + 3)))
(is= 9 (infix ((1 + 2) * 3)))


(is= 35 (infix ((2 + 3) * (3 + 4))))
(is= 26 (infix ((2 * 3) + (4 * 5))))

我不想加入运算符优先级方面的复杂性。如果绝对必要,我不会亲自编写它,而是使用出色的Instaparse库来实现。

Alan,感谢你的回答,非常感激。我想说这是早期版本,但我已经试图做了几个星期。正如您建议的那样,我尝试简化事情,只保留三个符号形式的功能,但仍然遇到“java.lang.RuntimeException: Can't take value of a macro”错误。请问您是否能够提供一些具体的实现指针?非常感谢。 - Ooberdan
Alan,再次感谢 - 因为我没有本地的Clojurists小组可以讨论,所以这种详细程度正是我所需要的。 - Ooberdan
请务必加入Clojure Google Group:https://groups.google.com/forum/#!forum/clojure - Alan Thompson

2

扩展您的调用将为您提供线索:

(if (= + *) 
  (* infix (2 * 3 + 4 * 2)) 
  (infix * (2 * 3 + 4 * 2)))

你可能错误地认为,在宏的参数被展开之前,宏本身会被展开。但实际上在这个例子中:(~i2 ~i1 ~(first i3))i1 仍然是一个 infix 符号。据我所见,解决方法是添加一些新的条件分支,以特殊方式处理 infix 形式。

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