Haskell/Frege 会重新计算惰性列表的元素吗?

Does Haskell/Frege ever recalcuate elements of a lazy list?

假设我有一个所有素数的列表,定义为

primes :: (Enum α, Integral α) => [α]
primes = sieve [2..]
  where sieve :: (Integral α) => [α] -> [α]
        sieve []     = undefined
        sieve (x:xs) = x : (sieve $ filter ((/= 0) . (flip mod x)) xs)

我想通过多个不同的函数来喂养 primes,例如:

sumOfPrimesLessThan :: (Integral α) => α -> α
sumOfPrimesLessThan n = sum $ takeWhile (< n) primes

productOfPrimesLessThan :: (Integral α) => α -> α
productOfPrimesLessThan n = foldl (*) 1 $ takeWhile (< n) primes

什么的,就好像在做

main = do print (sumOfPrimesLessThan     1000 :: Integer)
          print (productOfPrimesLessThan 1000 :: Integer)

Haskell 是否会在任何时间点重新计算 primes 或其任何部分?缓存什么?什么不会被缓存?

附录 0:假设我有另一个函数

prime = flip elem primes

如果 prime 使用不同的参数进行评估,每个评估是否会重新评估 primes?例如:

allPrime = all prime

在你的情况下(对于 Haskell GHC)答案是 primes 被重新计算。然而,如果你有一个像 primes :: [Int] 这样的单态签名,那就不是这样了。这是调试它的方法:导入 Debug.Trace 并将 trace 函数添加到 primes

primes :: (Enum α, Integral α) => [α]
primes = trace "call to primes" $ sieve [2..]
  where sieve :: (Integral α) => [α] -> [α]
        sieve []     = undefined
        sieve (x:xs) = x : (sieve $ filter ((/= 0) . (flip mod x)) xs)

现在,每次调用 primes 时,您都会看到 "call to primes" 被打印出来。编译您的程序(有或没有优化),我收到两次 primes.

的调用

但是为什么呢?

Haskell 实际上将您的 primes 版本编译为一个接受一个类型参数的函数,因此在 sumOfPrimesLessThan 和 [=22= 中使用 primes ] 实际上是一个函数调用(出于非常明显的原因,函数调用通常不会存储在 Haskell 中)。例如,当您调用 sumOfPrimesLessThan 1000 :: Integer 时,您实际上给了它两个参数:值 1000 和类型参数 IntegersumOfPrimesLessThan 然后将第二个参数传递给素数。

另一方面,如果您的类型签名是单态的,例如 primes :: [Int]sumOfPrimesLessThan :: Int -> IntproductOfPrimesLessThan :: Int -> Int,Haskell 编译 primes下降到只是一个常规的 thunk 值,所以它只被评估一次。

是我不久前给出的关于Haskell如何在里面表示值和函数的另一种解释。

SPECIALIZE pragma(特定于 GHC)

有时您希望能够告诉 GHC,即使您的表达式是多态的,您也希望它被视为几个类型的单态。 (所以你有点希望你有第二个版本 primes :: [Integer] 即使通常 primes :: (Enum α, Integral α) => [α]。)你可以使用 SPECIALIZE pragma:

来指定它
{-# SPECIALIZE primes :: [Integer] #-}
primes :: (Enum a,Integral a) => [a]
primes = ...

现在,仅对于 Integer 类型,primes 将只计算一次。对于其他类型(如 Int),我们仍然会得到与以前相同的行为。

回应附录

对于 prime = flip elem primes 的多次调用,其中 prime 是单态的 ("instantiated"),您仍然只能调用一次 primes。一旦 primes 是单态的,它就可以随处共享。另外,请注意 allPrime = all prime 示例中的单态限制 - 您需要指定 Foldable 想要的实例(allPrime = all primeallPrime xs = all prime xs 略有不同)。