再帰はそれ自体が機能ですか?


116

...それとも単なる練習ですか?

私は教授との議論のためにこれを求めています:クラスで再帰をカバーしなかったという理由で関数を再帰的に呼び出すことの信用を失いました、そして私の議論は学習returnと方法によって暗黙的にそれを学んだということです。

誰かが明確な答えを持っていると思うので、私はここで尋ねています。

たとえば、次の2つの方法の違いは何ですか。

public static void a() {
    return a();
    }

public static void b() {
    return a();
    }

a永久に継続する」(実際のプログラムでは、無効な入力が提供されたときにユーザーに再度プロンプトを表示するために正しく使用されます)以外に、との間に基本的な違いはaありbますか?最適化されていないコンパイラにとって、それらはどのように異なる方法で処理されますか?

最終的にはそれがに学習することでかどうかに降りてくるreturn a()からb、我々はまたに学んだそのことreturn a()からa。しましたか?


24
優れた議論。このように教授に説明されたのでしょうか。もしそうなら、私は彼があなたに失われた信用を与えるべきだと思います。
Michael Yaworski、2014年

57
再帰は、コンピュータサイエンス専用の概念ではありません。フィボナッチ関数、階乗演算子、および数学(およびおそらく他のフィールド)からの他の多くのものは、再帰的に表現されます(または少なくとも表現できます)。教授はあなたにもこれらのことに気づいていないことを要求しますか?
Theodoros Chatzigiannakis 14年

34
教授は問題を解決するためのエレガントな方法を考え出すために、あるいは発想から発想のために、代わりに彼に追加の信用を与えるべきです。

11
割り当ては何でしたか?これは、プログラミングの課題を提出するときに、マークされているもの、問題を解決する能力、または学んだことを使用する能力について、私がよく疑問に思っていた問題です。これら2つは必ずしも同じではありません。
レオン

35
FWIW、適切になるまで入力を求めるのは、再帰を使用するのに適した場所ではありません。スタックをオーバーフローするのは簡単です。この特定のインスタンスでは、などを使用することをお勧めしますa() { do { good = prompt(); } while (!good); }
ケビン

回答:


113

特定の質問に答えるには:いいえ、言語を学習する観点からすると、再帰は機能ではありません。教授が本当に教えていない「機能」を使用するためにあなたのマークを本当にドッキングした場合、それは間違っていました。

行間を読むと、1つの可能性として、再帰を使用することで、彼のコースの学習結果となるはずの機能の使用を回避できた可能性があります。たとえば、反復をまったく使用しなかった場合や、およびのfor両方forを使用する代わりにループのみを使用した場合などwhileです。課題は特定のことを実行する能力をテストすることを目的としていることが一般的であり、それらを実行しない場合、教授はその機能のために取っておいたマークを付与することはできません。ただし、それが本当に失点の原因だった場合、教授はこれを自分自身の学習体験としてとらえる必要があります。特定の学習成果を示すことが課題の基準の1つである場合は、生徒に明確に説明する必要があります。 。

そうは言っても、ここでの再帰よりも反復の方が良い選択であるという他のコメントや回答のほとんどには同意します。いくつかの理由があり、他の人々がある程度それらに触れたが、彼らが彼らの背後にある考えを完全に説明したとは思えない。

スタックオーバーフロー

より明白なのは、スタックオーバーフローエラーが発生するリスクがあることです。実際には、ユーザーが実際にスタックオーバーフローをトリガーするために何度も誤った入力を行わなければならないため、作成したメソッドが実際に1につながる可能性はほとんどありません。

ただし、注意すべき点の1つは、メソッド自体だけでなく、呼び出しチェーン内の上位または下位の他のメソッドがスタックに存在することです。このため、利用可能なスタックスペースをさりげなく取得することは、どの方法でもかなり失礼です。他のコードが不必要に多くのコードを使い果たしてしまうリスクがあるため、コードを作成するときはいつでも、空きスタック領域について常に心配する必要はありません。

これは、抽象化と呼ばれるソフトウェア設計のより一般的な原則の一部です。本質的に、を呼び出すときDoThing()、気にする必要があるのは、事が行われたことだけです。実装方法の詳細について心配する必要はありません。しかし、スタックの貪欲な使用はこの原則を破ります。コードのすべてのビットは、コールチェーンの他の場所のコードによって残されたと想定できるスタックの量を心配する必要があるためです。

読みやすさ

他の理由は読みやすさです。コードが目指すべき理想は、人間が読める形式のドキュメントになることです。この場合、各行はコードが何をしているかを簡単に説明します。次の2つの方法を使用します。

private int getInput() {
    int input;
    do {
        input = promptForInput();
    } while (!inputIsValid(input))
    return input;
}

private int getInput() {
    int input = promptForInput();
    if(inputIsValid(input)) {
        return input;
    }
    return getInput();
}

はい、どちらも機能し、どちらも非常に理解しやすいです。しかし、2つのアプローチはどのようにして英語で記述できるでしょうか。私はそれが次のようなものになると思います:

入力が有効になるまで入力を求め、それを返す

入力を要求し、入力が有効な場合はそれを返し、それ以外の場合は入力を取得してその結果を返します

おそらく、後者については少し不格好な表現を考えることもできますが、最初の表現は、実際に何をしようとしているのか、概念的にはより正確な説明になるといつも思うでしょう。これは、再帰が常に読みにくくなると言っているのではありません。ツリートラバーサルのように、それが輝く状況では、再帰と別のアプローチの間で同じ種類の並列分析を行うことができ、再帰が行ごとに、より明確に自己記述的なコードを与えることがほぼ確実にわかります。

単独では、これらはどちらも小さな点です。スタックオーバーフローが実際に発生する可能性はほとんどなく、可読性の向上はわずかです。しかし、どのプログラムもこれらの小さな決定の多くのコレクションになるので、孤立していてもそれほど重要ではない場合でも、正しい決定の背後にある原則を学ぶことが重要です。


8
再帰は機能ではないという主張を拡張できますか?すべてのコンパイラが必ずしもサポートしているわけではないため、そうだと私は答えました。
Harry Johnston

5
すべての言語が必ずしも再帰をサポートしているわけではないため、適切なコンパイラを選択することだけが問題であるとは限りません。2番目のポイントは、最初にマシンコードプログラミングの背景がなくても、プログラムを学ぶ人(現在は通常)の観点からは公平です。:-)
Harry Johnston、

2
「読みやすさ」の問題は構文の問題であることに注意してください。再帰について本質的に「判読不能」なものはありません。実際、帰納法は、ループやリスト、シーケンスなどの帰納的データ構造を表現する最も簡単な方法です。そして、ほとんどのデータ構造は帰納的です。
名目

6
言い回しでデッキを積み上げたと思います。反復バージョンを機能的に説明しました。逆も同様です。両方のより公平な行ごとの説明は、「入力を要求します。入力が無効な場合、有効な入力が得られるまでプロンプトを繰り返します。それから私はそれを返します。」vs「入力を要求します。入力が有効な場合は、それを返します。それ以外の場合は、やり直しの結果を返します。」(私の子供たちは就学前のやり直しの機能的な概念を理解していたので、ここでは再帰的な概念についての正当な英語の要約だと思います。)
pjs

2
@HarryJohnston再帰のサポートの欠如は、新しい機能の欠如というよりは、既存の機能の例外になります。特に、この質問のコンテキストでは、「新しい機能」は「まだ教えていない有用な動作が存在する」ことを意味します。これは、教えられた機能の論理的な拡張であるため、再帰には当てはまりません(つまり、手順には指示、およびプロシージャー呼び出しは指示です)。まるで教授が生徒の足し算を教えた後、「掛け算をカバーしていない」ため、同じ値を2回以上足すように叱ったようなものです。
nmclean 2014年

48

メタ質問ではなく文字通りの質問に答えるために:再帰、すべてのコンパイラーや言語が必ずしも許可するわけではないという意味での機能です。実際には、すべての(通常の)現代のコンパイラー、そして確かにすべてのJavaコンパイラーに期待されています!-しかし、それは普遍的に真実ではありません。

再帰がサポートされない理由の不自然な例として、静的な場所に関数の戻りアドレスを格納するコンパイラを検討してください。これは、たとえば、スタックを持たないマイクロプロセッサーのコンパイラーの場合に当てはまる可能性があります。

このようなコンパイラの場合、このような関数を呼び出すと

a();

それはとして実装されています

move the address of label 1 to variable return_from_a
jump to label function_a
label 1

そしてa()の定義、

function a()
{
   var1 = 5;
   return;
}

として実装されています

label function_a
move 5 to variable var1
jump to the address stored in variable return_from_a

うまくいけば、そのa()ようなコンパイラで再帰的に呼び出そうとするときの問題は明白です。戻りアドレスが上書きされたため、コンパイラーは外部呼び出しから戻る方法を認識しなくなりました。

私が実際に使用したコンパイラ(70年代後半または80年代前半)では、再帰はサポートされていません。問題はそれよりも少し微妙でした。戻りアドレスは、最新のコンパイラと同じようにスタックに格納されますが、ローカル変数はありませんでした。ない (理論的には、これは非静的ローカル変数のない関数で再帰が可能だったことを意味しますが、コンパイラーが明示的にそれをサポートしたかどうか覚えていません。何らかの理由で暗黙のローカル変数が必要だった可能性があります。)

今後の展望として、特殊なシナリオ(多並列システムなど)を想像できます。この場合、すべてのスレッドにスタックを提供する必要がないことが有利であり、再帰が許可されるのは、コンパイラーがそれをループにリファクタリングできる場合のみです。(もちろん、上で説明したプリミティブコンパイラは、コードのリファクタリングなどの複雑なタスクを実行できませんでした。)


たとえば、Cプリプロセッサはマクロの再帰をサポートしていません。マクロ定義は関数と同様に動作しますが、再帰的に呼び出すことはできません。
2014年

7
あなたの「考案された例」は、それほど考案されたものではありません。Fortran77標準では、関数が自分自身を再帰的に呼び出すことを許可していませんでした。(関数が実行されたときにジャンプするアドレスは、関数コード自体の末尾、またはこの配置に相当するものに格納されていたと思います。)これについては、こちらを参照してください。
アレクシス2014年

5
例として、シェーダー言語またはGPGPU言語(たとえば、GLSL、Cg、OpenCL C)は再帰をサポートしていません。これまでのところ、「すべての言語でサポートされているわけではない」という議論は確かに有効です。再帰は、スタックの等価物を(それは必ずしも必要ではない前提スタックが、店舗のリターンアドレスおよび機能フレームに手段が必要である何とか)。
デイモン

1970年代初頭に少し作業したFortranコンパイラには、コールスタックがありませんでした。各サブルーチンまたは関数には、戻りアドレス、パラメーター、および独自の変数用の静的メモリー領域がありました。
パトリシアシャナハン2014年

2
Turbo Pascalの一部のバージョンでも再帰はデフォルトで無効になっており、それを有効にするにはコンパイラディレクティブを設定する必要がありました。
dan04 2014年

17

先生はあなたが勉強したかどうか知りたがっています。どうやらあなたは彼があなたに教えた方法(良い方法 ;反復)で問題を解決しなかったので、あなたは解決しなかったと見なします。私はすべてクリエイティブソリューションですが、この場合、別の理由で先生に同意する必要があります。
ユーザーが無効な入力を何度も(つまり、Enterキーを押し続けることで)提供すると、スタックオーバーフロー例外が発生し、ソリューションがクラッシュします。さらに、反復ソリューションはより効率的で維持が容易です。それがあなたの先生があなたに与えるべきだった理由だと思います。


2
このタスクを特定の方法で実行するように指示されていません。反復だけでなく、方法についても学びました。また、個人の好みに合わせて読みやすい方を残しておきます。自分にとって良さそうなものを選びました。SOエラーは私にとって初めてのものですが、再帰はそれ自体が機能であるという考えはまだ確立されていないようです。

3
「どちらが読みやすいかは個人の好みに任せます」。同意した。再帰はJavaの機能ではありません。これらがあります。
マイク

2
@Vality:テールコールの除去?一部のJVMはそれを行う場合がありますが、例外のスタックトレースを維持する必要があることにも注意してください。テールコールの除去を許可すると、ナイーブに生成されたスタックトレースが無効になる可能性があるため、一部のJVMはその理由でTCEを実行しません。
icktoofay 2014年

5
どちらにしても、壊れたコードを壊れにくくするために最適化に依存することは、かなり悪い形です。
cHao 2014年

7
+1は、ユーザーが通Xboxに、連続して同じ起こっを入力しボタンを押したときのUbuntuで最近ログイン画面が壊れていたことがわかり
セバスチャン

13

「クラスで再帰をカバーしなかった」のでポイントを差し引くのはひどいです。関数Bを呼び出す関数Aを呼び出す方法を学び、関数Cを呼び出す関数Bに戻り、Bに戻り、Aに戻り、呼び出し元に戻ります。教師は、これらが異なる関数である必要があることを明示していません(これはたとえば、古いFORTRANバージョンの場合)、A、B、Cがすべて同じ関数である可能性がある理由はありません。

一方、実際のコードを見て、特定のケースで再帰を使用することが本当に正しいことかどうかを判断する必要があります。詳細はあまりありませんが、間違っているようです。


10

あなたが尋ねた特定の質問に関して見るべき多くの視点がありますが、私が言えることは、言語を学ぶという観点から、再帰はそれ自体の機能ではないということです。教授がまだ教えていない「機能」を使用するためにあなたのマークを本当にドッキングした場合、それは間違っていましたが、私が言ったように、ポイントを差し引くときに教授が実際に正しいとする他の見方があります。

私があなたの質問から推測できることから、再帰関数を使用して入力が失敗した場合に入力を求めることは、すべての再帰関数の呼び出しがスタックにプッシュされるため、良い方法ではありません。この再帰はユーザー入力によって駆動されるため、無限再帰関数を使用してStackOverflowを生成することが可能です。

質問であなたが言及した2つの例には、それらが何をするかという意味で違いはありません(ただし、他の点では異なります)。どちらの場合も、戻りアドレスとすべてのメソッド情報がスタックにロードされています。再帰の場合、戻りアドレスはメソッド呼び出しの直後の行です(もちろん、コード自体に表示されるのではなく、コンパイラが作成したコードに表示されます)。Java、C、Pythonでは、再帰は新しいスタックフレームの割り当てを必要とするため、反復(一般的に)に比べてかなり高価です。言うまでもなく、入力が何度も有効でない場合は、スタックオーバーフロー例外が発生する可能性があります。

再帰はそれ自体の主題であると考えられており、プログラミングの経験がない人が再帰を考える可能性は低いため、教授はポイントを差し引いたと思います。(もちろん、そうではないという意味ではありませんが、そうではありません)。

私見、教授はあなたにポイントを差し引いて正しいと思います。検証部分を別のメソッドに簡単に変換して、次のように使用できます。

public bool foo() 
{
  validInput = GetInput();
  while(!validInput)
  {
    MessageBox.Show("Wrong Input, please try again!");
    validInput = GetInput();
  }
  return hasWon(x, y, piece);
}

あなたがしたことが実際にその方法で解決できるなら、あなたがしたことは悪い習慣であり、避けるべきです。


メソッド自体の目的は、入力を検証し、別のメソッドの結果を呼び出して返すことです(そのため、メソッド自体が返されます)。具体的には、Tic-Tac-Toeゲームの動きが有効hasWon(x, y, piece)かどうかを確認してから戻ります(影響を受ける行と列のみを確認します)。

検証部分のみを簡単に取り、たとえば "GetInput"と呼ばれる別のメソッドに入れて、回答に書いたように使用することができます。私はそれがどのように見えるべきかで私の答えを編集しました。もちろん、GetInputが必要な情報を保持する型を返すようにすることもできます。
Yonatan Nir ​​2014年

1
ヨナタン・ニル:再帰が悪い習慣だったのはいつですか?バイトコードのセキュリティのためにHotspot VMが最適化できなかったために、JVMが爆発するかもしれません。あなたのコードは、異なるアプローチを使用していること以外はどのように違いますか?

1
再帰は常に悪い習慣であるとは限りませんが、それが回避でき、コードをクリーンで維持しやすい状態に保つことができる場合は、回避する必要があります。Java、C、Pythonでは、再帰は新しいスタックフレームの割り当てを必要とするため、反復(一般的に)に比べてかなり高価です。一部のCコンパイラでは、コンパイラフラグを使用してこのオーバーヘッドを排除できます。これにより、特定の種類の再帰(実際には特定の種類の末尾呼び出し)が関数呼び出しではなくジャンプに変換されます。
Yonatan Nir ​​2014年

1
明確ではありませんが、ループを無限の反復回数で再帰に置き換えた場合、それは悪いことです。Javaは末尾呼び出しの最適化を保証しないため、スタック領域が簡単に不足する可能性があります。Javaでは、反復回数に制限があることが保証されていない限り(通常、データの合計サイズと比較した対数)、再帰を使用しないでください。
ハイド2014年

6

教授はまだ教えていないかもしれませんが、再帰の利点と欠点を学ぶ準備ができているようです。

再帰の主な利点は、多くの場合、再帰的アルゴリズムの方がはるかに簡単かつ迅速に記述できることです。

再帰の各レベルでは、スタックに追加のスタックフレームを追加する必要があるため、再帰の主な欠点は、再帰アルゴリズムがスタックオーバーフローを引き起こす可能性があることです。

プロダクションコードでは、スケーリングによってプログラマーの単体テストよりもはるかに多くのレベルの再帰が発生する可能性がありますが、通常はデメリットよりもデメリットが優先され、再帰的なコードは多くの場合、実用的に回避されます。


1
潜在的に危険な再帰アルゴリズムは常に、明示的なスタックを使用するように簡単に書き直すことができます。呼び出しスタックは、結局のところ、単なるスタックです。この場合、スタックを使用するようにソリューションを書き直した場合、それはばかげているように見えます-再帰的な答えがあまり良いものではないというさらなる証拠です。
アーロンノート2014年

1
スタックオーバーフローが問題になる場合は、.NET 4.0やその他の関数型プログラミング言語など、末尾呼び出しの最適化をサポートする言語/ランタイムを使用する必要があります
Sebastian

すべての再帰が末尾呼び出しであるとは限りません。
Warren Dew

6

特定の質問に関して、再帰は機能です、私はイエスと言う傾向がありますが、質問を再解釈した後。再帰を可能にする言語とコンパイラの一般的な設計上の選択肢があり、再帰をまったく許可しないチューリング完全言語が存在します。言い換えると、再帰は、言語/コンパイラの設計における特定の選択によって可能になる機能です。

  • ファーストクラスの関数をサポートすることで、最小限の前提の下で再帰が可能になります。例については、Unlambdaでのループの記述を参照してください。または、自己参照、ループ、または割り当てを含まない、この鈍いPython式:

    >>> map((lambda x: lambda f: x(lambda g: f(lambda v: g(g)(v))))(
    ...   lambda c: c(c))(lambda R: lambda n: 1 if n < 2 else n * R(n - 1)),
    ...   xrange(10))
    [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
    
  • 遅延バインディングを使用する言語またはコンパイラ、または前方宣言を定義する言語/コンパイラは、再帰を可能にします。たとえば、Pythonでは以下のコードを使用できますが、これは設計上の選択(遅延バインディング)であり、チューリング完全システムの要件ではありません。相互に再帰的な関数は、多くの場合、前方宣言のサポートに依存しています。

    factorial = lambda n: 1 if n < 2 else n * factorial(n-1)
    
  • 再帰的に定義された型を許可する静的型付け言語は、再帰の有効化に貢献します。GoのY Combinatorのこの実装を参照してください。再帰的に定義された型がなくても、Goで再帰を使用することは可能ですが、Yコンビネーターは特に不可能だと思います。


1
これは私の頭、特にUnlambda +1を爆発させました
John Powell

固定小数点コンビネーターは難しいです。関数型プログラミングを学ぶことにしたとき、私はYコンビネーターを理解するまで勉強し、それを他の便利な関数を書くために適用することを余儀なくさせました。しばらくかかりましたが、それだけの価値はありました。
wberry 2014年

5

私があなたの質問から推測できることから、入力が失敗した場合に再帰関数を使用して入力を求めることはお勧めできません。どうして?

すべての再帰関数呼び出しがスタックにプッシュされるからです。この再帰はユーザー入力によって駆動されるため、無限の再帰関数を持つことが可能で、結果としてStackOverflowが生成されます:-p

これを行うための非再帰的ループを持つことは、進むべき道です。


問題のメソッドの大部分、およびメソッド自体の目的は、さまざまなチェックを通じて入力を検証することです。入力が(指示どおりに)正しくなるまで入力が無効な場合、プロセスは最初からやり直しになります。

4
@fayしかし、入力が何度も無効である場合、StackOverflowErrorが発生します。再帰はよりエレガントですが、私の目には、通常、(スタックのため)通常のループよりも問題が多くなります。
Michael Yaworski、2014年

1
それが興味深い点です。私はそのエラーを考慮していませんでした。しかし、while(true)同じメソッドを呼び出すことで同じ効果を得ることができますか?もしそうなら、これが再帰の違いをサポートしているとは言いません。

1
@fay while(true)は無限ループです。あなたがbreak声明を書かない限り、あなたがプログラムをクラッシュさせようとしているのでなければ、私はそれの要点を見ません。私の要点は、同じメソッド(再帰)を呼び出すと、StackOverflowErrorが発生することがありますが、whileor forループを使用する場合はそうなりません。通常のループでは問題は存在しません。多分私はあなたを誤解しましたが、あなたへの私の答えはノーです。
Michael Yaworski、2014年

4
これは正直に言って、おそらく教授が採点を下した本当の理由のようです=)彼はそれを十分に説明しなかったかもしれませんが、そうでない場合は非常に貧弱なスタイルと見なされる方法で使用していたと言うのは正当な不満ですより深刻なコードに完全に欠陥があります。
司令官コリアンダーサラマンダー2014

3

再帰は、プログラミングの概念機能(反復など)、および実践です。リンクからわかるように、このテーマに特化した研究の大きな領域があります。おそらく、これらのポイントを理解するためにトピックの深いところに行く必要はありません。

機能としての再帰

簡単に言えば、Javaは暗黙的にそれをサポートします。これは、メソッド(基本的には特別な関数)がそれ自体と、それが属するクラスを構成する他のメソッドの「知識」を持つことができるためです。これが当てはまらない言語について考えてみましょう。そのメソッドの本体を記述するaことはできますが、その中に呼び出しを含めることはできませんa。唯一の解決策は、同じ結果を得るために反復を使用することです。そのような言語では、自分の存在を(特定の構文トークンを使用して)認識している関数と認識していない関数を区別する必要があります。実際には、言語のグループ全体がその区別を行い(LispおよびML参照たとえばファミリを)。興味深いことに、Perlは匿名関数(いわゆるlambdas)自分自身を再帰的に呼び出す(ここでも、専用の構文を使用)。

再帰なし?

再帰の可能性さえサポートしていない言語の場合、固定小数点コンビネーターの形で別の解決策がしばしばありますが、それでも、いわゆるファーストクラスオブジェクト(つまり、言語自体の中で操作される)。

慣習としての再帰

その機能を言語で使用できるということは、それが慣用的であることを必ずしも意味しません。Java 8ではラムダ式が含まれているため、プログラミングに関数型アプローチを採用する方が簡単になる場合があります。ただし、実際的な考慮事項があります。

  • 構文はまだ再帰的にフレンドリーではありません
  • コンパイラはその慣行を検出して最適化できない場合があります

肝心なこと

幸運なことに(またはより正確には、使いやすくするために)、Javaはデフォルトでメソッドに自分自身を認識させ、再帰をサポートするため、これは実際には問題ではありませんが、理論的な問題であり、私はあなたの教師は具体的にそれに取り組みたかったのです。さらに、最近の言語の進化に照らして、将来的に重要なものになるかもしれません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.