本文最初发布于 On stranger tides 博客,经原作者 Rahul Raveendranath 授权由 InfoQ 中文站翻译并分享。
导读: Clojure (发音类似"closure")是一种运行在 Java 平台上的 Lisp 方言,Lisp 是一种以表达性和功能强大著称的编程语言,但人们通常认为它不太适合应用于一般情况,而 Clojure 的出现彻底改变了这一现状。如今,在任何具备 Java 虚拟机的地方,都可以利用 Lisp 的强大功能,而且在设计上还兼顾了 Java 的互操作性。Clojure 是一种高级的,动态的函数式编程语言。它是基于 Lisp 编程语言设计的,并且具有编译器,可以在 Java 和 .Net 运行时环境上运行。从 20 世纪 50 年代开始至今,Lisp 语言家族已经存在很长时间了。Lisp 使用的是截然不同的 S - 表达式或前缀注释。这个注释可以被归结为 (function arguments…)。通常总是从函数名开始,然后列出要向这个函数添加的零个或多个参数。函数及其参数通过圆括号组织在一起。数量众多的括号也成为了 Lisp 的一大特征。你可能已经发现,Clojure 是一种函数式编程语言。专业人士可能会说它太过单一,但实际上它却囊括了函数式编程的所有精华:避免了不稳定状态、递归、更高阶的函数等。Clojure 还是一个动态类型的语言,你可以选择添加类型信息来提高代码中的关键路径的性能。Clojure 在设计上也考虑了并发性,并具有并发编程的一些独特特性。另外,我们也持续同步翻译了《通过 Lisp 语言理解编程算法》系列文章,有兴趣的读者可以去阅读该系列文章来理解 Lisp 的特性。Lisp 有很多方言,Clojure 只不过是其中之一,但为什么它能够脱颖而出呢?Rahul Raveendranath 分享了他的思考。
Clojure 在我的工作中作为主打编程语言已逾两年,我非常喜欢这门语言!我仍然会遇到很多搞开发的朋友和同事,他们属于这两种情况中的一种:
从未听说过 Clojure 或 Lisp。
听说过 Clojure,但将其视为又一个 Lisp 而不屑一顾。
在本文中,我希望就第二种情况讨论 Clojure 的特别之处。但对于第一种情况,我会从一个简短的背景介绍开始。
除非你一直生活在岩石下,否则我想你一定听说过函数式编程(Functional Programming,FP)。近年来,它保持了快速发展的势头,尤其是像 React、Redux 这样流行的前端框架采用了这种模式。即使是 OOP 的典型代表,从 Java8 开始,Java 也开始支持了函数式编程。现代服务需要能够在全球范围内扩展,通过互联网为全球受众提供服务,并发性就变得有必要了。这就是函数式编程的高光时刻。Lisp 就是一类编程语言,可以追溯到 1958 年,建立在 Lambda 演算 之上,支持函数范式。Clojure 是最接近 Lisp 的语言之一,由 Rich Hickey 于 2007 年创建。
那么,是什么使 Clojure 从其他 Lisp 方言中脱颖而出?
1. 数据结构的持久性和不可变性
Clojure 有一组丰富的数据结构。大多数人都没有意识到的是,在语言 / 数据结构设计方面,这些技术有多先进。这些都是基于该领域数十年进展之上并在其上进行改进的结晶。
随着时间的推移,数据结构不断演变,最终导致了 Clojure 的诞生。来源:《深入研究 Clojure 的数据结构》,Mohit Thatte。
不可变性
在程序员的职业生涯中,大多数不得不面对或修复意想不到的生产缺陷,他们都知道易变性的危害。Java 开发人员已经尝试使用一些措施来解决这一问题,比如,在代码库中遍布 final
关键字等。
Clojure 通过使所有数据结构在默认情况下不可变,从而在更高的抽象级别上解决了这个问题。如果你是这个概念的新手,你可能会想知道,如果一切都是不可变的,该如何让任何人能够完成任何事情呢?理解这一点的关键是认识到价值和身份之间的区别,这些概念在 OOP 和命令式世界中经常被混淆。简单来说,每当需要更改某个内容时,都会创建一个新版本,从使用角度来看,这个新版本与原始版本无关。
如果你考虑如何实现不可变性,一个简单的解决方案可能是,在每次修改整个数据结构时,对其进行复制。但你很快就将意识到这个方案的效率是多么低下。这就是 Clojure 使用的所有高级数据结构(如映射到 trie 或 HAMT 的哈希数组)用武之地。在过去,就像熊掌和鱼不可兼得一样,你必须在不可变性和性能之间做出选择。但现在,有了 Clojure,你可以同时获得不可变性和性能!
持久性
如上所述,使用不可变性,可以随着时间的推移,创建同一数据结构的不同版本。Clojure 实现了不可变的数据结构,你可以获取这些数据的任何先前或当前版本,并进一步修改它们来创建更新的版本,而不会出现任何问题。这一属性就是“持久性”。
在 Clojure 中不同版本之间的数据共享。来源:《实践理论:数据结构的持久性》
2. 可与 JVM(以及。Net 和 JavaScript)进行互操作
Clojure 编译为 Java 字节码,可在 JVM 上运行。我不能强调这对我的团队有多大的改变。一般来说,这使得在公司环境中开始使用 Clojure 变得更加容易,原因如下:
你可以像在现有 Java 代码库中添加库那样“偷偷”使用 Clojure。
你或 Clojure 社区不必通过重新创建 Java 中已存在的所有有用库来重新“发明轮子”。
在过去,采用其他 Lisp 时,无法做到这两点,一直是重要的限制因素。而第二点很重要:不仅可以利用 JVM 中的所有库,Clojure 代码还可以与为 JVM 无缝构建的工具 / 框架一起工作。互操作性真的很好。你甚至可以将 Java 方法和接口公开给 Clojure 代码,这样别人就无法分辨出其中的区别(参见 gen-classes)。
以上谈到的几点,适用于 .Net 生态系统以及 JavaScript 生态系统,因为 Clojure 的实现可以编译成各自的运行时格式。JS 版称为 ClojureScript。
3. 额外之物:Clojure.spec
Clojure 是一种动态编程语言,和大多数其他 Lisp 一样。其中一个关键的实际含义是,你无需对代码进行静态类型检查。虽然静态类型检查确实有助于偶尔在编译时捕获一些错误,但它有两个主要限制:
程序员无法选择静态类型的内容 / 位置。一切都是类型。这在代码中造成了大量的冗长和不必要的“噪音”。
类型系统的表达能力相当有限。只能检查语言设计器提供的数据类型。
除非你亲身体验,否则很难想想还有这样的一个世界,你可以获得比静态类型更强的验证能力,同时还没有这两个限制。Clojure spec 为我们提供了这一点。虽然 Clojure spec 的目标并不是成为一个类型系统,但它远不止于此。
为了说明可表达性的要点,请想想你最喜欢的静态类型语言(例如 Java)中的基本数据类型。你可以使用 Integer
和 String
数据类型。如果在你的领域 / 程序中,你希望关注具有同级别区别的正数和负数,该怎么办?如果不创建新的类和对象的话,就不能以简单 / 直接的方式进行。如果你的类型定义更复杂或涉及到动态性,那么这将会使情况变得更棘手。
使用 CLojure spec,你可以使用任意逻辑谓词(例如 int?
、pos?
等)来创建新类型。下面是一个创建名为 pos-int
的新规范的示例代码,该规范描述了一个正整数。
你可以选择规范你的重要数据(主要是接口),并验证你是否在运行时轻松接受 / 输出符合规范的数据。规范还支持根据规范自动生成测试数据的生成式测试。与使用单元 / 集成测试中使用任意虚拟数据调用函数不同,你指定了它的输入 / 输出,并使用它自动生成 100 个测试输入,以运行该函数并通过编程方式来验证输出是否符合规范。
结论
还有许多其他理由热爱 Clojure。下面是 Uncle Bob(因撰写 CleanCode 一书而闻名)最近写的一篇文章:Why Clojure? 讲述的是他为什么喜欢 Clojure。然而,以上所述是我认为 Clojure 在其他 Lisp 失败的情况下仍然取得成功的关键原因。
我希望你能够在下一个(爱好或工作)项目中认真考虑是否使用 Clojure。既然你读到这里,我建议你去看看 Rich Hickey(Clojure 的创造者)最受欢迎的演讲:Rich Hickey’s Greatest Hits。即使你最终没有选择使用 Clojure,观看这些演讲肯定会让你成为一名更好的程序员。
“每次观看他的演讲,都有一种醍醐灌顶的感觉。”
作者介绍:
Rahul Raveendranath,对科技和创业充满热情,他着迷的技术包括人工智能、增强 / 虚拟 / 混合现实,自然语言处理、编程语言等。
原文链接:
Why Clojure is not just Yet Another Lisp
评论