【问题标题】:I'm confused about what's recursive, tail recursive, primitive recursive and what's not我对什么是递归、尾递归、原始递归以及什么不是
【发布时间】:2015-09-17 16:43:06
【问题描述】:

我已经写了一个简单的猜数程序,我需要知道它是否涉及任何类型的递归,以及它是什么类型(原始/尾部)(我是新手,所以请多多包涵我)

module MyProgram where
import System.Random

guessNum :: IO()
guessNum =
do  --gen <- newStdGen
    --let num = randoms gen :: [Int]
    num<-randomRIO(20,100 :: Int)
    putStrLn "Guess the number: "
    input<-getLine
    let guess = (read input :: Int)
    checkGuess guess num


checkGuess:: Int -> Int -> IO()
checkGuess guess num1 |(num1<guess)=
                do  putStr"Too high! Guess again!"
                    input<-getLine
                    let guess = (read input)::Int
                    checkGuess guess num1
                |(num1>guess)=          
                do  putStr"Too low! Guess again!"
                    input<-getLine
                    let guess = (read input)::Int
                    checkGuess guess num1
                |(num1==guess)  =
                do  putStr"Congratulations! You found the number!"

【问题讨论】:

  • 我不是 haskell 专家,但递归是函数调用自身时,我相信“尾递归”仅在自调用是当前迭代执行的最后一条语句时发生(即,它不需要将函数保留在堆栈上,以便它可以返回并执行更多语句)
  • 我需要知道我在 checkGuess 中的函数调用 checkGuess 是否涉及递归,因为它不使用之前出现的任何信息,而是在每个实例中传递 num1
  • 是的,它是递归的。它不必“使用以前发生的任何信息”。如果它调用自己,它是递归的。仅此而已。
  • 在 Haskell 的上下文中,您在哪里遇到了原始递归这个术语?原始递归与 Haskell 没有任何关系。一个函数是否是尾递归的会对 Haskell(以及许多其他函数式语言)的性能产生影响,但它是否是原始递归的,在 TCS 上下文之外并不重要。

标签: haskell recursion functional-programming tail-recursion


【解决方案1】:

如果函数调用自己,则它是递归的(不一定在每种情况下,但至少在一种情况下)。例如:

sum [] = 0
sum (x:xs) = x + sum xs

上面的函数不是尾递归的。在第二个等式中,首先计算xsum xs,最终结果是它们的总和。由于最终结果不是对函数的调用,因此它不是尾递归的。要将这个函数转换为尾递归,我们可以使用累加器模式:

sum [] acc = acc
sum (x:xs) acc = sum xs (x + acc)

注意现在第二个等式首先计算xsx + acc,作为最后一步,它调用自身。尾递归函数很重要,因为它们可以系统地转换为循环,从而消除函数调用的开销。有些语言会进行这种优化,我认为 Haskell 中不需要这种优化(也请参阅下面的 hammar 评论)。

您的函数 checkGuess 可能看起来是尾递归的,但事实并非如此。 do 语法是使用 &gt;&gt;= 运算符的语法糖。

f = do
    x <- g
    h x

转化为

f = g >>= (\x -> h x)

因此,几乎在每个 do 表示法中,最后一个被调用的函数都是 &gt;&gt;=

如果一个函数可以使用here 中描述的 5 个构造来构造,那么它就是原始递归的。加法、乘法和阶乘是原始递归函数的示例,而 Ackermann 函数则不是。

这在可计算性理论中通常很有用,但就编程而言,通常并不关心(编译器不会尝试对此做任何事情)。

注意事项:

  • 可以说,如果一组函数相互调用的方式具有循环(f 调用 g,g 调用 h,h 最终调用 f),则它们是相互递归的。

【讨论】:

  • 值得注意的是,由于惰性,尾递归本身通常不足以在 Haskell 中获得恒定的空间循环。您通常还需要在累加器上使用seqBangPattern,以确保在每一步都对其进行评估,以免产生巨大的冲击。
  • 需要提一下,“调用自身”不一定是直接的,可以有函数ping调用pongpong调用ping,还是这样被认为是“递归的”。
  • @Landei 被称为相互递归
  • @is7s:相互递归的代码是递归的,因此得名。递归意味着函数是根据自身定义的。如果 ping 是根据 pong 定义的,而 pong 又是根据 ping 定义的,那么 ping 仍然是根据自身定义的。如果循环中涉及的所有调用都是尾调用,则一般的尾调用优化甚至可以在恒定空间中执行此类函数。
【解决方案2】:

如果函数在其代码中的任何位置调用自身,则该函数是递归的。所以guessNum 不是(在guessNum 代码或它调用的代码中没有调用guessNum),而checkGuess 是。

尾递归是当递归调用是函数做的最后一件事......但这是 Haskell 和尾递归是一个主要用于严格语言的术语,它允许优化递归函数使其不会增长堆栈(当前调用可以直接替换为递归调用,因为在递归调用返回后您无需执行任何操作)。因此,正如其他人所说 checkGuess 不是尾递归或不会使用严格的语言......但是,对于惰性语义, (a >> b) 将在许多 Monad(包括 IO)中被评估为 b 因为一旦 a 是评估(或者更确切地说是 IO 操作已完成),它可以被遗忘,b 的返回是唯一重要的事情。

简而言之,您的函数 checkGuess 是递归的,而不是大多数正式定义的尾递归,但这些定义不适合 Haskell 等非严格语言,并且 checkGuess 肯定会在常量空间中执行,就好像它是尾一样以严格的语言递归(至少在合理的 Haskell 实现中,例如 GHC)。

原始递归是在 N^k -> N 个函数上定义的概念,我认为这个问题对于 checkGuess 这样的函数没有意义,并非没有一些可疑的改编,并以更简单的方式查看函数的翻译语言(lamda-calculus 等价物),这意味着做出明确的 IO 语义等等……我想说的是,尽管您的函数对其 Int 参数没有做任何事情,而这对于原始递归函数来说是不可能的。

请注意,您的代码会重复自己,也许真正应该抽象出来的部分是:

input<-getLine
let guess = (read input :: Int)
checkGuess guess num

【讨论】:

    【解决方案3】:

    尾递归是指函数调用自身后什么都不做 这通常通过返回下一个递归调用来完成。

    所以你的在某种程度上是尾递归,因为在你的 checkGuess 被递归调用后你什么都不做。

    【讨论】:

    • 不是尾递归的。最后一个调用是 (>>=)。给定 IO monad 的某些翻译,编译器可能会将其转换为尾递归。
    猜你喜欢
    • 2011-04-10
    • 2013-09-14
    • 1970-01-01
    • 1970-01-01
    • 2010-11-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-02
    相关资源
    最近更新 更多