findIndices函数的说明

时间:2018-04-07 11:03:58

标签: haskell

我在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)

  • 可以解释第二个定义吗?什么是buildI#+#1#

  • 为什么第二个定义是内联的,而不是第一个定义?

2 个答案:

答案 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; []

这是用于方便:它根本不方便!但是,编译器优化器与buildfoldr组合在一起非常有效,因此代码以奇怪的方式编写,以便利用它。

此外,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 -> ...。这是在折叠列表时安排从左到右信息流的标准技巧(gofoldr 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报告中的模拟定义被用作实际代码时,它是编译标志,即使它们是作为可执行规范。