Haskell,终端呼叫优化和懒惰评估

时间:2013-09-23 05:05:55

标签: haskell optimization lazy-evaluation tail-recursion

我正在尝试编写一个findIndexBy,它将返回由排序函数在列表中选择的元素的索引。 此函数相当于对列表进行排序并返回顶部元素,但我想实现它以便能够处理没有大小限制的列表。

findIndexBy :: (Ord a) => (a -> a -> Bool) -> [a] -> Integer
findIndexBy f (x:xs) = findIndexBy' xs x 1 0
  where
    findIndexBy' [] _ _ i = i
    findIndexBy' (x:xs) y xi yi = if f x y
      then findIndexBy' xs x (xi + 1) xi
      else findIndexBy' xs y (xi + 1) yi

通过这个实现,我在处理大列表时得到Stack space overflow,如下例所示(平凡):

findIndexBy (>) [1..1000000]

我知道应该有更优雅的解决方案来解决这个问题,我有兴趣了解最惯用和最有效的解决方案,但我真的想了解我的功能有什么问题。

我可能错了,但我认为findIndexBy'的实现是基于终端递归的,所以我真的不明白为什么编译器似乎没有优化尾调用。

我认为这可能是由于if / then / else而且还尝试了以下操作,这会导致同样的错误:

findIndexBy :: (Ord a) => (a -> a -> Bool) -> [a] -> Integer
findIndexBy f (x:xs) = findIndexBy' xs x 1 0
  where
    findIndexBy' [] _ _ i = i
    findIndexBy' (x:xs) y xi yi = findIndexBy' xs (if f x y then x else y) (xi + 1) (if f x y then xi else yi)

是否有一种简单的方法可以让编译器显示(未)执行尾调用优化的位置?

作为参考,下面是我在Clojure中编写的等效函数,我现在正尝试移植到Haskell:

(defn index-of [keep-func, coll]
  (loop [i 0
         a (first coll)
         l (rest coll)
         keep-i i]
    (if (empty? l)
      keep-i
      (let [keep (keep-func a (first l))]
        (recur
          (inc i) (if keep a (first l)) (rest l) (if keep keep-i (inc i)))))))

有关信息,先前引用的Haskell代码是使用-O3标志编译的。

[leventov回答后编辑]

问题似乎与懒惰评估有关。 虽然我发现了$!seq,但我想知道使用它们修复原始代码时的最佳做法是什么。

我仍然对依赖Data.List的函数的更惯用的实现感兴趣。

[编辑]

最简单的解决方法是在yi `seq`语句之前的第一个代码段中添加if

3 个答案:

答案 0 :(得分:3)

添加爆炸模式对我有用。一世。即

{-# LANGUAGE BangPatterns #-}
findIndexBy :: (Ord a) => (a -> a -> Bool) -> [a] -> Integer
findIndexBy f (x:xs) = findIndexBy' xs x 1 0
  where
    findIndexBy' [] _ _ i = i
    findIndexBy' (x:xs) !y !xi !yi = findIndexBy' xs (if f x y then x else y) (xi + 1) (if f x y then xi else yi)

要查看GHC对代码的作用,请编译为ghc -O3 -ddump-simpl -dsuppress-all -o tail-rec tail-rec.hs > tail-rec-core.hs

请参阅Reading GHC Core

但是,我发现有{b}模式和没有爆炸模式的Core输出之间没有太大区别。

答案 1 :(得分:3)

  1. 你的代码需要累加器值才能产生返回值,所以这就是懒惰失败的情况。

  2. 当累加器是懒惰的时候,你会得到一个最终需要评估的松散链。这就是崩溃你的功能。声明累加器严格,你摆脱了t​​hunk,它适用于大型列表。在这种情况下,使用foldl'是典型的。

  3. Core

  4. 的差异

    没有刘海:

    main_findIndexBy' =
      \ ds_dvw ds1_dvx ds2_dvy i_aku ->
        case ds_dvw of _ {
          [] -> i_aku;
          : x_akv xs_akw ->
              ...
              (plusInteger ds2_dvy main4)
    

    刘海:

    main_findIndexBy' =
      \ ds_dyQ ds1_dyR ds2_dyS i_akE ->
        case ds_dyQ of _ {
          [] -> i_akE;
          : x_akF xs_akG ->
            case ds2_dyS of ds3_Xzb { __DEFAULT ->
            ...
            (plusInteger ds3_Xzb main4)
    

    确实差别很小。在第一种情况下,它使用原始参数ds2_dvy为其添加1,在第二种情况下,它首先模式匹配参数的值 - 甚至不查看它匹配的内容 - 这导致对它的评估,以及值进入ds3_Xzb。

答案 2 :(得分:2)

当你意识到懒惰是问题时,要考虑的第二件事是你在代码中实现的一般模式。在我看来,你真的只是迭代一个列表并携带一个中间值,然后当列表为空时返回 - 这是一个折叠!事实上,你可以用折叠方式实现你的功能:

findIndexBy f =
  snd . foldl1' (\x y -> if f x y then x else y) . flip zip [0..]

首先,此函数将每个元素与其flip zip [0..]列表中的索引((element, index))配对。然后foldl1'(崩溃的空列表的严格折叠版本)沿着列表运行并且拉出满足您的f的元组。然后返回此元组的索引(在这种情况下为snd)。

由于我们在这里使用了严格的折叠,它也将解决您的问题,而无需GHC的额外严格注释。