惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Apple Machine Learning Research
Apple Machine Learning Research
Blog — PlanetScale
Blog — PlanetScale
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
G
Google Developers Blog
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Threat Research - Cisco Blogs
A
Arctic Wolf
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Cloudflare Blog
博客园 - 【当耐特】
AWS News Blog
AWS News Blog
U
Unit 42
V
Vulnerabilities – Threatpost
P
Privacy International News Feed
T
Tor Project blog
Microsoft Security Blog
Microsoft Security Blog
宝玉的分享
宝玉的分享
Google DeepMind News
Google DeepMind News
爱范儿
爱范儿
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Recorded Future
Recorded Future
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
Threatpost
Latest news
Latest news
GbyAI
GbyAI
S
SegmentFault 最新的问题
MongoDB | Blog
MongoDB | Blog
N
Netflix TechBlog - Medium
Hacker News: Ask HN
Hacker News: Ask HN
美团技术团队
N
News | PayPal Newsroom
J
Java Code Geeks
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
The Hacker News
The Hacker News
The GitHub Blog
The GitHub Blog
V
V2EX
N
News and Events Feed by Topic
T
Troy Hunt's Blog
Security Latest
Security Latest
博客园 - 叶小钗
P
Palo Alto Networks Blog

暗无天日

读:AI Agent 安全日志——从可见性与隐私的两难说起 - 暗无天日 读:AI Agent 生产化——一份从原型到上线的速查清单 - 暗无天日 AI写作的语言指纹——如何让文字不那么像机器 - 暗无天日 读:50 条 Claude Code 技巧——一个工程经理的六个月使用心得 读:AI 辅助开发为什么让 E2E 测试更有价值 - 暗无天日 读:在Emacs中使用Claude Code(Spacemacs适配版) - 暗无天日 Claude Code 背后的工程哲学——读 Agent Harness Engineering 读:Agent Harness Engineering——AI 智能体不只是模型,还有套件 - 暗无天日 browser-harness:让 AI 直接接管你的浏览器 - 暗无天日 读:Security-First CI/CD —— DevSecOps 自动化实践指南 TIL: 数字小键盘的小数点陷阱与行内算术求值 - 暗无天日 Conducty:给 Claude Code 加上项目记忆和并行执行能力 - 暗无天日 读 — GitHub Trending 里的 Claude Code 技能包 读 — Prompt Caching 省钱指南 TIL: Emacs 中那些跟鼠标配合的冷门快捷键 - 暗无天日 读:Anvil——把 Emacs 变成 AI 的工具服务器 读:Emacs 代码折叠终极指南 - 暗无天日 读:Clojure 搭车客指南 - 暗无天日 git推送失败后恢复仓库损坏的完整记录 - 暗无天日 多智能体系统的两个有效模式——以及对 Claude Code 用户的启示 - 暗无天日 用 Org Babel 写 Literate 博文:扩展执行 + 定制导出 proced:Emacs 内置的进程查看器 - 暗无天日 从 proced 定制中学到的 Elisp 模式 读:让 Emacs proced 在 macOS 上显示 CPU 和内存 异步编程的函数着色税 - 暗无天日 链式调用的代价:JavaScript 和 Clojure 的共同教训 - 暗无天日 hyperfine:命令行基准测试工具 - 暗无天日 管道中的变量去哪了?——子 shell 作用域陷阱 - 暗无天日 开源包装器的信任陷阱:四个危险信号 - 暗无天日 程序员愿意为 AI 写文档,却不愿为同事写 - 暗无天日 mktemp: Shell 脚本中临时文件的安全陷阱与最佳实践 - 暗无天日 WSL9x —— 在 Windows 9x 里跑 Linux 内核 6.19 用 ox.el 做你想做的事 —— org-export 高级编程指南 读:Hot-wiring the Lisp Machine —— 用纯 Elisp 构建零依赖的 Org 静态站点生成器 Elisp 性能优化的六个实战教训 - 暗无天日 fcitx5 下 Emacs 无法切换输入法的排查 - 暗无天日 ERT 测试交互命令的三种方式 - 暗无天日 SEM Assistant: 当 Elisp 守护进程遇上 LLM 用 dmsg 给 Elisp 加上结构化调试日志 用 org-habit 追踪非每日习惯 - 暗无天日 Clojure X-Men:当编程语言特性变成超能力 - 暗无天日 TIL: 用 diff-hl 在 fringe 中显示 git 变更 读:llm-test —— 用 LLM agent 驱动 Emacs 测试 TIL: AI 时代的橡皮鸭调试 - 暗无天日 fcitx 启动后键盘输入卡顿的排查 - 暗无天日 TIL: 早期网页的图片热区导航 - 暗无天日 读 Seeing the Whole System 用 Emacs 自动生成每周链接推荐 - 暗无天日 读:ASCII control characters in my terminal 读 What to learn - 暗无天日 Lisp 的括号之痛——一个愚人节玩笑揭开的老伤疤 - 暗无天日 一本书该"线性读"还是"并行读" - 暗无天日 读 How to Monetize a Blog:一篇伪装成变现指南的讽刺文 Python Mock 第三方依赖的四种策略 - 暗无天日 Emacs Lisp 热重载实用指南 - 暗无天日 Prot 的 Emacs 配置哲学 - 暗无天日 TIL: 从直播对谈中学到的三个 Emacs 技巧 - 暗无天日 TIL: 自动使用项目虚拟环境的 Python - 暗无天日 TIL: 让 Help buffer 自动获得焦点 一条命令让本地开发用上 HTTPS —— slim 工具介绍 用 fsck 检查和修复 Linux 文件系统 排查Linux进程"卡死"实战:从strace到gdb全流程 - 暗无天日 PostgreSQL 索引:从基础到你可能不知道的高级用法 - 暗无天日 用 .pdbrc 自定义 Python 调试器 ANSI 转义码的标准化现状 - 暗无天日 终端程序的潜规则 - 暗无天日 PARA Org-mode 测试配置 - 暗无天日 AI越强越辣鸡?控制论说这是必然的 - 暗无天日 AI 越强越需要你盯着——反馈循环实操指南 - 暗无天日 你的AI代理正在偷你的密钥——四种你没想到的泄露通道 - 暗无天日 LLM 在 DevOps 中的三种角色 - 暗无天日 写作风格的反建议 - 暗无天日 反驳本质复杂性——Dan Luu 论为什么《没有银弹》错了 - 暗无天日 文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱 - 暗无天日 AI 时代的 PARA 方法:用 Org-mode 和 AI 打造个人知识管理系统 Linux 数据去重学习笔记 - 暗无天日 创建跨平台 ZIP 文件的隐藏陷阱:Extra Field - 暗无天日 X11 Forwarding 排障指南 - 暗无天日 IP欺骗端口扫描:当别人冒充你去扫描别人 - 暗无天日 Linux 输入栈全景解析:从硬件按键到屏幕响应 - 暗无天日 Unix 系统中那些被埋没的配置开关——以 FontConfig 为例 - 暗无天日 在Linux上限制儿童使用电脑 - 暗无天日 GIF不仅仅是一种图片格式——用GIF流做些奇怪的事 - 暗无天日 Leiningen 学习笔记:Clojure 项目构建与管理从入门到实战配置 - 暗无天日 Google SRE Book 读书笔记 - 暗无天日 yes 管道 head 发生了什么 - 暗无天日 为什么 nohup 在 crontab 中不起作用 Bash中的Indirection与Nameref - 暗无天日 Linux PAM 简介 - 暗无天日 从Linux ISO文件启动计算机 - 暗无天日 用 Bash 打造一个Screen Locker 用GitHub Actions自动构建EGO博客 - 暗无天日 blocking I/O 的作用 - 暗无天日 mobileog 手机端同步提示Error:2 No such file 的解决方法 回收 WSL2 VHDX 文件占用空间 使用 org-mode columnview 生成任务列表 - 暗无天日 Emacs 作为 MPD 客户端 - 暗无天日 移动文件路径却不破坏org file link的方法 - 暗无天日 如何合理的导出help link 成HTML - 暗无天日 笑话理解之Biology - 暗无天日
读:Immutability 不是万能药,它是一种权衡 - 暗无天日
2026-04-29 · via 暗无天日

原文:Immutability: Not a Universal Law, But a Trade-off

不可变性(immutability)是指数据一旦创建就不再修改,每次"修改"都产生新版本,这通常被认为是函数式编程的金科玉律。但原文作者 Ivan Gavlik 认为:不可变性不是一个普适法则,而是一种需要根据场景做取舍的权衡。他从代码层、数据层、API 层三个层面分析了不可变性的适用边界。

作为一个 Clojure 程序员,我想从 Clojure 的视角重新审视这篇文章的论点。Clojure 的核心数据结构(list、vector、map、set)全部默认不可变——你没法修改一个 Clojure map 里的某个键值对,只能通过 assocdissoc 生成一个新 map。这不像 Java 那样需要程序员主动选择用 final 或不可变集合,在 Clojure 里不可变性就是唯一的选择。但即便如此,Clojure 也在很多地方做了务实的妥协(比如 =atom=、=transient=)。这些妥协本身就能帮我们理解"什么时候该用不可变性,什么时候不该用"。

代码层:不可变性基本是稳赚的

在单个函数的粒度上,选择不可变性几乎总是对的。原文的论点很简单: 不可变 = 没有隐藏的副作用 = 更容易理解和测试

Clojure 天然就是这样的:

(defn add-balance [user amount]
  (update user :balance + amount))

(def user {:id 1 :balance 100})
(def updated-user (add-balance user 20))

;; user 不变,updated-user 是新的 map
updated-user
;; => {:id 1 :balance 120}
user 保持不变,updated-user 是一个全新的 map。这就是 Clojure 的默认行为,所有数据结构都是不可变的。

并发问题不是线程造成的,是共享可变状态造成的

原文用 Java 展示了一个经典的竞态条件:多个线程同时给 balance+=1 ,期望结果是 1000000,实际得到的数字远小于此且不可预测。问题出在 this.balance += amount 这一行——它不是原子操作,两个线程可以交错执行。

public class User {
    private int balance;

    public void addBalance(int amount) {
        this.balance += amount;     }

    public int getBalance() {
        return balance;
    }
}

User user = new User(); Thread[] threads = new Thread[10];
for (int t = 0; t < 10; t++) {
    threads[t] = new Thread(() -> {
        for (int i = 0; i < 100000; i++) {
            user.addBalance(1);
        }
    });
    threads[t].start();
}
for (Thread t : threads) t.join();
System.out.println("Final balance: " + user.getBalance());

Java 的解决这一问题的方法有好几种: synchronized (加锁,有效但代价高)、 AtomicInteger (Java 提供的一种整数类型,内部用 CPU 的 CAS 指令保证 +=1 这类操作不会被线程交错打断,但值本身仍然是可变的)、完全不可变对象(每次返回新实例)。原文最终推荐的是"不可变 + 协调"的组合方案。

Clojure 在这个问题上走得更远——它用 atom 提供了一种既不可变又高效的方案:

(def balance (atom 0))

;; 2 个线程各执行 1000 次 +1,用 swap! 保证原子更新
;; swap! 内部用 CAS(Compare-And-Swap),不阻塞线程
(dotimes [_ 2]
  (future
    (dotimes [_ 1000]
      (swap! balance inc))))

;; 等所有 future 完成
(Thread/sleep 100)
@balance
;; => 2000
2000

关键点: swap! 接受一个纯函数 inc ,atom 内部保证将这个函数应用到当前值的过程是原子的。你写的是纯函数式变风格的代码,Clojure 的运行时会帮你处理协调问题。这正是原文说的"不可变状态迫使你显式地表达变化如何发生"的意思:在 Clojure 里,你必须用 swap!reset! 这样的操作来表达"我要改变状态",而不是随手写一个 + 然后祈祷不会出事。

Clojure 还有 ref (配合 dosync 实现事务性更新)和 agent (异步更新),分别对应不同的协调需求。但核心思路都一样:值是不可变的,状态的变化必须明确表达出来。

代码层结论

在代码层面,不可变性基本没有争议——代价小,收益大。原文也认同这一点。真正的权衡从数据建模开始。

数据层:从"改一个字段"到"记一笔历史"

到了数据建模层面,问题从"要不要修改这个对象"变成了更根本的设计选择:你的数据模型是记录"当前状态"还是记录"发生过什么"?

状态模型 vs 事件模型

;; 状态模型:只存当前值
(def user-state (atom {:name "Alice" :balance 100}))
(swap! user-state update :balance + 20)
(swap! user-state update :balance - 10)
;; 余额是 110,但你不知道中间发生了什么
@user-state
;; => {:name "Alice" :balance 110}
{:name "Alice" :balance 110}
;; 事件模型:存每一笔变更
(def events
  [{:type :balance-increased :amount 20 :at "2026-04-29T10:00:00Z"}
   {:type :balance-decreased :amount 10 :at "2026-04-29T10:05:00Z"}])

;; 当前状态是推导出来的
(reduce (fn [balance event]
          (case (:type event)
            :balance-increased (+ balance (:amount event))
            :balance-decreased (- balance (:amount event))
            balance))
        100 events)
;; => 110
110

两种模型都能得到余额 110,但事件模型额外保留了"怎么到的 110"这个信息。这就是原文说的核心区别:状态模型回答"现在是什么",事件模型回答"怎么到的这里"。

事件模型的代价

但事件模型不是免费的:

  1. 存储成本 :每次变更都存一条记录,数据量远大于只存当前状态。一个用户改了 100 次名字,状态模型只有 1 条记录,事件模型有 100 条。
  2. 设计复杂度 :你得设计事件的结构、状态转换的规则、不变量(invariant,即"在任何时候都必须成立的条件")。比简单的 CRUD 难得多。
  3. 不一定需要 :用户的昵称改了 100 次,你有必要知道中间 99 次发生了什么吗?

什么时候该用事件模型

原文的判断标准很实用:

  • 跨系统共享状态 时——多个服务需要知道同一份数据的变化历史
  • 历史有业务价值 的领域——金融交易、审计日志、工作流状态
  • 需要回溯查询 时——我想知道"上周三下午 3 点这个订单是什么状态?"

什么时候不该用:

  • 大对象频繁小更新(比如一个巨大的 JSON 每次改一个字段)
  • 历史没有业务价值(比如用户头像 URL 改了,你不在乎它之前是什么)
  • 简单的 CRUD 系统(表单→数据库→保存,没有复杂的业务流程)

Clojure 中的 reduce 和事件模型

Clojure 的 reduce 天然适合事件模型——"从初始状态出发,依次应用每个事件,得到最终状态"。上文的例子已经展示了这一点。更实际的做法是定义一个 apply-event 函数,把状态转换逻辑集中管理:

(defn apply-event [state event]
  (case (:type event)
    :user-registered (assoc state :status :active)
    :user-suspended  (assoc state :status :suspended)
    :user-activated  (assoc state :status :active)
    state))

(reduce apply-event
        {:status :pending}
        [{:type :user-registered}
         {:type :user-suspended}
         {:type :user-activated}])
;; => {:status :active}
{:status :active}

不可变模型的几种变体

原文还列举了几种不同的不可变数据建模方式:

模式 存什么 适用场景
事件溯源(Event Sourcing) 领域事件(发生了什么) 金融、审计、工作流
版本化状态(Versioned State) 完整状态的每次版本 需要回溯但不想重放事件
追加日志(Append-Only Log) 字段变更(什么字段从什么值变成了什么值) 轻量级变更追踪
不可变值对象(Immutable Value Objects) 不可变的领域对象,但持久化当前状态 大多数 CRUD 系统

API 层:Command 和 Event 不是一回事

原文最后讨论了 API 层面。传统的 REST API 是面向状态的,即客户端发送最终期望的状态:

PUT /orders/123
{"status": "SHIPPED"}

这告诉服务端"把这个订单的状态设为 SHIPPED",但不表达"为什么"要这么做。

Command 风格的 API 改为发送意图:

POST /orders/123/ship

这个 API 表达的是"发货"这个业务动作,而不是"把某个字段设为某个值"。服务端负责决定发货之后状态应该怎么变。

Event 风格的 API 则记录发生了什么:

POST /orders/123/events
{"type": "OrderShipped", "timestamp": "2026-04-29T10:00:00Z"}

原文特别强调了一点:Command 是请求("请发货"),Event 是事实("已经发货了")。通常客户端发 Command,服务端处理完之后生成 Event。不应该让客户端直接发 Event——因为只有服务端才有资格确认"这件事确实发生了"。

什么时候用 Command/Event 风格

  • 工作流重要、业务逻辑复杂
  • 多个系统需要对同一个动作做出反应
  • 审计追溯有要求

什么时候该避开:

  • 简单的 CRUD 应用,由于没有复杂的业务流程,状态就能直接表示动作,这时加 /ship/cancel/refund 这些端点反而是过度设计

Clojure 的 multimethod 视角

Clojure 的 defmulti / defmethod 天然适合 Command 风格的路由——根据 Command 的类型分发到不同的处理函数:

(defmulti handle-command :action)

(defmethod handle-command :ship [_ order]
  ;; 发货逻辑:更新状态、触发通知等
  (assoc order :status :shipped))

(defmethod handle-command :cancel [_ order]
  (assoc order :status :cancelled))

(handle-command {:action :ship} {:id 123 :status :pending})
;; => {:id 123, :status :shipped}
{:id 123, :status :shipped}

这种写法的好处是新增一个 Command 只需要加一个 defmethod ,不需要修改已有的处理逻辑。每个 Command 的处理逻辑是独立的,互不干扰。

三个层面的权衡总结

层面 不可变性推荐度 关键权衡
代码层 强烈推荐 代价小(多用一点内存),收益大(并发安全、容易推理、容易测试)
数据层 看场景 事件模型提供完整历史但增加存储和设计复杂度;状态模型简单但丢失历史
API 层 看场景 Command/Event 风格适合复杂业务;CRUD 风格适合简单应用

Clojure 的设计哲学很好地体现了这个递进的权衡:在代码层面全面拥抱不可变性(所有数据结构默认不可变),在数据层面提供灵活的选择(你可以用 atom 存状态,也可以用 reduce 重放事件),在 API 层面不强制(你可以用 defmulti 做 Command 风格,也可以直接用 map 存状态)。

不可变性不是一个开关,而是一个旋钮。根据场景调到合适的位置,才是务实的做法。