上篇文章我们聊了 Clojure 语言的基本特性和语言生态,这篇文章我想讲讲 Clojure 的语法,以及最有特色的 REPL 驱动编程。

欢迎查看这个系列的上一篇文章,也欢迎关注我的全栈开发笔记系列专栏,我将更新更多精彩内容。


Clojure 一系列神奇的特性,比如非常好用的 REPL 驱动开发、基于宏的元编程等。

究竟是什么带来了这一系列可以极大提高开发者体验的特性呢?我希望在这篇文章初探一下。

1. Clojure 的基本语法结构

如果你看过 Lisp 语系的语言,一定会对其满屏的()小括号印象深刻。

不同于 C 系的语言普遍使用{}来划分代码段,Lisp 系语言使用的是层层叠叠的小括号。

我们来看一个 Clojure 官网提供的示意图:

Clojure的S表达式 (Source: clojure.org)

这个示意图上使用了一个非常基本的 Clojure 表达式:

(+ 3 4)

这和其他语言的3 + 4的含义是一样的,最后计算出的结果也是 7。

用 Clojure 的语法概念来分析,+加号一个符号,这个符号代表的是加法函数。后续的数字34,是传入函数的参数。

从另一个角度说,这整个表达式不仅仅表示了一个函数。如果我们不考虑这行代码在执行时的作用,只看字面形态,这个()由小括号括起来的结构实际上也可以代表 Clojure 原生数据结构:列表(List)。

也就是说,每个 Clojure 的表达式实际上也是一个 List,我们可以去评估这个表达式的执行结果,也可以像操作一个列表一样来操作这个表达式。

这个特性,实际上对于 Clojure 实现方便的宏编程有着重要的作用。当然,要讲到宏,还需要再写 2~3 篇文章,欢迎大家关注我。


2. 独特的代码评估过程

如果你接触过 Java,一定熟悉 Java 的代码评估流程。我们来看一个同样援引自 Clojure 官网的流程图:

Java源代码评估流程图 (Source: clojure.org)

Java 的源代码被编译器javac编译成可以在 JVM 上运行的字节码。

那么,同样作为运行在 JVM 上的语言,Clojure 的策略是什么样的呢?

Clojure独特的评估流程 (Source: clojure.org)

源代码在被编译成字节码前首先经历了一步,就是被Reader读取成了 Clojure 数据结构 ── 也就是上文提到的 List。

这种设计就意味着:

  1. Clojure 代码的最小执行单元不再是源代码文件,而是一个由()括起来的表达式;
  2. 由于源代码文件实际上也被读取成一个个表达式,那么其实完全可以由开发者交互性输入一个个需要的表达式然后执行,这就为 REPL 驱动开发提供了可能;
  3. 由于把源代码转化成数据结构的Reader和编译为字节码的Compiler两个步骤被严格分开,用代码生成代码的思路(也就是宏)成为可能。宏在编译阶段(Compiler)的早期进行展开,而这个编译阶段的输入是 Reader 产生的数据结构。宏展开发生在将数据结构编译成字节码之前;

下面,我们具体重点讨论第二点,也就是 REPL 驱动开发。


3. REPL 驱动开发

所谓 REPL,指的是“Read-Eval-Print-Loop”,也即读取 ── 评估 ── 打印循环。

如果你用过 Python 的 Jupyter Notebook 或者是 Wolfram Mathematica,我相信你一定不会对 REPL 感到陌生。每次需要修改代码时,无需从头到尾执行一遍代码文件,而是可以只执行一个较小的代码片段,立刻看到这个代码片段的执行结果。

事实上,Clojure 的 REPL 要比 Python、Wolfram 语言的 REPL 要好用得多。究其原因,还是因为 Lisp 语系独特的语法结构。

由于任何一个由()小括号括起来的表达式都可以执行,我们可以尝试按照任意顺序、执行任意嵌套层级的表达式。

举一个简单的例子:

这是我配置好的 VSCode + Calva 插件的环境,这是我从一个代码文件中间随便挑出的代码片段:

一段简单的代码示例

我可以随性所欲地执行代码片段看看结果:

使用Calva评估每行代码

这种即时反馈和探索能力,是不是极大地提升了开发体验?


总结来说,Clojure 基于 S 表达式(S-expression)的语言特性,使其天然具备了实现强大开发工具(如 REPL)和语言扩展能力(如宏)的基础,带来了许多独特且高效的开发体验。

今天这篇文章着重讲了 REPL 驱动开发的基本概念。下面几篇文章,我将谈谈 Clojure 的基本数据结构,流程控制,以及函数式风格的高阶函数写法等问题。

关注我,不错过更多有趣的技术博客!

已复制! 复制代码到剪贴板