let strings = ["foo", "bar", "baz"]
in [filter (== char) (concat strings) | char <- "oa"]
GHC会在concat strings
时评估char == 'o'
吗?char == 'a'
时评估concat strings == "foobarbaz"
吗?或者它是否记得{{1}}供以后使用?
我意识到我可以通过重构此代码来避免这种不确定性,但我对代码的编译方式感兴趣。
答案 0 :(得分:4)
GHC的评估可能远远少于9次。实际上它是这样做的,我们可以证明使用Debug.Trace.trace
module Main (main) where
import Debug.Trace
x = let strings = ["foo", "bar", "baz"]
in [filter (== char) (trace "\nconcat strings\n" (concat strings)) | char <- "oaxyz"]
main = do
print x
这里为-O0评估“oaxyz”5次,为-O1和-O2评估一次:
! 529)-> touch LC.hs ; ghc -O0 -o lc0 LC.hs
[1 of 1] Compiling Main ( LC.hs, LC.o )
Linking lc0 ...
(! 530)-> touch LC.hs ; ghc -O1 -o lc1 LC.hs
[1 of 1] Compiling Main ( LC.hs, LC.o )
Linking lc1 ...
(! 531)-> ./lc0; ./lc1; ./lc2
concat strings
concat strings
concat strings
concat strings
concat strings
["oo","aa","","","z"]
concat strings
["oo","aa","","","z"]
concat strings
["oo","aa","","","z"]
答案 1 :(得分:2)
克里斯在回答中说的都是真的。虽然您不能完全依赖要共享的表达式,但它是GHC共享它的有效优化,并且通常在启用优化时执行此操作。然而,你可能不想依赖这个功能,如果你可以通过解除lambda的concat
调用来明确预期的共享,那我就是这样。
将Debug.Trace.trace
用于此类目的是了解事物评估的时间和频率的好方法。
另一种选择是查看生成的核心代码。对于这个计划:
main = print x
x = let strings = ["foo", "bar", "baz"]
in [filter (== char) (concat strings) | char <- "oa"]
让我们编译无需优化并查看生成的代码:
$ ghc NrEval -fforce-recomp -ddump-simpl -dsuppress-all
[1 of 1] Compiling Main ( NrEval.hs, NrEval.o )
==================== Tidy Core ====================
Result size of Tidy Core = {terms: 50, types: 54, coercions: 0}
main
main =
print
($fShow[] ($fShow[] $fShowChar))
(let {
a_ssN
a_ssN = unpackCString# "foo" } in
let {
a1_ssQ
a1_ssQ = unpackCString# "bar" } in
let {
a2_ssT
a2_ssT = unpackCString# "baz" } in
let {
a3_ssU
a3_ssU = : a2_ssT ([]) } in
let {
a4_ssV
a4_ssV = : a1_ssQ a3_ssU } in
let {
strings_ahk
strings_ahk = : a_ssN a4_ssV } in
letrec {
ds_dsE
ds_dsE =
\ ds1_dsF ->
case ds1_dsF of _ {
[] -> [];
: ds3_dsG ds4_dsH ->
: (filter
(\ ds5_dsI -> == $fEqChar ds5_dsI ds3_dsG) (concat strings_ahk))
(ds_dsE ds4_dsH)
}; } in
ds_dsE (unpackCString# "oa"))
main
main = runMainIO main
我们可以看到,即使没有优化,列表理解也被map
ds_dsE
中的concat strings_ahk
的(内联)应用程序所取代。但是,ds1_dsF
仍然在lambda(ds_dsE
)下,这意味着每次评估函数时都会对它进行评估,这是两次:一次调用"oa"
到字符串{ {1}},一次在递归调用中ds_dsE ds4_dsH
。
现在让我们将结果与优化进行比较:
$ ghc NrEval -fforce-recomp -ddump-simpl -dsuppress-all -O
[1 of 1] Compiling Main ( NrEval.hs, NrEval.o )
==================== Tidy Core ====================
Result size of Tidy Core = {terms: 89, types: 105, coercions: 9}
main9
main9 = unpackCString# "foo"
main8
main8 = unpackCString# "bar"
main7
main7 = unpackCString# "baz"
main6
main6 = : main7 ([])
main5
main5 = : main8 main6
main_strings
main_strings = : main9 main5
main4
main4 =
\ ds_dsT ds1_dsS ->
: (letrec {
go_ato
go_ato =
\ ds2_atp ->
case ds2_atp of _ {
[] -> [];
: y_atu ys_atv ->
let {
z_XtU
z_XtU = go_ato ys_atv } in
letrec {
go1_XtX
go1_XtX =
\ ds3_XtZ ->
case ds3_XtZ of _ {
[] -> z_XtU;
: y1_Xu6 ys1_Xu8 ->
case y1_Xu6 of wild2_atz { C# c1_atB ->
case ds_dsT of _ { C# c2_atF ->
case eqChar# c1_atB c2_atF of _ {
False -> go1_XtX ys1_Xu8;
True -> : wild2_atz (go1_XtX ys1_Xu8)
}
}
}
}; } in
go1_XtX y_atu
}; } in
go_ato main_strings)
ds1_dsS
main3
main3 = unpackFoldrCString# "oa" main4 ([])
main2
main2 = showList__ $fShowChar_$cshowList main3 ([])
main1
main1 = \ eta_B1 -> hPutStr2 stdout main2 True eta_B1
main
main = main1 `cast` ...
main10
main10 = \ eta_Xp -> runMainIO1 (main1 `cast` ...) eta_Xp
main
main = main10 `cast` ...
在这里,我们可以看到发生了很多事情,但特别是,对concat strings
的调用已被提升到顶级并在编译时完全展开,导致main_strings
指向串联的三元素字符串列表。使用go_ato
调用main_strings
时,显然会分享这一点。
答案 2 :(得分:1)
它将被评估两次。 GHC没有memoize。 list comprehensions desugar喜欢这个
[term | x <- xs, y <- ys ...] -- I ignore guards
do
x <- xs
y <- ys
...
return term
与
相同 flip concatMap xs $ \x -> flip concatMap ys $ \y -> ... [term]
很明显,term
将被计算l1 * l2 * l3 ... ln
次li
是i
列表的长度。