在尝试了一段时间的 Scheme 之后偶尔用了一下 Clojure,发现 Clojure 有些设计实在是太好用了, 比如之前经常会去找 Scheme 上 let 的扩展实现,其实 Clojure 的 let 实现的 destructuring binding 正是我想要的功能, Clojure 的实现更加完整统一。所以我想知道
1. Clojure 能够脱颖而出最主要原因是什么,
2. 评价一下 Clojure 中的一些设计, 比如 STM 等,
3. Clojure 有哪些设计上的不足(如果有), 或者说这么问,Scheme 中有哪些优秀的东西是 Clojure 没有的(如果有)?
1. Clojure 能够吸引人的很重要一点是它是 JVM 之上的语言,这个决定非常关键。
首先,因为根植于 JVM 之上,并且做到了跟 Java 语言的相互调用,它能吸引很多成熟的 Java 开发者。其次,它可以使用 Java 社区丰富的开源软件,不需要从头去构建一个社区,你可以看到很多 Clojure 开源代码都是简单地包装 Java 的开源包,但是通过 Clojure 高度抽象简单的语法提供更便利的使用的方式;
第三,由于 JVM 平台本身的高度成熟和优化,clojure 的编译器生成的 byte code 跟 Java 编译器生成的 byte code 并无二致(不完全是),它的性能和稳定性也能马上得到保证,这比从头构建一个新平台成本低得多。
构建于 JVM 之上,Clojure 就是一门“严肃”的语言,而非很多人眼中的 Lisp“玩具”语言,你学习后可以马上使用并且实践。但是 Clojure 又是 Lisp 方言,Lisp 的神奇能力它还都保留,这样兼具美学和实用的语言如何让人不爱?我相信很多熟悉 Scheme 之类方言的童鞋,并且有 Java 背景的,都会对 Clojure 有相见恨晚的感觉。
2. Clojure 的设计原则可以概括成 5 个词汇:简单、专注、实用、一致和清晰。这不是我概括的,而是《The joy of clojure》概括的。
具体到 STM,我个人认为这个特性在日常编程中,其实你用到的机会不多。在 web 编程里,你的并发模型 Web Container 已经帮你处理(tomcat,jetty),事务也是数据库帮你处理,几乎找不到场合去使用 STM。这个特性在做一些中间件或者底层 framework 的时候才可能用到。这个特性的设计上面已经提到,跟 clojure 的设计目标是紧密相关的,跟 immutable 数据结构也是密不可分,同时它也不是没有代价,事务历史记录和慢事务频繁回滚的代价,有时候你还是需要退回去使用 Java 那套锁机制,庆幸的是 Clojure 不阻止你去使用,并且提供了类似 locking 这样的宏来方便你使用。
3. Scheme 我对它的了解也就是做过 SICP 的习题,粗粗看过《Programming Scheme》,两者对比的优缺点似乎谈不上来。需要对 Scheme 更熟悉的专家来做个对比。
Clojure 的设计缺陷不能说是缺陷,这是由于它设计的目标决定的,有得必有失。
首先还是 JVM,基于 JVM 有种种好处,但是 JVM 的启动速度实在悲剧,因此用 Clojure 写一些小的 script 处理日常事务,显得还是不够得心应手,这样的工作我还是用 Ruby,Python 的脚本语言来搞定更便捷。不过目前 Clojure 有一些其他语言之上的实现,比如 rouge-lang/rouge · GitHub 和 halgari/clojure-py 路 GitHub 这些实现应该会比 JVM 的启动快很多(抱歉,我没测试过)。
不仅如此,因为 Clojure 跟 JVM 平台的绑定如此之深,并且为了真正发挥 Clojure 的威力,你还需要去熟悉 Java 平台的东西,熟悉 Java 语言、类库、内存模型、GC 优化、多线程和网络编程、开源类库等等。可以这样认为:想成为一个好的 Clojure 程序员,首先需要是一名好的 Java 程序员。这也一定程度上阻碍了 Clojure 的推广,提高了学习成本。
其次,Clojure 的 API 设计上,有时候不符合你的直觉,而是符合 Clojure 的哲学,比如 contains?函数对 vector 等数组型集合的调用上。关于这一点,Rich 的回答是“Elegance and familiarity are orthogonal.”,也就是优雅和熟悉是正交关系的。保持 API 内在的一致性,比直觉的“熟悉”更重要。Clojure 不妥协于你的“直觉”。
第三,弱类型的好处足够多,灵活,减少声明代码,适合探索式编程;同样,坏处也不是没有,没有类型保障,错误可能要等到运行时才能发现,静态代码检查工具也没有办法帮你发现,这就需要你一定程度的测试代码来保证运行时行为。
第四,性能上,虽然 clojure 生成的字节码已经很高效,也有 type hint 这样的技术来帮助提升性能,但是会有不少的转型(checkcast)、装箱和拆箱(boxing and unboxing)以及类型判断分支跳转的多余指令,这在一些性能敏感的应用里可能会暴露出来。尽管我认为大多数网站型的应用瓶颈都会落在 IO 上。
以上是我对这个问题的一些看法,欢迎探讨。谢谢。
P.S. 我们的整个网站几乎都是基于 clojure 架构的,有兴趣可以看下 AVOS Cloud,每天承载的请求量也在亿次级别。