Clojure(/ˈkloʊʒər/)[16][17]是Lisp编程语言在Java平台上的现代、动态及函数式方言。[18][19] 与其他Lisp一样,Clojure视代码为数据且拥有一套Lisp宏系统。[20]Clojure的开发过程目前由社区驱动,[21]其作者里奇·希基则以终身仁慈独裁者的身份监督。[22]
编程范型 | 多重编程范型: |
---|---|
设计者 | 里奇·希基 |
发行时间 | 2007年 |
当前版本 |
|
类型系统 | |
系统平台 | |
许可证 | Eclipse公共许可证 |
文件扩展名 |
|
网站 | clojure |
启发语言 | |
影响语言 | |
|
Clojure提倡不可变性与持久数据结构并鼓励程序员显式地管理标识及其状态。[23]对利用不可变值及显式时间进展构造(explicit progression-of-time constructs)进行编程的专注旨在促进更加健壮的(尤其是并发)程序的开发。[24][25][16]Clojure的类型系统是完全动态的,但人们近期也开始探索其基于渐进类型化的实现。[26]
对Clojure的商业支持由Cognitect公司提供。[27] 每年都会在全球范围内举办年度Clojure会议,其中最著名的是Clojure/conj[28]。
历史与开发过程
里奇·希基创造了Clojure语言。[18]此前,他开发过类似但基于.NET平台的项目——dotLisp。[29]在那之前,他还尝试了三次在Lisp与Java之间提供互操作:Common Lisp的Java外语接口[30]、Lisp的外语对象接口[31]以及Lisp友好的Java Servlet接口[32]。
在公开发布之前,里奇·希基花了大约两年半的时间开发Clojure。他在没有外部资金的情况下,将其大部分时间都专门投入到了Clojure的工作上。开发快要完成,里奇·希基向Common Lisp社区里的一些朋友发布电邮,宣布他完成开发了Clojure。
Clojure的开发过程在Clojure JIRA项目网页由社区驱动并管理。[21][33]该网站可用于提交问题报告。一般的开发讨论是在Clojure谷歌网上论坛上进行的。[34] 任何人都可以提交错误报告和想法,而贡献补丁前则需要先签署Clojure贡献者协议。[35] JIRA错误报告由一组筛选者处理,最终由里奇·希基批准更改。[36]
设计理念
里奇·希基开发Clojure的原因是因为他想要一款适合函数式编程的现代Lisp。该语言既需要与已建立的Java平台共生又需要有适合并发性的设计。[24][25][37][16]
在Clojure中,状态以标识的概念为特征。[23]一系列状态随着时间的推移而产生,就是标识。由于状态是不可变的值,任意数量的工作单位都可以在其上并行实施操作,并发性就成为一道管理状态到状态的变化的问题。因此,Clojure提供了几个可变的引用类型。每个引用类型都有其明确定义的语义用于控制状态之间的跃迁。[23]
语言概述
版本 | 发布日期 | 主要功能/改进 |
---|---|---|
2007年10月16日[38] | 首次公开发布 | |
1.0 | 2009年5月4日[39] | 首个稳定版 |
1.1 | 2009年12月31日[40] | 将来 |
1.2 | 2010年8月19日[41] | 协议 |
1.3 | 2011年9月23日[42] | 增强对原始类型的支持 |
1.4 | 2012年4月15日[43] | 读取器字面量 |
1.5 | 2013年3月1日[44] | 归纳器 |
1.5.1 | 2013年3月10日[45] | 修复内存泄漏 |
1.6 | 2014年3月25[46] | Java API、经过改进的哈希算法 |
1.7 | 2015年6月30日[47] | 变换归纳器、读取器条件表达式 |
1.8 | 2016年1月19日[48] | 附加的字符串函数、直接连接、套接字伺服器 |
1.9 | 2017年12月8日[49] | 集成spec、命令行工具 |
1.10 | 2018年12月17日[50] | 经过改进的错误报告、Java兼容性 |
1.10.1 | 2019年6月6日[51] | 解决Java性能回归问题并改进clojure.main的错误报告 |
1.10.2 | 2021年1月26日[52] | Java互操作性/兼容性改进和其他重要语言修订 |
1.10.3 | 2021年3月4日[53] | prepl支持读者条件 |
1.11.0 | 2022年3月22日[54] | 新的关键字参数调用语法,新的clojure.math 命名空间,命名空间别名不用加载,并向clojure.core 增加新的帮助函数
|
1.11.1 | 2022年4月5日[15] | 在类型clojure.lang.Keyword 和clojure.lang.ArraySeq 的对象的二进制序列化中回滚意外的变更。
|
当前版本 |
Clojure执行于Java平台之上,因此,与Java紧密集成并完全支持从Clojure调用Java代码。[55][16] 与此同时,也可以从Java调用Clojure代码。[56] Leiningen是社区中普遍使用的项目自动化工具。Leiningen为Maven集成提供支持,处理项目包管理和依赖项。Leiningen的配置使用的则是Clojure语法。[57]
与其他大多数Lisp一样,Clojure的语法建立在S-表达式之上。S-表达式在被编译之前先由读取器解析为数据结构。[58][16] 除了列表之外,Clojure的读取器还支持映射、集合及向量等的字面量语法。这些字面量随后会被直接编译成上述数据结构。[58] Clojure是Lisp-1且有一套与其它Lisp不兼容的数据结构,因此,Clojure不支持与Lisp的其它方言之间的代码级兼容性。[20]
作为一门Lisp方言,函数在Clojure中是一等公民。此外,Clojure还支持读取﹣求值﹣输出循环以及一套宏系统。[6] Clojure的Lisp宏系统与Common Lisp的系统极为相似。唯一不同的是,Clojure的重音符(称为语法引用)用命名空间来限定符号。这有助于防止意外的名字捕获,因为Clojure禁止绑定到用命名空间限定的名字(namespace-qualified name)上。如果需要强制捕获宏扩展(capturing macro expansion,)那么就需要显示地完成该过程。Clojure不支持用户定义的读取器宏(reader macro,)但Clojure的读取器支持更具约束力的语法扩展形式。[59] Clojure支持多方法(multimethods。)[60] 对于类似接口的抽象,Clojure提供基于协议[61]的多态性以及基于记录[62]的数据类型系统。 Clojure通过这些设计来提供高性能且动态的多态性以避免所谓的“表达式问题”("expression problem"。)
Clojure支持惰性序列,并鼓励不可变性与持久数据结构(persistent data structure。)Clojure作为一门函数式编程语言将重点放在递归与高阶函数上而不是基于副作用的循环流程上。Clojure不支持自动尾调用优化,因为JVM还不支持该项优化,[63][64][65]但是,可以用recur
关键字显式地执行该项优化。[66] 对于并行与并发计算,Clojure提供软件事务内存、[67] 响应式代理系统[1]及基于通道的并发编程。[68]
Clojure 1.7引入了读取器条件表达式从而允许在同一命名空间中嵌入Clojure与ClojureScript代码。[47][58] 变换归纳器的加入则提供了另一种组合变换的方法。变换归纳器可以使高阶函数(如,map
和fold
)更加抽象从而使之独立于其输入数据源。传统地说,这些函数一般被应用于序列上,而变换归纳器允许这些函数被应用于通道上并让用户定义她们自己的变换归纳模型。[69][70][71]
平台
Clojure的主要平台是Java[19][55]但也存在其他目标平台上的实现。其中,最值得关注的是ClojureScript[72](可被编译成ECMAScript 3[73])和ClojureCLR[74](.NET平台上的完整移植版,可与其生态系统互操作。)2013年对1,060名受访者进行的Clojure社区调查[75]发现,47%的受访者在使用Clojure的同时也使用ClojureScript。2014年,这一数字增长到了55%,[76]而到了2015年,则达到了66%(根据2,445名受访者)。[77] 人气较高的ClojureScript项目包括React实现,如Reagent[78]、re-frame[79]、Rum[80]及Om[81][82]。
人气
随着对函数式编程的兴趣的持续升温,Clojure也越来越多地受到Java平台上的软件开发人员的青睐。该语言也一度成为知名软件开发老将的首选或推荐语言,如Brian Goetz[83][84][85]、Eric Evans[86][87]、詹姆斯·高斯林[88]、保罗·格雷厄姆[89]及Robert C. Martin(俗称“鲍勃大叔”)[90][91][92][93]等人。
在由Snyk和Java Magazine合作编写的“JVM生态系统报告2018”(据称是“Java开发人员有史以来规模最大的调查”)中,Clojure被评为用于“主要应用程式”的第二大人气编程语言(仅次于Java)。[94]
业内使用Clojure的公司有苹果公司[95][96]、Atlassian[97]、Funding Circle[98]、Netflix[99]、 Puppet [100]、沃尔玛[101]及其他大型软件公司[102]与美国太空总署[103]等政府机构。Clojure也一度被用于创意计算,包括视觉艺术、音乐、游戏和诗歌。[104]
美国知名软件咨询公司ThoughtWorks在为其“技术雷达”[105]评估函数式编程语言时表达了他们对Clojure的青睐,称其为“Lisp在JVM上的简单及优雅实现”,并在2012年将其状态提升为“采用”(“ADOPT”)[106]。
越来越多的非官方和/或实验性的其他平台实现也验证了该语言的人气:
- CljPerl[107]:Clojure的Perl实现
- Clojerl[108]:BEAM(Erlang虚拟机)上的Clojure
- clojure-py[109]:Clojure的纯Python实现
- Ferret[110]:可被编译成运行于微控制器的自包含(self-contained)C++11
- Joker[111]:用Go实现的解释器和linter
- Las3r[112]:执行于ActionScript虚拟机(Adobe Flash Player平台)的Clojure子集
- Pixie[113]:受Clojure启发并用RPython实现的Lisp方言
- Rouge[114]:Clojure基于YARV的Ruby实现
开发工具
Clojure的开发工具在近几年得到了显著的改善。以下是目前最具人气的集成开发环境/编辑器及其Clojure插件。[115]这些工具的结合为Clojure编程提供了出色的支持。
除了社区提供的开发工具之外,官方的命令行界面工具[127]也随着Clojure 1.9一起发布并可在GNU/Linux、macOS及Windows上使用。[128]
功能示例
以下示例均可在Clojure REPL中运行(如,使用Clojure命令行界面工具[127]启动的REPL或在REPL.it[129]上提供的在线REPL。)
由于强调简单性,典型的Clojure程序主要包括函数和简单的数据结构(即列表,向量,映射和集合):
;; 一个典型的Clojure程序的入口
(defn -main ; 函数名
[& args] ; 参数向量 (`&`表示可变参数)
(println "Hello, World!")) ; 函数体
与其他Lisp一样,Clojure的标志性特征之一是基于REPL的交互式编程。[130]在以下示例中,;;
表示一行注释的开始,而;; =>
则表示输出:
;; 定义一个var
(def a 42)
;; => #'user/a
;; 调用名为`+`的函数
(+ a 8)
;; => 50
;; 调用名为`even?`的函数
(even? a)
;; => true
;; 定义一个函数以返回n除10之余
(defn foo [n] (rem n 10))
;; => #'user/foo
;; 调用该函数
(foo a)
;; => 2
;; 打印`rem`的文档字符串(docstring)
(doc rem)
;; =>
-------------------------
clojure.core/rem
([num div])
remainder of dividing numerator by denominator.
;; 打印`rem`的源代码
(source rem)
;; =>
(defn rem
"remainder of dividing numerator by denominator."
{:added "1.0"
:static true
:inline (fn [x y] `(. clojure.lang.Numbers (remainder ~x ~y)))}
[num div]
(. clojure.lang.Numbers (remainder num div)))
与Clojure不同,其他语言的编译器会将程序中的名字编译掉使得它们在运行时不可用。而在Clojure中,可以用普通的数据结构对其运行时进行观察:
;; 定义一个var
(def a 42)
;; => #'user/a
;; 以映射(map)的形式获取在`user`名字空间中捕获的(interned)所有var
(ns-publics 'user)
;; => {a #'user/a}
;; 用`#'`(读取器宏)及其关联的、名字空间限定的符号`user/a`引用该var
#'user/a
;; => #'user/a
;; 解引用该var(获取其值)
(deref #'user/a)
;; => 42
;; 定义(并附加文档字符串)一个函数以返回n除10之余
(defn foo "返回`(rem n 10)`" [n] (rem n 10))
;; => #'user/foo
;; 获取var `#'user/foo`的元数据
(meta #'user/foo)
;; =>
{:arglists ([n]),
:doc "返回`(rem n 10)`",
:line 1,
:column 1,
:file "user.clj",
:name foo,
:ns #namespace[user]}
与其他Lisp类似,Clojure也具有同像性(又称代码即数据)。从下面的示例中可以看到,用Clojure编写代码从而修改代码本身是非常容易的:
;; 调用一个函数 (代码)
(+ 1 1)
;; => 2
;; 引用该函数调用
;;(将代码转换成数据,此处为含一组符号的列表)
(quote (+ 1 1))
;; => (+ 1 1)
;; 获取该列表上的首个元素
;; (视代码为数据并对其进行操作)
(first (quote (+ 1 1)))
;; => +
;; 获取该列表上的最后一个元素
;; (视代码为数据并对其进行操作)
(last (quote (+ 1 1)))
;; => 1
;; 替换原列表上的符号从而获取一个新列表
;; (视代码为数据并对其进行操作)
(map (fn [form]
(case form
1 'one
+ 'plus))
(quote (+ 1 1)))
;; => (plus one one)
穿梭宏(threading macro,如,->
、->>
等)可以在语法上表达一个数据集在一系列变换间穿梭的抽象:
(->> (range 10)
(map inc)
(filter even?))
;; => (2 4 6 8 10)
利用变换归纳器也可以更有效地实现该过程:
(sequence (comp (map inc)
(filter even?))
(range 10))
;; => (2 4 6 8 10)
线程安全的唯一序列号生成器(然而,和许多其他Lisp方言一样,Clojure内部使用其内建的gensym
函数):
(def i (atom 0))
(defn generate-unique-id
"每次调用会返回一个唯一数字ID。"
[]
(swap! i inc))
java.io.Writer
的一个匿名子类(不写任何内容)和一个宏(使用该类来静音其中打印的所有内容):
(def bit-bucket-writer
(proxy [java.io.Writer] []
(write [buf] nil)
(close [] nil)
(flush [] nil)))
(defmacro noprint
"在对给定的`forms`求值的同时静音所有向`*out*`打印的内容。"
[& forms]
`(binding [*out* bit-bucket-writer]
~@forms))
(noprint
(println "Hello, nobody!"))
;; => nil
作为其主要设计目标之一,Clojure从一开始就将其宿主平台视为其不可分割的一部分。Clojure与Java之间出色的互操作即得益于此:
;; 调用一个实例方法
(.toUpperCase "apple")
;; => "APPLE"
;; 调用一个静态方法
(System/getProperty "java.vm.version")
;; => "12+33"
;; 创建`java.util.HashMap`的一个实例
;; 并加入一些键值对(key-value pairs)
(doto (java.util.HashMap.)
(.put "apple" 1)
(.put "banana" 2))
;; => {"banana" 2, "apple" 1}
;; 创建`java.util.ArrayList`的一个实例
;; 并用`clojure.core/map`递增(increment)其元素
(def al (doto (java.util.ArrayList.)
(.add 1)
(.add 2)
(.add 3)))
(map inc al)
;; => (2 3 4)
;; 利用Java Swing显示一个消息对话框
(javax.swing.JOptionPane/showMessageDialog
nil
"Hello, World!")
;; => nil
10个线程操纵一个共享数据结构,该结构由100个向量组成,而每个向量包含10个(最初是连续的)唯一数字。每个线程随后在两个随机向量中重复选择两个随机位置并交换它们。通过使用Clojure的软件事务内存系统,对向量的所有更改都发生在事务中:
(defn run
[nvecs nitems nthreads niters]
(let [vec-refs
(->> (* nvecs nitems)
(range)
(into [] (comp (partition-all nitems)
(map vec)
(map ref))))
swap
#(let [v1 (rand-int nvecs)
v2 (rand-int nvecs)
i1 (rand-int nitems)
i2 (rand-int nitems)]
(dosync
(let [tmp (nth @(vec-refs v1) i1)]
(alter (vec-refs v1) assoc i1 (nth @(vec-refs v2) i2))
(alter (vec-refs v2) assoc i2 tmp))))
report
#(->> vec-refs
(into [] (comp (map deref)
(map (fn [v] (prn v) v))
cat
(distinct)))
(count)
(println "Distinct:"))]
(report)
(->> #(dotimes [_ niters] (swap))
(repeat nthreads)
(apply pcalls)
(dorun))
(report)))
(run 100 10 10 100000)
;; =>
[0 1 2 3 4 5 6 7 8 9]
[10 11 12 13 14 15 16 17 18 19]
...
[990 991 992 993 994 995 996 997 998 999]
Distinct: 1000
[382 318 466 963 619 22 21 273 45 596]
[808 639 804 471 394 904 952 75 289 778]
...
[484 216 622 139 651 592 379 228 242 355]
Distinct: 1000
nil
参考文献
延伸阅读
外部链接
Wikiwand in your browser!
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.