一般来说,我们会避免在应用项目中编写宏,无论是 c 那种比较简单的宏,还是rust 那种约束明确的,或者 Lisp 那种功能比较完善的宏机制。这主要是因为宏写起来比较困难,容易写错。项目开发通常来说有时间压力,要优先使用风险可控的技术。
但是这并不表示应该禁止写宏,特别是 Lisp 系的语言,宏可以有效提高生产力。既然在应用项目中实现宏不够经济,就更应该在平时准备好可以帮助我们工作的宏。
编写宏的时候,要时刻记住这么几个原则:宏在编译期(严格的说是预处理时)执行,而非运行时,所以如果程序逻辑中使用了运行时才能得到的信息,宏可能不适用
宏可以改变语法形式,但是应该仅仅在能够提高可读性的时候才这样做
宏的输出结果是“程序代码”,对于clojure这样的lisp语言来说,宏输出的是程序 List ,它会成为 AST 的一部分,而它本身也是一种程序代码,也遵循Clojure语法。
在夏天的时候,我为当时那个版本的撮合引擎写过一组测试:
...
(Thread/sleep 1000)
(.tell dash-actor depth-query world-ref)
(await)
(.expectMsgPF testKit "dash should empty"
(reify Function
(apply[this msg]
(is (=0 (.getVersion msg)))
(is (.isEmpty (.getAsk msg)))
(is (.isEmpty (.getBid msg))))))
(.tell dash-actor limit-bid world-ref)
(await)
(.expectMsgPF testKit "now one limit bid should become a maker"
(reify Function
(apply[this msg]
...
这里面大量的 `reify Function` ,是为了给 testkit 传递消息验证逻辑,现代的 Java ,反而简单的写一个lambda,由编译器自动推导生成这个 Function 变量。实践来说,这一层类型结构并没有带来必要的信息量,它只是一个适配结构,现代Java的这种进步是有意义的。有没有办法可以简化Clojure的 reify逻辑,让 clojure 代码也精简到只需要一层呢?
那就是宏:
(defmacrofunction [[this arg] & body]
`(reify Function
(apply[~this ~arg]
~@body)))
调用这个宏,跟调用 fn 基本一致:
(nsjaskell.handle-test
(:require [clojure.test :refer :all])
(:require [jaskell.handle :as h])
(:import [java.util.function Supplier]
[jaskell Handler Handler2]))
...
(deftest function-test
(let[anchor (atom 0)
functor (h/function [_ c]
(+c 1))]
(doseq[i (range10)]
(is (=(swap! anchor inc) (.apply functor i))))))
在 jaskell.handle 中,我实现了常用的三个Java接口,java.util.Function,java.util.Supplier,java.lang.Runnable,另外两个宏如下 :
(defmacrosupplier [[this] & body]
`(reify Supplier
(get[~this]
~@body)))
(defmacrorunnable [[this] & body]
`(reify Runnable
(run [~this]
~@body)))
因为宏在编译期展开,我们可以通过它简化形式的复杂度,使之更可读。其实简单的宏无非就是列表拼接,并没有特别困难。类似上面这种简单封装一个handler,把 reify 简化成一个 Java Lambda 的代码,我以前的项目中写也写过。只要知道反引号是代码模板,波浪号是模板里的替换变量,我相信读者们很容易就能理解这三个宏只是 reify 的封装而已。
新的 Java 库,通常都会倾向使用标准库内置的这些算子类型,但是有些库起源自更早的年代,或者因为其它考虑(例如兼容性),可能使用了自定义的接口协议,例如很常见的Handle定义:
public interface Handle {
U handle(T arg);
}
类似这种代码读者们可能在很多Java库里都见过。前面的几个宏显然不能兼顾未知的类型。那么有没有办法在将来遇到某个未知的接口,作用也是封装一个简单的单参数函数,也将其简化为一个 Clojure 函数定义的程度呢?
有一个办法是我们需要的时候现场定义一个新的宏,这很容易做到,特别是有了前面几个例子,读者朋友们可以简单的复制粘贴,改一下名字和reify参数即可定义出一个新的封装。
那么,有没有办法把这个定义新宏的方式自动化呢?
宏可以生成宏,在技术上这是完全可行的。当然二阶宏写起来比较麻烦,但这也正是我们平常积累工具代码的意义。足够的通用性,更可以持久的帮助我们提高效率。经过一番尝试,我实现了这么几个宏:
(defmacrodef-generator-0 [name[clz handler]]
`(defmacro~name[[this#] & body#]
`(reify ~'~clz
(~'~handler [~this#]
~@body#))))
(defmacrodef-generator-1 [name[clz handler]]
`(defmacro~name[[this# arg#] & body#]
`(reify ~'~clz
(~'~handler [~this# ~arg#]
~@body#))))
(defmacrodef-generator-2 [name[clz handler]]
`(defmacro~name[[this# a# b#] & body#]
`(reify ~'~clz
(~'~handler [~this# ~a# ~b#]
~@body#))))
下面这三个测试演示了它们的用法:
(h/def-generator-0 fun-0 [Supplier get])
(deftest generator-0-test
(let[anchor (atom 0)
supplier (fun-0 [_]
@anchor)]
(doseq[i (range10)]
(is (=(swap! anchor inc) (.get supplier))))))
(h/def-generator-1 fun-1 [Handler handle])
(deftest generator-1-test
(let[anchor (atom 0)
functor (fun-1 [_ c]
(+c 1))]
(doseq[i (range10)]
(is (=(swap! anchor inc) (.handle functor i))))))
(h/def-generator-2 fun-2 [Handler2 handle])
(deftest generator-1-test
(let[anchor (atom 0)
functor (fun-2 [_ a b]
(+a b (swap! anchor inc)))]
(doseq[i (range3 13)]
(is (=i (.handle functor 1 1))))))
这三个宏对应了有0个、1个和2个参数时的handle封装,它们不是用来封装handle的,而是生成对应的宏定义。所以调用这些 generator 相当于用给定的名字定义了宏。后面就可以用这些宏来封装 Handle 。细心的读者会发现二阶展开的部分,我们额外的加入了一层解引述操作。
需要注意的是,因为宏在编译时展开于调用处,所以我们这种在宏代码中使用了Java类型的源代码,就需要预先 import 我们用到的类型:
(nsjaskell.handle-test
(:require [clojure.test :refer :all])
(:require [jaskell.handle :as h])
(:import [java.util.function Function Supplier]
[jaskell Handler Handler2]))
不过这在我看来并不是一个很大的问题,从实践经验来说,这种宏封装仍然为我提高了工作效率。掌握这个知识对于 Java + Clojure 模式工作的程序员还是相当值得的。
===================