【问题标题】:When to use foldr with a continuation as an accumulation function?何时使用带有延续的 foldr 作为累积函数?
【发布时间】:2018-06-08 14:27:31
【问题描述】:

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。它不仅需要ab,还需要一个延续(b -> b),我们需要将b 传递给以得到最终的b

我最近在Parallel and Concurrent Programming in Haskell 一书中看到了一个使用这个技巧的例子。 Here 是使用此技巧的示例源代码的链接。 Here 是本书解释这个例子的章节的链接。

我冒昧地将源代码简化为一个类似(但更短)的示例。下面是一个函数,它接受一个字符串列表,打印出每个字符串的长度是否大于 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]is a continuation的论点?)


对此我有两个问题:

  • 是否绝对有必要使用这种将延续传递给foldr 的技巧,而不是使用带有正常值的foldr 作为累加器?有没有一个绝对不能使用foldr和正常值编写的函数的例子? (当然,除了foldl 和类似的功能。)
  • 我什么时候想在我自己的代码中使用这个技巧?有没有使用这个技巧可以显着简化的函数示例?
  • 在使用此技巧时是否需要考虑任何类型的性能注意事项? (或者,当使用这个技巧的时候?)

【问题讨论】:

    标签: haskell io fold continuation-passing


    【解决方案1】:

    对此我有两个问题:

    对于一些较大的“二”值:-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”是一种反模式。这是因为它并不比使用显式递归更短,并且有可能更不可读。

    我认为这个“成语”对于任何没有见过它的 Haskeller 来说都是相当令人费解的。可以这么说,这是一种后天习得的品味。

    也许,当延续函数本身就有意义时,使用这种风格可能是个好主意。例如,当将列表表示为差异列表时,差异列表的常规列表的连接可以非常优雅

    foldr (.) id listOfDLists []
    

    很漂亮,即使最后一个[] 起初可能令人费解。

    • 在使用此技巧时是否需要考虑任何类型的性能注意事项? (或者,好吧,什么时候不使用这个技巧?)

    性能应该与使用显式递归基本相同。 GHC 甚至可以生成完全相同的代码。

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

    【讨论】:

    • 感谢您的精彩回答!阅读您的答案后,我意识到我没有正确地问我的第一个问题。我的意思是,Is it every absolutely necessary to use this trick of passing a continuation to foldr, instead of using foldr with a normal (non-function) value as an accumulator? 我已经编辑了我的问题以添加缺失的细微差别。我很清楚折叠和展开总是可以用原始递归替换(反之亦然)!但是,您的其余答案非常有帮助! (感谢您了解“二”与“三”:-)
    • DList 的例子是一个很好的例子,当这种技巧是必要的(嗯,它基本上在使用 DList 时是必需的)。
    • @illabout 我认为当你有一个必须从左到右传递的输入值(如foldl)时,一个人“需要”这个技巧,而输出必须是正确的-向左(如foldlr)。也许人们也可以认为mapAccumL-R 需要一些类似的技巧。有时可以避免这个技巧(例如,drop 1 应该可以不用)。尽管如此,所有需要这样做的示例对我来说都有些foldl-ish。
    • 值得注意的是,foldr 有时在性能上胜过显式递归。如果它正在消耗“好生产者”的输出,foldr 可以参与列表融合。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-04-30
    • 1970-01-01
    • 1970-01-01
    • 2018-05-13
    • 1970-01-01
    • 1970-01-01
    • 2021-10-04
    相关资源
    最近更新 更多