【Janet】宏
Janet支持宏:接受代码做为输入并输出转换后的代码的程序。宏类似于函数,但是它转换的是代码本身,而不是数据,因此比函数更灵活。宏可以让你扩展语言本身的语法。
我们已经见过了一些宏。 let, loop 和 defn 表达式都是宏。当编译器遇到宏时,首先求值宏,然后编译求值结果。当编译器对宏求值之后我们就说这个宏已经展开了。简单来说, defn 宏可以被认为是将下面的形式
(defn1 myfun [x] body)
转换成
(def myfun (fn myfun [x] body))
我们可以这样编写这个宏:
(defmacro defn1 [name args body](tuple 'def name (tuple 'fn name args body)))
虽然这个宏还有些问题,但是对于简单的函数是完全可用的。
第一个问题是我们的 defn1 宏不能用来定义函数体中包含多个表达式的函数。我们可以使用可变参数,就像函数一样,下面是第二个版本。
(defmacro defn2 [name args & body](tuple 'def name (apply tuple 'fn name args body)))
很好,现在我们可以定义函数体包含多个元素的函数了。
我们还可以继续优化这个宏。首先我们可以给它添加一个文档。如果稍后有人使用这个宏,就可以使用 (doc defn3) 来获取文档。其次我们可以使用内置的 quasiquoting 来重新这个宏。
(defmacro defn3"Defines a new function."[name args & body]~(def ,name (fn ,name ,args ,;body)))
它的功能和前一个版本的 defn2 是相同的,这样写宏的输出更清晰。开头的 ~ 是 (quasiquote x) 表达式的语法糖,它类似于 (quote x) 除了在它内部可以 unquote 表达式。 name 和 args 前面的逗号是 unquote,让我们可以将值放进 quasiquote。没有 unquote,符号 name 会被放进返回的元组中,然后我们定义的每一个函数都会叫做 name。
表达式(代码)会被求值到一个值,quote 是将表达式保持为代码,而 unquote 是将代码再次求值为结果。
除了 name,我们还必须 unquote body。但是不能使用普通的 unquote,我们可以看看直接使用 unquote 会发生什么。
(def name 'myfunction)
(def args '[x y z])
(defn body '[(print x) (print y) (print z)])~(def ,name (fn ,name ,args ,body))
# -> (def myfunction (fn myfunction (x y z) ((print x) (print y) (print z))))
函数体两边多出了一对额外的括号!我们并不想多余的括号,幸运的是,Janet 有 (splice x) 表达式可以实现这一点,使用他的简写形式 ;,结合 unquote 表达式,我们可以得到想到的输出:
~(def ,name (fn ,name ,args ,;body))
# -> (def myfunction (fn myfunction (x y z) (print x) (print y) (print z)))
意外的绑定捕获
当我们写宏是,有时必须生成一些本地绑定。以下面的宏为例:
(defmacro max1"Get the max of two values."[x y]~(if (> ,x ,y) ,x ,y))
它会求值 x 和 y 两次,因为它们都在宏中出现了两次。例如, (max1 (do (print 1) 1) (do (print 2) 2)) 会打印 1 和 2 两次,这与我们预想的不同。
我们可以优化如下:
(defmacro max2"Get the max of two values."[x y]~(let [x ,xy ,y](if (> x y) x y)))
现在我们没有了重复求值问题,但同时也引入了一个更微妙的问题。猜猜看下面的代码会返回什么?
(def x 10)
(max2 8 (+ x 4))
我们希望最大值是14,但实际上它的结果是12。如果我们把宏展开就很好理解了。在 Janet 中我们可以使用 (macex1 x) 函数将宏展开一次。完全展开使用 (macex x),注意,Janet 有很多宏,完全展开可能让代码很难看懂。
(macex1 '(max2 8 (+ x 4)))
# -> (let (x 8 y (+ x 4)) (if (> x y) x y))
展开之后可以看到, y 错误的引用了宏内部的 x (它被绑定到8)而不是那个绑定到10的 x。问题是我们在宏内部重用了符号 x,它隐藏了之前的绑定。这个问题被叫做 hygiene 问题,许多语言(如 Scheme)通过 hygienic 宏来解决这个问题。Hygienic 宏可以防止这种意外捕获,但是也会对宏的编写产生一些限制。Janet 宏系统没有使用 hygienic 宏,参见 Kohlbecker et al 1986 的论文,而是需要程序员自己去避免意外捕获。
Janet 为这个问题提供了一个通用的解决方案,使用 (gensym) 函数,它会保证返回一个唯一且不与已定义的符号冲突的符号。下面是完全正确的版本。
(defmacro max3"Get the max of two values."[x y](def $x (gensym))(def $y (gensym))~(let [,$x ,x,$y ,y](if (> ,$x ,$y) ,$x ,$y)))
因为在宏里面创建多个唯一符号非常常见,Janet 提供了 with-syms 宏来简化代码。
(defmacro max4"Get the max of two values."[x y](with-syms [$x $y]~(let [,$x ,x,$y ,y](if (> ,$x ,$y) ,$x ,$y))))
如你所见,宏威力巨大但也容易出bug。你需要牢记,宏只是输出代码的函数,而且它输出的代码必须在任何场景都能工作。多数情况下使用函数就足够了,而且比宏更有用,因为函数做为一等公民可以被任意传递。

