Clojure 宏观成分
我正在玩Clojure宏,特别是尝试map的宏版本,我很有想象力地将其命名为“mmap”: 请不要深入探究我这样做的原因。这只是一个最小的可重复的例子 只要将集合传递给它,它就可以正常工作,但当我尝试用另一个宏组合它时,mmap会用宏本身而不是它的扩展来分割sexp 我希望“mmap”的工作方式如下:Clojure 宏观成分,clojure,macros,Clojure,Macros,我正在玩Clojure宏,特别是尝试map的宏版本,我很有想象力地将其命名为“mmap”: 请不要深入探究我这样做的原因。这只是一个最小的可重复的例子 只要将集合传递给它,它就可以正常工作,但当我尝试用另一个宏组合它时,mmap会用宏本身而不是它的扩展来分割sexp 我希望“mmap”的工作方式如下: (mmap inc [1 2 3 4]) => [2 3 4 5] (mmap dec (mmap inc [1 2 3 4])) => [1 2 3 4] NO
(mmap inc [1 2 3 4]) => [2 3 4 5]
(mmap dec (mmap inc [1 2 3 4])) => [1 2 3 4] NOPE. See the edit.
所以问题是:有没有办法强制将一个宏作为参数传递给另一个宏?在这种情况下,如何
感谢您的任何见解
编辑我刚刚注意到我的示例没有意义,传递mmap宏的结果就像调用:
(mmap inc (vector 1 2 3 4))
这在这个特殊的案例中毫无意义,但我仍然想知道它是否能以某种方式实现
EDIT2使用[1 2 3 4]数组而不是“arg”这样的符号是为了传达整个过程是在编译时完成的,而不是在运行时。函数总是在调用函数之前对其参数进行求值,导致表达式以熟悉的“由内而外”模式求值: 宏的目的是避免在调用函数之前对参数进行求值,从而导致表达式的求值结果为“由外而内”。这样,就可以像编写新的语言功能一样编写宏:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test))
(defmacro infix
[[l op r]]
`(~op ~l ~r))
(defmacro snoop
[& forms]
`(let [result# ~@forms]
(do
(println "snoop => " result#)
result#)))
(dotest
(spyx (infix (2 + 3)))
(spyx (infix (5 - 3)))
(newline)
(snoop (+ 1 2))
(snoop (infix (4 + 5)))
(newline)
(println :last
(infix ((snoop 4) + (snoop 5)))))
结果
(infix (2 + 3)) => 5
(infix (5 - 3)) => 2
snoop => 3
snoop => 9
snoop => 4
snoop => 5
:last 9
最后一个例子有一个中间阶段,看起来像:
(println :last
(+ (snoop 4) (snoop 5)))
在哪里可以看到infix
在2snoop
调用之前已经被计算过。这正是宏的强大之处,如果没有它,宏将毫无用处
您可以创建一个新宏,其工作方式如下:
(defmacro mmap
[fns coll]
`(let [comp-fn# (comp ~@fns)]
(mapv comp-fn# ~coll)))
(mmap [inc inc inc] [1 2 3]) => [4 5 6]
虽然上面的
mmap
有效,但它实际上是对宏系统的滥用,如果您给它一些编译时常量以外的东西,它可能会失败
基本函数的一个更好的替代方法是对正常函数的简单记忆:
(def do-stuff
(memoize ; will only execute on the first usage
(fn
[coll]
(mapv #(* 2 %) (mapv inc coll)))))
(do-stuff [1 2 3 4]) => [4 6 8 10]
如果您认为即使在运行时进行一次调用也太多,我建议您在预编译步骤中手动运行“类似宏”的代码,并将结果保存在新的“源文件”中,然后可以像往常一样进行编译(例如,您可能正在预计算一组素数)
结果是宏系统仅用于操作代码以添加新的语言功能。使用不同的技术可以更好地实现任何其他目标
关于
mapv
我也在几个月后偶然发现了mapv
请确保为其添加书签并始终打开浏览器选项卡。定期学习,直到你能记住每个功能。:)
还要注意的是,这里没有列出一些更模糊的项目
更新:
宏的“杀手特性”是添加新的语言特性。在Java等语言中,只能添加新的库。例如,上面的spyx是一个与snoop示例非常相似的宏。有关其他示例,请参见例外默认值、VAL->map和it->from。事实上,许多“核心”Clojure功能(如for表达式或and&or逻辑运算符)实际上是任何应用程序都可以编写(或修改!)的宏。宏扩展从最外层的形式开始工作,并重复调用,直到达到固定点为止。这意味着从宏中,您将获得未展开的代码。如果您希望它像函数一样工作,即在调用函数之前对参数求值,那么这可能是不合理的。使用宏时,不会展开任何内容,也不会计算任何内容,这使得您可以按照自己的意愿解释底层代码 但您也可以从宏中自由调用自己,这将确保首先展开嵌套表单。在您的例子中,这意味着使用文本值时,您将返回数据,并且您可以处理macroexpanded值,就像它被直接提供给您的宏一样
但请注意,只有当对宏的最内部调用被赋予一个文本值并返回一个文本值时,这才起作用。在任何情况下,宏都无法知道运行时哪个值可用。通常人们会尝试直接在表单上调用
eval
,但这不是一个好主意,因为代码依赖于运行时上下文,一般来说,在宏扩展/编译过程中对应该在运行时进行评估的代码进行评估是没有意义的。您可以在coll
上调用macroexpand,以确保首先进行宏扩展,前提是您始终有文字参数。如果集合是仅在运行时才知道的值,则在宏扩展期间无法对其应用函数。是的,整个过程是在宏扩展期间执行的,而不是在运行时执行的。还有,@coredump谢谢你的提示!既然你的建议行得通,你介意把它总结成一个答案,这样我就可以接受了吗?一个非常详尽和有教育意义的答案!我的实际代码是为了做一些不同于(令人困惑的)示例的事情。我想组合一个“映射”宏和一个“过滤”宏,但读了你的解释后,我意识到a)即使我让它工作起来,它确实是一件脆弱的事情b)我的思维方式是错误的:被评估的宏不是我们想象的那样,最终复合宏将是一个更好的解决方案。虽然我对编译时/vs运行时有相当的理解,但您的回答帮助我进一步了解了这一点,这也是我接受它的原因。非常感谢。顺便说一句,我不知道有mapv。在写了这么多次之后,我应该知道:p不确定您打算用“映射宏”和“过滤宏”编写什么。如果(map fa(filter fb coll))
或类似(>>coll(filter fb)(map fa))
的线程的基本组成不可取,请记住也检查Clojure传感器。我的目标是消除过度使用
(defmacro mmap
[fns coll]
`(let [comp-fn# (comp ~@fns)]
(mapv comp-fn# ~coll)))
(mmap [inc inc inc] [1 2 3]) => [4 5 6]
(def do-stuff
(memoize ; will only execute on the first usage
(fn
[coll]
(mapv #(* 2 %) (mapv inc coll)))))
(do-stuff [1 2 3 4]) => [4 6 8 10]