Clojure 基础教程:从函数式编程核心概念到并发开发与 Web 服务实战

系统掌握 Clojure 函数式编程语言:从 S-表达式与不可变数据结构出发,深入理解宏(Macro)、多方法(Multimethod)、STM 软件事务内存、Agent 并发模型,并通过 Ring/Compojure 构建 Web 服务,附完整代码示例与 REPL 驱动开发最佳实践。

Clojure 是一种运行在 JVM(以及 CLR、JavaScript 运行时)上的现代 Lisp 方言,以其不可变数据结构、纯函数优先和强大的并发抽象而闻名。它是函数式编程思想在工业级环境中的最佳落地实践之一,被广泛应用于金融系统、大数据处理、Web 后端和 DevOps 工具链。


1. 为什么选择 Clojure

特性Clojure 的实现工程价值
不可变数据所有核心数据结构(Persistent Vector/Map/Set)均为不可变消除状态共享隐患,天然线程安全
STM 并发软件事务内存(Software Transaction Memory)无锁并发,替代传统锁/信号量
代码即数据S-表达式(S-expression)同构 AST强大的宏系统,元编程能力极优
JVM 生态完整访问 Java 类库复用 Spring、Hadoop、Spark 等庞大生态
REPL 驱动运行时代码热加载开发效率大幅提升,调试零重启

真实案例:Stripe 使用 Clojure 构建核心入账系统;Amazon 用 Clojure 重构了部分供应链预测服务; CircleCI 的核心调度引擎以 Clojure 编写。


2. 基础语法

2.1 S-表达式与前缀记法

Clojure 代码由 S-表达式(括号包围的列表)组成,采用前缀记法(操作符在前):

;; 前缀记法:运算符在前
(+ 1 2 3)           ; => 6
(- 10 3 2)          ; => 5
(* 2 (+ 3 4))       ; => 14 (嵌套表达式)

;; 函数调用
(println "Hello World")
(str "Hello" " " "Clojure")

初学者最常见错误是忘记 Clojure 使用圆括号而非中括号来调用函数。

2.2 基础数据类型

;; 数字
42                  ; Long
3.14                ; Double
22/7                ; Ratio (精确分数,Clojure 特有)

;; 字符串
"Hello Clojure"
(str "Hello" " " "World")  ; => "Hello World"

;; 关键字 vs 符号
:status             ; 关键字(Keyword),类似 Ruby Symbol,自带 intern
'hello              ; 符号(Symbol),引用而非求值

;; 布尔与 nil
(true? true)        ; => true
(false? false)      ; => true
(nil? nil)          ; => true
;; Clojure 中空集合 不等同 nil
(nil? [])           ; => false

2.3 集合类型

;; 向量(Vector)— 可变长数组,索引访问 O(log₃₂ n)
(def my-vec [1 2 3 4])
(my-vec 0)          ; => 1 (索引访问)
(conj my-vec 5)     ; => [1 2 3 4 5] (尾部添加)

;; 列表(List)— 链表结构,prepend O(1)
(def my-list '(1 2 3 4))
(first my-list)     ; => 1
(rest my-list)      ; => (2 3 4)
(conj my-list 0)    ; => (0 1 2 3 4) (头部添加)

;; 映射(Map)— 不可变哈希表,共享结构
(def my-map {:name "Alice" :age 30})
(my-map :name)      ; => "Alice"
(:name my-map)      ; => "Alice" (关键字作为函数)
(assoc my-map :city "Beijing")   ; => {:name "Alice", :age 30, :city "Beijing"}

;; 集合(Set)
(def my-set #{1 2 3})
(conj my-set 3)     ; => #{1 2 3} (重复元素忽略)
(contains? my-set 2) ; => true

核心原理:Clojure 的不可变集合采用持久化数据结构(Persistent Data Structures),通过结构共享(Structural Sharing)实现。新集合复用旧集合的大部分树结构,新增/修改仅局部重建路径,时间复杂度均为 O(log₃₂ n),在实际规模下接近常数时间。


3. 变量绑定与函数定义

3.1 def 与局部绑定

;; 全局绑定(创建 Var)
(def app-name "ClojureDemo")

;; 局部绑定:let
(let [x 10
      y 20
      z (+ x y)]
  (println z))        ; => 30
;; x, y, z 仅在 let 代码块内有效

;; 解构绑定
(let [point {:x 100 :y 200}
      {px :x py :y} point]
  (println px py))    ; => 100 200

;; 向量解构
(let [[first second & rest] [1 2 3 4 5]]
  (println first second rest))  ; => 1 2 (3 4 5)

3.2 函数定义

;; 基础函数
(defn greet [name]
  (str "Hello, " name "!"))
(greet "Clojure")     ; => "Hello, Clojure!"

;; 多参数重载
(defn greeting
  ([] "Hello, stranger")
  ([name] (str "Hello, " name))
  ([name suffix] (str "Hello, " name " " suffix)))
(greeting)            ; => "Hello, stranger"
(greeting "Alice")    ; => "Hello, Alice"
(greeting "Alice" "dear") ; => "Hello, Alice dear"

;; 变参函数
(defn sum-all [& numbers]
  (reduce + numbers))
(sum-all 1 2 3 4 5)   ; => 15

;; 高阶函数
(defn make-multiplier [factor]
  (fn [x] (* x factor)))
(def triple (make-multiplier 3))
(triple 7)            ; => 21

4. 集合操作与高阶函数

(def users [{:name "Alice" :age 30 :active true}
            {:name "Bob" :age 25 :active false}
            {:name "Carol" :age 35 :active true}])

;; map: 映射转换
(map :name users)
; => ("Alice" "Bob" "Carol")

;; filter: 过滤
(filter :active users)
; => ({:name "Alice" ...} {:name "Carol" ...})

;; reduce: 归约
(reduce + (map :age users))
; => 90

;; ->> 线程宏:数据流管道
(->> users
     (filter :active)
     (map :age)
     (reduce +))
; => 65

;; -> 线程宏(插到第一个参数位置)
(-> "hello"
    (str " world")
    (str "!"))
; => "hello world!"

;; some, every?, not-any?
(some #(> (:age %) 30) users)   ; => true
every? #(:active %) users)     ; => false (Bob 不 active)

;; juxt: 同时应用多个函数
((juxt :name :age) (first users))
; => ["Alice" 30]

线程宏 (-> vs ->>) 是 Clojure 最具生产力的语法糖之一。-> 将结果插入每个形式的第一个参数位(适合面向对象风格链式调用),->> 插入最后一个参数位(适合数据管道)。


5. 宏(Macro)与元编程

宏是 Clojure 最强大的特性之一,允许在编译时转换代码结构。

;; 时间计量宏
(defmacro time-it [expr]
  `(let [start# (. System (nanoTime))
         result# ~expr]
     (println (str "Elapsed time: "
                   (/ (double (- (. System (nanoTime)) start#)) 1000000.0)
                   " ms"))
     result#))

(time-it (Thread/sleep 100))
; Elapsed time: 100.5 ms
; => nil

;; when-not 宏(Clojure 内置,print 源码)
(macroexpand '(when-not x (println "false")))
; => (if x nil (do (println "false")))

;; 自定义控制流宏
(defmacro unless [condition & body]
  `(if (not ~condition)
     (do ~@body)))

(unless (> 3 5)
  (println "3 is not greater than 5"))

注意:宏的过度使用会降低代码可读性。Clojure 核心哲学是"仅当函数无法解决时再使用宏"。


6. 并发模型:超越线程与锁

Clojure 提供了四种并发原子,覆盖不同场景:

原子用途同步方式适用场景
atom独立可变引用CAS 乐观锁计数器、配置状态
ref协调多个状态STM 事务银行账户转账等多对象一致性
agent异步状态更新消息队列异步日志、后台任务
var线程本地绑定—动态作用域参数
;; Atom:独立 CAS
(def counter (atom 0))
(swap! counter inc)         ; => 1 (原子递增)
(swap! counter + 10)        ; => 11
(reset! counter 0)          ; => 0 (直接重置)
@counter                     ; => 0 (解引用)

;; STM (Software Transaction Memory) — Ref
(def account-a (ref 1000))
(def account-b (ref 500))

;; 事务性转账
transfer [from to amount]
  (dosync
    (let [from-balance @from]
      (if (>= from-balance amount)
        (do
          (alter from - amount)
          (alter to + amount)
          true)
        false)))

(transfer account-a account-b 200)
@account-a      ; => 800
@account-b      ; => 700

;; Agent:异步执行
(def logger (agent []))
(send logger conj "Application started")
(send logger conj "User logged in")
;; 日志按顺序异步追加,不阻塞主线程
@logger         ; => ["Application started" "User logged in"]

;; Future:后台计算
(def result (future (do-some-heavy-computation)))
;; 做其他事情...
@result         ; 阻塞等待结果

STM 核心原理:dosync 包裹的事务在提交时进行乐观并发控制。若检测到读集被其他事务修改,则自动重试直到成功。开发者无需手动加锁,避免了死锁和优先级翻转。


7. 与 Java 互操作

Clojure 无缝调用 Java 类库,这是其"严肃工程语言"定位的核心。

;; Java 互操作语法
(.toUpperCase "hello")              ; => "HELLO"
(String/valueOf 42)                 ; => "42"
(new java.util.Date)                ; 创建实例
(java.util.Date.)                   ; 简写

;; 调用静态方法
(Math/pow 2 10)                     ; => 1024.0

;; 链式调用
(.. "hello" getBytes (clone))

;; doto 简写(对同一对象执行多个操作)
(doto (java.util.ArrayList.)
  (.add "Alice")
  (.add "Bob")
  (.add "Carol"))

;; 实现 Java 接口
(defprotocol IHandler
  (handle [_ request]))

(deftype HttpHandler []
  IHandler
  (handle [_ request]
    {:status 200 :body "OK"}))

8. Web 开发:Ring + Compojure

Ring 是 Clojure 的 Web 应用抽象层(类似 Python WSGI / Node HTTP 模块)。

;; project.clj 依赖
;; [ring "1.12.0"]
;; [compojure "1.8.0"]
;; [ring/ring-json "0.5.1"]

(ns myapp.core
  (:require [compojure.core :refer :all]
            [compojure.route :as route]
            [ring.middleware.json :refer [wrap-json-body wrap-json-response]]
            [ring.util.response :refer [response]]))

(defroutes app-routes
  (GET "/" [] (response {:message "Hello, Clojure API!"}))
  (GET "/users/:id" [id]
    (response {:user-id id :name (str "User-" id)}))
  (POST "/users" req
    (let [body (:body req)]
      (response {:created body :status "success"})))
  (route/not-found (response {:error "Not found"})))

(def app
  (-> app-routes
      wrap-json-body
      wrap-json-response))

;; 启动服务器
;; lein ring server

8.1 中间件模式

;; 日志中间件
(defn wrap-logger [handler]
  (fn [request]
    (let [start (System/currentTimeMillis)
          response (handler request)
          duration (- (System/currentTimeMillis) start)]
      (println (str (:request-method request) " "
                    (:uri request) " - "
                    (:status response) " " duration "ms"))
      response)))

;; CORS 中间件
(defn wrap-cors [handler]
  (fn [request]
    (let [response (handler request)]
      (-> response
          (assoc-in [:headers "Access-Control-Allow-Origin"] "*")
          (assoc-in [:headers "Access-Control-Allow-Methods"] "GET,POST,PUT,DELETE")))))

;; 认证中间件
(defn wrap-auth [handler]
  (fn [request]
    (if (get-in request [:headers "authorization"])
      (handler request)
      {:status 401 :body {:error "Unauthorized"}})))

9. REPL 驱动开发

REPL(Read-Eval-Print Loop)是 Clojure 最重要的生产力工具:

# 启动 REPL
lein repl
# 或
clojure
;; 在文件中定义函数 (src/myapp/core.clj)
(defn calculate-price [items]
  (reduce + (map :price items)))

;; 在 REPL 中实时加载和测试
(use 'myapp.core :reload)    ; 重载命名空间
(calculate-price [{:price 10} {:price 20}])
;; => 30

;; 连接远程 REPL(nREPL 协议)
;; nREPL 允许编辑器(Emacs/Cursive/VS Code)连接到运行中的进程

编辑器集成:

编辑器插件主要功能
EmacsCIDER最成熟的 Clojure 开发环境
IntelliJCursive商业级 IDE 体验
VS CodeCalva社区活跃,配置简单
VimConjureNeovim 首选

10. 总结

概念关键要點
不可变数据所有集合操作返回新集合,旧集合不受影响,天然线程安全
函数即值函数是一等公民,可传递、组合、返回
宏系统编译时代码生成,扩展语言语法
STM 并发多 ref 事务保持一致性,无死锁
REPL 驱动实时代码评估,零重启开发循环

Clojure 学习曲线虽然陡峭(括号和前缀记法需要时间适应),但其带来的不可变性保证和并发编程简化在高复杂度系统中是巨大的杠杆。对于需要处理大量并发状态、数据流转换的团队,Clojure 提供了独一无二的工具链。


延伸阅读

继续阅读

探索更多技术文章

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

全部文章 返回首页

「clojure」更多文章

  1. Datalog 查询与 Datomic/Xtdb:数据即事实、pull、时间旅行与架构
  2. Clojure 静态检查与格式化工具链:clj-kondo、cljfmt、zprint 与 CI
  3. Clojure 认证授权与安全实践:Ring 安全链、JWT、密码哈希与审计