java 宏_现代化的 Java (二十一)——宏和生成宏

一般来说,我们会避免在应用项目中编写宏,无论是 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 模式工作的程序员还是相当值得的。

===================

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值