首页 > 解决方案 > 宏观构成

问题描述

我正在玩一些 Clojure 宏,特别是尝试了 map 的宏版本,我想象中将其命名为“mmap”:

(defmacro mmap [f coll]
    `(vector ~@(map #(list f %) coll)))

请不要深究我这样做的原因。这只是一个最小的可重现示例。

只要将集合传递给它,它就可以正常工作,但是当我尝试用另一个宏组合它时,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] NOPE. See the edit.

所以问题是:有没有办法强制扩展作为参数传递给另一个宏的宏?在这种情况下,怎么做?

感谢您的任何见解!

编辑我只是注意到我的示例没有意义,传递 mmap 宏的结果就像调用:

(mmap inc (vector 1 2 3 4))

在这种特定情况下这没有任何意义,但我仍然想知道它是否可以以某种方式完成

EDIT2使用 [1 2 3 4] 数组而不是符号,例如“arg”是为了传达整个事情是在编译时完成的,而不是运行时。

标签: clojuremacros

解决方案


函数总是在调用函数之前计算它们的参数,导致表达式以熟悉的“由内而外”模式进行计算:

(/ (inc 3) (dec 3))  ; orig
(/ 4 2)              ; args sent to `/` function
2                    ; final result

宏的目的是避免在调用函数之前计算参数,从而导致表达式被“从外向内”计算。这允许人们编写宏,就好像每个宏都是一种新的语言功能:

(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)))

在哪里可以看到在 2 次调用infix之前已经评估过。snoop这正是宏的强大之处,没有它它们将毫无用处。

您也许可以创建一个新的宏,其工作方式如下:

(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偶然发现的,而且是在几个月后才发现的。

请务必为Clojure CheatSheet添加书签,并始终保持浏览器选项卡对其打开。定期研究它,直到你能记住每个功能。:)

另请注意,那里没有列出一些更晦涩的项目!


更新:

宏的“杀手级功能”是添加新的语言功能。在 Java 等中,只能添加新库。例如,上面的 spyx 是一个与 snoop 示例非常相似的宏。有关其他示例,请参阅Tupelo 库中的with-exception-default、vals->map 和 it-> 。事实上,许多“核心”Clojure 特性,如 for 表达式或 and & 或逻辑运算符实际上是任何应用程序都可以编写(或修改!)的宏。


推荐阅读