元编程技术综述

元编程技术综述

来源 https://zhuanlan.zhihu.com/p/399035868

 

# 作者简介 #

Yannis Lilis 博士是欧洲最大的汽车租赁公司的高级程序员,兼职在克里特大学的计算机科学系任客座讲师,在数学及应用数学系任兼职教师,同时兼任计算机科学研究所(ICS)FORTH 实验室的研究员。他的研究兴趣包括软件工程、机器学习、计算机视觉、人工智能、分布式系统等等。他总计发表了 20 篇会议 / 期刊等出版物,其中超过半数为元编程相关技术文章。

Anthony Savidis 教授是克里特大学计算机科学系的正教授和计算机科学研究所(ICS)FORTH 和 HCI 实验室的研究员。他的研究兴趣包括:编程语言、软件工程、用户界面开发工具、特定领域工具以及面向学习者的交互式编程环境。他参与了 35 项欧洲和国家研发项目,发表了 150 多篇会议 / 期刊 / 书籍出版物(2600 次引用,hIndex 30),曾任信息通信技术中心主任,现任克里特大学数据处理实验室主任。他在用户界面管理系统、脚本编程语言、自适应界面、下一代 IDE 和调试器、可访问的用户界面工具包方面做了开创性的工作,并在 2003 年开发了最早的物联网软件工具包之一。

个人主页:

https://www.ics.forth.gr/person/Savidis/Anthony​www.ics.forth.gr/person/Savidis/Anthony

# 译者按 #

元编程是计算机编程中一个非常重要、有趣的概念,可以很大程度地简化和复用代码。元编程几乎是现代编程语言的必备能力之一,在数值计算、编译器设计和实现(类型推导、代码生成……)、领域专用语言(DSL/eDSL)设计、框架开发等领域有广泛的应用。

本文《A Survey of Metaprogramming Languages》是一篇系统介绍元编程技术的综述类文献,比较全面地对已有的元编程模型做了分类,并且对各类元编程的特点做了介绍。让读者可以清晰地了解元编程的历史、编程范式、代表语言…… 这也是译者选择这篇文献的初衷。近几年,在系统编程领域大热的 Rust、发力科学计算领域 Julia、擅长 Web 应用的 Ruby,都非常依赖和推崇元编程技术。遗憾的是,本文没有对这几门语言中的元编程做分析归类。另,原文献第二章中除对概念的介绍外,列举了众多语言的元编程系统作为示例解读,由于篇幅原因,译者只摘选了针对主流语言的介绍,省略部分语言,感兴趣的读者可阅读原文了解更多。

由于文献的性质,本文只能算是对元编程的意况大旨,欢迎读者朋友加入我们的编程语言技术社区 SIG-元编程小组,和我们一起对元编程技术做更深入的探讨。(加入方式:添加小助手微信pl_lab_001,添加并备注加入SIG-元编程)

原论文地址:

https://dl.acm.org/doi/epdf/10.1145/3354584​dl.acm.org/doi/epdf/10.1145/3354584

#1 介绍

元编程是一种将计算机程序当作数据进行处理,从而生成新程序或修改现有程序的技术。元程序所用的语言,我们称之为元语言,生成或转换成的程序所使用的语言,我们称为对象语言 [1]。如果对象语言和元语言相同我们称为同构元编程,若不同,则称为异构元编程。元编程技术在生成或修改代码时,需要将代码在抽象层面进行表示,一般采用抽象语法树(AST,Abstract Syntax Tree,起源于 Lisp S 表达式 [2])的形式。为了让开发者更便捷的操作 AST,不同的语言有不同的形式,本质来说均是通过元语言编写元程序,修改目标代码的 AST。

运用元编程技术带来的收益 [3] 主要体现在以下三个方面:

提升程序性能:开发者根据特殊的应用场景(specializations),结合自己的领域知识,编写元程序自动生成对应的高效程序,而不是利用通用但低效的程序。另外,程序特化(partial evaluation)可以通过识别静态输入(编译期可知的输入)并在编译期进行求值从而最小化运行时开销。

提升程序的理解力:元编程技术可以分析对象程序特性,从而可以进一步对其进行优化,检查和验证(例如流分析器和类型检查器等)。

提升程序复用能力:这是元编程技术最大的优点。编程语言可以通过函数,泛型,多态,类和接口等支持代码复用,但是有一些重复出现的代码模式无法借助以上技术实现复用。而元编程可以对代码片段进行操作,转换或生成代码,因此可以借助结构化的表示描述那些模式,并作为独立的,可重复使用的单元进行发布。

元编程并不是一个新概念,其历史十分悠久,最早可追溯到 Lisp 的宏,图 1 展示了不同语言元编程系统的发展时间线。近年来,随着新的元编程范式的出现,如切面编程 aspect-oriented programming [4]、多阶段编程 multistage programming [5]、产生式编程 generative programming [6] 等,元编程领域受到越来越多人的关注。

图1:元编程相关语言及系统发展时间线,表格可能不详尽

参考:
1. 元语言和对象语言的概念来源于逻辑和语言学,见:https://en.wikipedia.org/wiki/Metalanguage
2. Guy L. Steele. 1990. Common Lisp: The Language (2nd ed.). Digital Press.
3. Tim Sheard. 2001. Accomplishments and research challenges in metaprogramming. In Proceedings of the 2nd International Workshop on Semantics, Application and Implementation of Program Generation (SAIG’01). Springer LNCS 2196, 2–44. DOI:http://dx.doi.org/10.1007/3-540-44806-3_2
4. Gregor Kiczales, John Lamping, Anurag Mendhekar, Chris Maeda, Cristina Lopes, Jean-Marc Loingtier, and John Irwin. 1997. Aspect-oriented programming. In Proceedings of the 11th European Conference on Object-Oriented Programming (ECOOP’97). Springer LNCS 1241, 220–242. DOI:http://dx.doi.org/10.1007/BFb0053381
5. Walid Taha and Tim Sheard. 1997. Multi-stage programming with explicit annotations. In Proceedings of the Symposium on Partial Evaluation and Semantic-Based Program Manipulation (PEPM’97), ACM, New York, 203–217. DOI:http://doi.acm. org/10.1145/258994.259019
6. Krzysztof Czarnecki and Ulrich W. Eisenecker. 2000. Generative Programming: Methods, Tools, and Applications. ACM Press/Addison-Wesley Publishing, New York.

 

#2 元编程模型

元编程采用的模型主要有以下几类:

  1. 宏系统(macro system)
  2. 反射系统(reflection system)
  3. 元对象协议系统(metaobject protocols)
  4. 切面编程(aspect-oriented programming)
  5. 产生式编程(generative programming)
  6. 多阶段编程(multistage programming)等

每种模型可以解决不同的工程问题,同时也存在密切的联系,跨模型的交叉应用能解决更具挑战的问题。

图 2:元编程的四个分类维度以及其各自的子类(含章节序号)

##2.1 宏系统

模型解释

宏是一个重要的元编程模型,其类似于函数的功能,完成某种输入到输出的映射。对于宏来说,目标代码片段作为输入序列,输出序列为新的代码片段。这个映射过程称为宏展开,会一直展开直到目标代码中没有宏为止,宏展开后的目标代码继续执行正常的编译流程,如图 3 所示。宏系统可以分为两大类:词法宏和语法宏。词法宏作用范围在标记序列上(sequence of tokens),如 C/C++ 的宏;语法宏能获取语法,语义信息,可以认为其作用在抽象语法树(AST)上。宏系统可以是过程式的(Rust 过程宏),即对输入进行系列计算从而得到输出;也可以基于模式匹配及替换(Rust 声明宏)完成代码变换。最后,宏系统一个重要的属性是卫生扩展(hygienic expansion),即保证没有由宏扩展引入的意外的由宏扩展引入的名称冲突,这个问题通常被称为变量捕获。

 

图 3:宏系统:宏调用在迭代宏展开的过程中更新程序表达,最终使用无宏调用的程序完成翻译

 

代表语言和系统

CPP(C preprocessor)[1] 是词法宏的典型代表。词法宏一般与语言的翻译器(编译器或解释器)是解耦的,有特殊的预处理器(如 C 语言预处理器)将目标代码中的宏进行预处理展开,然后进行翻译。这类宏只能处理比较简单的文本替换问题,且容易产生副作用,如命名冲突等。

第一个使用基于语义的宏系统的编程语言是 Lisp。在 Lisp 中,代码即数据,任何可以对数据结构进行的操作,Lisp 的宏均可以对代码进行类似操作。如下图所示,还有很多编程语言有各具特色的宏系统。

表 1:宏系统分类

 

参考:
Brian W. Kernighan and Dennis M. Ritchie. 1988. The C Programming Language (2nd ed.). Prentice-Hall, Englewood Cliffs, NJ.

 

##2.2 反射系统

模型解释

反射是指在运行时计算机程序可以访问,检测和修改它本身状态或行为的一种能力 [1]。反射的两个主要概念是内省(introspection)和调整(intercession)。内省是程序检查其自我表示的能力,而调整是程序修改其自我表示的能力。另一个区别是结构反射和行为反射之间的区别。结构反射是程序访问其结构表示的能力(例如,类、方法和字段),而行为反射是它能够访问其切面的动态表示(例如,变量赋值、对象创建、方法调用等)。这两个区别是正交的;内省和调整与访问的类型有关(即,只读或读 / 写),而结构与行为的区别与自身的类型有关。

代表语言和系统

在面向对象的编程语言中,反射允许在编译期间不知道接口、字段、方法的名称的情况下在运行时检查类,接口,字段和方法。它还允许根据判断结果进行实例化新对象和不同方法的调用。反射还可以使给定的程序动态地适应不同的运行情况。例如,考虑一个应用程序,它使用 2 个不同的类 X 和 Y 互相交替执行类似的操作。没有使用面向反射编程技术,应用程序可能是硬编码的(即把代码写死,缺乏灵活性),以调用方法名称的类 X 和 Y 类。然而,使用面向反射的编程范式中,应用程序可以在设计和编写利用反射在没有硬编码方法名称情况下调用类中的方法 X 和 Y。如下图介绍了 Java 反射系统。

图 4:Java 的动态代码生成 (上) 和反射(下)

虽然 Java 的反射是在运行时实现的,但反射不局限于此阶段。在编译期反射中,元程序通过反射获取的相应字段和方法信息,生成特定的目标代码。这样的反射系统主要聚焦于代码生成,也属于产生式编程的元编程模型,会在后续继续讨论。

如下图所示,还有很多语言支持反射系统。

表 2:反射系统相关的语言

Symbol ↓ means there is limited support for the feature.

参考:
1. Pattie Maes. 1987. Concepts and experiments in computational reflection. In Conference Proceedings on Object-Oriented Programming Systems, Languages and Applications (OOPSLA’87). ACM, New York, 147–155. DOI:http://dx.doi.org/10. 1145/38765.38821

 

##2.3 元对象协议系统

模型解释

元对象协议(MOPs,Metaobject Protocols)[1] 最初是一种能逐步改变原始语言行为和实现的接口(interface),最具代表性的例子有 Smalltalk 的 MOPs 和 Common Lisp 的对象系统。用通俗的话来说,MOP 能够访问对象系统中对象的结构和行为。在这个模型中,普通的类(class)被认为是元类(metaclass)的对象(object,或者说实例),称为元对象( metaobjects )。元类(metaclass)负责整个对象系统并提供 API 修改其行为(例如,通过继承不同的类来创建新方法,修改现有的方法,修改类的结构等),如图 5 所示。从这个角度看,所有在 meta-level 对元对象的修改,都会直接影响 base-level 继承自这些元对象的普通对象。

图 5:使用元类和元对象协议改变对象系统行为

上述内容是基于元类方法的典型示例。还有其他相关的类和元对象的设计。例如,在基于元对象的方法中,类和元对象是不同的,对象出于结构目的共享它们的类,但出于行为目的,每个对象都有一个单独的元对象。还有一种方法是,在消息传递上下文中,基于消息具体化,其中消息是能够对发送消息做出反应的消息类的对象,并且消息子类可以覆盖默认的消息解释。

代表语言和系统

很多语言采用了 MOP 的模型,如 Python,Ruby,Groovy,Perl 等。C++ 和 Java 虽然语言本身没有提供 MOP,但是有很多的扩展实现 MOP。

大部分 MOP 系统作用在运行时,但是也有编译期的例子。在 OpenC++ [2] 和 OpenJava [3] 中,在编译期可以获取元对象,同时通过编译期反射机制去操纵源代码,并通过类型驱动翻译 type-driven translation 的方式去完成代码转换。解析源代码后,系统为每个定义的类生成一个元对象。然后部署这些元对象将目标类翻译成正常语法,接着进行常规的编译流程。整体来说,编译期 MOP 系统的运行类似于高级宏系统,但是是基于元对象而不是文本或 AST 进行代码变换。

表 3:基于 MOPs 分类的语言

参考:
1. Gregor Kiczales, Jim Rivieres, and Daniel G. Bobrow. 1991. The Art of the Metaobject Protocol. MIT Press.
2. Shigeru Chiba. 1995. A metaobject protocol for C++. In Proceedings of the 10th Annual Conference on Object-Oriented Programming Systems, Languages, and Applications (OOPSLA’95). ACM, New York, 285–299. DOI:http://doi.acm.org/10. 1145/217838.217868
3. Michiaki Tatsubori, Shigeru Chiba, Marc-Olivier Killijian, and Kozo Itano. 2000. OpenJava: A class-based macro system for java. In Proceedings of the 1st OOPSLA Workshop on Reflection and Software Engineering: Reflection and Software Engineering. Springer LNCS 1826, 117–133. DOI:http://dx.doi.org/10.1007/3-540-45046-7_7

 

##2.4 面向切面编程

模型解释

面向切面编程(AOP)[1] 是一种将交叉切割问题(crosscutting concerns)建模为一种称为切面(aspects)的模块化单元的方法。

图 6:分离交叉切割问题(切面)并将它们基于某些匹配标准与基本程序功能一起编译,以形成最终程序

切面包含有关附加行为(advice)的信息,这些行为将按切面以及称为连接点(join points) 的程序位置添加到基础程序中,其中此额外行为将基于一些匹配标准(称为切入点 pointcuts)插入。将基本程序与纵向代码相结合,形成包含从各个切面交叉切割功能的最终代码(图 6)。此过程称为编织(weave),可能在程序编译或执行期间进行。编译期进行编织的称为静态 AOP,运行期编织的称为动态 AOP。静态 AOP 的性能更好,因为它的运行时间开销较少,而动态 AOP 更灵活,允许在运行时动态插入或拔下切面。

AOP 的想法源于反射和 MOP,因此元编程与 AOP 之间有着密切的关系。特别是,AOP 支持元编程作为一种程序转换方法,采用基于模式匹配的方法来表达交叉切割问题。与算法转换相比,这可能限制表现力,但能够以更大的灵活性和抽象性处理涉及交叉切割问题的元编程任务,从而实现更简洁、更直接、更安全的实现。

代表语言和系统

AspectJ [2] 是 AOP 的第一个语言和 reference 实现,扩展 了 Java 语法。AspectC++ [3] 将 AOP 引入 C++。AspectJ 和 AspectC++ 是具有代表性的静态 AOP 系统,它们都生成包含 AOP 功能的二进制代码。Java 具有代表性的动态 AOP 框架包括 JAC [4]、AspectWerkz [5](现已与 AspectJ 合并)、PROSE [6] 和 Spring [7]。JAC 和 PROSE 都使用正常的 Java 类来指定切面,编织器将切面对象部署到应用程序上。AspectWerkz 和 Spring 提供了相似的动态织入功能,使用 Java 注解或外部 XML 配置文件,定义切面、切入点和附加行为。

AOP 可以通过在匹配的连接点之前、之后或周围插入代码,以及通过引入 inter-type 类型定义的数据成员和方法来支持元编程。大多数支持 AOP 的系统都依赖于元编程技术在运行时和编译时进行传递。具体在什么阶段传递与系统提供的 AOP 类型直接相关,即静态或动态。

AspectS [8] 和 AspectL [9],分别在 Smalltalk 和 Lisp 的运行时 MOP 上构建支持 AOP。Reflex 也使用其 MOP 支持 AOP [10]。AspectR [11] 是一个 Ruby 库,利用元编程技术通过类方法适配器实现 AOP。大多数动态 AOP 的 Java 框架通过加载时或运行时字节码执行切面编织。

另一方面,Handi-Wrap [12] 和 AspectScheme [13] 分别是 Java 和 Scheme 的扩展,使用宏来支持 AOP。AOP++ [14] 是一个 C++ AOP 框架,使用模板元编程在编译时定义切入点和匹配连接点。Groovy AOP [15] 提供了一种基于元编程和字节码转换的混合动态 AOP 实现;切面、切入点和 advice 在编译时基于 Groovy DSL 指定,而 advice 在运行时使用动态编译编入字节码。

Meta-AspectJ(MAJ)[16] 是一个 Java 扩展,可定义生成 AspectJ 代码的模板。MAJ 采用产生式编程技术,在 AspectJ 之上开发 DSL,并扩展 AspectJ。最近,Lilis 和 Savidis 探索了两种实践的结合 [17],引入了 AOP 支持元程序和 multistage language 的整个处理流程。他们确定了三个切面类别,以在 multistage language 中充分支持 AOP:

  1. prestaging 切面 用于在原始代码中引入 staging 或转换现有 stages
  2. in-staging 切面 用于将典型的 AOP 应用于 stage 元程序
  3. poststaging 用于在最终程序中应用典型的 AOP 转换

表 4:AOP 分类

参考:
1. Gregor Kiczales, John Lamping, Anurag Mendhekar, Chris Maeda, Cristina Lopes, Jean-Marc Loingtier, and John Irwin. 1997. Aspect-oriented programming. In Proceedings of the 11th European Conference on Object-Oriented Programming (ECOOP’97). Springer LNCS 1241, 220–242. DOI:http://dx.doi.org/10.1007/BFb0053381
2. Gregor Kiczales, Erik Hilsdale, Jim Hugunin, Mik Kersten, Jeffrey Palm, and William G. Griswold. 2001. An overview of AspectJ. In Proceedings of the 15th European Conference on Object-Oriented Programming (ECOOP’01). Springer LNCS 2072, 327–354. DOI:http://dx.doi.org/10.1007/3-540-45337-7_18
3. Olaf Spinczyk, Andreas Gal, and Wolfgang Schröder-Preikschat. 2002. AspectC++: An aspect-oriented extension to the C++ programming language. In Proceedings of the 40th International Conference on Tools Pacific: Objects for Internet, Mobile and Embedded Applications (CRPIT’02), 53–60.
4. ‍Renaud Pawlak, Lionel Seinturier, Laurence Duchien, and Gerard Florin. 2001. JAC: A flexible solution for aspect-oriented programming in Java. In Proceedings of the 3rd International Conference on Metalevel Architectures and Separation of Crosscutting Concerns (REFLECTION’01). Springer LNCS 2192, 1–24. DOI:http://dx.doi.org/10.1007/3-540-45429-2_1
5. Jonas Bonér. 2004. What are the key issues for commercial AOP use: How does AspectWerkz address them? In Proceedings of the 3rd International Conference on Aspect-Oriented Software Development (AOSD’04). ACM, New York, 5–6. DOI: http://dx.doi.org/10.1145/976270.976273
6. Angela Nicoara and Gustavo Alonso. 2005. Dynamic AOP with PROSE. In Proceedings of the International Workshop on Adaptive and Self-Managing Enterprise Applications (ASMEA’05). Retrieved January 2019 from http://wwwhttp://deutsche-telekom-laboratories.com/∼angela/papers/AngelaNicoara-ASMEA2005.pdf.
7. Rod Johnson. 2011. Aspect oriented programming with spring. Retrieved January 2019 from http://docs.spring.io/spring/ docs/current/spring-framework-reference/html/aop.html.
8. Robert Hirschfeld. 2002. AspectS - aspect-oriented programming with squeak. In Revised Papers from the International Conference NetObjectDays on Objects, Components, Architectures, Services, and Applications for a Networked World (NODe’02). Springer LNCS 2591, 216–232. DOI:http://dx.doi.org/10.1007/3-540-36557-5_17
9. Pascal Costanza. 2004. A short overview of AspectL. In Proceedings of the European Interactive Workshop on Aspects in Software. EIWAS. Retrieved January 2019 from http://www.p-cos.net/documents/aspectl-short-final.pdf.
10. Éric Tanter, Rodolfo Toledo, Guillaume Pothier, and Jacques Noyé. 2008. Flexible metaprogramming and AOP in Java. Science of Computer Programming 72, 1–2 (2008), 22–30. DOI:http://dx.doi.org/10.1016/j.scico.2007.10.005
11. Avi Bryant and Robert Feldt. 2002. AspectR - Simple aspect-oriented programming in Ruby. Retrieved January 2019 from http://aspectr.sourceforge.net/.
12. Jason Baker and Wilson Hsieh. 2002a. Runtime aspect weaving through metaprogramming. In Proceedings of the 1st International Conference on Aspect-Oriented Software Development (AOSD’02). ACM, New York, 86–95. DOI: http://doi.acm.org/10.1145/508386.508396
13. Christopher Dutchyn, David B. Tucker, and Shriram Krishnamurthi. 2006. Semantics and scoping of aspects in higher-order languages. Science Computer Programming 63, 3 (2006), 207–239. DOI:http://dx.doi.org/10.1016/j.scico.2006.01.003
14. Zhen Yao, Qi-long Zheng, and Guo-liang Chen. 2005. AOP++: A generic aspect-oriented programming framework in C++. In Proceedings of the 4th International Conference on Generative Programming and Component Engineering (GPCE’05). Springer LNCS 3676, 94–108. DOI:http://dx.doi.org/10.1007/11561347_8
15. Chanwit Kaewkasi and John R. Gurd. 2008. Groovy AOP: A dynamic AOP system for a JVM-based language. In Proceedings of the 2008 AOSD Workshop on Software Engineering Properties of Languages and Aspect Technologies (SPLAT’08). ACM, Article 3. DOI:http://doi.acm.org/10.1145/1408647.1408650
16. ‍David Zook, Shan Shan Huang, and Yannis Smaragdakis. 2004. Generating Aspectj programs with meta-Aspectj. In Proceedings of the 3rd International Conference on Generative Programming and Component Engineering (GPCE’04), 1–18, DOI:http://dx.doi.org/ 10.1007/978-3-540-30175-2_1
17. Yannis Lilis and Anthony Savidis. 2014. Aspects for stages: Cross cutting concerns for metaprograms. Journal of Object Technology 13, 1 (2014), 1–36. DOI:http://dx.doi.org/10.5381/jot.2014.13.1.a1

 

##2.5 产生式编程

模型解释

产生式编程(Generative Programming)是在给定特定需求规范的情况下,可以通过配置从基本的、可重用的实现组件按需自动生产高度定制和优化的中间或最终产品 [1](图 7)。产生式编程在元编程的上下文中,基于某种算法和程序表示(通常是 AST)来操作和生成其他程序的程序。从这个意义上说,它们与宏系统密切相关;然而,宏往往会模糊普通代码和元代码之间的区别(通常只有宏定义需要额外的语法),而在产生式编程系统中,代码生成指令被明确标记。

图 7:使用程序生成器自动化创建一个具体程序,基于基础可重用的实现组件

我们整理了基于模板、AST 转换、编译时间反射、trait 和类组合等技术,来支持产生式编程的语言和系统。

 

代表语言和系统

模板 Templates

C++ 通过其模板系统支持元编程。元编程是通过实例化具有特定类型和无类型参数的模板代码(即骨架类和函数)生成具体的 C++ 代码,然后编译。不允许生成自由格式的源代码。C++ 模板是一种图灵完备的函数式语言 [2],允许表达任何编译时计算,这意味着使用适当的元编程逻辑进行类型操作(例如,模板类组合,类选择),在计算上可以表达任何程序。这种可行性是理论上的,因为在实践中,精心设计的元编程场景所涉及的实现复杂性超过了所获得的好处。

语法树转换 AST Transformation

大多数产生式编程系统通过 quasi-quotation 提供代码模板以支持 AST 创建和组合,并通过 AST 遍历或转换功能对它们进行补充以支持元编程功能。例如 Java、Groovy 等等。

编译时反射 Compile-Time Reflection

除了代码模板,许多产生式编程系统还提供编译时反射功能,可以根据现有代码结构生成代码,同时强调静态类型安全,即确保生成器始终为任何输入生成格式良好的代码。例如 cJ [3] 使用编译时谓词扩展 Java,它不同于其他条件编译技术(例如,C/C++ #ifdef),因为它是静态类型安全的。Genoupe [4] 是一个 C# 扩展,支持基于泛型类型的编译时程序生成器,提供类似的条件构造和循环构造,允许迭代类型参数的字段或方法并为每个匹配生成代码。Genoupe 提供了一个高度静态安全的类型系统,但它不能保证生成的代码总是类型良好。CTR [5] 克服了 Genoupe 的一些缺点,提供了更强的类型安全保证,并允许扩展现有元素而不是只生成新元素。MorphJ [6] 使用基于模式的反射声明扩展了 Java,并引入了类变形的概念。

类组合 Class Composition

还有一些系统专注于组合方法,例如混合和特征,用以提高灵活性和表现力。Metatrait Java(MTJ)[7] 是一个 Java 扩展,允许基于成员模式的编译时代码生成和自省。它引入了用户可自定义的 trait, 这些特征是参数化的超类型、值和名称,提供基于编译时模式的反射。Traits 支持统一的和类型安全的元编程方式,而无需求助于 AST,其类型系统结合了结构和有名子类型的混合。面向特征的编程方法也属于这一类。例如,FFJ [8] 是 Java 的面向特性的扩展,其中特性可以创建新类或通过类改进扩展现有类,同时静态保证类型安全。

如下表,还有很多有关产生式编程的扩展或语言特性,可作了解。

表 5:产生式编程系统分类

参考:
1. Krzysztof Czarnecki and Ulrich W. Eisenecker. 2000. Generative Programming: Methods, Tools, and Applications. ACM Press/Addison-Wesley Publishing, New York.
2. Todd Veldhuizen. 2003. C++ templates are Turing complete. Technical Report, Indiana University. Retrieved January 2019 from http://port70.net/∼nsz/c/c%2B%2B/turing.pdf.
3. Shan Shan Huang, David Zook, and Yannis Smaragdakis. 2007. cJ: Enhancing Java with safe type conditions. In Proceedings of the 6th International Conference on Aspect-oriented Software Development (AOSD’07). ACM, New York, 185–198. DOI:http://dx.doi.org/10.1145/1218563.1218584
4. Dirk Draheim, Christof Lutteroth, and Gerald Weber. 2005. A type system for reflective program generators. In Proceedings of the 4th International Conference on Generative Programming and Component Engineering (GPCE’05). Springer LNCS 3676, 327–341. DOI:http://dx.doi.org/10.1007/11561347_22
5. Manuel Fähndrich, Michael Carbin, and James R. Larus. 2006. Reflective program generation with patterns. In Proceedings of the 5th International Conference on Generative Programming and Component Engineering (GPCE’06). 275–284. DOI: http://doi.acm.org/10.1145/1173706.1173748
6. Shan Shan Huang and Yannis Smaragdakis. 2011. Morphing: Structurally shaping a class by reflecting on others. ACM Transactions on Programming Languages and Systems 33, 2, Article 6, 44. DOI:http://doi.acm.org/10.1145/1890028.1890029
7. John Reppy and Aaron Turon. 2007. Metaprogramming with traits. In Proceedings of the 21st European Conference on ObjectOriented Programming (ECOOP’07). Springer LNCS 4609, 373–398. DOI:http://dx.doi.org/10.1007/978-3-540-73589-2_18
8. Sven Apel, Christian Kästner, and Christian Lengauer. 2008. Feature featherweight Java: A calculus for feature-oriented programming and stepwise refinement. In Proceedings of the 7th International Conference on Generative Programming and Component Engineering (GPCE’08). ACM, New York, 101–112. DOI:https://doi.org/10.1145/1449913.1449931

 

## 2.6 多阶段编程

模型解释

多阶段编程(MSP,Multi-staging Programming)扩展了多级编程(Multi-Level Programming)[1] 的概念,MSP 将程序在求值阶段分为多个层级,并使程序员可以通过称为 staging 注解 [2] 的特殊语法访问它们。引入此类注解是为了明确指定程序计算的求值顺序,能够创建延迟(即未来阶段)计算以及执行当前阶段。从这个意义上说,MSP 与部分求值和程序特化密切相关,因为显式求值顺序允许以不涉及不必要的运行时开销的方式特化泛型和高度参数化的程序。

从另一个角度来看,MSP 也可以看作是程序生成的一个特例;延迟计算实际上是 quasi-quotation 构建的 AST 形式的代码片段,在当前阶段执行代码生成。从这个意义上说,MSP 也与过程宏系统相关,其中调用返回延迟计算的函数并在当前阶段执行它类似于宏扩展。前面讨论的句法差异也适用于此;宏系统使用额外的定义语法和正常(函数式)的调用语法。同时,在 MSP 中,调用点需要额外的语法来执行返回的延迟计算结果。

图 8:延迟计算在当前阶段执行代码生成

采用 MSP 的语言通常支持无限数量的阶段,因此称为多阶段语言 (MSL)。有一些语言恰好提供两个阶段的求值,被称为两阶段语言(two-stage languages)。当元语言与对象语言相同时,MSL 会被归类为同构的(homogeneous);当元语言与对象语言不同,MSL 被归类为异构的(heterogeneous)。具有无限级数的 MSL 总是同质的。

 

代表语言和系统

MSP 最初应用于 MetaML [3],通过 staging 注解扩展 ML:

  1. brackets,用于创建延迟计算
  2. escape,用于组合延迟计算
  3. run,用来执行一个延迟的当前阶段的计算
  4. lift,用于将结果值转换为代码

MetaML 引入了 crossstage persistence(CSP)概念,一种在未来阶段使用当前阶段可用的值的能力,以及保障跨阶段安全性,不允许在某个阶段绑定变量以便在较早的阶段使用。MetaML 依赖于运行时代码生成以及强类型安全的保证,保证了如果生成器类型良好,则任何生成的程序也类型良好。

MetaOCaml [4] 是另一个早期的 MSL,它扩展了 OCaml ,具有 staging 功能,并可通过 ASTs、gensym 和运行时反射实现作为编译后的 MetaML 变体运行。它在语言中明确了 CSP,并引入了 environment classifiers [5] 的概念,它是一种特殊的标识符,注解代码片段和变量声明,其作用域机制确保某些代码片段是关闭的并且可以安全运行。

表 6:MSL 分类

参考:
1. Robert Glück and Jesper Jørgensen. 1996. Fast binding-time analysis for multi-level specialization. In Proceedings of the 2nd International Andrei Ershov Memorial Conference on Perspectives of System Informatics. Springer LNCS 1181, 261–272. DOI:http://dx.doi.org/10.1007/3-540-62064-8_22
2. Kenichi Asai. 2014. Compiling a reflective language using MetaOCaml. In Proceedings of the 2014 International Conference on Generative Programming: Concepts and Experiences (GPCE’14). ACM, New York, 113–122. DOI:http://dx.doi.org/10. 1145/2658761.2658775.
3. Walid Taha and Tim Sheard. 1997. Multi-stage programming with explicit annotations. In Proceedings of the Symposium on Partial Evaluation and Semantic-Based Program Manipulation (PEPM’97), ACM, New York, 203–217. DOI:http://doi.acm. org/10.1145/258994.259019
4. Cristiano Calcagno, Walid Taha, Liwen Huang, and Xavier Leroy. 2003. Implementing multi-stage languages using ASTs, gensym, and reflection. In Proceedings of the 2nd International Conference on Generative Programming and Component Engineering (GPCE’03). Springer LNCS 2830, 57–76. DOI:http://dx.doi.org/10.1007/978-3-540-39815-8_4
5. Walid Taha and Michael Florentin Nielsen. 2003. Environment classifiers. In Proceedings of the 30th ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL’03). ACM, New York, 26–37. DOI:http://dx.doi.org/10.1145/ 604131.604134

 

#3 元程序求值阶段

元程序也可以按照被求值的阶段进行分类。我们按照一个元程序被求值的时机将其分为三类:

  1. 编译前,即预处理阶段
  2. 编译时
  3. 运行时

每一类都有它的特点,也就产生不同的优缺点,但这三种方式并不是互斥的。理论上他们之间的组合均是可行的;然而在实践中,大多数元语言只提供其中一两种选项,每个选项都可能支持不同的元编程实现或表达力水平。虽然求值的阶段并不是采用一种元编程模型的决定因素,但是对于大多数元编程模型通常只采用一两种求值阶段上,二者是有联系的。

 

##3.1 预处理期求值

求值类别

第一种应用元编程的阶段是在程序编译前,即源代码的预处理阶段,如图 9 示。这种情况,也就是我们说的预处理阶段元编程(PPTMP,preprocessing-time metaprogramming),源代码中的元程序在预处理阶段被求值,产生的源文件中不再含有元程序代码。

图 9:预处理期元编程的典型示例

因此转换可以复用语言的编译器或解释器,而不需要做任何扩展。它的输入输出都是源代码的形式,因此,以这种方式运作的系统我们称之为源到源预处理器。当原始的语言编译器可以被作为库使用时,它还可以和预处理器绑定在一起作为后端,直接生成二进制代码,模糊了预处理期和编译器的界限。二者的区别主要是实用性,以及大部分是设计和实现的问题(模块化、可扩展性、易开发性)。关于表达力,各种方法并没有理论上的壁垒,例如一个 PPTMP 系统可以不只在文本级别上运行,而是可以完全感知语言的语法和语义,并像编译器一样复杂。事实上,有这样的情况,比如早期的 C++ 编译器是 PPTMP 系统,将 C++ 代码转换为 C 代码。在我们的讨论中,输出源代码并需要外部编译器调用的系统被归类为 PPTMP,而集成语言编译器调用的系统被归类为编译时求值系统。

代表语言和系统

所有基于词法的宏系统都可以被归类为预编译期求值系统,因为他们不感知语言,并且需要在翻译前执行,典型的是 CPP 和 M4。CPP 用于展开 C/C++ 文件中的宏调用,生成不带宏的源代 码,随后再被 C/C++ 编译器编译(现代编译器把预处理作为编译的一部分)。类似的,M4 也是把输入拷贝到输出,在这个过程中做宏展开。另一方面,基于语法的宏可以作用于任何 阶段,包括在翻译之前,作用相当于预处理器。Camlp4 作用于 Ocaml 源文件,解析源代码,并输出预处理后的源代码用于翻译。类似的,在 sweet.js 和 ExJs 中,输入文件中的宏调用被展开成 JavaScript。在 Marco 宏系统中,Marco 程序与外部输入(例如,额外的对象语言片段)一起用于生成仅包含对象语言代码的源文件。最后,Racket 宏展开过程也是一个源到源编译器,它的输入是一种语言,输出是另一种语言 [1]。

在 PPTMP 系统中通常不会遇到反射、MOP 和 AOP。但有一个例外是 Reflective Java [2] 预处理器,它通过 MOP 生成 reflection stub 类来支持反射方法调用。另外,OpenC++ 和 OpenJava,具有编译时 MOP 的特点,也可以作为源到源预处理器调用,分别生成纯 C++ 和 Java 代码。关于 AOP,预处理大多限制在采用源代码级的交织系统。例如,AspectJ 1.0 编译器可以在预处理器模式下运行,使用交织代码生成 Java 源代码,然后使用 Java 编译器编译。类似地,AspectC++ 将其输入文件转换为纯 C++ 文件,其中包含基于源代码转换系统 PUMA 的交织代码。Delta 的切面转换也属于这一类,因为切面织入器作为源到源预处理器,生成 Delta 源文件提供给编译器。

许多支持 PPTMP 的系统都属于生成程序的类别,其中元程序充当代码生成器,将输入作为元语言中指定的代码,并将其翻译成目标语言代码。所有这些情况都涉及句法转换,即输入被解析为 AST,由元程序转换,然后被反解析产生最终源代码。例如,在 JTS 中,用 Jak 编写的输入代码,通过 Jak 转换程序被解析为 AST,然后被反解析成 Java 程序。Fan 和 Stratego/XT 的操作类似,分别通过语法扩展和重写规则进行 AST 变换。Genoupe 编译器解析 Genoupe AST,将它们转换为 C# 的 AST,并反解析输出相应的 C# 源文件。SugarJ 的操作类似,将扩展的 Java 代码解析为 SugarJ 和扩展 AST 的混合代码,通过抽取 Java 部分并反解析,它被解糖为纯 SugarJ AST。SafeGen 元程序也在 AST 上操作,创建并根据匹配的输入项生成 Java 代码。cJ 将类型条件代码转换成使用泛型的 Java 类和接口,而 MTJ 和 PTFJ 生成合并了 trait 功能的纯 Java 类。类似地,MAJ 生成纯 Java 代码,它使用 AST 库表示 AOP 结构的库。

参考:
1. Tero Hasu and Matthew Flatt. 2016. Source-to-source compilation via submodules. In Proceedings of the 9th European Lisp Symposium on European Lisp Symposium (ELS’16), Article 7, 8.
2. Zhixue Wu and Scarlet Schwiderski. 1997. Reflective Java: Making Java even more flexible, 456–458. Retrieved January 2019 from https://pdfs.semanticscholar.org/b7df/83217f05f1d2d8be3ec7ecaeda72781dc7e2.pdf.

 

##3.2 编译期求值

求值类别

另一种是在目标程序编译阶段对其应用元编程,如图 10 示。这种方式成为编译期元编程 (CTMP,compile-time metaprogramming),意味着语言的编译器会被扩展以处理元代码的转换和执行。这样的扩展可以采取各种形式,如编译器插件、语法添加、AST 转换,或多阶段转换。此外,元代码执行可能需要两种形式:一种是解释元代码,即在编译器中集成元语言解释器;另一种是将元代码编译为二进制,然后执行它。这些关于元编程模型的讨论都可以算作是 CTMP 系统。

图 10:编译期元编程的典型示例

代表语言和系统

有很多宏系统是在编译期进行操作的。许多是作为编译器插件支持的,也就是说宏作为一段 程序被单独编译,并在目标程序的编译期被使用。例如 Dylan,Nemerle,Maya,JSE 和 Scala 都是单独把宏编译成二进制(DLL 或者 Java 字节码),并把它作为编译器扩展插件。Racket 宏也可以被看做是对编译器的扩展,将语法展开已有的形式。在 Honu 中,宏体是编译时表达式,与运行时分离,就像在 Racket 中一样。另一种选择是在编译时通过解释器执行来对元程序求值。MS2 就是这样一个例子,因为它对 C 的子集使用一个嵌入式解释器。Lisp 方言的许多编译版本也属于这一类,因为它们通常使用用于编译时宏展开的解释器。最后,还有一些系统可以扩展他们的编译过程以允许 AST 变换。例如,bigwig [1] 的宏系统支持使用语法和替换进行 AST 转换。

还有一些系统具有编译时 MOP。在 OpenC++ 中,元对象的代码分别编译成二进制文件,然后在客户端程序编译时作为编译器插件动态加载。OpenJava 为编译时元对象生成 Java 字节码,使用它执行 Java 的源到源转换,然后使用标准 Java 编译器生成最终字节码。Jasper 提供了允许扩展其解析的元类,语法到语法的转换,以及漂亮的打印工具。元代码编译为 Java 字节码,并在编译时执行,以覆盖默认 MOP 实例。在 DeepJava 中,对于每个元语言类,编译器都会生成一组 Java 类,以匹配类型和实例方面定义,有效地创建 clabject 元关系结构,以及生成在运行时检测 clabject 实例化的代码。

编译时 AOP 系统通常涉及编译器和织入器操作字节码。例如,自 1.1 版以来,AspectJ 使用 字节码织入;普通代码和切面被编译成二进制形式,并织入在一起,以生成在 Java 虚拟机 上运行的字节码文件。其他编译时字节码织入系统包括 PostSharp [2] 和 http://Aspect.NET [3]。还有其他 CTMP 系统支持 AOP。例如, AOP++ 依赖于模板元编程,并使用 C++ 类型系统来表示切点并在编译时匹配连接点。此外,Handi-Wrap 和 AspectScheme 使用编译时宏类生成使能运行时切面织入的代码。

许多支持 CTMP 的系统属于产生式编程的类别。C++ 模板作为类型系统的一部分被实例化,而 constexpr 表达式和函数在编译时由 C++ 解释器求值。在 D [4] 中,解释器还处理编译时函数执行。在 Groovy 中,AST 转换在编译时执行,而转换代码必须在编译目标程序之前以二进制形式存在。OCaml PPX 扩展和 Java 注释处理器的操作类似,分别作为外部程序和 jar 包供编译器调用。在 Backstage Java(BSJ)[5] 中,成功合并的编辑脚本被应用于原始的 AST,结果被序列化为 Java 源代码,然后使用 Java 编译器编译为字节码。在 MetaFJig 中,每个编译时的 metareduction 步骤都会将代码转换为 Java,使用 Java 编译器将其编译为字节码,并在 JVM 中执行该步骤以更新编译时程序表示。

提供编译时反射的系统通常是产生式编程系统,因为基于反射的元代码最终生成目标语言代码。例如,CTR 的转换构造被单独编译为 DLL,并通过 C# 属性在客户端代码中使用。在将客户端代码编译为中间语言后,编译器应用任何匹配的转换来更新它,然后生成客户端代码二进制。MorphJ 泛型类被编译成非标准的带注释的字节码文件,用作扩展的模板;在编译用到它们的程序、并使用特定类型实例化时,根据反射迭代器信息修改注释字节码的副本,为类实例化生成有效的 Java 字节码文件。

还有各种支持 CTMP 的两阶段或多阶段语言。模板 Haskell 依靠 Glasgow Haskell 编译器的内置字节码编译器和解释器来运行拼接表达式并在编译时生成代码。Metalua [6] 元程序在编译时通过自定义解释器执行。在 Converge 中,对于每个拼接表达式,编译器生成一个临时模块,其中包含具有拼接表达式和其他必要定义的函数,然后将其编译为字节码,加载到虚拟机中,并调用拼接函数生成 AST 以替换拼接表达式。Mython [7] 引文是一类转换函数,它们在编译时通过将输入的 AST 编译为 Python 字节码,并在给定环境中求值该字节码。Delta [8] 使用嵌套的 stage 标记注解来为特定 stage 收集全部源代码,并使用原始编译器和 VM 来编译和执行它,以对目标程序做整体转换。重复该过程处理更多的 stage 代码;都处理完成后,最终的程序被正常编译。

参考:
1. bigwig: http://www.itu.dk/~brabrand/bigwig.pdf
2. Gael Fraiteur. 2008. AOP on .NET – PostSharp. Retrieved April 1, 2015, from https://www.postsharp.net/aop.net.
3. Vladimir O. Safonov and Dmitry A. Grigoriev. 2005. http://Aspect.NET – An aspect-oriented programming tool for Microsoft.NET. In Proceedings of IEEE Regional Conference 2005. Retrieved January 2019 from https://sites.google.com/site/aspectdotnet/ 5.pdf.
4. Andrei Alexandrescu. 2010. The D Programming Language. Addison-Wesley Professional.
5. Zachary Palmer and Scott F. Smith. 2011. Backstage Java: Making a difference in metaprogramming. In Proceedings of the 2011 ACM International Conference on Object-Oriented Programming Systems Languages and Applications (OOPSLA’11). ACM, New York, 939–958. DOI:https://doi.org/10.1145/2048066.2048137
6. Fabien Fleutot. 2007. Metalua Manual. Retrieved January 2019 from http://metalua.luaforge.net/metalua-manual.html.
7. Jonathan Riehl. 2009. Language embedding and optimization in Mython. In Proceedings of the 5th Symposium on Dynamic Languages (DLS’09). 39–48. DOI:http://doi.acm.org/10.1145/1640134.1640141
8. Yannis Lilis and Anthony Savidis. 2015. An integrated implementation framework for compile-time metaprogramming. Software Practice and Experience 45, 6 (2015), 727–763. DOI:http://dx.doi.org/10.1002/spe.2241

 

##3.3 执行期求值

求值类别

元编程也可以应用在程序执行期间,如图 11 示。这个过程被称为运行时元编程(RTMP,runtime metaprogramming)。支持这种形式的元编程需要扩展语言执行系统,并提供运行时库以实现动态代码生成和执行。RTMP 是唯一可以根据运行时状态和执行来扩展系统的情况,例如,使用新类,其代码在执行期间通过网络传递。对于解释型语言,RTMP 与在词法上下文中解释动态源文本的 eval 函数的存在紧密耦合。对于具有宏系统的解释型语言(例如一些 Lisp 和 Scheme 实现),宏展开和替换的位置被直接求值,在单个解释步骤中将宏调用与正常程序执行交织在一起(没有单独的编译和执行)。除此之外,RTMP 系统中通常存在的其他元编程模型包括反射、MOP、AOP 和 MSP。

图 11:运行期元编程在编译型语言(上)和解释型语言(下)中的示例

代表语言和系统

在 Java 和 C# 中,编译器和加载器作为库提供,可在运行时用于编译、加载和部署任何动态代码。在 'C(Tick C)中,引号表达式(quoted expression)被代码生成函数用于使用定制运行时系统进行代码生成。类似地,DynJava 生成生成器类,这些生成器类在运行时通过字节码操作库生成可执行代码。Jumbo 将引号语法与编译器 API 结合在一起,并允许创建和操作在运行时生成和执行 Java 字节码的代码。Mnemonics 依赖于 Scala 的高级类型特性,如类型推断和函数类型,在运行时在 Java 字节码中生成函数体。

大多数具有 MOP 的元编程系统都属于 RTMP。在 Smalltalk MOP 和 CLOS 中,元对象在执行期间都是可用的,可以被访问和操作以改变程序行为。同样的情况也适用于具有 MOP 的其他语言,包括 Python、Ruby、Groovy 和 Perl,以及各种 Java 扩展。对于后者,采用两种方法来支持 MOP 功能。第一种方法是扩展 JVM 以支持元对象,如 R-Java、MetaXa 和 Guaraná。Iguana/J 还扩展了运行时引擎,但它使用了通过非标准 JIT 编译器接口加载本机动态库,而不是修改 JVM。第二种方法是使用标准 JVM 并部署字节码工程库,以在运行时更改类的实现。例如,Kava 使用加载时转换框架实现其 MOP 功能,而 Reflex 使用 Javassist 框架进行加载时结构反射。最后,还可以选择通过解释器提供 MOP 功能,就像带反射的 Java 解释器 MetaJ 一样。

作用在运行的 AOP 系统是指那些支持动态 AOP 的系统,即字节码在加载或执行期间被实现为织入。实现这样的织入过程的一种选择是依靠运行时 MOP,如 AspectS、AspectL 和 Reflex。第二种选择是部署字节码库,如 JAC 和 Handi-Wrap,它们分别使用 Javassist 和 BCEL 在类加载期间修改字节码。另一个选择是依赖执行系统内置设施或自定义扩展。例如,AspectWerkz 使用定制类加载器在类加载时织入连接点,而 PROSE 使用 JVM 调试器接口或 JIT 编译器为连接点产生钩子代码。在 Groovy AOP 中,建议代码在运行时使用动态编译将其织入到字节码中。最后,Spring AOP 使用 JDK 动态代理或 CGLIB 为给定目标对象创建代理。

在传统的 MSL 中,如 MetaML 和 MetaOCaml,代码生成发生在程序执行期间。MetaML 依赖于解释器,该解释器可以在解释期间构造、归类和运行对象程序。MetaOCaml 使用修改的 OCaml 编译器,该编译器将 stage 构造转换为构建和操作 AST 的操作,在运行时调用 编译器,并执行结果。Metaphor 和 Mint 也采用了类似的方法。Metaphor 和 Mint 将括号和转义转换为一系列 AST 组合操作,这些操作在运行时重新创建 AST,而运行操作符在运行时使用 C# 和 Java 动态代码生成功能编译和执行代码。在 LMS 中,分段代码操作被指 定为 AST 节点特征,而运行时代码生成包括:收集所有相关的 AST、发出封装为类定义的 相应源代码、编译、加载生成的类文件,最后返回一个新实例化的对象来执行操作。

 

#4 元程序源码位置

元程序可以根据其源位置进一步分类。在这种情况下,他们可能嵌入在他们打算作为其源代码的一部分进行转换的程序中,或者他们可能位于外部作为单独的转换程序。我们进一步详细说明这种分类并在每个类别中展示系统。

##4.1 嵌入在主体程序中

元程序通常是它们转换的程序的一部分,通常与普通代码混合在一起。宏定义和调用与普通程序代码放在一起。产生式编程系统通常也有它们的代码模板和生成器程序代码。这同样适用于 MSL,其中暂存代码和代码生成指令是通过暂存注解与普通程序代码区分开来。即使元编程涉及反射系统或 MOP,任何反射行为或与元对象的交互在程序本身中指定。最后,在静态 AOP 的情况下,元语言通常扩展宿主语言,因此切面代码与普通代码混合,而在动态的情况下 AOP,面向切面的构造器作为宿主语言实体提供和部署,因此它们又是主体程序的一部分。

对于存在于主题程序中的元程序,有三个转换选项:

  1. 上下文不感知 Content Unaware 元程序生成代码直接插入到它的位置,没有任何上下文信息(如宏调用)
  2. 上下文感知 Content Aware 元程序知道位置上下文并且可以转换与该上下文相关的任何代码片段
  3. 全局 Global 元程序全局地转换或影响整个程序,而不管它的位置如何

图 12:3 种嵌入主体程序的元程序的方式

 

##4.2 在主体程序外部

元程序也可以在外部定义和应用到它们的主体程序中。在这种情况下,它们被指定为单独的转换程序,通过 PPTMP 系统或与目标程序一起作为额外参数提供给编译器。

 

#5 与对象语言的关系

元编程系统分类的一个突出方面是对象语言和元语言之间的关系。实际上,每种元编程语言都可以分为两层。第一层涉及基本对象语言,第二层,我们将称为元层 metalayer,涉及用于实现元程序的元编程元素。特别是,有一些语言从零开始设计的具有元层,而对于其他语言,元层是后来引入的,独立于对象语言。接下来,我们研究元层元素(元语言)和对象语言之间的关系。在这种情况下,元语言和对象语言之间可能没有区别,元语言可以是对象语言的扩展,或者元语言可以是一种全新的语言。在第一种情况下,目标代码和元代码都使用一种语言。第二种情况类似,元语言提供所有对象语言功能以及元编程的一些扩展。这种扩展的一个典型例子是大多数元语言提供的准引用结构。在第三种情况下,元语言是与对象语言不同的语言,使用与对象语言不同的并且通常更少的构造。

在这里,我们应该对为元编程支持而扩展的语言的分类做一些说明。在某些情况下,用于指定元代码的扩展语言也可以用于普通代码,这使得第一类和第二类之间的分类有点微妙。以 MetaML 为例,一方面它扩展了 ML(即可以归入第二类),另一方面它也可以同时扮演对象语言和元语言的角色,因此可以归为第一类。从本质上讲,分类与将哪种语言视为对象语言、原始语言或扩展语言有关。我们将这些情况分类为相对于原始语言的语言扩展。在其他情况下,尽管使用元编程结构扩展了原始语言,但元语言不一定是对象语言的扩展;事实上,在许多情况下,元语言仅由这些自定义结构组成,而不能使用宿主语言的结构。因此,我们将此类情况归类为使用自定义元语言。

如果元语言与对象语言或对象语言的扩展相同,则进一步的设计选择是重用宿主语言的运行时系统还是使用自定义的运行时系统。这种困境通常在支持 CTMP 的语言中遇到,因为在支持 RTMP 的语言中,重用执行系统或扩展它以支持元编程功能通常很简单。在这种情况下,临时运行时可能更容易实现,但涉及维护问题,因为对象级运行时系统上的任何更改都应复制到元级运行时系统。另一方面,重用相同的运行时作为编译器的一部分需要模块化设计和实现,而这通常仅限于字节码编译语言;对于本机编译的语言,唯一的选择是使用自定义运行时,例如自定义解释器或 JIT 编译器。

 

##5.1 元语言与对象语言不区分

元语言与对象语言无法区分的元编程系统分为两类。首先,对象语言和元语言可以通过相同的语法使用相同的结构。其次,元语言结构可以使用对象语言语法建模并通过特殊语言或执行系统特征来应用。

第一类的典型例子是 Lisp,它的宏系统利用完整的语言本身来指定转换逻辑。在 Lisp 中,代码和数据没有区别,因此任何可以对数据执行的操作也可以使用相同的执行系统对代码执行。同样,Groovy AST 转换和 Java 注解处理器是实现特定接口的普通类,可以使用所有对象语言语法和特性。Nemerle 宏、Groovy AST 转换、Java 注解处理器和 PPX 扩展都被编译为二进制(DLL、Java 字节码、OCaml 二进制),并由它们的编译器加载,重用宿主语言执行系统。C++ constexpr 表达式和函数也使用 C++ 语法。在 C++11 中,constexpr 函数提供了有限的语法,只允许一个 return 语句。C++14 放宽了许多限制,并支持更多的语言结构(例如,用于迭代而不是递归的局部变量和循环),但是 constexpr 函数仍然仅限于 C++ 语法的子集,因为必须满足特定要求(例如,不能使用 try-catch 块)。这也适用于 D 中的编译时函数执行;只有具有可移植和无副作用代码的函数才能在编译时运行。关于它们的执行系统,C++ constexprs 和 D 编译时函数都由自定义解释器计算。

第二类的例子是支持 MOP 和 AOP 的系统。MetaXa、Guaraná、Kava、Reflex 和 MetaJ 通过正常语言语法对元对象建模。对于它们的执行,MetaXa 和 Guaraná 使用修改后的 JVM 版本,Reflex 和 Kava 重用标准 JVM 以及字节码库,而 MetaJ 依赖于自定义解释器。JAC 和 PROSE 模型 AOP 构造为 Java 对象,而 AspectWerkz、Spring 和 PostSharp 使用 Java 注解和 C# 属性来指定对象语言语法中的 AOP 功能。AspectS、AspectR、AspectL 和 AspectScheme 是无需语言或执行系统扩展即可提供 AOP 功能的库。AspectS 和 AspectL 使用语言 MOP,AspectR 使用方法包装,而 AspectScheme 使用宏。需要注意的是,在某些 AOP 情况下,切入点是使用基于 AspectJ 的模式匹配语言的字符串指定的;因此,可以说也使用了自定义元语言。

 

##5.2 元语言扩展对象语言

当一种语言用元编程特性进行扩展时,其目的是在开发元程序时利用原始语言特性,提供一种元语言作为基础语言的扩展而不是限制,并且重用众所周知的特性,而不是采用自定义的编程结构。此类语言扩展通常涉及新的语法和功能,用于区分普通代码和元代码以及后者的部署方式。

这种语法的一个常见例子是在许多宏和产生式编程系统中发现的 quasi-quote 结构。例如,MS2 宏语言是提供模板替换机制的 C 扩展。’C 还使用 quasi-quote 运算符扩展了 C,同时添加了额外的类型构造函数和一些用于处理动态代码的特殊形式。Jak、JSE、Dyn-Java、Jumbo、MAJ 和 BSJ 都是带有 quasi-quote 结构的 Java 扩展,以支持创建和操作 AST。JSE 提供了基于 Dylan 重写规则的额外模式匹配构造,而 MAJ quasi-quote 表示 AspectJ 代码而不是 Java。Scala 宏还依赖于 AST 的创建和操作,并提供 quasi-quote 作为表达 AST 的便捷语法。最后,Mython 使用可扩展的引用语法扩展了 Python,而 Fan 使用引用语法扩展了 OCaml 以支持 DDSLs。

扩展其对象语言的元语言的另一个典型案例是两阶段和多阶段语言,其中对象级和元级语法通过分段注释分开。MetaML 和 MetaOCaml 使用基本的 staging 元素扩展了 ML 和 OCaml:括号、转义和运行。类似地,Metaphor 和 Mint 分别使用括号和转义扩展了 C# 和 Java,而 run 构造则作为可用于代码对象的方法提供。模板 Haskell 使用 quasi-quote 和拼接运算符扩展了 Haskell,而 Converge 为其类似 Python 的对象语言添加了类似的扩展。Metalua 使用运算符扩展了 Lua 以用于跨元级别转换,而 Delta 使用三个基本 MSP 构造以及用于表达元级别语句和定义的额外运算符扩展了其对象语言语法。

对象语言扩展也可以采用使用附加语法的形式来指定应如何部署元级代码。例如,反射系统 Reflective Java 和 R-Java 都使用用于指定反射类的额外关键字来扩展 Java。此外,许多具有 MOP 的系统对于元级和基本级类和对象具有相同的语法,并且仅对它们的关联使用额外的语法。OpenC++、Iguana、OpenJava 和 Iguana/J 都使用 C++ 和 Java 代码来表达元级和基本级功能,同时提供额外的语法来将类与元类相关联。Maya 使用语法扩展 Java,用于引入和指定语法转换。此外,Handi-Wrap 依赖于 Maya 来扩展 Java,并使用用于 AOP 的动态方法包装器。最后,MetaFJig 使用一组原始类组合运算符扩展了 Java。

 

##5.3 元语言与对象语言的区别

使用不同的语言进行元编程的目标是最小化元语言概念与其实现(即元语言语法中的实现)之间的差距。元语言语法和结构的选择是为了更好地反映元语言概念,以简化它们在开发元程序中的使用并使它们变得更加简洁和易于理解。然而,将元语言与对象语言分开可能会导致不同的开发实践体验,并消除了它们之间的设计或代码重用的可能性,从而显著影响软件工程开发效率。从可用性的角度来看,它还需要学习和精通两种语言而不是一种。

一个说明自定义元语言与对象语言相比有多么大的不同的例子是 C++。对象语言是具有面向对象特征的编译命令式语言,而元语言是解释型纯函数式语言。因此,C++ 模板需要完全不同的编程风格,并涉及偏离常见编程风格的自定义编码实践。例如,循环需要高级功能,如递归和部分模板特化。其他使用自定义元语言的系统包括具有宏、静态 AOP 和产生式编程的系统。

CPP 和 M4 等词法宏系统对对象语言一无所知,自然使用自定义语法来指定宏转换逻辑。但是,许多语法宏也使用自定义元语言。Scheme 语法规则宏及其派生类(例如,ExJs 和 sweet.js 规则宏)使用一种与主语言分离的基于模式的声明性语言。Dylan 的宏系统是一个模式匹配模板替换系统,它不同于典型的 Dylan 语法。最后,Camlp4 使用自定义语法对 OCaml 语法进行操作。

静态 AOP 系统(例如 AspectJ 和 AspectC++)也使用自定义语言来指定 AOP 构造。Aspect 定义类似于类定义,advice 类似于方法定义,但切入点涉及带有新关键字的自定义语法,这些关键字构成了一种特殊用途的模式匹配语言。http://Aspect.NET 还为 AOP 功能引入了自定义元语言(Aspect.Net.ML)。它可以跨.Net 语言使用,并且 http://Aspect.NET 预处理器将元语言注释转换为对象语言的. Net 属性。

各种产生式编程系统也采用自定义语言进行元编程。其中一些引入了对象语言中不存在的新语言结构。例如, CTR 依赖于转换构造来编写元代码以进行检查和生成目标代码。

最后,在某些情况下,元语言在设计上支持多种对象语言,因此它自然不同于对象语言。Stratego/XT 是一个有代表性的例子,因为它支持通用程序转换。Marco 和 MetaHaskell 也属于这一类,因为他们能够为各种对象语言编写元程序。

表 7:基于是否与对象语言不同的元语言分类