Clojure宏中的动态方法调用?

15

我试图编写一个宏,根据给定的参数调用Java setter方法。

例如:

(my-macro login-as-fred {"Username" "fred" "Password" "wilma"})

可能会扩展成以下内容:

(doto (new MyClass)
  (.setUsername "fred")
  (.setPassword "wilma"))

你会如何推荐解决这个问题?

具体而言,我在构建setter方法名以及让宏将其解释为符号方面遇到了困难。


你真的想用一个类作为第一个参数来调用 doto 吗?这样做会对类对象本身进行操作,而不是该类的实例。 - Brian Carper
啊,谢谢 - 那是个打错的字。我已经纠正了。 - npad
4个回答

20
宏的好处在于您不必深入了解类或任何其他内容。您只需要编写生成正确s表达式的代码。 首先,需要一个函数来生成像 (.setName 42) 这样的 s 表达式。
(defn make-call [name val]
  (list (symbol (str ".set" name) val)))

然后编写一个宏来生成表达式,并将它们插入到 doto 表达式中(~@)。

(defmacro map-set [class things]
  `(doto ~class ~@(map make-call things))

因为它是一个宏,所以它永远不必知道调用它的事物所属的类,甚至不必知道将要在哪个类上使用它。


无法工作。首先,你不能这样获取“.”的值。 - Brian Carper
修复以包含Brian Carper的更改。 - Arthur Ulfeldt

7
请不要使用list构建宏的s表达式,这将严重影响宏的卫生性。很容易犯错,难以追踪。请始终使用语法引用!尽管在这种情况下这不是问题,但养成只使用语法引用的习惯是好的! 根据您的映射源,您也可以考虑使用关键字作为键,使其看起来更像clojure。以下是我的想法:
(defmacro configure
  [object options]
  `(doto ~object
     ~@(map (fn [[property value]]
              (let [property (name property)
                    setter   (str ".set"
                                  (.toUpperCase (subs property 0 1))
                                  (subs property 1))]
                `(~(symbol setter) ~value)))
            options)))

这可以用作以下内容:
user=> (macroexpand-1 '(configure (MyClass.) {:username "fred" :password "wilma"}))
(clojure.core/doto (MyClass.) (.setUsername "fred") (.setPassword "wilma"))

我不同意。如果你不理解list的使用方式,那么它可能会很危险。但是,如果你只使用syntax-quote,那么很容易失去对宏内部工作原理的了解,并且认为syntax-quote是唯一的宏构建工具。有一件事情我会建议避免使用,那就是(map (fn [[x y]] ...) coll);这可以更清晰地表达为(for [[x y] coll] ...) - amalloy
是的,我不经常使用for。但是我坚持我的观点,关于列表。很容易犯引号与反引号的错误。在实际应用中这种错误也经常发生。即使是那些确切知道宏如何工作的人,在clojure.core和contrib中也会出现这种情况。语法引用可以消除一整类错误。总比事后修复要好。 - kotarak
好的,“class of bugs”可能有点过了。但它使符号捕获变得明确,而不是偶然发生的事情。 - kotarak
你可以很容易地同时使用:(list \first some-arg)`。 - amalloy
1
是的。而且键入(list 'first some-arg)很容易,而不太可能键入(~'first ~some-arg)而不是(first ~some-arg)。在这种情况下,甚至更短。如果你感觉舒服,可以使用list方法。但我会建议在95%的情况下避免使用它。(想象一下正确的位置有一些反引号。我不知道评论格式。) - kotarak
显示剩余2条评论

5

我相信是Arthur Ulfeldt曾经发布过一篇几乎正确的答案,但现在已被删除。

以下是一个可行版本:

(defmacro set-all [obj m]
  `(doto ~obj ~@(map (fn [[k v]]
                       (list (symbol (str ".set" k)) v))
                     m)))

user> (macroexpand-1 '(set-all (java.util.Date.) {"Month" 0 "Date" 1 "Year" 2009}))
(clojure.core/doto (java.util.Date.) (.setMonth 0) (.setDate 1) (.setYear 2009))

user> (set-all (java.util.Date.) {"Month" 0 "Date" 1 "Year" 2009})
#<Date Fri Jan 01 14:15:51 PST 3909>

3

你必须硬着头皮使用clojure.lang.Reflector/invokeInstanceMethod,像这样:

(defn do-stuff [obj m]
  (doseq [[k v] m]
    (let [method-name (str "set" k)]
      (clojure.lang.Reflector/invokeInstanceMethod
        obj
        method-name
        (into-array Object [v]))))
   obj)

(do-stuff (java.util.Date.) {"Month" 2}) ; use it

据我所知,不需要使用宏(至少对于一般情况而言,宏也无法规避反射)。请注意不要删除HTML标签。


宏并没有完全规避反射,至少在精神上没有。它盲目地生成s表达式,而不考虑它们恰好引用类的事实。 - Arthur Ulfeldt
使用反射来完成这个任务相当糟糕。宏和反射都是强大的工具,在使用之前应该三思而后行,但如果你可以将要设置的字段指定为文字,则宏似乎更加优雅。 - amalloy

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