用于简化递归的惯用 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
代表x
从n
开始到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 绑定不会有任何疑问,我已在我的下方添加了您的建议。
我会使用where
。 go x y n | n == 1 || n <= y = x | pn > y = go n pn (n-1) | otherwise = go x y (n-1) where pn = p n
.
介意我试试看吗?
关于p n
不绑定名字是否会被重新计算的问题;经过优化,可能不会。但可以肯定的是,需要查看核心。但是,我学会了永远不要盲目地信任编译器,并且引入本地绑定比查看核心要少工作,并且只要编译器甚至是四分之一理智,我就会做预期的事情,所以我在这种情况下这样做.【参考方案2】:
好的,让我看看我是否正确地解决了这个问题……你是说p n < n
是所有有趣的n
。并且您想计算p x
的x = 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 代码的主要内容,如果未能解决你的问题,请参考以下文章