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
安装完成后,你可以直接使用 clojure
或 clj
命令启动 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
安装完成后,同样可以使用 clojure
或 clj
启动 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 提供了 map
、filter
、reduce
等函数式编程工具:
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 中除了 false
和 nil
外,其他值都被视为真:
user=> (if 0 "True" "False")
"True"
user=> (if [] "True" "False")
"True"
user=> (if nil "True" "False")
"False"
6.2. 循环
函数式集合操作已能满足大多数循环需求。其他循环使用递归实现,或使用 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]
7. 总结
本文介绍了 Clojure 的基本语法和核心概念,包括 REPL 使用、基本类型、函数定义、集合操作和控制结构。Clojure 是一门强大的函数式语言,值得进一步探索和实践。
✅ 推荐你亲自上手试试,体验 Clojure 的独特魅力!