通过递归示例了解 Haskell 中的非严格性

Understanding non-strictness in Haskell with a recursive example

这两者在评价上有什么区别?

为什么这个“遵”(怎么说?)不严

recFilter :: (a -> Bool) -> [a] -> [a]
recFilter _ [] = []
recFilter p (h:tl) = if (p h) 
  then h : recFilter p tl
  else recFilter p tl

而这不是?

recFilter :: (a -> Bool) -> [a] -> Int -> [a]
recFilter _ xs 0 = xs
recFilter p (h:tl) len
  | p(h)  = recFilter p (tl ++ [h]) (len-1)
  | otherwise = recFilter p tl (len-1)

是否可以非严格地编写尾递归函数?

老实说,我也不明白第一个例子的调用堆栈,因为我看不到h: 到哪里去了。有没有办法在 ghci 中看到这个?

第二个实现没有多大意义:名为 len 的变量将 包含列表的长度。因此你需要传递这个,对于无限列表,这是行不通的,因为根本没有长度。

您可能想要生成如下内容:

recFilter :: (a -> Bool) -> [a] -> [a]
recFilter p = go []
    where go ys [] = ys  -- (1)
          go ys (x:xs) | p x = go (ys ++ [x]) xs
                       | otherwise = go ys xs

因此我们有一个累加器,我们将列表中的项目附加到该累加器,然后最终 return 累加器。

第二种方法的问题在于,只要累加器未被 return 编辑,Haskell 就需要继续递归,直到至少达到 weak head normal form (WHNF)。这意味着如果我们将结果与 [](_:_) 进行模式匹配,我们至少需要递归到第一种情况,因为其他情况只会产生一个新表达式,因此不会产生我们可以在其上进行模式匹配的数据构造函数。

这与第一个过滤器形成对比,如果我们在 [](_:_) 上进行模式匹配,则足以在第一种情况 (1) 或第三种情况 93) 处停止,其中表达式生成一个带有列表数据构造函数的对象。仅当我们需要额外的元素进行模式匹配时,例如 (_:_:_),它才需要评估 recFilter p tl in case (2) of the first implementation:

recFilter :: (a -> Bool) -> [a] -> [a]
recFilter _ [] = []  -- (1)
recFilter p (h:tl) = if (p h) 
  then h : recFilter p tl  -- (2)
  else recFilter p tl

有关详细信息,请参阅描述惰性如何与 thunks 一起工作的 Laziness section of the Wikibook on Haskell

非尾递归函数粗略地消耗一部分输入(第一个元素)来产生一部分输出(好吧,如果它至少没有被过滤掉的话)。然后递归处理输入的下一部分,依此类推。

你的尾递归函数将递归直到 len 变为零,并且只有在那个点它才会输出整个结果。

考虑这个伪代码:

def rec1(p,xs):
   case xs:
      []     -> []
      (y:ys) -> if p(y): print y
                rec1(p,ys)

并将其与这个基于累加器的变体进行比较。我没有使用 len 因为我使用了一个单独的累加器参数,我假设它最初是空的。

def rec2(p,xs,acc):
   case xs:
      []     -> print acc
      (y:ys) -> if p(y): 
                   rec2(p,ys,acc++[y])
                else:
                   rec2(p,ys,acc)

rec1 打印 before 递归:它不需要检查整个输入列表来开始打印输出。从某种意义上说,它以“steraming”方式工作。相反,rec2 只会在输入列表被完全扫描后的最后才开始打印。

当然,在您的 Haskell 代码中没有 print,但是您可以将 x : function call 作为“打印 x”返回,因为 xfunction call 实际创建之前可供我们函数的调用者使用 。 (好吧,迂腐地说,这取决于调用者将如何使用输出列表,但我会忽略这一点。)

因此非尾递归代码也可以在无限列表上工作。即使在有限的输入上,性能也会得到改善:如果我们调用 head (rec1 p xs),我们只会评估 xs 直到第一个未丢弃的元素。相比之下,head (rec2 p xs) 将完全过滤整个列表 xs,即使我们不需要它。