目录
本文以技术文章的方式回顾张炜昕老师在 SIG-元编程 技术沙龙上分享的议题《基于元编程的可扩展访问者模式》,感谢张老师为方便大家阅读梳理的文字版内容,此外,回顾视频已经上传 B 站,欢迎小伙伴们点开观看。
B站链接如下:SIG-元编程技术沙龙回顾|基于元编程的可扩展访问者模式
# 表达式问题 #
模块化和可扩展性是开发复杂软件系统的重要基础,而表达式问题(Expression Problem)则是检验编程语言对模块化和可扩展性支持程度的根本问题。表达式问题要求在不修改和重复既有代码、保障类型安全、分离编译和模块化类型检查的前提下同时扩展数据结构及其操作。传统的面向对象和函数式编程范式仅支持单一维度的扩展性。
我们将以皮亚诺(Peano)数及其扩展为例贯穿本文。一开始只支持零(Zero
)和后继(Succ
)来构造自然数和将皮亚诺表示转换为数字表示的求值操作(eval
)。例如eval
作用在 Succ(Zero)
上的结果是 1。以下的 Scala 代码分别用面向对象和函数式实现了皮亚诺数:
//面向对象式
trait Tm { def eval: Int }
object Zero extends Tm {
def eval = 0
}
class Succ(t: Tm) extends Tm {
def eval = t.eval + 1
}
//函数式
sealed trait Tm
case object Zero extends Tm
case class Succ(t: Tm) extends Tm
def eval(t: Tm): Int = t match {
case Zero => 0
case Succ(t1) => eval(t1) + 1
}
面向对象式是操作优先,先以接口描述数据结构所支持的操作,再用类实现接口来定义数据结构。在面向对象式中,添加数据结构易而添加新的操作难。添加数据结构仅需定义新的子类比如前驱(Pred
):
class Pred(t: Tm) extends Tm {
def eval = t.eval - 1
}
而添加新的操作则需修改接口及其所有子类。相反,函数式是数据结构优先,先以代数数据类型来定义数据结构,再用模式匹配定义操作。在函数式中,添加操作易而添加新的数据结构难。添加操作仅需定义一个新的函数比如打印(print
):
def print(t: Tm): String = t match {
case Zero => "Zero"
case Succ(t1) => "(Succ " + print(t1) + ")"
}
而添加数据结构则需修改代数数据类型定义以及给已有函数增加一条新的模式匹配语句。
# 访问者模式#
那么如何在面向对象语言中实现操作扩展呢?这就需要借助访问者设计模式(Visitor Pattern)了。访问者模式将操作从类层次结构中分离出来从而允许添加新的操作而不修改既有的类层次结构,如下所示:
trait Tm {
def accept[A](v: Visitor[A]): A
}
object Zero extends Tm {
def accept[A](v: Visitor[A]) = v.zero
}
class Succ(val t: Tm) extends Tm {
def accept[A](v: Visitor[A]) = v.succ(this)
}
//访问者接口
trait Visitor[A] {
def zero: A
def succ(x: Succ): A
}
访问者接口声明的访问方法(zero
和 succ
)与类层次结构的每个子类(Zero
和 Succ
)一一对应,用来实现类层次结构中的 accept
方法。类型参数 A
抽象了访问方法的返回类型。
操作通过实现访问者接口定义:
class Eval extends Visitor[Int] {
def zero = 0
def succ(x: Succ) = x.t.accept(this) + 1 //递归调用accept方法遍历子表达式
}
重复实现访问者接口即可添加新的操作,比如打印:
class Print extends Visitor[String] {
def zero = "Zero"
def succ(x: Succ) = "(Succ " + x.t.accept(this) + ")"
}
然而,传统的访问者模式只是转换了扩展维度并没有解决表达式问题:当我们想添加一个新的子类时,访问者接口并没有对应的访问方法来实现其 accept
方法。因此,需要修改访问者接口及已有的访问者。此外,访问者模式引入了样板代码(boilerplate code),使用起来较为繁琐。
为解决上述缺陷,本文将分别介绍基于 Java 和 Scala 两种语言的可扩展访问者模式元编程框架 EVF[1] 和 Castor[2]。
# 第一部分:EVF#
# 一种基于 Java 的可扩展访问者模式
让访问者模式可扩展的关键在于如何解耦访问者接口和类层次结构。用可扩展访问者模式定义的皮亚诺数访问者接口如下:
interface Visitor<Tm, A> {
A Zero();
A Succ(Tm t);
A visitTm(Tm t);
}
通过新增类型参数 Tm
来抽象数据结构类型以及从 Tm
转换到返回值类型 A
的方法 visitTm
来解耦访问者接口和类层次结构。求值访问者的定义如下:
interface Eval<Tm> extends Visitor<Tm, Integer> {
default Integer Zero() {
return 0;
}
default Integer Succ(Tm t) {
return visitTm(t) + 1; //递归调用visitTm方法遍历子表达式
}
}
在定义具体的访问者时,Tm
保持抽象并通过递归调用 visitTm
方法实现对子表达式的遍历。这里运用 Java 8 引入的 default methods 使得访问者不仅可以扩展还能利用接口多重继承来组合访问者。
> 扩展
现在,前驱可以模块化地定义了:
//扩展访问者接口
interface ExtVisitor<Tm,A> extend