`let` 和 `where` 表达式的结果会存储在 haskell 中吗?

will the result of `let` and `where` expressions be stored in haskell?

我对 Haskell 比较陌生,在阅读 this and some performance tips on strictness 之后,我仍然想知道这如何应用于 letwhere 表达式。如果我有这样的代码:

f :: Int -> Int -> Int
f a b
  |a==b = <simple computation>
  |otherwise = e1 + 2 * e1 - e1^2
  where e1 = <lengthy computation>

<lengthy computation>多久评估一次?我假设给定 Haskell 在 e1 中的惰性求值如果 a==b 根本不会被求值。但如果不是,是 e1 被替换到 otherwise 表达式中,然后每次遇到它时都求值,还是在第一次遇到它时求值,然后存储并在所有后续出现时重复使用?还有:

这与 非常相似,但我找不到 Haskell 的答案。

非常感谢解释。

通常,constant applicative formwherelet 块中的代码只被评估 一次,并且只评估必要的深度(即,如果它根本没有使用,它也根本不会被评估)。

f 不是常量应用形式,因为它有参数;相当于

f' = \a b -> let e1 = <lengthy computation>
             in if a==b
                 then <simple computation>
                 else e1 + 2 * e1 - e1^2

因此,每次您使用两个参数 调用该函数时,e1 都会计算一次 。这可能也是您想要的,事实上,如果 <lengthy computation> 取决于 ab,则可能的最佳行为。如果它,比如说,只依赖于 a,你可以做得更好:

f₂ a = \b -> 
  if a==b then <simple computation>
           else e1 + 2 * e1 - e1^2
 where e1 = <lengthy computation>

当您执行以下操作时,此表单会更有效率map (f 34) [1,3,9,2,9]:在该示例中,e1 只会对整个列表计算一次。 (但是 <lengthy computation> 范围内不会有 b,所以它 不能 依赖于它。)

OTOH,也可能存在您根本不想 e1 保留的情况。 (例如,如果它占用大量内存,但计算起来 quick)。在这种情况下,您可以将其设为“零函数”

f₃ a b
  | a==b = <simple computation>
  | otherwise = e1() + 2 * e1() - e1()^2
 where e1 () = <lengthy computation>

默认情况下不记忆函数,因此在上面,如果 a==b<lengthy computation> 执行零次,否则执行三次。

还有另一种可能性是强制 e1 总是只计算一次。您可以使用 seq:

f₄ a b = e1 `seq` if a==b
          then <simple computation>
          else e1 + 2 * e1 - e1^2
 where e1 = <lengthy computation>

这是唯一真正改变 语义 的建议,而不仅仅是性能:假设我们总是定义 e1 = error "too tough"。那么 ff'f₂f₃ 都将仍然有效,前提是 a==b;然而 f₄ 在这种情况下甚至会失败。


至于优化(-O-O2)——这些通常不会改变程序的严格属性(即不能在 [=16= 的行为之间改变) ] 和 f₄)。除此之外,编译器几乎可以自由地进行它认为对性能有益的任何更改。但是通常,它不会改变我上面说的任何东西。正如 Taren 所说,主要的例外是类似于 f₃:编译器将很容易地内联 e1 (),然后共享对计算值的引用,这会阻止垃圾收集器回收内存。所以最好不要依赖这种(无论如何有点老套)技术。

您可以实际查看 GHC 将如何优化您的代码:

ghc -ddump-simpl -dsuppress-idinfo -dsuppress-coercions -dsuppress-type-applications -dsuppress-uniques -dsuppress-module-prefixes -fforce-recomp .\scratch.hs

这有点冗长,因此您可能需要为其添加别名。这在很大程度上取决于优化级别,因此您可能想对每个优化级别进行尝试。

使用 g i = sum [1..i] 作为昂贵的计算和 -O2 我得到这个输出:

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 64, types: 23, coercions: 0}

Rec {
-- RHS size: {terms: 16, types: 3, coercions: 0}
$wgo :: Int# -> Int# -> Int#
$wgo =
  \ (w :: Int#) (ww :: Int#) ->
    case w of wild {
      __DEFAULT -> $wgo (+# wild 1#) (+# ww wild);
      10000# -> +# ww 10000#
    }
end Rec }

-- RHS size: {terms: 15, types: 1, coercions: 0}
f2 :: Int
f2 =
  case $wgo 1# 0# of ww { __DEFAULT ->
  I# (-# (+# ww (*# 2# ww)) (*# ww ww))
  }

-- RHS size: {terms: 2, types: 0, coercions: 0}
f1 :: Int
f1 = I# 42#

-- RHS size: {terms: 17, types: 8, coercions: 0}
f :: Int -> Int -> Int
f =
  \ (a :: Int) (b :: Int) ->
    case a of _ { I# x ->
    case b of _ { I# y ->
    case tagToEnum# (==# x y) of _ {
      False -> f2;
      True -> f1
    }
    }
    }

与您的 haskell 版本相比,这非常难看,但稍微眯着眼睛看,它并没有复杂多少。 $wgo 是我们昂贵的功能。这里有趣的部分是 f1 或 f2,f 的可能 return 值,仅在第一次需要时才计算一次。对于程序的其余部分 运行,它们被重复使用。