Clojure解谜游戏
Table of Contents
1. 基础
1.1. = 与 ==
1.1.1. 源码
(= 1 1N 1.0 1.0M) (== 1 1N 1.0 1.0M)
false true
1.1.2. 要点
=支持结构与数值宽松相等(数值跨类型比较, 可为真).==仅用于数值, 专注数值比较; 对非数值会抛错.- 对于 1 的不同数值类型(
Long/BigInt/Double/BigDecimal), 二者都认为相等.
1.1.3. 解释
- 两行都为
true.=在数值上进行宽松比较;==仅用于数值, 但也会正确比较数值等价性.
1.2. 整数溢出, 自动提升与非检查运算
1.2.1. 源码
(def bignum 9223372036854775807) (def biggernum 9223372036854775808) (= biggernum (+ bignum 1)) (= biggernum (+' bignum 1)) (= biggernum (unchecked-add bignum 1))
1.2.2. 要点
- Clojure 的
+溢出超界异常;+'强制使用任意精度;unchecked-add则保留溢出行为.
1.2.3. 解释
- 第一行抛出异常.
- 第二行
true; 最后一行为false, 因为unchecked-add溢出回绕到负数.
1.3. NaN 的相等性
1.3.1. 源码
(= ##NaN ##NaN) (== ##NaN ##NaN) (= ##Inf ##Inf)
false false true
1.3.2. 要点
- IEEE 754 规定 NaN 与自身不相等, 因此
=与==都返回false.
1.3.3. 解释
- 两行均为
false, 体现 NaN 的特殊性.
1.4. 真值与假值
1.4.1. 源码
(true? true) (true? :sky-is-blue) (false? false) (false? nil) (false? `()) (false? 0)
true false true false false false
1.4.2. 要点
- 在 Clojure 中, 除了
nil与false外, 其他任何值都被视为" 真" . true?/false?严格检查布尔字面量本身.
1.4.3. 解释
- 仅第一与第三行为
true; 其余均为false, 表明谓词对精确布尔值进行检查.
1.5. 空可变参数的逻辑值
1.5.1. 源码
(and) (every? odd? []) (or)
true true nil
1.5.2. 要点
(and)没有参数时为true;(or)没有参数时为nil.every?对空集合返回true(真空真).
1.5.3. 解释
- 分别返回
true,true,nil.
2. 容器
2.1. 序列判等
2.1.1. 源码
(= [0 1 2 3 4 5] '(0 1 2 3 4 5) (range 6))
true
2.1.2. 要点
- Clojure 将序列按元素内容比较, 类型不同但元素相同也相等.
2.1.3. 解释
- 表达式为
true.
2.2. 无中生有
2.2.1. 源码
(nil? (assoc nil :ex :nihilo))
false
2.2.2. 要点
(assoc nil k v)会返回一个只含该键值对的新 map, 而不是报错.
2.2.3. 解释
- 表达式结果为
false, 因为返回的是 map 而非nil.
2.3. 大海捞针
2.3.1. 源码
(def haystack (shuffle (conj (range 100) :needle))) (contains? haystack :needle)
false
2.3.2. 要点
contains?对 map 检查键, 对 set 检查成员, 但对向量检查" 合法索引" .- 例中
shuffle返回向量,contains?用:needle(关键字)查索引, 故不成立.
2.3.3. 解释
- 结果为
false. 如需成员检查, 应将序列转为set后判断.
2.4. 记录类型
2.4.1. 源码
(defrecord Album [name artist]) (def news-of-the-world (->Album "News of the World" "Queen")) (instance? Album (dissoc news-of-the-world :artist))
false
2.4.2. 要点
defrecord创建的值在移除键后,dissoc返回普通 map, 不再是该记录类型的实例.
2.4.3. 解释
- 最后一行为
false.
2.5. 入乡随俗
2.5.1. 源码
(= (conj '(:colosseum :vatican) :pantheon :trevi-fountain) (conj [:colosseum :vatican] :pantheon :trevi-fountain))
false
2.5.2. 要点
conj对向量追加到尾部; 对列表则插入到表头, 且多元素时按给定顺序逐个插入(因此顺序反转).- 与序列相等比较时, 顺序不同导致不相等.
2.5.3. 解释
- 左边是列表: 结果为
(:trevi-fountain :pantheon :colosseum :vatican); 右边向量结果为[:colosseum :vatican :pantheon :trevi-fountain]; 不相等.
2.6. 有序集比较
2.6.1. 源码
(contains? (sorted-set 1 2 3) :hello)
class java.lang.ClassCastException
2.6.2. 要点
sorted-set基于比较器; 当向其中查询或加入不同可比性的类型时, 比较器可能抛ClassCastException.
2.6.3. 解释
- 在部分环境下会抛出类型比较异常(关键字与数字不可比); 此例提醒混用类型在有序集合上并不安全.
3. 求值
3.1. #_ 的作用
3.1.1. 源码
(def my-msgs {:emails [[:from "boss"] [:from "mom"] #_ #_[:from "Nigerian Prince"] [:from "LinkedIn"]] :discord-msgs {"Clojure Camp" 6 #_ #_"Heart of Clojure" 3 "DungeonMasters" 20} :voicemails ["Your voicemail box is full."]}) (defn unread [msgs] (let [{:keys [emails discord-msgs voicemails]} msgs] (+ (count emails) (reduce + (vals discord-msgs)) (count voicemails)))) (= 34 (unread my-msgs))
false
3.1.2. 要点
- reader宏
#_在读取期直接丢弃紧随其后的表单(可连续使用多次). - 与
comment不同,#_不会在数据结构中留下nil占位, 适合在字面量中注释掉元素.
3.1.3. 解释
- 被
#_丢弃的两条" 邮件" 和一个群组计数不参与求值, 未读总数为 34.
3.2. comment 的作用
3.2.1. 源码
(map inc [1 2 3 (comment 4) 5])
class java.lang.NullPointerException
3.2.2. 要点
comment是宏, 整体返回nil, 但若用在字面量集合里, 会在集合里留下一个nil元素.inc nil会抛异常; 若想在读取时直接丢弃代码, 使用读者宏#_.
3.2.3. 解释
- 上式运行会在处理到
nil时报错; 改用[1 2 3 #_4 5]则安全.
3.3. 掷骰子
3.3.1. 源码
(= #{(rand-int 6) (rand-int 6)} #{(rand-int 6) (rand-int 6)})
class java.lang.IllegalArgumentException
3.3.2. 要点
- reader会检查是否表达式一致.
3.3.3. 解释
- 连reader都过不了.
3.4. 匿名函数展开
3.4.1. 源码
(def names ["Scarlet" "Dandelion" "Cerulean"]) (def codes ["FD0E35" "FED85D" "02A4D3"]) (= (map #([%1 %2]) names codes) '(["Scarlet" "FD0E35"] ["Dandelion" "FED85D"] ["Cerulean" "02A4D3"]))
class clojure.lang.ArityException
3.4.2. 要点
#()是 Clojure 中创建匿名函数的阅读器宏 (reader macro).#([%1 %2])这段代码会被 Clojure 的阅读器展开(fn [%1 %2] ([%1 %2])).
3.4.3. 解释
- 编译期间抛出异常
3.5. 引用与语法引用
3.5.1. 源码
(def raven "nevermore") (= raven "nevermore") (= 'raven (symbol "raven")) (= `raven (symbol "user/raven"))
true true false
3.5.2. 要点
'raven产生未命名空间限定的符号;`raven产生带当前命名空间限定的符号(如user/raven).- 普通变量比较与符号比较要区分.
3.5.3. 解释
- 三行均为
true, 演示值, 未限定符号, 语法引用符号的差异.
3.6. -> 与匿名函数
3.6.1. 源码
(= (-> 10 inc) (-> 10 #(+ % 1)))
class clojure.lang.Compiler$CompilerException
3.6.2. 要点
(-> 10 inc)的宏展开为(inc 10)(-> 10 #(+ % 1))(fn* 10 [p1_20220#] (+ p1_20220# 1))
3.6.3. 解释
- 所以编译错误
4. 运行时和库
4.1. case 的常量匹配语义
4.1.1. 源码
(def photo1 :hero) (def photo2 :villain) (defn identify [photo] (case photo photo1 "Our hero!" photo2 "The dastardly villain" "Unknown")) (= (identify photo1) "Our hero!") (= (identify photo2) "The dastardly villain")
false false
4.1.2. 要点
case的分支测试必须是编译期常量, 分派使用哈希, 不在运行期做求值.- 此处
photo1/photo2在同一命名空间提前定义为关键字, 因而作为常量可用.
4.1.3. 解释
- 由于
photo1/photo2已被定义为关键字,case得以在编译期内联这些常量, 匹配成功. - 两个断言均为
true. 若分支里使用非常量(如运行时变量),case将报错.
4.2. 大爆炸
4.2.1. 源码
(defn doubled "Returns a sequence of all i's from 0 to n (exclusive), doubled. Ex: (doubled 3) ;; (0 0 1 1 2 2)" [n] (loop [i 0 output ()] (if (< i n) (recur (inc i) (concat output [i i])) output))) (= 0 (first (doubled 50000)))
class java.lang.StackOverflowError
4.2.2. 要点
- 在循环中频繁使用
concat累积结果, 会导致时间复杂度退化(构建长序列时代价高). - 更好的做法是使用
conj到向量中, 或使用transient/reduce等方式.
4.2.3. 解释
- 能得到正确结果, 但
concat每次都会遍历其左侧参数, 累积成本高. - 若使用
transient向量或reduce构造, 将显著提速并减少内存压力.
4.3. 字符串分割
4.3.1. 源码
(require '[clojure.string :as str]) (= (str/split "banana" #"an") ["b" "" "a"]) (= (str/split "banana" #"na") ["ba"])
true true
4.3.2. 要点
clojure.string/split默认会丢弃结尾处的空字符串.- 正则分割存在" 不可重叠" 匹配的特性, 导致像 "banana" 中的重叠片段表现出特定结果.
4.3.3. 解释
- 第一个分割: 匹配 "an" 两次, 得到片段 "b", "", "a"(中间空串源于相邻匹配边界).
- 第二个分割: 理论上得到 ["ba" "" ], 但结尾空串被默认丢弃, 故只剩 ["ba"].
4.4. 嵌套
4.4.1. 源码
(def ex1 (for [a [0 1 2] b [3 4 5]] (* a b))) (def ex2 (for [a [0 1 2]] (for [b [3 4 5]] (* a b)))) (= ex1 ex2)
false
4.4.2. 要点
for的多个绑定向量会产生笛卡尔积的" 扁平" 序列.- 嵌套两层
for则产生" 序列的序列" . - 因此两者结果结构不同, 不能相等.
4.4.3. 解释
ex1是" 扁平" 序列, 如(0 0 0 3 4 5 6 8 10);ex2是三段子序列的序列.- 最后一行返回
false.
4.5. 恋栈不去
4.5.1. 源码
(require '[clojure.java.io :as jio] '[clojure.string :as str]) (defn grep "Return lazy sequence of matches to substring in the file at path" [substring path] (with-open [reader (jio/reader path)] (->> reader (line-seq) (filter #(str/includes? % substring))))) (grep "defn" "book/src/hanging_around.clj")
class java.io.FileNotFoundException
4.5.2. 要点
with-open退出后关闭 Reader; 若返回基于该 Reader 的懒序列, 消费时将面对已关闭资源.- 解决: 在
with-open内部强制实现(如doall/into)以确保读取完成.
4.5.3. 解释
- 返回的是懒序列, 但
with-open已关闭文件句柄. 后续惰性消费将失败或得到空结果. - 改进:
(with-open [r ...] (doall (filter ... (line-seq r))))或使用slur/jio/file等先读完.
4.6. 真假捕获
4.6.1. 源码
(defn sentence [subject object] (str "Is " subject " a " object "?")) (def planet "Mars") (def f1 (partial sentence planet)) (def f2 (fn [object] (sentence planet object))) ;; redefine (def planet "Earth") (= (f1 "dream") (f2 "dream"))
false
4.6.2. 要点
partial在创建时固定了已提供参数的" 值" .- 显式
fn闭包中对planet的引用会在调用时解引用 Var 的当前根值; 后续def重新定义会影响它.
4.6.3. 解释
f1仍使用 "Mars";f2使用更新后的 "Earth"; 因此最终比较为false.
4.7. 解构
4.7.1. 源码
(def flip {:head :tail, :tail :head}) (let [empty {} left nil right nil {:keys [left right] :or {left :head, right (flip left)}} empty right-1 right left nil, right nil {:keys [right left] :or {left :head, right (flip left)}} empty right-2 right] (println "Results:" right-1 right-2) (= right-1 right-2))
false
4.7.2. 思路与陷阱
- 解构映射时,
:or中的默认值表达式按变量绑定顺序求值. - 当默认值表达式依赖同组中其它键的局部名时, 不同的
:keys顺序会影响该局部名是否已就绪, 进而影响默认值. - 下面示例两次使用相同的
:or, 但:keys的顺序不同, 导致right的默认值在一次中能引用到left, 另一次引用不到.
4.7.3. 解释
- 第一段中先绑定
left默认值为:head, 因此right的默认(flip left)能看到left, 得到:tail. - 第二段交换了
:keys顺序,right的默认值在left默认化之前运行,left仍是nil, 于是(flip left)得到nil. - 因此
right-1与right-2一般不同, 表达式返回false.
4.8. 类型提示与元数据 :tag
4.8.1. 源码
(def ^double PI 3.14159) (= 'double (-> #'PI meta :tag))
false
4.8.2. 要点
^double给 Var 附加:tag元数据, 供编译器/IDE/反射使用.- 可通过
#'Var取其 Var, 再读meta查看:tag.
4.8.3. 解释
- 当类型提示是一个 Java 类时(例如 ^String 或 ^java.util.Date), :tag 的值是对应的 Class 对象(例如 java.lang.String).
- 当类型提示是一个 Clojure 核心库中的原始类型时(例如 ^double, ^long, ^int), :tag 的值是 clojure.core 中对应的转换函数 (例如 clojure.core/double, clojure.core/long). 这些函数不仅用于类型转换, 也代表了这些原始类型本身.