Haskell 什么时候将foldr与continuation一起用作累加函数?

Haskell 什么时候将foldr与continuation一起用作累加函数?,haskell,io,fold,continuation-passing,Haskell,Io,Fold,Continuation Passing,有一种技术我已经在foldr上见过好几次了。它涉及使用一个函数来代替foldr中的累加器。我想知道什么时候需要这样做,而不是使用一个仅仅是常规值的累加器 大多数人在使用foldr定义foldl时都见过这种技术: myFoldl :: forall a b. (b -> a -> b) -> b -> [a] -> b myFoldl accum nil as = foldr f id as nil where f :: a -> (b ->

有一种技术我已经在
foldr
上见过好几次了。它涉及使用一个函数来代替
foldr
中的累加器。我想知道什么时候需要这样做,而不是使用一个仅仅是常规值的累加器

大多数人在使用
foldr
定义
foldl
时都见过这种技术:

myFoldl :: forall a b. (b -> a -> b) -> b -> [a] -> b
myFoldl accum nil as = foldr f id as nil
  where
    f :: a -> (b -> b) -> b -> b
    f a continuation b = continuation $ accum b a
这里,组合函数
f
的类型不仅仅是
a->b->b
像普通函数一样,而是
a->(b->b)->b->b
。它不仅需要一个
a
b
,而且还需要一个延续(
b->b
),我们需要将
b
传递给它才能得到最终的
b

我最近在书中看到了一个使用这种技巧的例子。是指向使用此技巧的示例源代码的链接。是本书中解释此示例的章节的链接

我冒昧地将源代码简化为一个类似(但更短)的示例。下面是一个函数,它获取字符串列表,打印出每个字符串的长度是否大于5,然后仅打印长度大于5的字符串的完整列表:

import Text.Printf

stringsOver5 :: [String] -> IO ()
stringsOver5 strings = foldr f (print . reverse) strings []
  where
    f :: String -> ([String] -> IO ()) -> [String] -> IO ()
    f str continuation strs = do
      let isGreaterThan5 = length str > 5
      printf "Working on \"%s\", greater than 5? %s\n" str (show isGreaterThan5)
      if isGreaterThan5
        then continuation $ str : strs
        else continuation strs
下面是一个使用GHCi的示例:

> stringsOver5 ["subdirectory", "bye", "cat", "function"]
Working on "subdirectory", greater than 5? True
Working on "bye", greater than 5? False
Working on "cat", greater than 5? False
Working on "function", greater than 5? True
["subdirectory","function"]
就像在
myFoldl
示例中一样,您可以看到组合函数
f
使用了相同的技巧

然而,我突然想到,这个
stringsOver5
函数可能不用这个技巧就可以编写:

stringsOver5PlainFoldr :: [String] -> IO ()
stringsOver5PlainFoldr strings = foldr f (pure []) strings >>= print
  where
    f :: String -> IO [String] -> IO [String]
    f str ioStrs = do
      let isGreaterThan5 = length str > 5
      printf "Working on \"%s\", greater than 5? %s\n" str (show isGreaterThan5)
      if isGreaterThan5
        then fmap (str :) ioStrs
        else ioStrs
(尽管您可能会提出以下论点:
IO[String]
?)


关于这一点,我有两个问题:

  • 是否绝对有必要使用将延续传递给
    foldr
    的技巧,而不是使用具有正常值的
    foldr
    作为累加器?是否有一个函数的示例,它绝对不能使用正常值的
    foldr
    编写?(当然,除了foldl和类似的函数之外。)
  • 我什么时候想在自己的代码中使用这个技巧?有没有任何函数可以通过使用此技巧显著简化的示例
  • 在使用此技巧时,是否需要考虑任何类型的性能因素?(或者,当不使用此技巧时?)
关于这一点,我有两个问题:

对于“两个”的某些较大值,-p

  • 是否绝对有必要使用这种将延续传递给foldr的技巧?是否有一个函数的示例 没有这个技巧绝对写不出来?(除了foldl和 当然是这样的功能。)
不,从来没有。每次
foldr
调用都可以被显式递归替换

当代码变得更简单时,应该使用
foldr
和其他众所周知的库函数。如果没有,则不应将代码钉在鞋钉上,使其符合
foldr
模式

在没有明显替换的情况下,使用简单的递归并不羞耻

将您的代码与此进行比较,例如:

stringsOver5 :: [String] -> IO ()
stringsOver5 strings = go strings []
  where
  go :: [String] -> [String] -> IO ()
  go []     acc = print (reverse acc)
  go (s:ss) acc = do
      let isGreaterThan5 = length str > 5
      printf "Working on \"%s\", greater than 5? %s\n" str (show isGreaterThan5)
      if isGreaterThan5
        then go ss (s:acc)
        else go ss acc
  • 我什么时候想在自己的代码中使用这个技巧?是否有任何函数的示例可以通过使用 这个把戏
依我拙见,几乎从来没有

就我个人而言,在大多数情况下,“使用四个(或更多)参数调用
foldr
”是一种反模式。这是因为它不比使用显式递归短,并且可能可读性差得多

我认为这个“习语”对于任何一个从未见过它的哈斯凯勒来说都是相当令人费解的。可以说,这是一种后天养成的品味

也许,当连续函数本身有意义时,使用这种样式可能是一个好主意。例如,当将列表表示为差异列表时,差异列表的常规列表的串联可能非常优雅

foldr (.) id listOfDLists []
是美丽的,即使最后一个
[]
一开始可能令人困惑

  • 在使用此技巧时,是否需要考虑任何类型的性能因素?(或者,当不使用此技巧时?)
性能应该与使用显式递归基本相同。GHC甚至可以生成完全相同的代码


也许使用
foldr
可以帮助GHC触发一些折叠/构建优化规则,但我不确定在使用continuations时是否需要这样做。

谢谢您的回答!读了你的答案后,我意识到我的第一个问题问得不对。我想问的是,
是否绝对有必要使用这个技巧,将continuation传递给foldr,而不是使用具有正常(非函数)值的foldr作为累加器?
我编辑了我的问题,以添加缺少的细微差别。我很清楚折叠和展开总是可以用原始递归替换(反之亦然)!然而,你的其余答案非常有用!(感谢您学习了“两个”与“三个”:-)DList示例是一个很好的例子,用于说明这种技巧是必要的(嗯,在使用DLists时基本上是必需的)。@illabout我认为当您有一个必须从左到右传递的输入值(如
foldl
)时,“需要”这个技巧,相反,输出必须从右向左生成(如
foldlr
)。也可以考虑<代码> MaPixLab< /COD>和<代码> -R >代码>,需要类似的技巧。有时,这一技巧是可以避免的(例如,
drop 1
应该可以不使用)。尽管如此,所有需要这种方法的示例对我来说都有点像
foldl
-ish。值得注意的是,
foldr
有时是显式递归的巨大性能优势。如果它消耗“好生产者”的输出,
foldr
可以参与列表融合。