评估策略

时间:2012-02-24 23:23:26

标签: haskell lazy-evaluation

如何在Haskell中的以下示例中进行功能评估的理由:

let f x = ...
    x = ...
in map (g (f x)) xs

在GHC中,有时(f x)只评估一次,有时一次评估xs中的每个元素,具体取决于fg的确切内容。当f x是一项昂贵的计算时,这可能很重要。它刚刚绊倒了我正在帮助的Haskell初学者,我不知道该告诉他什么,除了它取决于编译器。还有更好的故事吗?

更新

在以下示例中,(f x)将被评估4次:

let f x = trace "!" $ zip x x
    x = "abc"
in map (\i -> lookup i (f x)) "abcd" 

4 个答案:

答案 0 :(得分:9)

使用语言扩展程序,我们可以创建重复评估f x 必须的情况:

{-# LANGUAGE GADTs, Rank2Types #-}
module MultiEvG where

data BI where
    B :: (Bounded b, Integral b) => b -> BI

foo :: [BI] -> [Integer]
foo xs = let f :: (Integral c, Bounded c) => c -> c
             f x = maxBound - x
             g :: (forall a. (Integral a, Bounded a) => a) -> BI -> Integer
             g m (B y) = toInteger (m + y)
             x :: (Integral i) => i
             x = 3
         in map (g (f x)) xs

关键是即使f x的参数具有g多态性,我们必须创建一种情况,其中所需的类型无法预测(我的第一个stab使用的是Either a b而不是BI,但在优化时,这当然导致最多只有f x的两次评估。

对于每种类型,必须至少评估一次多态表达式。这是单态限制的一个原因。但是,当可能需要的类型范围受到限制时,可以记住每种类型的值,并且在某些情况下GHC会这样做(需要优化,我希望所涉及的类型数量不能太多大)。在这里我们用基本上不均匀的列表来面对它,所以在g (f x)的每次调用中,可能需要满足约束的任意类型,因此计算不能在map之外解除(技术上) ,编译器仍然可以在每个使用的类型上构建值的缓存,因此每种类型只会评估一次,但GHC不会,很可能不值得麻烦)。

  • 单态表达式只需要评估一次,它们可以共享。它们是否取决于实施;纯度,它不会改变程序的语义。如果表达式绑定到一个名称,实际上你可以依赖它来共享,因为它很容易,显然是程序员想要的。如果它没有绑定名称,那就是优化问题。使用字节码生成器或没有优化,通常会重复计算表达式,但是通过优化重复评估将指示编译器错误。
  • 多态表达式必须至少针对它们所使用的每种类型进行一次评估,但是通过优化,当GHC可以看到它可以在同一类型中多次使用时,它(通常)仍然会被共享在较大的计算期间输入。

底线:始终使用优化进行编译,通过将您希望共享的表达式绑定到名称来帮助编译器,并在可能的情况下提供单态类型签名。

答案 1 :(得分:8)

你的例子确实非常不同。

在第一个示例中,map的参数为g (f x),并且最有可能作为部分应用函数传递一次到map。 当g (f x)应用于map中的参数评估其第一个参数时,f x应该只执行一次,然后将使用结果更新thunk(f x)。

因此,在您的第一个示例中,{{1}}最多将评估 1次。

在编译器得出(f x)在lambda表达式中始终保持不变的结论之前,您的第二个示例需要进行更深入的分析。也许它永远不会优化它,因为它可能知道跟踪不是 kosher 。因此,这可以在跟踪时评估4次,在不跟踪时评估4次或1次。

答案 2 :(得分:6)

这实际上取决于GHC的优化,正如您所能说的那样。

最佳要做的是研究优化程序后得到的GHC core。我会查看生成的Core并检查f x是否在let之外有map语句。

如果您想确定,那么您应该将f x分解为let中指定的自己的变量,但实际上并没有保证的方法除了阅读Core之外的其他内容。

所有这一切,除了使用trace的{​​{1}}之外,这将永远不会改变程序的语义:它的实际行为。

答案 3 :(得分:6)

在没有优化的GHC中,每次调用函数时都会计算函数体。 (A" call"表示该函数应用于参数并对结果进行求值。)在下面的示例中,f x位于函数内部,因此每次调用函数时都会执行。let f x = trace "!" $ zip x x x = "abc" in map (\i -> lookup i (f x)) "abcd" 。 (GHC可以如FAQ [1]中所讨论的那样优化该表达式。)

f x

但是,如果我们将let f x = trace "!" $ zip x x x = "abc" in map ((\f_x i -> lookup i f_x) (f x)) "abcd" 移出函数,它将只执行一次。

let f x = trace "!" $ zip x x
    x = "abc"
    g f_x i = lookup i f_x
in map (g (f x)) "abcd" 

这可以更容易地重写为

{{1}}

一般规则是,每次将一个函数应用于一个参数时,一个新的" copy"功能体的创建。函数应用程序是唯一可能导致表达式重新执行的东西。但是,请注意,某些函数和函数调用在语法上看起来不像函数。

[1] http://www.haskell.org/haskellwiki/GHC/FAQ#Subexpression_Elimination