用于简化递归的惯用 Haskell 代码

Posted

技术标签:

【中文标题】用于简化递归的惯用 Haskell 代码【英文标题】:Idiomatic Haskell code to simplify recursion 【发布时间】:2013-03-16 04:31:33 【问题描述】:

我需要计算foo n = maximumBy (comparing p) [1..n],其中p :: Int -> Int 很慢。但我知道p n < n 代表所有n > 0,并希望通过以下方式加快计算速度:我计算p x 代表xn 开始到1,记住当前最大。一旦我达到小于或等于当前最大值的x,我就知道这个最大值必须是全局最大值,我就完成了。

所以我的尝试看起来像这样:

foo n = go (0, 0) n where
    go (c, _) 1 = c
    go (c, c') !x = if c' >= x then c else go (c2, c'2) (x-1) where
        x' = p x
        (c2, c'2) = if c' >= x' then (c, c') else (x, x')

这可行,但看起来不是很惯用。所以我正在寻找一个更优雅的解决方案。你有什么建议吗?

【问题讨论】:

【参考方案1】:

您可以使用模式匹配来减少使用 if ... then ... else 另一个技巧是给你的变量一个数字,它可以让你记住起始情况 var0 并且对于另一个递归调用,你可以使用更好的 var 最后请注意,您有一些 if 在相同形式的谓词之后返回相同的值并共享相同的环境,那么您可以将它们组合在一起。

foo n0 = go (0, 0) n0
  where
    go (x, y) n
        | (n  == 1) || (y >= n) = x
        | y < (p n) = go (n, (p n)) (n-1)
        | otherwise = go (x, y) (n-1)

考虑评论重写,

foo n0 = go 0 0 n0
  where
    go x y n
        | (n  == 1) || (y >= n) = x
        | pn > y                = go n pn (n-1)
        | otherwise             = go x y (n-1)
          where
            pn = p n

【讨论】:

去掉这对,把它改成go x y n ...,我会把p n绑定到一个名字上(甚至不给编译器重新计算它的机会),否则我就是这样做。简单、清晰、高效。 谢谢,按照您的建议更换这对,肯定会更好。无论如何,我必须承认我对在绑定中使用 let ... 犹豫不决,因为我真的不知道在我的第二个模式匹配中, (p n) 是否会计算两次。如果我必须证明我的代码的合理性,我会认为 Haskell 本质上是懒惰的,这意味着它的评估策略是按需调用,并且根据定义“按需调用是按名称调用的记忆版本”,那么它应该没问题,不是吗?确实,按照您的建议使用 let in 绑定不会有任何疑问,我已在我的下方添加了您的建议。 我会使用wherego x y n | n == 1 || n &lt;= y = x | pn &gt; y = go n pn (n-1) | otherwise = go x y (n-1) where pn = p n. 介意我试试看吗? 关于p n不绑定名字是否会被重新计算的问题;经过优化,可能不会。但可以肯定的是,需要查看核心。但是,我学会了永远不要盲目地信任编译器,并且引入本地绑定比查看核心要少工作,并且只要编译器甚至是四分之一理智,我就会做预期的事情,所以我在这种情况下这样做.【参考方案2】:

好的,让我看看我是否正确地解决了这个问题……你是说p n &lt; n 是所有有趣的n。并且您想计算p xx = n to 1,直到x 小于目前看到的最大p x

好吧,看起来您可以将所有 p x 计算为惰性列表。现在问题已简化为扫描此列表,直到找到所需内容。我建议takeWhile,除了我们还需要折叠 列表以找到当前最大值。嗯,也许我们可以将每个值与运行最大值配对?

类似

foo n =
  let
    ps = [ p x | x <- [n, n-1 .. 1] ]
    qs = fold (\ px' (px, maxPX) -> (px', maxPX `max` px') ) ps
  in last . takeWhile (\ (px, maxPX) -> px >= maxPX) qs

或类似的?

【讨论】:

以上是关于用于简化递归的惯用 Haskell 代码的主要内容,如果未能解决你的问题,请参考以下文章

什么时候需要显式递归?

惯用高效的 Haskell 追加?

安全执行不受信任的 Haskell 代码

惯用的 Haskell 中如何实现动态规划算法?

学习惯用 Haskell 的资源(eta 缩减、符号中缀运算符、库等)[关闭]

惯用列表构造