Contents

Clojure 简介

1. 简介

Clojure 是一种完全在 Java 虚拟机上运行的函数式编程语言,其方式类似于 Scala 和 Kotlin。Clojure 被认为是 Lisp 的衍生产品,任何有其他 Lisp 语言经验的人都会熟悉。

本教程介绍了 Clojure 语言,介绍了如何开始使用它以及它如何工作的一些关键概念。

2. 安装 Clojure

Clojure 可作为安装程序和便利脚本在 Linux 和 macOS 上使用。不幸的是,在这个阶段,Windows 没有这样的安装程序。

但是,Linux 脚本可能在 Cygwin 或 Windows Bash 等系统中工作。还有一个在线服务可以用来测试语言,而旧版本有一个可以使用的独立版本。

2.1. 独立下载

可以从Maven Central 下载独立的 JAR 文件。不幸的是,由于 JAR 文件已被拆分为更小的模块,因此 1.8.0 之后的版本不再以这种方式轻松工作。

下载此 JAR 文件后,我们可以将其用作交互式 REPL,只需将其视为可执行 JAR:

$ java -jar clojure-1.8.0.jar
Clojure 1.8.0
user=>

2.2. REPL 的 Web 界面

Clojure REPL 的 Web 界面可在 https://repl.it/languages/clojure 获得,我们无需下载任何内容即可尝试。目前,这只支持 Clojure 1.8.0,不支持较新的版本。

2.3. MacOS 上的安装程序

如果您使用 macOS 并安装了 Homebrew,则可以轻松安装最新版本的 Clojure:

$ brew install clojure

这将支持最新版本的 Clojure——在撰写本文时为 1.10.0。安装后,我们可以简单地使用 clojure或 clj命令加载 REPL:

$ clj
Clojure 1.10.0
user=>

2.4. Linux 上的安装程序

我们可以使用自安装 shell 脚本在 Linux 上安装工具:

$ curl -O https://download.clojure.org/install/linux-install-1.10.0.411.sh
$ chmod +x linux-install-1.10.0.411.sh
$ sudo ./linux-install-1.10.0.411.sh

与 macOS 安装程序一样,这些将可用于最新版本的 Clojure,并且可以使用 clojure或 clj命令执行。

3. Clojure REPL 简介

以上所有选项都使我们能够访问 Clojure REPL。这是用于 Java 9 及更高版本的 JShell 工具的直接 Clojure 等效项,允许我们输入 Clojure 代码并立即直接查看结果。这是试验和发现某些语言功能如何工作的绝妙方法。

加载 REPL 后,我们将有一个提示符,在该提示符处可以输入任何标准 Clojure 代码并立即执行。这包括简单的 Clojure 构造,以及与其他 Java 库的交互——尽管它们需要在要加载的类路径上可用。

REPL 的提示是我们正在工作的当前命名空间的指示。对于我们的大部分工作,这是 user命名空间,因此提示将是:

user=>

本文其余部分的所有内容都假定我们可以访问 Clojure REPL,并且都可以直接在任何此类工具中工作。

4. 语言基础

Clojure 语言看起来与许多其他基于 JVM 的语言非常不同,而且一开始可能看起来很不寻常。它被认为是 Lisp 的一种方言,并且  与其他 Lisp 语言具有非常相似的语法和功能。

我们用 Clojure 编写的许多代码——就像其他 Lisp 方言一样——以 Lists 的形式表达。然后可以评估列表以产生结果 - 以更多列表或简单值的形式。

例如:

(+ 1 2) ; = 3

这是一个由三个元素组成的列表。“+”符号表示我们正在执行这个调用——加法。然后将其余元素用于此调用。因此,这评估为“1 + 2”。

通过在这里使用 List 语法,这可以很容易地扩展。例如,我们可以这样做:

(+ 1 2 3 4 5) ; = 15

这计算为“1 + 2 + 3 + 4 + 5”。

还要注意分号字符。这在 Clojure 中用于指示注释,而不是我们在 Java 中看到的表达式的结尾。

4.1. 简单类型

Clojure 构建在 JVM 之上,因此我们可以访问与任何其他 Java 应用程序相同的标准类型。类型通常是自动推断的,不需要明确指定。

例如:

123 ; Long
1.23 ; Double
"Hello" ; String
true ; Boolean

我们也可以指定一些更复杂的类型,使用特殊的前缀或后缀:

42N ; clojure.lang.BigInt
3.14159M ; java.math.BigDecimal
1/3 ; clojure.lang.Ratio
#"[A-Za-z]+" ; java.util.regex.Pattern

请注意,使用 clojure.lang.BigInt类型而不是 java.math.BigInteger。这是因为 Clojure 类型有一些小的优化和修复。

4.2. 关键字和符号

Clojure 为我们提供了关键字和符号的概念。关键字仅指它们自己,并且通常用于诸如地图键之类的东西。另一方面,符号是用来指代其他事物的名称。例如,变量定义和函数名都是符号。

我们可以使用以冒号为前缀的名称来构造关键字:

user=> :kw
:kw
user=> :a
:a

关键字与自己直接相等,而不与其他任何东西相等:

user=> (= :a :a)
true
user=> (= :a :b)
false
user=> (= :a "a")
false

Clojure 中大多数其他不是简单值的东西都被认为是符号。这些评估为它们所指的任何内容,而关键字始终评估为自身:

user=> (def a 1)
#'user/a
user=> :a
:a
user=> a
1

4.3. 命名空间

Clojure 语言具有用于组织代码的命名空间的概念。我们编写的每一段代码都存在于一个命名空间中。

默认情况下,REPL 在 user命名空间中运行——如提示“user=>”所示。

我们可以使用 ns关键字创建和更改命名空间:

user=> (ns new.ns)
nil
new.ns=>

一旦我们更改了命名空间,旧命名空间中定义的任何内容都不再可供我们使用,而新命名空间中定义的任何内容现在都可用。

我们可以通过完全限定它们来访问跨命名空间的定义。例如,命名空间 clojure.string定义了一个函数 upper-case

如果我们在 clojure.string命名空间中,我们可以直接访问它。如果不是,那么我们需要将其限定为 clojure.string/upper-case

user=> (clojure.string/upper-case "hello")
"HELLO"
user=> (upper-case "hello") ; This is not visible in the "user" namespace
Syntax error compiling at (REPL:1:1).
Unable to resolve symbol: upper-case in this context
user=> (ns clojure.string)
nil
clojure.string=> (upper-case "hello") ; This is visible because we're now in the "clojure.string" namespace
"HELLO"

我们还可以使用 require 关键字以更简单的方式从另一个命名空间访问定义。我们可以通过两种主要方式使用它——定义一个名称更短的命名空间,以便更容易使用,以及直接从另一个命名空间访问定义而无需任何前缀:

clojure.string=> (require '[clojure.string :as str])
nil
clojure.string=> (str/upper-case "Hello")
"HELLO"
user=> (require '[clojure.string :as str :refer [upper-case]])
nil
user=> (upper-case "Hello")
"HELLO"

这两个都只影响当前的命名空间,因此更改为不同的命名空间需要有新的 require。这有助于保持我们的命名空间更干净,并让我们只访问我们需要的内容。

4.4. 变量

**一旦我们知道如何定义简单的值,我们就可以将它们分配给变量。**我们可以使用关键字 def来做到这一点:

user=> (def a 123)
#'user/a

完成此操作后,我们可以在 要表示此值的任何地方使用符号a

user=> a
123

变量定义可以像我们想要的那样简单或复杂。

例如,要将变量定义为数字的总和,我们可以这样做:

user=> (def b (+ 1 2 3 4 5))
#'user/b
user=> b
15

请注意,我们永远不必声明变量或指明它是什么类型。Clojure 会自动为我们确定所有这些。

如果我们尝试使用尚未定义的变量,则会出现错误:

user=> unknown
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: unknown in this context
user=> (def c (+ 1 unknown))
Syntax error compiling at (REPL:1:8).
Unable to resolve symbol: unknown in this context

请注意,def函数的输出看起来与输入略有不同。定义一个变量 a返回一个字符串 ‘user/a。这是因为结果是一个符号,而这个符号是在当前命名空间中定义的。

4.5. 功能

我们已经看到了几个如何在 Clojure 中调用函数的示例。我们创建一个以要调用的函数开头的列表,然后是所有参数。

当这个列表求值时,我们从函数中获取返回值。例如:

user=> (java.time.Instant/now)
#object[java.time.Instant 0x4b6690c0 "2019-01-15T07:54:01.516Z"]
user=> (java.time.Instant/parse "2019-01-15T07:55:00Z")
#object[java.time.Instant 0x6b8d96d9 "2019-01-15T07:55:00Z"]
user=> (java.time.OffsetDateTime/of 2019 01 15 7 56 0 0 java.time.ZoneOffset/UTC)
#object[java.time.OffsetDateTime 0xf80945f "2019-01-15T07:56Z"]

我们还可以嵌套对函数的调用,因为当我们想将一个函数调用的输出作为参数传递给另一个时:

user=> (java.time.OffsetDateTime/of 2018 01 15 7 57 0 0 (java.time.ZoneOffset/ofHours -5))
#object[java.time.OffsetDateTime 0x1cdc4c27 "2018-01-15T07:57-05:00"]

此外,如果我们愿意,我们还可以定义我们的功能。函数是使用 fn命令创建的:

user=> (fn [a b]
  (println "Adding numbers" a "and" b)
  (+ a b)
)
#object[user$eval165$fn__166 0x5644dc81 "user$eval165$fn__166@5644dc81"]

不幸的是,这并没有给函数一个可以使用的名称。相反,我们可以使用def定义一个表示这个函数的符号, 就像我们在变量中看到的那样:

user=> (def add
  (fn [a b]
    (println "Adding numbers" a "and" b)
    (+ a b)
  )
)
#'user/add

现在我们已经定义了这个函数,我们可以像调用任何其他函数一样调用它:

user=> (add 1 2)
Adding numbers 1 and 2
3

为方便起见,Clojure 还允许我们使用 defn在一次 go 中定义一个具有名称的函数

例如:

user=> (defn sub [a b]
  (println "Subtracting" b "from" a)
  (- a b)
)
#'user/sub
user=> (sub 5 2)
Subtracting 2 from 5
3

4.6. let和局部变量

def调用定义了一个对当前命名空间是全局的符号。这通常不是执行代码时所期望的。相反,Clojure 提供了 let调用来定义 block 的局部变量。这在函数内部使用它们时特别有用,您不希望变量泄漏到函数外部。

例如,我们可以定义我们的子函数:

user=> (defn sub [a b]
  (def result (- a b))
  (println "Result: " result)
  result
)
#'user/sub

但是,使用它会产生以下意想不到的副作用:

user=> (sub 1 2)
Result:  -1
-1
user=> result ; Still visible outside of the function
-1

相反,让我们使用let重写它 :

user=> (defn sub [a b]
  (let [result (- a b)]
    (println "Result: " result)
    result
  )
)
#'user/sub
user=> (sub 1 2)
Result:  -1
-1
user=> result
Syntax error compiling at (REPL:0:0).
Unable to resolve symbol: result in this context

这次 result符号在函数之外是不可见的。或者,实际上,在使用它的 let块之外。

5. 集合

到目前为止,我们主要与简单的值进行交互。我们也看到了列表,但仅此而已。Clojure 确实有一整套可以使用的集合,包括列表、向量、映射和集合

  • 向量是值的有序列表——任何任意值都可以放入向量中,包括其他集合。
  • 集合是值的无序集合,并且不能多次包含相同的值。
  • 映射是一组简单的键/值对。在 map 中使用关键字作为键是很常见的,但我们可以使用任何我们喜欢的值,包括其他集合。
  • 列表与向量非常相似。区别类似于 Java 中的ArrayList和 *LinkedList之间的区别。*通常,向量是首选,但如果我们想将元素添加到开头,或者我们只想按顺序访问元素,则列表会更好。

5.1. 构建集合

可以使用速记符号或使用函数调用来创建其中的每一个:

; Vector
user=> [1 2 3]
[1 2 3]
user=> (vector 1 2 3)
[1 2 3]
; List
user=> '(1 2 3)
(1 2 3)
user=> (list 1 2 3)
(1 2 3)
; Set
user=> #{1 2 3}
#{1 3 2}
user=> (hash-set 1 2 3)
#{1 3 2}
; Map
user=> {:a 1 :b 2}
{:a 1, :b 2}
user=> (hash-map :a 1 :b 2)
{:b 2, :a 1}

请注意,SetMap示例不会以相同的顺序返回值。这是因为这些集合本质上是无序的,我们所看到的取决于它们在内存中的表示方式。

我们还可以看到,创建列表的语法与表达式的标准 Clojure 语法非常相似。Clojure 表达式实际上是一个被评估的列表,而这里的撇号字符表示我们想要实际的值列表而不是评估它。

当然,我们可以像任何其他值一样将集合分配给变量。我们还可以将一个集合用作另一个集合中的键或值。

列表被认为是一个 seq。这意味着该类实现了 ISeq接口。所有其他集合都可以 使用 seq函数转换为seq

user=> (seq [1 2 3])
(1 2 3)
user=> (seq #{1 2 3})
(1 3 2)
user=> (seq {:a 1 2 3})
([:a 1] [2 3])

5.2. 集合访问

一旦我们有了一个集合,我们就可以与它交互以再次获取值。我们如何做到这一点取决于所讨论的集合,因为它们中的每一个都有不同的语义。

向量是唯一可以让我们通过索引获取任意值的集合。这是通过将向量和索引评估为表达式来完成的:

user=> (my-vector 2) ; [1 2 3]
3

我们也可以使用相同的语法对地图做同样的事情

user=> (my-map :b)
2

我们还有访问向量和列表的函数,以获取第一个值、最后一个值和列表的其余部分:

user=> (first my-vector)
1
user=> (last my-list)
3
user=> (next my-vector)
(2 3)

Maps 具有额外的功能来获取键和值的整个列表:

user=> (keys my-map)
(:a :b)
user=> (vals my-map)
(1 2)

我们必须设置的唯一真正访问权限是查看特定元素是否为成员。

这看起来与访问任何其他集合非常相似:

user=> (my-set 1)
1
user=> (my-set 5)
nil

5.3. 识别集合

我们已经看到,我们访问集合的方式取决于我们拥有的集合类型。我们有一组函数可以用来确定这一点,无论是具体的还是更通用的方式。

我们的每个集合都有一个特定的函数来确定给定值是否属于该类型 - *list?*对于列表, *set?对于集合,等等。此外,还有 seq? 用于确定给定值是否是任何类型的seq *和 *associative?*确定给定值是否允许任何类型的关联访问——这意味着向量和映射:

user=> (vector? [1 2 3]) ; A vector is a vector
true
user=> (vector? #{1 2 3}) ; A set is not a vector
false
user=> (list? '(1 2 3)) ; A list is a list
true
user=> (list? [1 2 3]) ; A vector is not a list
false
user=> (map? {:a 1 :b 2}) ; A map is a map
true
user=> (map? #{1 2 3}) ; A set is not a map
false
user=> (seq? '(1 2 3)) ; A list is a seq
true
user=> (seq? [1 2 3]) ; A vector is not a seq
false
user=> (seq? (seq [1 2 3])) ; A vector can be converted into a seq
true
user=> (associative? {:a 1 :b 2}) ; A map is associative
true
user=> (associative? [1 2 3]) ; A vector is associative
true
user=> (associative? '(1 2 3)) ; A list is not associative
false

5.4. 集合操作

在 Clojure 中,与大多数函数式语言一样,所有集合都是不可变的。我们为更改集合所做的任何事情都会导致创建一个全新的集合来表示更改。这可以带来巨大的效率优势,并意味着不存在意外副作用的风险。

然而,我们也必须小心我们理解这一点,否则我们的集合的预期变化将不会发生。

**使用 conj向向量、列表或集合添加新元素。**这在每种情况下的工作方式都不同,但具有相同的基本意图:

user=> (conj [1 2 3] 4) ; Adds to the end
[1 2 3 4]
user=> (conj '(1 2 3) 4) ; Adds to the beginning
(4 1 2 3)
user=> (conj #{1 2 3} 4) ; Unordered
#{1 4 3 2}
user=> (conj #{1 2 3} 3) ; Adding an already present entry does nothing
#{1 3 2}

**我们还可以使用 disj从集合中删除条目。**请注意,这不适用于列表或向量,因为它们是严格排序的:

user=> (disj #{1 2 3} 2) ; Removes the entry
#{1 3}
user=> (disj #{1 2 3} 4) ; Does nothing because the entry wasn't present
#{1 3 2}

使用assoc向map添加新元素。我们还可以使用dissoc 从map中删除条目:

user=> (assoc {:a 1 :b 2} :c 3) ; Adds a new key
{:a 1, :b 2, :c 3}
user=> (assoc {:a 1 :b 2} :b 3) ; Updates an existing key
{:a 1, :b 3}
user=> (dissoc {:a 1 :b 2} :b) ; Removes an existing key
{:a 1}
user=> (dissoc {:a 1 :b 2} :c) ; Does nothing because the key wasn't present
{:a 1, :b 2}

5.5. 函数式编程构造

Clojure 本质上是一种函数式编程语言。这意味着我们可以接触到许多传统的函数式编程概念——例如mapfilterreduce。这些通常与在其他语言中的工作方式相同。但是,确切的语法可能略有不同。

具体来说,这些函数通常将要应用的函数作为第一个参数,并将应用它的集合作为第二个参数:

user=> (map inc [1 2 3]) ; Increment every value in the vector
(2 3 4)
user=> (map inc #{1 2 3}) ; Increment every value in the set
(2 4 3)
user=> (filter odd? [1 2 3 4 5]) ; Only return odd values
(1 3 5)
user=> (remove odd? [1 2 3 4 5]) ; Only return non-odd values
(2 4)
user=> (reduce + [1 2 3 4 5]) ; Add all of the values together, returning the sum
15

6. 控制结构

与所有通用语言一样,Clojure 功能需要标准控制结构,例如条件和循环。

6.1. 条件句

条件由if语句处理。这需要三个参数:一个测试,如果测试为true则执行一个块,如果测试为false则执行一个块。这些中的每一个都可以是一个简单的值或一个将按需评估的标准列表:

user=> (if true 1 2)
1
user=> (if false 1 2)
2

我们的测试可以是我们需要的任何东西——它不需要是true/false值。它也可以是一个被评估的块,为我们提供我们需要的值:

user=> (if (> 1 2) "True" "False")
"False"

所有标准检查,包括 *=、><*都可以在这里使用。还有一组谓词可以用于各种其他原因——我们在查看集合时已经看到了一些,例如:

user=> (if (odd? 1) "1 is odd" "1 is even")
"1 is odd"

测试可以返回任何值——它不仅需要是 true或 false。但是,如果该值是除 false或 nil之外的任何值,则认为它是 true。这与 JavaScript 的工作方式不同,在 JavaScript 中存在大量被认为是“真值”但不是true

user=> (if 0 "True" "False")
"True"
user=> (if [] "True" "False")
"True"
user=> (if nil "True" "False")
"False"

6.2. 循环

我们对集合的功能支持处理了大部分循环工作 ——我们没有在集合上编写循环,而是使用标准函数并让语言为我们进行迭代。

除此之外,循环完全使用 recursion 完成。我们可以编写递归函数,也可以使用 loop和 recur关键字编写递归风格的循环:

user=> (loop [accum [] i 0]
  (if (= i 10)
    accum
    (recur (conj accum i) (inc i))
  ))
[0 1 2 3 4 5 6 7 8 9]

loop 调用启动了一个在每次迭代时执行的 内部块,并通过设置一些初始参数开始。recur调用然后 回调到循环中,提供用于迭代的下一个参数。如果 没有调用recur,则循环结束。

在这种情况下,每当 i值不等于 10 时,我们都会循环,然后一旦它等于 10,我们就会返回数字的累加向量。