【问题标题】:Int overflow coerced to 0?int溢出强制为0?
【发布时间】:2023-03-17 06:27:02
【问题描述】:

如果我这样写一个递归阶乘:

fact 0 = 1
fact n = n * (fact (n-1))

ghci 告诉我它的类型为 (Eq p, Num p) => p -> p

我希望 Haskell 很聪明,并且能够快速实现这个阶乘。因此,如果我写:fact (10 :: Int) 使用Ints 时,所有这些都将是快速数学,如果我写fact (1000 :: Integer),则使用Integers 时,所有或部分计算将是慢速数学。

ghci 在这里很有帮助:

λ: :t fact (2 :: Int)
fact (2 :: Int) :: Int
λ: :t fact (2 :: Integer)
fact (2 :: Integer) :: Integer

我有两个问题:

Q1 : 当我运行时会发生什么

λ: fact (10 :: Int)
>3628800
λ: fact (40 :: Int)
>-7060926325325235253252...
λ: fact (66 :: Int)
>0

很明显,Haskell 无法为较大的参数计算正确的结果,因为它会溢出。所以我对fact 40 的结果并不感到惊讶,我得到了与fact 65 相似的结果。但是从fact 66开始,函数总是返回0。怎么会呢?

Q2:给定一个对 fact 的调用,所有对 fact 的递归调用都将使用相同的类型是否正确,即使在某些调用中 Int 可以替换为 Integer (即整个计算速度较慢)如果编译器能够在运行时在每次调用的基础上在 Int 和 Integer 之间做出决定,那将是不可能的)。

【问题讨论】:

  • 因为0 * 任何其他号码都是0
  • 我想这是一个很好的观点,如果fact 66 为 0,那么所有向上的东西也将为 0。但是,fact 66 为零有一个重要的原因吗?如果我计算 toInteger((fact 65 :: Int)) * 66 我会得到一个我不明白为什么它会是 0 作为 Int 的值。
  • 这是因为Int 具有固定数量的位,并且每次都省略了较高的位。在某些时候,该值将为零。特别是因为我们经常乘以 2 的幂,因此我们将这些位“移出”最低位。
  • 是的,有一个原因:Numeric.showHex (product [1..66 :: Integer]) "" "10b395943e6086f3f811b267cc58286d7c1eb06b9a4de25bacad2cd8d5dc2b0000000000000000" 的最后 64 位
  • @ARRG 比较 toInteger (fact 65 :: Int) * 66(2^64) * 33 :: Integer 可能会有所启发。

标签: haskell


【解决方案1】:

Int 是一个 64 位整数(至少在您的平台上),因此您看到的结果是模 2^64(然后解释为有符号)。 fact 66 是第一个恰好是 2^64 倍数的阶乘,因此 fact 66 `mod` 2^64 为 0。由于每个阶乘都是先前阶乘的倍数,所有较大的阶乘也是 2^64 的倍数。

给定一个对 fact 的调用,所有对 fact 的递归调用都使用相同的类型是否正确

是的,* 的类型是Num a => a -> a -> a,所以* 的两个操作数必须具有相同的类型,这也是其结果的类型。因此,在n * fact (n-1) 中,fact (n-1) 必须与n 具有相同的类型(并且n-1 也与n 具有相同的类型,因为-* 具有相同的类型)。

【讨论】:

  • 也可表示为:sum [Data.Bits.countTrailingZeros n | n <- [1 .. 66 :: Int]],计算 2 的幂,此时达到 64。
【解决方案2】:

我得到了与fact 65 相似的结果。但是从fact 66 开始,函数总是返回0。怎么会?

如果给定数字的fact 产生0,那么对于具有更大索引的每个其他fact,结果也将是0,因为n * 0 仍然是0。像Int 这样的类型具有固定的位数(例如,Int16 为 16 位)。如果我们将两个较大的数字相乘,产生一个超过 16 位的数字,则忽略较高的位。所以例如:

  fact 7 |    5040 |   0001 0011 1011 0000
x      8 |  x    8 | x                1000
------------------------------------------
  40 320 |  40 320 |   1001 1101 1000 0000
------------------------------------------
 -25 216 | -25 216 |   1001 1101 1000 0000

这里没有值被移出Int16,但是由于有符号的解释,我们得到了负值。后来我们乘以9,然后得到:

  fact 8 |  -25 216 |   1111 1111 1111 1111 1001 1101 1000 0000
x      9 | x      9 | x                                    1001
---------------------------------------------------------------
  40 320 | -226 944 |   1111 1111 1111 1100 1000 1001 1000 0000
---------------------------------------------------------------
 -30 336 |  -30 336 |                       1000 1001 1000 0000

因此 CPU 将在更大的寄存器中执行计算(例如 32 位),并取该结果的最低 16 位。由于每次与偶数相乘都会将值向左移动至少一个位置,因此设置的位最终将移出具有固定位数的整数表示(尽管如果我们有更多位,此过程将当然需要更长的时间)。

对于具有 n 位的数字,我们在不到 2×n 步内达到零(因为每次索引为偶数时,我们将其移动 至少向右一个位置)。

Q2:给定一个对 fact 的调用,所有对 fact 的递归调用都将使用相同的类型是否正确,即使在某些调用中 Int 可以替换为 Integer 也是如此 p>

类型不是在运行时决定的(所以“per call”),而是在编译时决定的。 Haskell 分析函数时首先假设它有一个类型 a -> b,其中 ab 可以是任何东西。

接下来做一些分析:

fact 0 = 1

表示输入类型a 和输出类型b 必须属于Num 类型类。此外,我们执行 implicit 相等性检查(Haskell 将在幕后执行(0 ==))。所以现在我们知道类型是:

fact :: (Num a, Num b, Eq a) => a -> b

现在我们可以分析递归调用了:

fact n = n * fact (n-1)

这相当于:

-- equivalent to
fact n = (*) n (fact (n-1))

Haskell 假设这个递归的fact 调用具有相同的类型。因此,递归调用的类型可以说是(Num c, Num d, Eq c) => c -> d

但是我们可以分析函数(*) Num e => e -> e -> e。请注意,在 Haskell 中,当我们执行乘法时,两个操作数和结果都具有相同的类型。由于第一个操作数是n,我们知道e ~ ae ~ d(递归调用的结果类型)和e ~ b(我们外部fact 的结果)。所以我们知道e ~ a ~ b ~ d。这意味着ab 是相同的类型,因此fact 具有类型:

fact :: (Num a, Eq a) => a -> a

因此我们知道对于递归调用,c ~ d ~ a。因此递归调用也有类型fact :: (Num a, Eq a) => a -> a(递归函数调用)。所以确实,这里的递归调用使用了相同的类型。

【讨论】:

    【解决方案3】:

    回答问题 2,这种类型的检查方法只有一种:

    fact n = n * fact (n-1)
    

    fact 特化为Int,递归调用也必须特化为Int

    (fact :: Int -> Int) n = n * (fact :: Int -> Int) (n-1)
    

    在其他情况下,递归调用可能会默认为Integer,但如果您始终使用(至少)-Wall 进行编译,则默认会触发警告。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-11-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多