基于元编程的可扩展访问者模式

本文探讨了在编程语言中解决表达式问题的挑战,并介绍了访问者模式作为应对策略。文章详细阐述了基于Java的EVF框架和Scala的Castor框架,这两种框架通过元编程实现了可扩展的访问者模式,以支持在不修改原有代码的情况下扩展数据结构和操作。通过对皮亚诺数的示例,展示了如何在面向对象和函数式编程中应用这些框架,以及它们如何简化代码生成和遍历模板。最后,文章对比了两种框架在实际项目中的应用和性能表现。
摘要由CSDN通过智能技术生成

目录

# 表达式问题 #

# 访问者模式#

# 第一部分:EVF#

# 一种基于 Java 的可扩展访问者模式

# 用EVF框架简化代码

# 遍历模版

# EVF的实现

# 第二部分:Castor #

# 一种基于 Scala 的可扩展访问者模式

# 用Castor框架简化代码

# GADT和模式匹配

# Castor的实现

# 案例分析#

# 结语#


本文以技术文章的方式回顾张炜昕老师在 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
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值