Java. 的 Stream API,有经验的 Java 工程师应该都不陌生了。它为数据处理逻辑提供了一组串化的操作序列接口,为流式的业务提供了一个整齐一致的接口。但是要指出的是,在常见的编程语言中,Java的标准库几乎是这方面最弱的。而Akka Stream 是一组非常强大的工具集,它允许我们用非常直观的方式定义数据流,不仅仅是线性流,还可以是有向图。例如官方给出的这个例子:
val
它直接对应的这样一个逻辑:
我相信很多同行即使没有scala的经验,对照这个图也能很容易的理解代码。但是这里面有一个问题,同样功能的代码在 Java 中是这样的:
final
相比之下 Java 版本冗长的实在过分了。
这还是一个非常简单的图。做量化数据订阅的时候,我还写过一些更复杂的图,我需要把服务器订阅的数据解压,然后分离心跳数据和业务数据,响应后再从出口发送回去。这个过程用 Java 写实在是太不友善了。
但是好在我们有 Clojure ,这几天我想了一下,为 Akka Stream 做了一些 Clojure 的封装。
Scala 的 graph dsl 之所以简洁有力, `~>` 操作符功不可没。Java 因为没有操作符重载,只能用方法链代替,加上没有隐式类型,造成了大量形式上的冗余。
Clojure 的 s 表达式,可以用几乎任意字符来定义方法,加上宏,它可以把 Java 版的 graph dsl 封装到非常漂亮的程度。我们先看一下对应前面那个例子的 Clojure 版本代码:
(
这一段代码可以说干净漂亮很多,特别是定义边时,可以用 `|=>` 一次串起多个节点。
实际上,`|=>` 是一个“平凡”的 Clojure 函数:
(
它看起来比较笨拙,但是把这些“笨拙”的代码封装成简洁漂亮的形式,不正是高级语言的价值所在吗?
这个函数中,其实就是对给定的节点序列,逐个判断类型,调用对应的方法,把它们连城一个合法的 Graph DSL 线。这其中多少参考了 Akka Stream 的实现代码。
对于刚开始学习 Clojure 的同行,可以在这里看到 loop recur 实现递归逻辑的 clojue 风格代码。说起来没有智能的尾递归一直算是 Clojue 的一个短板,目前标准的尾递归定式就是用 recur 。
我这里没有用 scala 里的 `~>` 运算符,是因为 `~` 在 Clojure 里用于它的宏模板。如果我们在代码中加入这个字符,我不确认写在宏里是否会遇到麻烦,所以刻意回避了它,同理也没有使用 `#` 和 `@` 。想来想去,`|=>` 虽然麻烦一点,但是 S 表达式的形式特点使得它处理多个节点的串接时特别方便,多写一个字符也就可以接受了。
在这个代码中,展示的是 from 到 to 的连接逻辑,其中 via 操作的代码在另一个函数中:
(
对于静态语言,这种分派反而简单很多,不过把它封装起来后,我们就可以找回 Lisp 语族简洁的体验。我针对官方 [Working with Graphs] 提到的一些例子,构造了对应的 Clojure 实现,作为 akka-stream-clojure 库的测试代码,例如:
(
这个测试展示的是整数序列数据经过 filter 分离成奇偶两个数据流,在通过 zip 合成一个 pair 的逻辑,官方的 Scala 版本是:
val
Java 版本我们就不放在这里了,实在是比较冗长。当然有一部分原因是 Clojure 自带了可以无限求值的惰性序列,这些Clojure工具库极大的提升了它的表达能力。
有 Akka 经验的朋友可能会知道,除了从 from 到 to 的 `~>` ,Akka Stream 还允许我们从 to 节点反向连接到 from ,这个运算符是 `<~` 。
对应的,我也实现了 `<=|` 操作符:
(
但是暂时我还没有为它写测试。
需要注意的一个细节就是,Graph DSL 的节点(Xxx)和它的图形节点(XxxShape)还是有区别的,一般来说我们需要把构造出来的逻辑节点 add 到构造器(builder)中,从 add 调用的返回值得到对应的图形节点。我提供了一些方法,封装了几个常用的节点构造逻辑,可以一步得到对应的图形节点,这个将来应该还会继续追加。这个约束也是我们一定要在 `|=>` 和其它工具方法中传入 builder 对象的原因。在 Scala 中,因为有强大(同时也是难以驾驭的)的 implicit 功能,我们可以进一步简化形式,把 builder “藏起来”。但是我想了很久,还是决定在 Clojure 里不做太复杂的“黑魔法”,朴素的把 builder 放在明处。考虑到 S 表达式和其它抽象带来的代码简化,多写一个 builder 还是可以接受的。虽然用宏可以模拟此类功能,但是需要做大量的工作,并且降低了代码的扩展能力,在工程上,还是要考虑代价和收益的平衡。
暂时这个库还只是针对 graph dsl,但是将来可能会尝试加入更丰富的功能,这样我们可以通过 clojure 代码,把 java 项目变得更简洁优雅,容易维护。但是接下来,可能我会先针对 akka-http 的 java api 做一些尝试。
相关代码在 MarchLiu/akka-stream-clojure