【问题标题】:Factorising an integer recursively and some questions about functions in Haskell递归分解整数以及有关 Haskell 中函数的一些问题
【发布时间】:2020-05-02 23:46:31
【问题描述】:

我刚从 Python 开始学习 Haskell,我有几个关于函数的问题。我写了以下代码:

--generating prime list
primes = sieve [2..]
sieve (p:ps) = p : sieve [x | x <- ps, mod x p /= 0]

--factorising function
--takes an input of a number and a list of primes and outputs a list of its prime factors
factorise (n,ps)
    | mod n head ps /= 0    = div n head ps : factorise (div n head ps, ps)
    | otherwise             = factorise (n,tail ps)

首先,当我尝试编译时,我得到一个与n相关的错误,说我cannot construct the infinite type: a ~ [a] -&gt; a,这是为什么?

其次,虽然我了解创建无限列表背后的逻辑,但为什么不必显式声明函数 sieve 的类型,是隐含的类型吗?对于factorise 函数,我必须这样做吗?

最后,有没有更简洁的方法来编写上述算法(据我所知效率非常高)?

【问题讨论】:

  • 提示:mod n head ps 需要一些括号...此外ps 可以为空。
  • why do you not have to explicitly state the types of the function sieve, are the types implied? 实际上,Haskell 具有类型推断功能,因此在编译代码时需要类型签名相对较少。但是,仍然强烈建议为此类顶级定义包含类型签名。它们可以更好地记录您的代码,并且通常会导致更易于理解的错误消息。
  • 我同意 Robin,但想补充一点,包括显式类型声明也可以导致编译器捕获错误,否则它会完全错过。
  • 新手添加不正确的类型签名也很常见,这会阻止编译器编译它自己会推断出正确类型的代码。

标签: haskell types primes prime-factoring type-signature


【解决方案1】:

我的解决方案(我忘了给出递归的基本情况以及其他一些更正):

--generating prime list
primes :: Integral a => [a]
primes = sieve [2..]

sieve   :: Integral a => [a] -> [a]
sieve (p:ps) = p : sieve [x | x <- ps, mod x p /= 0]

--factorising function
--takes an input of a number and a list of primes and outputs a list of its prime factors
factorise :: Integral a => (a, [a]) -> [a]
factorise (n, ps)
    |   n == 1          = []
    |   mod n f == 0    = f : factorise (v, ps)
    |   otherwise       = factorise (n, tail ps)
    where
        f = head ps
        v = div n f

【讨论】:

  • 这里有两个算法性能问题,都与sqrt及其逆相关。 :) 一个在sieve,另一个在factorise。提示:factorise (47, 2:3:5:7:undefined) 不应导致错误。第二个提示:take 1000 (sieve [2..]) 应该只创建一个由sieve 的 24 个嵌套调用组成的链,而不是 999; take 10000 ... - 只有 66,而不是 9999; take 1000000 ... - 只有 546,而不是 999999。
【解决方案2】:

在这里和那里添加一些括号可以解决问题:

factorise (n, ps)
    | mod n (head ps) /= 0  = div n (head ps) : factorise (div n (head ps), ps)
    | otherwise             = factorise (n, tail ps)

括号用于在 Haskell 中进行分组。 mod n (head ps)mod 对两个参数nhead ps 的应用;没有括号 mod n head psmod 对三个参数 nhead 和 ps` 的应用,这只是不进行类型检查。

确实在修复错误后所有类型都成功推断出来了,因为

primes :: Integral a => [a]
sieve   :: Integral a => [a] -> [a]
factorise :: Integral a => (a, [a]) -> [a]

现在您可以根据需要对它们进行专业化,因为

primes :: [Integer]
sieve   :: [Integer] -> [Integer]
factorise :: (a ~ Integer) => (a, [a]) -> [a]

(为factorise 编写类型的后一种方式需要启用GADTs 扩展)。

对于包含类型签名的原始代码,

factorise :: Integral a => (a, [a]) -> [a]
factorise (n,ps)
    | mod n head ps /= 0    = div n head ps : factorise (div n head ps, ps)
    | otherwise             = factorise (n,tail ps)

错误信息基本相同。它说“无法推断出(a ~ ([a] -&gt; a))”,但仍然表明,就编译器而言,a 类型也必须同时为[a] -&gt; a(因此它也必须为a ~ [a] -&gt; ([a] -&gt; a)a ~ [[a] -&gt; a] -&gt; ([a] -&gt; ([a] -&gt; a))等(因此在第一条错误消息中使用“无限”类型引用)。

如果这确实是您想要的类型,则可以通过将其命名并使其显式递归地使其合法,如

data T a = MkT ([T a] -> T a)

这个允许的。 Haskell 完全有能力拥有递归类型。毕竟简单的list类型是递归的,好像是由

定义的
data [] a = [] | a : ([] a)
data L a = Nil | Cons a (L a)

我将在factorise 中解决效率问题,以备后用。 sieve 虽然很简单:它的主要问题是它一次只产生一个素数,而它完全有能力在每一步产生更多,远不止于此。

你能找到方法吗?


您的factorise 函数完美呈现了简单的试除因式分解算法(除了一些简单的错误)。提高简洁性(以及随后的正确性!)的一种方法是使用let(或者更好的是where,以便能够在警卫中使用它们)引入临时变量,如

factorise (n, ps)
  | mod n f /= 0  =                        -- is the test correct?
                    v : factorise (v, ps)  -- is it really v : ... ?
  | otherwise     = factorise (n, tail ps)
     where
       f = head ps 
       v = div n f

...除了它永远不会停止!您必须包括一个额外的测试才能停止生成主要因素列表。

你能找到方法吗? :)

【讨论】:

  • 现在可以使用了!感谢您对错误的解释并澄清了我对类型的理解!
猜你喜欢
  • 2021-05-19
  • 1970-01-01
  • 2016-04-09
  • 1970-01-01
  • 2013-02-08
  • 1970-01-01
  • 2023-04-09
  • 1970-01-01
  • 2011-02-14
相关资源
最近更新 更多