算是自己的一个脑洞。简单写一写。

如果

把流水线中的节点看作 “函数”,把流水线上传递的数据看作 “数据”,那么如果有一个流水线是:

1
input => A -> B -> C -> D => output

也就可以把这个过程看作:

其实在一些函数式语言中会有这样的表达:

1
2
let input = ...
let output = input |> A |> B |> C |> D

在现实中,这表示着 “pipeline 上有 A B C D 四个步骤”。再具体一点,它们可能各自完成:代码克隆、编译、测试、发布。

但是一般的流水线设计会复杂一些,流水线不仅拥有 “单方向 / 形式” 的输入,即:

1
2
3
         a    b    c    d
| | | |
input => A -> B -> C -> D => output

这个过程可以表示为:

流水线不仅以前节点的输出为输入,还可能有各节点特别的输入。在现实生活中,这表现为 “各步骤除自动接受 pipeline 上流动的数据之外,有些额外配置需要提供人工输入”。再具体一点,很可能上图中 a 表示着 A 需要克隆的 repo URL;c 表示着 C 是否需要提供 coverage report。

因此可以大概得知,流水线上的一个节点的操作可以用以下步骤描述:

  1. 接受先前节点的输出;
  2. 接受特定的输入;
  3. 从 (1) (2) 中获取需要的数据;
  4. 数据操作;
  5. 产生影响;
  6. 向 pipeline 中输出数据。

对于上述 $output = D(C(B(A(input, a), b), c), d)$ 这个式子来说,我们不能把它写作上面那种简洁的方式,只能写成:

1
2
3
4
5
val input = ...
val r1 = A(input, a)
val r2 = B(r1, b)
val r3 = C.(r2, c)
val output = D.(r3, d)

定制配置导致表达形式不是特别美观。加一些限制是必要的。

对于一些野生的函数而言,我们无法要求其输入输出类型相同或共同继承某一基类。如果这种事情发生在流水线上,即 “A 的输出类型与 B 的输出类型不同”,那流水线将不是一个整体。如果使用颜色代表不同类型去标注 pipeline 上节点间的连接,上述流水线将五颜六色。

统一流水线上数据是必要的。

1
2
3
4
class a extends PipelineSpec {}
class b extends PipelineSpec {}
class c extends PipelineSpec {}
class d extends PipelineSpec {}

但这样仍然无法优化表达式,还需要统一流水线上节点的操作:

1
2
3
4
5
6
7
class A(spec: PipelineSpec) extends PipelineNode {
override def fetchPrevNodeOutput(context: PipelineContext) {}
override def before() {}
override def do() {}
override def after() {}
override def writeOutput(context: PipelineContext) {}
}

这样,上述操作即可变成:

1
2
3
4
5
6
var context: PipelineContext = input
Array[PipelineNode](new A(a), new B(b), new C(c), new D(d)).foreach(node => {
node.fetchPrevNodeOutput(context)
node.do
node.writeOutput(context)
})

流水线通常是有分支结构的,因此上述 fetchPrevNodeOutput 的参数一般是列表,为了更正规,还可以给 pipeline 加上一个 manager 管理 nodes / specs,存储更丰富的数据。

最近和 visitor pattern 打交道比较多。作为 GOF 中最复杂的一种设计模式,其实根本不复杂。(或者说 GOF 中提到的都不复杂)由于某种奇怪的原因,我在写代码的时候一不注意就把 OOP 中的 visitor pattern 和 FP 中常见的 map-reduce chain 写在了一起。

一开始,只有一个 visit,大概是这样的:

1
2
3
4
def visit(cu: CompilationUnit): MyVisitor =
val visitor = new MyVisitor()
cu.accept(visitor)
visitor

就是给定一个 cu,我去遍历一遍,然后返回 visitor。其中 MyVisitor 的实现差不多是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyVisitor extends ASTVisitor {
var types = Array.empty[TypeDeclaration]
var enums = Array.empty[EnumDeclaration]
override def visit(node: TypeDeclaration): Boolean =
if node.isMemberTypeDeclaration || node.isPackageMemberTypeDeclaration then
types = types :+ node
end if
super.visit(node)

override def visit(node: EnumDeclaration): Boolean =
if node.isMemberTypeDeclaration || node.isPackageMemberTypeDeclaration then
enums = enums :+ node
end if
super.visit(node)
}

但是我要筛选出 cu 中满足我要求的部分。(比如 cu 必须定义 package;cu 的 types 必须不为空等等)所以一开始我就写了具有这些代码的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
val packageDecl = cu.getPackage
if packageDecl == null then
Global.LOG.warn("cu has no package name")
return Array.empty[String]

val packageName = packageDecl.getName.toString
val visitor = visit(cu)

if visitor.types.isEmpty then
Global.LOG.warn("cu has 0 types. (maybe annotation declaration)")
return Array.empty[String]

val r1 = visitor.types.map(t => {
val className = ClassUtil.getClassName(t)
t.getMethods.map(m => {
val methodName = MethodUtil.getShortMethodSig(m)
MethodUtil.getFullyMethodSig(packageName, className, methodName)
})
}).filter(elem => !elem.isEmpty)

if r1.isEmpty then
Global.LOG.warn("type declarations in cu have no method")
return Array.empty[String]

不用说我都觉得很蠢啊。就是单纯想到什么写什么。

写完这部分后不久,我就盯着屏幕发呆。也不知道是我脑子搭错弦了还是怎么的。我不知道为什么感觉这段代码像 sb 一样的 Actions。于是我开始考虑 pipeline 和 FP 之前我觉得完全没关系的两个东西间的关系。再之后,我代码就变成这样了:

1
2
3
4
5
6
FileUtil.getAllJavaFiles(projPath)
.map (JDTUtil.getCompilationUnit)
.filter (JDTUtil.isCuHasPackageDecl)
.map (JDTUtil.visitCu)
.filter (JDTUtil.isCuHasTypeDecl)
.foreach(visitor => { ... })