如果我可以用foldl定义一个函数,它会使它尾递归吗?

时间:2015-10-12 21:20:20

标签: haskell functional-programming

我的函数式编程课程中有一个赋值,要求我重写几个函数,比如mapfilter是尾递归的。

我并非100%确定如何解决此问题,但我知道您可以通过致电foldrfoldl来定义功能。我知道foldl是尾递归的,所以如果我可以用filter定义说foldl,它也会变成尾递归吗?

2 个答案:

答案 0 :(得分:6)

有两种方法可以使递归函数尾递归:

  1. 将函数转换为累加器传递样式。这只适用于某些情况。
  2. 将函数转换为延续传递样式。这适用于所有情况。
  3. 考虑map函数的定义:

    map :: (a -> b) -> [a] -> [b]
    map _ []     = []
    map f (x:xs) = f x : map f xs
    

    在累加器传递样式中,我们有一个额外的参数来累积结果:

    mapA :: (a -> b) -> [a] -> [b] -> [b]
    mapA _ []     = id
    mapA f (x:xs) = mapA f xs . (f x :)
    

    原始的map函数可以恢复如下:

    map :: (a -> b) -> [a] -> [b]
    map f xs = reverse $ mapA f xs []
    

    请注意,我们需要reverse结果。这是因为mapA反向累积结果:

    > mapA (+1) [1,2,3,4,5] []
    > mapA (+1) [2,3,4,5]  [2]
    > mapA (+1) [3,4,5]  [3,2]
    > mapA (+1) [3,5]  [4,3,2]
    > mapA (+1) [5]  [5,4,3,2]
    > mapA (+1) [] [6,5,4,3,2]
    > [6,5,4,3,2]
    

    现在,考虑继续传递风格:

    mapK :: (a -> b) -> [a] -> ([b] -> r) -> r
    mapK _ []     k = k []
    mapK f (x:xs) k = mapK f xs (k . (f x :))
    

    原始的map函数可以恢复如下:

    map :: (a -> b) -> [a] -> [b]
    map f xs = mapK f xs id
    

    请注意,我们不需要reverse结果。这是因为虽然mapK反向累积了连续,但是当最终应用于基本情况时,展开连续以产生正确顺序的结果:

    > mapK (+1) [1,2,3,4,5] id
    > mapK (+1) [2,3,4,5]  (id . (2:))
    > mapK (+1) [3,4,5]    (id . (2:) . (3:))
    > mapK (+1) [4,5]      (id . (2:) . (3:) . (4:))
    > mapK (+1) [5]        (id . (2:) . (3:) . (4:) . (5:))
    > mapK (+1) []         (id . (2:) . (3:) . (4:) . (5:) . (6:))
    > (id . (2:) . (3:) . (4:) . (5:) . (6:)) []
    > (id . (2:) . (3:) . (4:) . (5:))       [6]
    > (id . (2:) . (3:) . (4:))            [5,6]
    > (id . (2:) . (3:))                 [4,5,6]
    > (id . (2:))                      [3,4,5,6]
    >  id                            [2,3,4,5,6]
    > [2,3,4,5,6]
    

    请注意,在这两种情况下,我们都要做两倍的工作量:

    1. 首先,我们以相反的顺序累积中间结果。
    2. 接下来,我们按照正确的顺序生成最终结果。
    3. 有些函数可以在累加器传递样式中有效写入(例如sum函数):

      sumA :: Num a => [a] -> a -> a
      sumA []     = id
      sumA (x:xs) = sumA xs . (+ x)
      

      原始的sum函数可以恢复如下:

      sum :: Num a => [a] -> a
      sum xs = sumA xs 0
      

      请注意,我们不需要对结果进行任何后期处理。

      但是,以尾递归样式编写的列表函数总是需要反转。因此,我们不以尾递归样式编写列表函数。相反,我们依赖懒惰来处理所需的列表。

      应该注意的是,延续传递风格只是累加器传递风格的一个特例。由于foldl都是尾递归并使用累加器,因此您可以使用mapA编写mapKfoldl,如下所示:

      mapA :: (a -> b) -> [a] -> [b] -> [b]
      mapA f xs acc = foldl (\xs x -> f x : xs) acc xs
      
      mapK :: ([b] -> r) -> (a -> b) -> [a] -> r
      mapK k f xs = foldl (\k x xs -> k (f x : xs)) k xs []
      

      对于,mapK如果您将k视为id,则会获得map

      map :: (a -> b) -> [a] -> [b]
      map f xs = foldl (\k x xs -> k (f x : xs)) id xs []
      

      同样,filter

      filter :: (a -> Bool) -> [a] -> [a]
      filter p xs = foldl (\k x xs -> k (if p x then x : xs else xs)) id xs []
      

      你有它,尾递归mapfilter函数。但是,不要忘记他们实际上做了两倍的工作。此外,他们不会为无限列表工作,因为在到达列表末尾之前不会生成结果(对于无限列表永远不会发生)。

答案 1 :(得分:2)

我怀疑教授/讲师正在期待使用尾递归的解决方案"直接",即词汇,在函数的源代码中,而不是间接地,或动态"动态& #34;,其中尾递归仅在某些子例程调用范围内的运行时发生。

否则,您也可以提供,例如Prelude.foldl作为您自定义foldl的实现,因为它可能会在引擎盖下使用尾递归,因此可以使用它:

import Prelude as P

foldl = P.foldl

但显然不会接受这样的事情。