【问题标题】:How is a recursive prime number checker written in Haskell?递归素数检查器是如何用 Haskell 编写的?
【发布时间】:2013-02-04 18:25:10
【问题描述】:

我对 Haskell(以及一般的函数式编程)非常陌生,我正在尝试一些基本练习来尝试理解该语言。我正在编写一个“朴素”的素数检查器,它将输入下的每个数字除以检查是否有余数。到目前为止,我学到的唯一结构是理解列表和递归函数,所以我仅限于此。这是我正在尝试的:

isprime 1 = False
isprime 2 = True
isprime n = isprimerec n (n-1)

isprimerec _ 1 = False
isprimerec n t = if (n `rem` t) == 0 then False else isprimerec n (n-1)

目的是用户将使用isprime n。然后isprime 将使用isprimerec 来确定该数字是否为素数。这是一种相当迂回的方式,但我对 Haskell 的了解有限,不知道其他方式。

当我尝试这个时会发生以下情况:

isprimerec 10 9

永远运行。我必须使用 Ctrl+C 来阻止它。

isprimerec 10 5

返回 False。 else 部分永远不会被评估,所以函数永远不会调用自己。

我不确定为什么会这样。此外,这是否接近于 Haskell 程序员解决这个问题的方式? (我不是说检查素数,我知道这不是这样做的方法。我只是作为练习这样做)。

【问题讨论】:

  • 你可以用isprime n = isprimerec n (n `div` 2)替换isprime n = isprimerec n (n-1),因为n不能被任何大于n/2的数整除
  • @CarlosLópez-Camey 我正在考虑这样做,但我担心它会返回浮点数而不是整数。那么div 是否只返回整数?
  • 是的,div的类型是Integral a => a -> a -> a,而Prelude中Integral只有两个实例:IntegerInt(这个可以用:i Integral查在 ghci 中)

标签: haskell primes


【解决方案1】:

问题出在这一行

isprimerec n t = if (n `rem` t) == 0 then False else isprimerec n (n-1)

您使用(n - 1) 作为第二个参数,它应该是(t - 1)。还有一点,我想你想要isprimerec _ 1 case = True

至于你更笼统的问题,这是否是惯用的,我认为你在正确的轨道上。 ghci 有一个不错的命令行调试器。我通过将您的代码放在一个文件中,加载它,然后发出命令:break isprimerec 发现了这一点。然后我调用了你的函数并使用:step 逐步完成它。

【讨论】:

    【解决方案2】:

    您的错误是一个简单的错字;在isprimerec 的末尾,您的第二个参数变为n-1 而不是t-1。但除此之外,该功能不是很地道。这是我如何编写它的第一遍:

    isPrime :: (Ord a, Integral a) => a -> Bool
    isPrime n | abs n <= 1 = False
    isPrime 2 = True
    isPrime n = go $ abs n - 1
      where go 1 = False
            go t = (n `rem` t /= 0) && go (t-1)
    

    (我可能会调用go 类似checkDivisors,但go 是循环的惯用语。)请注意,这会暴露代码中的错误:一旦goisPrime 的本地,你就不用'不需要传递n,因此更清楚的是递归它是不正确的。我所做的更改按重要性的粗略顺序排列:

    1. 我将isprimerec 设为本地函数。没有其他人需要调用它,我们丢失了额外的参数。

    2. 我对函数进行了总计。在0 上没有理由失败,也没有任何理由因为负数而失败。 (从技术上讲,p 是素数当且仅当 -p 是素数。)

    3. 我添加了一个类型签名。养成一个好习惯。使用Integer -&gt; Bool,甚至Int -&gt; Bool,也是合理的。

    4. 我切换到 interCaps 而不是全小写。只是格式化,但这是习惯性的。

    除了我可能会让事情变得更简洁。在 Haskell 中手动递归通常是不必要的,如果我们完全摆脱它,你的 bug 就变得不可能了。您的函数会检查从2n-1 的所有数字不除n,因此我们可以直接表示:

    isPrime :: (Ord a, Integral a) => a -> Bool
    isPrime n | abs n <= 1 = False
              | otherwise  = all ((/= 0) . (n `rem`)) [2 .. abs n - 1]
    

    你可以把它写成一行

    isPrime :: (Ord a, Integral a) => a -> Bool
    isPrime n = abs n > 1 && all ((/= 0) . (n `rem`)) [2 .. abs n - 1]
    

    但看到最后两种实现中的任何一种,我都不会感到惊讶。正如我所说,这些实现的好处是你的错字不可能在这些表示中出现:t 隐藏在all 的定义中,所以你不能不小心把它弄错价值。

    【讨论】:

    • 最后一个想法:使用-XViewPatterns 语言扩展名,您可以编写isPrime (abs -&gt; n) = n &gt; 1 &amp;&amp; ...,这可以说更简洁(如果您喜欢该语言扩展名)。
    【解决方案3】:

    您的else 分支已损坏,因为它每次都调用isprimerec n (n-1)。您可能应该写 isprimerec n (t-1) 来倒计时。

    您还可以使用高阶函数all 来简化此操作。

    isprime 1 = False
    isprime n = all (\t -> n `rem` t /= 0) [2..(n-1)]
    

    【讨论】:

      【解决方案4】:

      好吧,你有两个错误:你的

      isprimerec _ 1 = False
      isprimerec n t = if (n `rem` t) == 0 then False else isprimerec n (n-1)
      

      应该是

      isprimerec _ 1 = True
      isprimerec n t = if (n `rem` t) == 0 then False else isprimerec n (t-1)
      

      或者,使用列表理解,

      isprime n = n>1 && and [ rem n t /= 0 | t <- [n-1,n-2..2] ]
      

      (内部化那个额外的参数t,反正这是个技术问题!--啊哈,但是你问and是什么?它只是就像这个递归函数,foldr (&amp;&amp;) True :: [Bool] -&gt; Bool.)

      但现在一个主要的算法缺点变得视觉上很明显:我们以错误顺序进行测试。如果我们按升序测试会更快:

      isprime n = n>1 && and [ rem n t /= 0 | t <- [2..n-1] ]
      

      如果我们停在sqrt,甚至更快,

      isprime n = n>1 && and [ rem n t /= 0 | t <- [2..q] ]
         where  q = floor (sqrt (fromIntegral n))
      

      或仅在 2 之后通过 odds 进行测试(如果我们已经通过 2 进行了测试,为什么还要通过 6 进行测试/em> 已经?):

      isprime n = n>1 && and [ rem n t /= 0 | t <- 2:[3,5..q] ]
         where  q = floor (sqrt (fromIntegral n))
      

      或者只是通过 primes(如果我们已经通过 3 测试,为什么还要通过 9 测试?):

      isprime n = n>1 && and [ rem n t /= 0 | t <- takeWhile ((<= n).(^2)) primes ]
      primes = 2 : filter isprime [3..]  
      

      为什么要在过滤素数时测试 evens - 首先不生成它们不是更好吗?

      primes = 2 : filter isprime [3,5..]  
      

      但是isprime 总是用 2 测试除法——但我们只输入奇数;所以,

      primes = 2 : 3 : filter (noDivs (drop 1 primes)) [5,7..]
      noDivs factors n = and [ rem n t /= 0 | t <- takeWhile ((<= n).(^2)) factors ]
      

      为什么要生成 3 的倍数(即[9,15 ..] == map (3*) [3,5..]),只是为了稍后测试和删除它们? --

      {-       [5,7..]
          ==   [j+i | i<-[0,2..], j<-[5]]                 -- loop unrolling, 3x:
          ==   [j+i | i<-[0,6..], j<-[5,7,9]]
          == 5:[j+i | i<-[0,6..], j<-[7,9,11]]            
          == 5:[7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43, ...
          \\   [  9,      15,      21,      27,      33,      39,       ...
          ==   [j+i | i<-[0,6..], j<-[  9   ]]
      -}
      primes = 2:3:5: filter (noDivs (drop 2 primes)) 
               [j+i | i<-[0,6..], j<-[7,  11]]
      

      我们也可以提前跳过5的倍数(作为Euler's sieveeuler (x:xs) = x : euler (xs `minus` map (x*) (x:xs))中的另一个步骤):

      --       [j+i | i<-[0, 6..], j<-[7, 11]]            -- loop unrolling, 5x:
      --  == 7:[j+i | i<-[0,30..], j<-[11,13,17,19,23,25,29,31,35,37]]
      --  \\   [j+i | i<-[0,30..], j<-[               25,      35   ]]
      
      primes = 2:3:5:7: filter (noDivs (drop 3 primes)) 
               [j+i | i<-[0,30..], j<-[11,13,17,19,23,   29,31,   37]]
      

      ...但这已经在进行中far enough, for now

      【讨论】:

      • 最后两个例子中ij的值是根据什么选择的?
      • @is7s 我们从赔率开始,[5,7..] == [j+i | i&lt;-[0,2..], j&lt;-[5]] == [j+i | i&lt;-[0,6..], j&lt;-[5,7,9]]。最后一个与5:[j+i | i&lt;-[0,6..], j&lt;-[7,9,11]] 相同。 5被带走;然后9 被抛出,因为它生成 3 的倍数,本身就是 3 的倍数。然后留下[j+i | i&lt;-[0,6..], j&lt;-[7,11]]。这实际上遵循欧拉的筛子,eulers (x:xs) = x:(xs `minus` map(x*) (x:xs)),更多haskellwiki page
      • @is7s 类似地进行下一步,从[j+i | i&lt;-[0,6..], j&lt;-[7,11]] 我们经过五个“段/轮长度”到[j+i | i&lt;-[0,30..], j&lt;-[7,11,13,17,19,23,25,29,31,35]],然后从生成的种子值中删除25,357 被选中,所以 37 代替进来。
      • 不客气。 :) 这只是避免concat 的另一种方法。列表推导更加直观
      猜你喜欢
      • 1970-01-01
      • 2016-08-13
      • 2015-03-29
      • 1970-01-01
      • 1970-01-01
      • 2011-10-31
      • 1970-01-01
      • 1970-01-01
      • 2013-04-21
      相关资源
      最近更新 更多