如何在Haskell中的以下示例中进行功能评估的理由:
let f x = ...
x = ...
in map (g (f x)) xs
在GHC中,有时(f x)
只评估一次,有时一次评估xs
中的每个元素,具体取决于f
和g
的确切内容。当f x
是一项昂贵的计算时,这可能很重要。它刚刚绊倒了我正在帮助的Haskell初学者,我不知道该告诉他什么,除了它取决于编译器。还有更好的故事吗?
更新
在以下示例中,(f x)
将被评估4次:
let f x = trace "!" $ zip x x
x = "abc"
in map (\i -> lookup i (f x)) "abcd"
答案 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不会,很可能不值得麻烦)。
底线:始终使用优化进行编译,通过将您希望共享的表达式绑定到名称来帮助编译器,并在可能的情况下提供单态类型签名。
答案 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