问题描述
我目前对类方法 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 的 asMinimalDecimalFraction
和 asApproximateFraction
对我来说最有意义,因为它们在数学上产生了更“精确”的结果。我不明白其他人。为什么具有大分子和分母但显然不太精确的值的分数是对 asExactFraction
的响应?为什么我想要这样的回应?为什么在 Pharo 中我选择 asFraction
或 asTrueFraction
似乎并不重要?为什么会有这些变体?
如果我想将一个浮点数表示为一个分数,我想我会想要基于构成分子和分母的整数的精度等级,或者可能基于最大分母的近似近似值。
>我查看了 Bluebook,它几乎没有提到 asFraction
,也没有提到任何变体。
解决方法
Float
是一种编码数字的数据结构,无论我们如何看待或解释它,从数学上讲,它只能是有理量(即整数或分数)。这种编码适用于 CPU 高速执行的算术运算。我们付出的代价是编纂没有展示它所代表的分子和分母。方法 Float >> #asTrueFraction
用这些数字回答,换句话说,它解码 Float
实例中包含的位,并用它编码的实际分数回答。
您必须了解的是,当您编写 0.001
时,您是在告诉编译器创建一个近似分数 Float
的 1/1000
。如果 CPU 使用十进制而不是二进制表示,这将类似于要求它使用有限数量的小数位对 1/3
进行编码,这不可避免地导致 0.33333..3
,对于某些最大位数 { {1}}。在分母不是 3
的幂的情况下,CPU 必须解决类似的问题并最终逼近提供的数量,以便它适合分配给 2
的位数。方法 Floats
反转该过程并揭示近似值的确切值,该值 #asTrueFraction
隐藏在它打印其实例的方式后面。
在 Pharo 中,Float
与 Float >> #asFraction
相同,因此没有区别。
Float >> #asTrueFraction
中的注释非常清楚,它会给出您通常期望的内容,即转换回为浮点数时将等于 self 的最短小数部分。
最后,Float >> #asMinimalDecimalFraction
使用某种算法来生成可接受的接收器近似值。
出于显而易见的原因,GNU 的 asFraction
和 Pharo 的 asMinimalDecimalFraction
和 asApproximateFraction
对我来说最有意义,因为它们在数学上产生了更“精确”的结果。
相反,他们执行的操作是寻找输入的近似值。 但是他们收到的输入实际上并不是数字 0.001,即使这看起来是你写的——而且这些方法中的任何一种都无法知道你最初写的是什么。
因此,有些方法会准确返回给定的数字(以不同的表示形式),而另一些方法会返回与您最初编写的文本相吻合的近似值(如果令人困惑!)。
稍微改写代码可能会有所帮助,以便您了解近似值的实际发生位置。 让我们首先关注 GNU Smalltalk。
x := '0.001' asNumber.
y := x asExactFraction.
在这个片段中,'0.001' asNumber
是唯一可以做任何近似的操作:而不是返回一个表示数字 0.001 的 Float
实例(事实上,没有这样浮!),它返回一个Float
表示的最接近(IEEE 754 binary64)浮点数,其可以进行各种写为1152921504606846976分之1152921504606847,或作为0.001000000000000000020816681711721685132943093776702880859375,或作为为0x1 .0624dd2f1a9fcp−10 以最方便的形式准确写入二进制浮点数。
只需编写 0.001
即可获得相同的结果:Smalltalk 将自动舍入到最接近的浮点数。
我明确地将其写为 '0.001' asNumber
以明确表示这是返回您编写的数字 0.001 的近似值的操作。
然后 y := x asExactFraction
将 ? 设置为一个 Fraction
实例,表示完全相同的数字; Pharo 中的 y := x asTrueFraction
也是如此。
号码还是1152921504606847/1152921504606846976; asExactFraction
将永远返回分母中除了 2 的幂以外的任何数字(至少,不会返回用于存储二进制浮点数的类)。
如果相反,您评估(在 GNU Smalltalk 中)
z := x asFraction.
那么你在 ? 中得到的是一个 Fraction
实例,它代表了最简单有理数,它四舍五入为 ?——非常粗略地说,区间 [? − ulp(? )/2,? + ulp(?)/2],其中 ulp(?) ≈ 2−52? 是 ? 的浮点表示的最低有效数字的大小(有警告)在间隔的边缘周围以及当 ? 等于 2 的幂时)。
这里区间内“最简单”的有理数是分母最小的有理数。
这个对 ? 的近似是通过扩展 ? 的连分数表示直到第一个舍入到 ? 的收敛获得的。1
这可能(尽管我没有仔细观察以验证)与您使用 Pharo's definition of asApproximateFraction
得到的结果相同。
相反,Pharo's asMinimalDecimalFraction
不返回最简单的有理数;相反,它只考虑分母中 10 = 2⋅5 次幂的有理数,并返回将四舍五入为 ? 的分子最小的那个。
总结:
-
x := '0.001' asNumber
集?于Float
表示(IEEE 754 binary64)实例浮点数最接近0.001,这是1152921504606846976分之1152921504606847= 0.001000000000000000020816681711721685132943093776702880859375 = 0x1.0624dd2f1a9fcp-10;您可以通过编写x := 0.001
获得相同的效果,但这会使近似值的发生变得更加模糊 -
y := x asExactFraction
在 GNU Smalltalk 中,或y := x asTrueFraction
或y := asFraction
在 Pharo 中,将 ? 设置为一个Fraction
实例,表示完全相同的数字 ?
GNU Smalltalk 中的 -
z := x asFraction
或 Pharo 中的z := x asApproximateFraction
将 ? 设置为一个Fraction
实例,表示将四舍五入为 ? 的最简单的有理数 >
Pharo 中的 -
w := x asMinimalDecimalFraction
将 ? 设置为Fraction
实例,表示具有最短十进制扩展的数字,该数字将四舍五入为 ?;如果您想以十进制记数法写入浮点数,并确保返回相同的数字,而无需写入过多的数字,则可以使用此功能
(如您所见,GNU Smalltalk 和 Pharo 在 asFraction
是否应该返回近似值方面存在分歧:在 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,芝加哥大学出版社,1964 年,第 6 节“Convergents as best approximations”,用于进一步讨论连分数收敛作为最佳有理近似。
连分数是数学中一个美丽的角落,但可悲的是在现代教育中被忽视了!
虽然其他答案深入探讨了为什么分数 1/1000
不等于 64 位二进制浮点数 0.001
,但这里的答案略有不同:
0.001 printStringBase: 2
"=>" '1.00000110001001001101110100101111000110101001111111e-10'
这就是0.001
真正在引擎盖下的样子,作为有限精度的二进制浮点数(仅限 64 位) )。这就是为什么它不等于1/1000
:
1/1000 = 0.001
"=>" false
如果你想要精确小数和无限精度,你需要告诉系统。像 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 位,而 double 只提供 53 位有效数)。虽然相等的结果是真的,因为在测试之前整数值被转换为double。
大多数 Smalltalk 方言也是如此,在 2000 年初,1/10 = 0.1
确实回答为 true,尽管这两个数字的值不完全相同...幸运的是,我们采用了更明智的 Scheme 语言策略,因为: 准确比较。
现在我们有了关于平等的契约,我们可以表达关于转换的进一步契约。第一:
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 值(因此,它可以是在阅读/评估/打印活动中无所畏惧地使用)。
在 Squeak 中,打印 Float 精确十进制值的方法是这样的:
aFloat printShowingMaxDecimalPlaces: Float emin - Float precision + 1.
这是因为可以用双精度表示的 2 的最小幂是
(2 raisedTo: Float emin - Float precision + 1) = Float fminDenormalized.
而且因为 1/2^n 需要打印小数点后 n 位(它是 5^n/10^n)。
虽然连分数是个好东西,但我不知道有任何关于 asApproximateFraction
的合同。它可能会也可能不会四舍五入到相同的浮点数。问题是我们在哪里停止递归?
历史注释:转换 Integer>>asFloat
和 Fraction>>asFloat
将在现代 Smalltalk 中回答最接近其确切值的 Float,至少在 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 实现都应该关心这些特性(合约)。