【问题标题】:Haskell, terminal call optimisation and lazy evaluationHaskell、终端调用优化和惰性求值
【发布时间】:2013-09-23 05:05:54
【问题描述】:

我正在尝试编写一个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 的函数的更多惯用实现感兴趣。

[编辑]

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

【问题讨论】:

  • 您不会发现任何概念上的不同。 findIndexBy f (x:xs) = snd $ fst $ foldl' (\(i, found@(foundI, foundX)) x -> (i + 1, if f x foundX (i + 1, x) else found)) xs (1, (0, x))

标签: haskell optimization lazy-evaluation tail-recursion


【解决方案1】:

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

{-# 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

但是,我没有发现 Core 输出有和没有 bang 模式有太大区别。

【讨论】:

  • 谢谢,进一步搜索我发现了 $!seq,并通过强制 xi 评估使我的代码工作(这是唯一懒惰评估的东西吗?)。但这仍然不是很令人满意,因为它降低了代码的可读性......
  • @killy971 目前在 GHC 中,您必须以某种方式明确声明严格性。这就是为什么,例如,像 foldl' 这样的函数会出现在标准库中。
  • 谢谢我现在明白了。我最初非常专注于终端递归,以至于完全忘记了惰性求值。
【解决方案2】:
  1. 您的代码需要累加器值才能产生返回值,因此这是惰性丢失的情况。

  2. 当累加器是惰性的时,你会得到一个长长的 thunk 链,最终需要评估。这就是使您的功能崩溃的原因。将累加器声明为严格的,您就可以摆脱 thunk 并且它适用于大型列表。在这种情况下,foldl' 的使用很常见。

  3. Core的区别:

没有刘海:

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。

【讨论】:

    【解决方案3】:

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

    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 严格性注释。

    【讨论】:

    • foldl' 在这里不会做任何有用的事情。 (,) 构造函数的参数并不严格。
    • 您还需要在应用于 f 之前提取元组的正确部分,否则您将得到错误的类型签名。
    猜你喜欢
    • 2023-04-05
    • 1970-01-01
    • 2014-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多