我正在尝试编写一个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
。
答案 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)
你的代码需要累加器值才能产生返回值,所以这就是懒惰失败的情况。
当累加器是懒惰的时候,你会得到一个最终需要评估的松散链。这就是崩溃你的功能。声明累加器严格,你摆脱了thunk,它适用于大型列表。在这种情况下,使用foldl'
是典型的。
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。
答案 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的额外严格注释。