Haskell,终端呼叫优化和延迟评估

Haskell,终端呼叫优化和延迟评估,haskell,optimization,lazy-evaluation,tail-recursion,Haskell,Optimization,Lazy Evaluation,Tail Recursion,我正在尝试编写一个findIndexBy,它将返回排序函数在列表中选择的元素的索引。 这个函数相当于对列表进行排序并返回顶部元素,但我希望实现它,以便能够处理列表而不受大小限制 findIndexBy::(Ord a)=>(a->a->Bool)->[a]->整数 findIndexBy f(x:xs)=findIndexBy'xs x 10 哪里 findIndexBy'[]\uui=i FixDexBy’(x:xs)y席i==f x y 然后,FixDexBy’xs x(席+ 1)席 els

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

findIndexBy::(Ord a)=>(a->a->Bool)->[a]->整数
findIndexBy f(x:xs)=findIndexBy'xs x 10
哪里
findIndexBy'[]\uui=i
FixDexBy’(x:xs)y席i==f x y
然后,FixDexBy’xs x(席+ 1)席
else findIndexBy'xs y(xi+1)yi
在这个实现中,当处理大列表时,我得到一个
堆栈空间溢出
,如下例所示(微不足道):

findIndexBy(>)[1..1000000]
我知道应该有更优雅的解决方案来解决这个问题,我很想知道最惯用和有效的解决方案,但我真的想了解我的函数有什么问题

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

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

findIndexBy::(Ord a)=>(a->a->Bool)->[a]->整数
findIndexBy f(x:xs)=findIndexBy'xs x 10
哪里
findIndexBy'[]\uui=i
FixDexBy(x:xs)y席i= FunDexxBy’s(如果F x y然后x否则y)(席席1)(如果f x y然后Xi另一个i)
有没有一种简单的方法可以让编译器显示尾部调用优化(未)执行的位置

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

(定义[keep func,coll]的索引)
(循环[i 0
a(第一科)
l(休息课)
保持-i]
(如果(空?l)
keep-i
(让[keep(keep func a(first l))]
(重现
(inc i)(如果保留a(第一个l))(剩余l)(如果保留-i(inc i(()())))
作为参考,前面引用的Haskell代码是使用
-O3
标志编译的

[在leventov回答后编辑]

这个问题似乎与懒惰的评估有关。 虽然我找到了关于
$的信息
seq
,我想知道使用它们修复原始代码的最佳实践是什么

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

[编辑]


最简单的修复方法是在
if
语句之前的第一个片段中添加
yi`seq`

添加bang模式对我来说很有效。我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 siml-dsuppress all-o tail rec tail-rec.hs>tail rec core.hs

然而,我没有发现有和没有bang模式的
Core
输出之间有多大区别

  • 您的代码需要累加器值来生成返回值,因此这种情况下惰性会消失

  • 当累加器是惰性的时,您会得到一长串的thunk,需要在最后进行评估。这就是导致函数崩溃的原因。将累加器声明为严格的,就可以去掉thunks,并且它可以在大型列表上工作。在这种情况下,使用
    foldl'
    是典型的

  • 内核中的差异:

  • 没有刘海:

    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。

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

    findIndexBy f =
      snd . foldl1' (\x y -> if f x y then x else y) . flip zip [0..]
    
    首先,此函数将每个元素与其
    (元素,索引)
    列表中的索引(
    flip-zip[0..]
    )配对。然后,
    foldl1'
    (严格版本的折叠会因空列表而崩溃)沿着列表运行,并拉出满足您的
    f
    的元组。然后返回该元组的索引(
    snd,在本例中为


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

    谢谢,进一步搜索我找到的
    $和<代码> SEQ < /代码>,并通过强制席评价来实现我的代码(这是唯一的一个被怠慢的评估)吗?但它仍然不是很令人满意,因为它使代码可读性降低…@killy971目前在GHC中,您必须以某种方式明确声明严格性。这就是为什么像
    foldl'
    这样的函数出现在标准库中的原因。我最初是如此专注于终端递归,以至于我完全忘记了惰性计算。你不会发现任何概念上的不同
    findIndexBy f(x:xs)=snd$fst$foldl'(\(i,found@(foundI,foundX))x->(i+1,如果f x foundX(i+1,x)else-foundX)xs(1,(0,x))
    foldl'
    在这里没有任何用处。
    (,)
    构造函数的参数不严格。在应用于f之前,您还需要提取元组的正确部分,否则会得到错误的类型签名。