我在this question中说我不理解findIndices
的源代码。
事实上我没有给予足够的重视,我没有看到这个函数有两个的定义:
findIndices :: (a -> Bool) -> [a] -> [Int]
#if defined(USE_REPORT_PRELUDE)
findIndices p xs = [ i | (x,i) <- zip xs [0..], p x]
#else
-- Efficient definition, adapted from Data.Sequence
{-# INLINE findIndices #-}
findIndices p ls = build $ \c n ->
let go x r k | p x = I# k `c` r (k +# 1#)
| otherwise = r (k +# 1#)
in foldr go (\_ -> n) ls 0#
#endif /* USE_REPORT_PRELUDE */
我理解第一个定义,即我没有看到的定义。我不明白第二个。我有几个问题:
什么是if defined(USE_REPORT_PRELUDE)
?
可以解释第二个定义吗?什么是build
,I#
,+#
,1#
?
为什么第二个定义是内联的,而不是第一个定义?
答案 0 :(得分:3)
对于C编程语言,CPP
扩展启用C预处理器。这里,它用于测试编译期间是否设置了标志USE_REPORT_PRELUDE
。根据该标志,编译器使用#if
或#else
代码变体。
build
是一个可以定义为
build f = f (:) []
因此,使用build (\c n -> ...
基本上允许c
加入&#34; cons&#34; (:)
和n
到&#34; nil&#34; []
。
这是不用于方便:它根本不方便!但是,编译器优化器与build
和foldr
组合在一起非常有效,因此代码以奇怪的方式编写,以便利用它。
此外,I# ...
是整数的低级构造函数。我们通常写的时候
x :: Int
x = 4+2
GHC实现x
(非常粗略地),指向一些读取为unevaluated: 4+2
的内存。第一次强制x
后,此内存将被evaluated: I# 6#
覆盖。这是实现懒惰所必需的。
&#34;拳击&#34;这里指的是通过指针的间接。
相反,类型Int#
是一个普通的机器整数,没有指针,没有间接,没有未评估的表达式。它是严格的(而不是懒惰的),但是更低级别它更有效。一个创建一个值,如
x' :: Int#
x' = 6#
x :: Int
x = I# x'
确实,Int
定义为newtype Int = I# Int#
。
请记住,这不是标准的Haskell,而是GHC特定的低级细节。在普通代码中,您不需要使用此类未装箱的类型。在图书馆中,作者这样做是为了获得更多的表现,但就是这样。
有时,即使在我们的代码中我们只使用Int
s,GHC也足够智能自动将我们的代码转换为使用Int#
并实现更高的效率,避免装箱。如果我们要求GHC转储核心&#34;这可以被观察到。这样我们就可以看到优化的结果。
例如,编译
f :: Int -> Int
f 0 = 0
f n = n + f (n-1)
GHC产生一个较低级别的版本(这是GHC Core,而不是Haskell,但它足够类似于理解):
Main.$wf :: GHC.Prim.Int# -> GHC.Prim.Int#
Main.$wf = \ (ww_s4un :: GHC.Prim.Int#) ->
case ww_s4un of ds_X210 {
__DEFAULT ->
case Main.$wf (GHC.Prim.-# ds_X210 1#) of ww1_s4ur { __DEFAULT ->
GHC.Prim.+# ds_X210 ww1_s4ur
};
0# -> 0#
}
答案 1 :(得分:2)
注意go
的参数数量。 go x r k = ...
=== go x r = \k -> ...
。这是在折叠列表时安排从左到右信息流的标准技巧(go
在foldr go (\_ -> n) ls 0#
中用作减速器函数)。在这里,它是[0..]
的计数,在最初的k=0
和每个步骤的(k + 1)
被解释(k
是一个不幸的命名选择,i
似乎更好; k
超载了无关的“常数”和“延续”,而不仅仅是“计数器”,这可能是这里的意图)。
foldr/build
(sic) fusion(由评论中的luqui链接)将foldr c n $ findIndices p [a1,a2,...,an]
转换为循环,公开foldr
定义的内部findIndices
,避免构建实际列表findIndices
调用结果的结构:
build g = g (:) []
foldr c n $ build g = g c n
foldr c n $ findIndices p [a1,a2,...,an]
==
foldr c n $ build g where {g c n = ...}
=
g c n where {g c n = ...}
=
foldr go (const n) [a1,a2,...,an] 0 where {go x r k = ...}
=
go a1 (foldr go (const n) [a2,...,an]) 0
=
let { x=a1, r=foldr go (const n) [a2,...,an], k=0 }
in
if | p x -> c (I# k) (r (k +# 1#)) -- no 'cons' (`:`), only 'c'
| otherwise -> r (k +# 1#)
=
....
所以你看,让foldr
定义函数需要一个输入参数,从左到右排列是一个标准技巧处理输入列表时的信息流。
所有带有井号的东西都是“原始”或“更接近机器级”的实体。 I#
是原始Int
构造函数; 0#
是机器级0
;等等。这可能是也可能不完全正确,但它应该足够接近。
foldr/build
融合似乎是基于传感器的代码转换的特殊情况,它基于嵌套折叠通过组合其减速器的变换器(也称为传感器)而融合的事实,如在
foldr c n $
foldr (tr2 c2) n2 $
foldr (tr3 c3) n3 xs
=
foldr (tr2 c) n $ -- fold "replaces" the constructor nodes with its reducer
foldr (tr3 c3) n3 xs -- so just use the outer reducer in the first place!
=
foldr (tr3 (tr2 c)) n xs
=
foldr ((tr3 . tr2) c) n xs
和build g === foldr . tr
可以为给定的tr
选择g
,以便
build g = g c n = (foldr . tr) c n = foldr (tr c) n
至于USE_REPORT_PRELUDE
,我不能用任何权限说这个,但我总是假设当Haskell报告中的模拟定义被用作实际代码时,它是编译标志,即使它们是作为可执行规范。