Verse 如何驯服 NaN:对 IEEE-754 的三处修改
几乎所有语言都照搬 IEEE-754 浮点标准,连同它最著名的怪癖:NaN 不等于任何值,包括它自己。Verse 罕见地对标准动了刀——三处修改,换来 float 的全序和确定性。这一篇讲清改了什么、为什么值得。
一、在别的语言里,NaN 是惯犯
NaN(Not a Number,非数)是浮点运算的「事故产物」——0 除以 0、给负数开平方,都会产出它。IEEE-754 标准规定:NaN 与任何值比较都不成立,包括和它自己比较。这条规定让 NaN 成了各大语言 bug 榜的常客:
在 JavaScript 里,NaN === NaN 是 false,于是 [NaN].indexOf(NaN) 返回 -1——数组里明明躺着一个 NaN,你却永远搜不到它,语言不得不另发明一个 Number.isNaN() 来补锅。排序更是重灾区:比较器遇到 NaN 时「既不大于、也不小于、也不等于」,排序算法的前提假设直接崩塌,同一组数据可能排出不同结果。C# 、Java 里也有同款陷阱,每个新手都要在这里交一次学费。
别的地方,NaN 是出了名的麻烦。它最要命的一条是「跟谁都不相等,连自己都不等于自己」:于是你往一堆数据里塞了个 NaN,回头却怎么都找不到它——因为「找它」靠的是「哪个值等于 NaN」,可没有任何值等于 NaN。排序更是重灾区:排序要靠「谁大谁小」来排,可 NaN 遇上谁都是「既不大、也不小、也不等」,排序的地基直接塌了,同一组数据可能排出不同结果。这个坑几乎每个平台的新手都踩过。
更麻烦的是,IEEE-754 里 NaN 还不止一个——标准允许无数个位模式不同的 NaN 同时存在标准允许一大堆「内部长得不一样」的 NaN 同时存在。对一门把「确定性」写进设计目标的语言来说,这一整套行为都是不可接受的:同样的代码、同样的输入,必须永远得到同样的结果。
二、Verse 的三处修改
官方 float 文档明确列出了 Verse 对 IEEE-754 的偏离,恰好三条:
其一,全语言只有一个 NaN。不管它从哪条事故现场产出,都是同一个值——没有「位模式不同的 NaN 家族」,比较和判断从此有了唯一答案。没有那种「内部长得各不相同的一整家子 NaN」,比较和判断从此有了唯一答案。
其二,NaN 等于它自己。NaN = NaN 在 Verse 里是成功的比较。别的语言里「找不到自己」的幽灵,在这里是个行为端正的普通值。
其三,NaN 参与比较,并且比所有其他 float 都大。它不再是比较运算里的「第三态」,而是稳坐数轴最右端的哨兵。
三条合在一起,买到的东西叫全序:任何两个 float 之间,大于、小于、等于三者必居其一。排序一个含 NaN 的数组不会出现未定义行为,结果永远确定;Min、Max、二分查找这些依赖比较的算法,前提假设永远成立。这和 Verse 的事务性赋值、可回滚的失败上下文是同一种精神:宁可偏离通行标准,也要保证「同样的程序永远算出同样的结果」。
三条合在一起,买到的东西叫全序:任何两个 float 之间,大于、小于、等于三者必居其一。排序一个含 NaN 的数组,结果永远是确定的,不会时好时坏;Min、Max、二分查找这些靠「比大小」吃饭的算法,前提永远成立。这和 Verse 的另外两件事是同一副脾气——赋值要么整段落地、要么整段撤回,一条线走不通就连它做过的改动一起回滚:宁可偏离通行标准,也要保证「同样的程序永远算出同样的结果」。
当然,天下没有免费的全序——「NaN 最大」有一个必须记住的推论:如果一堆数据里混进了 NaN,Max 会把它当最大值捞出来,污染你的统计结果。排行榜、最高伤害记录这类逻辑,数据入口处就该把 NaN 拦下。
好消息是,在 Verse 里拦 NaN 反而比别的语言简单——既然 NaN 等于自己,直接比较就是检测:
using { /UnrealEngine.com/Temporary/Diagnostics }
# NaN 等于它自己,所以「等于比较」就是检测手段
# TODO: NaN 常量的具体写法以 Verse API Reference 为准
if (Value = NaN):
Print("捞到一个 NaN,拦下")
else:
Print("数值正常,放行")
读图翻译:一个 Branch(if)节点判断「Value 等于 NaN 吗」——在 Verse 里这条比较能走通,就说明它真是 NaN(别的地方这招行不通,因为那里 NaN 不等于自己)。走通就 Print String「拦下」,走 else 引脚就 Print String「放行」。
对比一下:JavaScript 要 Number.isNaN(),C 要 isnan() 宏,老派技巧还有靠 x != x 反向利用怪癖的——Verse 里一次普通的相等比较就够了。顺带补一句第 5 课提过的边界:Verse 的 float 除法不可失败,官方文档把 float 的值空间描述为「有限数加 NaN」,连 ±Infinity 都没有名分;但 1.0 / 0.0 实际产出什么值,文档没有明说,下结论前请以编辑器实测为准。
对比一下:别的平台要么给你一个专门的「是不是 NaN」函数,要么靠「一个数不等于它自己」这种反着用怪癖的老把戏——Verse 里一次普通的相等比较(一个 = 判断)就够了。顺带补一句第 5 课提过的边界:Verse 的 float 除法不会走不通,官方文档把 float 能取的值描述成「有限的数,外加 NaN」,连正负无穷都没给名分;但 1.0 / 0.0 实际产出什么值,文档没有明说,下结论前请以编辑器实测为准。
在 Verse 里,比较表达式 NaN = NaN 的结果是?
一组玩家伤害数据里混进了一个 NaN,对它们求 Max 会得到什么?
来源
本文整理自官方文档:Float in Verse(UEFN 文档)↗ · Float in Verse(Fortnite 文档)↗