two case studies of NaN

Two Case Studies of NaN

Two Case Studies of NaN (2026-07-09)

IEEE-754 NaN is weird. And because of that, it’s often accidentally left unaccounted for. I found two instances of this leaking into programming language design. That is, the semantics of these languages hold implicit assumptions which break with NaN.

IEEE-754 标准中的 NaN(Not-a-Number)非常古怪。正因如此,它经常在无意中被忽略。我发现了两个 NaN 渗透进编程语言设计的案例。也就是说,这些语言的语义中包含了一些在遇到 NaN 时会失效的隐性假设。

Case Study 1: Python

>>> from math import nan
>>> nan == nan
False
>>> [nan] == [nan]
True

As an optimization, when comparing lists for equality, elements are first compared by identity. They’re only checked for equality if their identities are unequal. In general, it’s assumed that an object will always compare equal to itself. This is explicitly noted in the Python 3 reference manual: “User-defined classes that customize their comparison behavior should follow some consistency rules, if possible: Equality comparison should be reflexive. In other words, identical objects should compare equal: x is y implies x == y.”

作为一种优化手段,在比较列表是否相等时,Python 会先比较元素的标识(identity)。只有当标识不相等时,才会进一步检查它们的值是否相等。通常情况下,人们假设一个对象总是等于它自身。Python 3 参考手册中明确指出:“如果可能,自定义比较行为的用户定义类应遵循一些一致性规则:相等性比较应该是自反的。换句话说,相同的对象应该相等:x is y 意味着 x == y。”

This isn’t true for NaN. And that’s fine, but because the assumption does hold for pretty much everything else, design choices are made which leave NaN unaccounted for (or at least, give NaN very weird behavior). To be clear, I’m not necessarily saying that Python’s behavior here is wrong, or that this optimization is bad because NaN behaves weirdly. But I think it’s at least worth pointing out.

对于 NaN 来说,这一规则并不成立。这本身没问题,但由于该假设在几乎所有其他情况下都成立,设计者在做决策时往往会忽略 NaN(或者至少导致 NaN 表现出非常奇怪的行为)。需要明确的是,我并不是说 Python 的这种行为是错误的,也不是说因为 NaN 的怪异表现就认为这种优化很糟糕。但我认为,指出这一点至少是有价值的。

Case Study 2: Lua

Numerical for-loops in Lua look like this: for i = 1, 10 do stuff() end. The way this is supposed to work is that i starts with the initial value (here that’s 1), and each iteration it increments by the step (optional third operand; defaults to 1), until the new value is greater than the limit (10), at which point the loop terminates. So this loop iterates 10 times, from 1 to 10 (inclusive).

Lua 中的数值型 for 循环看起来是这样的:for i = 1, 10 do stuff() end。其工作原理是:i 从初始值(此处为 1)开始,每次迭代增加步长(可选的第三个操作数,默认为 1),直到新值大于上限(10)时循环终止。因此,这个循环会从 1 到 10(包含 10)执行 10 次。

So what happens when NaN (0/0) is thrown into the mix? Here’s the behavior in the reference implementation (PUC-Rio Lua):

那么,当 NaN (0/0) 被引入其中时会发生什么?以下是参考实现(PUC-Rio Lua)中的表现:

-- executes once
for i = 0/0, 10 do print(i) -- nan end

-- executes once
for i = 0/0, 0/0 do print(i) -- nan end

-- never executes
for i = 1, 10, 0/0 do print(i) end

-- executes once
for i = 10, 1, 0/0 do print(i) -- 10.0 end

Read through that code and try to figure out what’s going on. Every single one of those results is extremely unintuitive. Here’s what’s happening: the first two only execute once because the for-loop check is implemented as limit < init, but subsequent iterations check idx <= limit. So NaN passes the first test, but not the second test. The latter two examples show that NaN is always treated as negative when used as the step, even if you do math.abs(0/0). This is because the check for whether the step is positive is 0 > step, which is of course always false for NaN.

读读这段代码,试着弄清楚发生了什么。每一个结果都极其反直觉。实际情况是:前两个循环只执行一次,因为 for 循环的初始检查实现为 limit < init,但后续迭代检查的是 idx <= limit。因此,NaN 通过了第一次测试,却没通过第二次。后两个例子表明,当 NaN 被用作步长时,它总是被视为负数,即使你使用了 math.abs(0/0) 也是如此。这是因为判断步长是否为正数的逻辑是 0 > step,而对于 NaN 来说,这显然永远为假。

None of this is documented btw, so this is likely a genuine oversight. But that’s really interesting: using NaN causes the specific comparison operator used by the implementation to leak into the interpreter’s behavior. The simple solution here is to just check for and disallow NaN in numerical for-loops. Lua already raises an error when 0 is used as the step, so it’s interesting that there’s no such check for NaN.

顺便提一下,这些都没有被记录在文档中,所以这很可能是一个真正的疏忽。但有趣的是:使用 NaN 会导致实现中使用的特定比较运算符“泄漏”到解释器的行为中。这里简单的解决方案是在数值 for 循环中检查并禁止 NaN。Lua 在步长为 0 时已经会报错,所以没有针对 NaN 的检查确实很有意思。

Conclusion

I guess Chris Siebenmann also previously wrote about the weird behavior of NaNs as map keys in Go, so I guess that makes 3 case studies of NaN’s weirdness. NaN is weird. And that means other things behave weirdly with it.

我想 Chris Siebenmann 之前也写过关于 NaN 作为 Go 语言 map 键时的怪异行为,所以这大概构成了 NaN 怪异性的三个案例。NaN 很怪,这意味着其他事物在与它交互时也会表现得很怪。