Learn Clojure by Examples

Learn Clojure by Examples系列文章可以帮助读者通过实际的例子逐步掌握Clojure这门语言。从入门到高级,涵盖动态类型开发、宏编程、Java互操作、STM并发和clojure.spec数据验证。

本文是面向有编程基础读者的 Clojure 系统性教程。假设你已经熟悉至少一门主流编程语言(推荐先阅读 Java快速入门教程 或 Python 极简入门教程 作为前置知识),我们将通过大量可运行的 REPL 示例带你深入函数式编程的 Lisp 世界。

“Learn Clojure by Examples"系列文章可以帮助读者通过实际的例子逐步掌握Clojure这门语言。如果你正在同时对比 Rust基础知识入门 、Golang快速入门 或 Erlang语言基础入门:并发、分布式与OTP框架详解 ,本文中的函数式编程思想也可以帮助你理解这些语言中相似的设计决策。


1. 入门篇

1.1 Clojure 简介

Clojure 是由 Rich Hickey 于 2007 年创建的函数式编程语言。它是 Lisp 家族的一员,运行在 Java 虚拟机(JVM)之上,具备动态类型、不可变数据结构和强大的并发抽象等核心特性。与其它 Rust基础知识入门 这类强调静态编译时安全的系统语言不同,Clojure 选择将开发效率、表达力和交互式探索作为其设计核心。

REPL(Read-Eval-Print Loop)驱动开发是 Clojure 最独特的体验方式。你不需要编写整个程序、编译再运行,而是可以逐行与程序对话,实时观察数据转换的效果。这种开发模型对数据探索、算法验证和原型设计尤为高效。

1.2 安装和设置开发环境

安装 Clojure 需要 Java 运行时。建议安装 JDK 17 或更高版本。安装 Clojure CLI Tools 后,你可以在终端启动一个交互式会话:

;; 启动 REPL
;; clj

;; 在 REPL 中输入表达式,即时获得结果
user=> (+ 1 2 3)
6

;; 定义变量和函数
user=> (def greeting "Hello, Clojure!")
#'user/greeting
user=> greeting
"Hello, Clojure!"

推荐使用 IntelliJ IDEA 配合 Cursive 插件,或 VS Code 配合 Calva 插件作为编辑器。两者都提供了括号配对、代码导航和与外部 REPL 连接的功能。

1.3 编写第一个 Clojure 程序

创建一个 deps.edn 文件,写入项目依赖信息。随后我们可以编写第一个正式的命名空间程序:

(ns my-app.core)

(defn say-hello [name]
  (str "你好, " name "!"))

(defn -main []
  (println (say-hello "Clojure 学习者")))

运行方式:clj -M -m my-app.core。这里的 -main 是程序的入口点,遵循 JVM 的 main 方法惯例。这段代码展示了函数定义(defn)、局部变量绑定(name 作为参数)以及字符串拼接的核心用法。

1.4 Clojure 的语法基础

Clojure 采用前缀表示法(Polish notation),所有操作都以括号包围的列表形式书写,第一个元素是运算符或函数名,其余是参数:

;; 函数调用
(+ 1 2 3)                           ; => 6
(* 2 (+ 3 4))                       ; => 14,嵌套调用

;; 特殊形式
def                                ; 全局绑定
let                                ; 局部绑定
if                                 ; 条件分支
fn                                 ; 匿名函数

前缀表示法是 Lisp 家族的标志。初看之下可能不习惯,但它消除了运算符优先级歧义,使代码结构更加直观。一个 (+ 1 2 3) 中的操作数和操作符天然并列,语法结构天然对应抽象语法树(AST)。

1.5 变量和命名规则

Clojure 使用 def 创建全局 Var(变量单元),使用 let 进行词法作用域绑定。

;; 全局绑定(创建一个动态 Var)
(def max-retries 5)

;; 局部绑定:let 表达式内部可见
(let [x 10
      y 20]
  (+ x y))                          ; => 30

;; Clojure 命名规范
;; 函数名使用 kebab-case: calculate-total
;; 谓词函数以 ? 结尾: empty?
;; 带有副作用的函数以 ! 结尾: swap!, reset!
;; 常量全大写: MAX-VALUE
;; 宏以 earmuffs 包裹表示动态绑定: out

1.6 基本数据类型

Clojure 在 JVM 上提供了丰富的数据类型抽象,所有核心数据结构都是不可变的:

;; 数值与精确计算
(/ 10 3)                            ; => 10/3(精确的有理数)
(double (/ 10 3))                   ; => 3.3333333333333335

;; 关键字(Keyword)是自身求值的标识符,效率远高于字符串作为键
(def person {:name "张三" :age 30})
(:name person)                      ; => "张三"

;; 符号(Symbol)是引用实体名称的对象
;; 在求值上下文中,符号会被解析为 Var 的值
;; 使用 quote 可以阻止求值
(quote +)                           ; => +(符号本身)
'foo                               ; => foo(quote 的语法糖)

;; 布尔与空值
;; nil 表示无值(对应 Java 的 null)
;; 只有 nil 和 false 在逻辑上为假
(if nil :yes :no)                   ; => :no
(if 0 :yes :no)                     ; => :yes(0 是真值!)
(if "" :yes :no)                    ; => :yes(空字符串也是真值)

2. 基础篇

2.1 列表和向量

列表(List)在 Clojure 中有双重身份:它是代码表达式的外在表示,也可以作为数据结构使用。向量(Vector)则是最常用的顺序集合索引方式,支持 O(1) 的随机访问。

;; 列表 — 需要 quote 来阻止求值
'(1 2 3)                            ; 纯数据列表
(list 1 2 3)                        ; => (1 2 3)
(conj '(1 2) 0)                     ; => (0 1 2),从头部添加

;; 向量 — 默认字面量,方括号表示 [1 2 3]
[1 2 3]
(def nums [10 20 30])
(nums 1)                            ; => 20,O(1) 索引访问
(conj nums 40)                      ; => [10 20 30 40],从尾部添加

;; 向量常见于:参数列表、let 绑定、多值返回
(defn divide [a b]
  [a b (/ a b)])
(divide 10 2)                       ; => [10 2 5]

区别关键:列表在首元素是函数或宏时会被求值,向量永远不会自动求值,它是纯粹的容器。因此绝大部分情况下作为数据容器应使用向量。

2.2 映射和集合

映射(Hash Map)是键值对的集合,是 Clojure 中使用最广泛的数据结构之一。

;; 字面量映射
(def user {:name "李四"
           :email "li@example.com"
           :roles #{:admin :editor}})

;; 访问
(:name user)                        ; => "李四"
(get user :email)                   ; => "li@example.com"
(get user :age 25)                  ; => 25,提供默认值

;; 更新(返回新映射,原映射不受影响)
(assoc user :age 30)                ; 添加/更新键值
(dissoc user :email)                ; 移除键
(update user :roles conj :viewer)   ; 使用函数更新值

;; 嵌套数据结构操作
(def nested {:server {:host "0.0.0.0"
                      :port 8080
                      :ssl {:enabled true
                            :cert-path "/etc/ssl/cert.pem"}}})
(get-in nested [:server :ssl :enabled])  ; => true
(assoc-in nested [:server :ssl :key-path] "/etc/ssl/key.pem")
(update-in nested [:server :port] + 80)  ; 端口 8080 -> 8160

;; 集合(Set)—— 无序、无重复元素
(def tags #{"clojure" "functional" "jvm"})
(conj tags "lisp")                  ; => #{"clojure" "functional" "jvm" "lisp"}
(contains? tags "functional")       ; => true

映射和集合在 Clojure 的设计哲学中扮演着核心角色——它们使配置信息、API 响应和领域实体天然易于序列化和反序列化。

2.3 函数定义和调用

函数是 Clojure 的一等公民。你可以定义具名函数、创建匿名函数,并将函数作为参数传递给其它函数。

;; 基本函数定义
(defn square [x]
  (* x x))

;; 多参数列表与重载
(defn greet
  ([] (greet "World"))
  ([name] (str "你好, " name "!"))
  ([name greeting]
   (str greeting ", " name "!")))
(greet)                             ; => "你好, World!"
(greet "Alice")                     ; => "你好, Alice!"
(greet "Alice" "Bonjour")           ; => "Bonjour, Alice!"

;; 不定参数(变长参数)
(defn average [& numbers]
  (/ (apply + numbers) (count numbers)))
(average 10 20 30)                  ; => 20

;; 匿名函数(两种写法)
(map (fn [x] (* x x)) [1 2 3])      ; (1 4 9)
(map #(* % %) [1 2 3])              ; 简写形式

2.4 高阶函数和函数式编程

Clojure 提供了大量内置高阶函数,彻底改变了你处理集合数据的方式。

;; map: 对每个元素应用函数,返回惰性序列
(map inc [1 2 3])                   ; => (2 3 4)
(map #(* %2 %2) [1 2 3] [4 5 6])    ; => (25 64)

;; filter: 筛选满足条件的元素
(filter even? (range 10))           ; => (0 2 4 6 8)

;; reduce: 聚合序列
(reduce + [1 2 3 4])                ; => 10
(reduce (fn [acc x] (+ acc x)) 0 [1 2 3 4])

;; 函数组合
(def process
  (comp
    (partial filter even?)
    (partial map #(* % 3))
    (partial take 100)))
(process (range))                   ; 取100个数,乘3,再筛选偶数

;; 线程宏:让函数调用从左到右阅读
;; -> 将值插入第一个参数位置
(-> (range 10)
    (filter even?)
    (map #(Math/pow % 2))
    (reduce +))

;; ->> 将值插入最后一个参数位置(数据管道风格更常见)
(->> (range 10)
     (map #(str "item-" %))
     (clojure.string/join ","))

这些高阶函数消除了显式的循环与索引维护,代码直接表达做什么而非怎么做。

2.5 条件语句和循环

;; cond — 多路分支(推荐用于多于两个分支的情况)
(defn grade [score]
  (cond
    (>= score 90) "A"
    (>= score 80) "B"
    (>= score 60) "C"
    :else "D"))

;; case — 基于常量值的分支
(defn http-status [code]
  (case code
    200 "OK"
    404 "Not Found"
    500 "Internal Server Error"
    "Unknown"))

;; doseq — 对集合做副作用遍历
(doseq [i (range 5)]
  (println (str "当前索引: " i)))

;; loop/recur — 尾递归循环(唯一真正循环结构)
(defn factorial [n]
  (loop [i n
         acc 1]
    (if (<= i 1)
      acc
      (recur (dec i) (* acc i)))))
(factorial 5)                       ; => 120

;; 注意:recur 必须是尾部调用,否则编译器报错

2.6 错误处理

Clojure 基于 Java 的异常机制,但提供了更符合函数式风格的处理 API。

;; 使用 ex-info 创建结构化异常
(defn fetch-data [url]
  (if (nil? url)
    (throw (ex-info "URL 不能为空"
                    {:url url
                     :timestamp (java.time.Instant/now)}))
    (str "获取到来自 " url " 的数据")))

;; try/catch/finally
(try
  (fetch-data nil)
  (catch clojure.lang.ExceptionInfo e
    (let [data (ex-data e)]
      (println (str "错误详情: " data))))
  (finally
    (println "清理资源...")))

;; 函数式错误处理
(defn safe-div [a b]
  (if (zero? b)
    {:success false :error "不能除以零"}
    {:success true :result (/ a b)}))

3. 进阶篇

3.1 宏:编写可扩展的代码

宏是 Clojure 最强大的元编程工具。它允许你编写在编译期运行的代码,通过操作代码本身来生成代码。

;; defn 本身是一个宏,它在内部使用 def + fn
;; 下面展示如何定义一个简单的断言宏
(defmacro assert-positive [x]
  `(let [val# ~x]
     (if (pos? val#)
       val#
       (throw (ex-info "值必须为正数"
                       {:value val#
                        :expr '~x})))))

;; 使用
(assert-positive 42)                ; => 42

;; 防止多次求值(使用 gensym 生成的 val#)是宏安全性的核心
;; quote(`)、unquote(~)和 unquote-splicing(~@)是三个核心语法糖

;; 循环控制宏
(defmacro do-times [n & body]
  `(dotimes [i# ~n]
     ~@body))
(do-times 3
  (println "循环执行中..."))

3.2 多态和协议

协议(Protocol)类似于 Java 的接口,它定义了一组方法和对应的名称,让不同类型的数据可以共享行为。

;; 定义协议
(defprotocol Shape
  (area [this])
  (perimeter [this]))

;; 为不同类型实现协议
(defrecord Rectangle [width height]
  Shape
  (area [this] (* width height))
  (perimeter [this] (* 2 (+ width height))))

(defrecord Circle [radius]
  Shape
  (area [this] (* Math/PI radius radius))
  (perimeter [this] (* 2 Math/PI radius)))

;; 使用
(def rect (Rectangle. 3 4))
(area rect)                         ; => 12
(perimeter rect)                    ; => 14

;; 为已存在的类型扩展协议
(extend-type java.lang.String
  Shape
  (area [s] (count s))
  (perimeter [s] (* 2 (count s))))

(area "hello")                      ; => 5(将字符串长度作为面积)

协议使得 Clojure 可以优雅地解决表达式问题:在不修改已有类型源码的情况下,为它们添加新行为。

3.3 并发和并行编程

;; pmap — 并行 map
(time (pmap #(* % %) (range 10000)))

;; future — 在后台线程执行
(let [result (future (do (Thread/sleep 1000)
                         (+ 1 2 3)))]
  (println "计算进行中...")
  (println "结果:" @result))        ; 阻塞等待结果

;; promise — 线程间传递值
(def p (promise))
(future
  (Thread/sleep 500)
  (deliver p "异步数据已就绪"))

3.4 元编程和代码即数据

Clojure 最大的设计特征之一是 同像性(Homoiconicity):代码与数据使用完全相同的语法规则表示。这意味着你可以使用标准的数据操作函数来操作程序代码本身。

;; 一段代码和内层表达式的直接对应关系
'(defn add [a b] (+ a b))
;; => 返回一个包含四个元素的列表:
;; (defn add [a b] (+ a b))

;; 可以用标准函数处理代码结构
(first '(+ 1 2 3))                  ; => +
(second '(+ 1 2 3))                 ; => 1
(rest '(+ 1 2 3))                   ; => (1 2 3)

;; 用代码生成代码:一个简化实现
(defn make-adder [n]
  (list 'fn '[x] (list '+ 'x n)))
(make-adder 5)                      ; => (fn [x] (+ x 5))
(eval (make-adder 5))               ; => 一个函数对象
((eval (make-adder 5)) 10)          ; => 15

这种同像性使得宏的存在成为可能:宏本质上就是在编译期运行的函数,它接收代码(作为数据结构)并生成新的代码(同样作为数据结构)。

3.5 状态管理:Atoms、Refs 和 Agents

Clojure 提供了三种核心状态容器,各有其线程安全和语义保证:

;; Atom — 独立状态的同步更新(最常用)
(def counter (atom 0))
(swap! counter inc)                 ; atomically: (+ counter 1)
(println @counter)                  ; => 1

;; Ref — 多引用协调更新(STM 事务)
(def account-a (ref 1000))
(def account-b (ref 500))

(dosync
  (alter account-a - 100)
  (alter account-b + 100))
;; dosync 内所有操作要么全部成功,要么全部回滚

;; Agent — 异步更新(消息队列风格)
(def message-count (agent 0))
(send message-count inc)            ; 异步执行
;; 等待所有消息处理完成
(await message-count)
(println @message-count)

4. 实践篇

4.1 构建 RESTful API

使用 Ring 和 Compojure 构建 Web 服务是 Clojure 社区的标准做法。

(require '[ring.adapter.jetty :as jetty]
         '[compojure.core :refer :all]
         '[compojure.route :as route])

(defroutes app
  (GET "/api/users/:id" [id]
    {:status 200
     :headers {"Content-Type" "application/json"}
     :body (str "{\"id\": " id "}")})
  (POST "/api/users" req
    {:status 201
     :body "用户已创建"})
  (route/not-found "接口未找到"))

;; 启动服务器
;; (jetty/run-jetty app {:port 3000})

4.2 与数据库交互

(require '[next.jdbc :as jdbc])

(def ds (jdbc/get-datasource {:dbtype "postgresql"
                              :dbname "mydb"
                              :user "app"
                              :password "secret"}))

;; 查询
(jdbc/execute! ds
               ["SELECT id, name FROM users WHERE active = ?" true])

;; 插入并返回生成键
(jdbc/execute! ds
               ["INSERT INTO users (name, email) VALUES (?, ?)"
                "王五" "wang@example.com"]
               {:return-keys true})

4.3 构建 Web 应用

Reagent 是一个基于 React 的 ClojureScript 前端库,配合 Re-frame 可以构建复杂的状态驱动应用。

;; ClojureScript + Reagent 示例
(ns my-app.views
  (:require [reagent.core :as r]))

(defn greeting-component []
  (let [name (r/atom "Clojure")]
    (fn []
      [:div
       [:h1 "欢迎学习 " @name]
       [:input {:type "text"
                :value @name
                :on-change #(reset! name (-> % .-target .-value))}]])))

4.4 单元测试和测试驱动开发

(require '[clojure.test :refer :all])

(defn fib [n]
  (if (<= n 1)
    n
    (+ (fib (- n 1)) (fib (- n 2)))))

(deftest test-fibonacci
  (is (= 0 (fib 0)))
  (is (= 1 (fib 1)))
  (is (= 21 (fib 8)))
  (is (thrown? ArithmeticException (fib -1))))

(run-tests)

4.5 性能优化技巧

;; 1. 使用 transducer 替代级联高阶函数
;; 传统做法创建多个中间序列
(->> (range 1000000)
     (map inc)
     (filter odd?)
     (take 10))

;; Transducer 版本避免中间分配
(sequence
  (comp
    (map inc)
    (filter odd?)
    (take 10))
  (range 1000000))

;; 2. 类型提示避免反射
(defn fast-add [^long a ^long b]
  (+ a b))

;; 3. 循环使用 recur 而非递归调用

5. 高级篇

5.1 ClojureScript:Clojure 的 JavaScript 版本

ClojureScript 将 Clojure 编译为 JavaScript,让你可以在浏览器和 Node.js 中使用同样的开发体验。它共享宿主语言核心库,但编译目标从 JVM 字节码变为 Google Closure Compiler 优化的 JavaScript。

;; ClojureScript 示例代码
(ns my-app.core
  (:require [goog.dom :as gdom]))

(defn init []
  (let [el (gdom/getElement "app")]
    (set! (.-innerHTML el) "Hello, ClojureScript!")))

;; 启动
(init)

使用 ClojureScript 的优势包括:强类型编译检查、不可变数据带来的可预测 UI 状态、以及通过 Figwheel 或 Shadow-CLJS 实现的即时 Reload 开发体验。

5.2 异步编程和 Reactive Programming

;; 使用 core.async 提供 Go 语言风格的通道(Channel)
(require '[clojure.core.async
           :refer [go chan <! >! <!! timeout]])

(def c (chan 10))                   ; 缓冲通道

(go
  (>! c "异步消息")                  ; >! 非阻塞写入
  (println "消息已发送"))

(go
  (let [msg (<! c)]                 ; <! 非阻塞读取
    (println "收到:" msg)))

;; select 风格的多路复用
(require '[clojure.core.async :refer [alts!]])

(go
  (let [[value ch] (alts! [c (timeout 5000)])]
    (if value
      (println "获得值:" value)
      (println "超时!"))))

5.3 深入理解 Clojure 的内存模型

Clojure 的数据结构基于 持久化数据结构(Persistent Data Structures) 实现,采用结构共享(Structural Sharing)而非深度拷贝。这意味着更新操作只修改路径上的节点,与旧版本共享未修改的子树。

;; 示例:两个版本共享大部分内部节点
(def original {:a {:b 1 :c 2} :d 3})
(def updated (assoc-in original [:a :b] 999))

;; original 和 updated 共享了 [:d] 和 [:a :c] 路径
;; 时间复杂度为 O(log32 n),实际应用中接近 O(1)

5.4 构建大型应用的架构模式

推荐在大型 Clojure 应用中使用的架构模式:

  1. 分层架构:将应用分为路由层(handler)、业务层(service)和数据访问层(repository)。
  2. 组件系统(Component / Integrant / Mount):管理有生命周期依赖的对象图,确保数据库连接、消息队列等外部资源正确初始化和关闭。
  3. 配置选择器:使用 aero 或 cprop 库从环境变量、系统属性和配置文件加载分层配置。
  4. 领域模型设计:优先使用纯函数和不可变映射表示领域实体,状态变更通过返回新值实现。

5.5 与 Java 和其他语言的互操作性

;; 调用 Java 类
(java.util.Date.)                   ; 创建实例
(.toString (java.util.Date.))       ; 调用方法
(System/currentTimeMillis)          ; 静态方法

;; 实现 Java 接口(用于 GUI、回调等场景)
(def button-listener
  (reify java.awt.event.ActionListener
    (actionPerformed [_ event]
      (println "按钮被点击!"))))

;; 导入并使用
(import '[java.util.concurrent Executors])
(def pool (Executors/newFixedThreadPool 4))

;; 从 Java 调用 Clojure:使用 clojure.java.api
;; Clojure.var("clojure.core", "+").invoke(1, 2, 3);

6. 项目篇

6.1 项目规划和设计

使用 deps.edn 作为项目工具链核心,deps 替代了传统的 Leiningen 的 project.clj,提供了更直接和透明的依赖管理方式:

;; deps.edn 示例
{:paths ["src" "resources"]
 :deps {org.clojure/clojure {:mvn/version "1.11.1"}
        ring/ring-core {:mvn/version "1.10.0"}
        com.github.seancorfield/next.jdbc {:mvn/version "1.3.874"}}
 :aliases {:dev {:extra-paths ["dev"]
                 :extra-deps {org.clojure/tools.namespace {:mvn/version "1.4.4"}}}
           :test {:extra-paths ["test"]
                  :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1"
                                                                     :git/sha "dfb30dd"}}
                  :main-opts ["-m" "cognitect.test-runner"]}}}

6.2 版本控制和协作开发

Clojure 社区广泛使用 Git 和 GitHub 进行开源协作。建议为每个项目配置 .dir-locals.el 或 bb.edn 提供本地开发工具链。并将 REPL 会话中提炼出的稳定代码通过 Pull Request 流程合并。

6.3 部署和维护 Clojure 应用

标准部署选项:

  1. Uberjar 打包:将整个应用(包含所有依赖)打包为单一 JAR 文件,使用 java -jar 启动。
  2. Native Image:通过 GraalVM 将 Clojure 编译为原生二进制,启动时间从数秒缩短至毫秒级,非常适合命令行工具和 Serverless 函数。
  3. Docker 容器化:基于 eclipse-temurin:17-jre 镜像构建轻量级部署容器。

6.4 案例研究:分析和重构现有代码

一个典型的重构案例是将命令式循环转换为函数式管道:

;; 重构前:命令式风格
(defn process-old [items]
  (let [result (atom [])]
    (doseq [item items]
      (when (satisfies? SomeProtocol item)
        (let [processed (.process item)]
          (when (> (:score processed) 50)
            (swap! result conj processed)))))
    @result))

;; 重构后:函数式管道
(defn process-new [items]
  (let [valid? #(satisfies? SomeProtocol %)]
    (->> items
         (filter valid?)
         (map :process)
         (filter #(> (:score %) 50)))))

6.5 构建自己的开源项目

发起开源项目时,请关注以下社区惯例:

  1. 选择 GitHub 或 GitLab 等公共平台存放代码。
  2. 在根目录添加 LICENSE(EPL-2.0 是 Clojure 社区的首选)。
  3. 提供 README.md 说明安装方式、基本用法和 API 概览。
  4. 使用 CHANGELOG.md 记录各版本的变更。
  5. 通过 Clojars 发布库。使用 deps-deploy 或 lein deploy 进行发布。

7. 附录

A.1 常用库和工具

  • Web:Ring、Compojure、Reitit、HugSQL
  • 数据:next.jdbc、clojure.java.jdbc、DataScript、core.logic
  • 测试:clojure.test、Midje、test.check(基于属性的测试)
  • 工具:tools.logging、tools.namespace、cljfmt、clj-kondo(静态分析 linter)
  • 构建:tools.build(官方)、leiningen、boot

A.2 学习资源和社区

  • 官方文档:clojure.org
  • ClojureDocs:社区维护的示例集合
  • Clojure Verse:问答社区
  • Slack:clojure 频道是获得实时帮助的最佳场所

A.3 常见问题解答

Q:Clojure 的运行速度如何?
A:运行时代码在 JVM 上执行,性能与 Java 处于同一数量级。启动时间因 JVM 特性较长,但对于长期运行的服务端应用和一次性脚本的场景,后者可以通过 GraalVM Native Image 解决。

Q:为什么选择 Clojure 而不是 Scala?
A:Clojure 语法简单、动态类型开发体验更轻快、同像性带来了更强的元编程能力。Scala 拥有更成熟的类型系统和 并发编程模式actor详解 支持。两者面向不同偏好。

Q:Clojure 在业界的使用率?
A:金融科技、数据管道、电商后台和实时分析是 Clojure 的传统优势领域。Nubank、CircleCI 和 Grammarly 等公司均大规模使用 Clojure。

A.4 术语表

  • S-expression:符号表达式,Clojure 和 Lisp 的基础语法单元。
  • REPL:读取-求值-打印循环,Clojure 的核心交互开发工具。
  • Var:Clojure 中的命名全局绑定单元。
  • Persistent Data Structure:不可变和共享结构的数据结构。
  • Transducer:可组合的无上下文数据转换函数。
  • Homoiconicity:代码即数据的特性。

8. 结语

感谢你阅读本文。Clojure 作为一门设计简洁但表达力极强的语言,在现代软件工程中仍然占有独特地位。无论你是从 Python 极简入门教程 转来寻找函数式编程的优雅,还是从 Java快速入门教程 的背景出发探索 JVM 生态中更灵活的表达,Clojure 都值得投入时间学习。

鼓励读者参与 Clojure 社区。加入 Clojure Slack、在 GitHub 上为喜爱的开源库贡献代码、撰写学习笔记——每一步都会让你掌握得更深入。后续建议继续探索 ClojureScript 前端开发、Datomic 数据库以及 Duct 等企业级应用框架。


9. 新增深度章节

9.1 动态类型与 REPL 交互式开发

与静态类型语言要求先定义好类型再编译运行不同,Clojure 的动态类型系统允许你以探索式开发的方式与程序对话。你可以先定义函数骨架,在 REPL 中传入不同参数观察行为,再逐步精化实现。

;; REPL 驱动开发示例:逐行构建分析逻辑
user=> (def raw-data [{:price 100 :qty 2} {:price 50 :qty 4}])
#'user/raw-data

user=> (map :price raw-data)
(100 50)

user=> (map #(-> % :price (* (:qty %))) raw-data)
(200 200)

;; 逐步封装为生产级函数
user=> (defn total-value [items]
         (reduce + (map #(* (:price %) (:qty %)) items)))
#'user/total-value

user=> (total-value raw-data)
400

这种开发方式的效率来自于即时反馈。错误不会累积到编译时一次性爆发,而是被即时暴露和修复。对数据处理管道和复杂业务逻辑尤为高效。

9.2 宏编程实战

宏不仅是语法糖,它们是构建内部 DSL(领域特定语言)的核心。下面展示一个实用的资源清理宏:

(defmacro with-resource
  "安全使用需要关闭的资源"
  [[binding resource] & body]
  `(let [~binding ~resource]
     (try
       ~@body
       (finally
         (when ~binding
           (.close ~binding))))))

;; 使用
(with-resource [reader (clojure.java.io/reader "data.txt")]
  (doseq [line (line-seq reader)]
    (println line)))
          ; 文件读取完毕自动关闭

更复杂的情况下,你可以构建测试框架的断言宏、路由定义宏,甚至是完整的数据描述语言。

9.3 Clojure 与 Java 互操作

Clojure 无缝运行在 JVM 上,这意味着所有 Java 类库和框架都可以直接使用。这是 Clojure 在工业界的核心竞争力之一。

;; 完整互操作示例:使用 Java NIO 进行文件处理
(import '[java.nio.file Files Path Paths])
(import '[java.nio.charset StandardCharsets])

(defn read-lines-fast [filepath]
  (let [^Path path (Paths/get filepath (into-array String []))]
    (Files/readAllLines path StandardCharsets/UTF_8)))

;; 使用 Java Stream API
(.stream (java.util.Arrays/asList (into-array String ["a" "b"])))

;; 创建匿名内部类
(def runnable
  (proxy [java.lang.Runnable] []
    (run []
      (println "在新线程中执行!"))))

互操作的灵活性让 Clojure 可以渐进式引入现有 Java 系统,团队可以在保留历史代码的同时逐步迁移到函数式风格。

9.4 STM 并发编程

Clojure 的软事务内存(Software Transactional Memory)是其并发模型的核心创新。STM 将数据库事务的 ACID 概念引入内存状态管理:

(def inventory-a (ref {:apple 100 :banana 50}))
(def inventory-b (ref {:apple 80 :banana 30}))

(defn transfer-fruit [from to fruit amount]
  (dosync
    ;; 确保库存充足
    (let [from-stock (get @from fruit)]
      (assert (>= from-stock amount)
              (str "库存不足: " fruit)))
    ;; 原子性更新
    (alter from update fruit - amount)
    (alter to update fruit + amount)))

;; 两个线程同时转账不会导致竞态条件
(future (transfer-fruit inventory-a inventory-b :apple 10))
(future (transfer-fruit inventory-a inventory-b :banana 5))

;; STM 保证:要么两个 alter 都成功,要么都回滚
;; 发生冲突时,Clojure STM 会自动重试事务

STM 规避了传统锁模型的死锁和优先级反转问题,将并发正确性的责任从开发者转移到运行时。

9.5 clojure.spec 数据验证

clojure.spec 提供了一套描述数据形状和约束的系统,既可以用于运行时验证,也可以用于生成测试数据、实现合约式编程。

(require '[clojure.spec.alpha :as s])
(require '[clojure.spec.gen.alpha :as gen])

;; 定义规格
(s/def ::name string?)
(s/def ::age (s/and int? #(>= % 0) #(<= % 150)))
(s/def ::email (s/and string? #(re-matches #".+@.+\..+" %)))
(s/def ::user (s/keys :req-un [::name ::age ::email]))

;; 验证
(s/valid? ::user {:name "Alice" :age 30 :email "a@b.com"})
;; => true

(s/valid? ::user {:name "Bob" :age -5 :email "invalid"})
;; => false

;; 解释验证失败原因
(s/explain ::user {:name "Bob" :age -5 :email "invalid"})
;; => 针对每个失败条件输出结构性诊断信息

;; 生成有效测试数据
(gen/sample (s/gen ::user) 3)
;; => ({:name "", :age 0, :email "a@b.com"}, ...)

在微服务边界或函数 API 契约处使用 spec,可以在开发和测试阶段尽早发现数据不一致问题,而不必等到运行时出错。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「clojure」更多文章

  1. Clojure 桌面 UI:cljfx 与 JavaFX 实战
  2. JSON/EDN 序列化与数据格式互操作
  3. JVM 调优与容器化部署:GC、JFR 与 Docker