1. 简介

Clojure 是一门运行在 Java 虚拟机(JVM)上的函数式编程语言,与 Scala 和 Kotlin 类似。Clojure 是 Lisp 的一种方言,如果你有使用其他 Lisp 语言的经验,那么对 Clojure 的语法和风格会感到比较熟悉。

本教程将带你初步了解 Clojure 语言,介绍如何开始使用它,以及它的一些核心概念。

2. 安装 Clojure

Clojure 提供了适用于 Linux 和 macOS 的安装程序和便捷脚本。不过目前 Windows 还没有官方的安装器。

不过,Linux 的安装脚本可能可以在 Cygwin 或 Windows Bash 上运行。此外,还有一个在线服务可以让你直接尝试 Clojure,而且旧版本还提供了一个独立的 JAR 文件可以直接运行。

2.1. 独立 JAR 包下载

你可以从 Maven Central 下载 Clojure 1.8.0 的独立 JAR 文件。可惜的是,1.8.0 之后的版本不再支持直接运行 JAR,因为它们被拆分成了多个模块。

下载后,你可以像运行可执行 JAR 一样启动 Clojure 的 REPL:

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

2.2. 在线 REPL 工具

你也可以通过 https://repl.it/languages/clojure 在线使用 Clojure REPL,无需下载任何内容。不过目前它只支持 Clojure 1.8.0 版本。

2.3. macOS 安装

如果你使用 macOS 并且安装了 Homebrew,可以通过以下命令安装最新版 Clojure:

$ brew install clojure

安装完成后,你可以直接使用 clojureclj 命令启动 REPL:

$ clj
Clojure 1.10.0
user=>

2.4. Linux 安装

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

安装完成后,同样可以使用 clojureclj 启动 REPL。

3. Clojure REPL 简介

以上所有方式都可以启动 Clojure 的 REPL。它类似于 Java 9 及以上版本的 JShell,允许你直接输入 Clojure 代码并立即看到结果。这是探索语言特性、快速实验的绝佳方式。

启动 REPL 后,你会看到一个提示符,例如:

user=>

这个提示符表示当前所在的命名空间(namespace),默认是 user

本文后续所有示例都假设你已经启动了 REPL,并且可以直接在其中运行。

4. 语言基础

Clojure 的语法与大多数 JVM 语言大相径庭,初看可能觉得非常奇怪。它是一种 Lisp 方言,语法和功能都与其他 Lisp 语言相似。

Clojure 中的代码大多以“列表”(List)的形式表达。列表可以被求值,返回结果——可能是另一个列表,也可能是简单值。

例如:

(+ 1 2) ; = 3

这是一个包含三个元素的列表。+ 表示执行加法操作,后面的元素是参数。因此,这个表达式等价于 1 + 2

由于使用了列表语法,扩展操作非常容易:

(+ 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 使用 clojure.lang.BigInt 而不是 java.math.BigInteger,因为它做了一些优化和修复。

4.2. 关键字与符号

Clojure 提供了 关键字(keyword)和 符号(symbol)两种概念:

  • 关键字只引用自身,常用于 map 的键。
  • 符号是用于引用其他事物的名称,比如变量或函数名。

你可以通过在名称前加冒号来构造关键字:

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

关键字与自身相等,与其他值不等:

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

而符号则会求值为它所引用的内容:

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

4.3. 命名空间

Clojure 使用 命名空间(namespace)来组织代码。每段代码都属于某个命名空间。

默认情况下,REPL 运行在 user 命名空间中,提示符为 user=>

你可以使用 ns 关键字切换或创建命名空间:

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

切换命名空间后,旧命名空间中的定义将不可见,而新命名空间中的定义则可用。

要跨命名空间访问定义,可以使用完整限定名。例如,clojure.string 命名空间中定义了 upper-case 函数:

user=> (clojure.string/upper-case "hello")
"HELLO"
user=> (upper-case "hello") ; 在 user 命名空间中不可见
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") ; 现在可见
"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"

这些操作只影响当前命名空间。

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

4.5. 函数

函数调用使用列表形式,第一个元素是函数名,后面是参数:

user=> (java.time.Instant/now)
#object[java.time.Instant 0x4b6690c0 "2019-01-15T07:54:01.516Z"]

函数可以嵌套调用:

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

或者使用 defn 简化定义:

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 定义的是全局变量,通常我们更希望定义局部变量。使用 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 提供了多种集合类型:

  • Vector(向量):有序列表,支持任意值。
  • Set(集合):无序,元素唯一。
  • Map(映射):键值对,常用关键字作键。
  • List(列表):类似向量,但更适合头部插入。

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}

注意:Set 和 Map 是无序的。

列表使用 ' 表示不求值的列表,而不是表达式:

user=> (seq [1 2 3])
(1 2 3)

5.2. 访问集合

不同集合访问方式略有不同:

user=> (my-vector 2) ; Vector 按索引访问
3
user=> (my-map :b) ; Map 按键访问
2
user=> (first my-vector)
1
user=> (last my-list)
3
user=> (next my-vector)
(2 3)
user=> (keys my-map)
(:a :b)
user=> (vals my-map)
(1 2)
user=> (my-set 1)
1
user=> (my-set 5)
nil

5.3. 集合类型判断

user=> (vector? [1 2 3]) ; true
user=> (list? '(1 2 3)) ; true
user=> (map? {:a 1}) ; true
user=> (set? #{1 2}) ; true
user=> (seq? '(1 2)) ; true
user=> (seq? [1 2]) ; false
user=> (associative? {:a 1}) ; true
user=> (associative? [1 2]) ; true
user=> (associative? '(1 2)) ; false

5.4. 集合操作

Clojure 中集合是不可变的,所有操作都返回新集合:

user=> (conj [1 2 3] 4) ; 向量末尾添加
[1 2 3 4]
user=> (conj '(1 2 3) 4) ; 列表头部添加
(4 1 2 3)
user=> (conj #{1 2 3} 4)
#{1 4 3 2}
user=> (disj #{1 2 3} 2)
#{1 3}
user=> (assoc {:a 1 :b 2} :c 3)
{:a 1, :b 2, :c 3}
user=> (dissoc {:a 1 :b 2} :b)
{:a 1}

5.5. 函数式编程构造

Clojure 提供了 mapfilterreduce 等函数式编程工具:

user=> (map inc [1 2 3])
(2 3 4)
user=> (filter odd? [1 2 3 4 5])
(1 3 5)
user=> (remove odd? [1 2 3 4 5])
(2 4)
user=> (reduce + [1 2 3 4 5])
15

6. 控制结构

6.1. 条件判断

使用 if 实现条件判断:

user=> (if true 1 2)
1
user=> (if false 1 2)
2
user=> (if (> 1 2) "True" "False")
"False"
user=> (if (odd? 1) "1 is odd" "1 is even")
"1 is odd"

Clojure 中除了 falsenil 外,其他值都被视为真:

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

6.2. 循环

函数式集合操作已能满足大多数循环需求。其他循环使用递归实现,或使用 looprecur

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]

7. 总结

本文介绍了 Clojure 的基本语法和核心概念,包括 REPL 使用、基本类型、函数定义、集合操作和控制结构。Clojure 是一门强大的函数式语言,值得进一步探索和实践。

推荐你亲自上手试试,体验 Clojure 的独特魅力!


原始标题:Introduction to Clojure