Deterministic Core, Non-Deterministic Shell

Deterministic Core, Non-Deterministic Shell

确定性核心,非确定性外壳

3 Aug 2026 Fourteen years ago, Gary Bernhardt coined the term Functional Core, Imperative Shell. Like most good ideas in computing it was not entirely new, but his conception had great clarity, and it forms an excellent basis for talking about testing and determinism in existing systems. 2026年8月3日,十四年前,Gary Bernhardt 提出了“函数式核心,命令式外壳”(Functional Core, Imperative Shell)这一术语。像计算机领域大多数优秀的思想一样,这并非全新的概念,但他的构想极其清晰,并为讨论现有系统中的测试与确定性奠定了极好的基础。

Briefly, Functional Core/Imperative Shell architecture divides the code into two parts. The Functional Core is purely functional - that is no IO, and no destructive state updates. It is concerned with the business logic of the application. The Imperative Shell has comparatively little pathing, but maintains state, coordinates external dependencies, and deals with the outside world - that is to say IO. Its job is to query the core with values, receive values back as the result of some blackbox decision, and use that to interact with the outside world; whether that’s writing to a database, sending a request, or updating a GUI. 简而言之,“函数式核心/命令式外壳”架构将代码分为两部分。函数式核心是纯函数式的——即没有 IO 操作,也没有破坏性的状态更新。它专注于应用程序的业务逻辑。命令式外壳的逻辑路径相对较少,但负责维护状态、协调外部依赖并处理与外部世界的交互——也就是 IO 操作。它的工作是向核心查询数值,接收作为黑盒决策结果返回的数值,并利用这些数值与外部世界进行交互;无论是写入数据库、发送请求还是更新图形用户界面(GUI)。

The Shell and the Core in this model have distinct characteristics: 在此模型中,外壳与核心具有截然不同的特征:

CoreShell
Makes decisionsCoordinates dependencies
More branching execution pathsMore linear execution
Isolated from the worldIntegrates with the world
核心外壳
做出决策协调依赖关系
更多分支执行路径更线性的执行
与世界隔离与世界集成

This makes the core very amenable to testing. Since it’s purely functional, the same inputs will always get the same results. Since it’s isolated, there is nothing to mock or stub. And since it handles complex business logic, the tests can tell us a lot about how the system behaves. 这使得核心非常易于测试。由于它是纯函数式的,相同的输入总是会得到相同的结果。由于它是隔离的,因此无需进行 Mock 或 Stub。而且由于它处理复杂的业务逻辑,测试可以告诉我们很多关于系统行为的信息。

Functional Purity and Determinism

函数式纯度与确定性

A shorter way of describing the properties that make pure functions amenable to testing is that they are deterministic. That is - given a stream of inputs, a pure function always returns the same stream of outputs; their behaviour is repeatable. But pure functional programming is not the only way to get there. If we tilt our heads a little we can see that a stream of values and a sequence of assignments are different ways of expressing the same thing, and State Machines can bring us the same benefits. 描述纯函数易于测试的属性,更简洁的方式是称其为“确定性”。也就是说,给定一系列输入,纯函数总是返回相同的一系列输出;它们的行为是可重复的。但纯函数式编程并不是达到这一目标的唯一途径。如果我们换个角度思考,就会发现数值流和赋值序列是表达同一事物的不同方式,而状态机(State Machines)也能为我们带来同样的好处。

Consider the following code: 请看以下代码:

function add(ns) { return ns.reduce((a, b) => a + b, 0) }

class AddMachine {
  #state = 0
  transition(input) { this.#state += input }
  get state() { return this.#state }
}

The function add is easy to reason about; it’s pure and thus deterministic. But the AddMachine is also deterministic - given the same sequence of calls to the transition function, AddMachine will return the same state. It being imperative does not change that. 函数 add 很容易推理;它是纯的,因此是确定性的。但 AddMachine 也是确定性的——给定相同的 transition 函数调用序列,AddMachine 将返回相同的状态。它是命令式的这一事实并不会改变这一点。

const output = add([1, 2, 3]) // 6

const a = new AddMachine()
a.transition(1)
a.transition(2)
a.transition(3)
const output = a.state // 6

Pure functional programming is a fine paradigm, but due to language or performance considerations, it is not always practical - I would not want to try it in C! But weakening the requirements from purely functional to merely deterministic, we retain the testability benefits of “Functional Core, Imperative Shell”, while broadening its applicability. And so the title of this post: Deterministic Core, Non-Deterministic Shell. 纯函数式编程是一种优秀的范式,但由于语言或性能方面的考虑,它并不总是实用的——我可不想在 C 语言中尝试它!但如果将要求从“纯函数式”放宽到“仅确定性”,我们既保留了“函数式核心,命令式外壳”带来的可测试性优势,又拓宽了其适用范围。这就是本文标题的由来:确定性核心,非确定性外壳。

Determinism can feel like a more abstract concept than functional purity. How do you know it when you see it? I find it’s easier to start with what is not deterministic and work backwards. Here are some common examples of non-repeatable behaviour: 确定性可能比函数式纯度听起来更抽象。当你看到它时,如何识别它呢?我发现从“什么是非确定性的”入手并反向推导会更容易。以下是一些不可重复行为的常见示例:

  • Calling RNGs that aren’t seeded (未设置种子的随机数生成器)
  • Asynchronous and multi-threaded operations (异步和多线程操作)
  • Communication over the network (网络通信)
  • Communication with other processes (与其他进程通信)
  • Reading/Writing to local storage (读写本地存储)
  • Database interactions (数据库交互)
  • Asking the OS for the date or time (向操作系统查询日期或时间)

All these belong in the non-deterministic shell. Whenever you find them in your business logic, you have a natural target for defragmentation - either splitting the function in two around them, or lifting them up a layer and injecting their result as a parameter. It’s illustrative to think of the “shell” metaphor quite literally; it should surround the logic, querying the heart of the application to get what it needs. 所有这些都属于非确定性外壳。每当你在业务逻辑中发现它们时,你就找到了一个进行“碎片整理”的天然目标——要么围绕它们将函数一分为二,要么将它们提升一层,并将它们的结果作为参数注入。将“外壳”这个比喻字面化理解很有启发意义;它应该包裹住逻辑,通过查询应用程序的核心来获取所需的内容。

Working with what you have

利用现有资源

“This is all well and good”, you might think, “but what use of it is to me, toiling away in the legacy & vibe-code mines of industry?”. A fair accusation, imaginary reader; not everyone can be Foundation DB and make that distinction from day one (they actually went a step further, but that’s a topic for another post). “这听起来很不错,”你可能会想,“但对于我这种在工业界的遗留代码和‘随缘代码’矿坑中苦苦挣扎的人来说,这有什么用呢?”这位虚构的读者,你的指责很合理;并不是每个人都能像 Foundation DB 那样从第一天起就做出这种区分(他们实际上走得更远,但这又是另一个话题了)。

Determinism and non-determinism are highly entwined in almost every real life codebase I have seen, and I’ve seen my fair share. But don’t let perfect be the enemy of good! One way to think of your average (ie, terrible) codebase is that it has many deterministic cores. There are thousands, strewn through the slop as stars in the sky. The glass half empty take is these codebases are an irredeemable legacy mess. But glass half full is that there are many deterministic cores hidden somewhere inside, and maybe only a handful. 在我见过的几乎每一个现实世界的代码库中,确定性和非确定性都高度交织在一起,而且我见过不少这样的代码库。但不要让完美成为优秀的敌人!看待你那平庸(即糟糕)的代码库的一种方式是:它拥有许多确定性核心。它们成千上万地散落在混乱的代码中,如同天上的繁星。悲观的看法是,这些代码库是无可救药的遗留烂摊子。但乐观的看法是,其中隐藏着许多确定性核心,哪怕只有少数几个。

Users of older Windows systems may remember the “Disk Defragmenter”; it took files whose contents were scattered physically across the spinning hard disk and made them contiguous. In an era where read speed depended on physical distance on the media, this mattered a lot. There was something so satisfying about seeing the red segments slowly give way to the blue. 老式 Windows 系统的用户可能还记得“磁盘碎片整理程序”;它将物理上分散在旋转硬盘上的文件内容重新整理为连续的块。在那个读取速度取决于介质上物理距离的时代,这非常重要。看着红色的碎片慢慢变成蓝色的连续块,有一种莫名的满足感。

So one gradual approach for existing code is to practice the Defragmentation of Determinism. Identify it wherever you can - files, classes, even a few lines in individual functions - and start collecting them. The more determinism that can be grouped, the more easily testable functionality you have, and the more you can feel confident about the behaviour and reliability of the program as a whole. The surface area for “hard to test” (non-deterministic code) starts to shrink. On a large enough codebase you will likely never get to a single deterministic core, but even hundreds is better than thousands. 因此,对于现有代码,一种渐进的方法是进行“确定性碎片整理”。尽可能地识别它们——无论是文件、类,甚至是单个函数中的几行代码——并开始收集它们。能够归类的确定性越多,你拥有的易于测试的功能就越多,你对程序整体行为和可靠性的信心也就越强。“难以测试”(非确定性代码)的表面积开始缩小。在足够大的代码库中,你可能永远无法将其整合为一个单一的确定性核心,但即使是几百个也比几千个要好。

Unleash the State Machine Within!

释放内在的状态机!

Every nasty mess of a codebase I’ve seen has one or more much nicer deterministic state machines locked inside. I promise you they are there, even if it’s not obvious. And once you find them, you’ll be delighted with how… 我见过的每一个糟糕透顶的代码库中,都锁着一个或多个更优雅的确定性状态机。我向你保证它们就在那里,即使并不明显。一旦你找到它们,你将会为……感到欣喜。