【问题标题】:Is there a more clean, elegant, Haskell way to write this LCM function?有没有更干净、优雅、Haskell 的方式来编写这个 LCM 函数?
【发布时间】:2015-11-16 17:11:37
【问题描述】:

我刚开始使用 Haskell,并敲定了这个简单的递归算法,以找到列表中每个数字的 LCM。它可以工作,但很混乱,我希望能获得一些同行评议,了解如何使它更优雅、更易读和更符合 Haskell 风格。

lcms list 
  | length list > 1 = lcms (lcm (head list) (head (tail list)):(tail (tail list)))
  | otherwise = list

因此,它需要一个列表并对前两项进行 LCM,然后将其添加到列表中减去这两个元素。基本上,我要的伪代码是这样的:

lcms [a,b,c] = lcm (a, (lcm (b, c))

有什么建议吗?我渴望在 Haskell 上有所改进,写出人们可以真正阅读的东西。也欢迎效率提示!

谢谢大家!

【问题讨论】:

    标签: haskell recursion fold readability lcm


    【解决方案1】:

    这是一个折叠:

    import Data.List
    
    lcms :: [Int] -> Int
    lcms xs = foldl' lcm 1 xs
    

    lcm 仅计算两个数字的 lcm:

    lcm :: Int -> Int -> Int
    

    【讨论】:

    • 你也可以使用foldr,如lcms = foldr lcm 1
    • @clball foldl 几乎从来都不是你想要的。
    • 这篇文章对这三者进行了深入的处理,最后附上了一些经验法则。 wiki.haskell.org/Foldr_Foldl_Foldl%27
    • @user5402 foldrfoldl' 都可能是合理的选择,具体取决于使用情况。考虑到这种特殊用途的类型限制为 Int - 一种固有的严格类型 - 我认为 foldl' 是自然选择。
    • @PyRulez,我热切地等待您对惰性自然列表的首选 LCM 算法进行性能分析。
    【解决方案2】:

    您几乎可以使用您建议的语法编写它,因此:

    lcms (a:b:c) = lcms (lcm a b:c)
    lcms list = list
    

    我发现第二个子句有点奇怪,但并不可怕:它可以优雅地处理空列表,尽管当你知道你最多会返回一个项目时返回一个列表可能会被一些 hasochist 认为有点不精确类型。您还可以考虑使用 Maybe,这是规范的 0 或 1 元素类型:

    lcms (a:b:c)  = lcms (lcm a b:c)
    lcms [answer] = Just answer
    lcms []       = Nothing
    

    另一个不错的选择是确定一个合理的基本案例。对于二元运算,运算的单位通常是一个不错的选择,所以对于lcm,我会选择1,因此:

    lcms (a:b:c)  = lcms (lcm a b:c)
    lcms [answer] = answer
    lcms []       = 1
    

    一般来说,尽可能避免显式递归;另一个答案显示了如何迈出这一步。在这种情况下,有一个中间转换使基本情况在美学上更令人愉悦:而不是将累加器保持在列表的开头——这只是偶然地起作用,因为你的累加器与列表元素的类型相同——一个可以使累积更明确或更少。因此,其中之一:

    lcms (x:xs) = lcm x (lcm xs)
    lcms []     = 1
    
    -- OR
    
    lcms = go 1 where
        go acc (x:xs) = go (lcm acc x) xs
        go acc []     = acc
    

    这两个实现对应选择foldrfoldl来消除显式递归; foldl' 与第二个类似,但多了一个seq

    lcms = go 1 where
        go acc (x:xs) = let acc' = lcm acc x in acc' `seq` go acc' xs
        go acc []     = acc
    

    【讨论】:

    • 感谢非常详细的解释!为什么通常避免显式递归?只是通常不是做某事的最佳方式吗?此外,您能够实现几乎完全是伪代码的解决方案真的很酷。这真是太棒了。
    • @clball 通常使用像mapfilter 和类似的函数(在一定程度上是foldr)优于一般递归,因为插入错误的方法更少,并且因为更便于读者一目了然。如果我看到递归,我总是想看看它有哪些特殊情况使其不适合标准模式。
    • @clball 请注意此解决方案如何避免headtail。这些函数很危险,因为如果您在空列表中调用它们,它们会使您的程序崩溃。然后,您就有责任小心地插入防护装置,例如您的length list > 1。相反,使用上述模式匹配不会使程序崩溃,除非您没有涵盖所有可能的情况。幸运的是,如果定义不详尽,-Wall 会警告您。我觉得这非常很有帮助。此外,模式匹配可以使代码更具可读性。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-08-31
    • 1970-01-01
    • 2019-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-20
    相关资源
    最近更新 更多