了解 Float>>asFraction 及其变体

Understanding Float>>asFraction and its variants

我目前对 class 方法 Float>>asFraction 及其各种形式提供的响应感到困惑。这里有几个例子:

GNU Smalltalk

0.001 asFraction
1/1000
0.001 asExactFraction
1152921504606847/1152921504606846976

法罗

0.001 asFraction
1152921504606847/1152921504606846976
0.001 asTrueFraction
1152921504606847/1152921504606846976
0.001 asMinimalDecimalFraction
1/1000
0.001 asApproximateFraction
1/1000

出于明显的原因,GNU 的 asFraction 和 Pharo 的 asMinimalDecimalFractionasApproximateFraction 对我来说最有意义,因为它们在数学上产生了更“精确”的结果。我不明白其他人。为什么分子和分母都很大但数值显然不太精确的分数会成为对 asExactFraction 的响应?为什么我想要那种回应?为什么在 Pharo 中选择 asFractionasTrueFraction 似乎并不重要?为什么会有这些变体?

如果我想将浮点数表示为分数,我想我可能希望基于构成分子和分母的整数的精度 class 或基于最大分母。

我查看了蓝皮书,其中很少提及 asFraction,也没有提及任何变体。

A Float 是一种编码数字的数据结构,无论我们如何看待或解释它,从数学上讲,它只能是有理数(即整数或分数)。这种编码适用于 CPU 高速执行的算术运算。我们付出的代价是编纂没有展示它所代表的分子和分母。 Float >> #asTrueFraction 方法用这些数字回答,换句话说,它解码包含在 Float 实例中的位,并用它编码的实际分数回答。

您必须了解的是,当您编写 0.001 时,您是在告诉编译器创建一个接近分数 1/1000Float。如果 CPU 使用十进制而不是二进制表示,这将类似于要求它使用有限数量的小数位对 1/3 进行编码,这将不可避免地导致 0.33333..3,对于某些最大值位数 3。在分母不是 2 的幂的情况下,CPU 必须解决类似的问题并最终近似提供的数量,使其适合分配给 [=20] 的位数=].方法 #asTrueFraction 反转该过程并显示近似值的确切值,Float 隐藏在其打印实例的方式后面。

在 Pharo 中,Float >> #asFractionFloat >> #asTrueFraction 相同,因此没有区别。

Float >> #asMinimalDecimalFraction中的注释非常清楚,它会给出您通常期望的结果,即当转换回 asFloat 时等于 self 的最短小数分数.

最后,Float >> #asApproximateFraction 使用某种算法生成可接受的接收器近似值。

For obvious reasons, GNU's asFraction and Pharo's asMinimalDecimalFraction and asApproximateFraction make the most sense to me since they are producing, mathematically, more "exact" results.

相反,他们执行的操作是找到输入的近似值。 但是他们收到的 input 实际上不是数字 0.001,即使这似乎是你写的——而且这些方法中的任何一个都无法知道你最初是什么写了。

因此,有些方法 return 与给定的数字完全相同(以不同的表示形式),而其他方法 return 近似值偶然(如果令人困惑!)与您最初编写的文本一致。


稍微改写一下代码可能会有所帮助,这样您就可以看到真正发生近似值的地方。 让我们先关注 GNU Smalltalk。

x := '0.001' asNumber.
y := x asExactFraction.

在此片段中,'0.001' asNumber 是唯一进行任何近似的操作: 而不是 returning 表示 Float 的实例数字 0.001(事实上,没有这样的浮点数!),它 return 是一个 Float 代表 最近的 (IEEE 754 binary64) 浮点数,可以以不同的方式编写为1152921504606847/1152921504606846976,或0.001001000000000000000000000020816668171717172168516851329430943093776702888808880859375,AS 0B24DIRTIENT INFORTIENT INFOR1.BORTIENT INFORITIENT INFOR1

你只需要写0.001就可以得到相同的结果:Smalltalk 会自动舍入到最接近的浮点数。 我明确地把它写成 '0.001' asNumber 以明确这是 return 对你写的数字 0.001 的近似值的操作。

然后 y := x asExactFraction 设置为 Fraction 代表 完全相同的 数字的实例; Pharo 中的 y := x asTrueFraction 也是如此。 号码还是1152921504606847/1152921504606846976; asExactFraction 永远不会 return 分母中除了 2 的幂之外的任何数字(至少, class 不会用于存储二进制浮点数-点数).


相反,如果您评估(在 GNU Smalltalk 中)

z := x asFraction.

那么你得到的是一个 Fraction 实例,表示 最简单的 有理数舍入到 — 非常粗略地,区间 [ - ulp 中的最简单的有理数()/2, + ulp()/2],其中 ulp() ≈ 2−52 是浮点表示的最低有效位的大小(周围有警告间隔的边缘和何时等于二的幂)。 这里区间内“最简单”的有理数是分母最小的有理数。 通过扩展 的连分数表示直到第一个收敛到 .1

获得此近似值

这可能(尽管我没有仔细观察以验证)与您使用 Pharo's definition of asApproximateFraction 得到的结果相同。 相反,Pharo's asMinimalDecimalFraction 不是 return 最简单的有理数;相反,它只考虑分母中 10 = 2⋅5 次幂的有理数,而 returns 是具有最小分子的数,将四舍五入为 .


总结:

  • x := '0.001' asNumber sets to a Float instance representing the (IEEE 754 binary64) floating-point number nearest to 0.001, which is 1152921504606847/1152921504606846976 = 0.001000000000000000020816681711721685132943093776702880859375 = 0x1.0624dd2f1a9fcp−10;您可以通过编写 x := 0.001 获得相同的效果,但这会使正在发生的近似值变得更加模糊
  • GNU Smalltalk 中的
  • y := x asExactFraction,或 Pharo 中的 y := x asTrueFractiony := asFraction,设置为代表 完全相同数字的 Fraction 实例 作为
  • GNU Smalltalk 中的
  • z := x asFraction 或 Pharo 中的 z := x asApproximateFraction 设置为一个 Fraction 实例,代表 最简单的有理数 将四舍五入为
  • Pharo 中的
  • w := x asMinimalDecimalFraction 设置为一个 Fraction 实例,表示具有 最短小数扩展 的数字,该数字将四舍五入为 ;如果你想用十进制表示法写浮点数,你可以使用这个,并确保你得到相同的数字而不写比你必须写的更多的数字

(如您所见,GNU Smalltalk 和 Pharo 在 asFraction 是否应该 return 一个近似值上存在分歧:在 GNU Smalltalk 中是,而在 Pharo 中不是。 这很不幸,因为这是两人唯一共享的名字!)


为了好玩,请在 Pharo 中尝试以下示例:

3.141592653589793 asApproximateFractionAtOrder: 1
3.141592653589793 asApproximateFractionAtOrder: 2
3.141592653589793 asApproximateFractionAtOrder: 3
3.141592653589793 asApproximateFractionAtOrder: 4
3.141592653589793 asApproximateFractionAtOrder: 5
3.141592653589793 asApproximateFraction
3.141592653589793 asMinimalDecimalFraction
3.141592653589793 asTrueFraction

1.618033988749895 asApproximateFractionAtOrder: 1
1.618033988749895 asApproximateFractionAtOrder: 2
1.618033988749895 asApproximateFractionAtOrder: 3
1.618033988749895 asApproximateFractionAtOrder: 4
1.618033988749895 asApproximateFractionAtOrder: 5
1.618033988749895 asApproximateFraction
1.618033988749895 asMinimalDecimalFraction
1.618033988749895 asTrueFraction

看看您是否注意到有关输出的任何内容——也许您会认出一些分数;查看它们与真实分数的绝对和相对误差有多远;看看分母有多大


1 这就是 GNU Smalltalk's definition of asFraction 目前所做的。 从技术上讲,文档对近似的性质没有任何承诺,但这是 Fraction 最自然的方法,因为它提供了独立于任何基数选择的最佳有理近似。 见 A. Ya。 Khinchin, Continued Fractions, University of Chicago Press, 1964, §6 “Convergents as best approximations” 进一步讨论连分数收敛作为最佳有理逼近。 连分数是数学的一个美丽角落,但遗憾的是在现代教育中被忽视了!

虽然其他答案深入探讨了 为什么 分数 1/1000 不等于 64 位二进制浮点数 0.001,但这里的答案略有不同:

0.001 printStringBase: 2
"=>" '1.00000110001001001101110100101111000110101001111111e-10'

这就是 0.001 真正 在幕后的样子,作为 binary float有限 精度(仅限 64 位)。这就是为什么 等于 1/1000:

1/1000 = 0.001
"=>" false

如果你想要 exact 精度 unlimited 的小数,你需要告诉系统。像 0.001s 这样的小数确实正好等于分数 1/1000:

0.001s asFraction
"=>" (1/1000)

1/1000 = 0.001s
"=>" true

我们不经常使用小数的原因是它们效率较低 - 64 位二进制浮点数数学是在硬件中实现的,精确数学是在软件中实现的,这使得速度慢了几个数量级。

除了已经非常出色的答案之外,我唯一想补充的是突出一些合同。

第一个约定,现代Smalltalk中的相等、不等和比较操作总是基于比较精确值。至少,在 Dolphin、gnu、Pharo、Squeak 上是这样。

并非总是如此。以此 C 代码为例:

int64_t i=1<<60+1;
double d=(double) i;
printf("%d\n',d==i);

这两个数字不具有相等的值(它们不能,因为整数需要 61 位,而双精度仅提供 53 位有效数字)。虽然相等的结果为真,但因为整数值在测试之前被转换为双精度。

这也是大多数 Smalltalk 方言的情况,在 2000 年初,1/10 = 0.1 确实回答了 true,尽管这两个数字的值并不完全相同......幸运的是,我们采用了更明智的方案策略语言自:完全比较。

现在我们有了平等契约,我们可以表达更多关于转换的契约。第一:

aFloat asTrueFraction = aFloat.
"which means that they share the exact same value"
"replace with asExactFraction in gst"

第二份合同是这样的:

aFloat asMinimalDecimalFraction asFloat = aFloat.
"Though the decimal fraction may differ, it will always convert back to same float"

asMinimalDecimalFraction 将回答将四舍五入到相同浮点数的最短小数。它与快速准确地打印浮点数非常相关,实际上共享相同的算法。这与Python中的repr完全一样。另见 Squeak/Pharo 中的 absPrintExactlyOn:。请注意,这不是一个好的名称,因为它不打印 EXACT 值,而是打印 SHORTEST 值,该值将四舍五入为相同的浮点数(因此,它可以在 read/eval/print 活动中无所畏惧地使用)。

在 Squeak 中,打印 Float 的精确十进制值的方法是这样的:

aFloat printShowingMaxDecimalPlaces: Float emin - Float precision + 1.

这是因为可以用双精度表示的最小二乘方是

(2 raisedTo: Float emin - Float precision + 1) = Float fminDenormalized.

又因为1/2^n需要打印小数点后n位(即5^n/10^n)。

虽然连分数是个好东西,但我不知道有任何关于 asApproximateFraction 的合同。它可能会也可能不会舍入到同一个 Float。问题是我们在哪里停止递归?

历史记录:转换 Integer>>asFloatFraction>>asFloat 将在现代 Smalltalk 中回答最接近其精确值的浮点数,至少在 gst Squeak/Pharo 中是这样。 2000 年初情况并非如此,也许现在每一种方言都不是这种情况。写成合同:

(aFraction - aFraction asFloat asTrueFraction) abs <= (aFraction - aFraction asFloat predecessor asTrueFraction) abs and: [
(aFraction - aFraction asFloat asTrueFraction) abs <= (aFraction - aFraction asFloat successor asTrueFraction) abs] 

未能提供此类基本属性会破坏表达更高级别的清晰合同的机会。当您尝试检查和理解发生了什么时,它也可能会产生很大的误导。

现在每个 Smalltalk 实现都应该关心这些特性(契约)。