☰
Learn X in Y Minutes:韩语版 Clojure 快速上手指南(learnxinyminutes-docs 仓库实战解析)
2026/10/9 2:59:01 网站建设 项目流程
  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载

Clojure 是为 Java 虚拟机(JVM)打造的 Lisp 系语言,比 Common Lisp 更强调纯函数式编程,并通过软件事务内存(STM)等机制优雅地管理状态。本文以 ko/clojure.md(韩语版)为骨架,结合仓库中的英文原版 clojure.md 与宏专题文档 ko/clojure-macros.md,完整讲解 Clojure 的语法形态、集合与序列、函数定义、映射与集合、模块加载、Java 互操作以及 STM 并发模型,让你读完后能直接上手编写可运行的 Clojure 代码。本文所有示例均来自仓库文档,可直接复制到 REPL 中验证(需要 Clojure 1.2 及以上版本)。

Clojure 是什么:JVM 上的 Lisp 与纯函数式哲学

Clojure 是一门运行在 Java 虚拟机上的 Lisp 系语言。与 Common Lisp 相比,它更加强调纯函数式编程,即函数不依赖也不修改外部状态;同时它内置了多种软件事务内存(STM)工具,用于在函数式不可变的大前提下处理"状态"。

这种"纯函数 + STM"的组合,让并发处理(concurrent processing)变得非常简单,多数情况下甚至无需手动加锁,系统即可自动完成协调。这正是 Clojure 在并发场景下备受青睐的核心原因。

使用前提:本文及仓库文档中的示例均需要Clojure 1.2 或更高版本。

从 Forms 与 Reader 开始:一切皆括号

Clojure 代码由"形态(forms)"构成——forms 就是括号包裹、以空白分隔的元素列表。Clojure 的 reader 读取一个 form 时,会默认把第一个元素当作要调用的函数或宏,其余元素当作参数。

; 注释以分号(;)开头 ; 文件中的第一个调用应该是 ns,用于设置命名空间 (ns learnclojure) ; str 将它的所有参数拼接成一个字符串 (str "Hello" " " "World") ; => "Hello World" ; 数学运算非常直观(前缀表达式) (+ 1 1) ; => 2 (- 2 1) ; => 1 (* 1 2) ; => 2 (/ 2 1) ; => 2 ; 用 = 判断相等 (= 1 1) ; => true (= 2 1) ; => false ; 逻辑运算同样需要 not (not true) ; => false ; forms 可以嵌套,按预期工作 (+ 1 (- 3 2)) ; = 1 + (3 - 2) => 2

韩语原文档特别强调了一个关键事实:(+ 1 2)这样的括号形式本身并没有被"计算"——如果不想让它被求值,需要显式阻止。这正是后面集合部分quote(')语法出现的原因。

类型系统:直接使用 Java 的对象类型

Clojure 的布尔值、字符串和数字直接复用 Java 的对象类型,可以用class函数检查其真实类型:

(class 1) ; 整数字面量默认是 java.lang.Long (class 1.) ; 小数字面量是 java.lang.Double (class "") ; 字符串必须用双引号包裹,是 java.lang.String (class false) ; 布尔值是 java.lang.Boolean (class nil) ; "null" 值在 Clojure 中称为 nil

阻止求值:quote 与 eval

要创建一个"数据列表"而不是执行函数调用,需要使用'(即quote的缩写)阻止求值:

'(+ 1 2) ; => (+ 1 2) ; 等价于 (quote (+ 1 2)) ; 被 quote 的列表仍然可以重新求值 (eval '(+ 1 2)) ; => 3

这段示例清晰展示了 Lisp 系语言"代码即数据"的同像性(homoiconicity)基础,也是后续学习宏的起点。

集合(Collections)与序列(Sequences)

列表与向量:底层数据结构差异

  • **列表(List)**是链表(linked-list)结构;
  • **向量(Vector)**是数组支撑(array-backed)的结构。

两者底层同样是 Java 类:

(class [1 2 3]) ; => clojure.lang.PersistentVector (class '(1 2 3)) ; => clojure.lang.PersistentList

列表本可直接写成(1 2 3),但必须 quote 以避免 reader 把它当成函数调用;因此(list 1 2 3)等价于'(1 2 3)。

集合与序列的抽象区别

  • "集合(collection)"只是数据的组合,列表和向量都是集合:
    (coll? '(1 2 3)) ; => true (coll? [1 2 3]) ; => true
  • "序列(seq)"是对数据列表的抽象描述,只有列表是序列:
    (seq? '(1 2 3)) ; => true (seq? [1 2 3]) ; => false

惰性序列:可以定义无限序列

序列只需在访问时提供一个条目,因此可以是惰性(lazy)的——这意味着可以定义无限增长的序列:

(range 4) ; => (0 1 2 3) (range) ; => (0 1 2 3 4 ...) 无限序列 (take 4 (range)) ; => (0 1 2 3)

核心操作:cons、conj、concat

; cons:在列表或向量的头部添加元素 (cons 4 [1 2 3]) ; => (4 1 2 3) (cons 4 '(1 2 3)) ; => (4 1 2 3) ; conj:以最"高效"的方式向集合添加元素 ; 对列表在头部插入,对向量在尾部插入 (conj [1 2 3] 4) ; => [1 2 3 4] (conj '(1 2 3) 4) ; => (4 1 2 3) ; concat:拼接列表或向量 (concat [1 2] '(3 4)) ; => (1 2 3 4)

注意conj的行为差异正是由列表/向量的底层结构决定的——链表头插高效,数组尾插高效。

高阶函数:map、filter、reduce

; map 与 filter 处理集合 (map inc [1 2 3]) ; => (2 3 4) (filter even? [1 2 3]) ; => (2) ; reduce 将集合"折叠"成一个值 (reduce + [1 2 3 4]) ; = (+ (+ (+ 1 2) 3) 4) ; => 10 ; reduce 还可以接收初始值参数 (reduce conj [] '(3 2 1)) ; = (conj (conj (conj [] 3) 2) 1) ; => [3 2 1]

reduce的两种用法展示了它在函数式编程中作为"累积器"的威力:既可以无初始值直接折叠,也可以指定初始值控制结果形态(例如用空向量[]配合conj反转列表)。

函数定义:fn、def、defn 与各种变体

; fn 创建函数,函数永远返回最后一个语句的结果 (fn [] "Hello World") ; => fn ; 调用需要额外加括号 ((fn [] "Hello World")) ; => "Hello World" ; def 创建 var(变量) (def x 1) x ; => 1 ; 把函数赋给 var (def hello-world (fn [] "Hello World")) (hello-world) ; => "Hello World" ; 用 defn 简写上述过程 (defn hello-world [] "Hello World") ; [] 是函数的参数列表 (defn hello [name] (str "Hello " name)) (hello "Steve") ; => "Hello Steve" ; 匿名函数简写:#(...) 中 %1 表示第一个参数 (def hello2 #(str "Hello " %1)) (hello2 "Fanny") ; => "Hello Fanny"

多参数(variadic)函数

函数可以定义多套参数列表,按调用时参数个数分发:

(defn hello3 ([] "Hello World") ([name] (str "Hello " name))) (hello3 "Jake") ; => "Hello Jake" (hello3) ; => "Hello World"

也可以把额外参数打包成一个序列:

; & 后的参数会被收集为序列 (defn count-args [& args] (str "You passed " (count args) " args: " args)) (count-args 1 2 3) ; => "You passed 3 args: (1 2 3)" ; 常规参数与打包参数可以混用 (defn hello-count [name & args] (str "Hello " name ", you passed " (count args) " extra args")) (hello-count "Finn" 1 2 3) ; => "Hello Finn, you passed 3 extra args"

映射(Maps):哈希映射与数组映射

哈希映射(hash map)与数组映射(array map)共享同一接口。哈希映射查找更快,但不保留键的顺序:

(class {:a 1 :b 2 :c 3}) ; => clojure.lang.PersistentArrayMap (class (hash-map :a 1 :b 2 :c 3)) ; => clojure.lang.PersistentHashMap

数组映射在经历足够多的操作变大后,会自动升级为哈希映射,因此无需担心性能退化。

关键字(Keyword)作为键

映射可以用任何可哈希类型作为键,但通常关键字(keyword)最佳——它类似字符串但更高效:

(class :a) ; => clojure.lang.Keyword (def stringmap {"a" 1, "b" 2, "c" 3}) stringmap ; => {"a" 1, "b" 2, "c" 3} (def keymap {:a 1, :b 2, :c 3}) keymap ; => {:a 1, :c 3, :b 2}

注意:逗号始终被当作空白处理,不做任何事,只是为了提高可读性。

取值:把映射当函数调用

; 通过"调用映射"取值 (stringmap "a") ; => 1 (keymap :a) ; => 1 ; 关键字也能反过来从映射中取自己的值 (:b keymap) ; => 2 ; 但字符串不行——字符串不是函数 ;("a" stringmap) ; => Exception: java.lang.String cannot be cast to clojure.lang.IFn ; 取不存在的键返回 nil (stringmap "d") ; => nil

assoc 与 dissoc:增删键,但不可变

; assoc 添加新键 (def newkeymap (assoc keymap :d 4)) newkeymap ; => {:a 1, :b 2, :c 3, :d 4} ; 但 Clojure 类型是不可变的! keymap ; => {:a 1, :b 2, :c 3} ; dissoc 删除键 (dissoc keymap :a :b) ; => {:c 3}

这里必须牢记:所有 Clojure 集合都是不可变的,assoc/dissoc返回的是新映射,原映射毫发无损。这正是函数式编程与并发安全的基础。

集合(Sets):去重与成员测试

(class #{1 2 3}) ; => clojure.lang.PersistentHashSet (set [1 2 3 1 2 3 3 2 1 3 2 1]) ; => #{1 2 3} ; conj 添加成员 (conj #{1 2 3} 4) ; => #{1 2 3 4} ; disj 移除成员 (disj #{1 2 3} 1) ; => #{2 3} ; 把集合当函数调用可测试成员存在性 (#{1 2 3} 1) ; => 1 (#{1 2 3} 4) ; => nil

更丰富的集合操作函数位于clojure.sets命名空间,下文"模块"部分会演示如何加载并使用。

常用控制形态:if、let、do

; Clojure 中的逻辑构造其实就是宏,写法与其它形式无异 (if false "a" "b") ; => "b" (if false "a") ; => nil(没有 else 分支时返回 nil) ; let 创建临时绑定 (let [a 1 b 2] (> a b)) ; => false ; do 将多个语句组成一个语句块,返回最后一个表达式的值 (do (print "Hello") "World") ; => "World"(同时打印 "Hello") ; 函数体内隐式包含 do (defn print-and-say-hello [name] (print "Saying hello to " name) (str "Hello " name)) (print-and-say-hello "Jeff") ; => "Hello Jeff"(打印 "Saying hello to Jeff") ; let 内部同样隐式包含 do (let [name "Urkel"] (print "Saying hello to " name) (str "Hello " name)) ; => "Hello Urkel"(打印 "Saying hello to Urkel")

韩语原文档在此处点到"if 与宏可以构建出其它逻辑运算",英文原版 clojure.md 则补充了线程宏(threading macros)这一实战利器,值得一并掌握。

线程宏:->、->> 与 as->

线程宏让数据变换的表达更加清晰:

; "thread-first" 宏 (->) 把上一步结果作为下一个 form 的第一个参数插入 (-> {:a 1 :b 2} (assoc :c 3) ;=> (assoc {:a 1 :b 2} :c 3) (dissoc :b)) ;=> (dissoc (assoc {:a 1 :b 2} :c 3) :b) ; 等价于 (dissoc (assoc {:a 1 :b 2} :c 3) :b) ; 结果为 {:a 1 :c 3} ; "thread-last" 宏 (->>) 把结果插入到每个 form 的末尾,特别适合集合操作 (->> (range 10) (map inc) ;=> (map inc (range 10)) (filter odd?) ;=> (filter odd? (map inc (range 10))) (into [])) ;=> (into [] (filter odd? (map inc (range 10)))) ; 结果: [1 3 5 7 9] ; as-> 可以给变换结果命名,自由指定插入位置 (as-> [1 2 3] input (map inc input) ; 上一个变换的输出放在末尾 (nth input 2) ; 也可以放在第二个位置 (conj [4 5 6] input 8 9 10)) ; 甚至可以放在中间 ; 结果: [4 5 6 4 8 9 10]

这三个宏是日常 Clojure 数据处理中出镜率最高的工具,熟练掌握可以大幅提升代码可读性。

模块(Modules):use、require 与 ns

; use 导入模块中的所有函数 (use 'clojure.set) ; 现在可以使用集合运算了 (intersection #{1 2 3} #{2 3 4}) ; => #{2 3} (difference #{1 2 3} #{2 3 4}) ; => #{1} ; 也可以只导入一部分函数 (use '[clojure.set :only [intersection]]) ; require 导入模块 (require 'clojure.string) ; 用 / 调用模块中的函数 (clojure.string/blank? "") ; => true ; 导入时可以为模块起短别名 (require '[clojure.string :as str]) (str/replace "This is a test." #"[a-o]" str/upper-case) ; => "THIs Is A tEst." ; (#"" 表示正则表达式字面量)

在现代 Clojure 代码中,更推荐在ns里用:require声明依赖(英文原版注释甚至建议"use 就别用了"):

(ns test (:require [clojure.string :as str] [clojure.set :as set]))

这样无需对模块名 quote,且依赖关系一目了然。仓库中的宏专题 ko/clojure-macros.md 同样遵循这一模式组织示例。

Java 互操作:直接调用 Java 标准库

Java 拥有庞大而实用的标准库,Clojure 提供了多种语法糖来调用它们:

; import 加载 Java 模块 (import java.util.Date) ; 也可以在 ns 中声明 import (ns test (:import java.util.Date java.util.Calendar)) ; 类名末尾加 "." 创建新实例 (Date.) ; <一个 Date 对象> ; 用 . 调用实例方法;或用 ".方法名" 简写 (. (Date.) getTime) ; <一个时间戳> (.getTime (Date.)) ; 完全等价 ; 用 / 调用静态方法 (System/currentTimeMillis) ; <一个时间戳>(System 始终存在) ; doto 让"可变"类的链式操作更顺手 (import java.util.Calendar) (doto (Calendar/getInstance) (.set 2000 1 1 0 0 0) .getTime) ; => 一个被设为 2000-01-01 00:00:00 的 Date

这段互操作能力意味着 Clojure 程序员可以随时"借用"整个 Java 生态,这是它作为 JVM 语言最实际的优势之一。

STM 与状态管理:atom、swap! 与 deref

软件事务内存(Software Transactional Memory)是 Clojure 处理持久化(persistent)状态的机制,包含几种核心构造。

atom:最简单的事务构造

; 创建 atom,传入初始值 (def my-atom (atom {})) ; swap! 更新 atom ; swap! 接收一个函数,以 atom 当前值为第一个参数、 ; 其余参数依次传入,然后调用该函数 (swap! my-atom assoc :a 1) ; 将 my-atom 设为 (assoc {} :a 1) 的结果 (swap! my-atom assoc :b 2) ; 将 my-atom 设为 (assoc {:a 1} :b 2) 的结果 ; 用 @ 解引用(dereference)atom 获取值 my-atom ; => Atom<#...>(返回的是 Atom 对象本身) @my-atom ; => {:a 1 :b 2}

实战:用 atom 实现计数器

(def counter (atom 0)) (defn inc-counter [] (swap! counter inc)) (inc-counter) (inc-counter) (inc-counter) (inc-counter) (inc-counter) @counter ; => 5

每次swap!都是无锁的原子更新,多个线程可以安全地并发调用inc-counter,这正是"并发处理被极大简化"的直观体现。

其它 STM 构造:ref 与 agent

除 atom 外,Clojure 的 STM 还提供ref(支持事务性同步更新,适合需要多个状态协同变更的场景)与agent(异步更新,适合后台任务)两种构造。仓库文档仅作指引性介绍,深入内容可查阅官方文档:

  • Refs: http://clojure.org/refs
  • Agents: http://clojure.org/agents

进阶延伸:宏(Macros)与同像性

在掌握本文全部语法后,强烈推荐继续阅读仓库中的 ko/clojure-macros.md(韩语版宏指南)。它与本文同源于英文原版,讲解了:

  • defmacro定义宏,宏必须返回可被求值的 Clojure 列表;
  • macroexpand/macroexpand-1查看宏展开结果;
  • 反引号`与~、~@的模板与拼接技巧;
  • gensym与x#实现卫生宏(hygienic macros),避免变量捕获(variable capture)问题。

例如unless宏的经典写法:

(defmacro unless [arg & body] `(if (not ~arg) (do ~@body))) (macroexpand '(unless true (reverse "Hello World"))) ;; -> (if (clojure.core/not true) (do (reverse "Hello World"))) (unless true "Hello") ; -> nil (unless false "Hello") ; -> "Hello"

宏是 Clojure 同像性赋予语言的"元编程"能力,但要谨记文档的告诫:能用函数解决的问题不要用宏,只有需要控制参数求值时机时才该使用宏。

仓库中的文档工程实践

作为 learnxinyminutes-docs 仓库的一员,韩语版 ko/clojure.md 遵循了仓库统一的文档规范:

  • YAML Front Matter:文件头部包含contributors(贡献者)与translators(译者)元数据,本例中贡献者为 Adam Bard,韩语译者为 netpyoung;
  • 代码即文档:正文以完整可运行的带注释代码为主,每个示例都标注了预期输出(; => ...),便于读者在 REPL 中逐段验证;
  • 质量保障工具:仓库提供了 lint/frontmatter.py(校验 Markdown 文件头部 YAML 的键名与类型合法性,允许的键包括name、filename、contributors、translators等)与 lint/encoding.sh(检查所有.md文件的编码必须是 UTF-8 或 US-ASCII 且无 BOM),韩语文档正是在这样的工程规范下维护的。

进一步学习

本文与 ko/clojure.md 只是入门,好在可补充的资源非常多:

  • Clojure.org官方文档站收录了大量专题文章:http://clojure.org/
  • Clojuredocs.org为核心函数提供带示例的文档:http://clojuredocs.org/quickref/Clojure%20Core
  • 4Clojure是提升 Clojure / 函数式编程技能的在线练习平台:https://4clojure.oxal.org/
  • Clojure-doc.org拥有大量入门文章:http://clojure-doc.org/
  • 仓库内进阶材料:英文原版 clojure.md 与宏专题 ko/clojure-macros.md

从"一切皆括号"的 forms 到并发友好的 STM,Clojure 用一套简洁而强大的原语,让函数式编程与 Java 生态无缝衔接。现在就把本文的示例粘进你的 REPL,亲手感受前缀表达式、不可变集合与原子更新的魅力吧。

  • 文档
  • 教程

【免费下载链接】learnxinyminutes-docs

Code documentation written as code! How novel and totally my idea!

项目地址:https://gitcode.com/gh_mirrors/le/learnxinyminutes-docs
点击查看免费下载
上一篇:Wotr-BD-LR神话道途全解析:天使、恶魔、灵使最强搭配方案
下一篇:libiec61850项目中T/S/P选择器的配置原理与实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询