Clojure is a compiled language, yet remains completely dynamic -- every feature supported by Clojure is supported at runtime. javascript
Rich Hickey clojure.org/html
这里的 runtime 指的是 JVM,JVM 之初是为运行 Java 语言而设计,而如今已经发展成一重量级平台,除了 Clojure 以外,不少动态语言也都选择基于 JVM 去实现。
为了更加具体描述 Clojure 运行原理,会分两篇文章来介绍。
本文为第一篇,涉及到的主要内容有:编译器工做流程、Lisp 的宏机制。
第二篇将主要分析 Clojure 程序编译成的 bytecode 如何保证动态语言的特性以及如何加速 Clojure 程序执行速度,这会涉及到 JVM 的类加载机制、反射机制。前端
SO 上有个问题 Is Clojure compiled or interpreted,根据本文开始部分的官网引用,说明 Clojure 是门编译型语言,就像 Java、Scala。可是 Clojure 与 Java 不同的地方在于,Clojure 能够在运行时进行编译而后加载,而 Java 明确区分编译期与运行期。java
与解释型语言里的解释器相似,编译型语言经过编译器(Compiler)来将源程序编译为字节码。通常来讲,编译器包括两个部分:git
Clojure 的编译器也遵循这个模式,大体能够分为如下两个模块:github
上图给出了不一样阶段的输入输出,具体实现下面一一讲解。编程
通常来讲,具备复杂语法的编程语言会把词法分析与语法分析分开实现为 Lexer 与 Parser,但在 Lisp 家族中,源程序的语法就已是 AST 了,因此会把 Lexer 与 Parser 合并为一个过程 Reader,核心代码实现以下:后端
for (; ; ) {
if (pendingForms instanceof List && !((List) pendingForms).isEmpty())
return ((List) pendingForms).remove(0);
int ch = read1(r);
while (isWhitespace(ch))
ch = read1(r);
if (ch == -1) {
if (eofIsError)
throw Util.runtimeException("EOF while reading");
return eofValue;
}
if (returnOn != null && (returnOn.charValue() == ch)) {
return returnOnValue;
}
if (Character.isDigit(ch)) {
Object n = readNumber(r, (char) ch);
return n;
}
IFn macroFn = getMacro(ch);
if (macroFn != null) {
Object ret = macroFn.invoke(r, (char) ch, opts, pendingForms);
//no op macros return the reader
if (ret == r)
continue;
return ret;
}
if (ch == '+' || ch == '-') {
int ch2 = read1(r);
if (Character.isDigit(ch2)) {
unread(r, ch2);
Object n = readNumber(r, (char) ch);
return n;
}
unread(r, ch2);
}
String token = readToken(r, (char) ch);
return interpretToken(token);
}复制代码
Reader 的行为是由内置构造器(目前有数字、字符、Symbol 这三类)与一个称为read table
的扩展机制(getMacro)驱动的,read table
里面每项记录提供了由特性符号(称为macro characters
)到特定读取行为(称为reader macros
)的映射。微信
与 Common Lisp 不一样,普通用户没法扩展 Clojure 里面的read table
。关于扩展read table
的好处,能够参考 StackOverflow 上的 What advantage does common lisp reader macros have that Clojure does not have?。Rich Hickey 在一 Google Group里面有阐述不开放 read table
的理由,这里摘抄以下:数据结构
I am unconvinced that reader macros are needed in Clojure at this
time. They greatly reduce the readability of code that uses them (by
people who otherwise know Clojure), encourage incompatible custom mini-
languages and dialects (vs namespace-partitioned macros), and
complicate loading and evaluation.
To the extent I'm willing to accommodate common needs different from
my own (e.g. regexes), I think many things that would otherwise have
forced people to reader macros may end up in Clojure, where everyone
can benefit from a common approach.
Clojure is arguably a very simple language, and in that simplicity
lies a different kind of power.
I'm going to pass on pursuing this for now,
截止到 Clojure 1.8 版本,共有以下九个macro characters
:
Quote (')
Character (\)
Comment (;)
Deref (@)
Metadata (^)
Dispatch (#)
Syntax-quote (`)
Unquote (~)
Unquote-splicing (~@)复制代码
它们的具体含义可参考官方文档 reader#macrochars。
Compiler 类主要有三个入口函数:
clojure.core/compile
时使用clojure.core/require
、clojure.core/use
时使用clojure.core/eval
时使用这三个入口函数都会依次调用 macroexpand、analyze 方法,生成Expr
对象,compile 函数还会额外调用 emit 方法生成 bytecode。
Macro 毫无疑问是 Lisp 中的屠龙刀,能够在编译时
自动生成代码:
static Object macroexpand(Object form) {
Object exf = macroexpand1(form);
if (exf != form)
return macroexpand(exf);
return form;
}复制代码
macroexpand1 函数进行主要的扩展工做,它会调用isMacro
判断当前Var
是否为一个宏,而这又是经过检查var
是否为一个函数,而且元信息中macro
是否为true
。
Clojure 里面经过defmacro
函数建立宏,它会调用var
的setMacro
函数来设置元信息macro
为true
。
interface Expr {
Object eval();
void emit(C context, ObjExpr objx, GeneratorAdapter gen);
boolean hasJavaClass();
Class getJavaClass();
}
private static Expr analyze(C context, Object form, String name)复制代码
analyze 进行主要的语义分析,form
参数便是宏展开后的各类数据结构(String/ISeq/IPersistentList 等),返回值类型为Expr
,能够猜想出,Expr
的子类是程序的主体,遵循模块化的编程风格,每一个子类都知道如何对其自身求值(eval)或输出 bytecode(emit)。
这里须要明确一点的是,Clojure 编译器并无把 Clojure 代码转为相应的 Java 代码,而是借助 bytecode 操做库 ASM 直接生成可运行在 JVM 上的 bytecode。
根据 JVM bytecode 的规范,每一个.class
文件都必须由类组成,而 Clojure 做为一个函数式语言,主体是函数,经过 namespace 来封装、隔离函数,你可能会想固然的认为每一个 namespace 对应一个类,namespace 里面的每一个函数对应类里面的方法,而实际上并非这样的,根据 Clojure 官方文档,对应关系是这样的:
gen-class
都会生成一个.class
文件<filename>__init
的加载类gen-class
生成固定名字的类,方便与 Java 交互生成的 bytecode 会在本系列第二篇文章中详细介绍,敬请期待。
每一个 Expr
的子类都有 eval 方法的相应实现。下面的代码片断为 LispExpr.eval
的实现,其他子类实现也相似,这里不在赘述。
public Object eval() {
IPersistentVector ret = PersistentVector.EMPTY;
for (int i = 0; i < args.count(); i++)
// 这里递归的求列表中每项的值
ret = (IPersistentVector) ret.cons(((Expr) args.nth(i)).eval());
return ret.seq();
}复制代码
以前看 SICP 后实现过几个解释器,可是相对来讲都比较简单,经过分析 Clojure 编译器的实现,加深了对 eval-apply 循环的理解,还有一点就是揭开了宏的真实面貌,以前一直认为宏是个很神奇的东西,其实它只不过是编译时运行的函数
而已,输入与输出的内容既是构成程序的数据结构,同时也是程序内在的 AST。
PS: 微信公众号,头条,掘金等平台均有我文章的分享,但个人文章会随着我理解的加深不按期更新,建议你们最好去个人博客 liujiacai.net 阅读最新版。