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 提供了幾個可變的引用型別。每個引用型別都有其明確定義的語義用於控制狀態之間的躍遷。

語言概述

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 1.9 一起釋出並可在 GNU/Linux、macOS 及 Windows 上使用。

功能示例

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

7.1 簡單性

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

; 一個典型的 Clojure 程式的入口

(defn -main ; 函式名

& args ; 引數向量 (`&` 表示可變引數)

(println "Hello, World!")) ; 函式體

7.2 REPL 程式設計

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

; 定義一個 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)))

7.3 執行時可用的名字

與 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 #namespaceuser}

7.4 程式碼即資料（同像性）

與其他 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)

7.5 富有表現力的資料變換運算子

穿梭宏（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)

7.6 標識及其狀態的執行緒安全管理

執行緒安全的唯一序列號生成器（然而，和許多其他 Lisp 方言一樣，Clojure 內部使用其內建的 gensym 函式）：

(def i (atom 0))

(defn generate-unique-id

"每次呼叫會返回一個唯一數字 ID。"

(swap! i inc))

7.7 宏

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

7.8 Java 互操作

作為其主要設計目標之一，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

7.9 軟體事務記憶體

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