FAQ: Why isn’t mutable a subtype of immutable, or vice versa?
FAQ: Why isn’t mutable a subtype of immutable, or vice versa?
常见问题:为什么可变类型不是不可变类型的子类型,反之亦然?
17 September 2026 2026年9月17日
I remember the moment when I learned about immutability. It changed everything. — Denis Defreyne 我记得我第一次了解“不可变性”(immutability)的那一刻。它改变了一切。—— Denis Defreyne
Periodically, in various programming language forums, the discussion comes up of why a certain language doesn’t provide the immutable and mutable variants of some data structure as subtypes or supertypes of one another. 在各种编程语言论坛中,经常会出现这样的讨论:为什么某种语言不将数据结构的不可变和可变变体设计为彼此的子类型或父类型。
Now, it’s not impossible to do this, but it’s actually not formally correct to do so, and by doing so you’ll lose at least some of the type checking guarantees your language can usually make for you. 当然,这样做并非不可能,但在形式逻辑上是不正确的。一旦这样做,你至少会失去一部分语言通常能为你提供的类型检查保证。
To understand why this doesn’t work, you have to remember the definition of a subtype. Namely, Liskov’s substitution principle: a type S is a subtype of T if a value of type S can be used in every context where a value of type T is expected. 要理解为什么行不通,你必须回顾“子类型”的定义。即里氏替换原则(Liskov’s substitution principle):如果类型 S 的值可以在任何预期使用类型 T 的值的地方被使用,那么 S 就是 T 的子类型。
As usual when dealing with formal matters, this definition is strictly interpreted. Every really does mean every, not just most. (You might have learned the substitution principle as a mere recommended design pattern for OO classes, but formally speaking a true subtype has to fulfil this criterion.) 像处理所有形式化问题一样,这个定义必须被严格解读。“每一个”确实意味着“每一个”,而不仅仅是“大多数”。(你可能在面向对象类的设计模式中将此原则仅仅视为一种推荐做法,但从形式上讲,真正的子类型必须满足这一准则。)
A static type system which supports subtyping will have to prove this for you in order to pass your program through its type checker. 支持子类型化的静态类型系统必须为你证明这一点,才能让你的程序通过类型检查。
To illustrate this, let’s take the simplest compound data structure imaginable: the humble pair. Here are our operations on an immutable version. 为了说明这一点,让我们以最简单的复合数据结构为例:朴素的“对”(pair)。以下是我们对不可变版本定义的操作:
(cons a d) construct a new pair containing a and d and return it (cons a d) 构造一个包含 a 和 d 的新对并返回它
(car p) return the value of a provided when the pair p was constructed (car p) 返回构造对 p 时提供的 a 的值
(cdr p) return the value of d provided when the pair p was constructed (cdr p) 返回构造对 p 时提供的 d 的值
That’s it! Our mutable variant adds two new operations: 就是这样!我们的可变变体增加了两个新操作:
(set-car! p a) change the value of a within the pair p (set-car! p a) 修改对 p 中 a 的值
(set-cdr! p d) change the value of d within the pair p (set-cdr! p d) 修改对 p 中 d 的值
(And a new constructor, but we’ll deal with that below.) (还有一个新的构造函数,我们稍后处理。)
Now, it should be obvious that an immutable pair can’t be provided where a mutable pair is expected. A place that needs a mutable pair will presumably try to use of these two operations on it, which aren’t defined on an immutable pair, so there will be a typing error. 现在显而易见的是,在需要可变对的地方不能提供不可变对。需要可变对的地方很可能会尝试对其使用上述两个操作,而这些操作在不可变对上并未定义,因此会产生类型错误。
But why couldn’t it be the other way around? All of the operations provided by an immutable pair are also provided by a mutable pair, so it seems like we should be able to use a mutable pair wherever an immutable pair is expected. 但为什么不能反过来呢?不可变对提供的所有操作在可变对中也都有,所以看起来我们似乎应该能够在任何需要不可变对的地方使用可变对。
The reason is more subtle. The substitution principle extends beyond the set of operations (methods) a type provides to the implicit contract which comes with those operations. 原因更为微妙。替换原则不仅涵盖了类型提供的一组操作(方法),还涵盖了这些操作所附带的隐式契约。
When we take the car or cdr of an immutable pair, we can depend on a contract which says the result will always be the same every time we call it on that pair. 当我们对一个不可变对执行 car 或 cdr 操作时,我们可以依赖一个契约:即每次调用该操作时,结果始终保持不变。
This contract means that we can, for example, safely calculate the hash value of the pair based on its contents, store it away in another data structure, and know that it won’t be different when we recalculate it later to try to retrieve it. (In other words, immutability is a prerequisite for hash consing!) 这个契约意味着,例如,我们可以根据内容安全地计算该对的哈希值,将其存储在另一个数据结构中,并确信稍后重新计算以检索它时,结果不会发生变化。(换句话说,不可变性是哈希共用(hash consing)的前提!)
Because of this, immutable and mutable pairs have to be completely different types: 正因如此,不可变对和可变对必须是完全不同的类型:
(icons a d) construct a new immutable pair containing a and d and return it (icons a d) 构造一个包含 a 和 d 的新不可变对并返回它
(icar i) return the value of a provided when the immutable pair i was constructed (icar i) 返回构造不可变对 i 时提供的 a 的值
(icdr i) return the value of d provided when the immutable pair i was constructed (icdr i) 返回构造不可变对 i 时提供的 d 的值
(mcons a d) construct a new mutable pair containing a and d and return it (mcons a d) 构造一个包含 a 和 d 的新可变对并返回它
(mcar m) return the value of a provided when the mutable pair m was constructed (mcar m) 返回构造可变对 m 时提供的 a 的值
(mcdr m) return the value of d provided when the mutable pair m was constructed (mcdr m) 返回构造可变对 m 时提供的 d 的值
(set-mcar! m a) change the value of a within the mutable pair m (set-mcar! m a) 修改可变对 m 中 a 的值
(set-mcdr! m d) change the value of d within the mutable pair m (set-mcdr! m d) 修改可变对 m 中 d 的值
It’s a typing error if an i is a mutable pair or m is an immutable pair. 如果 i 是可变对或 m 是不可变对,则会产生类型错误。
Objection: But I’m not mutating it and I really don’t care about the contract of immutability for my use case 反对意见:但我并没有修改它,而且在我的用例中,我真的不在乎不可变性的契约。
Because the two types of pair don’t form a subtype hierarchy, they have to be completely separate types and have separate sets of operations defined on them. 由于这两种对类型不构成子类型层次结构,它们必须是完全独立的类型,并定义各自独立的操作集。
Fortunately, many languages offer one or another mechanism for ad hoc polymorphism, where the same operations can be defined on multiple types even if they don’t form a hierarchy. 幸运的是,许多语言提供了一种或多种“特设多态”(ad hoc polymorphism)机制,即使多种类型不构成层次结构,也可以在它们上面定义相同的操作。
As a Schemer, I tend to think that this a bad idea in a dynamically typed context, because it makes the reasoning you have to do about the flow of data types vastly more complicated, and thus more difficult to get right. 作为一名 Scheme 程序员,我倾向于认为在动态类型环境下这是一个坏主意,因为它会使你对数据类型流向的推理变得极其复杂,从而更难保证正确性。
In practice, most languages do offer some mechanism for this, whether dynamically typed or statically typed. Let’s consider the statically typed case first. 实际上,大多数语言确实为此提供了一些机制,无论是动态类型还是静态类型。让我们先考虑静态类型的情况。
Wadler and Blott introduced a mechanism for formally reasoning about ad hoc polymorphism and ensuring the type checker can actually prove it sound. Wadler 和 Blott 引入了一种机制,用于对特设多态进行形式化推理,并确保类型检查器能够证明其可靠性。
In their terminology, mutable and immutable pairs are different types, but both can belong to a common pair type class whose operations are the original car and cdr we defined above. 用他们的术语来说,可变对和不可变对是不同的类型,但它们都可以属于一个共同的“对类型类”(pair type class),其操作就是我们上面定义的原始 car 和 cdr。
On mutable pairs, these refer to the underlying mcar and mcdr operations, and on immutable pairs to the icar and icdr operations. 在可变对上,这些操作指向底层的 mcar 和 mcdr;在不可变对上,则指向 icar 和 icdr。
This is still formally sound because the pair type class defines a new contract that says nothing about mutability. 这在形式上仍然是可靠的,因为“对类型类”定义了一个新的契约,该契约并未涉及可变性。
In a proper implementation of type classes, the type system will stop you trying to use the mutators in a method where the most you defined about the input type to your function is that they are some kind of pair, mutable or immutable. 在类型类的正确实现中,如果你的函数输入类型仅被定义为“某种对”(无论是可变的还是不可变的),类型系统会阻止你在该方法中使用修改器(mutators)。
It won’t prevent you from using the car and cdr operations expecting them to be immutable when they might not be – but it does let you choose the granularity explicitly both ways, declaring the input type to your function as either a mutable pair or immutable pair or either, depending on the contract your function actually expects. 它不会阻止你使用 car 和 cdr 操作(即使你预期它们是不可变的,但实际上可能并非如此),但它确实让你能够显式地选择粒度,根据函数实际预期的契约,将输入类型声明为可变对、不可变对或两者皆可。
A subtype relationship would only allow one way but not the other: you could declare your function as allowing immutable pairs, but potentially incorrectly implicitly including mutable pairs too; or, if it were the other way around, as allowing mutable pairs but p… 子类型关系只允许单向而不允许双向:你可以声明函数允许不可变对,但这可能会错误地隐式包含可变对;或者反过来,声明允许可变对,但……