Int溢出强制为0?

时间:2018-05-31 18:10:36

标签: haskell

如果我写这样的递归因子:

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

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

我希望Haskell能够变得聪明,并使这个阶乘实现变得更快。所以,如果我写:fact (10 :: Int)所有这些都是Int s的快速数学运算,如果我写fact (1000 :: Integer),所有或部分计算将是{ {1}}秒。

ghci在这里很有用:

Integer

我有两个问题:

Q1:当我跑

时会发生什么
λ: :t fact (2 :: Int)
fact (2 :: Int) :: Int
λ: :t fact (2 :: Integer)
fact (2 :: Integer) :: Integer

显然Haskell无法为更大的参数计算正确的结果,因为它会溢出。所以我对λ: fact (10 :: Int) >3628800 λ: fact (40 :: Int) >-7060926325325235253252... λ: fact (66 :: Int) >0 的结果并不感到惊讶,我得到的结果与fact 40相似。但是从fact 65开始,函数总是返回0.为什么呢?

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

3 个答案:

答案 0 :(得分:9)

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

  

鉴于对事实的调用,对事实的所有递归调用都将使用相同的类型

是否正确

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

答案 1 :(得分:6)

  

我得到的结果与fact 65相似。但从fact 66开始,该函数始终返回0。怎么样?

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

Int16

此处没有值移出 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

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

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

  

Q2:鉴于对事实的调用,所有对事实的递归调用都将使用相同的类型是正确的,即使在某些调用中Int可能已被替换为Integer

类型在运行时决定(所以“每次调用”),但是在编译时。 Haskell首先假设它具有 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 类型来分析该函数,其中a -> ba可以是任何类型。

接下来做一些分析:

b

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

(0 ==)

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

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

这相当于:

fact n = n * fact (n-1)

Haskell 假设此递归-- equivalent to fact n = (*) n (fact (n-1)) 调用具有相同的类型。因此,递归调用具有类型fact

然而,我们可以分析函数(*) Num e => e -> e -> e。注意,在我们执行乘法时,在Haskell中,两个操作数和结果都具有相同的类型。由于第一个操作数是(Num c, Num d, Eq c) => c -> d,我们知道ne ~ a(递归调用的结果类型)和e ~ d(我们的外部{{1的结果) }})。所以我们知道e ~ b。这意味着facte ~ a ~ b ~ d属于同一类型,因此a具有类型:

b

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

答案 2 :(得分:2)

回答问题2,此类型只有一种检查方式:

fact n = n * fact (n-1)

fact处专攻Int,递归调用也必须专注于Int

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

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