[译]元编程
本文翻译自 stereobooster 的博客文章:metaprogramming ,少量删改。完整内容参阅原文。
什么是元编程
不好的是,没有一个达成共识的单一定义。让我们参阅一下:
元编程是一种编程技术,指计算机程序具有将其他程序视为其数据的能力。 – wikipedia
元编程是指程序具有了解自身或操纵自身的多种方式。 – stackoverflow 上的流行答案
“支持元编程”意味着用户可以有效修改该语言内置语法(例如 Lisp 的宏)或扩展该语言常规语法(例如 C 的预处理程序)。 – rosettacode
没有一个很好的定义,让我们看一些例子。当人们谈论元编程时,他们可能指的是:
- macros in Lisp (1960)
- preprpcessor in C (1973)
- hygenic macros in Scheme (1986)
- C++ templates (1986)
- “Dynamic” metaprogramming in Smalltalk (1980) and Ruby (1995-2005?)
- Reflections in Java (1997)
两类元编程
元编程大致分为两类:
- 一种是(编译时)作为源代码(例如宏,预处理器,模板),通常称为“宏”
- 另一种是(运行时)基于“OOP 技巧”(例如动态调度和反射)以支持其他行为,这没有名字,我把它称为“动态”
compile time | runtime | |
---|---|---|
macros in Lisp | ? | + |
Preprocessor, templates | + | |
Dynamic metaprogramming | + |
动态元编程
元编程是编写在运行时操纵(自身)语言结构的代码。 – Ruby 元编程
元编程在 Ruby 中比在其他的动态类型语言中更常用,尤其是在 Rails 中,例如:Path and URL Helpers 。动态元编程的缺点是“事物”没有源代码:你看到了一个函数,但是你不知道它的定义位置,这破坏了“grep test ”。另一个缺点是它趋向于变慢,例如,参见 Rails / DynamicFindBy 。
编程语言:
主要用法:
- 消除重复代码(DRY),例如,David Beazley 在演讲中 展示了很多例子。
- 创建嵌入式领域特定语言(EDSL),Martin Fowler 称它们为内部 DSL 。例如,Sass (能转换为 CSS 的 Ruby EDSL),Haml (能转换为 HTML 的 Ruby EDSL),Active Record 查询接口(能转换为 SQL 的 Ruby EDSL),最重要的是 Rake(替代 Make 的 Ruby EDSL)。
- “扩展语言”
关于扩展语言
如何扩展语言?可以添加更多关键字(扩展词法),也可以添加更多关键字的组合规则(扩展语法)。
我们很容易添加更多关键字,例如,定义新的函数、模块、变量,但并不是所有种类 – 仅限于语言语法允许使用的标识符(例如,我不能定义 :?:
)。在 Ruby 和 Python 中,可以重载运算符(+
,-
,>
,<
等),但不能定义新的运算符。据我所知,这些语言均不允许我定义新的语法规则,例如,我无法定义自己的 if/else
版本。
但程序员们总能找到一种方法来解决这个问题 – 可以重用现有的语法,让它看起来像是另一种语法。例如,在函数式语言中,有一个漂亮的“模式匹配”的概念。OCaml:
|
|
或是 Scheme:
|
|
这是在 JavaScript 中的实现:
|
|
这是一个旧语法,但如果你细心,它看起来就像 OCaml 中的模式匹配。在幕后,它使用 toString
检查实际的代码,因为以前没有一等公民的反射对象。另一个值得注意的技术是“链式”(例如 jQuery 和 Active Record 查询接口)。
Macros(宏)
宏是一个宽泛的范畴,让我们看一下使用示例来了解这一点。
语法扩展
在 Lisp 中 if/else
表达式像这样:
|
|
定义具有相同结构的函数很容易:
|
|
关键在于,Lisp 中的函数是立即执行的。这意味着在将参数传递给函数之前,它就会执行 then
和 else
两个分支,这就是宏的作用。有了宏,就可以定义自己的 if
版本,像你期望的那样。
另请参阅:
DSL
JSX 是 ECMAScript 中类似 XML 的语法扩展,没有任何定义的语义 – Draft: JSX Specification
它本质上是一个 DSL。而负责编译它的 Babel 插件是一个预处理器。你可以使用其他的元编程技术来实现同样的结果 – 参见 JSX 的替代方案 。
多态性
…多态语言,其中一些值和变量可能有一个以上的类型。多态函数是指其操作数(实际参数)可以有一个以上类型的函数。多态类型是指其操作可以适用于一种以上类型的值的类型。 – On Understanding Types, Data Abstraction, and Polymorphism
令我惊讶的是:
- 动态类型语言,是非常灵活的(但也很容易给自己找麻烦)。
- 静态类型的语言,具有完全的多态性支持,如 OCaml,Haskell 等。
- 没有多态性或在多态性上有一定限制的静态类型语言(Pascal,Go)。
最后一类编程语言可以使用元编程来实现类似多态性的东西(“提高灵活性”)。在 GO 中,没有参数多态(或类型参数,或泛型),于是人们创造了解决方法,例如,gengen (类似的解决方案 genny ,generic ,gen )。
|
|
然后,您需要运行预处理器:
|
|
你会得到类型准确的代码:
|
|
另请参阅:Who needs generics? Use … instead! , The Next Step for Generics.
DRY
模板元程序员利用这种机制来提高:源代码的灵活性和运行时性能。 – Walter E. Brown “Modern Template Metaprogramming: A Compendium, Part I”
在 C++中,有函数重载(即一种多态),但它会产生很多重复:
|
|
你可以编写函数模板:
|
|
性能
通常认为,在编译时进行宏扩展可以提高性能。对我来说这很合理,但我没有很好的例子。
相关:Compile-time reflection and compile-time code execution in Zig.
宏和类型
Lisp(和 Scheme)宏非常强大,但它们不能与静态类型检查器一起很好地工作。假设我们有确保能够终止的宏,并且能在编译时扩展(语法糖)并进行类型检查生成的代码,下一个问题是在生成的代码中报告类型错误,这也会很混乱。
有多种尝试使宏与静态类型更好地配合使用,例如:
参阅资料