【问题标题】:Most efficient or idiomatic way to test singleton list contents in Haskell?在 Haskell 中测试单例列表内容的最有效或惯用的方法是什么?
【发布时间】:2014-08-21 03:57:50
【问题描述】:

这个问题现在对我来说是严格的学术问题,但有一天我会看到它有实际应用。通过 Haskell 自学,我成功地构建了无限的素数列表。其中一部分是这个函数:

isPrime n
    | n < 2                         = False
    | head (primeFactorsOf n) == n  = True
    | otherwise                     = False

其中primeFactorsOf 以升序返回数字的质因数列表。 1 不是素数,所以素数n 的素数是单例列表[n]。因此,第二个保护案例可以替换为:

    | primeFactorsOf n == [n]       = True

其中一个比另一个更有效吗?如果不是,是一种更好的风格吗?我的直觉是调用 head 并比较两个简单的数字比调用 cons 并比较两个单例列表要快,所以我已经得到的将是最好的。但如果没有区别,我认为替代方案看起来更干净。

【问题讨论】:

  • 我不认为这是你的算法的一部分,速度真的很重要,GHC 可能会优化一点。也就是说,我认为第一个很好。或者您可以编写一个 smallestPrimFactor 函数,该函数仅在 primeFactorsOf n 上使用模式匹配,使其更具可读性和可理解性。
  • 只是一个旁注。 | foo = bar | xyz = True | otherwise = False 写成| foo = bar | otherwise = xyz 可能更好。
  • @n.m.奇怪的是,这是我在任何命令式语言中默认会做的事情,但我花了大约 30 秒才弄清楚你可能的意思! Haskell 的第一步是“忘掉一切”。我确定第 3 步将是“忘记第 1 步”。

标签: performance list haskell comparison


【解决方案1】:

如果速度真的很重要,唯一确定的方法就是对其进行基准测试,我建议criterion。哪个提供更好的性能尚不完全清楚。如果primeFactorsOf 被内联,那么编译器可能会注意到您正在比较第二种情况下的两个列表并自动删除装箱。也可能不是,在这种情况下,您的预感很可能是正确的。

至于哪种风格更好,第二种是。大多数时候最好避免使用像head 这样的部分函数。但是,也许您可​​以做得更好?

isPrime n = case primeFactorsOf n of
  [n'] | n == n' && n >= 2 -> True
  _ -> False

或者您可以将n &gt;= 2 的支票放在外面。或者,如果您知道primeFactorsOfn &lt; 2 返回一个空列表,则可以完全省略它。

编辑:打高尔夫球

isPrime n = case primeFactorsOf n of
  [_] -> n > 1
  _ -> False

【讨论】:

  • 我会说case primeFactorsOf n of { [_] -&gt; True; _ -&gt; False }
  • 列表中只有 1 和 -1 为空。负数可以有质因数,零有无限集,所以 [_] -> n > 1 看起来是最干净的解决方案!
  • @TheodoreLiefGannon 你是完全正确的,这是一个模式匹配和一个守卫。如果其中任何一个失败,则 case 将进入下一个语句,这是一个通配符,因此它将始终匹配。
【解决方案2】:

(total rewrite) 是的,您的第一个变体更好。相当于

isPrime n = n > 1 && head fs == n
    where
       fs = primeFactorsOf n

第二个变种是

isPrime n = n > 1 && fs == [n]
    where
       fs = primeFactorsOf n

相当于

isPrime n = n > 1 && head fs == n && null (tail fs)
    where
       fs = primeFactorsOf n

对于复合材料,两者的行为相同,但对于素数,后一种变体将不必要地执行额外的测试。

我们可能会想“简化和改进”最后一个变体

isPrime n = n > 1 && null (tail fs)
    where
       fs = primeFactorsOf n

这将节省素数的数字比较操作,但对于复合它实际上会强制计算 n 的第二个因子,这是不必要的,这可能会非常昂贵(例如 n = 2 * pp 一些大素数)。您的第一个变体将找到第一个因素并立即返回 False


为了纠正这个问题,我们可以手动“融合”isPrimeprimeFactors。假设它被定义为

primeFactors n = factor n primes
    where
        factor n (p:ps) 
            | p*p > n        = [n]
            | n `mod` p == 0 = p : factor (n `div` p) (p:ps)
            | otherwise      = factor n ps

融合上一个版本,变成了

isPrime n = n > 1 && hasOneFactor n primes  -- (null . tail . primeFactorsOf)
    where                                   --   == hasOneFactor
        hasOneFactor n (p:ps) 
            | p*p > n        = True
            | n `mod` p == 0 = False
            | otherwise      = hasOneFactor n ps

这是现在最有效的代码。它可以进一步重写为(另外,remmod 快)

isPrime n = n > 1 && hasOneFactor primes    -- no need to pass `n` around
    where
        hasOneFactor (p:ps) = p*p > n ||
                              ( n `rem` p /= 0 && 
                                hasOneFactor ps )

isPrime n = n > 1 && foldr (\p r-> p*p > n || (rem n p /= 0 && r))
                           undefined primes

【讨论】:

  • 第二个变体究竟如何不比较两个单例列表?在我看来,这正是发生的事情。编译器可能会优化它,但可能不会(我检查时没有优化,但 YMMV 取决于上下文)。
  • 是的,但是如果您要比较两个列表,您首先必须检查外部构造函数是[] 还是:[n] 是哪个?最好不要是[],这只会留下缺点。
  • 啊,我想我明白我不清楚的地方。我是说在比较primeFactorsOf n == [n] 时,RHS 有一个额外的间接([n] === n:[] === cons n [],它比head (primeFactorsOf n) == n 效率低(正如 OP 所建议的那样),因为在后一种情况下,RHS 的间接性更少,并且只有LHS 需要解构。这可能不是一个重要的问题,除非在一个非常紧密的循环中。我希望 GHC 在某些情况下可以为您移除它。
  • 我怀疑 OP 正在做与我相同的事情,并且在说“比较两个单例列表”时只考虑 True 情况(性能问题适用于 WLOG 两种情况)。
猜你喜欢
  • 1970-01-01
  • 2017-07-28
  • 2017-06-12
  • 1970-01-01
  • 2015-10-23
  • 1970-01-01
  • 1970-01-01
  • 2021-07-21
  • 2012-01-02
相关资源
最近更新 更多