Clojure

Clojure（ˈ）是Lisp程式語言在Java平台上的現代、動態及函數式方言。與其他Lisp一樣，Clojure視代碼為數據且擁有一套Lisp宏系統。Clojure的開發過程目前由社區驅動，其作者里奇·希基則以終身仁慈獨裁者的身份監督。

Clojure提倡不可變性與持久數據結構並鼓勵程式設計師顯式地管理標識及其狀態。對利用不可變值及顯式時間進展構造（explicit progression-of-time constructs）進行編程的專注旨在促進更加健壯的（尤其是並發）程序的開發。Clojure的類型系統是完全動態的，但人們近期也開始探索其基於漸進類型化的實現。

對Clojure的商業支持由Cognitect公司提供。每年都會在全球範圍內舉辦年度Clojure會議，其中最著名的是Clojure/conj。

## 歷史與開發過程

里奇·希基創造了Clojure語言。此前，他開發過類似但基於.NET平台的項目——dotLisp。在那之前，他還嘗試了三次在Lisp與Java之間提供互操作：Common Lisp的Java外語接口、Lisp的外語對象接口以及Lisp友好的Java Servlet接口。

在公開發布之前，里奇·希基花了大約兩年半的時間開發Clojure。他在沒有外部資金的情況下，將其大部分時間都專門投入到了Clojure的工作上。開發快要完成，里奇·希基向Common Lisp社區裡的一些朋友發布電子郵件，宣布他完成開發了Clojure。

Clojure的開發過程在Clojure JIRA項目網頁由社區驅動並管理。該網站可用於提交問題報告。一般的開發討論是在Clojure谷歌網上論壇上進行的。任何人都可以提交錯誤報告和想法，而貢獻補丁前則需要先簽署Clojure貢獻者協議。JIRA錯誤報告由一組篩選者處理，最終由里奇·希基批准更改。

## 設計理念

里奇·希基開發Clojure的原因是因為他想要一款適合函數式編程的現代Lisp。該語言既需要與已建立的Java平台共生又需要有適合併發性的設計。

在Clojure中，狀態以標識的概念為特徵。一系列狀態隨著時間的推移而產生，就是標識。由於狀態是不可變的值，任意數量的工作單位都可以在其上並行實施操作，並發性就成為一道管理狀態到狀態的變化的問題。因此，Clojure提供了幾個可變的引用類型。每個引用類型都有其明確定義的語義用於控制狀態之間的躍遷。

## 語言概述

版本｜發布日期｜主要功能/改進

2007年10月16日｜首次公開發布

1.0｜2009年5月4日｜首個穩定版

1.1｜2009年12月31日｜將來

1.2｜2010年8月19日｜協議

1.3｜2011年9月23日｜增強對原始類型的支持

1.4｜2012年4月15日｜讀取器字面量

1.5｜2013年3月1日｜歸納器

1.5.1｜2013年3月10日｜修復記憶體洩漏

1.6｜2014年3月25｜Java API、經過改進的哈希演算法

1.7｜2015年6月30日｜變換歸納器、讀取器條件表達式

1.8｜2016年1月19日｜附加的字串函數、直接連接、套接字伺服器

1.9｜2017年12月8日｜集成spec、命令行工具

1.10｜2018年12月17日｜經過改進的錯誤報告、Java兼容性

1.10.1｜2019年6月6日｜解決Java性能回歸問題並改進clojure.main的錯誤報告

1.10.2｜2021年1月26日｜Java互操作性/兼容性改進和其他重要語言修訂

1.10.3｜2021年3月4日｜prepl支持讀者條件

1.11.0｜2022年3月22日｜新的關鍵字參數調用語法，新的`clojure.math`名字空間，名字空間別名不用加載，並向`clojure.core`增加新的幫助函數

c｜2022年4月5日｜在類型`clojure.lang.Keyword`和`clojure.lang.ArraySeq`的對象的二進位序列化中回滾意外的變更。

l

Clojure執行於Java平台之上，因此，與Java緊密集成並完全支持從Clojure調用Java代碼。與此同時，也可以從Java調用Clojure代碼。Leiningen是社區中普遍使用的項目自動化工具。Leiningen為Maven集成提供支持，處理項目包管理和依賴項。Leiningen的配置使用的則是Clojure語法。

與其他大多數Lisp一樣，Clojure的語法建立在S-表達式之上。S-表達式在被編譯之前先由讀取器解析為數據結構。除了列表之外，Clojure的讀取器還支持映射、集合及向量等的字面量語法。這些字面量隨後會被直接編譯成上述數據結構。Clojure是Lisp-1且有一套與其它Lisp不兼容的數據結構，因此，Clojure不支持與Lisp的其它方言之間的代碼級兼容性。

作為一門Lisp方言，函數在Clojure中是一等公民。此外，Clojure還支持讀取﹣求值﹣輸出循環以及一套宏系統。Clojure的Lisp宏系統與Common Lisp的系統極為相似。唯一不同的是，Clojure的重音符（稱為語法引用）用名字空間來限定符號。這有助於防止意外的名字捕獲，因為Clojure禁止綁定到用名字空間限定的名字（namespace-qualified name）上。如果需要強制捕獲宏擴展（capturing macro expansion）那麼就需要顯示地完成該過程。Clojure不支持使用者定義的讀取器宏（reader macro）但Clojure的讀取器支持更具約束力的語法擴展形式。Clojure支持多方法（multimethods。）對於類似接口的抽象，Clojure提供基於協議的多態性以及基於記錄的數據類型系統。Clojure通過這些設計來提供高性能且動態的多態性以避免所謂的「表達式問題」（"expression problem"。）

Clojure支持惰性序列，並鼓勵不可變性與持久數據結構（persistent data structure。）Clojure作為一門函數式程式語言將重點放在遞歸與高階函數上而不是基於副作用的循環流程上。Clojure不支持自動尾調用優化，因為JVM還不支持該項優化，但是，可以用`recur`關鍵字顯式地執行該項優化。對於並行與並發計算，Clojure提供軟體事務內存、響應式代理系統及基於通道的並發編程。

Clojure 1.7引入了讀取器條件表達式從而允許在同一名字空間中嵌入Clojure與ClojureScript代碼。變換歸納器的加入則提供了另一種組合變換的方法。變換歸納器可以使高階函數（如，`map`和`fold`）更加抽象從而使之獨立於其輸入數據源。傳統地說，這些函數一般被應用於序列上，而變換歸納器允許這些函數被應用於通道上並讓使用者定義她們自己的變換歸納模型。

## 平台

Clojure的主要平台是Java但也存在其他目標平台上的實現。其中，最值得關注的是ClojureScript（可被編譯成ECMAScript 3）和ClojureCLR（.NET平台上的完整移植版，可與其生態系統互操作。）2013年對1,060名受訪者進行的Clojure社區調查發現，47％的受訪者在使用Clojure的同時也使用ClojureScript。2014年，這一數字增長到了55％，而到了2015年，則達到了66％（根據2,445名受訪者）。人氣較高的ClojureScript項目包括React實現，如Reagent、re-frame、Rum及Om。

## 人氣

隨著對函數式編程的興趣的持續升溫，Clojure也越來越多地受到Java平台上的軟體開發人員的青睞。該語言也一度成為知名軟體開發老將的首選或推薦語言，如Brian Goetz、Eric Evans、詹姆斯·高斯林、保羅·格雷厄姆及Robert C. Martin（俗稱「鮑勃大叔」）等人。

在由Snyk和Java Magazine合作編寫的「JVM生態系統報告2018」（據稱是「Java開發人員有史以來規模最大的調查」）中，Clojure被評為用於「主要應用程式」的第二大人氣程式語言（僅次於Java）。

業內使用Clojure的公司有蘋果公司、Atlassian、Funding Circle、Netflix、 Puppet、沃爾瑪及其他大型軟體公司與美國國家航空航天局等政府機構。Clojure也一度被用於創意計算，包括視覺藝術、音樂、遊戲和詩歌。

美國知名軟體諮詢公司ThoughtWorks在為其「技術雷達」評估函數式程式語言時表達了他們對Clojure的青睞，稱其為「Lisp在JVM上的簡單及優雅實現」，並在2012年將其狀態提升為「採用」（「ADOPT」）。

越來越多的非官方和/或實驗性的其他平台實現也驗證了該語言的人氣：

- CljPerl：Clojure的Perl實現
- Clojerl：BEAM（Erlang虛擬機）上的Clojure
- clojure-py：Clojure的純Python實現
- Ferret：可被編譯成運行於微控制器的自包含（self-contained）C++11
- Joker：用Go實現的解釋器和linter
- Las3r：執行於ActionScript虛擬機（Adobe Flash Player平台）的Clojure子集
- Pixie：受Clojure啟發並用RPython實現的Lisp方言
- Rouge：Clojure基於YARV的Ruby實現

## 開發工具

Clojure的開發工具在近幾年得到了顯著的改善。以下是目前最具人氣的集成開發環境/編輯器及其Clojure插件。這些工具的結合為Clojure編程提供了出色的支持。

集成開發環境/編輯器｜Clojure插件

Atom｜Chlorine

Emacs｜CIDER

IntelliJ IDEA｜Clojure-Kit或Cursive（提供免費的非商業許可證）

Light Table｜（不適用）

Vim｜fireplace.vim, vim-iced或Conjure（僅限Neovim）

Visual Studio Code｜Calva

除了社區提供的開發工具之外，官方的命令行界面工具也隨著Clojure 1.9一起發布並可在GNU/Linux、macOS及Windows上使用。

## 功能示例

以下示例均可在Clojure REPL中運行（如，使用Clojure命令行界面工具啟動的REPL或在REPL.it上提供的在線REPL。）

### 簡單性

由於強調簡單性，典型的Clojure程序主要包括函數和簡單的數據結構（即列表，向量，映射和集合）：

```clojure
;; 一个典型的Clojure程序的入口
(defn -main ; 函数名
  [& args] ; 参数向量 (`&`表示可变参数)
  (println "Hello, World!")) ; 函数体
```

### REPL編程

與其他Lisp一樣，Clojure的標誌性特徵之一是基於REPL的交互式編程。在以下示例中，`;;`表示一行注釋的開始，而`;; =>`則表示輸出：

```clojure
;; 定义一个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中，可以用普通的數據結構對其運行時進行觀察：

```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編寫代碼從而修改代碼本身是非常容易的：

```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，如，`->`、`->>`等）可以在語法上表達一個數據集在一系列變換間穿梭的抽象：

```clojure
(->> (range 10)
     (map inc)
     (filter even?))
;; => (2 4 6 8 10)
```

利用變換歸納器也可以更有效地實現該過程：

```clojure
(sequence (comp (map inc)
                (filter even?))
          (range 10))
;; => (2 4 6 8 10)
```

### 標識及其狀態的線程安全管理

線程安全的唯一序列號生成器（然而，和許多其他Lisp方言一樣，Clojure內部使用其內置的`gensym`函數）：

```clojure
(def i (atom 0))

(defn generate-unique-id
  "每次调用会返回一个唯一数字ID。"
  []
  (swap! i inc))
```

### 宏

`java.io.Writer`的一個匿名子類（不寫任何內容）和一個宏（使用該類來靜音其中列印的所有內容）:

```clojure
(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
```

### Java互操作

作為其主要設計目標之一，Clojure從一開始就將其宿主平台視為其不可分割的一部分。Clojure與Java之間出色的互操作即得益於此：

```clojure
;; 调用一个实例方法
(.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的軟體事務內存系統，對向量的所有更改都發生在事務中：

```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
```

## 基本資料

- 編程范型：多重編程范型:、代理人導向、並發計算、函數式、邏輯編程、宏、管道編程
- 發行日期：2007年
- 設計者：里奇·希基
- 軟體版本週期：1.11.1
- 軟體版本週期：2022年4月5日
- 類型系統：動態類型、強類型
- 受影響於：C#、C++、Common Lisp、Erlang、Haskell、Java、Mathematica、ML、Prolog、Racket、Ruby、Scheme
- 施影響於：Elixir、Hy、LFE語言、Pixie、Rhine
- 平台：Java、JavaScript、.NET
- 許可證：Eclipse公共許可證
- 文件擴展名：.clj、.cljs、.cljc、.edn
- wikibooks：Clojure Programming